ESP32-C3 低功耗模式下 RTC 内存数据完整性校验与异常恢复策略

1. 为什么需要关注 RTC 内存完整性?

ESP32-C3 在进入 Deep-sleep 模式后,主 CPU 和大部分外设断电,仅 RTC 域(包括 RTC 内存、RTC 定时器、ULP 协处理器)保持供电。RTC 内存(RTC_SLOW_MEM,约 8KB)是唯一能在深睡期间保留数据的存储区域,常用于保存设备状态、传感器校准值、网络重连计数等关键信息。

然而,RTC 内存并非绝对可靠:

  • 异常复位:看门狗复位、电源跌落、外部干扰可能导致 RTC 内存内容被破坏。
  • 软件缺陷:写入过程中断(如写入一半发生复位)会造成数据不一致。
  • 电池耗尽:极端低电压下 RTC 域供电不稳,可能翻转内存位。

因此,仅简单读写 RTC 内存是不够的,必须引入完整性校验异常恢复机制。

2. 核心原理:CRC32 + 双区备份

2.1 CRC32 校验

CRC32(循环冗余校验)能检测数据在存储或传输过程中的意外改动。ESP-IDF 提供 esp_crc32_le() 函数,计算速度快,适合嵌入式场景。我们在数据末尾附加 4 字节 CRC,读取时重新计算并比对,即可判断数据是否有效。

2.2 双区备份(A/B 区)

仅靠 CRC 只能发现错误,无法恢复。采用双区备份策略:

  • 将 RTC 内存划分为两个相同大小的区域:区 A 和区 B。
  • 每次写入时,先写区 A,再写区 B(或交替写入)。
  • 读取时优先读取校验通过的区域;若两个区域均校验失败,则使用默认值并重新初始化。

这种策略能容忍单次写入中断或单区损坏,极大提升可靠性。

3. 硬件与软件环境

  • 芯片:ESP32-C3(ESP32-C3-MINI-1 模组)
  • 框架:ESP-IDF v5.x(支持 CMake)
  • 关键 API:esp_sleep.hesp_crc.hesp_attr.h

4. 实现步骤

4.1 定义数据结构与内存布局

首先定义需要持久化的业务数据,以及包含 CRC 的存储结构。

// 业务数据(示例)
typedef struct {
    uint32_t boot_count;      // 启动次数
    uint16_t sensor_offset;   // 传感器校准偏移
    uint8_t  flags;           // 功能标志位
} app_data_t;

// 带 CRC 的存储块(每个区一个)
typedef struct {
    app_data_t data;
    uint32_t crc;             // 对 data 字段计算的 CRC32
} rtc_store_block_t;

// 定义两个区在 RTC 慢速内存中的位置
// 使用 RTC_NOINIT_ATTR 防止启动时被清零
RTC_NOINIT_ATTR static rtc_store_block_t rtc_area_a;
RTC_NOINIT_ATTR static rtc_store_block_t rtc_area_b;

注意RTC_NOINIT_ATTR 确保变量位于 RTC 内存且不会被系统启动代码初始化。

4.2 计算与校验 CRC

static uint32_t compute_crc(const app_data_t *data) {
    return esp_crc32_le(0, (const uint8_t *)data, sizeof(app_data_t));
}

static bool is_block_valid(const rtc_store_block_t *block) {
    if (block->crc != compute_crc(&block->data)) {
        return false;
    }
    // 可增加更多合理性检查,例如 boot_count 不超过某个阈值
    if (block->data.boot_count > 1000000) return false;
    return true;
}

4.3 写入与读取策略

写入时,先更新区 A,再更新区 B。读取时,优先返回校验通过的区域;若都失败,则返回默认值。

void rtc_store_save(const app_data_t *data) {
    // 更新区 A
    rtc_area_a.data = *data;
    rtc_area_a.crc = compute_crc(data);
    // 更新区 B
    rtc_area_b.data = *data;
    rtc_area_b.crc = compute_crc(data);
    // 确保写入完成(RTC 内存写入是即时的,但可加内存屏障)
    esp_memory_barrier();
}

bool rtc_store_load(app_data_t *out) {
    // 优先读取区 A
    if (is_block_valid(&rtc_area_a)) {
        *out = rtc_area_a.data;
        return true;
    }
    // 区 A 无效,尝试区 B
    if (is_block_valid(&rtc_area_b)) {
        *out = rtc_area_b.data;
        // 可选:用区 B 修复区 A
        rtc_area_a = rtc_area_b;
        return true;
    }
    // 两个区都无效,返回默认值
    out->boot_count = 0;
    out->sensor_offset = 0;
    out->flags = 0;
    return false;
}

4.4 在深睡流程中集成

在进入 Deep-sleep 前保存数据,在启动后加载并校验。

void app_enter_deep_sleep(void) {
    app_data_t data;
    // 从当前运行状态填充 data
    data.boot_count = g_boot_count;
    data.sensor_offset = g_sensor_offset;
    data.flags = g_flags;
    rtc_store_save(&data);

    // 配置唤醒源(例如定时器 10 分钟)
    esp_sleep_enable_timer_wakeup(10 * 60 * 1000000ULL);
    esp_deep_sleep_start();
}

void app_startup(void) {
    app_data_t loaded;
    bool valid = rtc_store_load(&loaded);
    if (!valid) {
        ESP_LOGW("RTC", "RTC data corrupted, using defaults");
        // 执行恢复动作:重新校准、重置计数等
    }
    g_boot_count = loaded.boot_count + 1;
    g_sensor_offset = loaded.sensor_offset;
    g_flags = loaded.flags;
}

4.5 完整示例代码

以下是一个可直接编译的完整模块(rtc_store.c):

#include <string.h>
#include "esp_sleep.h"
#include "esp_crc.h"
#include "esp_attr.h"
#include "esp_log.h"

typedef struct {
    uint32_t boot_count;
    uint16_t sensor_offset;
    uint8_t  flags;
} app_data_t;

typedef struct {
    app_data_t data;
    uint32_t crc;
} rtc_store_block_t;

RTC_NOINIT_ATTR static rtc_store_block_t rtc_area_a;
RTC_NOINIT_ATTR static rtc_store_block_t rtc_area_b;

static uint32_t compute_crc(const app_data_t *data) {
    return esp_crc32_le(0, (const uint8_t *)data, sizeof(app_data_t));
}

static bool is_block_valid(const rtc_store_block_t *block) {
    if (block->crc != compute_crc(&block->data)) return false;
    if (block->data.boot_count > 1000000) return false;
    return true;
}

void rtc_store_save(const app_data_t *data) {
    rtc_area_a.data = *data;
    rtc_area_a.crc = compute_crc(data);
    rtc_area_b.data = *data;
    rtc_area_b.crc = compute_crc(data);
    esp_memory_barrier();
}

bool rtc_store_load(app_data_t *out) {
    if (is_block_valid(&rtc_area_a)) {
        *out = rtc_area_a.data;
        return true;
    }
    if (is_block_valid(&rtc_area_b)) {
        *out = rtc_area_b.data;
        rtc_area_a = rtc_area_b;  // 修复区 A
        return true;
    }
    memset(out, 0, sizeof(app_data_t));
    return false;
}

// 测试函数
void rtc_store_test(void) {
    app_data_t d = { .boot_count = 42, .sensor_offset = 123, .flags = 0xAB };
    rtc_store_save(&d);
    app_data_t out;
    bool ok = rtc_store_load(&out);
    ESP_LOGI("RTC", "load ok=%d boot=%lu offset=%u flags=0x%02X", ok, out.boot_count, out.sensor_offset, out.flags);
}

5. 注意事项与进阶策略

  • RTC 内存容量:ESP32-C3 的 RTC 慢速内存约 8KB,但实际可用大小受链接脚本限制。若数据量大,可考虑压缩或只保存关键字段。
  • 写入频率:RTC 内存写入次数没有限制,但频繁写入会增加功耗。建议在进入深睡前一次性写入,避免多次小写。
  • CRC 碰撞:CRC32 碰撞概率极低,但若数据安全性要求极高,可叠加一个 16 位累加和或使用两个不同多项式。
  • 多区扩展:对于关键数据,可扩展为三区(A/B/C),采用多数表决机制,进一步提高容错性。
  • 调试技巧:在开发阶段,可故意破坏某个区的数据(如直接写 0xFF)来验证恢复逻辑是否正常工作。
  • 与 NVS 对比:NVS(非易失存储)基于 Flash,写入次数有限(约 10 万次),且深睡期间无法访问。RTC 内存适合高频更新的临时状态,而 NVS 适合长期保存的配置。两者可结合使用。

6. 总结

本文针对 ESP32-C3 低功耗场景下的数据可靠性问题,提出了基于 CRC32 校验和双区备份的完整解决方案。通过合理利用 RTC 内存的 RTC_NOINIT_ATTR 属性,配合校验与恢复逻辑,能有效应对异常复位和数据损坏,为物联网设备提供更稳健的持久化机制。开发者可根据实际业务需求调整数据结构,并扩展为多区冗余策略,以平衡可靠性与资源开销。