4. 存储与校验优化:双备份机制、校验算法与Flash磨损均衡
固件升级最怕什么?升级到一半断电,设备变砖。我早年做消费类产品时,就吃过这个亏。从那以后,双备份机制就成了我设计升级方案时的底线。今天咱们就聊聊存储与校验这块的三个核心话题:A/B分区怎么玩、校验算法怎么选、Flash磨损怎么均衡。
4.1 双备份机制(A/B分区)
说白了,A/B分区就是准备两份固件。一份在跑,一份待命。升级时只写备用分区,写完了校验通过,再切换启动。这样就算升级中途断电,设备重启后还能从老分区正常启动。
核心原则:永远保证至少有一个可启动的固件副本。
4.1.1 分区布局
我习惯把Flash分成这么几块:
- Bootloader:负责引导,也负责判断从哪个分区启动
- 分区A(Slot A):当前运行固件
- 分区B(Slot B):备用固件
- 元数据区:记录当前激活分区、版本号、校验值等
举个例子,一个8MB Flash的典型布局:
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x000000 | 64KB | 引导程序,不可变 |
| 元数据区 | 0x010000 | 4KB | 分区状态、版本、校验值 |
| 分区A | 0x011000 | 3.5MB | 固件A |
| 分区B | 0x3A1000 | 3.5MB | 固件B |
| 用户数据区 | 0x732000 | 剩余 | 配置、日志等 |
4.1.2 升级流程
我建议的升级流程是这样的:
- Bootloader读取元数据,确定当前激活分区(比如A)
- 下载新固件到备用分区(B),边下载边校验
- 全部写入完成后,对B分区做一次完整校验
- 校验通过,更新元数据,将激活分区标记为B
- 系统重启,Bootloader发现B是激活分区,从B启动
- 如果B启动失败,Bootloader回退到A
我的习惯:在元数据区里多存一份备份。万一元数据区被写坏了,还能从备份恢复。我曾经遇到过Flash某块坏掉导致元数据丢失的情况,从那以后我就坚持双份元数据。
4.2 校验算法选择
校验算法这事儿,说白了就是平衡。你要在安全性和性能之间找个合适的点。我见过有人为了省时间用简单的累加和,结果固件被篡改了都不知道。也见过有人不管什么场景都上SHA256,结果升级慢得要命。
4.2.1 常见算法对比
| 算法 | 输出长度 | 计算速度 | 安全性 | 适用场景 |
|---|---|---|---|---|
| CRC32 | 4字节 | 极快 | 低(防随机错误) | 传输校验、Flash完整性检查 |
| MD5 | 16字节 | 快 | 中(已不推荐安全场景) | 非安全场景的固件校验 |
| SHA256 | 32字节 | 较慢 | 高(抗碰撞) | 安全升级、签名验证 |
4.2.2 我的选择策略
我个人习惯分层使用:
- 传输层:用CRC32。每收到一个包就计算一次,发现错误立即重传。速度快,够用。
- 存储层:用CRC32或MD5。写入Flash后做一次校验,确保数据没写错。
- 安全层:用SHA256。配合签名,确保固件来源可信。
你想想看,如果每包都用SHA256,MCU算力有限,升级时间会翻好几倍。但如果你完全不校验,万一Flash有个坏块,设备就挂了。
注意:CRC32只能检测随机错误,不能防恶意篡改。如果有人故意修改固件,CRC32很容易被绕过。安全场景必须上SHA256+签名。
4.2.3 代码示例:边下载边校验
// 伪代码:边下载边计算CRC32
uint32_t crc = 0;
uint32_t total_bytes = 0;
while (receive_packet(&packet)) {
// 写入Flash
flash_write(packet.addr, packet.data, packet.len);
// 更新CRC
crc = crc32_update(crc, packet.data, packet.len);
total_bytes += packet.len;
// 进度反馈
report_progress(total_bytes, firmware_size);
}
// 下载完成后,对比CRC
if (crc != expected_crc) {
// 校验失败,回滚
rollback();
return ERROR_CRC_MISMATCH;
}
4.3 Flash磨损均衡
Flash的擦写次数是有限的。NOR Flash一般10万次,NAND Flash只有几千到几万次。如果你每次都往同一个地址写元数据,那个块很快就会报废。
4.3.1 磨损均衡策略
我常用的方法有几种:
- 元数据轮转:在元数据区里划出多个槽位,轮流使用。每次更新元数据时写到下一个槽位。
- 动态映射:用一张映射表,把逻辑地址映射到物理地址。每次写操作都选一个擦写次数最少的块。
- 分区交换:A/B分区本身就有均衡效果。每次升级写的是备用分区,两个分区轮流被写。
4.3.2 元数据轮转实现
举个例子,元数据区有4个槽位,每个槽位4KB:
// 元数据槽位结构
typedef struct {
uint32_t magic; // 魔数,标识有效槽位
uint32_t sequence; // 序列号,越大越新
uint32_t active_slot; // 激活分区(0=A, 1=B)
uint32_t firmware_crc; // 固件CRC
uint32_t reserved[60]; // 保留
} metadata_slot_t;
// 查找最新槽位
int find_latest_slot(void) {
int latest_idx = -1;
uint32_t max_seq = 0;
for (int i = 0; i < NUM_SLOTS; i++) {
metadata_slot_t slot;
flash_read(SLOT_ADDR(i), &slot, sizeof(slot));
if (slot.magic == MAGIC_NUMBER && slot.sequence > max_seq) {
max_seq = slot.sequence;
latest_idx = i;
}
}
return latest_idx;
}
// 写入新元数据
void write_metadata(metadata_slot_t *new_data) {
int next_slot = (find_latest_slot() + 1) % NUM_SLOTS;
new_data->sequence = get_next_sequence();
// 擦除槽位
flash_erase(SLOT_ADDR(next_slot));
// 写入新数据
flash_write(SLOT_ADDR(next_slot), new_data, sizeof(*new_data));
}
避坑指南:我曾经遇到过一个问题——元数据槽位擦除后还没写完就断电了,结果所有槽位都无效。后来我加了个"写入中"标志,先写标志再写数据,写完再清除标志。这样就算断电,也能识别出哪个槽位是完整的。
4.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到,双备份、校验、磨损均衡这三块是互相配合的。
4.5 总结
嗯,这一章的内容就这些。双备份机制是底线,校验算法要分层选,磨损均衡不能忽视。我做了这么多年固件,见过太多因为省事而翻车的案例。你想想看,一个产品卖出去几万台,如果因为升级失败导致批量变砖,那损失可不是一星半点。
所以我的建议是:别在这三个地方省钱。该做的备份要做,该算的校验要算,该均衡的磨损要均衡。这样你的固件升级方案才算真正靠谱。