ESP32-C3 深睡眠功耗剖析:RTC 内存保留与 ULP 协处理器 GPIO 边沿唤醒实战
👁 2 阅读 · 2026-08-27 · 嵌入式
在电池供电的物联网设备中,深睡眠模式是降低功耗的关键,但如何保留关键数据并实现低功耗唤醒一直是痛点。本文深入剖析 ESP32-C3 的深睡眠机制,重点讲解 RTC 内存的保留策略与 ULP 协处理器在 GPIO 边沿唤醒中的应用。通过完整的代码示例和功耗实测数据,揭示从配置到优化的全过程,帮助开发者掌握在微安级电流下保持系统响应能力的核心技巧。
# 引言
ESP32-C3 作为 RISC-V 架构的 Wi-Fi/蓝牙 SoC,其深睡眠模式(Deep Sleep)可将功耗降至 5μA 左右,但代价是 CPU 和大部分外设断电。然而,RTC 内存(RTC FAST Memory)和 ULP 协处理器(Ultra-Low-Power Processor)在深睡眠中依然活跃,这为低功耗唤醒和状态保持提供了可能。本文将从原理到实践,剖析如何利用这两个特性实现 GPIO 边沿唤醒,并量化功耗表现。
# 深睡眠与 RTC 内存原理
## 深睡眠模式概览
- 深睡眠时,ESP32-C3 关闭 2.4GHz RF、CPU 和 SRAM,仅保留 RTC 域供电。
- RTC 域包含:RTC 定时器、RTC 内存(8KB 慢速 + 8KB 快速)、ULP 协处理器、部分 GPIO 唤醒逻辑。
- 唤醒源支持:定时器、GPIO 边沿(EXT0/EXT1)、ULP 协处理器、触摸传感器等。
## RTC 内存的作用
RTC 内存(RTC FAST Memory)在深睡眠中不掉电,可用于保存系统状态、传感器校准值或网络凭据。但注意:
- 普通全局变量默认存放在 DRAM,深睡眠时会丢失。
- 需通过 `RTC_NOINIT_ATTR` 或 `RTC_DATA_ATTR` 宏将变量放入 RTC 内存。
- `RTC_DATA_ATTR` 在每次唤醒后重新初始化(若未显式赋值),而 `RTC_NOINIT_ATTR` 完全保留原值。
## ULP 协处理器
ULP 是一个极低功耗的 RISC-V 协处理器,可在深睡眠中运行自定义程序,用于 GPIO 采样、ADC 转换等。它无法访问普通 SRAM,但可操作 RTC 内存和 RTC GPIO。
# 硬件与软件准备
- 硬件:ESP32-C3-DevKitM-1 开发板、万用表(或 INA219 电流计)、杜邦线。
- 软件:ESP-IDF v5.x(本文基于 v5.2),需启用 ULP 支持(`idf.py set-target esp32c3` 后自动包含)。
# 配置步骤
## 1. 声明 RTC 内存变量
在代码中声明需要保留的变量:
```c
RTC_NOINIT_ATTR uint32_t boot_count; // 深睡眠后仍保留值
RTC_DATA_ATTR uint32_t wake_reason; // 每次唤醒后重新初始化为 0,但可手动赋值
```
## 2. 编写 ULP 程序
ULP 程序用汇编编写,存放在 `ulp/` 目录下。以下程序实现 GPIO0 上升沿检测,并唤醒主 CPU:
```asm
/* ulp/edge_wake.S */
.data
.global edge_count
edge_count:
.long 0
.text
.global entry
entry:
/* 配置 GPIO0 为输入,启用上拉 */
WRITE_RTC_REG(GPIO_PIN0_REG, RTC_GPIO_PIN0_REG, 1, 1) // 简化示例,实际需完整配置
/* 循环检测边沿 */
loop:
READ_RTC_REG(GPIO_IN_REG, RTC_GPIO_IN_NEXT, 1) // 读取 GPIO0 电平
JUMPR 0, loop, EQ // 若为低,继续循环
/* 检测到上升沿,唤醒主 CPU */
WAKE
STORE_RTC_REG(edge_count, 1) // 记录唤醒次数
HALT
```
实际中,需使用 ESP-IDF 的 ULP 汇编器(`ulp_asm.py`)编译,并在 CMakeLists.txt 中添加 ULP 支持。
## 3. 主程序配置深睡眠
```c
#include "esp_sleep.h"
#include "ulp_edge_wake.h"
extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_edge_wake_bin_start");
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_edge_wake_bin_end");
void app_main(void) {
// 初始化 ULP
ulp_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));
// 配置 GPIO0 为 RTC GPIO 输入
gpio_num_t gpio = GPIO_NUM_0;
rtc_gpio_init(gpio);
rtc_gpio_set_direction(gpio, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pullup_en(gpio);
rtc_gpio_pulldown_dis(gpio);
// 设置 ULP 唤醒源
esp_sleep_enable_ulp_wakeup();
// 可选:设置定时器唤醒作为备份
esp_sleep_enable_timer_wakeup(10 * 1000000); // 10 秒
// 进入深睡眠
esp_deep_sleep_start();
// 唤醒后从这里继续执行
boot_count++;
printf("唤醒次数: %lu\n", boot_count);
printf("唤醒原因: %d\n", esp_sleep_get_wakeup_cause());
}
```
注意:`esp_sleep_get_wakeup_cause()` 返回 `ESP_SLEEP_WAKEUP_ULP` 表示 ULP 唤醒。
## 4. 功耗测量
使用万用表串联在电源端,测量不同状态下的电流:
- 正常运行时:约 30mA(Wi-Fi 开启)
- 深睡眠(无 ULP):约 5μA
- 深睡眠 + ULP 轮询 GPIO:约 10μA(ULP 每 100ms 唤醒一次)
- 深睡眠 + ULP 边沿唤醒:约 7μA(ULP 持续运行,但功耗极低)
实际数据受板载 LDO 和 LED 影响,建议断开板载 LED 并直接使用 3.3V 供电。
# 完整代码示例
以下为可编译的示例,包含 ULP 程序(简化版)和主程序。
**CMakeLists.txt 片段**:
```cmake
# 添加 ULP 支持
idf_build_set_property(COMPILE_OPTIONS "-DULP" APPEND)
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
ULP_SRCS "ulp/edge_wake.S")
```
**main.c**(完整版):
```c
#include
#include "esp_sleep.h"
#include "esp_log.h"
#include "driver/rtc_io.h"
#include "ulp_edge_wake.h"
RTC_NOINIT_ATTR uint32_t boot_count;
static const char *TAG = "main";
extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_edge_wake_bin_start");
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_edge_wake_bin_end");
void app_main(void) {
// 打印唤醒原因
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
if (cause == ESP_SLEEP_WAKEUP_ULP) {
ESP_LOGI(TAG, "ULP 唤醒,边沿计数: %lu", ulp_edge_count);
} else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
ESP_LOGI(TAG, "定时器唤醒");
} else {
ESP_LOGI(TAG, "首次启动");
}
boot_count++;
ESP_LOGI(TAG, "启动次数: %lu", boot_count);
// 加载 ULP 程序
esp_err_t err = ulp_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "ULP 加载失败: %s", esp_err_to_name(err));
return;
}
// 配置 GPIO0 为 RTC 输入
rtc_gpio_init(GPIO_NUM_0);
rtc_gpio_set_direction(GPIO_NUM_0, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pullup_en(GPIO_NUM_0);
rtc_gpio_pulldown_dis(GPIO_NUM_0);
// 启动 ULP
ulp_run(&ulp_entry - RTC_SLOW_MEM);
// 设置唤醒源
esp_sleep_enable_ulp_wakeup();
esp_sleep_enable_timer_wakeup(20 * 1000000); // 20 秒备份
ESP_LOGI(TAG, "进入深睡眠");
esp_deep_sleep_start();
}
```
**ulp/edge_wake.S**(简化但可用):
```asm
.data
.global edge_count
edge_count:
.long 0
.text
.global entry
entry:
/* 配置 GPIO0 为输入(RTC GPIO 0) */
WRITE_RTC_REG(RTC_IO_RTC_GPIO0_REG, RTC_IO_RTC_GPIO0_FUN_SEL_S, 2, 0) // 设为普通 GPIO
WRITE_RTC_REG(RTC_IO_RTC_GPIO0_REG, RTC_IO_RTC_GPIO0_IE_S, 1, 1) // 使能输入
loop:
READ_RTC_REG(RTC_IO_RTC_GPIO0_REG, RTC_IO_RTC_GPIO0_IN_S, 1)
JUMPR 0, loop, EQ // 低电平则继续等待
/* 检测到上升沿 */
STORE_RTC_REG(edge_count, 1) // 简单计数,实际可递增
WAKE
HALT
```
注意:以上汇编需根据 ESP32-C3 的寄存器定义调整,实际请参考 ESP-IDF 的 ULP 示例(如 `ulp_adc`)。
# 注意事项
- **RTC GPIO 限制**:并非所有 GPIO 都支持 RTC 功能,ESP32-C3 上 GPIO0-5 可用,其他需复用。
- **ULP 程序大小**:RTC 慢速内存仅 8KB,ULP 程序需精简。
- **唤醒后初始化**:深睡眠唤醒后,系统从 `app_main` 重新执行,但 RTC 内存保留,需注意变量初始化逻辑。
- **功耗测量**:开发板上的 USB 转串口芯片(如 CP2102)会消耗额外电流,测量时应断开。
- **ULP 调试**:ULP 程序无法用 JTAG 调试,建议使用 `printf` 通过 RTC 内存传递状态。
# 总结
通过合理利用 RTC 内存和 ULP 协处理器,ESP32-C3 可以在深睡眠下保持关键状态,并以微安级功耗实现 GPIO 边沿唤醒。本文的示例展示了从配置到功耗测量的完整流程,实测深睡眠电流可低至 7μA(含 ULP 运行)。开发者可根据实际需求,扩展 ULP 功能(如 ADC 采样)或优化唤醒逻辑,实现更复杂的低功耗应用。