引言

ESP32 的低功耗模式(尤其是 Deep Sleep)是物联网设备省电的关键,但很多开发者在使用 RTC 内存保持数据和 UART 唤醒时,会遇到数据丢失或无法唤醒的问题。本文基于 ESP-IDF v4.4 实测,梳理 RTC 内存的可用区域、UART 唤醒的硬件限制,以及唤醒后的系统状态,并提供完整的代码示例。

1. RTC 内存保持的边界条件

1.1 RTC 内存分区

ESP32 的 RTC 内存分为两个区域:

  • RTC Fast Memory(8KB,地址 0x3FF80000):可被 CPU 和 DMA 访问,适合存放关键变量。
  • RTC Slow Memory(8KB,地址 0x50000000):仅可被 RTC 外设访问,通常用于备份。

在 Deep Sleep 模式下,RTC 内存由 RTC 电源域供电,数据可保持,但 普通 RAM(DRAM/IRAM)会掉电

1.2 数据保持的实测条件

  • 使用 esp_sleep_pd_config() 可配置电源域,但默认情况下 RTC 电源域是开启的。
  • 若调用 esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF),则 RTC Slow Memory 会掉电,数据丢失。
  • 实测中,RTC Fast Memory 始终可用,但需注意其容量有限(8KB),且与 WiFi/BT 蓝牙栈共享(若启用则可用空间减少)。

1.3 数据保持的代码示例

#include "esp_sleep.h"
#include "esp_attr.h"

// 放在 RTC Fast Memory 中的变量
RTC_DATA_ATTR int boot_count = 0;

void app_main() {
    // 每次唤醒后递增
    boot_count++;
    printf("Boot count: %d\n", boot_count);

    // 配置 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);

    // 进入深度睡眠 10 秒
    esp_sleep_enable_timer_wakeup(10 * 1000000);
    esp_deep_sleep_start();
}

注意:RTC_DATA_ATTR 宏将变量放入 RTC Fast Memory,但若同时启用 WiFi,则可用空间会减少,需检查链接错误。

2. UART 唤醒的边界条件

2.1 硬件限制

ESP32 的 UART 唤醒并非所有引脚都支持。根据芯片手册,仅 RTC GPIO(即 GPIO 0-15,但 GPIO 6-11 用于 SPI Flash,不可用)可触发 UART 唤醒。具体来说,UART 唤醒需要将 UART 的 RX 引脚映射到 RTC GPIO 上,且该引脚必须支持外部中断。

实测发现:

  • 支持唤醒的引脚:GPIO 0, 2, 4, 12, 13, 14, 15(但 GPIO 12 有上拉,需注意电平)。
  • 不支持:GPIO 16-39(非 RTC GPIO)。

2.2 唤醒逻辑

UART 唤醒的触发条件是 RX 引脚上的下降沿(即从高电平到低电平)。因此,在进入睡眠前,必须将 RX 引脚配置为输入模式,并启用 UART 的唤醒功能。

2.3 配置步骤

  1. 初始化 UART,并设置 RX 引脚为 RTC GPIO。
  2. 调用 uart_set_wakeup_threshold() 设置唤醒阈值(通常为 1,表示一个下降沿)。
  3. 调用 esp_sleep_enable_uart_wakeup() 使能 UART 唤醒。
  4. 进入 Deep Sleep。

2.4 完整代码示例

#include "driver/uart.h"
#include "esp_sleep.h"

#define UART_NUM UART_NUM_1
#define RX_PIN   4  // 必须是 RTC GPIO

void app_main() {
    // 配置 UART1,波特率 115200,RX 引脚 GPIO4
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
    };
    uart_param_config(UART_NUM, &uart_config);
    uart_set_pin(UART_NUM, UART_PIN_NO_CHANGE, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // 启用 UART 唤醒,阈值 1(下降沿)
    uart_set_wakeup_threshold(UART_NUM, 1);
    esp_sleep_enable_uart_wakeup(UART_NUM);

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

// 唤醒后,需要重新初始化 UART,因为外设状态已丢失
void wakeup_handler() {
    // 重新初始化 UART,并配置中断等
}

3. 唤醒后的系统状态

3.1 时钟与电源域

  • Deep Sleep 唤醒后,系统从复位向量启动,主时钟(PLL)和 XTAL 均关闭,默认使用内部 RC 振荡器(约 8MHz),直到调用 esp_clk_init()esp_pm_configure() 重新配置。
  • RTC 外设(如 RTC 定时器)保持,但 UART、SPI 等外设需重新初始化。

3.2 内存状态

  • RTC 内存数据保持,但普通变量(非 RTC_DATA_ATTR)会丢失。
  • 堆栈指针在启动时指向 RTC 内存,因此 app_main 中的局部变量在唤醒后是未定义的,需谨慎使用。

3.3 实测注意事项

  • 唤醒后,UART 的 RX 引脚可能仍处于输入状态,需重新配置为 UART 功能。
  • 若使用 WiFi,唤醒后需重新初始化 WiFi 栈,且 RTC Fast Memory 中用于 WiFi 的部分会被覆盖。

4. 常见陷阱与解决方案

  • 陷阱1:使用非 RTC GPIO 作为 UART RX → 唤醒失败。解决:改用 GPIO 0-15 中的可用引脚。
  • 陷阱2:RTC 内存电源域被关闭 → 数据丢失。解决:确保 esp_sleep_pd_config 未关闭 RTC 内存域。
  • 陷阱3:唤醒后未重新初始化 UART → 无法通信。解决:在唤醒后调用 uart_driver_install 等初始化函数。
  • 陷阱4:唤醒阈值设置不当 → 误唤醒或漏唤醒。解决:根据信号电平设置阈值,通常为 1 或 2。

5. 总结

ESP32 的 RTC 内存保持和 UART 唤醒是可行的,但边界条件明确:RTC 内存仅限 RTC 电源域,UART 唤醒仅限 RTC GPIO。开发者需仔细阅读芯片手册,并在代码中显式配置电源域和唤醒源。本文的实测代码可作为基础模板,但实际项目中需根据具体硬件调整引脚和阈值。

最后,建议在进入睡眠前,将所有外设置于低功耗状态,并检查电源域配置,以最大化电池寿命。