ArduinoIDE开发环境ESP32-C3通过AsyncUDP调用NTPv5服务器对RTC精确授时获得64位时间戳避免2036问题

这现在NTP马上在2036年溢出为了防止这个问题只好在NTP第5版协议上加了8位的时代编码,让溢出时间从136年变成了约3万年,但3万年后呢?那估计会又有其他解决方案了。
但我也不确定各NTP服务提供商是按照第五版协议增加了Era字段还是盲发老款报文。
在这里写一个精确授时的,由于RTC不是原子钟是会存在温漂问题的,如果要切换RTC晶振为外部32768或者内部高频时钟,需要去找配置文件,我也没找到。
代码是详细的算出授时的时间,授时应该很准确应该能达到微秒级的准确度,但由于UDP封包存在时间不对称性或丢包之类的原因,所以我也对此不做保证,如果真的需要高精度时间应采用原子钟氢钟作为内部时钟源授时采用GPS授时之类的ns级授时装置。而不是简单的拿着这些基于晶振或者RC震荡来做。
以下是获得时间戳的代码,设备对时成功后仅对持续供电状态下有时间,
距离最后一次对时136年后就会数据溢出。我想应该没有什么设备能持续工作136年且中途不授时一次时间

#include <AsyncUDP.h>//https://github.com/me-no-dev/ESPAsyncUDP
#include <WiFi.h>
// #include <sys/time.h>
// #include "ESP32Time.h"
// #include "time.h"
// #include <TimeLib.h>
#define LEAP_YEAR(Y)     ( ((1970+(Y))>0) && !((1970+(Y))%4) && ( ((1970+(Y))%100) || !((1970+(Y))%400) ) )
static  const uint8_t monthDays[]={31,28,31,30,31,30,31,31,30,31,30,31}; // API starts months from 1, this array starts from 0
 
AsyncUDP udp;
char* ntpServer = "pool.ntp.org";//time.nist.gov只有协议3 pool.ntp.org CN.NTP.ORG.CN
const int timeZone = 0;
const unsigned int localPort = 23511;
unsigned long long ntpTime = 0;//最后一次对时距离1900年的秒数
int currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond;
unsigned long timesendudp;
unsigned long timegetudp;
bool needupadte=1;
//定义了一种传统格里高利时间的结构体
typedef struct {
  uint8_t Second;
  uint8_t Minute;
  uint8_t Hour;
  uint8_t Wday;    // day of week, Sunday is day 1
  uint8_t Day;
  uint8_t Month;
  uint32_t Year;   // offset from 1970;END AY 4294967295YEAR
} tmElement64s_t;

void setup() {
  // 连接到 Wi-Fi
  Serial.begin(115200);
  WiFi.begin("S22", "12345678");
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to WiFi");

  // 初始化 UDP
  udp.listen(localPort);
  udp.onPacket([](AsyncUDPPacket packet) {
    // 处理接收到的 UDP 数据包
    handleNtpResponse(packet);
  });



}
void getNtpTimeAsync() {
    
    IPAddress ntpIpAddress;
    byte packetBuffer[48];
    packetBuffer[0] = 0b00101011;// LI, Version, Mode 不闰秒 ntpv5 客户端
    packetBuffer[1] = 0; // Stratum, or type of clock时钟精度 1最高15最低
    packetBuffer[2] = 6;
    packetBuffer[3] = 0x00;
  if (WiFi.hostByName(ntpServer, ntpIpAddress)) {


    udp.writeTo(packetBuffer, 48, ntpIpAddress, 123);
    timesendudp=micros();
  } else {
    // Serial.println("Failed to resolve NTP server");
  }
}
void handleNtpResponse(AsyncUDPPacket packet) {
  int packlength=packet.length();

  if (packlength !=0) {
    timegetudp=micros();
    if(timegetudp<timesendudp){return;}
    if((timegetudp-timesendudp)>3000000){return;}//如果对时包返回的过晚则代表网络拥塞此次对时失败
    byte packetBuffer[packlength];
    packet.read(packetBuffer, packlength);
    // Serial.println("当前服务器");
    // Serial.println(ntpServer);
    unsigned long bite1 =word(0x00, packetBuffer[0]);
    bite1=bite1 & 0x38;
    bite1=bite1 >>3;
    if(bite1!=5){return;}//如果对时版本号不对则代表服务器不支持ntp5
    // Serial.println("版本号");
    // Serial.println(bite1);
    // Serial.println("原始数据");
    // for (size_t i = 0; i < packlength; i++) {
    // if(i%4==0){Serial.println();}
    // Serial.printf("%02x",packetBuffer[i]);
    // //Serial.print(packetBuffer[i], HEX);
    
    // Serial.print(" ");
    // }
    
    // 提取时间戳字段
    
    unsigned Era = packetBuffer[5];
    unsigned ntpgetTimehighWord = word(packetBuffer[32], packetBuffer[33]);
    unsigned ntpgetTimelowWord = word(packetBuffer[34], packetBuffer[35]);
    unsigned ntpgetTimeMicrohighWord = word(packetBuffer[36], packetBuffer[37]);
    unsigned ntpgetTimeMicrolowWord = word(packetBuffer[38], packetBuffer[39]);
    unsigned long long ntpgetTime = (static_cast<unsigned long long>(ntpgetTimehighWord) << 48) |(static_cast<unsigned long long>(ntpgetTimelowWord) << 32) | (static_cast<unsigned long long>(ntpgetTimeMicrohighWord) << 16) | ntpgetTimeMicrolowWord;
    unsigned ntpsendTimehighWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned ntpsendTimelowWord = word(packetBuffer[42], packetBuffer[43]);
    unsigned ntpsendTimeMicrohighWord = word(packetBuffer[44], packetBuffer[45]);
    unsigned ntpsendTimeMicrolowWord = word(packetBuffer[46], packetBuffer[47]);
    unsigned long long ntpsendTime = (static_cast<unsigned long long>(ntpsendTimehighWord) << 48) |(static_cast<unsigned long long>(ntpsendTimelowWord) << 32) | (static_cast<unsigned long long>(ntpsendTimeMicrohighWord) << 16) | ntpsendTimeMicrolowWord;
    unsigned long long ntpserverspendTime = ntpsendTime - ntpgetTime; 
    
    unsigned long long totalroadtime=(timegetudp-timesendudp);//总路程时间
    totalroadtime = (totalroadtime << 32) / 1000000;//总路程时间毫秒
    unsigned long long roadtime=(totalroadtime-ntpserverspendTime)/2;//单程时间
    unsigned long long deviceGetTime=ntpsendTime+roadtime;//设备得到UDP包时间16进制
    struct timeval tv;//创造一个结构体用于重置内部RTC
    tv.tv_sec=0;//定义当前时刻为0时0分0秒 也就是说对时时刻为纪元原点 对时136年后时间戳溢出
    unsigned long long microtimenow=micros()+63;//63位最后的计算补时即下7行代码需要消耗63微秒时间
    if(microtimenow<timegetudp){return;}//如果微妙时间戳恰好溢出则对时失败
    unsigned long long calculationTime = (microtimenow-timegetudp);//计算耗时
    calculationTime = (calculationTime << 32) / 1000000;//计算耗时微秒
    unsigned long long devicesetTime=deviceGetTime+calculationTime;//计算当前时间戳
    unsigned long long microtime =(devicesetTime & 0xFFFFFFFF);//计算当前时间秒小数
    microtime = (microtime * 1000000) >> 32;//计算当前时间微秒秒数
    tv.tv_usec = microtime;//设置毫秒数
    settimeofday(&tv, NULL);//RTC对时
    unsigned long long ErastartTime=(static_cast<unsigned long long>(Era) << 32) |(devicesetTime>>32);
    ntpTime=ErastartTime;
    // Serial.println(ErastartTime);
    // Serial.println("纪元开始时间");//正确
    // Serial.println(microtime);
    // Serial.println("纪元开始微秒");//正确

    // 保存 NTP 时间
    ntpTime -= 2208988800ULL; // 从 1900 年转换为 Unix 时间戳
    ntpTime += timeZone * 3600; // 考虑时区偏移
    // ntpTime *= 1000; // 转换为毫秒
    // Serial.println(ntpTime);
    // 更新全局变量
    ntpTime = ntpTime;
    needupadte=0;
  }
}
void loop() {
  // 执行其他任务
  struct timeval tv;//创造一个结构体用于重置内部RTC
  if(needupadte){delay(3000);getNtpTimeAsync();Serial.println("等待授时");}
  else{
  // delay(300);
  gettimeofday(&tv, NULL);//从RTC获得时间 此函数在ESP编程包底层 很难找,好不容易找到的
  // Serial.println("RTC时间秒(上次对时到现在的时间)");
  // Serial.println(tv.tv_sec);
  // Serial.println("RTC时间微秒数");
  // Serial.println(tv.tv_usec);
  // Serial.println("UNIX时间戳");
  // Serial.println(ntpTime+tv.tv_sec);
  delay(1000);
  // ntpTime=5981690747;//当前纪元起点时间戳
  // ntpTime=1686726496;

  
  tmElement64s_t tm;//生成一种自定义结构体存储时间
  break64Time(ntpTime+tv.tv_sec,tm);//把时间戳扔给日历转换算法对结构体进行更改
  Serial.println("当前UTC时间");
  Serial.print(tm.Year);Serial.print("年");
  Serial.print(tm.Month);Serial.print("月");
  Serial.print(tm.Day);Serial.print("日");
  Serial.print(tm.Hour);Serial.print("时");
  Serial.print(tm.Minute);Serial.print("分");
  Serial.print(tm.Second);Serial.println("秒");
  Serial.print("当前微秒");Serial.println(tv.tv_usec);
  break64Time(ntpTime+tv.tv_sec+28800,tm);//扔给日历转换算法 北京时间和UTC差28800
  Serial.println("当前北京时间");
  Serial.print(tm.Year);Serial.print("年");
  Serial.print(tm.Month);Serial.print("月");
  Serial.print(tm.Day);Serial.print("日");
  Serial.print(tm.Hour);Serial.print("时");
  Serial.print(tm.Minute);Serial.print("分");
  Serial.print(tm.Second);Serial.println("秒");
  }


}

void break64Time(uint64_t timeInput, tmElement64s_t &tm){
// break the given time_t into time components
// this is a more compact version of the C library localtime function
// note that year is offset from 1970 !!!

  uint32_t year;
  uint8_t month, monthLength;
  uint64_t time;
  unsigned long days;

  time = (uint64_t)timeInput;
  tm.Second = time % 60;
  time /= 60; // now it is minutes
  tm.Minute = time % 60;
  time /= 60; // now it is hours
  tm.Hour = time % 24;
  time /= 24; // now it is days
  tm.Wday = ((time + 4) % 7) + 1;  // Sunday is day 1 
  
  year = 0;  
  days = 0;
  while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) {
    year++;
  }
  tm.Year = year+1970; // year is offset from 1970 
  // Serial.println("内部年");
  // Serial.println(year);
  days -= LEAP_YEAR(year) ? 366 : 365;
  time  -= days; // now it is days in this year, starting at 0
  
  days=0;
  month=0;
  monthLength=0;
  for (month=0; month<12; month++) {
    if (month==1) { // february
      if (LEAP_YEAR(year)) {
        monthLength=29;
      } else {
        monthLength=28;
      }
    } else {
      monthLength = monthDays[month];
    }
    
    if (time >= monthLength) {
      time -= monthLength;
    } else {
        break;
    }
  }
  tm.Month = month + 1;  // jan is month 1  
  tm.Day = time + 1;     // day of month
}


代码用settimeofday(&tv, NULL);和 gettimeofday(&tv, NULL);对内部ESP内部RTC进行调用(需要安装ESP的开发包),采用AsyncUDP做异步回调计算时间,原生UDP不支持异步回调,UDP包到达客户端后将放在缓冲区靠轮询进行计算。利用AsyncUDP的异步回调,UDP包到达时立刻唤起句柄计算耗时。
关键字段Era = packetBuffer[5];是时代编码,下一个时代2036年2月之后也不知道会不会置1。先这样写着吧。真的那一天到了如果不支持就只好人工计算。现在NTP的时间原点是1900-01-01 00:05:43 (UTC) 具体我也不清楚为何如此定义但现在例程的计算是无误的。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值