联网与数据交换 / 时间与状态发布 内容更新

ESP32 对时与时区:断网后时钟还能信吗?

把首次未同步、在线已同步、断网保持和过期分开记录;用固定ESP-IDF工程保留epoch、UTC、本地偏移与同步年龄,并检查重启后的信任边界。

读完能做什么
得到日志中同时含时间与可信状态的SNTP时钟;断网或重启时不会只留一个貌似正确的日期。
开始之前
先完成前置文章的实际基础步骤;下载包README含独立安装、配置、构建与恢复命令。
所需器材
指定S3板、USB数据线、电脑与固定IDF5.5.5、2.4GHz WPA2-PSK网络、可访问的NTP服务器
版本组合
框架:ESP-IDF5.5.5 / esp32s3依赖:SDK内置esp_netif SNTP;更新600秒
适用范围

官方DevKitC-1 v1.1 / N8R8;ESP-IDF5.5.5、Flash8MB、DIO80MHz、不启用PSRAM、桥接UART115200;只用USB,无外接GPIO。需自备可互访WPA2网络与NTP服务。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/network-data.md#n05;固定上游章节与来源摘要。验证日期:
构建 / 配置已通过
docs/esp32-evidence/logs/n05-build.log;真实Exit0,构建输入/日志/固件SHA见network-data-builds.json。对象:ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8版本:ESP-IDF5.5.5验证日期:
仿真不适用
未执行ESP32或器件仿真;同源主机输入检查单独留证,不代表网络、I2C、调度或电气测试。
本站实板待验证
本站无这套固件烧录/串口/实体网络或传感器运行记录,observations.csv实测字段为空。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 时间与状态

有日期与有依据,是两件事

设备可能打印1970,也可能在复位后保留一个看似合理的日期。若日志只存年月日,之后无法判断采样时是否已经校时。这次每秒同时输出单调uptime、epoch、UTC、本地时间、online、state和sync_age_ms,未同步时格式化时间写NA。

本次交付已有真实构建和同源主机输入检查;本站尚无该固件联网、SNTP服务器通信、断网或重启实板结果。下面检查顺序是执行要求,不能当成已完成的实验记录。

做到这里,你应该看到
能够说明有IP、有墙上时间、本次启动收到同步通知是三个不同事实。

参考:[1] ESP-IDF v5.5.5 System Time:RTC/高分辨率时钟、SNTP模式、POSIX时区[2] ESP-IDF v5.5.5 esp_netif SNTP配置与默认宏

02 / 时间与状态

锁住目标,再填写自己的NTP源

使用指定S3板的USB-to-UART接口与115200串口,不接其他器件。本工程固定IDF5.5.5;下载包含根CMake、main、网络模块、Kconfig、公开defaults和真实lock,完整安装/烧录命令见README。

Demo configuration填写自己的2.4GHz WPA2网络、密码和NTP主机名或IPv4。NTP使用UDP123,不能填http://网址;默认REPLACE_WITH_NTP_SERVER不启动SNTP。凭据只在本地sdkconfig,源码和日志不包含真实密码,但编译后的固件仍含配置。

公共network-link.defaults只供编译检查实际网络分支,SSID/口令/192.0.2.123均为公开无效测试值。作者已核对ELF中的esp_wifi_connect和esp_netif_sntp_init符号;这不证明服务器可达。

代码 / 配置
idf.py --version
# 应为ESP-IDF v5.5.5;进入本项目根目录
idf.py set-target esp32s3
idf.py menuconfig
idf.py build
idf.py -p REPLACE_WITH_UART_PORT flash monitor
做到这里,你应该看到
版本、板型、桥接串口、NTP服务器与网络权限都能逐项说明。

参考:[2] ESP-IDF v5.5.5 esp_netif SNTP配置与默认宏[5] ESP-IDF v5.5.5官方Wi-Fi STA事件示例

03 / 时间与状态

用本次启动与单调年龄管理可信状态

clock_policy.h只保存本次启动已同步标志、最后同步的单调毫秒和观察到的通知数。首次启动无论epoch大小都UNSYNCED;已同步且有IP为SYNCED,掉线为HOLDOVER,年龄超过应用阈值为STALE。即使网络仍ONLINE,太久未同步也会过期。

默认同步周期600秒、过期阈值7200秒。阈值可配60–86400秒,是本应用策略,不是两小时精度保证。SYNCED也不认证NTP服务器或网络时延。普通SNTP未提供本例的认证时间来源。

RTC可能跨某些复位保持时间,断电又不同;本例每次启动清本次同步标志,不从年份或保留日期恢复信任,也不把last_sync持久化。

状态本次启动依据时间展示
UNSYNCED尚无本次SNTP通知;有IP也一样utc/local=NA,原始epoch保留
SYNCED本次已同步、当前有IP、年龄≤阈值UTC及本地偏移
HOLDOVER本次已同步、当前无IP、年龄≤阈值继续显示并标保持
STALE年龄>阈值或模型时基异常保留诊断值并标过期

参考:[1] ESP-IDF v5.5.5 System Time:RTC/高分辨率时钟、SNTP模式、POSIX时区[2] ESP-IDF v5.5.5 esp_netif SNTP配置与默认宏

04 / 时间与状态

完整主循环:回调送通知,应用更新状态

首次ONLINE后才调用esp_netif_sntp_init,wait_for_sync=false不创建等待信号量;本例不调用esp_netif_sntp_sync_wait,因此主循环不等待对时;回调只复制epoch和单调时刻到长度1队列。最新通知会覆盖旧通知,因此notices_observed只统计应用看见的通知,不是全部NTP包。 主循环先出队保存是否收到通知,再采当前单调时间,避免回调在两句之间跨毫秒时,把合法首次通知当成未来输入消费丢弃。

smooth_sync=false采用立即校正,墙上时间可能前跳或后跳;超时和同步年龄仍使用esp_timer。网络模块的2/4/8/16/30秒退避由断线事件和单次计时器驱动,LOST_IP会单独清在线位,不在主循环无限等上线。

代码 / 配置
// SPDX-License-Identifier: MIT
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_netif_sntp.h"
#include "sdkconfig.h"
#include "net.h"
#include "clock_policy.h"
static const char *TAG = "N05";
typedef struct { int64_t mono_ms, epoch; } sync_notice_t;
static QueueHandle_t sync_queue;
// lwIP task callback: copy only, no logging/Wi-Fi calls/calendar conversion.
static void synced(struct timeval *tv) {
    sync_notice_t notice = {.mono_ms = esp_timer_get_time() / 1000, .epoch = tv->tv_sec};
    xQueueOverwrite(sync_queue, &notice); // latest event retained; intermediate notices may coalesce
}
void app_main(void) {
    sync_queue = xQueueCreate(1, sizeof(sync_notice_t));
    if (!sync_queue) { ESP_LOGE(TAG, "queue allocation failed"); return; }
    if (setenv("TZ", CONFIG_DEMO_TZ, 1)) { ESP_LOGE(TAG, "TZ allocation failed"); return; }
    tzset();
    clock_policy_t clock = {0}; // deliberately distrust every new boot until this boot receives SNTP
    bool started = false, server_valid = strlen(CONFIG_DEMO_NTP_SERVER) && strcmp(CONFIG_DEMO_NTP_SERVER, "REPLACE_WITH_NTP_SERVER");
    if (!server_valid) ESP_LOGW(TAG, "NTP_CONFIG_MISSING: clock remains UNSYNCED");
    net_start();
    for (;;) {
        bool online = net_online();
        if (online && server_valid && !started) {
            esp_sntp_config_t cfg = ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_DEMO_NTP_SERVER);
            cfg.sync_cb = synced;
            cfg.wait_for_sync = false;
            cfg.smooth_sync = false; // immediate wall-clock corrections can jump; intervals use monotonic time
            esp_err_t err = esp_netif_sntp_init(&cfg);
            started = err == ESP_OK;
            ESP_LOGI(TAG, "sntp_init=%s", esp_err_to_name(err));
        }
        sync_notice_t notice;
        // Dequeue first, then sample now: a callback in this interval must not look like a future notice.
        const bool has_notice = xQueueReceive(sync_queue, &notice, 0) == pdTRUE;
        const int64_t now_ms = esp_timer_get_time() / 1000;
        if (has_notice && clock_note_sync(&clock, notice.mono_ms, now_ms))
            ESP_LOGI(TAG, "SYNC_NOTICE received_epoch=%" PRId64, notice.epoch);
        time_t now = time(NULL);
        char utc[40] = "NA", local[48] = "NA";
        if (clock.synced) {
            struct tm u, l;
            gmtime_r(&now, &u); localtime_r(&now, &l);
            strftime(utc, sizeof utc, "%Y-%m-%dT%H:%M:%SZ", &u);
            strftime(local, sizeof local, "%Y-%m-%dT%H:%M:%S%z", &l);
        }
        ESP_LOGI(TAG, "uptime_ms=%" PRId64 " epoch=%" PRId64 " state=%s online=%u sync_age_ms=%" PRId64 " notices_observed=%" PRIu32 " utc=%s local=%s TZ=%s",
                 now_ms, (int64_t)now, clock_state(&clock, online, now_ms, (int64_t)CONFIG_DEMO_STALE_SECONDS * 1000),
                 online, clock_age(&clock, now_ms), clock.notices, utc, local, CONFIG_DEMO_TZ);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

参考:[2] ESP-IDF v5.5.5 esp_netif SNTP配置与默认宏[3] ESP-IDF v5.5.5 SNTP封装:TCP/IP线程调用、通知回调与启动[5] ESP-IDF v5.5.5官方Wi-Fi STA事件示例

05 / 时间与状态

时区只改变展示,不给epoch加八小时

默认POSIX字符串CST-8表示UTC+8;负号方向与常写的“UTC+8”相反。setenv("TZ",...)后调用tzset,再用localtime_r格式化,UTC则使用gmtime_r并写Z。示意图采用epoch=0的合成输入,只说明格式差异,不是有效测量时刻。

不是IANA时区数据库,不把Asia/Shanghai写进本例就宣称已验证。日光节约规则须按实际地区的POSIX表达式另核对;本例采用CST-8和UTC0两个清楚的对照。

做到这里,你应该看到
同一epoch的UTC与本地字符串差8小时,但epoch本身不手动增加。

参考:[1] ESP-IDF v5.5.5 System Time:RTC/高分辨率时钟、SNTP模式、POSIX时区[4] ESP-IDF v5.5.5官方SNTP示例:CST-8、tzset与时间格式

06 / 时间与状态

按首次离线、成功同步、断网和重启顺序验收

先用占位配置验证首次离线的UNSYNCED;再仅填Wi-Fi、暂使NTP不可达,确认ONLINE仍不能使它变为SYNCED。接通可访问的NTP源后,保留首次SYNC_NOTICE和随后完整日志,并与可信参考时钟比较。

成功同步后关闭网络,观察HOLDOVER及超龄STALE。恢复网络只有下一次真实同步通知才能重置年龄,不承诺一恢复IP就立刻校时。可将阈值临时改60秒缩短过期实验,但同步周期仍是600秒。

分别RESET和断电重启,在离线条件下检查本应用重新UNSYNCED,之后重新校时;将TZ改UTC0再重建核对格式。CSV记录板号、源服务器、配置、断网和复位方式。以上实板项目当前均pending。

输入/步骤必须保留的事实当前证据
首次离线UNSYNCED、age=-1、utc/local=NA主机策略已测;实板待验
IP可用但无SNTP仍UNSYNCED主机策略已测;UDP通信待验
同步后掉线/超龄HOLDOVER→STALE主机边界已测;实板待验
RESET/断电本次同步标志重置主机策略已测;两种实板复位待验

参考:[1] ESP-IDF v5.5.5 System Time:RTC/高分辨率时钟、SNTP模式、POSIX时区[2] ESP-IDF v5.5.5 esp_netif SNTP配置与默认宏

07 / 时间与状态

先复现模型,再按故障层修复

host_check.c直接包含固件用clock_policy.h,实际通过9组检查,覆盖首次离线、仅有IP、非法通知、首次同步、保持、年龄边界、重新同步、重启策略及主机POSIX格式。主机libc的格式检查不等于ESPnewlib、无线或SNTP实测。

一直UNSYNCED先看配置、DNS、UDP123和NTP服务;差8小时同时检查UTC、epoch与TZ,不靠修改epoch解决。NVS报需恢复时不自动擦除,只有确认整片Flash数据可丢弃才手动擦;网络故障通常不需要这一步。下一篇可将时间可信状态与MQTT样本一起上报。

代码 / 配置
cc -std=c11 -Wall -Wextra -Werror host_check.c -o /tmp/n05-host
/tmp/n05-host

参考:[1] ESP-IDF v5.5.5 System Time:RTC/高分辨率时钟、SNTP模式、POSIX时区[3] ESP-IDF v5.5.5 SNTP封装:TCP/IP线程调用、通知回调与启动[5] ESP-IDF v5.5.5官方Wi-Fi STA事件示例

BACK TO THE SOURCE

资料来源与版本说明

按固定版本官方源码与具体板级核对;实际构建、主机输入检查和实板是分开记录的状态。没有把预期步骤写成本站已运行结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. ESP-IDF v5.5.5 System Time:RTC/高分辨率时钟、SNTP模式、POSIX时区
  2. ESP-IDF v5.5.5 esp_netif SNTP配置与默认宏
  3. ESP-IDF v5.5.5 SNTP封装:TCP/IP线程调用、通知回调与启动
  4. ESP-IDF v5.5.5官方SNTP示例:CST-8、tzset与时间格式
  5. ESP-IDF v5.5.5官方Wi-Fi STA事件示例