ESP32 低功耗模式下的 RTC 内存备份与快速唤醒上下文恢复技巧

1. 为什么需要 RTC 内存备份?

ESP32 进入深度睡眠(Deep Sleep)时,主 CPU、Wi-Fi/蓝牙模块均断电,仅 RTC 域(RTC 时钟、RTC 内存、ULP 协处理器)保持供电。常规 RAM(SRAM)内容丢失,若需保存关键数据(如传感器校准值、网络连接状态、运行计数器),必须利用 RTC 快速内存(RTC Fast Memory,约 8KB)或 RTC 慢速内存(约 8KB)。

核心痛点:唤醒后若从 Flash 重新加载所有上下文,耗时可达数百毫秒;而 RTC 内存访问延迟极低(纳秒级),可实现“瞬时恢复”。

2. 原理:RTC 内存与唤醒流程

  • RTC Fast Memory:位于 RTC 域,地址 0x50000000 起,CPU 可直接访问,断电保持。
  • RTC Slow Memory:地址 0x50000000 之后的区域,供 ULP 协处理器使用,也可存数据。
  • 唤醒源:定时器、外部 GPIO、ULP 协处理器等。唤醒后,CPU 从 esp_wakeup_startup 开始执行,此时 RTC 内存内容原样保留。

恢复策略:在进入睡眠前,将关键变量结构体写入 RTC 内存(通过 RTC_DATA_ATTR 或指针操作);唤醒后,直接从该内存读取,无需重新初始化。

3. 配置步骤:从备份到恢复

3.1 定义 RTC 内存变量

使用 RTC_DATA_ATTR 宏将变量放入 RTC 快速内存:

// 定义上下文结构体
RTC_DATA_ATTR struct {
    uint32_t boot_count;
    float sensor_offset;
    uint8_t wifi_connected;
    char ssid[32];
} ctx = {0};

3.2 保存上下文(进入睡眠前)

void save_context() {
    ctx.boot_count++;
    ctx.sensor_offset = read_sensor_calibration(); // 从 Flash 或传感器读取
    ctx.wifi_connected = (wifi_status == CONNECTED);
    strncpy(ctx.ssid, current_ssid, sizeof(ctx.ssid));
}

3.3 配置唤醒源并进入深度睡眠

void enter_deep_sleep() {
    // 保存上下文
    save_context();
    
    // 配置唤醒源:定时器 + 外部 GPIO
    esp_sleep_enable_timer_wakeup(10 * 1000000); // 10 秒
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_4, 0); // 低电平唤醒
    
    // 进入深度睡眠
    esp_deep_sleep_start();
}

3.4 唤醒后恢复上下文

void app_main() {
    // 检查唤醒原因
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    
    if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        // 定时唤醒:直接使用 RTC 内存中的上下文
        printf("Boot count: %lu\n", ctx.boot_count);
        printf("Sensor offset: %.2f\n", ctx.sensor_offset);
        // 恢复 Wi-Fi 配置(若之前已连接)
        if (ctx.wifi_connected) {
            wifi_reconnect(ctx.ssid);
        }
    } else if (cause == ESP_SLEEP_WAKEUP_EXT0) {
        // GPIO 唤醒:可能需要重新初始化外设
        printf("GPIO wakeup, boot count: %lu\n", ctx.boot_count);
    } else {
        // 首次启动(冷启动)
        ctx.boot_count = 0;
        ctx.sensor_offset = default_offset;
        ctx.wifi_connected = false;
    }
    
    // 继续正常流程...
}

4. 高级技巧:ULP 协处理器与 RTC 内存交互

ULP 协处理器可在深度睡眠期间运行,访问 RTC 慢速内存。若需在睡眠中采集数据并唤醒,可定义共享变量:

// RTC 慢速内存(ULP 可访问)
RTC_DATA_ATTR uint16_t ulp_sample_count;
RTC_DATA_ATTR uint16_t ulp_last_value;

// ULP 程序示例(汇编或 C 编译)
// 每 100ms 采样一次,计数并存储

唤醒后,CPU 读取这些变量即可获得睡眠期间的数据,无需额外通信。

5. 注意事项与避坑指南

  • RTC 内存容量有限:快速内存仅 8KB,避免存储大数组;若需大块数据,考虑压缩或仅存关键摘要。
  • 初始化陷阱RTC_DATA_ATTR 变量在冷启动时自动清零,但若未显式初始化,其值可能为随机,务必在冷启动分支中赋初值。
  • 栈与堆:深度睡眠会丢失栈和堆,所有需保留的数据必须放在 RTC 内存,不能依赖局部变量。
  • 唤醒后外设状态:GPIO、SPI、I2C 等外设寄存器会复位,需重新初始化,但可依据上下文跳过耗时配置(如 Wi-Fi 连接)。
  • 电源域:若使用 esp_sleep_pd_config 关闭 RTC 外设电源,RTC 内存可能丢失(取决于芯片版本),默认保持供电。
  • 调试技巧:使用 esp_sleep_get_wakeup_cause() 区分冷启动与热启动,便于测试。

6. 完整示例:定时唤醒计数器

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

RTC_DATA_ATTR uint32_t wake_count = 0;

void app_main() {
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    
    if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        wake_count++;
        printf("Wake count: %lu\n", wake_count);
    } else {
        wake_count = 0;
        printf("Cold boot\n");
    }
    
    // 配置 GPIO4 作为唤醒源(可选)
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << GPIO_NUM_4),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
    };
    gpio_config(&io_conf);
    
    // 进入深度睡眠 5 秒
    esp_sleep_enable_timer_wakeup(5 * 1000000);
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_4, 0);
    printf("Entering deep sleep\n");
    esp_deep_sleep_start();
}

编译并烧录后,观察串口输出,每次定时唤醒计数递增,GPIO 唤醒同样递增,冷启动清零。

7. 总结

RTC 内存备份是 ESP32 低功耗设计的核心技巧。通过合理利用 RTC_DATA_ATTR 和唤醒原因判断,你可以实现:

  • 微安级待机功耗(深度睡眠典型 10μA 左右)
  • 毫秒级唤醒恢复(无需重新初始化 Wi-Fi 或传感器)
  • 睡眠期间数据采集(配合 ULP)

掌握这些技巧,你的嵌入式设备将更省电、更响应。实践时注意内存容量和初始化逻辑,避免常见陷阱。