In this project, we will build a digital clock that combines internet precision with the stability of a physical time module. Upon startup, the clock connects to a WiFi network and retrieves the exact time via the NTP protocol. To ensure the clock does not lose time during a connection outage, it uses a DS3231 RTC module, which maintains the time using a battery backup. The data is displayed on a high-resolution 2.0-inch TFT screen.
When using the Arduino Nano ESP32, we must pay attention to two things:
Correct wiring is the key to the stable operation of your smart clock. This project uses two different communication protocols: SPI for fast data rendering on the TFT screen and I2C for reading time from the RTC module. Pay special attention to the power pins; the Arduino Nano ESP32 operates on 3.3V logic, so all components are powered from the corresponding output on the board. Follow the table below for the exact pin layout to enable coordinated operation of all components.
| TFT Display / Pin | Arduino Nano ESP32 Pin |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL | D13 |
| SDA | D11 |
| CS | D10 |
| DC | D9 |
| RST | D8 |
| RTC DS3231 / Pin | Arduino Nano ESP32 Pin |
|---|---|
| VCC | Vin 5V |
| GND | GND |
| SDA | A4 |
| SCL | A5 |
The program code is the heart of this project and handles three main tasks: establishing a WiFi connection, synchronizing with world time (NTP), and securely storing this data in the local RTC module. The code is designed to be optimized – the screen does not refresh entirely; instead, using the fillRect function, we clear and redraw only the parts (seconds, minutes, days) that have actually changed. This prevents unpleasant image flickering and ensures a smooth real-time display.
/*
* PROJECT: Arduino Smart Clock with WiFi & NTP Sync
* AUTHOR: Borut Bukovnik
* DATE: 2026-05-01
* DESCRIPTION: Digital clock with time updates via WiFi (NTP), backup with DS3231 RTC module, and TFT display.
* HARDWARE: Arduino Nano ESP32, DS3231, ST7789 TFT (2.0-inch)
*/
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <RTClib.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
// WiFi settings
const char* ssid = "ssid name";
const char* password = "wifi password";
// NTP settings
WiFiUDP ntpUDP;
// 7200 seconds = GMT+2 (Slovenia summer time)
NTPClient timeClient(ntpUDP, "pool.ntp.org", 7200, 60000);
// TFT settings (SPI)
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST 8
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// DS3231 settings (I2C)
#define I2C_SDA A4
#define I2C_SCL A5
RTC_DS3231 rtc;
// Variables for screen refresh
int previousSecond = -1;
int previousDay = -1;
unsigned long lastNtpSync = 0;
const unsigned long NTP_SYNC_INTERVAL = 600000; // 10 minutes
void setup() {
Serial.begin(115200);
Wire.setPins(I2C_SDA, I2C_SCL);
if (!rtc.begin()) {
Serial.println("RTC not found!");
while (1);
}
// Display initialization
tft.init(240, 320);
tft.setRotation(1);
tft.fillScreen(ST77XX_BLACK);
// WiFi connection
tft.setCursor(10, 100);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(2);
tft.print("Connecting to WiFi...");
WiFi.begin(ssid, password);
int timeout = 0;
while (WiFi.status() != WL_CONNECTED && timeout < 20) {
delay(500);
timeout++;
}
if (WiFi.status() == WL_CONNECTED) {
timeClient.begin();
timeClient.update();
rtc.adjust(DateTime(timeClient.getEpochTime()));
lastNtpSync = millis();
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_GREEN);
tft.setCursor(10, 200);
tft.print("WiFi: Connected");
} else {
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_RED);
tft.setCursor(10, 200);
tft.print("WiFi: Offline (RTC)");
}
delay(1000);
}
void loop() {
// NTP synchronization
if (WiFi.status() == WL_CONNECTED && (millis() - lastNtpSync >= NTP_SYNC_INTERVAL)) {
if(timeClient.update()) {
rtc.adjust(DateTime(timeClient.getEpochTime()));
lastNtpSync = millis();
}
}
DateTime now = rtc.now();
// Date display (only on day change)
if (now.day() != previousDay) {
tft.fillRect(0, 20, 320, 30, ST77XX_BLACK);
tft.setCursor(10, 20);
tft.setTextColor(ST77XX_YELLOW);
tft.setTextSize(2);
char dateBuf[20];
sprintf(dateBuf, "%02d.%02d.%04d", now.day(), now.month(), now.year());
tft.print(dateBuf);
previousDay = now.day();
}
// Time display (every second)
if (now.second() != previousSecond) {
tft.fillRect(0, 80, 320, 60, ST77XX_BLACK);
tft.setCursor(10, 80);
tft.setTextColor(ST77XX_CYAN);
tft.setTextSize(5);
char timeBuf[10];
sprintf(timeBuf, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
tft.print(timeBuf);
previousSecond = now.second();
}
delay(200);
}
In our code, it works like this:
WiFi and NTP are the managers that check every 10 minutes if the clock happens to be lagging by a millisecond and correct the RTC module. The RTC, however, is the worker that "toils" every second and sends data to the TFT screen.