NAND Flash固件备份存储实现
你有没有遇到过这样的情况:设备远程升级到一半突然断电,重启后直接“变砖”?😅 或者现场调试时,一个误操作把固件刷坏了,只能拆机用烧录器救砖?——这在工业网关、IoT终端、智能家居控制器里太常见了。
其实,解决这类问题的关键,并不在于“别出错”,而在于 系统要有容错能力 。就像飞机不会因为一个引擎故障就坠毁一样,我们的嵌入式系统也得具备“双引擎”甚至“应急滑翔”的能力。而其中最经济、最实用的一招,就是: 基于 NAND Flash 的固件备份机制 。
现在大多数中高端嵌入式设备都用 NAND Flash 存储固件——便宜、容量大,动辄几百MB甚至几GB。但 NAND 有个“小脾气”:它天生可能带坏块,擦写多了还会产生新坏块,数据保存时间久了也可能出错。更麻烦的是,它不能像内存那样随意覆盖写入,必须先擦除整个块才能写。
所以,如果你直接把固件往物理地址一扔,不出半年,设备可能就在某个清晨默默罢工了。😤
那怎么办?聪明的做法是: 别指望 NAND 完美无瑕,而是设计一套“即使它出问题也能正常启动”的机制 。
我们来看看怎么玩转这套“保险系统”。
想象一下,你的设备里有两份一模一样的固件,分别放在主区和备份区。平时系统从主区启动;一旦检测到主固件损坏——比如 CRC 校验失败、签名无效、或者干脆读不出来——Bootloader 就会自动切换到备份区启动。是不是有点像“备用轮胎”?
但这还不是全部。真正的高手玩法是: OTA 升级时,新固件不是覆盖当前运行的系统,而是先写进备份区 。等写完、校验通过后,再告诉 Bootloader:“下次启动请从备份区走”。重启之后,新系统顺利跑起来,原来的主区反而变成了新的备份区。🔁
这个过程,学名叫 A/B 分区升级(或称无缝升级) ,Android 系统早就这么干了。而在资源受限的嵌入式系统里,只要合理规划,同样可以低成本实现。
要让这一切跑起来,底层驱动和顶层逻辑都得配合好。先看 NAND 自己的“性格特点”:
- 最小读写单位是“页”(Page),一般是 2KB 到 16KB;
- 擦除单位是“块”(Block),一个块包含几十到几百个页;
- 每个块的擦写寿命有限,SLC 大概 10 万次,MLC/TLC 就更少;
- 出厂就有坏块,使用中还会动态产生坏块;
- 数据容易出错,必须配 ECC(纠错码),比如 BCH 或 LDPC;
- OOB 区(也叫 Spare Area)虽然只有几十字节,但能存关键元信息,比如坏块标记、固件版本、状态标志等。
所以,你不能直接裸读裸写物理地址,得有一层管理机制。Linux 里的
MTD(Memory Technology Device)子系统
就是干这个的。它抽象出
mtdblock
设备节点,帮你处理 ECC、坏块跳过、地址映射等问题。你可以把它理解为 NAND 的“管家”。
在实际初始化时,第一件事就是扫描所有块,看看哪些是坏的:
int nand_init(void) {
uint32_t block;
for (block = 0; block < TOTAL_BLOCKS; block++) {
if (nand_is_bad_block(block)) {
mark_block_as_reserved(block);
continue;
}
if (!nand_read_oob(block * PAGES_PER_BLOCK, &oob_data)) {
if (is_firmware_valid(&oob_data)) {
add_to_firmware_list(block);
}
}
}
return 0;
}
int nand_is_bad_block(uint32_t block) {
uint8_t marker;
read_spare_area(block * PAGES_PER_BLOCK, &marker, 1);
return (marker != 0xFF); // 厂标非0xFF即为坏块
}
这段代码干了三件事:
1. 遍历所有块;
2. 检查是否为坏块(通过读首个页的 OOB 区);
3. 如果不是坏块,再看看里面有没有有效的固件头。
这样一来,系统启动时就知道“哪里能读、哪里有货”,避免踩雷。💣
接下来是重头戏: Bootloader 怎么决定从哪启动?
我们可以在每个固件镜像前面加一个“头信息”,记录版本、CRC、状态等:
typedef struct {
uint32_t fw_version;
uint32_t crc32;
uint8_t status; // 0: invalid, 1: valid, 2: upgrading
uint8_t reserved[7];
} firmware_header_t;
然后 Bootloader 上电后就这么判断:
boot_source_t select_boot_source(void) {
firmware_header_t primary_hdr, backup_hdr;
if (read_firmware_header(PRIMARY_FW_BASE, &primary_hdr) == 0 &&
primary_hdr.status == FW_VALID &&
verify_crc(&primary_hdr)) {
return BOOT_PRIMARY;
}
if (read_firmware_header(BACKUP_FW_BASE, &backup_hdr) == 0 &&
backup_hdr.status == FW_VALID &&
verify_crc(&backup_hdr)) {
return BOOT_BACKUP;
}
enter_recovery_mode();
return BOOT_NONE;
}
你看,逻辑非常清晰:
- 先试主区,成功就走;
- 主区不行,再试备份区;
- 两个都挂了?那就进 recovery 模式,比如通过 USB 或串口重新烧录。
这种设计下,哪怕 OTA 升级中断,顶多就是“升级失败”,不会导致设备无法启动。👏
再来看看典型的应用场景——比如一台工业网关:
+---------------------+
| Application |
+---------------------+
| OS / RTOS |
+---------------------+
| Firmware Image B | ← OTA 升级目标
+---------------------+
| Firmware Image A | ← 当前运行系统
+---------------------+
| FTL / MTD | ← NAND 管理层(ECC + 坏块处理)
+---------------------+
| NAND Flash HW |
+---------------------+
它的升级流程大概是这样:
- 设备收到新固件包,通过 HTTPS 或 MQTT 下发;
- 固件被写入 备份分区 (Image B),不影响当前运行的 Image A;
- 写完后计算 CRC,验证完整性;
- 更新启动配置:设置“下次从 B 启动”;
- 发出 reboot 指令;
- U-Boot 启动后读取标记,发现该切了,于是加载 Image B;
- 新系统运行成功,再把旧的 A 区标记为备份,完成轮换。
整个过程,用户几乎无感。🎉
如果不幸在第 2 步断电了呢?没关系!下次上电,Bootloader 发现主区还是好的,继续从 A 启动,一切照常。等网络恢复后,OTA 任务还能重试。
当然,这种方案也不是“开箱即用”,实际落地时有几个坑要注意:
🔧 分区大小怎么定?
建议主备分区大小一致,且预留至少 10% 的额外空间用于磨损均衡和垃圾回收。别忘了,NAND 不是无限擦写的!
🔐 ECC 强度够不够?
SLC NAND 可以用 BCH-4bit,但如果你用的是 MLC 或 TLC 颗粒,建议上 BCH-8bit 甚至 LDPC,否则纠不了错,备份也没意义。
💾 写放大怎么控制?
频繁写小数据会导致大量无效擦写。建议采用缓冲合并策略,批量写入,减少对 NAND 的折腾。
⚡ 断电保护怎么做?
最关键的状态标记(比如“即将切换”)一定要写多次,并做 CRC 校验。最好还能加上掉电检测电路,在断电瞬间完成关键数据落盘。
🔒 安全性考虑
固件最好加上 AES 加密 + RSA 签名。否则黑客拿个编程器一读,算法全暴露;再伪造个低版本固件降级攻击,系统就危险了。防回滚机制一定要有,Bootloader 得拒绝比当前版本低的固件启动。
说到这里,你可能会问:为什么不直接用 NOR Flash?它支持 XIP(就地执行),启动快,还稳定。
答案很简单:
成本和容量
。
一块 32MB 的 NOR Flash 可能比 1GB 的 NAND 还贵。而现在的 Linux 系统动不动就上百MB,APP 越来越臃肿,NOR 根本装不下。NAND 虽然复杂点,但胜在“性价比之王”。
最后总结一下,真正靠谱的固件存储方案,不是靠硬件“不坏”,而是靠架构“容错”。而 NAND Flash + 双分区备份 + 智能 Bootloader 的组合,正是这一思想的最佳实践之一。
它带来的不只是“不怕升级失败”,更是:
- 远程运维的底气;
- 快速迭代的能力;
- 用户体验的保障;
- 维护成本的大幅降低。
换句话说, 这不是锦上添花的功能,而是现代智能设备的“生存底线” 。🛠️
所以,下次你在画 PCB、选 Flash 颗粒、写 Bootloader 的时候,不妨多花半小时想想:我的系统,有“备胎”吗?🚗💨
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
452

被折叠的 条评论
为什么被折叠?



