# 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_ATTR` 或 `RTC_NOINIT_ATTR` 宏,否则变量默认放在普通 DRAM,深度睡眠后内容丢失。 ```c 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_HIGH` 或 `ESP_EXT1_WAKEUP_ALL_LOW`,导致唤醒后 GPIO 电平被拉低/拉高,影响 RTC 域电源。 ```c #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()` 设置电源域策略。 ```c 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()` 获取具体引脚,避免误处理。 ```c 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 唤醒,同时避免冲突。 ```c #include #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_ATTR` 或 `RTC_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 状态,可以完全避免。本文提供的排查步骤和代码示例,可直接应用于实际项目,帮助您快速定位问题,提升开发效率。