RTC:维护系统时间与深睡眠计时

2026-09-10 00:00    #ESP32-S3   #ESP-IDF   #外设   #阶段3  

本课对应《DNESP32S3 使用指南(IDF 版)》第 23 章。配套完整工程:examples/18-rtc-system-time。

环境与验收边界: DNESP32S3 V1.2,ESP-IDF v5.5.5;本机 macOS 构建通过。Ubuntu 26、外设接线、屏幕效果和实际测量仍待实板验收。下文输出均为预期格式,不能当作已采集的硬件日志。

本课完成后能做什么

硬件连接与资源

信号或资源对应关系
计时资源ESP32-S3 内部 RTC 与高分辨率系统时间
外部接线无
保持条件整板持续供电;本例验证深睡眠
断电继续计时需另外选用电池供电 RTC,本例没有该硬件

本课信号与数据处理流程

先修正“掉电计时”这个说法

教材第 23 章使用芯片内部 RTC。RTC 是实时时钟的缩写,不代表器件一定有后备电池。DNESP32S3 整板断电后,本例没有独立供电的时间源,因此不能继续计时。

这里区分三种情况:普通任务延时期间系统时间继续前进;深睡眠期间由 RTC 保持计时;完全断电后时间不可依赖。软件复位和 EN 复位也不能混作一个验收项,应记录复位原因和实际保持情况。本课选择明确可重复的“深睡眠十秒再唤醒”,不会许诺所有形式重启均保持。

若产品必须拔掉主电源仍准确计时,需要带后备电池的外部 RTC;联网设备则可启动后使用 SNTP 校时。把最后一个时间戳写 EEPROM 只能记住断电前的时刻,无法知道断电持续了多久。

日历时间与单调时间分别解决什么问题

time() 返回 Unix 秒数,适合显示日期;localtime_r() 按时区展开为年月日。代码通过 settimeofday() 设置时间,并设置 TZ=CST-8 显示北京时间。POSIX TZ 符号方向容易误读,CST-8 在这里表示 UTC+8。

系统时间可能被校准向前或向后跳变,所以测量任务间隔仍用第 06 课的 esp_timer_get_time()。日期不是可靠的“计时器差值”来源。

为什么不能每次启动都设置同一个时间

程序先判断时间是否处于合理年代,只有未校时时才写入示例日期。这个日期是教学输入,不是自动获取的真实当前时间。每次唤醒都重写相同日期,会把之前累计的十秒抵消,看起来像 RTC 没有工作。

RTC_DATA_ATTR wake_count 用于观察深睡眠唤醒次数,它保存在 RTC 内存中;它本身不负责计时,也不保证掉电保存。程序输出五次日期后进入十秒深睡眠,唤醒会重新执行 app_main(),时间应包含睡眠期间经过的秒数。

工程如何组织

本例围绕一个系统外设展开,先在 main/main.c 中按顺序展示完整调用过程;可复用的多设备驱动在后续工程中放到 components/BSP/。不要把芯片驱动的初始化藏在无法看到的外部脚本里。

应用代码:入口与处理流程

下面代码直接取自配套工程的 main/main.c,头文件和 BSP 实现在仓库中一并提供。初学时先看 app_main(),再追踪它调用的模块。

 1#include <stdio.h>
 2#include <stdint.h>
 3#include <stdbool.h>
 4#include <string.h>
 5#include <stdlib.h>
 6#include <assert.h>
 7#include "esp_err.h"
 8#include "esp_log.h"
 9#include "esp_timer.h"
10#include "freertos/FreeRTOS.h"
11#include "freertos/task.h"
12#include <time.h>
13#include <sys/time.h>
14#include "esp_sleep.h"
15#include "esp_system.h"
16#include "esp_attr.h"
17RTC_DATA_ATTR static unsigned wake_count;
18void app_main(void)
19{
20    setenv("TZ","CST-8",1); // POSIX 时区符号与通常的 UTC+8 写法方向相反。
21    tzset();
22    time_t now; time(&now);
23    // 仅在未校时状态设置示例时间,不能每次启动都覆盖已经前进的 RTC。
24    if(now<1700000000) {
25        struct tm initial={.tm_year=2026-1900,.tm_mon=9,.tm_mday=24,.tm_hour=12};
26        struct timeval tv={.tv_sec=mktime(&initial)};
27        ESP_ERROR_CHECK(settimeofday(&tv,NULL)==0 ? ESP_OK : ESP_FAIL);
28        ESP_LOGW("rtc","Using DEMO time; replace with actual time or SNTP");
29    }
30    ESP_LOGI("rtc","reset=%d wake_count=%u",esp_reset_reason(),wake_count++);
31    for(int i=0;i<5;i++) {
32        time(&now);
33        struct tm local; localtime_r(&now,&local);
34        char text[40]; strftime(text,sizeof(text),"%Y-%m-%d %H:%M:%S",&local);
35        ESP_LOGI("rtc","%s",text);
36        vTaskDelay(pdMS_TO_TICKS(1000));
37    }
38    ESP_LOGI("rtc","deep sleep 10 seconds; disconnecting power loses time");
39    ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(10000000));
40    esp_deep_sleep_start();
41}

CMake 与中文注释

1# 私有依赖只供本组件源码使用。
2idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
3    PRIV_REQUIRES esp_timer esp_hw_support)

每个工程有自己的 project() 名称和 sdkconfig.defaults。VS Code 设置已关闭普通 CMake 自动配置,C/C++ 语言服务读取 build/compile_commands.json;首次构建前文件尚不存在,应先激活 IDF 并构建,而不是手工拼头文件路径。

编译、烧录与观察

 1# 激活 EIM 安装的 IDF;每个新终端都需要激活。
 2source ~/.espressif/tools/activate_idf_v5.5.5.sh
 3# 进入单个示例根目录,不是在 examples 总目录运行。
 4cd ~/mycode/esp32-blog/examples/18-rtc-system-time
 5# 首次配置目标;已有正确配置时不用每次重复 set-target。
 6idf.py set-target esp32s3
 7# 编译会生成 build/compile_commands.json,供 VS Code 使用。
 8idf.py build
 9# 把这个值换成实际连接开发板的端口;macOS 通常为 /dev/cu.*。
10PORT=/dev/ttyUSB0
11idf.py -p "$PORT" flash monitor
12# 串口监视退出:Ctrl+]。

预期输出或屏幕内容(示意;数值、日志前缀和启动时间依现场而变):

1rtc: Using DEMO time; replace with actual time or SNTP
2rtc: 2026-10-24 12:00:00
3rtc: deep sleep 10 seconds; disconnecting power loses time

故障定位与逐项验收

先检查目标是否是 esp32s3、实际端口与供电,再按本文引脚表核对跳线。编译成功只能说明源码与工具链兼容,无法证明模块型号、物理线路或时序已经正确。

遇到通信错误先看错误码和对应模块,禁止简单用“错误时返回 0”掩盖异常。预期日志是阅读程序的参考,验收时应保存自己的现象、日志与接线照片。

与教材和官方实现对照

对照教材第 23 章正文:保留系统时间 API,增加首次校时判断、时区说明和深睡眠演示;修正原路线中容易误解的“掉电计时”标题。

本地验证记录与参考

返回课程目录