ESP32-C3 深度睡眠后快速恢复外设状态:RTC 内存实战技巧

1. 为什么需要 RTC 内存?

ESP32-C3 进入深度睡眠(Deep Sleep)后,大部分数字外设(如 UART、SPI、GPIO)的寄存器内容会丢失,仅 RTC 域(RTC_SLOW_MEM 和 RTC_FAST_MEM)保持供电。传统做法是唤醒后重新初始化外设,但若外设配置复杂(如自定义波特率、PWM 占空比、传感器校准值),重新计算会引入延迟和代码冗余。利用 RTC 内存,我们可以在睡眠前将关键状态打包存储,唤醒后直接读取并恢复,省去重新配置时间,尤其适合需要频繁唤醒采集数据的场景。

2. RTC 内存原理与分区

ESP32-C3 的 RTC 内存分为两部分:

  • RTC_FAST_MEM:8KB,可被 CPU 快速访问,但深度睡眠时可能断电(取决于电源管理配置)。
  • RTC_SLOW_MEM:8KB,始终供电,适合存储持久数据。

我们通常使用 RTC_SLOW_MEM,通过 RTC_NOINIT_ATTR 属性定义全局变量,或使用 esp_sleep_get_wakeup_cause() 配合 esp_sleep_get_wakeup_cause() 判断唤醒源。注意:RTC 内存中变量在复位(非睡眠)时不会清零,但上电复位会清零,因此需用魔数(Magic Number)校验数据有效性。

3. 实战:保存并恢复 GPIO 和 PWM 状态

3.1 硬件准备

  • ESP32-C3 DevKit
  • LED 连接到 GPIO8(PWM 通道)
  • 按键连接到 GPIO9(外部唤醒源)

3.2 代码实现

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "esp_sleep.h"
#include "esp_log.h"

#define LED_GPIO     8
#define WAKE_GPIO    9
#define MAGIC_NUM    0x5A5A

// 定义 RTC 内存结构体
RTC_NOINIT_ATTR struct {
    uint16_t magic;
    uint32_t pwm_duty;
    uint32_t pwm_freq;
    bool led_state;
} rtc_status;

// 初始化 PWM
void pwm_init(uint32_t freq, uint32_t duty) {
    ledc_timer_config_t timer_conf = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .timer_num = LEDC_TIMER_0,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .freq_hz = freq,
        .clk_cfg = LEDC_AUTO_CLK
    };
    ledc_timer_config(&timer_conf);

    ledc_channel_config_t channel_conf = {
        .gpio_num = LED_GPIO,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LEDC_CHANNEL_0,
        .intr_type = LEDC_INTR_DISABLE,
        .timer_sel = LEDC_TIMER_0,
        .duty = duty,
        .hpoint = 0
    };
    ledc_channel_config(&channel_conf);
}

// 保存状态到 RTC 内存
void save_state() {
    rtc_status.magic = MAGIC_NUM;
    rtc_status.pwm_duty = ledc_get_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
    rtc_status.pwm_freq = 5000; // 假设固定频率,实际可从配置读取
    rtc_status.led_state = gpio_get_level(LED_GPIO);
}

// 从 RTC 内存恢复状态
void restore_state() {
    if (rtc_status.magic != MAGIC_NUM) {
        // 首次启动,使用默认值
        rtc_status.pwm_duty = 512; // 50% 占空比
        rtc_status.pwm_freq = 5000;
        rtc_status.led_state = true;
    }
    pwm_init(rtc_status.pwm_freq, rtc_status.pwm_duty);
    gpio_set_level(LED_GPIO, rtc_status.led_state);
}

void app_main() {
    // 配置唤醒 GPIO
    gpio_config_t wake_cfg = {
        .pin_bit_mask = (1ULL << WAKE_GPIO),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_LOW_LEVEL
    };
    gpio_config(&wake_cfg);
    esp_sleep_enable_gpio_wakeup();

    // 检查唤醒原因
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_GPIO) {
        ESP_LOGI("MAIN", "唤醒自 GPIO,恢复状态");
        restore_state();
    } else {
        ESP_LOGI("MAIN", "首次启动,初始化默认状态");
        pwm_init(5000, 512);
        gpio_set_level(LED_GPIO, 1);
        // 保存初始状态
        save_state();
    }

    // 模拟运行 5 秒后进入深度睡眠
    vTaskDelay(pdMS_TO_TICKS(5000));

    // 更新状态并保存
    // 例如:改变 PWM 占空比
    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 768);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
    save_state();

    ESP_LOGI("MAIN", "进入深度睡眠");
    esp_deep_sleep_start();
}

3.3 代码解析

  • RTC_NOINIT_ATTR:将结构体放入 RTC_SLOW_MEM,睡眠期间数据不丢失。
  • 魔数校验:首次上电时 RTC 内存内容不确定,通过 magic 判断是否有效。
  • 保存与恢复save_state() 在睡眠前调用,restore_state() 在唤醒后调用,避免重新配置。

4. 注意事项与优化

  • RTC 内存容量:仅 8KB,避免存储大数组,建议只存关键状态。
  • 数据校验:除了魔数,可加入 CRC 校验防止数据损坏。
  • 唤醒源:本例使用 GPIO 唤醒,也可组合定时器唤醒,但需注意 esp_sleep_get_wakeup_cause() 的返回值。
  • 外设时钟:深度睡眠后,部分外设时钟关闭,唤醒后需重新使能(如 ledc_timer_resume)。
  • 电源域:确保 CONFIG_PM_SLP_DISABLE_GPIO 未开启,否则 GPIO 状态也可能丢失。
  • 调试技巧:使用 esp_sleep_get_wakeup_cause() 打印唤醒原因,便于排查。

5. 总结

通过 RTC 内存保存外设状态,ESP32-C3 在深度睡眠唤醒后能快速恢复,省去重新初始化时间,特别适合电池供电的传感器节点。本文提供的代码框架可直接移植到其他外设(如 UART 配置、I2C 地址等)。记住:合理利用 RTC 内存,但不要过度依赖,保持代码简洁。