En este nuevo tutorial te mostraré como usar la librería GxEPD2 para mostrar textos y gráficos en un display de tinta electrónica usando la placa XIAO RA4M1 programada en Arduino.
Introducción
En un tutorial anterior te expliqué que son las pantallas de tinta electrónica y cual es su principio básico de funcionamiento. También te presenté la placa adaptadora para tinta electrónica de Seed Studio que permite usar estas pantallas con distintas placas de la familia XIAO de una forma simple y cómoda. Luego vimos las características principales del display GDEY0213F51 y los pasos que debes seguir para mostrar una imagen a pantalla completa usando un programa escrito empleando Arduino y la placa XIAO RA4M1. En esta ocasión seguiremos profundizando en las posibilidades de esta pantalla pero ahora empleando la librería GxEPD2, una popular librería de Arduino para controlar pantallas ePaper.
La librería GxEPD2
GxEPD2 es la evolución de GxEPD, la primera versión de una librería para Arduino especializada en el control de pantallas de tinta electrónica o EPD (Electronic Paper Display) con interfaz SPI. Desarrollada por Jean-Marc Zingg (ZinggJM), GxEPD2 amplía las capacidades de su predecesora y ofrece soporte para una gran variedad de pantallas de los principales fabricantes, como Good Display y Waveshare.
La librería GxEPD2 depende de otra librería ampliamente utilizada, Adafruit_GFX, para su funcionamiento. Adafruit_GFX fue desarrollada por la empresa Adafruit y es una de las librerías más populares en el ecosistema Arduino. Proporciona un conjunto de funciones que permiten dibujar texto y formas geométricas en una variedad de pantallas, como LCD y OLED.
La relación entre ambas librerías se puede resumir de la siguiente manera: mientras que Adafruit_GFX se ocupa de las primitivas gráficas y proporciona las herramientas básicas para representar gráficos y texto, GxEPD2 complementa su uso al gestionar específicamente el control de pantallas de tinta electrónica (EPD) y su interfaz de comunicación SPI. De esta forma, Adafruit_GFX maneja la parte visual y GxEPD se enfoca en el manejo y funcionamiento de las pantallas EPD.
A continuación te explicaré como instalar las librerías GxEDP2 y Adafruit_GFX en el Arduino IDE 2 para usarlas con placa XIAO RA4M1 y como aprovechar sus capacidades para mostrar textos, gráficos e imágenes en el display de 4 colores GDEY0213F51 de Seeed Studio.
Modelos XIAO: Aunque este tutorial fue realizado con una placa XIAO RA4M1, se aplica en general a otras placas de la familia XIAO que funcionen con el adaptador para tinta electrónica.
Si quieres conocer más sobre la placa XIAO RA4M1 puedes leer este artículo anterior. Si no conoces la familia de placas XIAO te sugiero que consultes esta completa guía.
Instalación
Antes de poder utilizar estas librerías, es necesario instalarlas en el IDE siguiendo el procedimiento habitual.
Dado que GxEPD2 depende de Adafruit_GFX, basta con instalar GxEPD2 directamente. Durante el proceso de instalación, el IDE detectará la dependencia y nos preguntará si deseamos instalar la librería de Adafruit. En ese momento, simplemente debemos confirmar y proceder con la instalación.
El procedimiento es el mismo que debemos seguir para la instalación de cualquier librería, hacemos click en el Gestor de Bibliotecas en la barra de la izquierda o elegimos Herramientas – Gestionar bibliotecas en el menú principal.
Siguiendo cualquiera de los dos métodos te aparecerá un cuadro de búsqueda. Escribe GxEPD2 para que busque la librería y cuando la veas, pulsa sobre el botón INSTALAR.
Versión: Asegurate de instalar la última versión de GxEPD2, no te confundas con GxEPD, que es la versión anterior.
En el IDE deberías ver algo como lo siguiente:
Luego de pulsar INSTALAR, el IDE notará la dependencia con otras librerías (como Adafruit_GFX) y te preguntará si también quieres instalarlas. Confirma que si haciendo click sobre INSTALAR TODO.
Luego de unos instantes, el IDE descargará e instalará todos los archivos necesarios.
Conexiones del ePaper
Como veremos más adelante, uno de los primeros pasos al utilizar GxEPD es especificar cómo está conectado el ePaper a nuestra placa, en este caso, la XIAO RA4M1. En esta guía, usaré la placa adaptadora para tinta electrónica de Seeed Studio (o eInk Expansion Board).
Esta placa adaptadora resuelve la conexión entre la XIAO y el ePaper. El detalle de que pines de la XIAO están conectados al ePaper se puede encontrar en la Wiki de Seeed:
Conexiones: Si no usas la placa adaptadora de Seeed Studio y realizas tu mismo la conexión al ePaper, debes tener presente cómo has realizado esas conexiones cuando comiences a utilizar GxEPD. Asimismo debes tener cuidado con los niveles de tensión que soporta el ePaper.
Usando GxEPD2
El siguiente es un ejemplo básico que usa GxEPD2 para mostrar en pantalla el clásico “Hola Mundo!”:
// Incluir las bibliotecas necesarias #include <GxEPD2_4C.h> #include <Fonts/FreeSans9pt7b.h> // Definición de pines para placa ePaper const int EINK_BUSY = D5; // D5 const int EINK_RST = D0; // D0 const int EINK_DC = D3; // D3 const int EINK_CS = D1; // D1 const int EINK_SCK = D8; // D8 (SCK) const int EINK_MOSI = D10; // D10 (MOSI) // Crear el objeto asociado a la pantalla GxEPD2_4C<GxEPD2_213c_GDEY0213F51, GxEPD2_213c_GDEY0213F51::HEIGHT> display(GxEPD2_213c_GDEY0213F51(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY)); void setup() { // Inicialización del epaper display.init(115200); display.setFullWindow(); display.fillScreen(GxEPD_WHITE); display.setRotation(1); display.setTextColor(GxEPD_BLACK); display.setTextSize(2); display.setFont(&FreeSans9pt7b); display.setCursor(0, 30); display.print ("Hola mundo!"); display.display (); } void loop() { //No hace nada }
Al correr el programa se puede ver en la pantalla el siguiente resultado:
Analicemos este código para empezar a comprender la lógica de funcionamiento de GxEPD2.
En las primeras líneas, incluimos las librerías GxEPD2_4C, que contiene las funciones y definiciones para pantallas de 4 colores y FreeSans9pt7b que define una “fuente” (font) o tipo de letra.
// Incluir las bibliotecas necesarias #include <GxEPD2_4C.h> #include <Fonts/FreeSans9pt7b.h>
A continuación definimos como constantes los nombres de los pines de conexión del display, como vimos antes en la Fig. 3. Esto se hace sólo por claridad y no es estrictamente necesario, podríamos usar D5, D0 o D3 en vez de EINK_BUSY, EINK_RST o EINK_DC sin afectar el funcionamiento del código.
// Definición de pines para placa ePaper const int EINK_BUSY = D5; // D5 const int EINK_RST = D0; // D0 const int EINK_DC = D3; // D3 const int EINK_CS = D1; // D1 const int EINK_SCK = D8; // D8 (SCK) const int EINK_MOSI = D10; // D10 (MOSI)
Luego viene una definición un tanto compleja que consiste en crear el objeto asociado a la pantalla, en este caso con el nombre display.
// Crear el objeto asociado a la pantalla GxEPD2_4C<GxEPD2_213c_GDEY0213F51, GxEPD2_213c_GDEY0213F51::HEIGHT> display(GxEPD2_213c_GDEY0213F51(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY));
Para crear este objeto se usa una plantilla (template) llamada GxEPD2_4C que tiene como parámetros GxEPD2_213c_GDEY0213F51 que es el identificador de la clase que funciona como controlador específico de este display en particular y la constante GxEPD2_213c_GDEY0213F51::HEIGHT que define la altura del display y se debe especificar para definir si se usa el modo paginado o no paginado (luego te explicaré que significa, no te preocupes).
Finalmente, se usa un constructor para el objeto display (le puedes cambiar el nombre si quieres) al que se le pasa como parámetros el identificador de display nuevamente y el detalle de los pines de conexión (usando las constantes que definimos antes).
Constructor: Esta declaración es fundamental para el resto del código porque define la pantalla que vas a usar y por lo tanto todas las funciones o métodos que estarán disponibles para esa pantalla y sus características.
GxEPD2 no soporta cualquier pantalla de manera arbitraria. Antes de poder usar un ePaper debemos asegurarnos de que este soportado y buscar cual es el controlador que le corresponde.
Siguiendo con el análisis del código de ejemplo, a continuación tenemos la función Setup (donde ocurre todo lo interesante) y Loop (que no hace nada).
Al comienzo de Setup tenemos una primera inicialización:
void setup() { // Inicialización del epaper display.init(115200); display.setFullWindow(); display.fillScreen(GxEPD_WHITE); display.setRotation(1);
Como puedes ver, consisten en llamadas a distintos métodos pertenecientes al objeto display, por eso se usa la sintaxis display.método (parámetros). Veamos que hace cada uno:
- init inicializa varios elementos del objeto display. En este caso sólo le proporcionamos el valor 115200 que es la velocidad en baudios para la salida de diagnóstico por el monitor serie. Lo puedes desactivar dandole el valor 0, pero es útil para saber qué está haciendo la librería.
- setFullWindow indica que vas a usar la pantalla completa, porque también es posible usar sólo una parte para acelerar el proceso de refresco (aunque no en la pantalla que estoy usando para esta guía).
- fillScreen rellena la pantalla, en este caso de color blanco (GxEPD_WHITE) borrando en contenido que podría haber quedado anteriormente. Se puede usar cualquiera de los colores que soporta este display:
- Negro (
GxEPD_BLACK
) - Blanco (
GxEPD_WHITE
) - Rojo (
GxEPD_RED
) - Amarillo (
GxEPD_YELLOW
)
- Negro (
- setRotation define la orientación de la pantalla en múltiplos de 90 grados.
El siguiente fragmento de código prepara cómo se verán los textos en pantalla:
display.setTextColor(GxEPD_BLACK); display.setTextSize(2); display.setFont(&FreeSans9pt7b);
El método setTextColor establece el color del texto, setTextSize el tamaño de las letras y setFont indica la fuente o tipo de letra que se usará para imprimir los textos.
Finalmente, se posiciona el cursor y se imprime el texto:
display.setCursor(0, 30); display.print ("Hola mundo!");
En realidad, llegados a este punto, el código no muestra nada en la pantalla. Todas las instrucciones realizan sus acciones en un buffer o área de memoria que luego debe ser volcado a la pantalla para que realmente se visualice. Una de las formas de hacerlo es empleando el método display, luego veremos otra.
display.display ();
Este volcado del buffer de memoria a la pantalla también se denomina refresco (refresh).
Usando fuentes (fonts)
Como vimos en el ejemplo anterior, al mostrar texto en la pantalla se puede cambiar su tamaño y color. Además puedes elegir entre una variedad de tipos de letras o fuentes, con distintas características, tamaños y estilos.
Las fuentes disponibles están definidas en la librería GFX_Adafruit, dentro de la carpeta Fonts. Cada fuente es un archivo de cabera (.h)
El nombre de cada fuente nos indica el tipo de letra, estilo y tamaño. Por ejemplo, la fuente FreeSans9pt7b que usamos en el ejemplo anterior tiene un nombre que puede desglosarse en los siguientes elementos:
- Free: Es una fuente libre, sin restricciones de licencia
- Sans: Es una fuente de estilo moderno, como la letra Arial. También puede ser Mono, una fuente simple y monoespaciada o Serif, mas adornada como la Times New Roman.
- Bold: Significa que está resaltada con negrita. También puede ser Oblique (itálica) o regular si no dice nada.
- 9pt: Es el tamaño en puntos de la fuente.
- 7b: Indica que la fuente está definida usando valores de 7 bits.
Para seleccionar en que fuente se verá un texto debes usar el método setFont, pero primero debes cargar la definición de la fuente incluyendo el archivo de cabecera correspondiente.
El siguiente es un ejemplo que muestra un texto usando distintas fuentes, estilos y tamaños:
// Incluir las bibliotecas necesarias #include <GxEPD2_4C.h> //Incluir las definiciones de las fuentes #include <Fonts/FreeMono9pt7b.h> #include <Fonts/FreeMono12pt7b.h> #include <Fonts/FreeMonoBold12pt7b.h> #include <Fonts/FreeMonoOblique12pt7b.h> #include <Fonts/FreeSerifBold18pt7b.h> // Definición de pines para placa ePaper const int EINK_BUSY = D5; // D5 const int EINK_RST = D0; // D0 const int EINK_DC = D3; // D3 const int EINK_CS = D1; // D1 const int EINK_SCK = D8; // D8 (SCK) const int EINK_MOSI = D10; // D10 (MOSI) // Crear el objeto asociado a la pantalla GxEPD2_4C<GxEPD2_213c_GDEY0213F51, GxEPD2_213c_GDEY0213F51::HEIGHT> display(GxEPD2_213c_GDEY0213F51(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY)); void setup() { // Inicialización del epaper display.init(115200); display.setFullWindow(); display.fillScreen(GxEPD_WHITE); display.setRotation(1); display.setTextColor(GxEPD_BLACK); display.setTextSize(1); display.setFont(&FreeMono9pt7b); display.setCursor(0, 20); display.print ("Hola mundo!"); display.setFont(&FreeMono12pt7b); display.setCursor(0, 40); display.print ("Hola mundo!"); display.setFont(&FreeMonoBold12pt7b); display.setCursor(0, 60); display.print ("Hola mundo!"); display.setFont(&FreeMonoOblique12pt7b); display.setCursor(0, 80); display.print ("Hola mundo!"); display.setFont(&FreeSerifBold18pt7b); display.setCursor(0, 110); display.print ("Hola mundo!"); display.display (); } void loop() { //No hace nada }
Como puedes ver entre las líneas 34 y 52 se imprime el mismo texto con 4 fuentes distintas y en distintas posiciones. El resultado puede verse en la siguiente imagen:
Memoria: Carga sólo las fuentes que vayas a utilizar, porque cada definición ocupa memoria. Esto es importante sobre todo en placas con poca memoria como la RA4M1.
Además de las fuentes disponibles con la librería puedes buscar otras que sean compatibes en repositorios de Internet. Incluso existen herramientas on-line o de línea de comandos que te permiten adaptar una fuente cualquiera a un formato que puedas usar con GxEPD2.
Dibujando gráficos
La librería GxEPD2 también contiene métodos que permiten dibujar elementos gráficos como puntos y líneas y formas geométricas como rectángulos, círculos y triángulos, tanto vacíos como rellenos.
El siguiente ejemplo muestra algunos de estos métodos, combinados con texto:
// Incluir las bibliotecas necesarias #include <GxEPD2_4C.h> //Incluir las definiciones de las fuentes #include <Fonts/FreeMonoBold12pt7b.h> // Definición de pines para placa adaptadora de Seeed Studio const int EINK_BUSY = D5; // D5 const int EINK_RST = D0; // D0 const int EINK_DC = D3; // D3 const int EINK_CS = D1; // D1 const int EINK_SCK = D8; // D8 (SCK) const int EINK_MOSI = D10; // D10 (MOSI) // Crea el objeto asociado a la pantalla GxEPD2_4C<GxEPD2_213c_GDEY0213F51, GxEPD2_213c_GDEY0213F51::HEIGHT> display(GxEPD2_213c_GDEY0213F51(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY)); void setup() { // Inicialización del epaper display.init(115200); display.setFullWindow(); display.fillScreen(GxEPD_WHITE); display.setRotation(1); display.setTextSize(1); // Limpiar la pantalla display.setFullWindow(); display.fillScreen(GxEPD_YELLOW); // Fondo blanco // 1.Gráfico de tortas // Definir el centro y el radio del círculo int centerX = 65; int centerY = 80; int radius = 40; // Dibujar el círculo relleno de rojo display.fillCircle(centerX, centerY, radius, GxEPD_RED); // Calcular los ángulos para las secciones del gráfico float angle1 = 0; float angle2 = 144; // 40% de 360 grados (0.4 * 360) float angle3 = 252; // 144 + 108 (30% de 360 grados) // Convertir grados a radianes float angle1_rad = angle1 * PI / 180; float angle2_rad = angle2 * PI / 180; float angle3_rad = angle3 * PI / 180; // Calcular las coordenadas de los puntos en el borde del círculo int x1 = centerX + radius * cos(angle1_rad); int y1 = centerY + radius * sin(angle1_rad); int x2 = centerX + radius * cos(angle2_rad); int y2 = centerY + radius * sin(angle2_rad); int x3 = centerX + radius * cos(angle3_rad); int y3 = centerY + radius * sin(angle3_rad); // Dibujar las líneas desde el centro hasta el borde display.drawLine(centerX, centerY, x1, y1, GxEPD_WHITE); display.drawLine(centerX, centerY, x2, y2, GxEPD_WHITE); display.drawLine(centerX, centerY, x3, y3, GxEPD_WHITE); // 2.Gráfico de barras int originX = 135; //X del origen de coordenadas int originY = 115; //Y del origen de coordenadas int barW = 20; //Ancho de las barras int barGap = 5; //Separacion entre barras int barH1 = 50; //Altura barra1 int barH2 = 70; //Altura barra2 int barH3 = 45; //Altura barra3 // Dibujar los ejes X e Y display.drawLine (originX, originY, originX+90, originY, GxEPD_BLACK); display.drawLine (originX, originY, originX, originY-70, GxEPD_BLACK); // Dibujar las barras display.fillRect(originX+barGap, originY-barH1, barW, barH1, GxEPD_RED); display.fillRect(originX+2*barGap+barW, originY-barH2, barW, barH2, GxEPD_RED); display.fillRect(originX+3*barGap+2*barW, originY-barH3, barW, barH3, GxEPD_RED); // 3.Texto del título // Configuración del texto display.setTextColor(GxEPD_BLACK); display.setCursor(15, 20); display.setFont(&FreeMonoBold12pt7b); // Imprimir texto display.print ("ACME Corporation"); // Refrescar pantalla display.display (); } void loop() { //No hace nada }
El código tiene una inicialización como en anteriores ejemplos y luego tres bloques en los que se muestran los gráficos de tortas y barras y luego el título. Veamos cada uno de ellos:
// 1.Gráfico de tortas // Definir el centro y el radio del círculo int centerX = 65; int centerY = 80; int radius = 40; // Dibujar el círculo relleno de rojo display.fillCircle(centerX, centerY, radius, GxEPD_RED); // Calcular los ángulos para las secciones del gráfico float angle1 = 0; float angle2 = 144; // 40% de 360 grados (0.4 * 360) float angle3 = 252; // 144 + 108 (30% de 360 grados) // Convertir grados a radianes float angle1_rad = angle1 * PI / 180; float angle2_rad = angle2 * PI / 180; float angle3_rad = angle3 * PI / 180; // Calcular las coordenadas de los puntos en el borde del círculo int x1 = centerX + radius * cos(angle1_rad); int y1 = centerY + radius * sin(angle1_rad); int x2 = centerX + radius * cos(angle2_rad); int y2 = centerY + radius * sin(angle2_rad); int x3 = centerX + radius * cos(angle3_rad); int y3 = centerY + radius * sin(angle3_rad); // Dibujar las líneas desde el centro hasta el borde display.drawLine(centerX, centerY, x1, y1, GxEPD_WHITE); display.drawLine(centerX, centerY, x2, y2, GxEPD_WHITE); display.drawLine(centerX, centerY, x3, y3, GxEPD_WHITE);
Este bloque muestra el gráfico de tortas, que consiste en un círculo relleno de rojo con tres líneas blancas en su interior delimitando tres secciones distintas de la “torta”.
Lo primero es definir con variables el centro del círculo y el radio, para poder ubicar el gráfico en la pantalla de una manera cómoda ajustando estos valores.
A continuación siguen una serie de cálculos en los que se determinan las coordenadas de los puntos finales de las líneas que separan cada sección del círculo.
Primero se determinan los ángulos de esas líneas respecto de la horizontal en grados, luego se pasa esos valores a radianes y finalmente se los trata como si fueran vectores y se calculan sus componentes en x e y para determinar los puntos hasta donde se deben tirar las líneas rectas.
Código: Podría haber “escondido” este procedimiento, hacerlo aparte y luego harcodear en el programa los valores de estas coordenadas, pero me pareció útil mantenerlo con fines didácticos. Además, de esta manera es fácil cambiar los “porcentajes” que corresponden a cada sección de la torta y obtener así un gráfico diferente.
Finalmente se dibujan las líneas en blanco para marcar las tres secciones.
El segundo bloque es algo mas sencillo y es el encargado de dibujar el gráfico de barras.
// 2.Gráfico de barras int originX = 135; //X del origen de coordenadas int originY = 115; //Y del origen de coordenadas int barW = 20; //Ancho de las barras int barGap = 5; //Separacion entre barras int barH1 = 50; //Altura barra1 int barH2 = 70; //Altura barra2 int barH3 = 45; //Altura barra3 // Dibujar los ejes X e Y display.drawLine (originX, originY, originX+90, originY, GxEPD_BLACK); display.drawLine (originX, originY, originX, originY-70, GxEPD_BLACK); // Dibujar las barras display.fillRect(originX+barGap, originY-barH1, barW, barH1, GxEPD_RED); display.fillRect(originX+2*barGap+barW, originY-barH2, barW, barH2, GxEPD_RED); display.fillRect(originX+3*barGap+2*barW, originY-barH3, barW, barH3, GxEPD_RED);
Como puedes ver el criterio es similar al anterior, uso algunas variables con la ubicación y tamaño del gráfico para que sea sencillo modificarlo.
Al principio se definen las coordenadas del origen del sistema de ejes, el ancho de las barras, la separación entre ellas y las alturas de cada una de las tres barras que se muestran.
A continuación se dibujan los ejes como dos líneas en color negro y luego las tres barras.
El último bloque imprime el título y refresca el display (recuerda que el método display es imprescindible para que el contenido del buffer de memoria se transfiera a la pantalla).
// 3.Texto del título // Configuración del texto display.setTextColor(GxEPD_BLACK); display.setCursor(15, 20); display.setFont(&FreeMonoBold12pt7b); // Imprimir texto display.print ("ACME Corporation"); // Refrescar pantalla display.display ();
Como puedes apreciar, se configura el color, posición y fuente del texto y luego se lo imprime.
El resultado del código anterior es el siguiente:
Mostrando imágenes
Si los elementos gráficos mencionados anteriormente te resultan insuficientes y quieres mostrar imágenes de tipo bitmap, tengo buenas noticias: GxEPD2 también lo permite. Sin embargo, hay una limitación: solo es posible mostrar imágenes en un solo color. Pero no te preocupes; con un sencillo truco que te mostraré a continuación puedes mostrar imágenes tipo bitmap que aprovechen todos los colores de tu epaper.
Imágenes monocromáticas
Por una cuestión de simplicidad empecemos viendo como mostrar imágenes monocromáticas en blanco y negro.
Lo primero que debes hacer es preparar la imagen a mostrar. Debe estar en un formato monocromático (por ejemplo en BMP) con unas dimensiones adecuadas para el ePaper (menos de 122 x 250 en el caso del ePaper que uso para este artículo). Adicionalmente, la imagen debe estar rotada 90 grados a la derecha.
La siguiente es una imagen de ejemplo, de 120 x120 pixeles que ya está rotada 90 grados:
Una vez que tienes la imagen lista, con el tamaño y la cantidad de colores adecuada, debes convertirla a otro formato para poder incorporarla al código escrito en el Arduino IDE.
Esto se puede hacer empleando el programa image2lcd, que toma la imagen y la convierte a un archivo de cabecera (un archivo “.h”) con la información de la imagen en la forma de un array de bytes (unsigned char).
Si no conoces este programa, te sugiero que leas este artículo anterior en donde explico con detalle como se descarga, instala y registra y los pasos a seguir para realizar la conversión de la imagen.
Siguiendo ese procedimiento, obtendrás un archivo de cabecera (“.h”) como el siguiente:
Copia este archivo de cabecera en la misma carpeta que el código de ejemplo, que te muestro a continuación.
// Incluir las bibliotecas necesarias #include <GxEPD2_4C.h> //Incluir las definiciones de las fuentes #include <Fonts/FreeSansBold18pt7b.h> #include <Fonts/FreeSansBold24pt7b.h> // Incluir el bitmap #include "cafe120.h" // Definición de pines para placa adaptadora de Seeed Studio const int EINK_BUSY = D5; // D5 const int EINK_RST = D0; // D0 const int EINK_DC = D3; // D3 const int EINK_CS = D1; // D1 const int EINK_SCK = D8; // D8 (SCK) const int EINK_MOSI = D10; // D10 (MOSI) // Crea el objeto asociado a la pantalla GxEPD2_4C<GxEPD2_213c_GDEY0213F51, GxEPD2_213c_GDEY0213F51::HEIGHT> display(GxEPD2_213c_GDEY0213F51(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY)); void setup() { // Inicialización del epaper display.init(115200); display.setFullWindow(); display.setRotation(1); display.setTextSize(1); // Limpiar la pantalla display.fillScreen(GxEPD_YELLOW); // Fondo amarillo // Cargar bitmap display.drawBitmap(6, 6, gImage_cafe120, 120, 120, GxEPD_BLACK); // Imprimir "Latte" display.setTextColor(GxEPD_BLACK); display.setCursor(145, 50); display.setFont(&FreeSansBold18pt7b); display.print ("Latte"); // Imprimir "-10%" display.setTextColor(GxEPD_RED); display.setCursor(130, 100); display.setFont(&FreeSansBold24pt7b); display.print ("-10%"); // Refrescar pantalla display.display (); } void loop() { //No hace nada }
En el código anterior introduje como novedad el método drawBitmap. Este método muestra en pantalla un bitmap en las coordenadas especificadas y con un color determinado, dentro de la paleta de colores de la pantalla. Su formato es el siguiente:
drawBitmap (x, y, imagen, hor, ver, color);
Donde:
- x,y: Son las coordenadas en donde se empieza a mostrar el bitmap.
- imagen: Es el bitmap a mostrar. Luego de convertirlo a un archivo “.h” debes incluirlo al inicio del código.
- hor, ver: La cantidad de pixeles horizontales y verticales del bitmap.
- color: El color en el que se muestra el bitmap.
El resultado se vé de la siguiente forma:
Imagenes a colores
En el ejemplo anterior, observamos cómo el método drawBitmap
puede mostrar un bitmap en un solo color. Utilicé un diseño en negro sobre un fondo amarillo, pero podría haber elegido otros colores para el bitmap, como rojo o blanco, cualquiera de los disponibles en el ePaper.
Esto tal vez ya te ha dado una pista de cual es el truco para mostrar imagenes en varios colores: descomponer la imagen original en varias, una para cada color, convertir cada una a un archivo “.h” y luego mostrarlas superpuestas para recomponer la imagen original.
Dado que el ePaper utilizado en este caso admite cuatro colores, sería necesario generar cuatro imágenes: una para el blanco, otra para el negro, y lo mismo para el amarillo y el rojo. Sin embargo, puedes simplificar el proceso omitiendo la imagen correspondiente al color de fondo. Por ejemplo, si el fondo es blanco, solo serían necesarias tres imágenes: una para cada uno de los otros tres colores.
Desde el punto de vista del código esto es muy simple, solo debemos tener tres directivas “include” y tres métodos drawBitmap, pero se requiere de mas tiempo en la preparación de las imágenes.
A modo de ejemplo, te muestro el procedimiento para cargar en el ePaper la imagen de más abajo, que ya usé en otro tutorial:
La imagen original tiene sólo los cuatro colores del ePaper y el mismo tamaño: 122 x 250 pixeles.
Usando el programa GIMP (seguramente puedes hacer lo mismo con otro) separé cada color en una capa y las guardé como un BMP monocromo no sin antes girarlas 90 grados.
Quedaron de la siguiente forma (correspondiente a los colores negro, rojo y amarillo de izquierda a derecha):
Siguiendo el mismo procedimiento que te expliqué antes para imagenes monocromáticas, generé tres archivos “.h”, uno para cada color con el programa img2lcd.
Finalmente, el siguiente es el código que utilicé. Como puedes apreciar al inicio se incluyen los tres archivos “.h” con la información de cada color y luego se muestran los tres bitmaps exactamente en la misma posición pero con tres colores diferentes (hay que poner un offset de 6 puntos en la coordenada y para que se muestre correctamente).
// Incluir las bibliotecas necesarias #include <GxEPD2_4C.h> // Incluir los bitmaps de cada color #include "gatoNegroRot.h" #include "gatoRojoRot.h" #include "gatoAmarilloRot.h" // Definición de pines para placa adaptadora de Seeed Studio const int EINK_BUSY = D5; // D5 const int EINK_RST = D0; // D0 const int EINK_DC = D3; // D3 const int EINK_CS = D1; // D1 const int EINK_SCK = D8; // D8 (SCK) const int EINK_MOSI = D10; // D10 (MOSI) // Crea el objeto asociado a la pantalla GxEPD2_4C<GxEPD2_213c_GDEY0213F51, GxEPD2_213c_GDEY0213F51::HEIGHT> display(GxEPD2_213c_GDEY0213F51(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY)); void setup() { // Inicialización del epaper display.init(115200); display.setFullWindow(); display.setRotation(1); // Limpiar la pantalla display.fillScreen(GxEPD_WHITE); // Fondo blanco // Mostrar bitmaps (agregar offset en Y) display.drawBitmap(0, 6, gImage_gatoNegroRot, 250, 122, GxEPD_BLACK); display.drawBitmap(0, 6, gImage_gatoRojoRot, 250, 122, GxEPD_RED); display.drawBitmap(0, 6, gImage_gatoAmarilloRot, 250, 122, GxEPD_YELLOW); // Refrescar pantalla display.display (); } void loop() { //No hace nada }
Esta es la imagen correspondiente:
Desde luego, puedes hacer lo mismo con un bitmap de un tamaño menor al de la pantalla e incluir otros elementos como textos, formas, etc.
Modo paginado
Los distintos métodos de GxEPD2 como print, fillRect o drawBitmap no acceden directamente a la pantalla ePaper sino que modifican un búffer de memoria que luego se copia a la pantalla al invocar al método display.
Cuanto más grande sea la pantalla, mayor será el tamaño del buffer necesario, y el incremento en la cantidad de colores complicará aún más la situación. Esto puede ser un problema en microcontroladores con poca RAM (como el RA4M1) ya que el buffer ocupará un porcentaje importante de la memoria.
Para no usar tanta RAM, GxEPD2 tiene un modo de trabajo alternativo que se denomina Modo Paginado. En este modo, se define un buffer mas chico, por ejemplo la mitad de lo que sería necesario para toda la pantalla y el refresco se hace en dos etapas: primero se genera en el buffer el contenido de una mitad de la pantalla y se lo transfiere y luego se usa el mismo buffer para preparar la segunda mitad y se vuelve a transferir. Esto es mas lento que hacerlo de una vez, pero usa menos RAM.
Todos los ejemplos que vimos hasta ahora no usan paginacion. Para usar el modo paginado debes modificar el código de la siguiente manera:
En la creación del objeto de la pantalla (display), especificar que el buffer es una fracción de la pantalla completa. Para que sea la mitad debes usar HEIGHT/2 por ejemplo.
- Indicar el inicio del ciclo de paginación con el método firstPage
- Encerrar los métodos que modifican la pantalla en un lazo do..while.
- El bucle se debe repetir mientras el método nextPage devuelva Verdadero, lo que indica que todavía hay información que falta enviar a la pantalla.
Uno de los primeros ejemplos, el que muestra el texto “Hola mundo!” usando el modo paginado quedaría de la siguiente forma:
// Incluir las bibliotecas de GxEPD2 #include <GxEPD2_4C.h> //Incluir fuente para el texto #include <Fonts/FreeSans9pt7b.h> // Definición de pines para placa adaptadora Seeed Studio const int EINK_BUSY = D5; // D5 const int EINK_RST = D0; // D0 const int EINK_DC = D3; // D3 const int EINK_CS = D1; // D1 const int EINK_SCK = D8; // D8 (SCK) const int EINK_MOSI = D10; // D10 (MOSI) //Crea objeto del display con HEIGHT/2 GxEPD2_4C<GxEPD2_213c_GDEY0213F51, GxEPD2_213c_GDEY0213F51::HEIGHT/2> display(GxEPD2_213c_GDEY0213F51(EINK_CS, EINK_DC, EINK_RST, EINK_BUSY)); void setup() { // Inicialización del epaper display.init(115200); display.setFullWindow(); display.fillScreen(GxEPD_WHITE); display.setRotation(1); //Inicializa parámetros del texto display.setTextColor(GxEPD_BLACK); display.setTextSize(2); display.setFont(&FreeSans9pt7b); // Iniciar el ciclo de paginación display.firstPage(); do { //Imprime texto display.setCursor(0, 30); display.print ("Hola mundo!"); } while (display.nextPage()); //Repite mientras falten páginas } void loop() { //No hace nada }
Como puedes ver, al crear el objeto display se define un buffer con la mitad del tamaño de la pantalla (HEIGHT/2). Luego se inicia el paginado con la llamada a firstPage y el método que muestra texto en pantalla (print) está dentro de un bucle do..loop usando nextPage como condición.
No es necesario emplear el método display porque al llamar a nextPage se realiza el refresco de la pantalla.
Refresco parcial
Algunos dispositivos ePaper ofrecen la posibilidad de realizar un refresco parcial de la pantalla. Esto significa que, en lugar de transferir todo el contenido del buffer (ya sea de una sola vez o en varias si se utiliza el modo paginado), se transmite únicamente la parte correspondiente al área de la pantalla que ha cambiado, dejando el resto intacto.
Evidentemente, esta técnica permite realizar actualizaciones mucho más rápidas, ya que solo se modifica una porción de la pantalla y, por lo tanto, se deben transferir muchos menos datos. Puede ser especialmente útil para mostrar información que cambia con frecuencia, como la proveniente de sensores.
Sin embargo, no todos los ePaper disponen de esta funcionalidad. Es más común en pantallas monocromáticas y menos frecuente en pantallas con varios colores. Además, en estos casos, a menudo el refresco parcial está disponible solo para un color (por ejemplo, el negro) y no para todos.
Para usar el refresco parcial se debe indicar la zona a refrescar con el método setPartialWindow en vez de usar setFullWindow, pero el display GDEY0213F51 que uso para este tutorial no cuenta con esa funcionalidad, así que no entraré en mas detalles.
Repositorio
Todos los ejemplos que te mostré en este tutorial están disponibles en el siguiente repositorio (click para acceder):
Conclusiones
En este tutorial te mostré cómo aprovechar al máximo las pantallas de tinta electrónica (ePaper) utilizando la biblioteca GxEPD2 junto con placas de la familia XIAO como la RA4M1. Te guié a través de la instalación de las bibliotecas necesarias, y te di ejemplos en Arduino de cómo mostrar textos, gráficos e imágenes en diferentes colores. También te expliqué cómo optimizar el uso de memoria mediante el modo paginado y discutimos brevemente el concepto de refresco parcial, una técnica que permite actualizaciones rápidas en ciertas pantallas.
Mi intención fue proporcionarte no solo ejemplos prácticos que puedas replicar, sino también ayudarte a comprender las fortalezas y limitaciones de esta tecnología y las posibilidades de la librería GxEPD2. Para no extenderme te presenté sólo algunos de los métodos de la librería, pero con esta base puedes seguir investigando y probando otras funcionalidades.
Espero que este tutorial te haya resultado útil y que puedas aplicar lo aprendido en tus proyectos. ¡Nos vemos!