Skip to Content

Relógio Digital em Ecrã e-Paper

Relógio Digital em Ecrã e-Paper

Neste tutorial, vais aprender a construir um relógio digital usando um ecrã e-Paper e um ESP32. Também vais aprender a sincronizar o relógio com um fornecedor de tempo na internet (servidor SNTP). Por fim, vais aprender a combinar estas funções com a capacidade de deep-sleep do ESP para aumentar o tempo de funcionamento com bateria.

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

Peças Necessárias

Para as peças, recomendo aqui 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 ou ESP8266 com memória suficiente também funciona.

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

O ecrã e-Paper usado neste projeto é um módulo 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 este módulo tem um pequeno jumper/switch que permite alternar entre SPI de 4 fios e SPI de 3 fios. Vamos usar o SPI de 4 fios por defeito.

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

O módulo do ecrã funciona com 3,3V ou 5V, tem uma corrente de sleep muito baixa de 0,01µA e consome apenas cerca de 26,4mW durante a atualização. Para mais informações sobre ecrãs e-Paper, consulta os seguintes dois tutoriais: Interfacing Arduino To An E-ink Display e Weather Station on e-Paper Display.

Ligação e Teste do e-Paper

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

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

Abaixo está uma 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 saída de 3,3V e as linhas de dados SPI devem ser 3,3V!

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

Podes usar uma breadboard para ligar tudo. Mas eu liguei o ecrã diretamente ao ESP32 com fios Dupont e também adicionei uma bateria LiPo para testar o funcionamento do relógio com bateria.

ESP32 wired with e-Paper and LiPo battery
ESP32 ligado ao e-Paper e bateria LiPo

Instalar a biblioteca GxEPD2

Antes de poderes desenhar ou escrever no ecrã e-Paper, precisas de instalar duas bibliotecas. A Adafruit_GFX é uma biblioteca gráfica 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

Depois de instalares a biblioteca, executa o seguinte código de teste para garantir que tudo 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() {}

O código imprime o texto “Makerguides” e, após algum piscar do ecrã (atualização completa), deverás vê-lo no teu ecrã:

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

Se não aparecer, algo está errado. Muito provavelmente o ecrã não está corretamente ligado ou o driver do ecrã escolhido está errado. A linha crítica do código é esta:

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

A Readme para a biblioteca GxEPD2 lista todos os ecrãs suportados e podes encontrar os detalhes nos ficheiros header, por exemplo GxEPD2.h e GxEPD2_display_selection_new_style.h. Encontra o driver específico para o teu ecrã. Isto pode requerer algum teste e erro.

Código para um Relógio Digital no e-Paper

Se o código de teste acima funcionar, então não deverás ter problemas com o código abaixo. É o código completo para um relógio digital que sincroniza automaticamente a hora com um servidor de tempo na internet (SNTP) e mostra a hora e data num ecrã e-Paper.

Entre as atualizações do ecrã, o ESP32 e o ecrã são colocados em modo deep-sleep para preservar a bateria. Dá uma vista rápida ao código completo primeiro, e depois vamos analisar os detalhes:

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

Se carregares o código no teu ESP32, deverás ver a hora e a data mostradas no teu ecrã e-Paper como mostrado abaixo:

Time and Date on a 2.9" e-Paper Display
Hora e Data num ecrã e-Paper de 2,9″

Bibliotecas

Começamos por definir a constante ENABLE_GxEPD2_GFX como 0. Se definida como 1, ativa a classe base GxEPD2_GFX para passar ponteiros para a instância do ecrã como parâmetro. Mas isso usa cerca de 1,2k a mais de código e não precisamos, por isso está definida como 0.

#define ENABLE_GxEPD2_GFX 0

De seguida incluímos o ficheiro header GxEPD2_BW.h para o ecrã e-Paper 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"
#include "Fonts/FreeSans12pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"

Também incluímos dois ficheiros para as fontes usadas para mostrar a hora e a data. Queremos mostrar a hora numa fonte maior, 24pt, negrito (FreeSansBold24pt7b) e a data numa fonte menor, 7pt (FreeSans12pt7b). Podes encontrar uma visão geral das AdaFruit GFX fonts aqui.

Finalmente, incluímos as bibliotecas WiFi.h e esp_snt.h, que vamos precisar para sincronizar o relógio com um servidor de tempo SNTP. Mais sobre isso depois.

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

Constantes

As três constantes seguintes terás de alterar. Primeiro o SSID e PASSWORD para o teu WiFi e depois o TIMEZONE da tua zona horária.

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

A especificação da zona horária acima “AEST-10AEDT,M10.1.0,M4.1.0/3” é para a Austrália, que corresponde ao Australian Eastern Standard Time (AEST) com ajustes de horário de verão.

As partes desta definição de zona horária são as seguintes

  • AEST: Australian Eastern Standard Time
  • -10: Deslocamento UTC de 10 horas à frente do Tempo Universal Coordenado (UTC)
  • AEDT: Australian Eastern Daylight Time
  • M10.1.0: Transição para horário de verão ocorre no 1º domingo de outubro
  • M4.1.0/3: Transição de volta para horário padrão ocorre no 1º domingo de abril, com diferença de 3 horas em relação ao UTC.

Para outras definições de zona horária, consulta o Posix Timezones Database. Basta copiar e colar a string que encontrares lá e alterar a constante TIMEZONE em conformidade.

Além da hora e data, também queremos mostrar o nome do dia e do mês atuais. As constantes seguintes definem esses nomes. Podes mudar a língua ou escolher nomes mais longos. Só certifica-te que cabem no ecrã.

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 a largura W e altura H do ecrã. Isto é principalmente para conveniência. Note que a largura e altura estão invertidas, pois rodamos o ecrã (setRotation(1)) na função initDisplay.

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

Variáveis e Objetos

De seguida definimos algumas variáveis globais e objetos.

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

A variável syncing indica se o relógio está atualmente a sincronizar a hora com o servidor SNTP. Se isso acontecer, o ecrã mostrará um caractere ‘*’ após a hora. Vê a função drawTime() onde esta variável é usada. Não é estritamente necessária, mas é útil para depuração para ver se a sincronização da hora funciona.

A variável wakeups é incrementada sempre que o ESP32 acorda do deep-sleep. É usada para decidir se se faz uma atualização completa do ecrã ou apenas sincronizar a hora. Vê a função setup() onde é usada.

O objeto epd define o objeto do ecrã (epaper display). Deves definir este objeto para corresponder ao ecrã e-Paper que tens. Vê o Readme da biblioteca GxEPD2 e o ficheiro GxEPD2.h para exemplos.

Função initDisplay

A função initDisplay inicializa o ecrã, define a orientação para landscape, o tamanho do texto para 1 e a cor do texto para preto.

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

Se a variável initial for verdadeira, o ecrã e-Paper fará uma atualização completa quando o ESP32 acordar do deep-sleep. Para evitar isso, devemos definir a variável initial para falso. Mas só queremos fazer isso após o primeiro despertar, que é o que a função wakeups == 0 faz.

Função initTime

A função initTime apenas define o TIMEZONE. Tens de garantir que esta função é executada sempre que o ESP32 acorda, caso contrário o relógio vai estar errado.

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

Função syncTime

A função syncTime cria uma ligação WiFi e depois sincroniza o relógio interno do ESP32 com o servidor SNTP “pool.ntp.org” chamando configTzTime().

Podes especificar outros, ou mesmo múltiplos servidores SNTP para ligar. Vê o tutorial How to synchronize ESP32 clock with SNTP server para mais informações.

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

Também definimos a variável syncing para verdadeiro, que é usada na função drawTime para mostrar que uma sincronização de hora foi feita. No próximo despertar (a cada 30 segundos), a variável syncing é definida de novo para falso.

Função drawBackground

A função drawBackground faz uma atualização completa (setFullWindow) do ecrã e-Paper e simplesmente preenche o ecrã com branco. Podes também adicionar texto estático, gráficos ou outras informações que raramente mudam.

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

Como a atualização completa é lenta e provoca muito piscar, é chamada apenas ocasionalmente – essencialmente para evitar o “burn in” das imagens residuais da atualização parcial. O Waveshare Manual tem mais informações sobre isto.

Função drawTime

A função drawTime faz uma atualização parcial (setPartialWindow), que é muito mais rápida que a atualização completa e, mais importante, atualiza o conteúdo sem piscar.

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

Simplesmente obtém a hora local e depois imprime a hora e a data com diferentes fontes em posições específicas do cursor no ecrã. Se quiseres imprimir outras ou informações adicionais, por exemplo segundos, aqui estão todos os valores disponíveis no tipo de dados 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

Função setup

A função setup é executada sempre que o ESP32 acorda. Começa por inicializar o ecrã e definir a zona horária conforme descrito acima.

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

Depois executa ações diferentes, dependendo de quantas vezes o ESP32 foi acordado, contado na variável wakeups.

Especificamente, sincroniza a hora a cada 50º despertar. Como a duração do deep-sleep está definida para 30 segundos, isto significa que sincronizamos a cada 30s * 50 / 60s = 25 minutos. Podes alterar isto, mas quanto mais vezes sincronizares, maior o consumo da bateria. Por outro lado, queremos sincronizar pelo menos uma vez por hora para não perder a mudança entre horário de verão e horário padrão.

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

A cada 2000 despertares fazemos uma atualização completa (drawBackground) para remover imagens residuais deixadas pela atualização parcial que desenha a hora e data (drawTime). Novamente, podes fazer isto mais ou menos vezes. Como está, 30s * 2000 / 60s / 60min resulta numa atualização completa a cada 16,6 horas.

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

Para evitar overflow de inteiro, incrementamos wakeups até um máximo de 100000 antes de reiniciar. Lembra que a variável wakeups é guardada na memória RTC e mantém o seu valor no deep-sleep.

wakeups = (wakeups + 1) % 100000;

Finalmente, chamamos drawTime para atualizar a exibição da hora e data e depois colocamos o ecrã em hibernação para poupar bateria.

Depois colocamos o ESP32 em deep sleep por 30 segundos. Isto significa que o ecrã é atualizado a cada 30 segundos. Podes ir um pouco mais devagar, por exemplo 45 segundos, ou mais rápido, a cada 10 segundos, por exemplo. Como antes, é um compromisso entre um ecrã muito reativo e a preservação da bateria.

Os intervalos de despertar e atualização são escolhidos para seguir as recomendações para ecrãs e-Paper no Waveshare Manual. Para mais informações, vê também o tutorial Partial Refresh of e-Paper Display.

Conclusões

Neste tutorial aprendeste a construir um relógio digital que sincroniza a hora com um servidor SNTP e mostra sempre a hora e data corretas num ecrã e-Paper. O relógio utiliza a capacidade de deep-sleep do ESP32 para reduzir o consumo de energia. Isto permite que o relógio funcione por muito tempo com bateria.

Se usares o ESP32 LOLIN Lite sugerido, ligar uma bateria LiPo recarregável é simples. Também recomendaria Monitor Battery Levels with a MAX1704X e talvez mostrar um pequeno símbolo de bateria para visualizar o nível de carga.

Outra extensão comum para relógios digitais é também mostrar a temperatura ambiente ou dados meteorológicos. Vê o tutorial Weather Station on e-Paper Display para mais detalhes.

E se quiseres um relógio analógico em vez de digital, vê o nosso tutorial Analog Clock on e-Paper Display, que é muito semelhante a este.

Se tiveres algum comentário, sente-te à vontade para deixar na secção de comentários.

Boas construções ; )