进阶任务:从NTP服务器(注意数据交互格式的解析)同步时间,获取时间送显示屏(串口)显示。
搭配器件: W5500-EVB-Pico、 Adafruit Sharp Memory Display Breakout
NTP(Network Time Protocol, 网络时间协议)是基于UDP的一种用于计算机时间同步的应用层协议,NTP使用协调世界时(UTC)以极高的精度同步计算机时钟时间,例如在局域网(LAN)中低至1毫秒,在互联网上则在数十毫秒内。
NTP通过原子钟、天文台、卫星或者互联网上获取准确的时间来源,并通过不同的等级对服务器进行分层来同步时间。按照离外部UTC时间源的远近,NTP将服务器归入不同的层(Stratum)中,最顶层为有外部UTC接入的Stratum-1,而Stratum-2就会从Stratum-1获取时间,以此类推,最大层数为15层。因此,层数越大时间准确度相对越低,层数16表示未同步。
系统时钟的同步流程如下:
通过上面的NTP报文交互,NTP客户端获得4个时间参数,分别为T1、T2、T3、T4。由于NTP客户端和NTP服务器的时钟完全精确,我们可以通过以下公式计算出NTP客户端与NTP服务器之间的时间差,也就是NTP客户端需要调整的时间。
首先计算NTP报文从NTP客户端发送到NTP服务器所需要的时间Delay,Delay = [ ( T4 - T1 ) - ( T3 - T2 ) ] / 2
以T4时刻为例,在这个时刻点,NTP服务器发送过来的报文被NTP客户端接收到时,服务器的时刻已经为T3 + Delay。那么时间差Offset可由以下公式进行计算:T4 + Offset = T3 + Delay
公式整理之后,Offset = T3 + Delay - T4 = T3 + [ ( T4 - T1 ) - ( T3 - T2 ) ] / 2 - T4 = [ ( T2- T1 ) + ( T3 - T4 ) ] / 2。
NTP客户端根据计算得到Offset来调整自己的时钟,实现与NTP服务器的时钟同步。
具体实现代码如下:
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
// Screen
#include "PDLS_EXT3_Basic_Global.h"
// SDK
// #include <Arduino.h>
#include "hV_HAL_Peripherals.h"
// Include application, user and local libraries
// #include <SPI.h>
// Configuration
Screen_EPD_EXT3 myScreen(eScreen_EPD_EXT3_370, boardRaspberryPiPico_RP2040);
// Enter a MAC address for your controller below.
// Newer Ethernet shields have a MAC address printed on a sticker on the shield
byte mac[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
unsigned int localPort = 8888; // local port to listen for UDP packets
const char timeServer[] = "time.nist.gov"; // time.nist.gov NTP server
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
// A UDP instance to let us send and receive packets over UDP
EthernetUDP Udp;
void setup() {
// You can use Ethernet.init(pin) to configure the CS pin
//Ethernet.init(10); // Most Arduino shields
//Ethernet.init(5); // MKR ETH Shield
//Ethernet.init(0); // Teensy 2.0
//Ethernet.init(20); // Teensy++ 2.0
//Ethernet.init(15); // ESP8266 with Adafruit FeatherWing Ethernet
//Ethernet.init(33); // ESP32 with Adafruit FeatherWing Ethernet
// Open serial communications and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
myScreen.begin();
Serial.println(formatString("%s %ix%i", myScreen.WhoAmI().c_str(), myScreen.screenSizeX(), myScreen.screenSizeY()));
myScreen.clear();
// start Ethernet and UDP
if (Ethernet.begin(mac) == 0) {
Serial.println("Failed to configure Ethernet using DHCP");
// Check for Ethernet hardware present
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :(");
} else if (Ethernet.linkStatus() == LinkOFF) {
Serial.println("Ethernet cable is not connected.");
}
// no point in carrying on, so do nothing forevermore:
while (true) {
delay(1);
}
}
Udp.begin(localPort);
}
void loop() {
sendNTPpacket(timeServer); // send an NTP packet to a time server
// wait to see if a reply is available
delay(1000);
if (Udp.parsePacket()) {
// We've received a packet, read the data from it
Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
// the timestamp starts at byte 40 of the received packet and is four bytes,
// or two words, long. First, extract the two words:
unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
Serial.print("Seconds since Jan 1 1900 = ");
Serial.println(secsSince1900);
// now convert NTP time into everyday time:
Serial.print("Unix time = ");
// Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
const unsigned long seventyYears = 2208988800UL;
// subtract seventy years:
unsigned long epoch = secsSince1900 - seventyYears + 3600 * 8;
// print Unix time:
Serial.println(epoch);
myScreen.setOrientation(7);
// #if (USE_FONT_MODE == USE_FONT_TERMINAL)
myScreen.selectFont(Font_Terminal12x16);
// print the hour, minute and second:
Serial.print("The UTC time is "); // UTC is the time at Greenwich Meridian (GMT)
Serial.print((epoch % 86400L) / 3600); // print the hour (86400 equals secs per day)
Serial.print(':');
if (((epoch % 3600) / 60) < 10) {
// In the first 10 minutes of each hour, we'll want a leading '0'
Serial.print('0');
}
Serial.print((epoch % 3600) / 60); // print the minute (3600 equals secs per minute)
Serial.print(':');
if ((epoch % 60) < 10) {
// In the first 10 seconds of each minute, we'll want a leading '0'
Serial.print('0');
}
Serial.println(epoch % 60); // print the second
myScreen.gText(0, 0, formatString("The UTC time is %02i:%02i:%02i", (epoch % 86400L) / 3600, (epoch % 3600) / 60, epoch % 60));
myScreen.flush();
}
// wait ten seconds before asking for the time again
delay(20000);
Ethernet.maintain();
}
// send an NTP request to the time server at the given address
void sendNTPpacket(const char* address) {
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum, or type of clock
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
Udp.beginPacket(address, 123); // NTP requests are to port 123
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
}
因为北京时间为东八区,所以这行代码需要把时间再加上8个小时
上传程序到pico观察串口输出如下
同时在墨水屏上已经显示了同步时间
由于水墨屏的刷新速度较慢,我修改同步时间为20秒
总结:
NTP协议可以确保网络中各个设备的时钟保持同步,避免由于设备时钟不一致而导致的数据传输错误、日志记录混乱等问题,在日常的生活中有这十分重要的作用。
本帖最后由 zygalaxy 于 2024-2-23 19:18 编辑