返回首页

ESP8266 上的 NTP 服务器:无互联网精确时间 | 指南

本文描述了基于 ESP8266 的自主 NTP 服务器实现,具有微秒精度。详细分析了网络延迟补偿算法和同步间隔期间的时间精度维护。该解决方案适用于外部 NTP 服务器封锁情况。

自主 NTP 服务器:ESP8266 如何解决时间封锁问题
Advertisement 728x90

基于 ESP8266 的独立 NTP 服务器:无需外部依赖即可确保精确时间

当互联网连接不稳定且外部 NTP 服务器被屏蔽时,本地精确时间源就变得至关重要。我们将一步步指导您在 ESP8266 上构建一个自主 NTP 服务器,实现微秒级精度,即使没有持续互联网访问也能持续运行。

为什么标准解决方案无法满足需求

现代基础设施往往因网络限制和白名单策略而无法访问全球 NTP 服务器。当外部时间源中断时,系统时钟就会漂移失步,这对以下场景是个大问题:

  • 金融交易
  • 安全日志记录
  • 分布式计算系统
  • 具有严格时序需求的物联网设备

解决方案?构建一个本地 NTP 服务器,在同步间隔内自主保持准确时间。ESP8266 非常适合:低功耗、内置 WiFi,以及足够的性能来处理时间戳。

Google AdInline article slot

时间系统架构

主要挑战是突破 Arduino 标准时间库的限制,该库仅能跟踪整秒。NTP(RFC 5905)要求微秒精度。我们自定义的 JbTime 类解决了两个核心问题:

  • 系统时间不支持小数秒
  • 同步间隔内漂移积累

工作原理:

  • 当时间从外部服务器到来时,捕获处理器的微秒时间戳
  • 每个请求计算从同步时刻到当前的差值
  • 校正因子考虑网络延迟,使用此公式:offset = ((t2 - t1) + (t3 - t4)) / 2
#ifndef jb_time
#define jb_time

#include <stdint.h>

#define JB_TIME_MAX_AGE  600

class JbTime {
private:
  uint64_t _last;
  uint64_t _sec;
  uint32_t _usec;
  uint32_t _mark;

public:
  bool ok;
  bool fresh;

  JbTime(){
    _sec = 0;
    _usec = 0;
    _mark = 0;
    _last = 0;
    ok = false;
    fresh = false;
  }

  inline bool old() {
    if(_sec == 0) return true;
    if((_sec - _last) > JB_TIME_MAX_AGE) return true;
    return false;
  }

  inline void settime(uint64_t sec, uint32_t usec, uint32_t mark = micros()){
    _mark = mark;
    _sec = sec;
    _sec += usec / 1000000;
    _usec = usec % 1000000;
    ok = true;
    _last = sec;
  }

  inline void gettime(uint64_t *o_sec, uint32_t *o_usec){
    if(ok){
      uint32_t now = micros();
      uint32_t delta = now - _mark;
      _mark = now;

      uint64_t total = (uint64_t)_usec + delta;
      _sec += total / 1000000;
      _usec = total % 1000000;

      *o_sec = _sec;
      *o_usec = _usec;
    } else {
      *o_sec = 0;
      *o_usec = 0;
    }
  }
};
#endif

处理 NTP 请求

NTP 协议运行在 UDP 端口 123 上,以 64 位值发送时间(32 位秒,32 位分数)。最棘手部分?补偿网络延迟。算法如下:

Google AdInline article slot
  • 客户端发送带有 T1 时间戳的请求(其本地时间)
  • 服务器在 T2 接收,在 T3 发送回复
  • 客户端在 T4 接收回复
  • 偏移量:offset = ((T2 - T1) + (T3 - T4)) / 2

JbNTPClient 类考虑到 ESP8266 的特性来处理此过程:

  • 数据包按 RFC 5905 格式化
  • 自动纪元调整(NTP 从 1900 年开始,Unix 从 1970 年)
  • 过滤无效响应
bool JbNTPClient::requestTime(const char* server, JbTime * mytime) {
  // ... initialization
  
  // Calculation T1
  uint64_t t1_sec = 0;
  uint32_t t1_usec = 0;
  systime->gettime(&t1_sec, &t1_usec);
  t1_sec += NTP_UNIX_EPOCH_DIFF;
  
  // Sending request
  udp.beginPacket(timeServerIP, ntpPort);
  udp.write((uint8_t*)&packet, sizeof(NTPPacket));
  udp.endPacket();

  // Ozhidanie otveta with taymautom 2000 ms
  while (udp.parsePacket() == 0) {
    if (millis() > timeout) return false;
    delay(10);
  }

  // Raschet T4 and setevoy zaderzhki
  double t4 = ntpToDouble(t4_sec, t4_frac);
  _networkDelay = (t4 - t1) - (t3 - t2);
  double offset = ((t2 - t1) + (t3 - t4)) / 2;

  // Installation skorrektirovannogo vremeni
  mytime->settime(sec, usec, t4_mark);
  return true;
}

服务器端:在本地网络上提供时间服务

一旦与外部服务器同步,该设备就成为一个成熟的 NTP 来源。它通过 serve() 方法处理客户端请求:

  • 解析传入的 UDP 数据包
  • 验证模式(期望 mode=3 —— 客户端请求)
  • 使用 JbTime 的当前时间构建响应
  • 考虑处理时间以实现最大精度

关键特性:时间戳在响应发送时(T3)立即捕获,以减少误差。与现成选项不同,它直接从高分辨率计数器获取时间,跳过粗糙的系统函数。

Google AdInline article slot

主要特性

  • 自主性:同步间隔保持 20-30 微秒精度,即使离线数小时
  • 资源效率:ESP8266 活跃时功耗低于 70 mA,适合电池供电
  • 兼容性:符合 RFC 5905,可即插即用与任何 NTP 客户端配合
  • 灵活性:根据需求调整最大数据年龄(JB_TIME_MAX_AGE)
  • 容错性:主服务器宕机时自动切换到备份

— Editorial Team

Advertisement 728x90

继续阅读