3、存储介质与分区表:NOR Flash、NAND Flash、eMMC的特性差异,以及MBR与GPT分区表格式
做Bootloader开发,说白了就是跟存储介质和分区表打交道。这三者选型不对,后面分区表解析写得再好也白搭。我这些年踩过的坑,多半都跟没吃透存储介质特性有关。
今天咱们就把NOR Flash、NAND Flash、eMMC这三兄弟掰开揉碎,再聊聊MBR和GPT这两种分区表格式。嗯,内容有点多,但都是干货。
3.1 三种存储介质的核心差异
先看一张对比表,心里有个底:
| 特性 | NOR Flash | NAND Flash | eMMC |
|---|---|---|---|
| 读取速度 | 快(随机读取极快) | 较快(顺序读取快) | 快(内置控制器优化) |
| 写入速度 | 慢(按字节写入) | 快(按页写入) | 快(支持Cache) |
| 擦除方式 | 按扇区(64KB~128KB) | 按块(128KB~2MB) | 按块(由FTL管理) |
| 坏块管理 | 无坏块问题 | 出厂就有坏块,需管理 | 内置坏块管理 |
| XIP(原地执行) | 支持 | 不支持 | 不支持 |
| 寿命(P/E次数) | 10万~100万次 | 1千~10万次(取决于制程) | 由内部NAND决定 |
| 接口 | SRAM-like并行/SPI | NAND接口/SPI | MMC/SDIO |
| 典型容量 | 1MB~512MB | 128MB~1TB | 4GB~256GB |
| 成本 | 高 | 低 | 中等 |
3.2 NOR Flash:Bootloader的黄金搭档
NOR Flash最大的特点就是支持XIP(eXecute In Place)。什么意思?就是CPU可以直接从NOR Flash里面取指令执行,不需要先把代码拷贝到RAM里。这对Bootloader来说太重要了。
我记得早期做一款工业控制器,用的就是SPI NOR Flash。Bootloader只有64KB,直接在Flash里跑,上电后初始化DDR,再把主程序从NAND Flash里搬过去。整个过程干净利落。
NOR Flash在Bootloader中的典型用法:
- 存放一级Bootloader(SPL/FSBL),直接XIP执行
- 存放关键配置参数(MAC地址、序列号、校准数据)
- 存放启动向量表和安全固件
但NOR Flash也有短板——写入太慢,容量做不大。你想想看,擦除一个扇区要几百毫秒,写一个字节也要几十微秒。所以它只适合放小体量的关键代码。
个人经验:用SPI NOR Flash时,一定要把写保护引脚(WP#)拉高,否则调试时不小心写错数据,板子就变砖了。我吃过这个亏,焊了10块板子,有3块因为WP#悬空导致配置被意外擦除。
3.3 NAND Flash:大容量但需要伺候
NAND Flash容量大、价格低,但脾气也大。出厂就有坏块,用着用着还会产生新坏块。而且它不支持XIP,Bootloader不能直接在NAND上跑。
为什么会这样?因为NAND Flash的读取是页模式的,一次读一页(通常2KB~16KB),而且读取过程中可能有位翻转。CPU没法直接从这种介质里取指令。
所以NAND Flash的Bootloader方案通常是这样的:
- ROM Code(芯片内部固件)从NAND读取前几页到SRAM
- 这几页代码初始化DDR控制器
- 再把完整的Bootloader从NAND搬到DDR里
- 最后跳转到DDR执行
避坑指南:我曾经在一个项目里用了SLC NAND,没做坏块管理,结果生产了1000台设备,有30台启动失败。后来加上坏块表和ECC校验,问题才解决。记住:NAND Flash必须做坏块管理和ECC,这是硬性要求,不是可选项。
NAND Flash还有个特点——读写操作要遵循严格的时序。比如写一页之前必须先擦除整个块,不能像NOR那样直接覆盖写。这个特性直接影响分区表的设计。
3.4 eMMC:省心的集成方案
eMMC本质上就是把NAND Flash和一个微控制器(MMC Controller)封装在一起。这个控制器负责坏块管理、磨损均衡、ECC校验,对外暴露一个标准的MMC/SDIO接口。
对Bootloader开发者来说,eMMC最大的好处就是——你不用管底层那些破事了。坏块?控制器帮你处理了。位翻转?控制器帮你纠正了。你只需要通过CMD和DAT线读写块就行。
但eMMC也有坑:
- 启动分区:eMMC有专门的Boot Area Partition,可以配置成只读,用来放Bootloader。这个分区默认是写保护的,需要发特殊命令才能解锁。
- RPMB分区:Replay Protected Memory Block,用于存放安全数据。访问需要认证,Bootloader里要处理这个逻辑。
- 擦除超时:eMMC擦除大块数据时可能耗时较长,Bootloader里要设置合理的超时时间。
我建议:用eMMC做Bootloader存储时,把Bootloader放在Boot Area Partition,把文件系统和应用放在User Data Area。这样即使文件系统坏了,Bootloader还能启动进入恢复模式。
3.5 分区表格式:MBR vs GPT
分区表就是一张地图,告诉Bootloader哪个分区在哪、有多大、干什么用。目前主流就两种:MBR和GPT。
3.5.1 MBR(Master Boot Record)
MBR诞生于1983年,跟IBM PC/AT一个年代。它放在存储介质的第一个扇区(LBA 0),结构很简单:
MBR结构(512字节):
+------------------+ 0x000
| Boot Code | 446字节(x86引导代码)
+------------------+ 0x1BE
| 分区表项1 | 16字节
+------------------+ 0x1CE
| 分区表项2 | 16字节
+------------------+ 0x1DE
| 分区表项3 | 16字节
+------------------+ 0x1EE
| 分区表项4 | 16字节
+------------------+ 0x1FE
| 0x55 0xAA | 2字节(结束标志)
+------------------+ 0x1FF
每个分区表项16字节,包含:
- 引导标志(1字节):0x80表示活动分区
- 起始CHS地址(3字节):已经过时了
- 分区类型(1字节):0x0C=FAT32,0x83=Linux,0xEE=GPT保护分区
- 结束CHS地址(3字节):同样过时
- 起始LBA地址(4字节):最大2TB
- 分区大小(4字节):最大2TB
MBR的局限性很明显:
- 最多4个主分区(或者3主+1扩展)
- 单个分区最大2TB(因为LBA地址用32位)
- 没有备份,MBR坏了整个盘就废了
- 没有校验,数据损坏了也不知道
3.5.2 GPT(GUID Partition Table)
GPT是UEFI时代的产物,解决了MBR的所有痛点。它使用LBA 1~33来存放分区表,LBA 0放的是Protective MBR(防止老工具误识别)。
GPT布局:
+------------------+ LBA 0
| Protective MBR | 防止老工具误操作
+------------------+ LBA 1
| GPT Header | 主GPT头
+------------------+ LBA 2~33
| 分区表项数组 | 每个项128字节,最多128个分区
+------------------+ LBA 34~N
| 用户数据区 |
+------------------+ LBA -34~-1
| 备份分区表项 | GPT备份
+------------------+ LBA -1
| 备份GPT Header |
+------------------+
GPT的优势:
- 支持最多128个分区(Windows限制,理论上可以更多)
- 单个分区最大18EB(64位LBA)
- 有备份GPT,主GPT坏了还能恢复
- 每个分区有唯一的GUID,不会搞混
- 有CRC32校验,数据完整性有保障
Bootloader里怎么选?
- NOR Flash + 小容量:用固定偏移,不用分区表。Bootloader直接写在固定地址,参数区也固定。
- NAND Flash + 2TB以下:MBR够用,解析简单,代码量小。
- eMMC + 大容量:强烈建议用GPT。eMMC容量动辄32GB以上,MBR的2TB限制虽然暂时够用,但分区数量限制很烦人。
- 安全启动场景:GPT更合适,因为可以给每个分区加GUID类型,Bootloader根据GUID识别分区,不容易被篡改。
3.6 三种介质与分区表的适配关系
下面这张SVG图把整个知识体系串起来了:
3.7 Bootloader中解析分区表的注意事项
最后聊几个实战中容易踩的坑:
我曾经在解析MBR时犯过一个低级错误:直接读取LBA 0的MBR数据,没检查0x55 0xAA标志。结果有一批Flash出厂时前512字节被写成了全0,Bootloader把全0当成有效MBR,解析出来的分区全是0,直接跑飞了。后来加了个校验,如果MBR无效就尝试GPT,再不行就用默认分区布局。
几个关键点:
- 字节序:MBR和GPT都是小端字节序,ARM Cortex-M默认也是小端,但有些RISC-V核默认大端,记得做转换。
- 对齐:NAND Flash的分区起始地址要对齐到块大小(比如128KB),eMMC要对齐到擦除组大小(通常512KB)。不对齐的话,写入性能会暴跌。
- 备份:如果存储介质支持,把分区表备份一份。我习惯在GPT的备份区域之外,再在固定偏移存一份分区表副本。万一主备都坏了,还能用副本恢复。
- 分区类型:定义一套自己的分区类型GUID,比如Bootloader分区用"xxx-boot",参数分区用"xxx-param",文件系统分区用"xxx-rootfs"。这样Bootloader可以根据GUID自动识别分区,不用硬编码偏移。
我建议:写Bootloader时,把分区表解析做成一个独立的模块。输入是原始扇区数据,输出是分区列表。这样不管是MBR还是GPT,上层代码都不用改。我现在的项目里,分区表解析模块只有300行C代码,却支持了5种不同的存储介质。
嗯,存储介质和分区表这块就聊到这儿。记住一句话:没有最好的方案,只有最合适的方案。选型时把容量、成本、可靠性、开发复杂度都摆到桌面上,自然就知道该用啥了。