Skip to Content

Atualização Parcial do Ecrã e-Paper

Atualização Parcial do Ecrã e-Paper

Neste tutorial, vais aprender a realizar uma atualização parcial rápida de um ecrã e-Paper. As principais vantagens dos ecrãs e-Paper são o seu baixo consumo de energia e excelente legibilidade. Para além da gama de cores limitada, a sua maior desvantagem é a taxa de atualização lenta.

Normalmente, um ecrã e-Paper a preto e branco demora entre 2 a 4 segundos para atualizar o conteúdo de todo o ecrã, e para ecrãs a cores é muito mais tempo (>20 segundos). A atualização completa também provoca muito cintilamento, o que é irritante.

Felizmente, podes realizar uma atualização parcial, que é não só muito mais rápida (0,3 segundos) como também evita o cintilamento do ecrã. Para qualquer aplicação prática que atualize o conteúdo do ecrã mais do que uma vez a cada 10 minutos, deves definitivamente usar uma atualização parcial.

No entanto, implementar uma atualização parcial num ecrã e-Paper pode ser bastante complicado. Este tutorial mostra-te como atualizar uma ou várias áreas do ecrã, como combinar a atualização parcial com deep sleep e como atualizar parcialmente pixels individuais para plotagem.

Vamos começar com as peças necessárias.

Peças Necessárias

Para as peças necessárias, listei um ESP32 lite mais antigo, que já foi descontinuado mas ainda podes encontrá-lo barato. Existe um modelo sucessor (Amazon) com especificações melhoradas. Mas qualquer outro ESP32, ESP8266 ou Arduino com memória suficiente também funcionará.

Ecrã e-Paper de 2,9″

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cabo USB de Dados

Dupont wire set

Conjunto de Fios Dupont

Half_breadboard56a

Breadboard

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Ecrã e-Paper

Para este projeto, estou a usar um módulo de ecrã e-Paper de 2,9″, com resolução de 296×128 pixels e um controlador embutido com interface SPI.

Front and Back of 2.9" e-Paper display module
Frente e verso do módulo de ecrã e-Paper de 2,9″

Note que a maioria dos módulos de ecrã e-Paper com SPI tem um pequeno interruptor ou jumper que permite alternar entre SPI de 4 fios e SPI de 3 fios. Vamos usar o SPI de 4 fios por defeito aqui.

Back of display module with SPI-interface and controller
Verso do módulo de ecrã com interface SPI e controlador

O módulo de ecrã funciona a 3,3V ou 5V, tem uma corrente de sono muito baixa de 0,01µA e consome apenas cerca de 26,4mW durante a atualização do conteúdo. Isso significa que podes usar um ecrã e-Paper durante muito tempo mesmo com uma bateria pequena.

Para mais informações sobre ecrãs e-Paper, consulta o nosso tutorial Interfacing Arduino To An E-ink Display.

Ligação e Teste do e-Paper

Primeiro, vamos ligar e testar o funcionamento do e-Paper. A imagem seguinte mostra toda a ligação para alimentação e SPI.

Connecting e-Paper to ESP32 via SPI
Ligação do e-Paper ao ESP32 via SPI

E aqui está a tabela com todas as ligações para conveniência. Note que podes alimentar o ecrã com 3,3V ou 5V, mas o ESP32-lite tem apenas uma saída de 3,3V e as linhas de dados SPI devem ser de 3,3V!

Ecrã e-PaperESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

Instalar a biblioteca GxEPD2

Antes de podermos desenhar ou escrever no ecrã e-Paper, precisamos de instalar duas bibliotecas. A Adafruit_GFX é uma biblioteca gráfica base que fornece um conjunto comum de primitivas gráficas (texto, pontos, linhas, círculos, etc.). E a GxEPD2 fornece o software do driver gráfico para controlar um ecrã e-Paper via SPI.

Basta instalar as bibliotecas da forma habitual. Após a instalação, elas devem aparecer no Library Manager da seguinte forma.

Adafruit_GFX and GxEPD2 libraries in Library Manager
Bibliotecas Adafruit_GFX e GxEPD2 no Library Manager

Código de teste

Aqui está um código simples de teste que mostra o texto “Makerguides” no ecrã. Vê primeiro o código completo e depois discutimos.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(20, 20);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

void loop() {}

Vamos decompor o código em componentes para entender como funciona.

Constantes e Bibliotecas

O código começa por definir uma constante ENABLE_GxEPD2_GFX como 0. Podes defini-la como 1, o que segundo a documentação permite à classe base GxEPD2_GFX passar referências ou ponteiros para a instância do ecrã como parâmetro. Mas isso usa cerca de 1,2k mais código e não precisamos, por isso está definida como 0.

#define ENABLE_GxEPD2_GFX 0

De seguida, incluímos o ficheiro de cabeçalho GxEPD2_BW.h para o ecrã e-Paper a preto e branco (BW). Se tiveres um ecrã a 3 cores, incluirias GxEPD2_3C.h, ou GxEPD2_4C.h para um ecrã a 4 cores, e GxEPD2_7C.h para um ecrã a 7 cores, em vez disso.

#include "GxEPD2_BW.h"

Objeto do Ecrã

A linha seguinte é importante. Cria o objeto do ecrã e depende do tipo ou marca do ecrã. Testei um ecrã WeAct e um WaveShare e a linha seguinte funciona para ambos.

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

A biblioteca Readme for GxEPD2 lista todos os ecrãs suportados e podes encontrar os detalhes nos ficheiros de cabeçalho, por exemplo GxEPD2.h.

Função setup

Todo o trabalho real acontece na função setup. Primeiro, configuramos os parâmetros do ecrã, como velocidade de comunicação, rotação, fonte, cor do texto e cor de fundo do ecrã. Se tiveres problemas com a atualização do ecrã, podes ter de ajustar os parâmetros da função init().

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(20, 20);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

Depois, preenchemos o ecrã com branco, posicionamos o cursor, escrevemos o texto e finalmente colocamos o driver do ecrã em modo hibernação. Isso desliga o ecrã e coloca o controlador do ecrã em modo deep-sleep (não o ESP32, apenas o ecrã!).

Função loop

A função loop() está vazia neste exemplo, pois o conteúdo do ecrã é criado na função setup() e não precisa de ser atualizado continuamente.

void loop() {}

Carregar e Executar Código

Agora, estamos prontos para carregar e executar o código. Seleciona a placa que tens no board manager. No meu caso, é a WEMOS LOLIN32 Lite que foi listada nas Peças Necessárias:

WEMOS LOLIN32 Lite selected in Board Manager
WEMOS LOLIN32 Lite selecionada no Board Manager

Depois, pressiona upload e após algum cintilamento, o teu ecrã deverá mostrar o seguinte texto:

Test output on e-Paper display
Saída de teste no ecrã e-Paper

Atualização Completa

Só para demonstrar como é mau o aspeto de uma atualização completa num ecrã e-Paper, vamos usar o seguinte código. Ele imprime o texto “msec: ” e ao lado o número de milissegundos desde o início do microcontrolador.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
}

void loop() {
  epd.fillScreen(GxEPD_WHITE);
  epd.setCursor(40, 60);
  epd.print("msec: ");
  epd.setCursor(150, 60);
  epd.print(millis());
  epd.display();
  epd.hibernate();
}

O conteúdo do ecrã é atualizado e refrescado no loop principal tão rápido quanto possível. O vídeo curto seguinte mostra como isso se apresenta. Como podes ver, há muito cintilamento e a atualização do ecrã demora quase 3 segundos.

Full refresh of e-Paper display
Atualização completa do ecrã e-Paper

Portanto, para qualquer aplicação com atualizações frequentes, uma atualização completa demora demasiado para ser útil.

Atualização Parcial de Área Única

Em vez de realizar uma atualização completa do ecrã toda vez que queremos atualizar o valor dos milissegundos, podemos fazer uma atualização parcial apenas para essa área. O código seguinte realiza uma atualização completa uma vez, no início, e depois só atualiza a secção do ecrã que mostra o valor dos milissegundos:

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
  epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

O vídeo seguinte mostra como é a atualização parcial. Como podes ver, o cintilamento desapareceu e a atualização dos milissegundos é muito mais rápida (< 1 seg).

Partial refresh of e-Paper display
Atualização parcial do ecrã e-Paper

Há uma atualização completa quando o código começa, com o habitual cintilamento e tempo lento, mas isso acontece apenas uma vez.

Desenho paginado

Existem diferentes formas de implementar a atualização completa ou parcial. Usar a função drawPaged() é provavelmente a forma mais elegante e fácil. Ela recebe uma função drawCallback e um vetor de parâmetros pv para realizar um redesenho paginado:

void drawPaged(void (*drawCallback)(const void*), const void* pv)

Dentro da função drawCallback, chamamos setFullWindow() para realizar uma atualização completa ou setPartialWindow(x, y, w, h) para uma atualização parcial.

Usar drawPaged() também tem a vantagem de gerir as operações de desenho de forma eficiente em memória, o que é especialmente útil para ecrãs maiores e quando a RAM é limitada. Ele desenha o conteúdo em múltiplos passos pequenos (páginas) em vez de atualizar todo o ecrã de uma vez.

Função drawFull

Se olhares para a função drawFull(), podes ver que estamos a chamar setFullWindow(), a definir a posição do cursor e depois a imprimir o texto estático "msec: ".

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

Na função setup, iniciamos o ecrã, definimos as propriedades do ecrã e depois chamamos drawPaged(drawFull, 0) para realizar a atualização completa do ecrã.

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
  epd.drawPaged(drawFull, 0);
}

O vetor de parâmetros pv está definido como 0, pois não estamos a passar parâmetros. Mas poderias fornecer fonte, cor, texto ou outra informação relevante para a função drawFull aqui.

Função drawPartial

A principal diferença da função drawPartial() em comparação com a drawFull() é que setPartialWindow(x, y, w, h) é chamada para restringir a área do ecrã a atualizar.

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

Depois disso, posicionamos o cursor, imprimimos o texto dos milissegundos e desenhamos um retângulo para marcar a janela de atualização.

Finalmente, a função drawPartial() é chamada via drawPaged(drawPartial, 0) no loop principal, o que provoca uma atualização repetida dos milissegundos mostrados.

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

Note que qualquer coisa que desenhares fora da área da janela parcial não será exibida. Em drawPartial() definimos o cursor dentro da área e imprimimos o valor dos milissegundos dentro dela.

Para mostrar a área da atualização parcial, desenhamos um retângulo à volta. No entanto, isso não é totalmente preciso! Mais sobre isso na próxima secção.

Alinhamento da Janela de Atualização

Devido a limitações de endereçamento dos controladores de ecrã e-Paper, as coordenadas da janela de atualização precisam estar alinhadas a múltiplos de 8. Especificamente,

  • x e w devem ser múltiplos de 8, para rotação do ecrã 0 ou 2,
  • y e h devem ser múltiplos de 8, para rotação do ecrã 1 ou 3,

A função setPartialWindow(x, y, w, h) permite fornecer valores arbitrários, mas internamente alinha-os conforme necessário. Isso significa que a janela de atualização real pode ser maior do que a especificada.

Isto é importante porque significa que partes do ecrã serão atualizadas que talvez não tenhas pretendido. A imagem abaixo mostra a área de atualização pretendida (retângulo preto) e a área de atualização real (retângulo branco preenchido).

Intended and actual refresh window
Área de atualização pretendida e real

Como podes ver, a altura da janela de atualização real é maior do que a da janela especificada. Se estiveres a desenhar o layout do teu conteúdo, tens de ter isso em conta. Especialmente porque a função drawRect() preenche completamente o fundo da janela de atualização real com branco. Isso significa que vai apagar conteúdo que está fora da janela especificada mas dentro da janela real.

Se quiseres replicar o experimento acima, aqui está o código. Ele apenas inverte a cor de fundo e do texto do redesenho completo para tornar visível a janela de atualização real.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_BLACK);
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setTextColor(GxEPD_BLACK);
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_WHITE);
  epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

Atualização Parcial com Deep Sleep

Os ecrãs e-Paper são frequentemente usados em projetos alimentados por bateria, onde o microcontrolador normalmente entra em deep-sleep. Como combinar o deep-sleep do ESP32 com a atualização parcial de um ecrã e-Paper é o tema desta secção.

A principal dificuldade é que queremos realizar uma atualização completa apenas na primeira inicialização (reset) do ESP32, mas sempre que acordamos do deep-sleep queremos fazer apenas uma atualização parcial. O código seguinte consegue isso. Dá uma vista rápida e depois entramos nos detalhes.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

RTC_DATA_ATTR bool initial = true;

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
  initial = false;
}

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.setCursor(x + 100, y + 10);
  epd.print(random(10));  
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

void setup() {
  epd.init(115200, initial, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
  if (initial)
    epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();  
  esp_sleep_enable_timer_wakeup(1000);
  esp_deep_sleep_start();
}

O código é fundamentalmente o mesmo do exemplo anterior, com algumas mudanças importantes.

Primeiro, definimos uma variável initial que é armazenada em RTC memory, o que significa que o seu valor é preservado durante o deep-sleep.

RTC_DATA_ATTR bool initial = true;

A variável initial é definida como true e depois da primeira atualização completa via drawFull() é definida como false. Isso significa que initial=true após um reset, mas false após deep-sleep.

De seguida, precisamos informar o ecrã para não realizar uma atualização completa após o deep-sleep. A função init(serial_diag_bitrate, initial, reset_duration pulldown_rst_mode) tem o parâmetro initial para isso:

epd.init(115200, initial, 50, false); 

Estamos quase a terminar. Na função setup, só realizamos um redesenho completo se initial==true. Ou seja, apenas após um reset, mas não após acordar do deep-sleep.

void setup() {
  ...
  if (initial)
    epd.drawPaged(drawFull, 0);
}

Finalmente, na função loop, colocamos o ESP32 a dormir, depois de ter realizado uma atualização parcial.

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();  
  esp_sleep_enable_timer_wakeup(1000);
  esp_deep_sleep_start();
}

Segundo a documentação da função init(), só tens de garantir que a alimentação do ecrã é mantida durante o deep-sleep. Chamar hibernate, como mostrado acima, está correto. O vídeo seguinte mostra como isso funciona.

Partial refresh of e-Paper display with deep-sleep
Atualização parcial do ecrã e-Paper com deep-sleep

Como o millis() reinicia durante o deep-sleep e mostra um valor constante de 151 milissegundos, também mostro um número aleatório (0..9) após o valor dos msec para tornar a atualização visível.

Atualização Parcial de Múltiplas Áreas

Por vezes queremos atualizar parcialmente várias áreas em locais e tempos diferentes. Por exemplo, podes querer atualizar o ecrã de um relógio a cada segundo, mas o ecrã de uma medição de temperatura apenas a cada 5 minutos.

Estender o exemplo de código acima para atualizar parcialmente múltiplas áreas é fácil. Vê o código completo seguinte que atualiza o valor dos milissegundos em duas áreas diferentes.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void drawText(int16_t x, int16_t y, const char *text) {
  int16_t x1, y1;
  uint16_t w, h;
  epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
  epd.setPartialWindow(x1, y1, w, h);
  epd.setCursor(x, y);
  epd.print(text);   
  epd.drawRect(x1, y1, w, h, GxEPD_BLACK); 
}

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

void drawPartial1(const void* pv) {
  char text[16];
  sprintf(text, "1: %8d", millis());
  drawText(120, 60, text);
}

void drawPartial2(const void* pv) {
  char text[16];
  sprintf(text, "2: %8d", millis());
  drawText(120, 40, text);
}

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);
  epd.setTextSize(2);
  epd.drawPaged(drawFull, 0);
}

void loop() {  
  epd.drawPaged(drawPartial1, 0);
  epd.drawPaged(drawPartial2, 0);
  epd.hibernate();
}

Como podes ver, simplesmente definimos duas funções drawPartial1() e drawPartial2() que são chamadas na função loop para realizar a atualização parcial de duas áreas diferentes do ecrã.

void loop() {  
  epd.drawPaged(drawPartial1, 0);
  epd.drawPaged(drawPartial2, 0);
  epd.hibernate();
}

Para tornar as coisas um pouco mais interessantes e convenientes, adicionei uma função drawText() que calcula a caixa delimitadora para um dado texto, define a janela de atualização com as mesmas dimensões, imprime o texto e desenha a caixa delimitadora:

void drawText(int16_t x, int16_t y, const char *text) {
  int16_t x1, y1;
  uint16_t w, h;
  epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
  epd.setPartialWindow(x1, y1, w, h);
  epd.setCursor(x, y);
  epd.print(text);   
  epd.drawRect(x1, y1, w, h, GxEPD_BLACK); 
}

Se usares este código, certifica-te de que o texto tem sempre o mesmo comprimento. Um texto que encolhe entre atualizações pode deixar artefactos no ecrã, pois a caixa delimitadora e a área de atualização ficam menores. No código acima, uso o especificador de formato "%8d" para garantir que o texto tem comprimento constante apesar dos números mudarem. O vídeo abaixo mostra o código em ação:

Partial refresh of multiple e-Paper display areas
Atualização parcial de múltiplas áreas do ecrã e-Paper

Note que a atualização demora agora o dobro do tempo, pois estamos a realizar duas atualizações independentes. Não podes usar duas janelas de atualização diferentes (setPartialWindow) na mesma função de desenho. Mas poderias especificar uma janela maior que englobe ambos os conteúdos que queres atualizar. Nesse caso, também não terias de te preocupar tanto com diferentes comprimentos de texto.

Atualização Parcial usando Buffer de Ecrã

Como último exemplo, quis implementar um simples plotter de dados, por exemplo, para monitorizar a temperatura ao longo do tempo. A imagem seguinte mostra como o plotter vai parecer. Tem um título fixo “Temperature”, um eixo x fixo e valores de temperatura simulados que mudam dinamicamente.

Temperature plotter on e-Paper display
Plotter de temperatura no ecrã e-Paper

Idealmente, simplesmente plotaríamos pixels individuais (pontos de dados) com uma janela de atualização parcial de 1 pixel. No entanto, isso não funciona, pois as dimensões da janela de atualização são limitadas a múltiplos de 8 (ver acima). Se tentares, vais ver que a janela maior apaga alguns dos pixels previamente plotados e partes do eixo.

Para contornar este problema, desenhamos primeiro numa chamada “canvas”. Uma canvas é essencialmente um buffer de ecrã que podemos atualizar em segundo plano. A atualização parcial depois usa uma pequena área da canvas para atualizar o ecrã.

Partial refresh using canvas
Atualização parcial usando canvas

O código seguinte contém a implementação completa deste pequeno plotter de dados de temperatura.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

// W, H flipped due to setRotation(1)
const int W = GxEPD2_290_BS::HEIGHT;
const int H = GxEPD2_290_BS::WIDTH;

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));
GFXcanvas1 canvas(W, H);

void initCanvas() {
  canvas.setTextColor(BLACK);
  canvas.fillScreen(WHITE);
  
  canvas.setTextSize(2);
  canvas.setCursor(10, 10);
  canvas.print("Temperature");

  for (int i = 0; i < 10; i++) {
    int x = 10 + i * W / 10, y = H / 2;
    canvas.setTextSize(1);
    canvas.setCursor(x - 2, y + 10);
    canvas.print(i);
    canvas.drawLine(x, y - 5, x, y + 5, BLACK);
  }
  canvas.drawLine(10, H / 2, W - 10, H / 2, BLACK);
}

void drawCanvas() {
  epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}

void drawFull(const void* pv) {
  epd.setFullWindow();
  drawCanvas();
}

void drawPartial(const void* pv) {
  static int16_t x = 10;
  x += 1;
  if (x > W - 10) x = 10;
  int16_t y = (H / 2) + (x / 10.0) * sin(x / 10.0);
  canvas.writePixel(x, y, BLACK);
  epd.setPartialWindow(x, y, 1, 1);
  drawCanvas();
}

void setup() {
  initCanvas();
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

Primeiro, definimos constantes auxiliares para as dimensões do ecrã e as cores de primeiro plano e fundo. Note que largura e altura estão invertidas, pois rodamos o ecrã (setRotation(1)) na função setup.

// W, H flipped due to setRotation(1)
const int W = GxEPD2_290_BS::HEIGHT;
const int H = GxEPD2_290_BS::WIDTH;

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;

Objeto canvas

A parte seguinte importante é onde criamos o objeto canvas com as mesmas dimensões do ecrã. Como o meu ecrã e-Paper tem apenas duas cores (preto, branco), criamos uma canvas com profundidade de 1 bit (GFXcanvas1).

GFXcanvas1 canvas(W, H);

Se o teu ecrã tiver mais cores ou níveis de cinzento, precisas criar uma canvas que corresponda a isso. Consulta a Adafruit GFX Graphics Library documentation para detalhes.

initCanvas e drawCanvas

A função initCanvas() simplesmente escreve o título e desenha o eixo x na canvas, mas não mostra nada. A exibição real dos gráficos é feita pela função drawCanvas(), que copia a canvas como bitmap para o ecrã.

drawFull e drawPartial

A função drawFull() realiza uma atualização completa, enquanto a função drawPartial() realiza uma atualização parcial. Durante a atualização completa desenhamos o título e o eixo. Durante a atualização parcial desenhamos os pontos de dados individuais.

A fórmula seguinte cria os dados simulados de temperatura y, e x é o tempo simulado.

y = (H / 2) + (x / 10.0) * sin(x / 10.0);

A parte importante é onde escrevemos o ponto de dados (x,y) como um pixel na canvas, depois definimos uma janela de atualização apenas à volta desse pixel e desenhamos a canvas:

void drawPartial(const void* pv) {
  ...
  canvas.writePixel(x, y, BLACK);
  epd.setPartialWindow(x, y, 1, 1);
  drawCanvas();
}

Como a janela de atualização parcial está definida para (x,y,1,1), não atualizamos todo o ecrã, mas apenas uma pequena área à volta do pixel (lembra-te da limitação do múltiplo de 8). No entanto, como esta área é retirada da canvas, contém o que já foi desenhado e não apaga o conteúdo existente no ecrã. O vídeo seguinte mostra o plotter em ação:

Data plotter on e-Paper display
Plotter de dados no ecrã e-Paper

Podes ver que a curva cruza o eixo x sem apagá-lo e não há cintilamento. Embora a atualização não seja muito rápida (cerca de 1 seg), é suficientemente rápida para um plotter de temperatura.

Note que o deep-sleep não pode ser facilmente adicionado a este código. Teríamos de armazenar toda a canvas na memória RTC, que normalmente não é grande o suficiente (8K) para isso. Poderias armazenar apenas os pontos de dados, mas então a função de redesenho parcial seria um pouco mais complexa, pois terias de redesenhar toda a curva. Alternativamente, há também a opção de armazenar a canvas num cartão SD ou outra memória externa maior.

Recomendações

A Waveshare Manual fornece as seguintes recomendações ao operar um ecrã e-Paper (resumidas e reformuladas por mim):

Atualização completa: O ecrã e-Paper vai cintilar várias vezes durante o processo de atualização (o número de cintilações depende do tempo de atualização), e o cintilar serve para remover imagens residuais e alcançar o melhor efeito de visualização.

Atualização parcial: Neste caso, o ecrã não apresenta cintilamento durante o processo de atualização. Após várias operações de atualização parcial, deve ser realizada uma atualização completa para remover a imagem residual. Caso contrário, a imagem residual pode tornar-se permanente.

Recomenda-se definir o intervalo de atualização do ecrã e-ink para pelo menos 180 segundos (exceto para produtos que suportem atualização parcial).

Após uma operação de atualização, recomenda-se desligar ou hibernar o ecrã. Isso prolonga a vida útil do ecrã e reduz o consumo de energia.

Ao usar um ecrã e-ink a três cores, recomenda-se atualizar o ecrã pelo menos uma vez a cada 24 horas.

Os ecrãs e-Paper são recomendados para uso em interiores e não para uso exterior. Se quiseres usar o ecrã no exterior, coloca-o numa área sombreada e cobre completamente a parte da cola branca da fita de conexão do ecrã e-Paper com fita 3M.

Conclusões

Neste tutorial aprendeste como realizar atualizações parciais num ecrã e-Paper para evitar cintilamento e conseguir tempos de atualização mais rápidos. Especificamente, vimos como fazer atualização parcial para áreas únicas e múltiplas, em combinação com deep-sleep e atualizações pixel a pixel.

Poderias facilmente expandir o pequeno exemplo do plotter adicionando um sensor de temperatura real, como o sensor BME280, ao projeto. Consulta o tutorial Weather Station on e-Paper Display para mais detalhes. Note que poderias usar canvases separadas para temperatura, humidade e pressão do ar, e depois alternar entre elas para mostrar diferentes dados.

Se tiveres alguma dúvida, não hesites em perguntar na secção de comentários.

Boas experiências ; )