外观
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 开始算的,要 +1900tm_mon的范围是 0~11,要 +1tm_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"} |
第 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" 重新烧录,看时间差了多少。测完改回来。
动手试试
- 连上 WiFi,串口每秒打印一次时间,确认是正确的北京时间。
- 故意把时区写成
CST+8,看看差了多少小时。 - 对好表之后拔掉路由器电源,确认时间还在正常走。