ESP32 低功耗边界探索:RTC 内存保持与 ULP 协处理器协同唤醒的工程实践

1. 背景与问题

在物联网节点、可穿戴设备等场景中,ESP32 常需在微安级电流下运行。深度睡眠(Deep Sleep)模式可将主 CPU 关闭,仅保留 RTC 域和 ULP 协处理器。但许多开发者发现:ULP 无法访问所有 RTC 内存,且唤醒后主 CPU 可能因内存初始化而丢失数据。这源于对 ESP32 内存映射和电源域划分的理解不足。

2. 硬件架构:RTC 内存与 ULP 的物理边界

ESP32 的 RTC 域包含两类内存:

  • RTC FAST Memory:容量 8KB,地址 0x3FF80000 - 0x3FF81FFF。ULP 协处理器可直接访问,用于存放 ULP 程序和数据。
  • RTC SLOW Memory:容量 8KB,地址 0x50000000 - 0x50001FFF。主 CPU 可读写,但 ULP 无法直接访问(需通过 RTC FAST 中转)。

ULP 协处理器是独立的 RISC-V 核心,运行在 RTC 域,其指令集和地址空间受限。关键边界:ULP 只能执行 RTC FAST 中的代码,且数据访问也限于该区域。因此,若需在 ULP 中处理传感器数据,必须将数据放在 RTC FAST 中。

3. 协同唤醒的边界条件

3.1 唤醒源选择

  • ULP 定时唤醒:ULP 可配置定时器,周期性唤醒主 CPU,适合轮询传感器。
  • ULP GPIO 唤醒:ULP 可监测 GPIO 电平变化,适合外部事件触发。
  • 两者结合:ULP 先处理事件,再通过 ulp_set_wakeup_period 或 GPIO 唤醒主 CPU。

3.2 内存保持的边界

  • 深度睡眠期间,RTC SLOW 内存保持数据,但 RTC FAST 内存默认不保持(除非设置 esp_sleep_pd_configESP_PD_DOMAIN_RTC_FAST_MEMESP_PD_OPTION_ON)。
  • 若 ULP 程序需要保留中间变量,必须显式配置 RTC FAST 内存保持,否则唤醒后数据清零。

4. 配置步骤与代码示例

4.1 配置 RTC 内存保持

#include "esp_sleep.h"
#include "soc/rtc.h"

void configure_rtc_mem() {
    // 保持 RTC FAST 内存(ULP 程序和数据)
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
    // 保持 RTC SLOW 内存(主 CPU 数据)
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
}

4.2 编写 ULP 程序

ULP 程序需用汇编或 C 子集编写,并放置在 RTC FAST 内存。以下示例:ULP 读取 ADC,若超过阈值则唤醒主 CPU。

// ulp_adc_example.S
.data
adc_value: .long 0
threshold: .long 1000

.text
global entry
entry:
    // 读取 ADC(假设已配置)
    adc_read 0, adc_value
    // 比较
    lw t0, adc_value
    lw t1, threshold
    blt t0, t1, no_wake
    // 唤醒主 CPU
    wake
no_wake:
    halt

4.3 主 CPU 加载 ULP 并进入深度睡眠

#include "esp_ulp.h"
#include "ulp_adc_example.h"

extern const uint8_t ulp_main_bin[];
extern const size_t ulp_main_bin_size;

void app_main() {
    // 加载 ULP 程序
    esp_err_t err = ulp_load_binary(0, ulp_main_bin, ulp_main_bin_size);
    if (err != ESP_OK) return;

    // 配置 ULP 唤醒源
    ulp_set_wakeup_period(0, 1000000); // 1s 周期
    esp_sleep_enable_ulp_wakeup();

    // 配置 RTC 内存保持
    configure_rtc_mem();

    // 启动 ULP
    ulp_run(&ulp_entry - RTC_SLOW_MEM);

    // 进入深度睡眠
    esp_deep_sleep_start();
}

4.4 唤醒后处理

唤醒后,主 CPU 从 esp_deep_sleep_start 返回,可读取 RTC SLOW 内存中的共享数据。

void app_main() {
    // 检查唤醒原因
    if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_ULP) {
        // 读取 RTC SLOW 内存中的标志
        uint32_t flag = RTC_SLOW_MEM[0];
        // 处理数据...
    }
}

5. 注意事项与边界陷阱

  • RTC FAST 内存容量限制:仅 8KB,ULP 程序和数据需紧凑设计,避免溢出。
  • ULP 无法访问 RTC SLOW:若需共享数据,主 CPU 应在睡眠前将数据写入 RTC SLOW,ULP 通过 RTC FAST 中转(如使用指针映射)。
  • 电源域配置:若未开启 RTC FAST 保持,ULP 程序在唤醒后可能丢失,导致下次睡眠无法运行。
  • 唤醒延迟:ULP 唤醒主 CPU 后,主 CPU 需重新初始化外设,但 RTC 内存数据可直接使用,无需重新加载。
  • 调试技巧:使用 esp_sleep_get_wakeup_cause 区分唤醒源,避免误判。

6. 总结

ESP32 的低功耗设计并非简单调用 deep_sleep,而是需要深入理解 RTC 内存的分区与 ULP 的访问边界。通过合理配置电源域、精心布局 ULP 程序,可以实现微安级电流下的智能唤醒。记住:边界条件决定成败,在工程中务必验证内存保持和唤醒路径。