ESP32 低功耗模式下 RTC 内存保持与 GPIO 唤醒源冲突的排查手册

1. 背景与原理

ESP32 的深度睡眠模式(Deep Sleep)下,主 CPU 和大部分外设关闭,仅保留 RTC 域(RTC 内存、RTC 外设、ULP 协处理器)供电。RTC 内存(RTC FAST Memory 和 RTC SLOW Memory)用于保存唤醒后需要恢复的变量,而 GPIO 唤醒(EXT1/EXT2)由 RTC 控制器管理,通过 RTC GPIO 矩阵检测电平变化。

冲突根源:RTC 内存和 GPIO 唤醒都依赖 RTC 域,但 RTC 内存的保持需要 RTC 电源域稳定,而 GPIO 唤醒的配置可能意外修改 RTC 内存的映射或电源状态。常见问题包括:

  • 使用 esp_sleep_enable_ext1_wakeup() 后,RTC 内存数据被清零。
  • 唤醒后 GPIO 状态异常,导致无法再次进入睡眠。
  • 在 RTC 内存中存储指针或结构体,唤醒后访问崩溃。

2. 硬件与软件基础

2.1 RTC 内存分区

  • RTC FAST Memory:位于地址 0x3FF80000,容量 8KB,访问速度快,适合存放临时变量。
  • RTC SLOW Memory:位于 0x50000000,容量 8KB,访问速度慢,但可被 ULP 协处理器访问,适合存放长期保留数据。

2.2 GPIO 唤醒源

  • EXT0:单个 GPIO 唤醒,支持高/低电平触发。
  • EXT1:多个 GPIO 组合唤醒,支持任意一个或全部触发。
  • 唤醒引脚必须配置为 RTC GPIO(通过 rtc_gpio_pullup_en() 等函数)。

3. 冲突排查步骤

3.1 检查 RTC 内存属性

在变量声明时,必须使用 RTC_DATA_ATTRRTC_NOINIT_ATTR 宏,否则变量默认放在普通 DRAM,深度睡眠后内容丢失。

RTC_DATA_ATTR int boot_count = 0;  // 每次唤醒后保留
RTC_NOINIT_ATTR char buffer[128];  // 不自动初始化,保留原始值

注意RTC_NOINIT_ATTR 变量不会被系统启动代码清零,适合存放敏感数据,但需自行管理初始化。

3.2 确认 GPIO 唤醒配置

使用 esp_sleep_enable_ext1_wakeup() 时,第二个参数是触发模式,常见错误是误用 ESP_EXT1_WAKEUP_ANY_HIGHESP_EXT1_WAKEUP_ALL_LOW,导致唤醒后 GPIO 电平被拉低/拉高,影响 RTC 域电源。

#define WAKEUP_PIN_1 GPIO_NUM_4
#define WAKEUP_PIN_2 GPIO_NUM_15

void setup_wakeup() {
    // 必须先将引脚配置为 RTC GPIO
    rtc_gpio_deinit(WAKEUP_PIN_1);
    rtc_gpio_deinit(WAKEUP_PIN_2);
    rtc_gpio_pullup_en(WAKEUP_PIN_1);
    rtc_gpio_pullup_en(WAKEUP_PIN_2);
    rtc_gpio_hold_en(WAKEUP_PIN_1);  // 保持状态,防止漏电
    rtc_gpio_hold_en(WAKEUP_PIN_2);

    esp_sleep_enable_ext1_wakeup((1ULL << WAKEUP_PIN_1) | (1ULL << WAKEUP_PIN_2), ESP_EXT1_WAKEUP_ANY_HIGH);
}

3.3 检查电源域管理

某些 GPIO 在睡眠时会进入高阻态,导致 RTC 内存供电不稳。建议在进入睡眠前,将所有未使用的 RTC GPIO 配置为下拉或上拉,并调用 esp_sleep_pd_config() 设置电源域策略。

esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);  // 保持 RTC SLOW 内存供电
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);  // 保持 RTC FAST 内存供电

3.4 验证唤醒源

使用 esp_sleep_get_wakeup_cause() 判断唤醒原因,若为 EXT1,再通过 esp_sleep_get_ext1_wakeup_status() 获取具体引脚,避免误处理。

void check_wakeup_cause() {
    switch (esp_sleep_get_wakeup_cause()) {
        case ESP_SLEEP_WAKEUP_EXT1:
            uint64_t status = esp_sleep_get_ext1_wakeup_status();
            if (status & (1ULL << WAKEUP_PIN_1)) {
                // 处理引脚1唤醒
            }
            break;
        default:
            break;
    }
}

4. 完整代码示例

以下示例演示了如何在深度睡眠中保持 RTC 计数,并通过 GPIO 唤醒,同时避免冲突。

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

RTC_DATA_ATTR int wake_count = 0;
RTC_NOINIT_ATTR char rtc_buffer[64];

#define WAKEUP_PIN GPIO_NUM_4

void app_main() {
    // 初始化串口
    printf("Wake count: %d\n", wake_count);
    wake_count++;

    // 检查唤醒原因
    if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT1) {
        uint64_t status = esp_sleep_get_ext1_wakeup_status();
        if (status & (1ULL << WAKEUP_PIN)) {
            printf("Woken by GPIO %d\n", WAKEUP_PIN);
        }
    }

    // 配置唤醒引脚
    rtc_gpio_deinit(WAKEUP_PIN);
    rtc_gpio_pullup_en(WAKEUP_PIN);
    rtc_gpio_hold_en(WAKEUP_PIN);
    esp_sleep_enable_ext1_wakeup(1ULL << WAKEUP_PIN, ESP_EXT1_WAKEUP_ANY_HIGH);

    // 确保 RTC 内存供电
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);

    // 进入深度睡眠
    printf("Entering deep sleep...\n");
    esp_deep_sleep_start();
}

5. 注意事项与常见陷阱

  • 不要使用普通全局变量:除非用 RTC_DATA_ATTRRTC_NOINIT_ATTR,否则唤醒后值为随机。
  • GPIO 保持与唤醒冲突rtc_gpio_hold_en() 会锁存引脚状态,若唤醒后未调用 rtc_gpio_hold_dis(),可能导致后续配置失效。建议在唤醒后立即释放保持。
  • 电源域配置:默认情况下,RTC 内存可能被断电以省电,必须显式配置为 ESP_PD_OPTION_ON
  • 引脚选择:并非所有 GPIO 都支持 RTC 功能,参考数据手册,通常 GPIO 0, 2, 4, 12-15, 25-27, 32-39 可用。
  • 调试技巧:使用 esp_sleep_get_wakeup_cause()esp_sleep_get_ext1_wakeup_status() 打印信息,确认是否真的由 GPIO 唤醒,避免误判。

6. 总结

RTC 内存保持与 GPIO 唤醒的冲突,本质是资源竞争和配置顺序问题。通过正确使用 RTC 属性宏、合理配置电源域、严格管理 GPIO 状态,可以完全避免。本文提供的排查步骤和代码示例,可直接应用于实际项目,帮助您快速定位问题,提升开发效率。