3. 存储布局:Flash存储器原理、分区表设计、坏块管理策略
好,咱们进入第三章。这一章我打算聊聊存储布局。说白了,就是你的固件在Flash里怎么放、放哪儿、放多大。
很多新手工程师觉得这步很简单——找个空地儿,把固件写进去不就完了?嗯,我当年也是这么想的。直到有一次,我在一个量产项目上,因为分区表设计不合理,导致OTA升级时把Bootloader给覆盖了……那叫一个酸爽。从那以后,我再也不敢轻视存储布局了。
3.1 Flash存储器原理
先说说Flash本身。咱们游戏机里用的,基本都是NOR Flash或NAND Flash。我个人更偏爱NOR Flash做Bootloader,因为它支持XIP(片上执行),上电就能跑代码,不用先把代码拷到RAM里。
Flash有几个关键特性,你得刻在脑子里:
- 擦除粒度大:NOR Flash通常按扇区擦除(64KB或128KB),NAND Flash按块擦除(128KB或256KB)。你不能像写RAM那样,想改哪个字节就改哪个字节。
- 写入前必须擦除:Flash的写入操作只能把1变成0,想把0变回1?必须擦除整个扇区/块。
- 有寿命限制:每个扇区/块有擦写次数上限。NOR一般是10万次,NAND是1万到10万次不等。
核心要点:Flash不是RAM,别用RAM的思维去操作它。每次写入前,先问问自己——这个扇区擦过了吗?
你想想看,如果OTA升级时频繁擦写同一个区域,那块区域很快就报废了。所以分区设计时,得把频繁更新的区域和几乎不变的区域分开。
3.2 分区表设计
分区表,就是一张地图。它告诉Bootloader:你的代码在哪儿,OTA固件放哪儿,用户数据存哪儿。
我习惯把游戏机的Flash分成四个区:
| 分区名称 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x00000000 | 256KB | 上电启动、校验App、进入OTA模式 |
| Factory | 0x00040000 | 2MB | 出厂固件,永不修改,作为最后一道防线 |
| App | 0x00240000 | 4MB | 主固件,正常运行时执行 |
| OTA | 0x00640000 | 4MB | 下载新固件,校验后替换App区 |
这个布局,我用了好多年。为什么Bootloader只给256KB?因为Bootloader的功能很纯粹——启动、校验、跳转。代码量不会太大。给多了浪费,给少了不够用。256KB是个黄金尺寸。
3.2.1 Bootloader分区
Bootloader是第一个上电执行的程序。它的任务很简单:
- 初始化硬件(时钟、内存、Flash控制器)
- 校验App分区的固件完整性(CRC或签名)
- 如果校验通过,跳转到App区执行
- 如果校验失败,进入OTA模式等待升级
这里有个坑——Bootloader本身不能升级自己。为什么?因为如果升级Bootloader时断电了,你的设备就变砖了。所以Bootloader必须放在一个安全的位置,永远不变。
警告:千万不要把Bootloader放在OTA可覆盖的区域。我曾经见过一个方案,把Bootloader和App放在同一个分区,结果OTA升级时写错了偏移量……嗯,那批货全废了。
3.2.2 Factory分区
Factory分区,说白了就是「后悔药」。里面存的是出厂时的原始固件,永远不会被OTA覆盖。
什么时候用到它?当App和OTA都坏了的时候。Bootloader会检测到两个分区都校验失败,然后从Factory分区恢复固件到App区。这样设备至少还能正常启动。
我个人建议Factory分区的大小和App分区一致。这样恢复时直接整块拷贝,不用做地址转换,逻辑简单,不容易出错。
3.2.3 App分区和OTA分区
这两个分区是联动的。App分区存当前运行的固件,OTA分区存下载好的新固件。
升级流程是这样的:
- 设备正常运行,从服务器下载新固件到OTA分区
- 下载完成后,校验OTA分区的完整性
- 校验通过后,设置一个标志位,告诉Bootloader下次启动时从OTA分区拷贝到App分区
- 重启设备,Bootloader检测到标志位,执行拷贝操作
- 拷贝完成后,清除标志位,跳转到新的App分区执行
这个方案的好处是——即使拷贝过程中断电,设备重启后还会重新执行拷贝,不会变砖。因为OTA分区里的固件还在。
小技巧:我习惯在OTA分区末尾留4KB的空间,专门存升级状态标志。这样即使App分区被写坏了,Bootloader也能通过这个标志知道该做什么。
3.3 坏块管理策略
坏块管理,主要是NAND Flash的痛点。NOR Flash基本没有坏块问题,但NAND Flash出厂时就可能有坏块,而且随着使用还会产生新的坏块。
你想想看,如果OTA升级时恰好写到了一个坏块上,那固件就损坏了。所以必须有一套机制来避开坏块。
我常用的策略有三种:
3.3.1 跳过坏块(Skip Bad Block)
最简单粗暴的方法。在写入数据时,检测到坏块就跳过,把数据写到下一个好块里。
// 伪代码示例
for (block = start_block; block < end_block; block++) {
if (is_bad_block(block)) {
continue; // 跳过坏块
}
write_data(block, data);
data += block_size;
}
这个方法的问题在于——地址不连续了。如果App固件是连续存放的,跳过坏块后,后面的数据地址就乱了。所以这个方法只适合数据存储,不适合固件存储。
3.3.2 坏块替换(Bad Block Replacement)
在Flash的末尾预留一些备用块。当遇到坏块时,从备用块里拿一个好块来替换。
具体做法是维护一张映射表:
| 逻辑块号 | 物理块号 | 状态 |
|---|---|---|
| 0 | 0 | 好块 |
| 1 | 1 | 好块 |
| 2 | 3 | 坏块,替换到物理块3 |
| 3 | 4 | 好块 |
这个方法的好处是——上层软件看到的地址是连续的,不用关心底层坏块。坏块管理对App完全透明。
注意:映射表本身也要存在Flash里。我建议把映射表放在Flash的头部,并且存两份备份。万一读坏了,还有另一份可以用。
3.3.3 带ECC的坏块管理
这是最可靠的方法。在写入数据时,同时写入ECC校验码。读取时,如果发现数据有错误,ECC能纠正一定数量的比特错误。
NAND Flash的每个页(Page)通常有额外的空间(OOB区)来存ECC码。比如一个2KB的页,OOB区有64字节,足够存ECC了。
我建议的做法是:
- 每512字节数据,生成16字节的ECC码
- ECC码存在OOB区对应的位置
- 读取时,先读数据,再读ECC码,然后校验
- 如果校验失败且无法纠正,标记该块为坏块
这个方法能纠正1-2个比特的错误,对于NAND Flash常见的比特翻转问题非常有效。
重要提醒:ECC算法不要自己写。用现成的库,比如BCH或RS码。我自己曾经手写过ECC算法,结果有个边界条件没处理好,导致某些错误没被检测出来……从那以后,我再也不自己写ECC了。
3.4 实际项目中的经验总结
说了这么多,最后分享几个我在项目中踩过的坑:
- 分区大小要留余量:固件只会越来越大,不会越来越小。我建议每个分区至少留30%的余量。
- 坏块管理要尽早做:不要在项目后期才加坏块管理。那时候代码结构已经定了,加进去会很痛苦。
- 测试要覆盖坏块场景:我习惯在测试时,人为制造一些坏块,看看系统能不能正确处理。很多问题都是在测试中发现的。
- 备份!备份!备份!:关键数据(分区表、映射表、升级标志)至少存两份。一份坏了,还有另一份。
好了,这一章就到这里。下一章我会讲OTA升级的通信协议设计,包括怎么断点续传、怎么校验完整性。咱们到时候见。