# 引言 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 采样)或优化唤醒逻辑,实现更复杂的低功耗应用。