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.
The first word is describing many things, lets decode that first.
Code | Description | Size |
LI | Leap indicator | 2 bits |
VN | NTP protocol version (4) | 3 bits |
Mode | Mode (Clinet – 3, Server – 4 etc) | 3 bits |
Stratum | Stratum | 8 bits |
Poll | Poll | 8 bits |
Precision | Precision | 8 bits |
Client mode with NTPv4
LI (0) | VN (4) | Mode (3) | |||||
0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
Client mode with NTPv3
LI (0) | VN (3) | Mode (3) | |||||
0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
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.
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.