ESP32 低功耗模式下 RTC 内存数据丢失的边界条件与持久化技巧

1. 为什么 RTC 内存会丢数据?

ESP32 的 RTC 内存(RTC FAST Memory)在深度睡眠(Deep Sleep)期间由 RTC 域供电,可以保持数据不丢失。但很多开发者发现,在某些情况下数据会清零或变成随机值。这并非硬件 bug,而是由以下边界条件触发:

  • 电源模式(Power Domain):如果使用 esp_sleep_pd_config() 将 RTC 外设或 RTC 内存的电源域关闭(如 ESP_PD_DOMAIN_RTC_FAST_MEM 设为 ESP_PD_OPTION_OFF),则深度睡眠时 RTC 内存会掉电,数据必然丢失。
  • 复位源(Reset Source):如果唤醒后系统发生软件复位(esp_restart())或看门狗复位,RTC 内存内容可能被清零(取决于复位类型)。
  • 编译选项:如果使用 CONFIG_ESP32_RTCDATA_IN_FAST_MEMCONFIG_ESP32_RTCDATA_IN_SLOW_MEM 配置不当,可能导致 RTC 内存段被覆盖。
  • 烧录或下载模式:当通过串口烧录新固件时,RTC 内存会被初始化,因此旧数据必然丢失。

2. 正确使用 RTC 内存

2.1 声明 RTC 变量

在代码中,可以使用 RTC_DATA_ATTRRTC_NOINIT_ATTR 宏来定义 RTC 内存变量:

// 该变量在深度睡眠后保留,但每次上电或烧录后清零
RTC_DATA_ATTR int boot_count = 0;

// 该变量在软件复位后也保留(不自动清零)
RTC_NOINIT_ATTR int rtc_noinit_var;
  • RTC_DATA_ATTR:在系统启动时,如果从深度睡眠唤醒,该变量会保留;但如果是冷启动或软件复位,则会被清零(因为启动代码会初始化 .rtc.data 段)。
  • RTC_NOINIT_ATTR:该变量不会被启动代码初始化,因此即使软件复位,只要 RTC 域不掉电,数据就保留。但注意,如果 RTC 域掉电(如拔掉电源),则内容不确定。

2.2 检查复位原因

为了判断数据是否有效,建议在启动时读取复位原因:

#include "esp_sleep.h"
#include "esp_system.h"

void check_reset_reason() {
    esp_reset_reason_t reason = esp_reset_reason();
    switch (reason) {
        case ESP_RST_DEEPSLEEP:
            // 从深度睡眠唤醒,RTC 数据有效
            break;
        case ESP_RST_POWERON:
            // 上电复位,RTC 数据无效,需要初始化
            boot_count = 0;
            break;
        case ESP_RST_SW:
            // 软件复位,RTC_NOINIT 数据可能有效,但 RTC_DATA 可能被清零
            break;
        default:
            // 其他原因,谨慎处理
            break;
    }
}

3. 边界条件详解

3.1 电源域配置

在进入深度睡眠前,如果显式关闭 RTC 内存电源域,数据会丢失:

// 错误示例:关闭 RTC 快速内存电源域
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);

// 正确做法:保持默认(ON)或设为自动
// esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_AUTO);

注意:ESP_PD_DOMAIN_RTC_SLOW_MEM 同理,但通常我们使用 FAST MEM 作为 RTC 数据段。

3.2 深度睡眠唤醒源

不同的唤醒源(定时器、GPIO、触摸等)不会影响 RTC 内存,但如果在唤醒后立即执行 esp_restart(),则可能触发软件复位,导致 RTC_DATA_ATTR 变量被清零。

3.3 编译选项的影响

menuconfig 中,CONFIG_ESP32_RTCDATA_IN_FAST_MEM 默认开启,将 RTC 数据放在快速内存中。如果关闭,则放在慢速内存中,但慢速内存访问速度较慢,且容量较小(8KB)。确保该选项开启,并注意 RTC 内存总容量(约 8KB 快速 + 8KB 慢速)。

4. 持久化技巧:RTC 内存 + NVS 混合方案

由于 RTC 内存存在上述边界条件,对于关键数据(如校准值、计数值),建议采用“RTC 内存 + NVS”混合持久化:

  • RTC 内存:保存高频更新的临时状态(如唤醒次数),允许丢失。
  • NVS(非易失存储):保存低频但关键的数据(如设备配置、校准参数),每次修改后立即写入。

4.1 完整代码示例

#include <stdio.h>
#include "esp_sleep.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"

// RTC 内存变量:保存唤醒次数(允许丢失)
RTC_NOINIT_ATTR uint32_t wake_count_rtc;

// 关键数据:保存设备序列号(需要持久化)
#define KEY_SERIAL "serial"

void init_nvs() {
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }
}

void save_serial_to_nvs(uint32_t serial) {
    nvs_handle_t handle;
    if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) {
        nvs_set_u32(handle, KEY_SERIAL, serial);
        nvs_commit(handle);
        nvs_close(handle);
    }
}

uint32_t load_serial_from_nvs() {
    nvs_handle_t handle;
    uint32_t serial = 0;
    if (nvs_open("storage", NVS_READONLY, &handle) == ESP_OK) {
        nvs_get_u32(handle, KEY_SERIAL, &serial);
        nvs_close(handle);
    }
    return serial;
}

void app_main() {
    init_nvs();

    // 检查复位原因
    esp_reset_reason_t reason = esp_reset_reason();
    if (reason == ESP_RST_DEEPSLEEP) {
        // 深度睡眠唤醒,RTC 数据有效
        wake_count_rtc++;
        printf("Wake count (RTC): %lu\n", (unsigned long)wake_count_rtc);
    } else {
        // 冷启动或复位,RTC 数据可能无效
        wake_count_rtc = 1;
        printf("Cold boot, reset reason: %d\n", reason);
    }

    // 从 NVS 读取关键数据
    uint32_t serial = load_serial_from_nvs();
    if (serial == 0) {
        // 首次运行,生成并保存
        serial = 0x12345678;
        save_serial_to_nvs(serial);
    }
    printf("Serial: 0x%08lx\n", (unsigned long)serial);

    // 模拟工作,然后进入深度睡眠 10 秒
    vTaskDelay(pdMS_TO_TICKS(1000));
    esp_sleep_enable_timer_wakeup(10 * 1000000);
    esp_deep_sleep_start();
}

4.2 代码说明

  • wake_count_rtc 使用 RTC_NOINIT_ATTR,在软件复位后也可能保留,但上电时不确定,因此通过复位原因判断是否重置。
  • serial 存储在 NVS 中,即使 RTC 内存丢失或重新烧录,也能恢复。
  • 每次修改关键数据后立即 nvs_commit,确保写入 flash。

5. 注意事项

  • RTC 内存容量有限:快速内存约 8KB,慢速内存约 8KB,不要存储大数组。
  • NVS 写入次数限制:NVS 基于 flash,擦写次数有限(约 10 万次),避免频繁写入。
  • 电源域配置:不要随意关闭 ESP_PD_DOMAIN_RTC_FAST_MEM,除非你明确知道后果。
  • 调试技巧:在进入睡眠前打印 RTC 变量的地址和值,唤醒后对比,可快速定位丢失原因。
  • 使用 esp_sleep_get_wakeup_cause():可以区分唤醒源,辅助判断数据有效性。

6. 总结

ESP32 的 RTC 内存是低功耗设计中保存临时状态的好工具,但并非绝对可靠。理解其边界条件(电源域、复位源、编译选项)是避免数据丢失的关键。对于关键数据,务必使用 NVS 或外部存储进行持久化。通过混合方案,你可以在保证数据可靠性的同时,充分利用 RTC 内存的高速访问特性。

希望本文能帮助你避开常见的坑,设计出更健壮的嵌入式低功耗系统。