引言

ESP32 的深度睡眠模式(Deep Sleep)是低功耗应用的核心,它允许 CPU 关闭,但 RTC 外设和 RTC 内存(RTC FAST Memory)保持供电,用于保存唤醒后的状态。同时,GPIO 唤醒源(如 EXT1)允许外部信号唤醒芯片。然而,许多开发者发现,当同时使用 RTC 内存和 GPIO 唤醒时,会出现数据被清零或唤醒失败的现象。这通常不是芯片缺陷,而是配置冲突。本文将深入剖析冲突机理,并给出可复现的解决方案。

1. 冲突根源:RTC 内存与 GPIO 的电源域和引脚复用

1.1 电源域隔离

ESP32 的 RTC 外设(包括 RTC 内存)由 RTC 电源域供电,在深度睡眠时保持开启。而 GPIO 唤醒源(EXT0/EXT1)由 RTC 控制器管理,但部分 GPIO 引脚(如 GPIO0-5)在睡眠时可能被内部上拉/下拉电阻影响,或与 RTC 内存的访问冲突。

1.2 引脚复用冲突

  • RTC 内存访问:RTC 内存地址映射在 0x50000000 区域,但某些 GPIO 的 RTC 功能(如 RTC_GPIO)与 JTAG 或 Flash 引脚复用。
  • GPIO 唤醒源:EXT1 支持多个 GPIO,但要求这些引脚必须配置为 RTC 模式(RTC_GPIO),否则在睡眠时引脚状态不稳定。

关键点:如果用于唤醒的 GPIO 同时被 RTC 内存的某些特殊功能(如 RTC 时钟输出)占用,则会导致唤醒信号无法正确传递。

2. 典型冲突场景

  • 场景 A:使用 GPIO0 作为唤醒源,但 GPIO0 在启动时用于 Flash 启动模式,若未正确配置为 RTC 功能,睡眠后 GPIO0 被拉高,导致无法唤醒。
  • 场景 B:在 RTC 内存中存储数据,但初始化时调用了 esp_sleep_pd_config 关闭了 RTC 电源域的一部分,导致内存数据丢失。
  • 场景 C:多个 GPIO 配置为 EXT1 唤醒,但其中一个引脚与 RTC 内存的校验引脚冲突,造成系统复位。

3. 系统化排查步骤

3.1 检查电源域配置

确保未禁用 RTC 电源域。使用 esp_sleep_pd_config 时,只关闭不必要的域(如 RTC_PERIPH),但保留 ESP_PD_DOMAIN_RTC_FAST_MEM

// 正确示例:只关闭外设,保留 RTC 内存
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
// 不要关闭 RTC_FAST_MEM,否则数据丢失

3.2 确认 GPIO 的 RTC 模式

使用 rtc_gpio 系列函数配置唤醒引脚,而不是普通 GPIO。

// 配置 GPIO0 为 RTC 功能,并启用下拉电阻(根据唤醒电平)
rtc_gpio_init(GPIO_NUM_0);
rtc_gpio_set_direction(GPIO_NUM_0, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en(GPIO_NUM_0);

3.3 检查唤醒源优先级

ESP32 支持多个唤醒源,但 EXT0 和 EXT1 不能同时使用。若使用 EXT1,确保所有引脚在睡眠前处于确定状态。

3.4 验证 RTC 内存读写

在进入睡眠前,写入测试数据,唤醒后读取,以判断是否冲突。

// 写入 RTC 内存
uint32_t* rtc_data = (uint32_t*)0x50000000;
rtc_data[0] = 0xDEADBEEF;

4. 完整代码示例:深度睡眠 + RTC 内存 + GPIO 唤醒

以下代码演示了正确配置,避免冲突。

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

#define WAKE_GPIO GPIO_NUM_4
#define RTC_MEM_ADDR 0x50000000

void app_main() {
    // 1. 初始化 RTC 内存(写入测试数据)
    uint32_t* rtc_data = (uint32_t*)RTC_MEM_ADDR;
    rtc_data[0] = 0x12345678;
    printf("写入 RTC 内存: 0x%x\n", rtc_data[0]);

    // 2. 配置 GPIO 唤醒源(EXT1)
    // 使用 rtc_gpio 初始化,避免普通 GPIO 配置
    rtc_gpio_init(WAKE_GPIO);
    rtc_gpio_set_direction(WAKE_GPIO, RTC_GPIO_MODE_INPUT_ONLY);
    rtc_gpio_pulldown_en(WAKE_GPIO);  // 低电平唤醒

    // 3. 设置 EXT1 唤醒,只检测该引脚
    esp_sleep_enable_ext1_wakeup(1ULL << WAKE_GPIO, ESP_EXT1_WAKEUP_ANY_LOW);

    // 4. 配置电源域:保留 RTC 内存,关闭其他外设
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
    // 注意:不要关闭 ESP_PD_DOMAIN_RTC_FAST_MEM

    // 5. 进入深度睡眠
    printf("进入深度睡眠...\n");
    esp_deep_sleep_start();

    // 唤醒后从这里继续(但实际是重新启动,需通过复位原因判断)
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_EXT1) {
        printf("由 GPIO 唤醒\n");
        // 读取 RTC 内存数据
        printf("RTC 内存数据: 0x%x\n", rtc_data[0]);
        if (rtc_data[0] == 0x12345678) {
            printf("数据保持成功\n");
        } else {
            printf("数据丢失!\n");
        }
    }
}

5. 注意事项

  • 避免使用 GPIO0-3 作为唤醒源,除非明确处理启动模式,否则可能无法唤醒。
  • RTC 内存大小有限(约 8KB),只存储关键数据,并注意对齐。
  • 唤醒后系统会重新启动app_main 会再次执行,需通过 esp_sleep_get_wakeup_cause 判断是否从睡眠唤醒,以决定是否恢复数据。
  • 调试时,可先用 esp_sleep_get_wakeup_cause 打印原因,若为 ESP_SLEEP_WAKEUP_UNDEFINED,则说明唤醒源配置错误。
  • 电源域配置esp_sleep_pd_config 必须在 esp_deep_sleep_start 之前调用,且不要关闭 ESP_PD_DOMAIN_RTC_FAST_MEM

6. 总结

ESP32 低功耗模式下的 RTC 内存与 GPIO 唤醒冲突,多源于电源域误配置和引脚复用错误。通过使用 rtc_gpio 函数、正确设置电源域、并验证唤醒原因,可以彻底解决。建议在项目初期就建立最小测试用例,确保基础功能正常后再扩展。希望本文的排查思路和代码示例能帮助你快速定位问题,实现稳定可靠的低功耗设计。