# 引言 在物联网设备中,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. 定义数据结构 首先定义需要持久化的数据结构和校验元数据。 ```c // 用户数据结构,可自定义 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(查表法)以保持可移植性。 ```c // 软件 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。 ```c 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 失败则尝试备份。若两者都失败,则恢复默认值。 ```c 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 前写入数据,唤醒后读取。 ```c 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"); } } ``` # 完整示例代码 以下是一个可编译的示例,包含初始化、写入和读取流程。 ```c #include #include #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 内存中的数据损坏。该方法实现简单、开销低,适合大多数嵌入式场景。开发者可根据实际需求扩展,例如增加三备份或使用更复杂的校验算法,以进一步提升可靠性。记住,数据完整性是嵌入式系统稳定性的基石,尤其在无人值守的物联网设备中,提前防范远比事后调试更有效。