# ESP32-C3 低功耗模式下 RTC 内存保持与 ULP 协处理器唤醒延迟的精确测量 ## 引言 在电池供电的IoT设备中,低功耗设计至关重要。ESP32-C3 提供了深度睡眠(Deep Sleep)模式,其中 RTC 内存(RTC Fast Memory)可在睡眠期间保持数据,而 ULP 协处理器(Ultra-Low-Power Processor)能在主 CPU 休眠时执行简单任务,如传感器轮询。然而,唤醒延迟直接影响系统实时性,本文聚焦于如何精确测量 ULP 唤醒主 CPU 的延迟,并验证 RTC 内存数据的可靠性。 ## 原理讲解 ### RTC 内存保持 ESP32-C3 的 RTC 快速内存(RTC Fast Memory)大小为 8KB,位于 RTC 电源域,在深度睡眠时保持供电。数据可存于此区域,但需注意: - 使用 `RTC_NOINIT_ATTR` 属性将变量放置于 RTC 内存,避免初始化清零。 - 深度睡眠唤醒后,主 CPU 可正常访问这些变量。 ### ULP 协处理器与唤醒延迟 ULP 协处理器可独立于主 CPU 运行,通过 RTC 定时器或 GPIO 触发。当 ULP 检测到条件满足(如 ADC 值超阈值),可调用 `WAKE` 指令唤醒主 CPU。唤醒延迟定义为:ULP 发出唤醒信号到主 CPU 开始执行用户代码的时间。该延迟包括: - ULP 指令执行时间(微秒级) - 系统从深度睡眠恢复时间(约 1-2 ms) - 引导加载器执行时间(若未配置快速启动) 精确测量需借助内置计数器,如 `esp_timer` 或 RTC 定时器,但注意在唤醒初期系统时钟尚未稳定,建议使用 RTC 时钟(如 `rtc_time_get()`)或读取 `esp_timer_get_time()`(在系统初始化后)。 ## 配置步骤 ### 1. 项目环境 使用 ESP-IDF v5.x,创建新项目,目标芯片为 ESP32-C3。 ### 2. 定义 RTC 内存变量 在代码中声明变量,并放置于 RTC 内存: ```c #include "esp_attr.h" RTC_NOINIT_ATTR uint32_t rtc_data_counter; RTC_NOINIT_ATTR uint64_t rtc_timestamp; ``` ### 3. 编写 ULP 程序 ULP 程序使用汇编或 C 语言(通过 `ulp_riscv` 工具链)。以下示例使用汇编,实现简单的计数并唤醒: ```c // ulp_program.S #include "soc/rtc_cntl_reg.h" #include "soc/rtc_io_reg.h" #include "soc/soc_ulp.h" .data .global ulp_entry ulp_entry: // 递增 RTC 内存中的计数器(地址需映射) move r3, rtc_data_counter_addr ld r2, r3, 0 add r2, r2, 1 st r2, r3, 0 // 唤醒主 CPU wake // 停止 ULP halt ``` 注意:ULP 访问 RTC 内存需使用绝对地址,可通过 `&rtc_data_counter` 获取,但需在编译时映射。更简单的方式是使用 `ulp_riscv` 宏,但为清晰起见,此处用汇编。实际项目中,建议使用 C 语言编写 ULP 程序,通过 `ulp_riscv` 工具链编译。 ### 4. 主程序配置 主程序负责初始化 ULP、设置深度睡眠,并测量唤醒延迟。 ```c #include #include "esp_sleep.h" #include "ulp.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" extern const uint8_t ulp_program_start[] asm("_binary_ulp_program_start"); extern const uint8_t ulp_program_end[] asm("_binary_ulp_program_end"); // RTC 内存变量地址(需与 ULP 中一致) extern uint32_t rtc_data_counter; void app_main(void) { // 初始化 ULP esp_err_t err = ulp_load_binary(0, ulp_program_start, (ulp_program_end - ulp_program_start) / sizeof(uint32_t)); if (err != ESP_OK) { printf("ULP load failed: %s\n", esp_err_to_name(err)); return; } // 启动 ULP ulp_run(0); // 记录唤醒前时间(使用 RTC 时钟) uint64_t wake_time_before = esp_timer_get_time(); // 注意:深度睡眠前调用,esp_timer 在睡眠时停止,但此处仅作参考 // 进入深度睡眠,设置唤醒源为 ULP esp_sleep_enable_ulp_wakeup(); esp_deep_sleep_start(); // 唤醒后从这里继续执行 uint64_t wake_time_after = esp_timer_get_time(); // 系统时钟已恢复 printf("ULP wakeup! Counter = %u\n", rtc_data_counter); printf("Wakeup delay (esp_timer) = %llu us\n", wake_time_after - wake_time_before); // 更精确的测量:使用 RTC 定时器(在唤醒后立即读取) // 注意:esp_timer 在唤醒后需要初始化,但 RTC 时钟一直运行 // 我们可以在 ULP 唤醒前记录 RTC 时间,但 ULP 无法直接访问 RTC 寄存器,需通过主 CPU 在睡眠前设置一个标记 // 此处简化,使用 esp_timer 差值,但实际延迟应使用 RTC 计数器 // 重新配置 ULP 继续运行,或进入睡眠 // 示例:再次启动 ULP 并睡眠 ulp_run(0); esp_deep_sleep_start(); } ``` ### 5. 精确测量唤醒延迟 上述 `esp_timer` 差值包含了深度睡眠前的准备时间(如 `esp_deep_sleep_start` 内部操作),并非纯唤醒延迟。更精确的方法: - 在 ULP 唤醒主 CPU 前,ULP 可写入一个 RTC 内存变量记录时间戳(使用 RTC 定时器计数)。 - 主 CPU 唤醒后立即读取该时间戳,并与当前 RTC 时间比较。 但 ULP 无法直接读取 RTC 定时器,需通过 `RTC_CNTL` 寄存器。可在 ULP 中读取 `RTC_CNTL_TIME0_REG` 和 `RTC_CNTL_TIME1_REG`,但需注意这些寄存器在 ULP 中可访问。以下为改进方案: ```c // ULP 程序(汇编)中读取 RTC 时间寄存器 // 假设 RTC_CNTL_TIME0_REG 地址为 0x3f408000 + 0x10 #define RTC_CNTL_TIME0_REG 0x3f408010 #define RTC_CNTL_TIME1_REG 0x3f408014 // 读取时间戳 move r3, RTC_CNTL_TIME0_REG ld r2, r3, 0 move r4, rtc_timestamp_addr st r2, r4, 0 move r3, RTC_CNTL_TIME1_REG ld r2, r3, 0 move r4, rtc_timestamp_addr + 4 st r2, r4, 0 ``` 主程序唤醒后,读取 `rtc_timestamp`,并与当前 RTC 时间比较,差值即为唤醒延迟。RTC 时钟频率为 150 kHz(可配置),需转换为微秒。 ## 完整代码示例 以下为整合后的主程序,使用 RTC 时间戳测量延迟: ```c #include #include "esp_sleep.h" #include "ulp.h" #include "esp_timer.h" #include "soc/rtc_cntl_reg.h" #include "soc/rtc.h" // RTC 内存变量 extern uint32_t rtc_data_counter; extern uint64_t rtc_timestamp; // 由 ULP 写入 // ULP 程序二进制 extern const uint8_t ulp_program_start[] asm("_binary_ulp_program_start"); extern const uint8_t ulp_program_end[] asm("_binary_ulp_program_end"); void app_main(void) { // 初始化 ULP esp_err_t err = ulp_load_binary(0, ulp_program_start, (ulp_program_end - ulp_program_start) / sizeof(uint32_t)); if (err != ESP_OK) { printf("ULP load failed: %s\n", esp_err_to_name(err)); return; } // 启动 ULP ulp_run(0); // 进入深度睡眠,等待 ULP 唤醒 esp_sleep_enable_ulp_wakeup(); esp_deep_sleep_start(); // 唤醒后执行 // 读取 ULP 写入的时间戳(RTC 计数) uint64_t ulp_timestamp = rtc_timestamp; // 获取当前 RTC 时间(微秒) uint64_t current_time = rtc_time_get(); // 返回微秒 // 计算延迟(注意:ULP 写入的时间戳是 RTC 计数,需转换为微秒) // RTC 时钟频率为 150kHz,周期约 6.67us,但 rtc_time_get 返回微秒,需统一 // 更简单:ULP 写入的时间戳也是通过 rtc_time_get 类似的函数?不,ULP 直接读寄存器,需转换 // 此处假设 rtc_timestamp 存储的是微秒值(由 ULP 计算) // 实际中,ULP 可调用 rtc_time_get 的汇编版本,但复杂。此处简化,使用 esp_timer 差值 // 使用 esp_timer 差值(包含准备时间,但可接受) uint64_t wake_time = esp_timer_get_time(); // 由于我们无法获得睡眠前时间,此处仅打印计数器 printf("ULP wakeup! Counter = %u\n", rtc_data_counter); printf("Current time = %llu us\n", wake_time); // 重新进入睡眠 ulp_run(0); esp_deep_sleep_start(); } ``` 注意:上述代码中,`rtc_time_get()` 在唤醒后调用,但 ULP 写入的时间戳是原始寄存器值,需转换。为简化,建议在 ULP 中直接使用 `RTC_CNTL` 寄存器读取,并在主程序中转换。 ## 注意事项 - **RTC 内存访问**:ULP 访问 RTC 内存时,地址必须使用物理地址,且需确保变量未优化掉。使用 `RTC_NOINIT_ATTR` 可防止初始化。 - **唤醒延迟测量**:`esp_timer` 在深度睡眠期间停止,因此差值包含了睡眠前的时间,不准确。建议使用 RTC 定时器,但需注意 RTC 时钟频率和精度。 - **ULP 程序调试**:ULP 程序无法直接打印,可通过 RTC 内存传递数据。 - **电源域**:确保 RTC 外设(如 ULP)的电源域在睡眠时保持开启,可通过 `esp_sleep_pd_config` 配置。 - **快速启动**:启用 CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP 可减少唤醒时间。 - **测量多次**:延迟可能因温度、电压波动,建议多次测量取平均值。 ## 结语 通过本文,您应能掌握 ESP32-C3 在深度睡眠下 RTC 内存的使用和 ULP 唤醒延迟的测量方法。实际应用中,精确的延迟测量有助于优化低功耗系统的实时性。建议结合逻辑分析仪或示波器验证测量结果,以获得更可靠的时序数据。