ESP32-C3 低功耗模式下 RTC 内存数据完整性校验与防丢失技巧

1. 为什么 RTC 内存数据会丢失?

ESP32-C3 在深度睡眠(Deep Sleep)模式下,主 CPU 和大多数外设断电,但 RTC 域(包括 RTC 快速内存和 RTC 慢速内存)保持供电,用于存储唤醒后的上下文数据。然而,数据丢失可能源于:

  • 电源波动:电池电压骤降或瞬间断电,导致 RTC 域写入不稳定。
  • 复位干扰:外部复位或看门狗复位可能中断写入过程。
  • 固件 Bug:写入时未考虑对齐或大小限制,覆盖其他数据。
  • 老化与磨损:RTC 内存基于 SRAM,无磨损问题,但若频繁写入,可能因电压不稳导致位翻转。

因此,仅依赖 RTC 内存保存数据是不够的,必须引入完整性校验和冗余机制。

2. ESP32-C3 RTC 内存架构与配置

ESP32-C3 提供两类 RTC 内存:

  • RTC 快速内存:8KB,可被 CPU 在睡眠唤醒后快速访问,适合存放临时变量。
  • RTC 慢速内存:8KB,访问速度较慢,但容量更大,适合持久数据。

在 ESP-IDF 中,通过 RTC_NOINIT_ATTRRTC_DATA_ATTR 宏定义变量,即可将其放置在 RTC 内存中。例如:

RTC_DATA_ATTR uint32_t boot_count;
RTC_NOINIT_ATTR sensor_data_t sensor; // 自定义结构体

注意:RTC_DATA_ATTR 变量在每次上电时会被清零(除非使用 esp_sleep_get_wakeup_cause 判断),而 RTC_NOINIT_ATTR 则不会自动初始化,适合保存需跨睡眠周期的数据。

3. 数据完整性校验方案

3.1 CRC32 校验

CRC32 是常用且高效的校验算法,可检测数据位错误。ESP-IDF 提供 esp_crc32_le() 函数,无需额外库。

原理:在写入数据时,计算数据的 CRC32 值并存储在数据块末尾;读取时重新计算并与存储值比对,若不一致则判定数据损坏。

3.2 双备份(Double Buffer)

将数据复制两份到不同地址,写入时交替更新,读取时优先使用校验通过的那份。若两份都损坏,则恢复默认值。

3.3 磨损均衡(Wear Leveling)

虽然 SRAM 无磨损,但频繁写入同一地址可能因电源噪声导致数据不稳定。通过轮换写入位置,可降低风险。

4. 完整代码示例

以下示例演示如何定义数据结构、写入带 CRC 校验的数据,并在读取时验证。

#include <stdio.h>
#include <string.h>
#include "esp_sleep.h"
#include "esp_crc.h"

// 定义数据块结构(含 CRC 字段)
typedef struct {
    uint32_t magic;      // 魔数,用于识别有效数据
    uint32_t counter;    // 业务数据
    uint32_t crc;        // CRC32 校验值
} data_block_t;

// 分配两块 RTC 内存(双备份)
RTC_NOINIT_ATTR data_block_t block_a;
RTC_NOINIT_ATTR data_block_t block_b;

#define MAGIC_NUMBER 0xA5A5A5A5

// 计算 CRC32
uint32_t compute_crc(const data_block_t *block) {
    // 计算除 crc 字段外的所有字节
    return esp_crc32_le(0, (uint8_t *)block, offsetof(data_block_t, crc));
}

// 写入数据到指定块
void write_block(data_block_t *block, uint32_t counter) {
    block->magic = MAGIC_NUMBER;
    block->counter = counter;
    block->crc = compute_crc(block);
}

// 读取并校验块,返回有效数据或 -1
int read_block(const data_block_t *block, uint32_t *counter) {
    if (block->magic != MAGIC_NUMBER) return -1;
    if (block->crc != compute_crc(block)) return -1;
    *counter = block->counter;
    return 0;
}

// 双备份读取:优先使用校验通过的块
int read_safe(uint32_t *counter) {
    uint32_t val_a, val_b;
    int ok_a = read_block(&block_a, &val_a);
    int ok_b = read_block(&block_b, &val_b);

    if (ok_a == 0 && ok_b == 0) {
        // 两者都有效,取较新的(这里简单取 counter 较大者)
        *counter = (val_a > val_b) ? val_a : val_b;
        return 0;
    } else if (ok_a == 0) {
        *counter = val_a;
        return 0;
    } else if (ok_b == 0) {
        *counter = val_b;
        return 0;
    }
    return -1; // 都损坏
}

// 写入安全(交替写入)
void write_safe(uint32_t counter) {
    static uint8_t toggle = 0; // 注意:此变量在 RTC 内存中?需要持久化
    // 为了简单,这里使用 counter 的奇偶决定写入块
    if (counter % 2 == 0) {
        write_block(&block_a, counter);
    } else {
        write_block(&block_b, counter);
    }
}

void app_main() {
    // 初始化
    uint32_t count = 0;
    if (read_safe(&count) != 0) {
        // 数据无效,恢复默认
        count = 0;
        write_safe(count);
        printf("Data corrupted, reset to 0\n");
    } else {
        printf("Loaded counter: %lu\n", (unsigned long)count);
    }

    // 模拟业务:递增并写入
    count++;
    write_safe(count);
    printf("Saved counter: %lu\n", (unsigned long)count);

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

5. 配置步骤与注意事项

5.1 配置步骤

  1. 启用 RTC 内存:在 menuconfig 中无需特殊配置,默认支持。
  2. 定义变量:使用 RTC_NOINIT_ATTR 确保数据在深度睡眠后保留。
  3. 实现校验逻辑:如上述代码,使用 esp_crc32_le 计算 CRC。
  4. 测试:在开发板上运行,观察深度睡眠唤醒后数据是否正确。

5.2 注意事项

  • 对齐与大小:RTC 内存访问需 4 字节对齐,结构体建议使用 __attribute__((aligned(4)))
  • 写入时机:避免在中断中写入,确保写入操作原子性。
  • 电源稳定性:在写入前可短暂唤醒外设(如 ADC 检测电压),确保电压稳定。
  • 磨损均衡:虽然 SRAM 无磨损,但若频繁写入,建议增加轮换机制,防止因电源噪声导致同一地址反复出错。
  • 调试技巧:使用 esp_sleep_get_wakeup_cause() 判断唤醒原因,区分首次上电和深度睡眠唤醒,避免误初始化。

6. 总结

通过 CRC32 校验和双备份机制,可以显著提高 ESP32-C3 在低功耗模式下 RTC 内存数据的可靠性。本文提供的代码可直接集成到项目中,开发者可根据实际需求扩展,如增加多备份或使用 ECC 算法。记住,嵌入式系统的稳定性往往体现在细节中,数据完整性是其中的关键一环。