ESP32-C3深睡眠模式下的RTC内存保持与快速唤醒外设状态恢复技巧

1. 为什么深睡眠后外设状态会丢失?

ESP32-C3进入Deep Sleep时,主CPU、SRAM(包括RTC_FAST_MEM和RTC_SLOW_MEM之外的普通内存)都会断电,仅RTC外设(如RTC定时器、RTC_GPIO)和RTC内存保持供电。因此,普通全局变量、外设寄存器配置(如UART波特率、GPIO模式)在唤醒后全部复位为默认值。若直接访问外设,可能导致通信失败或引脚电平异常。

核心挑战

  • 唤醒后CPU从复位向量启动,所有静态变量重新初始化。
  • 外设寄存器需重新配置,但某些校准数据(如ADC offset)需保留。
  • 唤醒源多样(定时器、GPIO、UART),需区分处理。

2. RTC内存:深睡眠中的“诺亚方舟”

ESP32-C3提供8KB的RTC_SLOW_MEM(地址0x50000000),在Deep Sleep期间保持数据。通过RTC_NOINIT_ATTR属性或esp_sleep_get_wakeup_cause()配合,可保存关键状态。

两种常用方法

  • 静态变量:使用RTC_NOINIT_ATTR修饰,变量存放在RTC_SLOW_MEM,不自动清零。
  • RTC快速内存RTC_FAST_ATTR(但Deep Sleep时可能断电,需确认芯片版本,C3支持)。
// 保存到RTC_SLOW_MEM的变量
RTC_NOINIT_ATTR uint32_t g_uart_baudrate;
RTC_NOINIT_ATTR uint8_t  g_gpio_state;
RTC_NOINIT_ATTR bool     g_is_initialized;

3. 快速唤醒与状态恢复流程

3.1 唤醒源配置

ESP32-C3支持多种唤醒源,常用:

  • 定时器唤醒:esp_sleep_enable_timer_wakeup(time_us)
  • GPIO唤醒:esp_sleep_enable_gpio_wakeup()(需配置EXT1或EXT0)
  • UART唤醒:esp_sleep_enable_uart_wakeup(uart_num)
// 配置GPIO唤醒(例如GPIO4下降沿)
gpio_config_t io_conf = {
    .pin_bit_mask = (1ULL << GPIO_NUM_4),
    .mode = GPIO_MODE_INPUT,
    .pull_up_en = GPIO_PULLUP_ENABLE,
    .intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&io_conf);
esp_sleep_enable_gpio_wakeup();

3.2 进入深睡眠前保存状态

在调用esp_deep_sleep_start()前,将关键外设状态写入RTC变量。

void save_peripheral_state() {
    // 保存UART波特率(假设已配置)
    g_uart_baudrate = uart_get_baudrate(UART_NUM_0);
    // 保存GPIO输出电平
    g_gpio_state = gpio_get_level(GPIO_NUM_2);
    // 标记已初始化
    g_is_initialized = true;
}

void enter_deep_sleep() {
    save_peripheral_state();
    esp_deep_sleep_start(); // 永不返回
}

3.3 唤醒后恢复外设

app_main()开头,检查唤醒原因,并恢复外设。

void restore_peripheral_state() {
    // 检查RTC内存是否有效(首次上电为随机值,建议用魔数校验)
    if (g_is_initialized && g_magic == 0xDEADBEEF) {
        // 恢复UART
        uart_set_baudrate(UART_NUM_0, g_uart_baudrate);
        // 恢复GPIO
        gpio_set_level(GPIO_NUM_2, g_gpio_state);
    } else {
        // 首次启动,执行完整初始化
        init_peripherals();
        g_magic = 0xDEADBEEF;
        g_is_initialized = true;
    }
}

void app_main() {
    // 判断唤醒源
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_GPIO) {
        // GPIO唤醒,可能需处理外部事件
        restore_peripheral_state();
        handle_gpio_event();
    } else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        // 定时唤醒,恢复后执行周期任务
        restore_peripheral_state();
        do_periodic_task();
    } else {
        // 首次上电或复位
        init_peripherals();
    }
    // 继续执行或再次进入睡眠
}

4. 完整示例:GPIO状态保持与UART恢复

以下代码演示如何保存GPIO输出状态和UART配置,并在唤醒后恢复。

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

// RTC内存变量
RTC_NOINIT_ATTR uint32_t g_magic;
RTC_NOINIT_ATTR uint32_t g_uart_baud;
RTC_NOINIT_ATTR uint8_t  g_led_state;

#define MAGIC_NUM 0xA5A5A5A5
#define LED_PIN   GPIO_NUM_2
#define WAKE_PIN  GPIO_NUM_4

void init_peripherals() {
    // 初始化UART0(默认115200)
    uart_config_t uart_cfg = {
        .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_0, &uart_cfg);
    uart_driver_install(UART_NUM_0, 1024, 0, 0, NULL);

    // 初始化LED GPIO
    gpio_reset_pin(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
    gpio_set_level(LED_PIN, 0);

    // 初始化唤醒GPIO
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << WAKE_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };
    gpio_config(&io_conf);
    esp_sleep_enable_gpio_wakeup();
}

void save_state() {
    g_uart_baud = uart_get_baudrate(UART_NUM_0);
    g_led_state = gpio_get_level(LED_PIN);
    g_magic = MAGIC_NUM;
}

void restore_state() {
    if (g_magic == MAGIC_NUM) {
        // 恢复UART波特率(假设之前改为9600)
        uart_set_baudrate(UART_NUM_0, g_uart_baud);
        // 恢复LED状态
        gpio_set_level(LED_PIN, g_led_state);
        printf("State restored: baud=%d, led=%d\n", g_uart_baud, g_led_state);
    } else {
        // 首次启动
        init_peripherals();
        printf("First boot, init done.\n");
    }
}

void app_main() {
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_GPIO) {
        restore_state();
        // 处理唤醒事件(例如翻转LED)
        gpio_set_level(LED_PIN, !gpio_get_level(LED_PIN));
    } else {
        init_peripherals();
        // 设置初始状态
        gpio_set_level(LED_PIN, 1);
        uart_set_baudrate(UART_NUM_0, 9600); // 模拟修改波特率
    }

    // 保存状态并进入深睡眠
    save_state();
    printf("Entering deep sleep...\n");
    vTaskDelay(pdMS_TO_TICKS(100)); // 等待串口输出
    esp_deep_sleep_start();
}

5. 注意事项与陷阱

  • RTC内存校验:首次上电时RTC内存内容随机,务必使用魔数(如0xDEADBEEF)验证有效性,否则可能恢复错误状态。
  • 唤醒源区分esp_sleep_get_wakeup_cause()在唤醒后立即调用,若在初始化后调用可能返回ESP_SLEEP_WAKEUP_UNDEFINED
  • GPIO唤醒配置:EXT0/EXT1唤醒仅支持特定GPIO(ESP32-C3支持GPIO0-5),且需设置esp_sleep_enable_gpio_wakeup(),但注意该函数在ESP-IDF v4.4后需配合gpio_wakeup_enable()
  • UART唤醒:若使用UART唤醒,需在进入睡眠前调用uart_set_wakeup_threshold(),且唤醒后UART可能丢失数据,需重新初始化。
  • RTC_FAST_MEM:在Deep Sleep下可能断电,建议仅使用RTC_SLOW_MEM。
  • 功耗优化:深睡眠电流约5μA,但若RTC外设(如ULP)运行,功耗会增加。

6. 总结

通过RTC内存保存关键状态,结合唤醒源判断,ESP32-C3可实现毫秒级唤醒并恢复外设,避免重复初始化。此技巧在电池供电的传感器节点、智能家居设备中至关重要。记住:状态保存要全面,校验要严格,唤醒源要明确。掌握这些,你的低功耗设计将更上一层楼。