ESP32 低功耗模式下 RTC 内存数据丢失排查与恢复策略

在物联网设备中,ESP32 的低功耗模式(如 Deep-sleep)常被用来延长电池寿命。为了在唤醒后恢复状态,我们通常将关键数据(如 Wi-Fi 配置、传感器校准值、计数器)存储在 RTC 内存中。然而,不少开发者发现数据会意外丢失,导致设备行为异常。本文将深入探讨 RTC 内存的工作原理、数据丢失的常见原因,并给出系统化的排查与恢复方案。

一、RTC 内存原理与复位类型

ESP32 内部包含 8KB 的 RTC 快速内存(RTC FAST Memory)和 8KB 的 RTC 慢速内存(RTC SLOW Memory),这些内存在 Deep-sleep 模式下由 RTC 外设供电,可以保持数据。但并非所有复位都会保留 RTC 内存,理解复位类型至关重要。

  • 上电复位(Power-on Reset):系统完全断电后重新上电,RTC 内存内容丢失。
  • 深度睡眠唤醒(Deep-sleep Wakeup):由定时器、GPIO 或触摸传感器唤醒,RTC 内存保留。
  • 软件复位(Software Reset):调用 esp_restart() 或发生异常导致重启,RTC 内存保留(但需注意某些复位原因可能清除)。
  • 看门狗复位(Watchdog Reset):通常保留 RTC 内存,但若触发的是 RTC 看门狗,则可能清除。

关键点:RTC 内存的保持依赖于 RTC 电源域。如果设计中将 VDD3P3_RTC 引脚断电,则数据必然丢失。

二、数据丢失的常见原因排查

1. 复位原因检测

首先,我们需要确认设备实际经历的复位类型。ESP32 提供了 esp_reset_reason() 函数来获取复位原因。

#include "esp_system.h"

void check_reset_reason() {
    esp_reset_reason_t reason = esp_reset_reason();
    switch (reason) {
        case ESP_RST_POWERON:
            ESP_LOGI("RTC", "Power-on reset - RTC data lost");
            break;
        case ESP_RST_DEEPSLEEP:
            ESP_LOGI("RTC", "Deep-sleep wakeup - RTC data valid");
            break;
        case ESP_RST_SW:
            ESP_LOGI("RTC", "Software reset - RTC data valid");
            break;
        case ESP_RST_WDT:
            ESP_LOGI("RTC", "Watchdog reset - check if RTC data valid");
            break;
        default:
            ESP_LOGI("RTC", "Unknown reset reason");
    }
}

2. 检查 RTC 内存初始化标志

由于 RTC 内存是静态区域,我们需要通过一个魔法数字(Magic Number)来标记数据是否有效。

#define RTC_MAGIC 0x5A5A5A5A

RTC_DATA_ATTR uint32_t rtc_magic;
RTC_DATA_ATTR uint32_t rtc_counter;
RTC_DATA_ATTR char rtc_config[64];

void save_rtc_data() {
    rtc_magic = RTC_MAGIC;
    rtc_counter++;
    // 保存其他配置
}

bool is_rtc_data_valid() {
    return (rtc_magic == RTC_MAGIC);
}

3. 常见丢失原因

  • 电源设计问题:在 Deep-sleep 时,如果外部电路切断了 RTC 电源域(如某些低功耗板),数据会丢失。检查硬件原理图。
  • 编译选项:使用 RTC_DATA_ATTR 时,确保变量被正确放置在 RTC 段。如果使用了 -O2 优化,某些未使用的变量可能被优化掉,但通常不会。
  • 错误的内存访问:指针越界可能覆盖 RTC 内存区域,导致数据损坏。
  • 固件升级:OTA 升级后,RTC 内存内容可能被清除(取决于升级方式)。

三、恢复策略与代码实现

1. 数据备份与恢复流程

为了应对数据丢失,我们采用以下策略:

  • 在 RTC 内存中保存数据,并附加 CRC 校验。
  • 如果校验失败,尝试从 NVS(非易失存储)中恢复备份。
  • 如果 NVS 也没有,则使用默认配置。

2. 完整示例代码

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

#define RTC_MAGIC 0xA5A5A5A5
#define NVS_KEY "rtc_backup"

// RTC 内存变量
RTC_DATA_ATTR uint32_t rtc_magic;
RTC_DATA_ATTR uint32_t rtc_counter;
RTC_DATA_ATTR uint32_t rtc_crc;

// 计算简单 CRC32
uint32_t calc_crc32(const uint8_t *data, size_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (size_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
        }
    }
    return ~crc;
}

void save_rtc_data() {
    rtc_magic = RTC_MAGIC;
    rtc_counter++;
    // 计算 CRC,注意只对数据部分计算
    rtc_crc = calc_crc32((uint8_t*)&rtc_counter, sizeof(rtc_counter));
    
    // 同时备份到 NVS
    nvs_handle_t handle;
    if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) {
        nvs_set_u32(handle, NVS_KEY, rtc_counter);
        nvs_commit(handle);
        nvs_close(handle);
    }
}

bool load_rtc_data() {
    // 检查魔法数字和 CRC
    if (rtc_magic == RTC_MAGIC) {
        uint32_t crc = calc_crc32((uint8_t*)&rtc_counter, sizeof(rtc_counter));
        if (crc == rtc_crc) {
            ESP_LOGI("RTC", "RTC data valid, counter=%lu", rtc_counter);
            return true;
        } else {
            ESP_LOGW("RTC", "RTC data corrupted");
        }
    } else {
        ESP_LOGW("RTC", "RTC magic mismatch");
    }
    
    // 尝试从 NVS 恢复
    nvs_handle_t handle;
    if (nvs_open("storage", NVS_READONLY, &handle) == ESP_OK) {
        uint32_t backup = 0;
        if (nvs_get_u32(handle, NVS_KEY, &backup) == ESP_OK) {
            ESP_LOGW("RTC", "Recovered from NVS: %lu", backup);
            rtc_counter = backup;
            rtc_magic = RTC_MAGIC;
            rtc_crc = calc_crc32((uint8_t*)&rtc_counter, sizeof(rtc_counter));
            nvs_close(handle);
            return true;
        }
        nvs_close(handle);
    }
    
    // 默认值
    ESP_LOGW("RTC", "No backup, using default");
    rtc_counter = 0;
    rtc_magic = RTC_MAGIC;
    rtc_crc = calc_crc32((uint8_t*)&rtc_counter, sizeof(rtc_counter));
    return false;
}

void app_main() {
    // 初始化 NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }
    
    // 检查复位原因
    check_reset_reason();
    
    // 加载数据
    load_rtc_data();
    
    // 模拟工作
    ESP_LOGI("MAIN", "Current counter: %lu", rtc_counter);
    save_rtc_data();
    
    // 进入深度睡眠 5 秒
    esp_sleep_enable_timer_wakeup(5 * 1000000);
    esp_deep_sleep_start();
}

3. 注意事项

  • RTC 内存大小限制:ESP32 的 RTC 快速内存只有 8KB,慢速内存 8KB,但实际可用可能更少(取决于固件)。避免存储大数组。
  • NVS 写入寿命:NVS 基于 Flash,写入次数有限(约 10 万次)。频繁备份会磨损 Flash,建议仅在关键数据变化时备份。
  • 复位原因检测:在 app_main 开头检测复位原因,可以区分正常唤醒和异常复位,便于调试。
  • 硬件设计:确保在 Deep-sleep 时 RTC 电源域不断电。如果使用外部 LDO 控制,需检查是否关闭了 RTC 供电。
  • 多核访问:RTC 内存是全局的,但中断服务程序(ISR)中访问需谨慎,避免竞争。

四、总结

RTC 内存数据丢失通常由复位类型、电源设计或代码错误引起。通过检测复位原因、使用魔法数字和 CRC 校验,并结合 NVS 备份,可以构建健壮的数据恢复机制。在实际项目中,建议先记录复位原因,再逐步排查硬件和软件问题。希望本文的策略能帮助你解决 ESP32 低功耗模式下的数据丢失问题。