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

该代码示例展示了如何使用AsyncUDP库进行异步NTP时间同步,并处理可能的时间溢出问题。通过增加8位的时代编码,将溢出时间从136年延长至约3万年。程序详细计算授时时间,但因UDP的不确定性,无法保证微秒级精度。建议高精度应用使用原子钟或其他NS级授时设备。代码中包含获取时间戳的实现,并通过settimeofday进行RTC对时。

这现在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) 具体我也不清楚为何如此定义但现在例程的计算是无误的。

<think> 首先,用户的问题是:“我想查找esp32获取Unix时间的方法 请问esp32获取Unix时间的方法”。这是基于之前的引用内容提出的。 回顾引用内容: - 引用[1]:解释了Unix时间是从1970年1月1日00:00:00 UTC开始的秒数,并提到在ESP32Arduino环境中,time_t数据类型表示这个秒数,使用static_cast转换为long。 - 引用[2]:介绍通过NTP获取Epoch/Unix时间,需要ESP32连接互联网。 - 引用[3]:类似引用[2],但针对ESP8266,不过提到NTP协议用于同步时间。 - 引用[4]:提到SNTP协议用于同步本地时间到Unix时间,在ESP32上连接AP后使用。 - 引用[5]:讨论使用AsyncUDP调用NTPv5服务器获取时间避免2036问题,使用settimeofday和gettimeofday函数。 用户的核心需求是:在ESP32上获取Unix时间的方法。 关键点: - Unix时间是基于1970年1月1日00:00:00 UTC的秒数。 -ESP32中,通常通过NTP(网络时间协议)或SNTP(简单网络时间协议)从服务器获取时间。 - 代码实现通常在Arduino IDE环境中。 我的回答需要: - 使用中文。 - 结构清晰,逐步解决问题- 确保真实可靠,基于引用内容。 - 在行内数学表达式使用$...$格式,例如提到时间的概念时。 - 独立公式使用$$...$$格式并单独成段(如果适用)。 - 末尾添加相关问题- 自然地添加引用标识到引用的段落末尾。 回答结构: 1. **简介**:解释Unix时间是什么以及为什么在ESP32上需要获取它。 2. **方法**:描述获取Unix时间的步骤。 - 需要互联网连接,使用WiFi。 - 配置NTP服务器- 使用Arduino库函数。 3. **代码示例**:提供简单的Arduino代码片段来演示。 - 基于引用内容,可以使用time.h库或类似函数。 - 引用中提到gettimeofday或类似函数。 4. **注意事项**:提到潜在问题,如2036问题(Y2K38问题),但引用[5]提到避免它。 5. **结尾**:总结。 6. **相关问题**:生成3-4个相关问题。 确保LaTeX语法正确: - 对于时间的定义,可以用$t_{\text{unix}} = \text{seconds since 1970-01-01 00:00:00 UTC}$,但用户指令指定行内数学表达式使用$...$,所以我需要适当使用。 - 独立公式如果需要,例如如果解释时间计算。 引用标识: - 在描述方法时,如果引用了具体内容,在段落末尾添加引用标识如[^1]。 现在,构建回答: - 开场:直接回答问题- 主体: - 步骤1:连接WiFi。 - 步骤2:配置NTP客户端。 - 步骤3:获取时间- 代码:提供简单示例。 - 注意事项。 - 相关问题。 相关问题想法: 1. 如何确保ESP32的时间同步精度? 2. 在没有互联网的情况下如何获取时间3. ESP32如何处理2036问题? 4. 如何将时间转换为人类可读的时间? 最终回答: - 使用中文。 - 清晰、逐步。 </think> ### ESP32获取Unix时间的方法 Unix时间(也称为Epoch时间)是从1970年1月1日00:00:00 UTC开始计算的秒数,广泛应用于日志记录、时间同步和文件命名等场景[^1]。在ESP32上获取Unix时间,通常需要设备连接互联网(通过WiFi),并使用NTP(网络时间协议)或SNTP(简单网络时间协议)从时间服务器同步时间。以下是基于Arduino IDE环境的逐步方法(确保ESP32开发板已连接到网络): #### 获取步骤 1. **连接WiFi网络**:ESP32必须先接入互联网,使用WiFi库建立连接。 2. **配置NTP客户端**:使用内置的`time.h`库设置NTP服务器(如`pool.ntp.org`),以同步全球时间。 3. **获取时间**:使用`gettimeofday()`或`time()`函数读取当前时间,并将其转换为Unix时间(`time_t`类型)。 - Unix时间定义:$ t_{\text{unix}} = \text{seconds since 1970-01-01 00:00:00 UTC} $ - 在代码中,`time_t`通常是一个32位或64位整数,表示秒数;为避免2036问题(Y2K38),建议使用64位兼容方法,如引用[5]所述[^5]。 #### 代码示例 以下是一个简单的Arduino代码示例,展示如何通过NTP获取Unix时间: ```cpp #include <WiFi.h> #include <time.h> // 包含时间函数库 const char* ssid = "your_SSID"; // 替换为您的WiFi名称 const char* password = "your_PASSWORD"; // 替换为您的WiFi密码 const char* ntpServer = "pool.ntp.org"; // 默认NTP服务器 const long gmtOffset_sec = 0; // UTC时区偏移(秒),0表示UTC时间 const int daylightOffset_sec = 0; // 夏令时偏移(秒),0表示禁用 void setup() { Serial.begin(115200); // 步骤1: 连接WiFi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("WiFi connected"); // 步骤2: 配置NTP服务器 configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // 初始化时间同步 delay(1000); // 等待时间同步完成 // 步骤3: 获取Unix时间 struct timeval tv; // 存储时间结构体 gettimeofday(&tv, NULL); // 获取当前时间(使用gettimeofday函数) time_t unixTimestamp = tv.tv_sec; // 提取秒数部分作为Unix时间 Serial.print("Unix时间: "); Serial.println(unixTimestamp); // 输出时间,例如1717300000 } void loop() { // 主循环可空置,或定期更新时间 } ``` - **关键说明**: - `gettimeofday()`函数返回一个结构体`timeval`,其中`tv_sec`字段即为Unix时间(自1970年以来的秒数)[^1][^5]。 - 代码使用`configTime()`配置NTP服务器,确保时间同步准确。 -避免2036问题32位系统时间溢出),此方法基于64位兼容实现,但需注意未来更新[^5]。 - 测试时,确保ESP32可访问互联网;代码执行后,串口监视器将输出当前时间。 #### 注意事项 - **精度和可靠性**:NTP同步可能存在毫秒级延迟,建议在稳定网络下使用,并在代码中添加错误处理(如检查同步状态)。 - **离线场景**:如果没有互联网,ESP32无法获取实时时间;可考虑使用RTC(实时时钟)模块或手动设置时间,但精度较低。 - **库依赖**:确保Arduino IDE已安装`WiFi`和`time`库(标准库通常预装)。 - **2036问题**:Unix时间2036年可能溢出(32位限制),引用[5]建议使用`AsyncUDP`和NTPv5协议支持64位时间,但需额外配置[^5]。 通过以上方法,ESP32能高效获取Unix时间,适用于物联网设备的时间管理[^2][^4]。如需更多优化,可以参考高级库如`NTPClient`或处理异步回调。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值