[作品提交] 【得捷电子Follow me第4期】进阶任务:从NTP服务器同步时间,获取时间

zygalaxy   2024-2-24 21:30 楼主

进阶任务:从NTP服务器(注意数据交互格式的解析)同步时间,获取时间送显示屏(串口)显示。

搭配器件: W5500-EVB-Pico、 Adafruit Sharp Memory Display Breakout

 

一、NTP协议

NTP(Network Time Protocol, 网络时间协议)是基于UDP的一种用于计算机时间同步的应用层协议,NTP使用协调世界时(UTC)以极高的精度同步计算机时钟时间,例如在局域网(LAN)中低至1毫秒,在互联网上则在数十毫秒内。

二、NTP工作原理

NTP通过原子钟、天文台、卫星或者互联网上获取准确的时间来源,并通过不同的等级对服务器进行分层来同步时间。按照离外部UTC时间源的远近,NTP将服务器归入不同的层(Stratum)中,最顶层为有外部UTC接入的Stratum-1,而Stratum-2就会从Stratum-1获取时间,以此类推,最大层数为15层。因此,层数越大时间准确度相对越低,层数16表示未同步。

系统时钟的同步流程如下:

  1. NTP客户端在T1时刻发送一个NTP请求报文给NTP服务器,该请求报文携带离开NTP客户端时的时间戳T1。
  2. NTP请求报文到达NTP服务器,此时NTP服务器的时刻为T2。
  3. NTP服务器处理之后,于T3时刻发出NTP应答报文。该应答报文中携带离开NTP客户端时的时间戳T1、到达NTP服务器时的时间戳T2、离开NTP服务器时的时间戳T3。
  4. NTP客户端在T4时刻接收到该应答报文。

通过上面的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服务器的时钟同步。

三、NTP报文格式

image.png  

 

具体实现代码如下:


#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个小时

image.png  

 

上传程序到pico观察串口输出如下

image.png  

同时在墨水屏上已经显示了同步时间

image.png  

由于水墨屏的刷新速度较慢,我修改同步时间为20秒

 

总结:

NTP协议可以确保网络中各个设备的时钟保持同步,避免由于设备时钟不一致而导致的数据传输错误、日志记录混乱等问题,在日常的生活中有这十分重要的作用。

 

本帖最后由 zygalaxy 于 2024-2-23 19:18 编辑

回复评论

暂无评论,赶紧抢沙发吧
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复