引言

在物联网设备中,ESP32 常被设计为低功耗模式(如 Deep Sleep)以延长电池寿命。在进入 Deep Sleep 前,开发者通常将传感器校准值、网络配置或状态标志存入 RTC 内存(RTC_DATA_ATTR),以便唤醒后快速恢复。然而,RTC 内存并非绝对安全:电源电压跌落、看门狗复位、代码升级失误等都可能破坏数据。若未做完整性校验,系统可能使用错误数据运行,导致不可预知的故障。本文介绍一种实用的数据完整性校验方案,结合 CRC32 和双备份,确保数据在低功耗切换间安全可靠。

原理与挑战

RTC 内存特性

ESP32 的 RTC 内存(RTC FAST Memory)在 Deep Sleep 期间保持供电,容量约 8KB。它通过 RTC_DATA_ATTR 宏定义全局变量,或使用 esp_sleep_get_rtc_retain() 等 API 访问。但 RTC 内存不具备纠错功能,任何位翻转都可能被当作有效数据。

校验需求

  • 检测数据损坏:在唤醒后,快速判断数据是否被篡改。
  • 恢复能力:若主数据损坏,能从备份中恢复。
  • 低开销:校验过程不能消耗过多时间或电量。

方案选择

  • CRC32:计算速度快,碰撞概率低,适合小数据块。
  • 双备份:存储两份数据,若主数据校验失败,使用备份。
  • 序列号:每次写入增加计数器,防止旧数据覆盖新数据。

综合使用 CRC32 和双备份,可覆盖大多数损坏场景。

实现步骤

1. 定义数据结构

首先定义需要持久化的数据结构和校验元数据。

// 用户数据结构,可自定义
typedef struct {
    uint32_t magic;        // 魔数,用于快速识别
    float calibration;     // 校准值
    uint8_t channel;       // 通道号
    uint32_t sequence;     // 序列号,每次写入递增
} app_data_t;

// RTC 内存中的存储结构
#define RTC_DATA_ATTR app_data_t rtc_data_main;
#define RTC_DATA_ATTR app_data_t rtc_data_backup;
#define RTC_DATA_ATTR uint32_t rtc_crc_main;
#define RTC_DATA_ATTR uint32_t rtc_crc_backup;

2. 计算 CRC32

ESP32 提供硬件 CRC 加速器,也可用软件实现。这里使用软件 CRC32(查表法)以保持可移植性。

// 软件 CRC32 实现(多项式 0xEDB88320)
static uint32_t crc32_compute(const uint8_t *data, size_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (size_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            crc = (crc >> 1) ^ (0xEDB88320 & (0 - (crc & 1)));
        }
    }
    return ~crc;
}

3. 写入数据并更新校验

每次写入时,更新主数据和备份,并计算 CRC。

void rtc_data_write(const app_data_t *data) {
    // 更新主数据
    rtc_data_main = *data;
    rtc_crc_main = crc32_compute((uint8_t*)&rtc_data_main, sizeof(app_data_t));
    
    // 更新备份(可稍作延迟,但此处同步写)
    rtc_data_backup = *data;
    rtc_crc_backup = crc32_compute((uint8_t*)&rtc_data_backup, sizeof(app_data_t));
}

4. 读取并校验数据

唤醒后,读取主数据,若 CRC 失败则尝试备份。若两者都失败,则恢复默认值。

bool rtc_data_read(app_data_t *out) {
    // 检查主数据
    if (rtc_data_main.magic == APP_MAGIC && 
        crc32_compute((uint8_t*)&rtc_data_main, sizeof(app_data_t)) == rtc_crc_main) {
        *out = rtc_data_main;
        return true;
    }
    
    // 主数据失败,尝试备份
    if (rtc_data_backup.magic == APP_MAGIC && 
        crc32_compute((uint8_t*)&rtc_data_backup, sizeof(app_data_t)) == rtc_crc_backup) {
        *out = rtc_data_backup;
        // 可选:修复主数据
        rtc_data_main = rtc_data_backup;
        rtc_crc_main = rtc_crc_backup;
        return true;
    }
    
    // 全部失败,返回默认值
    memset(out, 0, sizeof(app_data_t));
    out->magic = APP_MAGIC;
    out->calibration = 1.0f;
    out->channel = 0;
    out->sequence = 0;
    return false;
}

5. 在应用中使用

在进入 Deep Sleep 前写入数据,唤醒后读取。

void app_enter_sleep() {
    app_data_t data = {
        .magic = APP_MAGIC,
        .calibration = 3.14f,
        .channel = 2,
        .sequence = rtc_data_main.sequence + 1
    };
    rtc_data_write(&data);
    esp_deep_sleep_start();
}

void app_wakeup() {
    app_data_t data;
    if (rtc_data_read(&data)) {
        ESP_LOGI("APP", "Data valid, seq=%u", data.sequence);
    } else {
        ESP_LOGW("APP", "Data corrupted, using defaults");
    }
}

完整示例代码

以下是一个可编译的示例,包含初始化、写入和读取流程。

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

#define APP_MAGIC 0xA5A5A5A5

// 数据结构定义...(同上)

// CRC32 函数...(同上)

// 写入和读取函数...(同上)

void app_main() {
    // 初始化 RTC 数据(首次启动)
    if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) {
        app_data_t init = {0};
        init.magic = APP_MAGIC;
        init.calibration = 1.0f;
        init.channel = 1;
        init.sequence = 0;
        rtc_data_write(&init);
    }

    // 读取数据
    app_data_t data;
    bool ok = rtc_data_read(&data);
    ESP_LOGI("MAIN", "Read ok=%d, seq=%u", ok, data.sequence);

    // 模拟修改数据
    data.sequence++;
    rtc_data_write(&data);

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

注意事项

  • RTC 内存大小:ESP32 的 RTC 内存有限,避免存储大数据。若需存储较多数据,考虑使用 NVS 或外部 Flash,但注意 NVS 在 Deep Sleep 时不可访问。
  • CRC 碰撞:CRC32 理论上有碰撞可能,但概率极低。对于关键安全数据,可叠加序列号或魔数增加可靠性。
  • 写入频率:频繁写入 RTC 内存会消耗少量功耗,但远低于 Flash 写入。建议在数据变化时才写入。
  • 备份同步:双备份时,若主数据损坏,备份可能也损坏(如电源故障)。可考虑三备份或使用 ECC 算法,但会增加开销。
  • 复位类型:区分上电复位和 Deep Sleep 唤醒。上电时 RTC 内存内容未定义,需重新初始化。
  • 编译器优化:使用 RTC_DATA_ATTR 时,确保变量不被优化掉,可加 volatile 或检查 map 文件。

总结

通过 CRC32 校验和双备份机制,ESP32 在低功耗模式下能够有效检测并恢复 RTC 内存中的数据损坏。该方法实现简单、开销低,适合大多数嵌入式场景。开发者可根据实际需求扩展,例如增加三备份或使用更复杂的校验算法,以进一步提升可靠性。记住,数据完整性是嵌入式系统稳定性的基石,尤其在无人值守的物联网设备中,提前防范远比事后调试更有效。