In this tutorial you will learn how to build a Digital clock using an e-Paper display and an ESP32. You will also learn how to synchronize the clock with an internet time provider (SNTP server). Finally, you will learn how to combine the these functions with the deep-sleep capability of the ESP to increase the running time on battery power.
Let’s start with the required parts.
Required Parts
For the parts, I am recommending an older ESP32 lite here, which has been deprecated but you can still get it for cheap. There is a successor model with improved specs. But any other ESP32, ESP8266 with sufficient memory will work as well.
2.9″ e-Paper Display
ESP32 lite
USB Data Cable
Dupont Wire Set
Breadboard
Makerguides.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to products on Amazon.com. As an Amazon Associate we earn from qualifying purchases.
E-paper Display
The e-Paper display used in this project is a 2.9″ display module, with 296×128 pixels resolution and an embedded controller with an SPI interface.
Note that this module has a little jumper pad/switch that allows you to switch from 4-wire SPI to 3-wire SPI. We are going to use the default 4-wire SPI here.
The display module runs on 3.3V or 5V, has a very low sleep current of 0.01µA and consumes only about 26.4mW while refreshing. For more information on e-Paper displays have a look at the following two tutorials: Interfacing Arduino To An E-ink Display and Weather Station on e-Paper Display.
Connecting and Testing the e-Paper
First, let us connect and test the function of the e-Paper. The following picture shows the complete wiring for power and SPI.
Below a table with all the connections for convenience. Note that you can power the display with 3.3V or 5V but the ESP32-lite has only a 3.3V output and the SPI data lines must be 3.3V!
e-Paper display | ESP32 lite |
---|---|
CS/SS | 5 |
SCL/SCK | 18 |
SDA/DIN/MOSI | 23 |
BUSY | 15 |
RES/RST | 2 |
DC | 0 |
VCC | 3.3V |
GND | G |
You can use a breadboard to wire everything up. But I actually connected the display directly to the ESP32 with Dupont wires and also added a LiPo battery to test running the clock on battery power.
Install GxEPD2 library
Before we can draw or write on the e-Paper display we need to install two libraries. The Adafruit_GFX library is a graphics library that provides a common set of graphics primitives (text, points, lines, circles, etc.). And the GxEPD2 library provides the graphics driver software to control an E-Paper Display via SPI.
Just install the libraries the usual way. After the installation they should appear in the Library Manager as follows.
Test code
Once you have installed the library run the following test code to ensure that everything works.
#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() {}
The text code prints the text “Makerguides” and after some flickering of the display (full refresh) you should see it on your display:
If not, then something is wrong. Most likely either the display is not correctly wired or the wrong display driver is chosen. The critical line of code is this:
GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));
The Readme for GxEPD2 library lists all the supported displays and you can find the specifics in the header files, e.g. GxEPD2.h. Find the display driver specific for your display. This may take some trial and error.
Code for a Digital Clock on e-Paper
If the test code above works then you should have not problems with the code below either. It is the complete code for a digital clock that automatically synchronizes its time with an internet time server (SNTP) and displays time and date on an e-Paper display.
Between display updates the ESP32 and the display are put into deep-sleep mode to preserve battery power. Have a quick look at the complete code first, and then we will go into it’s details:
#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() {}
If you upload the code to your ESP32, you should see time and date being displayed on your e-Paper as shown below:
Libraries
We start by defining the constant ENABLE_GxEPD2_GFX
as 0. If set to 1, it enables the base class GxEPD2_GFX to pass pointers to the display instance as parameter. But it uses ~1.2k more code and we don’t need it, so it is set to 0.
#define ENABLE_GxEPD2_GFX 0
Next we include the GxEPD2_BW.h
header file for the black and white (BW) e-Paper display. If you have a 3-color display you would include GxEPD2_3C.h
, or GxEPD2_4C.h
for a 4-color display, and GxEPD2_7C.h
for a 7-color display, instead.
#include "GxEPD2_BW.h" #include "Fonts/FreeSans12pt7b.h" #include "Fonts/FreeSansBold24pt7b.h"
We also include two files for the fonts used to display the time and date. We want to show the time in a bigger, 24pt, bold, font (FreeSansBold24pt7b
) and the date in a smaller, 7pt font (FreeSans12pt7b
). You can find an overview of the AdaFruit GFX fonts here.
Finally, we include the WiFi.h
and the esp_snt.h
libraries, which we will need to synchronize the clock with an SNTP internet time server. More about that later.
#include "WiFi.h" #include "esp_sntp.h"
Constants
The following three constants you will have to change. First the SSID
and PASSWORD
for your WiFi and then the TIMEZONE
you are living in.
const char* SSID = "SSID"; const char* PWD = "PASSWORD"; const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
The above time zone specification “AEST-10AEDT,M10.1.0,M4.1.0/3
” is for Australia, which corresponds to the Australian Eastern Standard Time (AEST) with daylight saving time adjustments.
The parts of this time zone definition are as follows
- AEST: Australian Eastern Standard Time
- -10: UTC offset of 10 hours ahead of Coordinated Universal Time (UTC)
- AEDT: Australian Eastern Daylight Time
- M10.1.0: Transition to daylight saving time occurs on the 1st Sunday of October
- M4.1.0/3: Transition back to standard time occurs on the 1st Sunday of April, with a 3-hour difference from UTC.
For other time zone definitions have a look at the Posix Timezones Database. Just copy and paste the string you find there and change the TIMEZONE
constant accordingly.
In addition to time and date, we also want to show the name of the current day and month. The following constants define those names. You could change the language, or pick longer names. Just make sure they fit on the display.
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" };
Finally, we define constants for the width W
and height H
of the display. This is mostly for convenience. Note that width and hight are flipped, since we rotate the display (setRotation(1)
) in the initDisplay
function.
// W, H flipped due to setRotation(1) const int W = GxEPD2_290_BS::HEIGHT; const int H = GxEPD2_290_BS::WIDTH;
Variables and Objects
Next we define a few global variables and objects.
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));
The syncing
variable indicates whether the clock is currently synchronizing its time with the SNTP server. If that happens, the display will show a ‘*’ character after the time. Have a look at the drawTime()
function where this variable is used. You don’t strictly need this but it is nice for debugging to see if the time synchronization actually works.
The wakeups
variable gets incremented whenever the ESP32 wakes up from deep-sleep. It is used to decided whether to perform a full refresh of the display or to synchronize the time. Have a look at the setup()
function, where it is used.
The epd
object defines the display object (e–paper display). You must define this object to match the e-Paper display you have. See the Readme for GxEPD2 library and the GxEPD2.h file for examples.
initDisplay Function
The initDisplay
function initializes the display, set the display orientation to landscape, the text size to 1 and the text color to black.
void initDisplay() { bool initial = wakeups == 0; epd.init(115200, initial, 50, false); epd.setRotation(1); epd.setTextSize(1); epd.setTextColor(GxEPD_BLACK); }
If the initial
variable is true, the e-Paper display will perform a full refresh when the ESP32 wakes up from deep-sleep. To avoid this, we must set the initial
variable to false. But we only want to do this after the first wake-up, which is what wakeups == 0
achieves.
initTime Function
The initTime function just sets the TIMEZONE
. You have to make sure that this functions runs every time the ESP32 wakes up, otherwise your clock will be off.
void initTime() { setenv("TZ", TIMEZONE, 1); tzset(); }
syncTime Function
The syncTime
function creates a WiFi connection and then synchronizes the internal clock of the ESP32 with the SNTP server “pool.ntp.org
” by calling configTzTime()
.
You can specify other, or even multiple SNTP servers to connect to. Have a look at the How to synchronize ESP32 clock with SNTP server tutorial for more information.
void syncTime() { WiFi.begin(SSID, PWD); while (WiFi.status() != WL_CONNECTED) ; configTzTime(TIMEZONE, "pool.ntp.org"); syncing = true; }
We also set the syncing
variable to true, which is used in drawTime
to show that a time synchronization was preformed. At the next wake-up (every 30 seconds), the syncing
variable is set back to false.
drawBackground Function
The drawBackground
function performs a full refresh (setFullWindow
) of the e-Paper display and simply fills the screen with white. You could also add static text, graphics or other information that rarely changes.
void drawBackground(const void* pv) { epd.setFullWindow(); epd.fillScreen(GxEPD_WHITE); }
Since the full refresh is slow and comes with a lot of flickering, it is called only occasionally – essentially to avoid a “burn in” of the after-images from the partial refresh. The Waveshare Manual has more information on this.
drawTime Function
The drawTime
function performs a partial refresh (setPartialWindow
), which is much faster than a full refresh and most importantly updates content without any flickering.
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); }
It simply gets the local time and then prints the time and date information with different fonts at specific cursor positions on the screen. If you want to print other or additional time information, e.g. seconds here are all the values available in the tm struct
data type:
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
setup Function
The setup
function is executed every time the ESP32 wakes up. It starts by initializing the display and setting the time zone as described above.
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(); }
After that it performs different actions, depending how often the ESP32 was woken up as counted in the wakeups
variable.
Specifically, it synchronizes the time every 50ths wakeup. Since the deep-sleep duration is set to 30 seconds, this means we are synchronizing every 30sec * 50 / 60 sec = 25 minutes. You can change this but the more often you synchronize the higher the battery consumption. On the other hand, we want to synchronize at least once an hour to not to miss the switch between daylight savings and standard time.
if (wakeups % 50 == 0) syncTime();
Every 2000 wakeups we perform a full refresh (drawBackground
) to remove after-images left by the partial refresh operation that draws the time and date (drawTime
). Again, you can do this more or less often. As it is, 30sec * 2000 / 60 sec / 6o mins result in a full refresh every 16.6 hours.
if (wakeups % 2000 == 0) epd.drawPaged(drawBackground, 0);
To avoid an integer overrun we increment wakeups
to maximum of 100000 before it resets. Remember that the wakeups variable is stored in RTC memory and retains its value in deep-sleep.
wakeups = (wakeups + 1) % 100000;
Finally, we call drawTime
to refresh the time and date display and then hibernate the e-Paper to save battery power.
After that we put the ESP32 for 30 seconds into deep sleep. This means the display updates every 30 seconds. You could go a bit slower, e.g. 45 second or a bit faster, every 10 seconds for instance. As before, it is a trade-off between a very reactive display of time or the preservation of battery power.
The wakeup and refresh intervals are chosen to follow the recommendations for e-Paper displays in the Waveshare Manual. For more information also have a look at the Partial Refresh of e-Paper Display tutorial.
Conclusions
In this tutorial you learned how to build a digital clock that synchronizes its time with an SNTP sever and shows always accurate time and date on an e-Paper display. The clock utilizes the deep-sleep capability of the ESP32 to reduce power consumption. This allows you to run this clock for a long time on a battery.
If you use the suggested ESP32 LOLIN Lite, connecting a rechargeable LiPo battery is simple. I would also recommend to Monitor Battery Levels with a MAX1704X and maybe display a little battery symbol to visualize the charge level.
Another common extensions for digital clocks is to also display ambient temperature or weather data. Have a look at the Weather Station on e-Paper Display tutorial for more details.
And if you want to have an analog instead of a digital clock have a look at our Analog Clock on e-Paper Display tutorial, which is very similar to this one.
Happy Tinkering ; )
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.