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方案通常是这样的:

  1. ROM Code(芯片内部固件)从NAND读取前几页到SRAM
  2. 这几页代码初始化DDR控制器
  3. 再把完整的Bootloader从NAND搬到DDR里
  4. 最后跳转到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图把整个知识体系串起来了:

存储介质与分区表适配关系 NOR Flash NAND Flash eMMC XIP支持 · 高可靠性 容量小 · 写入慢 大容量 · 低成本 需坏块管理 · 不支持XIP 集成控制器 · 接口标准 内置FTL · 启动分区 固定偏移(无分区表) MBR 分区表 GPT 分区表 典型场景: SPI Flash + 一级Bootloader 参数存储 · 启动向量 典型场景: 嵌入式Linux · 文件系统 大容量数据存储 典型场景: Android设备 · 车机系统 安全启动 · 多分区需求 核心原则:小容量NOR用固定偏移,中容量NAND用MBR,大容量eMMC用GPT Bootloader必须同时支持三种介质和两种分区表,才能做到平台通用

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种不同的存储介质。

嗯,存储介质和分区表这块就聊到这儿。记住一句话:没有最好的方案,只有最合适的方案。选型时把容量、成本、可靠性、开发复杂度都摆到桌面上,自然就知道该用啥了。