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

引言

ESP32 作为一款高集成度的 Wi-Fi/BLE SoC,其低功耗模式(尤其是 Deep-sleep)是物联网设备电池供电的关键。然而,许多开发者在使用 RTC 内存(RTC Fast Memory 或 RTC Slow Memory)保存唤醒后的状态时,发现数据在唤醒后丢失或损坏。本文将深入探讨这一问题的根源,并提供从硬件到软件的全面防护方案。

RTC 内存的工作原理

ESP32 的 RTC 内存分为两部分:

  • RTC Fast Memory:8KB,位于 RTC 域,CPU 可快速访问,用于存放唤醒向量和关键数据。
  • RTC Slow Memory:8KB,同样位于 RTC 域,访问速度较慢,但容量更大,常用于保存用户数据。

这些内存在 Deep-sleep 模式下由 RTC 电源域供电,只要 VDD3P3_RTC 引脚保持供电,数据就不会丢失。但实际应用中,数据丢失往往源于以下原因。

数据丢失的常见根因

1. 电源域切换或断电

  • 如果设计中使用外部开关切断 VDD3P3_RTC 电源(例如通过 GPIO 控制 MOSFET),则 RTC 内存会完全断电,数据必然丢失。
  • 某些开发板(如 ESP32-DevKitC)在 USB 断开时可能切断整个 3.3V 电源,导致 RTC 域失电。

2. 复位源干扰

  • 当发生外部复位、看门狗复位或软件复位时,RTC 内存内容可能被清除(取决于复位原因)。例如,ESP32 的 esp_restart() 会触发软件复位,但 RTC 内存默认保留;而 esp_deep_sleep_start() 后的唤醒复位(由 RTC 定时器或外部唤醒)则保留 RTC 内存。但若使用 esp_sleep_enable_gpio_switch 等配置不当,可能触发意外复位。

3. 编译优化或未正确标记

  • RTC 内存中的变量必须使用 RTC_DATA_ATTRRTC_NOINIT_ATTR 宏定义,否则编译器可能将其放在普通 RAM 中,Deep-sleep 时数据丢失。
  • 未使用 volatile 关键字可能导致编译器优化掉写入操作。

4. 电源噪声或电压跌落

  • 在 Deep-sleep 唤醒瞬间,电流峰值可能引起电源电压跌落,导致 RTC 域数据写入失败或损坏。

排查流程

当遇到 RTC 内存数据丢失时,按以下步骤系统排查:

  1. 确认硬件供电:用万用表测量 Deep-sleep 期间 VDD3P3_RTC 引脚电压,确保稳定在 3.3V。
  2. 检查复位原因:在唤醒后调用 esp_sleep_get_wakeup_cause()esp_reset_reason(),打印复位原因,排除意外复位。
  3. 验证变量定义:检查所有需要保留的变量是否使用 RTC_DATA_ATTRRTC_NOINIT_ATTR
  4. 添加调试输出:在写入和读取时打印数据,并加入 CRC 校验,判断是写入失败还是读取失败。
  5. 测试不同唤醒源:分别使用定时器唤醒和 GPIO 唤醒,观察差异。

防护策略

1. 正确使用 RTC 内存属性

#include "esp_attr.h"

// 使用 RTC_DATA_ATTR 定义,数据在 Deep-sleep 后保留
RTC_DATA_ATTR int boot_count = 0;

// 使用 RTC_NOINIT_ATTR 定义,不自动初始化,保留原始值
RTC_NOINIT_ATTR uint8_t sensor_calibration[16];

注意:RTC_DATA_ATTR 变量在每次上电时会被初始化为零(如果未指定初值),而 RTC_NOINIT_ATTR 则不会,适合存放运行时数据。

2. 添加数据完整性校验

在写入时计算 CRC,读取时验证,防止数据损坏。

#include "esp_crc.h"

#define DATA_SIZE 16

typedef struct {
    uint8_t payload[DATA_SIZE];
    uint32_t crc;
} rtc_data_t;

RTC_NOINIT_ATTR rtc_data_t rtc_data;

void save_data(const uint8_t* data) {
    memcpy(rtc_data.payload, data, DATA_SIZE);
    rtc_data.crc = esp_crc32_le(0, rtc_data.payload, DATA_SIZE);
}

bool load_data(uint8_t* out) {
    uint32_t crc = esp_crc32_le(0, rtc_data.payload, DATA_SIZE);
    if (crc != rtc_data.crc) {
        return false; // 数据损坏
    }
    memcpy(out, rtc_data.payload, DATA_SIZE);
    return true;
}

3. 管理电源域

  • 确保硬件上 VDD3P3_RTC 始终连接主电源,不要通过 GPIO 控制。
  • 如果必须断电,考虑使用外部 EEPROM 或 Flash 保存关键数据,但注意写入寿命。

4. 处理复位源

在进入 Deep-sleep 前,明确设置唤醒源,并清除不必要的复位标志:

void enter_deep_sleep() {
    // 设置唤醒源
    esp_sleep_enable_timer_wakeup(10 * 1000000); // 10秒
    // 可选:GPIO 唤醒
    // esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);
    
    // 进入睡眠
    esp_deep_sleep_start();
}

在唤醒后,检查复位原因:

void check_wakeup_reason() {
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    switch (cause) {
        case ESP_SLEEP_WAKEUP_TIMER:
            printf("Wakeup by timer\n");
            break;
        case ESP_SLEEP_WAKEUP_EXT0:
            printf("Wakeup by GPIO\n");
            break;
        default:
            printf("Wakeup by reset, RTC data may be lost\n");
    }
}

5. 使用 NVS 作为后备

对于重要数据,可同时写入 NVS(非易失性存储),但注意 NVS 写入次数有限。建议将频繁变化的数据放在 RTC 内存,关键配置放在 NVS。

完整示例代码

以下是一个完整的 Deep-sleep 计数示例,包含数据校验和复位原因检测:

#include <stdio.h>
#include "esp_sleep.h"
#include "esp_attr.h"
#include "esp_crc.h"
#include "esp_reset_reason.h"

typedef struct {
    uint32_t count;
    uint32_t crc;
} rtc_data_t;

RTC_NOINIT_ATTR rtc_data_t rtc_data;

void save_count(uint32_t count) {
    rtc_data.count = count;
    rtc_data.crc = esp_crc32_le(0, (uint8_t*)&rtc_data.count, sizeof(rtc_data.count));
}

bool load_count(uint32_t* count) {
    uint32_t crc = esp_crc32_le(0, (uint8_t*)&rtc_data.count, sizeof(rtc_data.count));
    if (crc != rtc_data.crc) {
        return false;
    }
    *count = rtc_data.count;
    return true;
}

void app_main() {
    // 检查复位原因
    esp_reset_reason_t reason = esp_reset_reason();
    printf("Reset reason: %d\n", reason);

    // 加载计数
    uint32_t count = 0;
    if (load_count(&count)) {
        printf("Loaded count: %lu\n", count);
    } else {
        printf("RTC data invalid, resetting count\n");
        count = 0;
    }

    // 增加计数
    count++;
    save_count(count);
    printf("New count: %lu\n", count);

    // 进入 Deep-sleep 10秒
    esp_sleep_enable_timer_wakeup(10 * 1000000);
    esp_deep_sleep_start();
}

注意事项

  • RTC 内存容量有限:Fast Memory 8KB,Slow Memory 8KB,不要存储大数组。
  • 避免在 RTC 内存中存储指针:因为唤醒后内存映射可能变化,指针可能无效。
  • 使用 volatile:如果变量在中断或回调中修改,务必加上 volatile
  • 测试不同唤醒源:GPIO 唤醒可能涉及 glitch 滤波,影响数据完整性。
  • 固件升级:OTA 升级会擦除 RTC 内存,升级后需重新初始化。

总结

ESP32 的 RTC 内存是低功耗应用的重要资源,但数据丢失问题往往由硬件供电、复位源或软件定义不当引起。通过正确使用 RTC_NOINIT_ATTR、添加 CRC 校验、管理电源域和检查复位原因,可以显著提升可靠性。对于关键数据,建议结合 NVS 使用。希望本文的排查流程和防护策略能帮助你在实际项目中避免踩坑。