ESP32-C3 低功耗模式下 RTC 内存保持与 ULP 协处理器唤醒延迟的精确测量
👁 2 阅读 · 2026-08-27 · 嵌入式
本文深入探讨ESP32-C3在深度睡眠(Deep Sleep)模式下RTC内存的数据保持机制,以及ULP协处理器唤醒延迟的精确测量方法。通过实际代码示例,展示如何配置RTC内存、编写ULP程序,并利用内置定时器测量唤醒时间,为低功耗IoT应用提供可靠的时序参考。
# 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 唤醒延迟的测量方法。实际应用中,精确的延迟测量有助于优化低功耗系统的实时性。建议结合逻辑分析仪或示波器验证测量结果,以获得更可靠的时序数据。