用 A/B 双页冗余保存 MCU 固定配置

Flash 擦除和写入需要时间。设备在这个过程中掉电、复位或受到干扰,正在更新的页可能只写入了一部分,单页存储随后无法提供完整配置。

长期使用中还可能出现偶发位翻转。CRC32 可以发现数据已经损坏,但单页存储没有第二份数据可供恢复。A/B 双页存储把同一份结构体写入两个独立擦除页:CRC32 负责识别损坏页,另一页负责提供可用副本。

本文示例来自实际工程中的 FlashStore 模块,现已整理成独立的 HAL 无关 C 库:FlashStore A/B Partition 开源仓库

双页冗余的职责

每个页都保存魔数、数据长度、CRC32 和 payload。A 页是优先读取的主副本,B 页用于回退。

这套实现没有代次号,也不会在两页之间轮流选择最新记录。两页保存同一份数据,目标是在一次擦除或写入失败后仍保留一份可校验副本。

先 A 后 B 的更新顺序

保存时先擦除并写入 A 页。A 页更新失败,流程立即停止,B 页中的旧值保持不变;A 页写入成功后再更新 B 页。

读取时先检查 A 页的魔数、长度和 CRC32。A 页无效时再检查 B 页。两个副本都无效,接口返回 FLASH_STORE_ERROR_NO_VALID_DATA,由应用决定加载默认值或进入故障处理。

HAL 无关接口

核心库只要求平台提供读取、擦除和整页写入三个回调。调用方再配置两个页地址、页大小和一个静态工作区:

FlashStore_Config config = {
    .io = {
        .read = board_flash_read,
        .erase = board_flash_erase,
        .program = board_flash_program,
    },
    .page_a_address = PAGE_A_ADDRESS,
    .page_b_address = PAGE_B_ADDRESS,
    .page_size = sizeof(workspace),
    .workspace = workspace,
    .workspace_size = sizeof(workspace),
};

FlashStore_Init(&store, &config);
FlashStore_Save(&store, key, (const uint8_t *)&settings, sizeof(settings));
FlashStore_Load(&store, key, (uint8_t *)&settings, sizeof(settings));

仓库中的 examples/air001_example.c 给出了 Air001 HAL 的回调实现。移植到其他 MCU 时,只需替换这三个回调,并按芯片手册处理页大小、写入粒度和对齐要求。

使用边界

该方案适合固定结构体、低频写入和代码空间敏感的 MCU。动态 KV、频繁更新、磨损均衡或时序记录仍应使用 FlashDB 等完整存储组件。两类方案的资源和维护成本对比见 FlashDB 占用约 7KB Flash 时,固定配置可考虑 A/B 双页存储