Skip to content

3.5 SNTP 时间同步 ​

这节要搞懂:怎么让时钟自动对时,永远准。 需要基础:3.4

一、为什么要联网校时 ​

芯片里虽然有计时器,但它不知道现在几点 —— 一上电就从 1970 年开始数。

而且它走得不够准,一天会差几秒。

解决办法:连上网,跟时间服务器对一次表。

二、什么是 SNTP ​

SNTP(Simple Network Time Protocol,简单网络时间协议)是一种通过网络从时间服务器获取标准时间的协议。它是 NTP 的简化版,精度稍低但实现简单,非常适合嵌入式设备。

打个比方:SNTP 就是打电话给"报时台"问现在几点,然后照着调自己的表。

三、WiFi 连接:用事件组等结果 ​

工程里 app_wifi_connect() 是阻塞的 —— 它内部用事件组(1.8 讲过)等待连接结果,成功或失败都会返回。

c
#define WIFI_CONNECTED_BIT   BIT0
#define WIFI_FAIL_BIT        BIT1

static EventGroupHandle_t s_wifi_event_group;
static int s_retry_num = 0;

// WiFi 事件回调
static void wifi_event_handler(void *arg, esp_event_base_t base,
                               int32_t event_id, void *data)
{
    if (base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    }
    else if (base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < MAX_RETRY) {
            esp_wifi_connect();
            s_retry_num++;
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);   // 重试够了,认输
        }
    }
    else if (base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);  // 成功
    }
}

esp_err_t app_wifi_connect(void)
{
    // ... 初始化 WiFi、注册回调、esp_wifi_start() ...

    // ⭐ 在这里等:要么成功,要么失败,两个位等哪个先来
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
                                           WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                                           pdFALSE,
                                           pdFALSE,      // 任一满足就返回
                                           portMAX_DELAY);

    if (bits & WIFI_CONNECTED_BIT) {
        return ESP_OK;
    }
    return ESP_FAIL;
}

为什么要两个位

只用一个"成功"位的话,连不上时就会永远卡在这里。

加一个 WIFI_FAIL_BIT,重试够次数就置位 —— 成功和失败都能让调用方继续往下走。

xWaitForAllBits 填 pdFALSE 表示"哪个先来算哪个"(1.9 讲过这个参数)。

四、SNTP 校时:轮询同步状态 ​

WiFi 成功之后,app_task 才调用 app_time_sync()。

它内部的做法是:启动 SNTP,然后循环检查同步状态,直到成功或超时。

c
esp_err_t app_time_sync(void)
{
    // 1. 配置并启动 SNTP
    esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
    esp_sntp_setservername(0, "ntp.aliyun.com");      // ⭐ 国内用这个
    esp_sntp_setservername(1, "pool.ntp.org");        // 备用
    esp_sntp_init();

    // 2. 循环等它同步好:最多 20 次,每次 500ms,合计约 10 秒
    int retry = 0;
    const int max_retry = 20;

    while (esp_sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED
           && ++retry < max_retry) {
        ESP_LOGI(TAG, "waiting for time sync... (%d/%d)", retry, max_retry);
        vTaskDelay(pdMS_TO_TICKS(500));
    }

    // 3. 设置时区
    setenv("TZ", "CST-8", 1);
    tzset();

    return (retry < max_retry) ? ESP_OK : ESP_ERR_TIMEOUT;
}

轮询 vs 回调

SNTP 也支持注册回调(sntp_set_time_sync_notification_cb),但工程里用的是轮询。

为什么轮询更简单:app_task 本来就要"等校时完成再往下走",用轮询可以顺着代码一行行读下来;用回调的话,还得再引入一个信号量或事件组来等它。

在"启动流程"这种一步接一步的场景里,轮询反而更清楚。

校时超时也会继续

注意第 3 步 —— 无论成功还是超时,函数都会返回,app_task 都会继续往下走进主界面。

超时的话时间还停在 1970 年,界面上显示占位内容,等下一次 time_sync_task 再试。

不能因为校时失败就卡住不开机。

五、什么是时区 ⭐ ​

时区(Time Zone)是地球上使用同一标准时间的区域。SNTP 服务器返回的是 UTC(协调世界时),要显示本地时间必须加上时区偏移。北京时间是 UTC+8。

c
setenv("TZ", "CST-8", 1);      // ⭐ 注意是减号 -8
tzset();                        // 必须调用这行才生效

符号是反的!

CST-8 表示北京时间(UTC+8)。

写成 CST+8 的话,时间会差 16 个小时。

这是 POSIX 时区格式的规定,违反直觉但就是这样,记住 CST-8 就对了。

六、读取时间 ​

c
time_t now;
struct tm timeinfo;

time(&now);                              // 取当前时间戳
localtime_r(&now, &timeinfo);            // 转成年月日时分秒(_r 是线程安全版本)

ESP_LOGI(TAG, "%04d-%02d-%02d %02d:%02d:%02d",
         timeinfo.tm_year + 1900,        // ⚠️ 年份要 +1900
         timeinfo.tm_mon + 1,            // ⚠️ 月份从 0 开始,要 +1
         timeinfo.tm_mday,
         timeinfo.tm_hour,
         timeinfo.tm_min,
         timeinfo.tm_sec);

三个偏移量

  • tm_year 是从 1900 开始算的,要 +1900
  • tm_mon 的范围是 0~11,要 +1
  • tm_wday 里 0 表示星期日

不加的话你会看到"126 年 8 月"这种奇怪日期。

七、怎么判断时间对好了没 ​

没对过表时,系统时间是 1970 年。可以这样判断:

c
bool time_is_valid(void)
{
    time_t now;
    time(&now);

    // 大于这个时间戳说明是 2023 年以后,是真实时间
    return now > 1700000000;
}

界面上:没对好表就显示 --:--:--,对好了再显示真实时间。

八、时间怎么刷到屏幕上 ​

工程里没有单独的时钟任务 —— 时间是在 app_task 最后的 update_dashboard_loop() 里刷新的(3.2、3.3 讲过):

c
static void update_dashboard_loop(void)
{
    char time_text[16];

    while (1) {
        // 取当前时间,格式化成 "HH:MM" 之类的字符串
        if (app_time_get_time_line(time_text, sizeof(time_text))) {
            app_display_update_time(time_text);      // 变了才画(3.7)
        }

        // 顺便刷新日期、温湿度、天气
        update_other_areas();

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

另外还有一个 time_sync_task 在后台周期性重新校时,防止时间越走越偏。

为什么用 vTaskDelayUntil

用 vTaskDelay(1000) 的话,实际周期 = 干活时间 + 1 秒,会慢慢偏,跑一天差好几秒。

vTaskDelayUntil 是严格每 1 秒一次(1.9 讲过)。

九、断网了会怎样 ​

情况表现
从没对过表显示 --:--:--
对过表之后断网✅ 时间继续正常走,只是不再校准
断电重启且没网回到 1970,显示 --:--:--

芯片自己走时一天大约差 2 秒,所以 1 小时对一次表完全够用。

本节要点 ​

  • SNTP = 通过网络获取标准时间的协议
  • app_wifi_connect() 用事件组等结果,成功和失败各一个位,两个都要有
  • app_time_sync() 轮询 esp_sntp_get_sync_status(),超时也会返回继续开机
  • NTP 服务器用国内的(ntp.aliyun.com)
  • ⭐ 时区写 CST-8,符号是反的,还要调 tzset()
  • 年份 +1900,月份 +1
  • 用 now > 1700000000 判断有没有对好表
  • 时间刷新在 update_dashboard_loop() 里,另有 time_sync_task 周期重新校时

操作过程 ​

目标:设备连上 WiFi,自动校准时间,屏幕上显示正确的北京时间。

第 1 步:先让它有 WiFi ​

两种方式选一种:

方式怎么做
网页配网连热点 ESP32-Clock-Setup(密码 12345678),打开 192.168.4.1 填账号密码
蓝牙配网小程序连上,往 WiFi 特征值写 {"ssid":"xxx","password":"yyy"}

详见 3.8 和 3.10。

第 2 步:编译烧录 ​

第 3 步:盯串口看这几行 ​

开机后按顺序应该出现:

I (2100) wifi: connecting to AP ...
I (4530) wifi: got ip: 192.168.1.107          ← ① 连上路由器了
I (4540) app_time: waiting for time sync... (1/20)
I (5040) app_time: waiting for time sync... (2/20)
I (5550) app_time: time synced                ← ② 校时成功
I (5560) main: 2026-09-11 20:35:12            ← ③ 时间正确

记下那个 IP

第 ① 行的 got ip: 192.168.1.107 要记下来 —— 后面用电脑访问设备网页要用它(3.8)。

第 4 步:验证时间对不对 ​

和手机上的时间对一下,误差应该在 1 秒以内。

差 8 小时或 16 小时

说明时区配错了。检查这两行:

c
setenv("TZ", "CST-8", 1);      // ⭐ 是减号 -8
tzset();                        // ⭐ 这行不能少

CST+8 会差 16 小时 —— 符号是反的。

第 5 步:验证断网后时间还走 ​

对好表之后,把路由器关掉(或者把设备带到没网的地方)。

时间应该继续正常走 —— 因为芯片内部有自己的计时器,SNTP 只是定期校准它。

第 6 步:故意做错一次 ​

把时区改成 "CST+8" 重新烧录,看时间差了多少。测完改回来。

动手试试 ​

  1. 连上 WiFi,串口每秒打印一次时间,确认是正确的北京时间。
  2. 故意把时区写成 CST+8,看看差了多少小时。
  3. 对好表之后拔掉路由器电源,确认时间还在正常走。