In this tutorial you will learn how to implement a digital clock on a CrowPanel 1.28″ Round Display with a GC9A01 TFT using the Adafruit_GC9A01A library. The example code for the clock will also show you how to use the CST816D touch screen, the buzzer, the vibration motor, the BM8563 Real Time Clock (RTC), deep sleep and internet time synchronization.
Required Parts
Obviously, you will need the CrowPanel ESP32 1.28-inch Round Display module for this project. Apart from a USB-C cable, which typically comes with the Display module no other parts are needed.
CrowPanel ESP32 1.28″ Round Display
Features of the CrowPanel 1.28″ Round Display
The CrowPanel 1.28″ module is composed of a round TFT display (GC9A01) with 240×240 pixels resolution, a capacitive touch screen (CST816D) and an integrated ESP32-C3. The picture below shows the front of the module:
Apart from the ESP32, the module contains a buzzer, vibration motor, a real-time-clock (BM8563) with backup battery, support for a LiPo battery with charging and several buttons. The following picture shows the back of the module, where you can see most of the parts:
Note that there is also small encoder with button support (on the left), which essentially allows you to add a crown to set the time as you would for a mechanical wrist watch – provided you write the code for it. But since it is fully programmable you could control all kinds of other functions or user interface interactions with it as well.
For programming (and power supply) there is a USB-C port and there are Reset and Boot buttons at the back. If a LiPo battery is connected, it will be charged over the USB-C port.
The following table summarizes the main features of the display module:
Main Chip | ESP32-C3 |
Processor | 32-bit RISC-V single-core processor, up to 160 MHz |
Memory | 384 KB ROM400 KB SRAM (16 KB for cache)8 KB SRAM in RTC |
Size | 1.28 inch |
Resolution | 240*240 |
Signal Interface | SPI |
Touch Type | Capacitive Touch |
Panel Type | TFT LCD, IPS Panel |
Color Depth | 262K |
Brightness | 350 cd/m² |
Viewing Angle | 178° |
Button | Rest Button, Boot Button, Custom Button |
Interface | Type-C Interface, Battery Interface |
Encoder | With Button Function, Without Pin (Pin Size: 0.8mm*0.8mm) |
Operation Power | Module: DC5VMain Chip: 3.3V |
Active Area | 32.51*32.51mm |
Dimensions | 42*42*9.8mm |
Net Weight | 15g |
Digital Clock on CrowPanel 1.28″ Round Display
In this section we will implement a Digital Clock on the CrowPanel 1.28″ Round Display. The clock will show the name of the current day, the time and the date. We also will be able to toggle between 24h and 12h format by touching a button on the screen. The image below shows the clock face with its features.
In addition, the clock can be switched to deep-sleep mode by pressing the physical button on the right (back) of the module. A second press will wake the clock up again. This is essential, if you want to run the clock on battery power.
We also will sound a tone every hour and will provide tactile feed back when the touch button is pressed by activating the vibration motor.
Finally, the clock will automatically synchronize the internal Real Time Clock (RTC) with an internet time provider server (SNTP) if WiFi is available.
With that we will have explored most of the technical features of the CrowPanel Display – apart from Bluetooth and the Encoder.
Code for Digital Clock on GC9A01 Display
The display of the CrowPanel 1.28″ is a GC9A01 display with a CST816D touchscreen. We will use the Adafruit_GC9A01A library for drawing on the display, since it is much simpler than the LVGL library that is used for the demo code that comes with the CrowPanel display.
However, if you want to implement a more complex User Interface with drop-down menus and other features the LVGL library is the way to go.
Below is the code for the Digital Clock. It is a larger piece of code and I suggest you just browse it to get a rough overview. We will discuss the details next.
#include "WiFi.h" #include "Adafruit_GFX.h" #include "Adafruit_GC9A01A.h" #include "I2C_BM8563.h" #include "CST816D.h" // I2C #define SDA 4 #define SCL 5 #define PI4IO_I2C_ADDR 0x43 // TFT display #define TFT_CS 10 #define TFT_DC 2 #define TFT_MOSI 7 #define TFT_SCLK 6 #define TFT_RST 4 #define TFT_BKL 2 #define DW 240 #define DH 240 // Touch panel #define TP_INT 0 #define TP_RST 3 #define BUTTON 1 #define BUZZER 3 #define MOTOR 0 const char* SSID = "SSID"; const char* PWD = "PASSWORD"; bool is_24h = true; const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3"; const char* DAYSTR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; const char* MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; // touch button const int bx = DW / 2; const int by = DH - 30; const int br = 10; Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK); I2C_BM8563 rtc(I2C_BM8563_DEFAULT_ADDRESS, Wire); CST816D touch(SDA, SCL, TP_RST, TP_INT); void init_ioex() { Wire.begin(SDA, SCL); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x01); // test register Wire.endTransmission(); Wire.requestFrom(PI4IO_I2C_ADDR, 1); uint8_t rxdata = Wire.read(); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x03); Wire.write((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4)); Wire.endTransmission(); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x07); Wire.write(~((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4))); Wire.endTransmission(); } void write_ioex(uint8_t pin_number, bool value) { Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x05); // test register Wire.endTransmission(); Wire.requestFrom(PI4IO_I2C_ADDR, 1); uint8_t rxdata = Wire.read(); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x05); // Output register if (!value) Wire.write((~(1 << pin_number)) & rxdata); else Wire.write((1 << pin_number) | rxdata); Wire.endTransmission(); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x05); // test register Wire.endTransmission(); Wire.requestFrom(PI4IO_I2C_ADDR, 1); rxdata = Wire.read(); } void set_rtc_time() { rtc.begin(); struct tm t; getLocalTime(&t); I2C_BM8563_TimeTypeDef ts; ts.hours = t.tm_hour; ts.minutes = t.tm_min; ts.seconds = t.tm_sec; rtc.setTime(&ts); I2C_BM8563_DateTypeDef ds; ds.weekDay = t.tm_wday; ds.month = t.tm_mon + 1; ds.date = t.tm_mday; ds.year = t.tm_year + 1900; rtc.setDate(&ds); } void sync_time() { WiFi.begin(SSID, PWD); for (int i = 0; (i < 5) && (WiFi.status() != WL_CONNECTED); i++) delay(100); if (WiFi.status() == WL_CONNECTED) { configTzTime(TIMEZONE, "pool.ntp.org"); set_rtc_time(); } } void display_time() { static char buf[16]; static I2C_BM8563_DateTypeDef ds; static I2C_BM8563_TimeTypeDef ts; rtc.getDate(&ds); rtc.getTime(&ts); tft.setTextColor(GC9A01A_LIGHTGREY, GC9A01A_BLACK); tft.setCursor(80, 60); tft.setTextSize(4); tft.print(DAYSTR[ds.weekDay]); if (is_24h) { sprintf(buf, "%02d:%02d:%02d", ts.hours, ts.minutes, ts.seconds); } else { int display_hours = ts.hours % 12; display_hours = display_hours ? display_hours : 12; const char* period = ts.hours >= 12 ? "pm" : "am"; sprintf(buf, "%02d:%02d %s", display_hours, ts.minutes, period); } tft.setTextColor(is_24h ? GC9A01A_WHITE : GC9A01A_YELLOW, GC9A01A_BLACK); tft.setCursor(45, 110); tft.setTextSize(3); tft.print(buf); sprintf(buf, "%2d %s %04d", ds.date, MONTHSTR[ds.month], ds.year); tft.setTextColor(GC9A01A_WHITE, GC9A01A_BLACK); tft.setCursor(45, 150); tft.setTextSize(2); tft.print(buf); } void sound_hour() { static I2C_BM8563_TimeTypeDef ts; rtc.getTime(&ts); if (ts.minutes == 0 && ts.seconds == 0) { tone(BUZZER, 1000); delay(50); tone(BUZZER, 0); delay(500); } } void check_touch() { uint8_t gesture; uint16_t x, y; bool touched = touch.getTouch(&x, &y, &gesture); if (touched) { if (abs(x - bx) < 2*br && abs(y - by) < 2*br) { write_ioex(MOTOR, true); delay(50); write_ioex(MOTOR, false); is_24h = !is_24h; display_touch(); delay(500); } } } void display_touch() { if (is_24h) { tft.fillCircle(bx, by, br, GC9A01A_BLACK); tft.drawCircle(bx, by, br, GC9A01A_DARKGREY); } else { tft.fillCircle(bx, by, br, GC9A01A_DARKGREY); } } void check_button() { if (digitalRead(BUTTON) == LOW) { tft.fillScreen(GC9A01A_BLACK); write_ioex(TFT_BKL, false); // Switch off TFT backlight delay(300); esp_deep_sleep_start(); } } void init_deep_sleep() { pinMode(BUTTON, INPUT); esp_deep_sleep_enable_gpio_wakeup(1ULL << BUTTON, ESP_GPIO_WAKEUP_GPIO_LOW); } void init_buzzer() { pinMode(BUZZER, OUTPUT); digitalWrite(BUZZER, LOW); } void init_tft() { tft.begin(); tft.setRotation(0); tft.fillScreen(GC9A01A_BLACK); display_touch(); } void set_ioex_pins() { write_ioex(TFT_RST, true); // TFT Reset write_ioex(TP_RST, true); // Touch panel reset write_ioex(TFT_BKL, true); // TFT backlight } void setup() { Serial.begin(115200); init_ioex(); set_ioex_pins(); touch.begin(); init_buzzer(); init_deep_sleep(); sync_time(); init_tft(); } void loop() { static unsigned long st = millis(); if (millis() - st > 300) { display_time(); sound_hour(); st = millis(); } check_touch(); check_button(); yield(); }
Let’s break down the code into its components for a clearer understanding.
Libraries
The code begins by including necessary libraries for WiFi, graphics, display control, real-time clock (RTC), and touch panel functionality.
#include "WiFi.h" #include "Adafruit_GFX.h" #include "Adafruit_GC9A01A.h" #include "I2C_BM8563.h" #include "CST816D.h"
You will have to install the Adafruit_GC9A01A library. Just open the Library Manager, search for “Adafruit_GC9A01A” and press “INSTALL”. The picture below shows you how that should look like once installed:
In addition, you will need the files for the Real Time Clock I2C_BM8563
and the touch screen CST816D
. Download the Arduino Demo Code, unzip it and go the folder ESP32_1.28_Arduino_Demo
. It should contain the following subfolders:
In the subfolders LvglWidgets
and RTC
you will find the required files. Copy them into the project folder for your digital clock code. The project folder content should look like this:
As you can see, I named the Arduino sketch “CrowPanel-Digital-Clock-1-28-Inch.ino” and since the folder name must match the sketch it is “CrowPanel-Digital-Clock-1-28-Inch”. But you can name it differently, just make sure sketch and folder name match.
However, to make things simple, I also zipped my project and you can download it using the following link:
Constants
Next, we define constants for I2C communication, TFT display pins, and touch panel pins.
#define SDA 4 #define SCL 5 #define PI4IO_I2C_ADDR 0x43 #define TFT_CS 10 #define TFT_DC 2 #define TFT_MOSI 7 #define TFT_SCLK 6 #define TFT_RST 4 #define TFT_BKL 2 #define DW 240 #define DH 240 // Touch panel #define TP_INT 0 #define TP_RST 3 #define BUTTON 1 #define BUZZER 3 #define MOTOR 0
You can find these pin definitions in the example code and in the Schematics of the CrowPanel 1.28″ Display. Especially of interest is the section below that shows the TFT and Touch Panel Connector:
It is important to note that the CrowPanel module contains a GPIO Expander (PI4IOE5V6408), since the TFT display utilizes most of the pins of the ESP32. Which means that some pins are GPIO pins of the ESP32, e.g. (BUZZER
) and others are controlled via the GPIO Expander. Specifically, LCD_RESET
(TFT_RST=4
), TP_RESET
(TP_RST=3
), the backlight LED_P2 (TFT_BKL=2
) and the vibration motor MOTOR_P0
(MOTOR=0
). Have a look at the schematics for the GPIO Expander below:
You will see later in the code that ESP32 pins and GPIO Expander pins need to controlled differently.
Global Variables
We declare several global variables, including WiFi credentials, timezone settings, and arrays for day and month names. We also define a boolean variable to toggle between 12-hour and 24-hour formats.
const char* SSID = "SSID"; const char* PWD = "PASSWORD"; bool is_24h = true; const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3"; const char* DAYSTR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; const char* MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
Note that you will have to replace the WiFi credentials (SSID
, PWD
) with the credentials for your WiFi network.
Most likely, you also want to change the TIMEZONE
. I am using AEST-10AEDT,M10.1.0,M4.1.0/3
, which corresponds to Melbourne, Australia. This string indicates the standard time offset and the rules for daylight savings time.
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 the string you find there and change the TIMEZONE
constant accordingly.
The remaining constants are just strings for the names of days and months. You could replace them with strings for a different language, for instance. But you will have to keep the lengths similar.
Initialization Functions
The code contains several functions to initialize various components. The init_ioex()
function sets up I2C communication and configures the I/O expander.
void init_ioex() { Wire.begin(SDA, SCL); Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x01); // test register ... }
This code originates from the demo code file LvglWidgets.ino
. I changed the name slightly and removed some print statements but otherwise it is the same code.
Same for the releated write_ioex()
function below, which allows you to write to the GPIO pins of the IO Expander:
void write_ioex(uint8_t pin_number, bool value) { Wire.beginTransmission(PI4IO_I2C_ADDR); Wire.write(0x05); // test register ... }
The init_tft()
function sets the default settings for drawing on the TFT display and also draws the white circle that represents the touch button by calling display_touch()
:
void init_tft() { tft.begin(); tft.setRotation(0); tft.fillScreen(GC9A01A_BLACK); display_touch(); }
The init_deep_sleep()
sets up the button that wakes up the ESP32 from deep sleep:
void init_deep_sleep() { pinMode(BUTTON, INPUT); esp_deep_sleep_enable_gpio_wakeup(1ULL << BUTTON, ESP_GPIO_WAKEUP_GPIO_LOW); }
And finally, the init_buzzer()
function sets up the buzzer pin.
void init_buzzer() { pinMode(BUZZER, OUTPUT); digitalWrite(BUZZER, LOW); }
RTC and Time Synchronization
The set_rtc_time()
function initializes the RTC and sets the current time and date based on the local time retrieved from the WiFi connection.
void set_rtc_time() { rtc.begin(); struct tm t; getLocalTime(&t); // Set RTC time and date... }
That local time is synchronized with an internet time provider (SNTP server) via the sync_time()
function:
void sync_time() { WiFi.begin(SSID, PWD); for (int i = 0; (i < 5) && (WiFi.status() != WL_CONNECTED); i++) delay(100); if (WiFi.status() == WL_CONNECTED) { configTzTime(TIMEZONE, "pool.ntp.org"); set_rtc_time(); } }
Note that the function tries 5 times to connect to the WiFi and then gives up. This is on purpose! If you use the clock away from your WiFi network it cannot connect and synchronize when restarted. That is fine, since the RTC will keep track of the time.
However, since we only synchronize when resetting the ESP32 and since the RTC has no concept of daylight savings times, the clock will miss the switch from DST and ST and vice versa. There are ways around this. Have a look at the Real-Time-Clock DS3231 with ESP32 tutorial.
Displaying Time
The display_time()
function retrieves the current time and date from the RTC and displays it on the TFT screen at the appropriate locations. Depending on the status of the is_24h flag, it formats the time in 12-hour or 24-hour.
void display_time() { static char buf[16]; static I2C_BM8563_DateTypeDef ds; static I2C_BM8563_TimeTypeDef ts; ... if (is_24h) { sprintf(buf, "%02d:%02d:%02d", ts.hours, ts.minutes, ts.seconds); } else { int display_hours = ts.hours % 12; display_hours = display_hours ? display_hours : 12; const char* period = ts.hours >= 12 ? "pm" : "am"; sprintf(buf, "%02d:%02d %s", display_hours, ts.minutes, period); } ... }
Note that you cannot easily use a different font to display time and date. The Adafruit_GC9A01A library allows you to set a background color when printing text, which clears the previously printed time and date.
tft.setTextColor(GC9A01A_WHITE, GC9A01A_BLACK); ... tft.print(buf);
However, this only works for the default font but not for proportional fonts. For more information see the Adafruit GFX Graphics Library Documentation. A way around this would be to use a Canvas, but that is more complex and the refresh of the TFT display might be slow.
Touch Button
The check_touch()
function detects touch events on the touch panel. If the touch is close to the defined button area (the white circle), it switches on the vibration motor for a short period to provide tactile feedback.
void check_touch() { uint8_t gesture; uint16_t x, y; bool touched = touch.getTouch(&x, &y, &gesture); if (touched) { if (abs(x - bx) < 2*br && abs(y - by) < 2*br) { write_ioex(MOTOR, true); delay(50); write_ioex(MOTOR, false); is_24h = !is_24h; display_touch(); delay(500); } } }
It then toggles the time format and updates the display. The following picture shows the two states of the display:
The little white circle at the bottom of the screen represents the touch button and is drawn by the display_touch()
function. Depending on its state it is either drawn as a filled or empty circle:
void display_touch() { if (is_24h) { tft.fillCircle(bx, by, br, GC9A01A_BLACK); tft.drawCircle(bx, by, br, GC9A01A_DARKGREY); } else { tft.fillCircle(bx, by, br, GC9A01A_DARKGREY); } }
Deep Sleep Button
The check_button()
function checks if the physical button at the side of the module is pressed. If so, it puts the device into deep sleep mode. Most importantly, it also switches of the backlight LED for the TFT display to save battery.
void check_button() { if (digitalRead(BUTTON) == LOW) { tft.fillScreen(GC9A01A_BLACK); write_ioex(TFT_BKL, false); // Switch off TFT backlight delay(300); esp_deep_sleep_start(); } }
A second press on the button wakes the ESP32 up again. Note that this button has an internal pullup resistor and therefore goes LOW
when pressed. See the schematics below:
Setup Functions
The setup()
function initializes the serial communication, I/O expander, touch panel, buzzer, and TFT display, and synchronizes the time.
void setup() { Serial.begin(115200); init_ioex(); ... }
Which means, the clock is only synchronized with the internet time when restarted. This saves battery power, since you can shutdown the WiFi after a restart. However, you could decide to synchronize more frequently, for instance, once an hour to catch the change to day light savings time.
Loop Function
The loop()
function continuously updates the display of time and date every 300 milliseconds and checks for touch events and the button state. You could add a short delay of 50ms instead of the yield()
function call.
void loop() { static unsigned long st = millis(); if (millis() - st > 300) { display_time(); sound_hour(); st = millis(); } check_touch(); check_button(); yield(); }
Demo of the Digital Clock
The following short video clip shows the clock in action. You can see the time running, the toggle between 24h and 12h format when the touch button is pressed, and the deep sleep mode:
And there you have it! We have explored most of the features of the CrowPanel 1.28″ Display and you should now be at a good starting point to implement your own clock with it’s unique features.
Conclusions
In this tutorial you learnt how to implement a digital clock on a CrowPanel 1.28″ Display Module. We used many features of the display module, including the touch screen, the buzzer, the vibration motor and the RTC. We also learned how to synchronize the RTC with an internet time provider, and how to put the clock into deep sleep mode.
If you want to learn more about time synchronization, have a look at the How to synchronize ESP32 clock with SNTP server tutorial, and the Real-Time-Clock DS3231 with ESP32 tutorial will help you to implement support for daylight savings time.
And, if you want to implement an Analog Clock, the Analog Clock on e-Paper Display will have some useful information, despite aimed at an E-Paper and not a TFT display.
Otherwise, there are many, many functions you could add to your clock. An obvious one, would be the display of weather information. Have a look at the Simple ESP32 Internet Weather Station tutorial, for an example. There is also some information on how to connect the CrowPanel Module to Home Assistant.
If you have any questions feel free to leave them in the comment section.
Have fun and happy tinkering ; )
Links
Below some links I found useful when writing this tutorial:
CrowPanel ESP32 1.28-inch Round Display
Data sheets
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.