Skip to Content

Actualización parcial de pantalla e-Paper

Actualización parcial de pantalla e-Paper

En este tutorial aprenderás cómo realizar una actualización parcial rápida de una pantalla e-Paper. Las principales ventajas de las pantallas e-Paper son su bajo consumo de energía y su excelente legibilidad. Aparte del rango limitado de colores, su mayor desventaja es la lenta tasa de refresco.

Normalmente, una pantalla e-Paper en blanco y negro tarda entre 2 y 4 segundos en actualizar todo el contenido, y para las pantallas a color es mucho más tiempo (>20 segundos). La actualización completa también produce mucho parpadeo, lo cual resulta molesto.

Por suerte, puedes realizar una actualización parcial, que no solo es mucho más rápida (0,3 segundos) sino que también evita el parpadeo de la pantalla. Para cualquier aplicación práctica que actualice el contenido más de una vez cada 10 minutos aproximadamente, definitivamente querrás usar una actualización parcial.

Sin embargo, implementar una actualización parcial en una pantalla e-Paper puede ser bastante complicado. Este tutorial te muestra cómo refrescar una o varias áreas de la pantalla, cómo combinar la actualización parcial con el modo deep sleep y cómo actualizar parcialmente píxeles individuales para gráficos.

Comencemos con las piezas necesarias.

Piezas necesarias

Para las piezas necesarias, he listado un ESP32 lite antiguo, que ya está descontinuado pero aún puedes conseguirlo barato. Hay un modelo sucesor (Amazon) con especificaciones mejoradas. Pero cualquier otro ESP32, ESP8266 o Arduino con suficiente memoria también funcionará.

Pantalla e-Paper de 2.9″

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cable de datos USB

Dupont wire set

Juego de cables Dupont

Half_breadboard56a

Protoboard

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.

Pantalla e-Paper

Para este proyecto, estoy usando un módulo de pantalla e-Paper de 2.9″, con resolución de 296×128 píxeles y un controlador integrado con interfaz SPI.

Front and Back of 2.9" e-Paper display module
Frontal y trasera del módulo de pantalla e-Paper de 2.9″

Ten en cuenta que la mayoría de los módulos de pantalla e-Paper con SPI tienen un pequeño interruptor o jumper que permite cambiar de SPI de 4 hilos a SPI de 3 hilos. Aquí usaremos el SPI de 4 hilos por defecto.

Back of display module with SPI-interface and controller
Parte trasera del módulo con interfaz SPI y controlador

El módulo funciona a 3.3V o 5V, tiene una corriente de reposo muy baja de 0.01µA y consume solo unos 26.4mW mientras actualiza su contenido. Esto significa que puedes usar una pantalla e-Paper durante mucho tiempo incluso con una batería pequeña.

Para más información sobre pantallas e-Paper, echa un vistazo a nuestro tutorial Interfacing Arduino To An E-ink Display.

Conexión y prueba de la pantalla e-Paper

Primero, conectemos y probemos el funcionamiento de la pantalla e-Paper. La siguiente imagen muestra el cableado completo para alimentación y SPI.

Connecting e-Paper to ESP32 via SPI
Conexión de e-Paper a ESP32 vía SPI

Y aquí está la tabla con todas las conexiones para mayor comodidad. Ten en cuenta que puedes alimentar la pantalla con 3.3V o 5V, pero el ESP32-lite solo tiene salida de 3.3V y las líneas de datos SPI deben ser de 3.3V.

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

Instalar la librería GxEPD2

Antes de poder dibujar o escribir en la pantalla e-Paper, necesitamos instalar dos librerías. La Adafruit_GFX es una librería gráfica base que proporciona un conjunto común de primitivas gráficas (texto, puntos, líneas, círculos, etc.). Y la GxEPD2 proporciona el software controlador gráfico para manejar una pantalla e-Paper vía SPI.

Simplemente instala las librerías de la forma habitual. Después de la instalación deberían aparecer en el Library Manager así.

Adafruit_GFX and GxEPD2 libraries in Library Manager
Librerías Adafruit_GFX y GxEPD2 en Library Manager

Código de prueba

Aquí tienes un código de prueba simple que muestra el texto «Makerguides» en la pantalla. Primero mira el código completo y luego lo analizamos.

#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 a desglosar el código en sus componentes para entender cómo funciona.

Constantes y librerías

El código comienza definiendo una constante ENABLE_GxEPD2_GFX como 0. Puedes ponerla a 1, lo que según la documentación habilita que la clase base GxEPD2_GFX pase referencias o punteros a la instancia de la pantalla como parámetro. Pero usa ~1.2k más de código y no lo necesitamos, así que está en 0.

#define ENABLE_GxEPD2_GFX 0

Luego incluimos el archivo de cabecera GxEPD2_BW.h para la pantalla e-Paper en blanco y negro (BW). Si tienes una pantalla de 3 colores incluirías GxEPD2_3C.h, o GxEPD2_4C.h para una pantalla de 4 colores, y GxEPD2_7C.h para una de 7 colores, respectivamente.

#include "GxEPD2_BW.h"

Objeto Display

La siguiente línea es importante. Crea el objeto display y depende del tipo o marca de pantalla. Probé una WeAct y una WaveShare y la siguiente línea funciona para ambas.

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

La librería Readme for GxEPD2 lista todas las pantallas soportadas y puedes encontrar los detalles en los archivos de cabecera, por ejemplo GxEPD2.h.

Función setup

Todo el trabajo real ocurre en la función setup. Primero configuramos parámetros de la pantalla como velocidad de comunicación, rotación, fuente, color de texto y color de fondo. Si tienes problemas con la actualización, puede que tengas que ajustar los parámetros para la función 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();
}

Luego llenamos la pantalla de blanco, posicionamos el cursor, escribimos el texto y finalmente ponemos el controlador en modo hibernación. Esto apaga la pantalla y pone el controlador en modo deep-sleep (no el ESP32, solo la pantalla).

Función loop

La función loop() está vacía en este ejemplo porque el contenido se crea en la función setup() y no necesita actualizarse continuamente.

void loop() {}

Subir y ejecutar el código

Ahora estamos listos para subir y ejecutar el código. Selecciona la placa que tienes en el board manager. En mi caso es la WEMOS LOLIN32 Lite que mencioné en las piezas necesarias:

WEMOS LOLIN32 Lite selected in Board Manager
WEMOS LOLIN32 Lite seleccionado en Board Manager

Luego pulsa subir y tras un poco de parpadeo, tu pantalla debería mostrar el siguiente texto:

Test output on e-Paper display
Salida de prueba en pantalla e-Paper

Actualización completa

Solo para demostrar lo molesto que es un refresco completo de una pantalla e-Paper, usaremos el siguiente código. Imprime el texto «msec: » y al lado los milisegundos desde el inicio del 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();
}

El contenido se actualiza y refresca en el loop principal tan rápido como sea posible. El siguiente video corto muestra cómo se ve. Como ves, hay mucho parpadeo y la actualización tarda casi 3 segundos.

Full refresh of e-Paper display
Refresco completo de pantalla e-Paper

Así que para cualquier aplicación con actualizaciones frecuentes, un refresco completo tarda demasiado para ser útil.

Actualización parcial de un área única

En lugar de hacer un refresco completo cada vez que queremos actualizar el valor de milisegundos, podemos hacer una actualización parcial solo para esa sección. El siguiente código hace un refresco completo al inicio y luego solo actualiza la sección que muestra el valor de milisegundos:

#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();
}

El siguiente video muestra cómo se ve la actualización parcial. Como ves, el parpadeo desaparece y la actualización es mucho más rápida (< 1 seg).

Partial refresh of e-Paper display
Actualización parcial de pantalla e-Paper

Hay un refresco completo al iniciar el código, con el parpadeo y lentitud habituales, pero solo ocurre una vez.

Dibujo paginado

Hay diferentes formas de implementar el refresco completo o parcial. Usar la función drawPaged() es probablemente la más elegante y sencilla. Toma una función drawCallback y un vector de parámetros pv para realizar un redibujo paginado:

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

Dentro de la función drawCallback, llamamos a setFullWindow() para un refresco completo o a setPartialWindow(x, y, w, h) para un refresco parcial.

Usar drawPaged() también tiene la ventaja de manejar las operaciones de dibujo de forma eficiente en memoria, lo cual es especialmente útil para pantallas grandes o con RAM limitada. Dibuja el contenido en varios pasos pequeños (páginas) en lugar de refrescar toda la pantalla de una vez.

Función drawFull

Si miras la función drawFull(), verás que llamamos a setFullWindow(), posicionamos el cursor y luego imprimimos el texto estático "msec: ".

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

En la función setup, iniciamos la pantalla, configuramos sus propiedades y luego llamamos a drawPaged(drawFull, 0) para hacer el refresco completo.

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

El vector de parámetros pv se pone a 0, ya que no pasamos parámetros. Pero podrías pasar fuente, color, texto u otra información relevante para la función drawFull aquí.

Función drawPartial

La principal diferencia de la función drawPartial() respecto a drawFull() es que setPartialWindow(x, y, w, h) se llama para limitar el área de la pantalla que se refresca.

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);
}

Después posicionamos el cursor, imprimimos el texto de milisegundos y dibujamos un rectángulo para marcar la ventana de refresco.

Finalmente, la función drawPartial() se llama vía drawPaged(drawPartial, 0) en el loop principal, lo que provoca la actualización repetida del valor mostrado.

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

Ten en cuenta que cualquier cosa que dibujes fuera del área parcial no se mostrará. En drawPartial() posicionamos el cursor dentro del área y luego imprimimos el valor de milisegundos dentro de ella.

Para mostrar el área de actualización parcial dibujamos un rectángulo alrededor. Sin embargo, esto no es del todo exacto. Más sobre esto en la siguiente sección.

Alineación de la ventana de refresco

Debido a limitaciones de direccionamiento de los controladores de pantallas e-Paper, las coordenadas de la ventana de refresco deben estar alineadas a múltiplos de 8. Específicamente,

  • x y w deben ser múltiplos de 8 para rotación 0 o 2,
  • y y h deben ser múltiplos de 8 para rotación 1 o 3.

La función setPartialWindow(x, y, w, h) permite valores arbitrarios pero internamente los alinea según lo requerido. Esto significa que la ventana real de refresco puede ser más grande que la especificada.

Esto es importante porque significa que se actualizarán secciones de la pantalla que quizás no querías. La imagen muestra el área de refresco deseada (rectángulo negro) y el área real de refresco (rectángulo blanco relleno).

Intended and actual refresh window
Área de refresco deseada y real

Como ves, la altura del área real es mayor que la especificada. Si diseñas el layout de tu contenido debes tener esto en cuenta. Especialmente porque la función drawRect() llena completamente el fondo del área real con blanco. Eso significa que borrará contenido fuera del área especificada pero dentro del área real.

Si quieres replicar el experimento anterior, aquí tienes el código. Solo invierte el color de fondo y texto del redibujo completo para hacer visible el área real de refresco.

#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();
}

Actualización parcial con deep sleep

Las pantallas e-Paper se usan a menudo en proyectos con batería, donde el microcontrolador suele entrar en deep-sleep. Cómo combinar el deep-sleep del ESP32 con la actualización parcial de una pantalla e-Paper es el tema de esta sección.

La dificultad principal es que queremos hacer un refresco completo solo al primer arranque (reset) del ESP32, pero cada vez que despertamos del deep-sleep queremos hacer solo una actualización parcial. El siguiente código lo consigue. Échale un vistazo rápido y luego entramos en detalles.

#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();
}

El código es básicamente el mismo que el ejemplo anterior, con algunos cambios importantes.

Primero, definimos una variable initial que se almacena en RTC memory, lo que significa que su valor se conserva en deep-sleep.

RTC_DATA_ATTR bool initial = true;

La variable initial se inicializa a true y tras el primer refresco completo vía drawFull() se pone a false. Esto significa que initial=true tras un reset, pero false tras deep-sleep.

Luego, debemos indicarle a la pantalla que no haga un refresco completo tras deep-sleep. La función init(serial_diag_bitrate, initial, reset_duration pulldown_rst_mode) tiene el parámetro initial para eso:

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

Ya casi terminamos. En la función setup solo hacemos un redibujo completo si initial==true. Esto significa solo tras un reset, no tras despertar de deep-sleep.

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

Finalmente, en la función loop ponemos el ESP32 a dormir después de hacer la actualización parcial.

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

Según la documentación de la función init(), solo hay que asegurarse de que la alimentación de la pantalla se mantenga durante el deep-sleep. Llamar a hibernate, como se muestra arriba, está bien. El siguiente video muestra cómo se ve.

Partial refresh of e-Paper display with deep-sleep
Actualización parcial de pantalla e-Paper con deep-sleep

Como millis() se reinicia durante el deep-sleep y muestra un valor constante de 151 ms, también muestro un número aleatorio (0..9) tras el valor de msec para hacer visible la actualización.

Actualización parcial de múltiples áreas

A veces queremos actualizar parcialmente varias áreas en diferentes ubicaciones y tiempos. Por ejemplo, podrías querer refrescar un reloj cada segundo pero la temperatura solo cada 5 minutos.

Extender el código anterior para actualizar parcialmente múltiples áreas es fácil. Mira el siguiente código completo que refresca el valor de milisegundos en dos á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 ves, simplemente definimos dos funciones drawPartial1() y drawPartial2() que se llaman en el loop para hacer la actualización parcial de dos áreas distintas.

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

Para hacerlo más interesante y cómodo, añadí una función drawText() que calcula el cuadro delimitador para un texto dado, ajusta la ventana de refresco a esas dimensiones, imprime el texto y dibuja el cuadro delimitador:

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); 
}

Si usas este código asegúrate de que el texto siempre tenga la misma longitud. Un texto que se acorte entre actualizaciones puede dejar artefactos, porque el cuadro delimitador y área de refresco se hacen más pequeños. En el código uso el especificador de formato "%8d" para asegurar longitud constante pese a números cambiantes. El clip muestra el código en acción:

Partial refresh of multiple e-Paper display areas
Actualización parcial de múltiples áreas en pantalla e-Paper

Ten en cuenta que la actualización ahora tarda el doble, porque hacemos dos actualizaciones independientes. No puedes usar dos ventanas de refresco diferentes (setPartialWindow) en la misma función de dibujo. Pero podrías especificar una ventana más grande que abarque ambos contenidos a actualizar. En ese caso no tendrías que preocuparte tanto por diferentes longitudes de texto.

Actualización parcial usando buffer de pantalla

Como último ejemplo, quería implementar un simple plotter de datos, por ejemplo para monitorizar temperatura en el tiempo. La siguiente imagen muestra cómo será el plotter. Tiene un título fijo «Temperature», un eje x fijo y valores simulados de temperatura que cambian dinámicamente.

Temperature plotter on e-Paper display
Plotter de temperatura en pantalla e-Paper

Idealmente, simplemente dibujaríamos píxeles individuales (puntos de datos) con una ventana de refresco parcial de 1 píxel. Pero esto no funciona, porque las dimensiones de la ventana deben ser múltiplos de 8 (ver arriba). Si lo intentas, verás que la ventana más grande borra algunos píxeles y partes del eje previamente dibujados.

Para evitar este problema, primero dibujamos en un «canvas». Un canvas es básicamente un buffer de pantalla que podemos actualizar en segundo plano. La actualización parcial toma un área pequeña del canvas y la usa para actualizar la pantalla.

Partial refresh using canvas
Actualización parcial usando canvas

El siguiente código contiene la implementación completa de este pequeño plotter 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();
}

Primero definimos constantes auxiliares para dimensiones de pantalla y colores de primer plano y fondo. Nota que ancho y alto están invertidos porque rotamos la pantalla (setRotation(1)) en la función 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

Lo siguiente importante es crear el objeto canvas con las mismas dimensiones que la pantalla. Como mi pantalla e-Paper solo tiene dos colores (blanco y negro), creamos un canvas con profundidad de 1 bit (GFXcanvas1).

GFXcanvas1 canvas(W, H);

Si tu pantalla tiene más colores o niveles de gris, debes crear un canvas que coincida. Mira la Adafruit GFX Graphics Library documentation para más detalles.

initCanvas y drawCanvas

La función initCanvas() simplemente escribe el título y dibuja el eje x en el canvas pero no muestra nada. La visualización real se hace con drawCanvas(), que copia el canvas como bitmap a la pantalla.

drawFull y drawPartial

La función drawFull() hace un refresco completo, mientras que la función drawPartial() hace un refresco parcial. Durante el refresco completo dibujamos el título y el eje. Durante el parcial dibujamos los puntos de datos individuales.

La siguiente fórmula crea los datos simulados de temperatura y, y x es el tiempo simulado.

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

Lo importante es donde escribimos el punto de datos (x,y) como un píxel en el canvas, luego definimos una ventana de refresco justo alrededor de ese píxel y dibujamos el canvas:

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

Como la ventana de refresco parcial se pone a (x,y,1,1) no refrescamos toda la pantalla sino solo un área pequeña alrededor del píxel (recuerda la limitación de múltiplos de 8). Sin embargo, como esta área se toma del canvas, contiene lo que ya se dibujó y no borra contenido existente en la pantalla. El siguiente clip muestra el plotter en acción:

Data plotter on e-Paper display
Plotter de datos en pantalla e-Paper

Puedes ver que la curva cruza el eje x sin borrarlo y no hay parpadeo. Aunque la actualización no es muy rápida (alrededor de 1 seg), es suficiente para un plotter de temperatura.

Ten en cuenta que no se puede añadir fácilmente deep-sleep a este código. Tendríamos que almacenar todo el canvas en la memoria RTC, que normalmente no es lo suficientemente grande (8K). Podrías almacenar solo los puntos de datos, pero entonces la función de redibujo parcial sería más compleja porque tendrías que redibujar toda la curva. Alternativamente, también está la opción de guardar el canvas en una tarjeta SD u otra memoria externa más grande.

Recomendaciones

La Waveshare Manual ofrece las siguientes recomendaciones para operar una pantalla e-Paper (resumidas y parafraseadas por mí):

Refresco completo: La pantalla e-Paper parpadea varias veces durante el proceso de refresco (el número de parpadeos depende del tiempo de refresco), y el parpadeo sirve para eliminar imágenes residuales y lograr el mejor efecto visual.

Refresco parcial: En este caso la pantalla no parpadea durante el refresco. Después de varias actualizaciones parciales, se debe hacer un refresco completo para eliminar imágenes residuales. De lo contrario, la imagen residual puede volverse permanente.

Se recomienda establecer el intervalo de refresco de la pantalla e-ink en al menos 180 segundos (excepto para productos que soportan refresco parcial).

Después de un refresco, se recomienda apagar o hibernar la pantalla. Esto prolonga la vida útil y reduce el consumo de energía.

Al usar una pantalla e-ink de tres colores, se recomienda actualizar la pantalla al menos una vez cada 24 horas.

Se recomienda usar pantallas e-Paper en interiores y no en exteriores. Si quieres usarla al aire libre, colócala en un área sombreada y cubre completamente la parte blanca del adhesivo de la cinta de conexión de la pantalla con cinta 3M.

Conclusiones

En este tutorial aprendiste cómo realizar actualizaciones parciales en una pantalla e-Paper para evitar parpadeos y lograr tiempos de refresco más rápidos. Específicamente, vimos cómo hacer actualizaciones parciales para áreas únicas y múltiples, en combinación con deep-sleep y actualizaciones píxel a píxel.

Podrías ampliar fácilmente el ejemplo del pequeño plotter añadiendo un sensor real de temperatura como el BME280 al proyecto. Echa un vistazo al tutorial Weather Station on e-Paper Display para más detalles. Ten en cuenta que podrías usar canvas separados para temperatura, humedad y presión atmosférica, y luego cambiar entre ellos para mostrar diferentes datos.

Si tienes alguna pregunta, no dudes en preguntar en la sección de comentarios.

¡Feliz bricolaje ; )