Skip to Content

Reloj digital en pantalla e-Paper

Reloj digital en pantalla e-Paper

En este tutorial aprenderás a construir un reloj digital usando una pantalla e-Paper y un ESP32. También aprenderás a sincronizar el reloj con un proveedor de tiempo por internet (servidor SNTP). Finalmente, aprenderás a combinar estas funciones con la capacidad de deep-sleep del ESP para aumentar el tiempo de funcionamiento con batería.

Comencemos con las piezas necesarias.

Piezas necesarias

Para las piezas, recomiendo 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 o ESP8266 con suficiente memoria también funcionará.

Pantalla e-Paper de 2.9″

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cable USB de datos

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

La pantalla e-Paper usada en este proyecto es un módulo 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 este módulo tiene un pequeño jumper o interruptor que permite cambiar de SPI de 4 cables a SPI de 3 cables. Usaremos el SPI de 4 cables 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 un consumo en modo sleep muy bajo de 0.01µA y consume solo unos 26.4mW durante la actualización. Para más información sobre pantallas e-Paper, consulta los siguientes dos tutoriales: Interfacing Arduino To An E-ink Display y Weather Station on e-Paper Display.

Conexión y prueba de la pantalla e-Paper

Primero, conectemos y probemos la función 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 al ESP32 vía SPI

A continuación una 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

Puedes usar una protoboard para conectar todo. Pero yo conecté la pantalla directamente al ESP32 con cables Dupont y también añadí una batería LiPo para probar el funcionamiento con batería.

ESP32 wired with e-Paper and LiPo battery
ESP32 conectado con e-Paper y batería LiPo

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 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 controlar una pantalla e-Paper vía SPI.

Solo 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 el Library Manager

Código de prueba

Una vez instalada la librería, ejecuta el siguiente código de prueba para asegurarte de que todo funciona.

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

El código imprime el texto «Makerguides» y tras un parpadeo de la pantalla (actualización completa) deberías verlo en tu pantalla:

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

Si no, algo está mal. Lo más probable es que la pantalla no esté correctamente cableada o que se haya elegido un controlador de pantalla incorrecto. La línea crítica de código es esta:

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

La Readme para la librería GxEPD2 lista todas las pantallas soportadas y puedes encontrar los detalles en los archivos header, por ejemplo GxEPD2.h y GxEPD2_display_selection_new_style.h. Encuentra el controlador específico para tu pantalla. Esto puede requerir prueba y error.

Código para un reloj digital en e-Paper

Si el código de prueba anterior funciona, no deberías tener problemas con el código siguiente. Es el código completo para un reloj digital que sincroniza automáticamente su hora con un servidor de tiempo por internet (SNTP) y muestra la hora y fecha en una pantalla e-Paper.

Entre actualizaciones de pantalla, el ESP32 y la pantalla se ponen en modo deep-sleep para conservar batería. Echa un vistazo rápido al código completo primero, luego entraremos en detalles:

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"
#include "Fonts/FreeSans12pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"
#include "WiFi.h"
#include "esp_sntp.h"

const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const char *DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
const char *MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May",
  "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 
};

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

bool syncing = false;
RTC_DATA_ATTR uint16_t wakeups = 0;

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


void initDisplay() {
  bool initial = wakeups == 0;
  epd.init(115200, initial, 50, false);
  epd.setRotation(1);
  epd.setTextSize(1);
  epd.setTextColor(GxEPD_BLACK);
}

void initTime() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

void syncTime() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED)
    ;
  configTzTime(TIMEZONE, "pool.ntp.org");
  syncing = true;
}

void drawBackground(const void* pv) {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_WHITE);
}

void drawTime(const void* pv) {
  static char buff[40];
  struct tm t;
  getLocalTime(&t);

  epd.setPartialWindow(10, 10, W - 20, H - 20);

  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(40, 60);
  sprintf(buff, " %s %2d:%02d%s ",
          DAYSTR[t.tm_wday],
          t.tm_hour, t.tm_min,
          syncing ? "*" : " ");
  epd.print(buff);

  epd.setFont(&FreeSans12pt7b);
  epd.setCursor(55, 100);
  sprintf(buff, " %s  %02d-%02d-%04d ",          
          MONTHSTR[t.tm_mon],
          t.tm_mday, t.tm_mon + 1, t.tm_year + 1900);
  epd.print(buff);
}

void setup() {
  initDisplay();
  initTime();

  if (wakeups % 50 == 0)
    syncTime();
  if (wakeups % 2000 == 0)
    epd.drawPaged(drawBackground, 0);
  wakeups = (wakeups + 1) % 100000;

  epd.drawPaged(drawTime, 0);
  epd.hibernate();

  esp_sleep_enable_timer_wakeup(30 * 1000 * 1000);
  esp_deep_sleep_start();
}

void loop() {}

Si subes el código a tu ESP32, deberías ver la hora y fecha mostradas en tu e-Paper como se muestra abajo:

Time and Date on a 2.9" e-Paper Display
Hora y fecha en una pantalla e-Paper de 2.9″

Librerías

Comenzamos definiendo la constante ENABLE_GxEPD2_GFX como 0. Si se pone a 1, habilita que la clase base GxEPD2_GFX pase punteros a la instancia de 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 header 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 de 4 colores, y GxEPD2_7C.h para una de 7 colores, en su lugar.

#include "GxEPD2_BW.h"
#include "Fonts/FreeSans12pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"

También incluimos dos archivos para las fuentes usadas para mostrar la hora y fecha. Queremos mostrar la hora en una fuente más grande, de 24pt, negrita (FreeSansBold24pt7b) y la fecha en una fuente más pequeña, de 7pt (FreeSans12pt7b). Puedes ver un resumen de las AdaFruit GFX fonts aquí.

Finalmente, incluimos las librerías WiFi.h y esp_snt.h, que necesitaremos para sincronizar el reloj con un servidor SNTP. Más sobre esto más adelante.

#include "WiFi.h"
#include "esp_sntp.h"

Constantes

Las siguientes tres constantes tendrás que cambiarlas. Primero el SSID y PASSWORD para tu WiFi y luego la TIMEZONE donde vives.

const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

La especificación de zona horaria anterior «AEST-10AEDT,M10.1.0,M4.1.0/3» es para Australia, que corresponde a la Hora Estándar del Este Australiano (AEST) con ajustes de horario de verano.

Las partes de esta definición de zona horaria son las siguientes

  • AEST: Hora Estándar del Este Australiano
  • -10: Desfase UTC de 10 horas adelantado respecto al Tiempo Universal Coordinado (UTC)
  • AEDT: Hora de Verano del Este Australiano
  • M10.1.0: La transición al horario de verano ocurre el primer domingo de octubre
  • M4.1.0/3: La transición de vuelta al horario estándar ocurre el primer domingo de abril, con una diferencia de 3 horas respecto a UTC.

Para otras definiciones de zona horaria, consulta el Posix Timezones Database. Solo copia y pega la cadena que encuentres allí y cambia la constante TIMEZONE en consecuencia.

Además de la hora y fecha, también queremos mostrar el nombre del día y mes actual. Las siguientes constantes definen esos nombres. Puedes cambiar el idioma o elegir nombres más largos. Solo asegúrate de que quepan en la pantalla.

const char *DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
const char *MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May",
  "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 
};

Finalmente, definimos constantes para el ancho W y la altura H de la pantalla. Esto es principalmente por comodidad. Ten en cuenta que ancho y alto están invertidos, ya que rotamos la pantalla (setRotation(1)) en la función initDisplay.

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

Variables y objetos

A continuación definimos algunas variables y objetos globales.

bool syncing = false;
RTC_DATA_ATTR uint16_t wakeups = 0;

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

La variable syncing indica si el reloj está sincronizando su hora con el servidor SNTP. Si eso ocurre, la pantalla mostrará un carácter ‘*’ después de la hora. Mira la función drawTime() donde se usa esta variable. No es estrictamente necesaria, pero es útil para depurar y ver si la sincronización funciona.

La variable wakeups se incrementa cada vez que el ESP32 despierta del deep-sleep. Se usa para decidir si hacer una actualización completa de la pantalla o solo sincronizar la hora. Mira la función setup() donde se usa.

El objeto epd define el objeto de pantalla (epaper display). Debes definir este objeto para que coincida con la pantalla e-Paper que tienes. Consulta el Readme de la librería GxEPD2 y el archivo GxEPD2.h para ejemplos.

Función initDisplay

La función initDisplay inicializa la pantalla, establece la orientación a horizontal, el tamaño del texto a 1 y el color del texto a negro.

void initDisplay() {
  bool initial = wakeups == 0;
  epd.init(115200, initial, 50, false);
  epd.setRotation(1);
  epd.setTextSize(1);
  epd.setTextColor(GxEPD_BLACK);
}

Si la variable initial es verdadera, la pantalla e-Paper realizará una actualización completa cuando el ESP32 despierte del deep-sleep. Para evitar esto, debemos poner la variable initial en falso. Pero solo queremos hacer esto después del primer despertar, que es lo que logra wakeups == 0.

Función initTime

La función initTime solo establece la TIMEZONE. Debes asegurarte de que esta función se ejecute cada vez que el ESP32 despierte, de lo contrario tu reloj se desincronizará.

void initTime() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

Función syncTime

La función syncTime crea una conexión WiFi y luego sincroniza el reloj interno del ESP32 con el servidor SNTP «pool.ntp.org» llamando a configTzTime().

Puedes especificar otros servidores SNTP o incluso varios para conectarte. Consulta el tutorial How to synchronize ESP32 clock with SNTP server para más información.

void syncTime() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED)
    ;
  configTzTime(TIMEZONE, "pool.ntp.org");
  syncing = true;
}

También ponemos la variable syncing a true, que se usa en drawTime para mostrar que se realizó una sincronización. En el siguiente despertar (cada 30 segundos), la variable syncing se pone de nuevo en false.

Función drawBackground

La función drawBackground realiza una actualización completa (setFullWindow) de la pantalla e-Paper y simplemente llena la pantalla de blanco. También podrías añadir texto estático, gráficos u otra información que cambie poco.

void drawBackground(const void* pv) {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_WHITE);
}

Como la actualización completa es lenta y produce mucho parpadeo, se llama solo ocasionalmente, básicamente para evitar «quemaduras» o imágenes residuales de la actualización parcial. El Waveshare Manual tiene más información sobre esto.

Función drawTime

La función drawTime realiza una actualización parcial (setPartialWindow), que es mucho más rápida que la completa y, lo más importante, actualiza el contenido sin parpadeos.

void drawTime(const void* pv) {
  static char buff[40];
  struct tm t;
  getLocalTime(&t);

  epd.setPartialWindow(10, 10, W - 20, H - 20);

  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(40, 60);
  sprintf(buff, " %s %2d:%02d ",
          DAYSTR[t.tm_wday],
          t.tm_hour, t.tm_min,
          syncing ? "*" : " ");
  epd.print(buff);

  epd.setFont(&FreeSans12pt7b);
  epd.setCursor(55, 100);
  sprintf(buff, " %s  %02d-%02d-%04d ",          
          MONTHSTR[t.tm_mon],
          t.tm_mday, t.tm_mon + 1, t.tm_year + 1900);
  epd.print(buff);
}

Simplemente obtiene la hora local y luego imprime la hora y fecha con diferentes fuentes en posiciones específicas del cursor en la pantalla. Si quieres imprimir otra información de tiempo, por ejemplo segundos, aquí están todos los valores disponibles en el tipo de dato tm struct:

  Member    Type  Meaning                   Range
  tm_sec    int   seconds after the minute  0-61*
  tm_min    int   minutes after the hour    0-59
  tm_hour   int   hours since midnight      0-23
  tm_mday   int   day of the month          1-31
  tm_mon    int   months since January      0-11
  tm_year   int   years since 1900
  tm_wday   int   days since Sunday         0-6
  tm_yday   int   days since January 1      0-365
  tm_isdst  int   Daylight Saving Time flag

Función setup

La función setup se ejecuta cada vez que el ESP32 despierta. Comienza inicializando la pantalla y configurando la zona horaria como se describió arriba.

void setup() {
  initDisplay();
  initTime();

  if (wakeups % 50 == 0)
    syncTime();
  if (wakeups % 2000 == 0)
    epd.drawPaged(drawBackground, 0);
  wakeups = (wakeups + 1) % 100000;

  epd.drawPaged(drawTime, 0);
  epd.hibernate();

  esp_sleep_enable_timer_wakeup(30 * 1000 * 1000);
  esp_deep_sleep_start();
}

Después realiza diferentes acciones, dependiendo de cuántas veces se haya despertado el ESP32, contado en la variable wakeups.

Específicamente, sincroniza la hora cada 50 despertares. Como la duración del deep-sleep está configurada a 30 segundos, esto significa que sincronizamos cada 30s * 50 / 60s = 25 minutos. Puedes cambiar esto, pero cuanto más frecuente sincronices, mayor será el consumo de batería. Por otro lado, queremos sincronizar al menos una vez por hora para no perder el cambio entre horario de verano y estándar.

if (wakeups % 50 == 0)
    syncTime();

Cada 2000 despertares realizamos una actualización completa (drawBackground) para eliminar imágenes residuales dejadas por la actualización parcial que dibuja la hora y fecha (drawTime). De nuevo, puedes hacerlo más o menos frecuente. Tal como está, 30s * 2000 / 60s / 60min resulta en una actualización completa cada 16.6 horas.

  if (wakeups % 2000 == 0)
    epd.drawPaged(drawBackground, 0);

Para evitar un desbordamiento de entero incrementamos wakeups hasta un máximo de 100000 antes de reiniciarlo. Recuerda que la variable wakeups se almacena en la memoria RTC y conserva su valor en deep-sleep.

wakeups = (wakeups + 1) % 100000;

Finalmente, llamamos a drawTime para refrescar la hora y fecha en pantalla y luego ponemos la pantalla e-Paper en hibernación para ahorrar batería.

Después ponemos el ESP32 en deep sleep por 30 segundos. Esto significa que la pantalla se actualiza cada 30 segundos. Podrías ir un poco más lento, por ejemplo 45 segundos, o un poco más rápido, cada 10 segundos. Como antes, es un equilibrio entre una pantalla muy reactiva o la conservación de batería.

Los intervalos de despertar y actualización están elegidos para seguir las recomendaciones para pantallas e-Paper en el Waveshare Manual. Para más información también consulta el tutorial Partial Refresh of e-Paper Display.

Conclusiones

En este tutorial aprendiste a construir un reloj digital que sincroniza su hora con un servidor SNTP y muestra siempre la hora y fecha exactas en una pantalla e-Paper. El reloj utiliza la capacidad de deep-sleep del ESP32 para reducir el consumo de energía. Esto permite que el reloj funcione durante mucho tiempo con batería.

Si usas el ESP32 LOLIN Lite sugerido, conectar una batería LiPo recargable es sencillo. También recomendaría Monitor Battery Levels with a MAX1704X y tal vez mostrar un pequeño símbolo de batería para visualizar el nivel de carga.

Otra extensión común para relojes digitales es mostrar también la temperatura ambiente o datos meteorológicos. Consulta el tutorial Weather Station on e-Paper Display para más detalles.

Y si quieres un reloj analógico en lugar de digital, echa un vistazo a nuestro tutorial Analog Clock on e-Paper Display, que es muy similar a este.

Si tienes algún comentario, no dudes en dejarlo en la sección de comentarios.

¡Feliz bricolaje ; )