How difficult is to build a simple NTP client for Digital Clocks

All digital clocks experience clock drift. They need to periodically synchronisation with reference clocks. Networked devices such as PCs and smartphones use Network Time Protocol (NTP) for clock synchronisation. Even with NTP it is not possible to get absolute synchronisation, there will be few milliseconds variance. In this post we will try to understand NTP client-server model and implement a simple client for ESP32 using Arduino.

ESP32 has WiFi so it can connect to a network and fetch the timestamp using NTP. When ESP32 first boots up it it will send a NTP request packet over UDP to the configured NTP server and uses the received timestamp to initialise the clock. Then ESP32 can continue to increment the seconds value every 1 second. We can also include logic to periodically resync the local time with NTP server.

There are several NTP servers that can be used for time synchronisation. These servers use atomic clocks, GPS etc to keep their clocks accurate. Currently NTP is at version 4. NTPv4 packet is made of multiple 32 bit words. Few components are marked with their size in bits if the size not 32 bits. For example all the timestamp fields are 64 bit length. If we exclude the optional words, then the packet size comes to 48 bytes. Both client and server uses same packet format.

NTP data format

The first word is describing many things, lets decode that first.

CodeDescriptionSize
LILeap indicator2 bits
VNNTP protocol version (4)3 bits
ModeMode (Clinet – 3, Server – 4 etc)3 bits
StratumStratum8 bits
PollPoll8 bits
PrecisionPrecision8 bits
NTP First word breakdown

Client mode with NTPv4

LI (0)VN (4)Mode (3)
00100011
NTPv4 first byte (0x23)

Client mode with NTPv3

LI (0)VN (3)Mode (3)
00011011
NTPv3 first byte (0x1B)

Since NTP is a network protocol, we need to account for network round trip delay to reduce variance. In our simple implementation we are not handling the round trip delay. we will directly use the timestamp provided by the server. “Transmit timestamp” will have time at which the packet departed the server. The transmit timestamp is made of two 32 bit integers, the first 32 bits represents the seconds from NTP epoch (01/01/1900) and second 32 bits represents the fraction.

NTP v4 timestamp format

The following python program send a request to NTP request to server and decodes the received the response packet.

import socket
import struct
import time
ntpOffset = 2208988800
host = 'time.google.com'
addr = socket.getaddrinfo(host, 123)[0][-1]
print(addr)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(1)
requestPacket = bytearray(48)
requestPacket[0] = 0x23
res = s.sendto(requestPacket, addr)
responsePacket = s.recv(48)
decodedRes =  struct.unpack('!4B2I4s8I',responsePacket)
print("".join("0x{:02x} ".format(x) for x in responsePacket))
print(decodedRes)
print(time.ctime(decodedRes[-2]-ntpOffset))
0x24 0x01 0x00 0xec 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x04 0x47 0x4f 0x4f 0x47 0xe9 0x7f 0xfa 0xc9 0xdc 0x57 0xac 0x0d 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xe9 0x7f 0xfa 0xc9 0xdc 0x57 0xac 0x0e 0xe9 0x7f 0xfa 0xc9 0xdc 0x57 0xac 0x10

(36, 1, 0, 236, 0, 4, b'GOOG', 3917478601, 3696733197, 0, 0, 3917478601, 3696733198, 3917478601, 3696733200)

Wed Feb 21 10:00:01 2024

We are sending a 48 byte packet to server on UDP port 123. The first byte 0x23 for v4 and client mode, remaining bytes can be 0s. The server will respond with 48 bytes packet of same format. The server time is encoded in last but one 32 bit word. Bytes at 40,41,42,43 represent a 32 bit integer (in network byte order). NTP epoch (01/01/1900) is different from unit epoch (01/01/1970), so we have to deduct the offset and pass the result to ctime to convert it to local time.

time.ctime(decodedRes[-2]-ntpOffset)

Arduino Code to get time from NTP server

The following arduino code is adopted from AsyncUDPClient arduino example. The arduino program is very similar to the python program. We connect to the target NTP server then register a callback method. The AsyncUDP library will call the callback method when there is new packet available. Once the callback is registered, we are sending the request packet.

As we have seen above, bytes from 40-44 will have the timestamp in seconds, we are extracting that into a 32 bit unsigned integer. From this timestamp we are computing hours,minutes and seconds.

Update the IPAddress in the arduino code with the one printed by the above python script. You can also get the list of available servers IPs by doing DNS lookup on time.google.com domain.

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "***";
const char * password = "***";

AsyncUDP udp;

void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.connect(IPAddress(216,239,35,0), 123)) {
        Serial.println("UDP connected");
        uint8_t packet[48]={0};
        packet[0] = 0x23;
        udp.onPacket([](AsyncUDPPacket pkt) {            
            // extract timestamp
            uint32_t ntpTimeStamp = (pkt.data()[40] << 24) + (pkt.data()[41] << 16) + (pkt.data()[42] << 8) + pkt.data()[43];
            int seconds = ntpTimeStamp%60;
            int mins = (ntpTimeStamp/60)%60;
            int hours = (ntpTimeStamp/3600)%24;
            char printBuffer[100];
            sprintf(printBuffer,"Ticks %u, Hour %d, Min %d, Sec %d",ntpTimeStamp,hours,mins,seconds);
            Serial.println(printBuffer);
        });
        //Send unicast
        udp.write(packet,48);
        Serial.println("Request sent");
    }
}

void loop()
{
}

There is no timezone handling, so the resulted time is in UTC.

Add a Comment

Your email address will not be published. Required fields are marked *