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

引言

在物联网应用中,ESP32 常通过深度睡眠(Deep Sleep)降低功耗,唤醒后需快速恢复现场。RTC 内存(RTC FAST Memory)是唯一在深度睡眠期间保持供电的 RAM 区域,用于保存唤醒原因、传感器校准值或状态标志。但不少开发者发现,唤醒后 RTC 数据被清零或篡改,导致系统逻辑混乱。本文将从原理出发,剖析丢失根源,并给出可落地的排查与恢复方案。

RTC 内存的工作原理

ESP32 内部包含 8KB 的 RTC FAST Memory(地址 0x3FFE0000 - 0x3FFE1FFF),由 RTC 域供电。在深度睡眠模式下,主 CPU 和大部分外设断电,但 RTC 域保持工作,因此 RTC 内存内容得以保留。访问 RTC 内存有两种方式:

  • 通过 RTC_DATA_ATTR 属性将全局变量放入 RTC 段;
  • 使用 esp_sleep_get_wakeup_cause() 等 API 读取唤醒源。

关键点:RTC 内存的保留依赖于 RTC 电源的稳定性和复位类型。若发生上电复位(Power-on Reset)或外部复位,RTC 内存会被清零;而深度睡眠唤醒(Timer 或 GPIO)则保留数据。

数据丢失的常见原因

1. 复位源干扰

  • 上电复位:系统重新上电时,RTC 内存必然清空。
  • 外部复位:EN 引脚被拉低,或看门狗触发复位(如 Task WDT),可能导致 RTC 内存重置。
  • 软件复位:调用 esp_restart() 会触发复位,但默认保留 RTC 内存(除非指定 ESP_RST_BROWNOUT 等)。

2. 电源管理配置错误

  • 使用 esp_sleep_pd_config() 错误地关闭了 RTC 域的电源域,例如 ESP_PD_DOMAIN_RTC_FAST_MEM 设置为 ESP_PD_OPTION_OFF,导致内存掉电。
  • 深度睡眠时,若 esp_sleep_enable_ulp_wakeup() 等配置不当,也可能影响 RTC 域。

3. 编译与链接问题

  • 变量未正确放入 RTC 段:未使用 RTC_DATA_ATTR,或使用了 const 修饰导致放入 flash。
  • 链接脚本覆盖了 RTC 段,或使用了自定义链接脚本导致段冲突。

4. 代码逻辑缺陷

  • 唤醒后未检查 esp_sleep_get_wakeup_cause(),误以为数据有效。
  • 多核访问竞争:RTC 内存访问未加锁,导致数据不一致。

排查步骤

  1. 确认复位源:唤醒后立即打印 esp_reset_reason()esp_sleep_get_wakeup_cause(),区分是上电复位还是深度睡眠唤醒。
  2. 检查电源域配置:打印 esp_sleep_get_pd_config() 或审查代码中的 esp_sleep_pd_config() 调用。
  3. 验证变量位置:在编译后查看 map 文件,确认变量位于 .rtc.data 段。
  4. 最小复现测试:仅保存一个简单计数器,进入深度睡眠 5 秒后唤醒,观察是否递增。若计数器丢失,则问题在配置;若递增,则问题在业务代码。

恢复策略

策略一:使用校验和与备份区

在 RTC 内存中划分两个区域:主数据区和备份区。每次写入时计算 CRC32 校验和,唤醒后先校验主区,若校验失败则尝试备份区,若两者均失败则执行初始化。

#include <esp_sleep.h>
#include <esp_system.h>
#include <rom/crc.h>

#define RTC_DATA_ATTR uint32_t rtc_magic;
#define RTC_DATA_ATTR uint32_t rtc_crc;
#define RTC_DATA_ATTR uint32_t rtc_data[4]; // 示例数据
#define RTC_DATA_ATTR uint32_t rtc_backup[4];
#define RTC_DATA_ATTR uint32_t rtc_backup_crc;

void save_rtc_data(uint32_t *data, size_t len) {
    memcpy(rtc_data, data, len * sizeof(uint32_t));
    rtc_crc = crc32_le(0, (uint8_t *)rtc_data, len * sizeof(uint32_t));
    // 备份
    memcpy(rtc_backup, data, len * sizeof(uint32_t));
    rtc_backup_crc = rtc_crc;
}

bool load_rtc_data(uint32_t *data, size_t len) {
    uint32_t crc = crc32_le(0, (uint8_t *)rtc_data, len * sizeof(uint32_t));
    if (crc == rtc_crc) {
        memcpy(data, rtc_data, len * sizeof(uint32_t));
        return true;
    }
    crc = crc32_le(0, (uint8_t *)rtc_backup, len * sizeof(uint32_t));
    if (crc == rtc_backup_crc) {
        memcpy(data, rtc_backup, len * sizeof(uint32_t));
        return true;
    }
    return false; // 数据无效,需初始化
}

策略二:使用 NVS 持久化关键数据

对于极其重要的数据(如设备配置),可定期写入 NVS(非易失存储)。虽然 NVS 写入有寿命限制,但低频写入可接受。唤醒后优先读 RTC,若无效则读 NVS。

#include <nvs_flash.h>
#include <nvs.h>

void save_to_nvs(uint32_t *data, size_t len) {
    nvs_handle_t handle;
    nvs_open("storage", NVS_READWRITE, &handle);
    nvs_set_blob(handle, "rtc_data", data, len * sizeof(uint32_t));
    nvs_commit(handle);
    nvs_close(handle);
}

bool load_from_nvs(uint32_t *data, size_t len) {
    nvs_handle_t handle;
    if (nvs_open("storage", NVS_READONLY, &handle) == ESP_OK) {
        size_t size = len * sizeof(uint32_t);
        if (nvs_get_blob(handle, "rtc_data", data, &size) == ESP_OK) {
            nvs_close(handle);
            return true;
        }
        nvs_close(handle);
    }
    return false;
}

策略三:利用 RTC 内存的 NOINIT 属性

在部分 ESP-IDF 版本中,可使用 RTC_NOINIT_ATTR 定义变量,该变量在软件复位时保留,但上电复位仍会丢失。适合保存临时状态,但需配合复位源检查。

RTC_NOINIT_ATTR uint32_t boot_count;

完整示例:带恢复的深度睡眠计数器

以下代码演示了如何结合校验和与复位源判断,实现可靠的计数器恢复。

#include <stdio.h>
#include <esp_sleep.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <rom/crc.h>

RTC_DATA_ATTR uint32_t counter;
RTC_DATA_ATTR uint32_t counter_crc;
RTC_DATA_ATTR uint32_t counter_backup;
RTC_DATA_ATTR uint32_t backup_crc;

void save_counter(uint32_t val) {
    counter = val;
    counter_crc = crc32_le(0, (uint8_t *)&counter, sizeof(counter));
    counter_backup = val;
    backup_crc = counter_crc;
}

bool load_counter(uint32_t *val) {
    if (crc32_le(0, (uint8_t *)&counter, sizeof(counter)) == counter_crc) {
        *val = counter;
        return true;
    }
    if (crc32_le(0, (uint8_t *)&counter_backup, sizeof(counter_backup)) == backup_crc) {
        *val = counter_backup;
        return true;
    }
    return false;
}

void app_main() {
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    uint32_t val = 0;
    bool valid = false;

    if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        valid = load_counter(&val);
    } else {
        // 上电复位或其他复位,尝试从 NVS 恢复
        nvs_flash_init();
        nvs_handle_t handle;
        if (nvs_open("storage", NVS_READONLY, &handle) == ESP_OK) {
            size_t size = sizeof(val);
            if (nvs_get_u32(handle, "counter", &val) == ESP_OK) {
                valid = true;
            }
            nvs_close(handle);
        }
    }

    if (!valid) {
        val = 0;
        printf("RTC data invalid, reset counter\n");
    } else {
        printf("Counter restored: %lu\n", (unsigned long)val);
    }

    val++;
    save_counter(val);

    // 保存到 NVS(每 10 次写一次,减少磨损)
    if (val % 10 == 0) {
        nvs_flash_init();
        nvs_handle_t handle;
        nvs_open("storage", NVS_READWRITE, &handle);
        nvs_set_u32(handle, "counter", val);
        nvs_commit(handle);
        nvs_close(handle);
    }

    printf("Entering deep sleep, counter=%lu\n", (unsigned long)val);
    esp_sleep_enable_timer_wakeup(5 * 1000000); // 5 秒
    esp_deep_sleep_start();
}

注意事项

  • CRC 算法:使用 crc32_le 时,注意字节序,确保与写入时一致。
  • NVS 磨损:频繁写入会缩短 flash 寿命,建议限制写入频率。
  • 复位源检查:务必在唤醒后立即检查 esp_sleep_get_wakeup_cause(),避免误用无效数据。
  • 电源域配置:默认情况下,RTC 内存电源域是开启的,除非显式关闭。不要随意调用 esp_sleep_pd_config()
  • 多核访问:若使用双核,访问 RTC 内存时需使用临界区或互斥锁,防止竞争。
  • 测试环境:在开发板上验证不同复位场景(如按键复位、看门狗复位),确保恢复逻辑覆盖所有情况。

结语

RTC 内存丢失并非玄学,通过系统排查和分层恢复策略,可以大幅提升系统的可靠性。建议在项目初期就设计好数据持久化方案,结合 RTC 内存、校验和与 NVS,实现多级保障。希望本文能帮助你解决 ESP32 低功耗模式下的数据丢失问题,构建更稳定的嵌入式应用。