ESP32 低功耗模式下 RTC 内存数据校验与恢复的工程实现

1. 为什么需要 RTC 内存校验?

ESP32 在 Deep Sleep 模式下,主 CPU、Wi-Fi 和大部分外设断电,仅 RTC 域(包括 RTC 内存、RTC 外设)保持供电。RTC 内存(RTC Fast Memory 和 RTC Slow Memory)可在深睡期间保存少量数据(如传感器校准值、状态标志、计数值),唤醒后快速恢复。

但 RTC 内存并非绝对可靠:

  • 电源电压跌落或纹波可能导致位翻转;
  • 固件升级或代码 bug 可能意外写入错误数据;
  • 外部电磁干扰(EMI)可能破坏存储内容。

若直接读取未校验的 RTC 数据,可能导致系统行为异常(如错误的校准值、状态错乱),甚至死机。因此,工程上必须引入校验机制,并在校验失败时执行恢复策略。

2. RTC 内存硬件与 API 基础

ESP32 的 RTC 内存分为两段:

  • RTC Fast Memory:8KB,位于 RTC 域,访问速度快,适合存放频繁读写的变量;
  • RTC Slow Memory:8KB,访问稍慢,但容量更大。

在 ESP-IDF 中,通过 RTC_NOINIT_ATTRRTC_DATA_ATTR 宏将变量放置在 RTC 内存中:

  • RTC_DATA_ATTR:变量在深睡后保留,但每次上电会初始化(若未使用 RTC_NOINIT_ATTR)。
  • RTC_NOINIT_ATTR:变量在深睡和软复位后均不初始化,保留原始值。
// 定义 RTC 内存结构体
RTC_NOINIT_ATTR struct {
    uint32_t magic;        // 魔数,用于标识数据有效性
    uint32_t crc;          // CRC32 校验值
    float calibration;     // 示例数据:校准系数
    uint32_t counter;      // 示例数据:唤醒计数
} rtc_data;

注意:RTC_NOINIT_ATTR 变量在系统复位(包括软复位)后仍保留,但上电复位(Power-on Reset)会清零。因此,需要结合魔数判断数据是否首次上电。

3. 校验机制设计

3.1 魔数(Magic Number)

魔数用于快速判断 RTC 内存是否被初始化过。首次上电时,RTC 内存内容为随机值或零,魔数不匹配则视为无效。

#define RTC_MAGIC 0xA5A5A5A5

3.2 CRC32 校验

CRC32 能检测数据位错误,但无法纠正。将数据区(除 CRC 字段外)计算 CRC,存储到 CRC 字段。读取时重新计算并比对。

ESP-IDF 提供 esp_crc32_le() 函数(需包含 esp_rom_crc.h)。

#include "esp_rom_crc.h"

uint32_t compute_crc(const uint8_t* data, size_t len) {
    return esp_rom_crc32_le(0, data, len);
}

3.3 校验流程

  1. 读取 RTC 内存结构体;
  2. 检查魔数是否匹配;
  3. 若魔数匹配,计算数据区(从 calibrationcounter)的 CRC,与存储的 CRC 比较;
  4. 若 CRC 匹配,数据有效;否则,视为损坏。

4. 恢复策略

当校验失败时,需要恢复默认值,并重新初始化 RTC 内存。恢复策略包括:

  • 使用默认校准值(如 1.0);
  • 重置计数器为 0;
  • 记录错误日志(通过 RTC 日志或非易失存储);
  • 重新写入有效数据。

工程上,建议在恢复后执行一次完整初始化,确保后续流程正确。

5. 完整代码实现

以下代码演示了在 Deep Sleep 唤醒后如何校验和恢复 RTC 数据。

#include <stdio.h>
#include <string.h>
#include "esp_sleep.h"
#include "esp_rom_crc.h"
#include "esp_log.h"

#define RTC_MAGIC 0xA5A5A5A5
#define TAG "RTC_CHECK"

// RTC 内存结构体(不含 CRC 和 magic,但为了计算方便,我们整体定义)
RTC_NOINIT_ATTR struct {
    uint32_t magic;
    uint32_t crc;
    float calibration;
    uint32_t counter;
} rtc_data;

// 计算数据区 CRC(从 calibration 开始,到 counter 结束)
uint32_t compute_data_crc(void) {
    // 跳过 magic 和 crc 字段
    uint8_t* data = (uint8_t*)&rtc_data.calibration;
    size_t len = sizeof(rtc_data) - offsetof(typeof(rtc_data), calibration);
    return esp_rom_crc32_le(0, data, len);
}

// 初始化 RTC 数据为默认值
void rtc_data_init_default(void) {
    rtc_data.magic = RTC_MAGIC;
    rtc_data.calibration = 1.0f;
    rtc_data.counter = 0;
    rtc_data.crc = compute_data_crc();
    ESP_LOGW(TAG, "RTC data initialized to defaults");
}

// 校验 RTC 数据有效性
bool rtc_data_validate(void) {
    if (rtc_data.magic != RTC_MAGIC) {
        ESP_LOGE(TAG, "Magic mismatch");
        return false;
    }
    uint32_t crc_calc = compute_data_crc();
    if (crc_calc != rtc_data.crc) {
        ESP_LOGE(TAG, "CRC mismatch: stored 0x%08X, calc 0x%08X", rtc_data.crc, crc_calc);
        return false;
    }
    return true;
}

// 更新 RTC 数据(例如增加计数)
void rtc_data_update(void) {
    rtc_data.counter++;
    rtc_data.crc = compute_data_crc();
}

void app_main(void) {
    // 检查是否是深睡唤醒
    if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER) {
        ESP_LOGI(TAG, "Woke up from deep sleep");
        if (rtc_data_validate()) {
            ESP_LOGI(TAG, "RTC data valid: calibration=%.2f, counter=%u", rtc_data.calibration, rtc_data.counter);
        } else {
            ESP_LOGE(TAG, "RTC data corrupted, restoring defaults");
            rtc_data_init_default();
        }
    } else {
        // 首次上电,初始化
        ESP_LOGI(TAG, "First boot, initializing RTC data");
        rtc_data_init_default();
    }

    // 模拟使用数据
    rtc_data_update();
    ESP_LOGI(TAG, "Counter after update: %u", rtc_data.counter);

    // 进入深睡 10 秒
    esp_sleep_enable_timer_wakeup(10 * 1000000);
    esp_deep_sleep_start();
}

6. 工程注意事项

  • 结构体对齐:RTC 内存访问可能要求对齐,建议使用 __attribute__((aligned(4))) 或确保结构体自然对齐。
  • CRC 计算范围:务必跳过 magiccrc 字段,否则 CRC 会因自身变化而失效。
  • 首次上电与深睡唤醒区分:通过 esp_sleep_get_wakeup_cause() 区分,避免误初始化。
  • 多任务安全:RTC 内存操作在深睡唤醒后是单线程的,但若使用其他唤醒源(如 GPIO),需考虑并发。
  • 日志输出:在深睡期间,UART 可能未初始化,建议在唤醒后延迟或使用 RTC 日志。
  • 测试:通过故意写入错误数据(如修改 rtc_data.calibration)来验证校验机制。

7. 扩展与优化

  • 冗余存储:在 RTC 内存中保存两份数据,校验失败时从备份恢复。
  • 错误日志:将校验失败次数记录在 NVS 中,用于诊断。
  • 动态校验:若数据更新频繁,可在每次写入后立即更新 CRC,但注意写入原子性。

总结

RTC 内存校验是 ESP32 低功耗应用的关键环节。通过魔数和 CRC32 双重校验,可以有效检测数据损坏,并通过默认值恢复保证系统稳定。本文提供的代码可直接集成到工程中,开发者可根据实际需求扩展恢复策略。记住:在深睡唤醒后,永远不要信任未经校验的数据。