ESP32 低功耗模式下 RTC 内存保持与 GPIO 唤醒源配置的坑与对策

一、RTC 内存与低功耗模式的关系

ESP32 有多种低功耗模式,其中 Deep Sleep 模式功耗最低(约 10μA),但此时 CPU、Wi-Fi、蓝牙等大部分外设都会关闭,只有 RTC 外设(包括 RTC 定时器、RTC GPIO、ULP 协处理器)和 RTC 内存(RTC Fast Memory 和 RTC Slow Memory)保持供电。

RTC 内存是 Deep Sleep 下唯一能保存数据的区域,它分为两个部分:

  • RTC Fast Memory:约 8KB,位于 RTC 电源域,CPU 在 Deep Sleep 唤醒后可直接访问。
  • RTC Slow Memory:约 8KB,同样位于 RTC 电源域,主要用于 ULP 协处理器和 RTC 外设。

在 Deep Sleep 模式下,普通 RAM(DRAM)和 Flash 都会掉电,因此任何需要在唤醒后保留的数据(如传感器校准值、运行状态、唤醒计数等)都必须存放在 RTC 内存中。

二、RTC 内存使用的坑与对策

2.1 坑:使用普通全局变量导致数据丢失

很多初学者会直接定义全局变量,期望在 Deep Sleep 后依然存在,但普通变量存储在 DRAM 中,Deep Sleep 时会丢失。

对策:使用 RTC_DATA_ATTR 宏将变量放置在 RTC Slow Memory 中。例如:

RTC_DATA_ATTR int boot_count = 0;
RTC_DATA_ATTR float calibration_value = 3.14f;

2.2 坑:RTC 内存大小限制和初始化问题

RTC Slow Memory 只有 8KB,如果存放过多数据,编译时会报错。另外,RTC 内存中的变量在每次上电时不会自动清零,因此需要手动判断是否为首次启动。

对策

  • 尽量只存放必要的数据,避免大数组。
  • 使用 esp_sleep_get_wakeup_cause() 判断唤醒原因,如果是首次上电(ESP_SLEEP_WAKEUP_UNDEFINED),则初始化变量。
void app_main() {
    if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) {
        boot_count = 0;
        calibration_value = 0.0f;
    } else {
        boot_count++;
    }
    printf("Boot count: %d\n", boot_count);
}

2.3 坑:RTC 内存未对齐导致访问异常

RTC 内存要求 4 字节对齐,否则可能导致硬件错误。

对策:使用 __attribute__((aligned(4))) 或确保结构体自然对齐。

RTC_DATA_ATTR struct {
    uint32_t counter;
    float value;
} __attribute__((aligned(4))) app_data;

三、GPIO 唤醒源配置的坑与对策

3.1 坑:GPIO 唤醒引脚选择错误

并非所有 GPIO 都能作为 Deep Sleep 的唤醒源。ESP32 只有 RTC GPIO(即 GPIO 0、2、4、12-15、25-27、32-39)支持从 Deep Sleep 唤醒。如果使用普通 GPIO(如 GPIO 5),则无法唤醒。

对策:查阅芯片数据手册,确认 RTC GPIO 列表。例如,使用 GPIO 13 作为唤醒引脚。

3.2 坑:唤醒电平配置与外部电路不匹配

esp_sleep_enable_ext0_wakeup() 只能配置为高电平或低电平唤醒,但外部按键电路可能是上拉或下拉,导致唤醒条件相反。

对策:根据外部电路选择正确的电平。例如,按键接地,平时为高电平,按下为低电平,则配置为低电平唤醒。

#define WAKEUP_PIN GPIO_NUM_13
#define WAKEUP_LEVEL 0  // 低电平唤醒

esp_sleep_enable_ext0_wakeup(WAKEUP_PIN, WAKEUP_LEVEL);

3.3 坑:多个 GPIO 唤醒源配置冲突

ext0 只能使用一个 GPIO,且不能与 ext1 同时使用。如果需求是多个 GPIO 唤醒,必须使用 ext1 模式。

对策:使用 esp_sleep_enable_ext1_wakeup() 配置多个引脚,并指定唤醒模式(任意一个或全部)。

#define WAKEUP_PIN_BITMASK (1ULL << GPIO_NUM_13) | (1ULL << GPIO_NUM_14)
esp_sleep_enable_ext1_wakeup(WAKEUP_PIN_BITMASK, ESP_EXT1_WAKEUP_ANY_LOW);

3.4 坑:唤醒后 GPIO 状态不稳定

Deep Sleep 唤醒后,RTC GPIO 会保持唤醒时的状态,但普通 GPIO 会恢复为默认状态,可能导致外部设备误动作。

对策:在进入 Deep Sleep 前,将不需要的 GPIO 设置为高阻态或固定电平,并在唤醒后重新初始化。

// 进入睡眠前
gpio_set_direction(GPIO_NUM_5, GPIO_MODE_INPUT);
gpio_set_pull_mode(GPIO_NUM_5, GPIO_PULLUP_ONLY);

四、完整配置步骤与代码示例

4.1 配置步骤

  1. 定义 RTC 内存变量,用于保存唤醒计数。
  2. app_main 中检查唤醒原因,初始化或递增计数。
  3. 配置唤醒源:使用 ext0ext1,并设置正确的电平。
  4. 配置其他 GPIO 状态,避免漏电。
  5. 调用 esp_deep_sleep_start() 进入睡眠。

4.2 完整代码示例

#include <stdio.h>
#include "esp_sleep.h"
#include "driver/gpio.h"

RTC_DATA_ATTR int wakeup_count = 0;

#define WAKEUP_PIN GPIO_NUM_13

void app_main() {
    // 检查唤醒原因
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_UNDEFINED) {
        wakeup_count = 0;
        printf("First boot\n");
    } else if (cause == ESP_SLEEP_WAKEUP_EXT0) {
        wakeup_count++;
        printf("Woken by GPIO, count: %d\n", wakeup_count);
    } else {
        printf("Woken by other: %d\n", cause);
    }

    // 配置唤醒源
    esp_sleep_enable_ext0_wakeup(WAKEUP_PIN, 0);  // 低电平唤醒

    // 设置其他 GPIO 为高阻,避免漏电
    gpio_set_direction(GPIO_NUM_5, GPIO_MODE_INPUT);
    gpio_set_pull_mode(GPIO_NUM_5, GPIO_PULLUP_ONLY);

    printf("Entering deep sleep...\n");
    esp_deep_sleep_start();
}

五、注意事项与调试技巧

  • RTC 内存数据校验:由于 RTC 内存可能因电源波动而损坏,建议在数据中增加 CRC 校验,唤醒后检查。
  • 唤醒源优先级:如果同时启用了多个唤醒源(如定时器和 GPIO),需要根据实际需求决定优先级,但 ext0ext1 不能同时启用。
  • 测量功耗:使用功耗仪或开发板上的电流表,确认实际电流是否符合预期。如果电流过大,检查是否有 GPIO 悬空或外部上拉电阻。
  • 日志输出:在调试时,使用 esp_sleep_get_wakeup_cause() 打印唤醒原因,有助于定位问题。

六、总结

ESP32 的 Deep Sleep 模式是低功耗设计的核心,但 RTC 内存和 GPIO 唤醒源配置存在不少隐藏的坑。通过使用 RTC_DATA_ATTR 保存数据、正确选择 RTC GPIO、合理配置唤醒电平,以及注意 GPIO 状态,可以避免大部分问题。希望本文的实战经验能帮助你少走弯路,设计出稳定可靠的低功耗系统。