嵌入式 Flash 配置应拆成独立键值对,而不是固耦合结构体

嵌入式固件里,把配置固化成 Flash 常量是基本需求。Wi-Fi 账号、设备名、采样间隔、功能开关——这些东西掉电后必须还在。

MCU 项目最本能的写法是定义一个 C 结构体,把整个结构体当一块连续内存写进 Flash:

struct device_config {
    char wifi_ssid[32];
    char ble_name[32];
    uint16_t sample_interval;
    uint8_t flags;
};

这个写法看着干净,但它把配置项之间锁死了。结构体方案有持久化能力,没有持久化弹性。

结构体方案的三个问题

定位方式决定耦合程度。结构体的字段靠偏移量寻址——sample_intervalble_name 后面 0 字节处,这是编译时写死的。所有字段挤在同一块连续内存里,定义结构体的那一刻,字段之间就已经锁死了。KV 的每个键值对靠名字寻址,键与键之间没有位置关系,天然独立。

版本锁死。固件升级加一个字段,必须放在结构体末尾。插在中间,所有后续字段的偏移量全变了,老数据读出来是乱的。每个版本还要维护一套 if-else 判断旧格式、逐字段搬运到新布局。KV 加一个 cfg/new_feature,老键值对不受任何影响,新旧并存,互不干扰。

不自描述。把 Flash dump 出来是一段二进制。没有结构体定义文件、没有对应的固件版本号,根本不知道里面有什么。KV 的 key 名本身就是元数据——list 一下就知道当前有哪些配置,浏览器端可以直接展示成表格。

KV 是怎么解耦的

KV 把配置从「一块连续的、布局锁死的二进制」变成「一组独立的、自描述的键值对」。

每个键值对独立存取。key 是名字,value 是数据。FlashDB 在底层负责扇区管理和磨损均衡,业务层只需要 getset

对比:

结构体方案 KV 方案
字段寻址 编译时偏移量,位置锁死 运行时按名字查找,独立定位
加一个字段 必须放末尾,否则偏移量全乱 新增 key,不影响已有键值对
版本升级 按版本号 if-else 逐字段搬运 新旧 key 并存,无需迁移
外部查看 二进制 blob,无结构体定义不可读 key 名即元数据,可直接 list

什么时候用结构体就够了

不是所有 Flash 数据都值得换 KV。

如果配置项极少(两三个字段)、格式永远不变、也不需要从外部查看或管理,结构体方案完全够用。一个 boot 标志位、一个校准值——KV 的代价(FlashDB 本身占用几 KB Flash 和少量 RAM)反而不划算。

一旦满足下面任意一条,就值得拆成 KV:

不止是嵌入式 Flash 的问题

这个对比不限于 MCU。Linux 上用 fwrite(&cfg, sizeof(cfg), 1, fp) 存二进制结构体,跟 MCU 上往 Flash 写结构体的困境完全一样——偏移量锁死、版本迁移靠 if-else、dump 出来不可读。

JSON 文件本质上就是文本 KV。{"sample_interval": 500} 跟 FlashDB 里 cfg/sample_interval → 500 是同一种思维方式:靠名字定位,不靠偏移量。好处也一致——加字段不用管顺序,老数据不受影响,任何文本编辑器都能打开看。

当然 JSON 有 JSON 的代价:每次启动要解析整个文件,文件大了解析慢。但结构上的优势是一样的。嵌入式 Flash 上选 KV,Linux 上选 JSON 或 TOML,背后是同一个原则:数据之间如果没有天然的先后依赖,就不要用位置关系把它们锁在一起。

结论

结构体存配置先快后慢:第一版很快,每次改版都慢一步。KV 存配置先慢后快:接入 FlashDB 多花一点初始工作量,之后每次加配置、改字段、外部管理都更独立。

核心原则很简单:把耦合的东西拆成独立的键值对。Flash 上的配置如此,很多嵌入式设计问题也如此。

FlashDB 写入机制简述

理解 FlashDB 的写入方式,要先理解 Flash 硬件的两个基本约束:

这意味着原位覆盖一个值是不可能的——旧值是 0 的位,你没法单独翻回 1。FlashDB 的处理方式是追加写:修改一个键,不碰旧记录,在扇区尾部的新位置写一条新记录,然后把旧记录的状态字节里的一个标记位从 1 写成 0,表示废弃。

每个扇区内部顺序追加,写满后触发 GC:把扇区中未被标记废弃的键值对拷贝到另一个干净扇区,再擦除整个旧扇区(全部位回到 1)。追加写和 GC 配合,磨损均衡自然达成——写入轮流落在不同扇区,不会盯着一个地址反复擦写。

每条 KV 记录带一个状态头,标记当前处于「准备写入」「已写入」「已删除」哪个阶段,外加 CRC 校验。如果在「准备写入」阶段复位,下次启动 CRC 对不上,这条记录直接丢弃,不影响已有数据。

本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。