SPI Flash读写原理详解:从硬件时序到BIOS代码层面
本文从PCH内存映射窗口讲到EDK2源码,把硬件时序和固件代码串成一条线。
一、SPI Flash不是"硬盘"——它是内存映射窗口里的字节流
刚入行的同事盯着电路板问我:“师傅,BIOS代码存在这颗Flash芯片里对吧?CPU上电以后怎么执行?能直接读Flash吗?”
能。但不是你以为的那种"能"。
他以为CPU把Flash当硬盘——先读到内存,再从内存执行。不是。CPU上电后的第一条指令,直接从Flash里取。没有"加载"这个过程。Flash自己就是内存的一部分——或者更准确地说,Flash通过PCH(Platform Controller Hub)里的内存映射机制,假装自己是ROM。
这个"假装"是怎么做到的?
复位向量落在哪
x86 CPU上电后,第一条指令的地址是 0xFFFFFFF0——复位向量。这个地址不在DRAM里,因为DRAM还没初始化。DDR时序训练、MRC跑起来之前,CPU根本没地方存东西。
那0xFFFFFFF0落在哪?落在SPI Flash的内存映射窗口里。
PCH内部有一个Memory Map Decoder,维护着一张地址路由表:哪些物理地址范围交给DRAM Controller,哪些交给SPI Controller。上电默认路由策略:高地址段(0xFF000000 ~ 0xFFFFFFFF,16MB顶部空间)映射到SPI Flash。
1:1直接映射:物理地址的偏移量直接等于Flash地址。CPU读0xFFF00000,SPI Controller就去Flash的0x00F00000取数据。
预取缓冲(Prefetch Buffer):SPI Controller内部有缓存。CPU读一个地址,Controller一口气取64字节甚至更多放进缓冲区。因为CPU取指令通常是顺序的,预取能大幅降低SPI总线上的命令开销。
关键数字:SPI Flash发一次Quad I/O Read命令,命令开销大约20~30个SPI时钟周期。如果CPU每次只取32字节,Controller得不停发新命令——命令相位占的时间比数据相位还多。固件启动慢的根源不是CPU执行的指令多,而是取指本身就慢。
就像你每次看一页纸都要重新打开保险柜、伸手进去、找到那一页——而不是把整本手册拿在手里翻。这就是SPI Flash XIP(Execute In Place)的本质。
二、Flash Descriptor:Flash的"分区表"
每颗SPI Flash最前面的4KB不是你的代码,是Flash Descriptor(IFD,Intel Flash Descriptor)。它是Flash的"分区表",没有它,PCH根本不知道该从哪读BIOS。
Region划分
| Region编号 | 名称 | 内容 |
|---|---|---|
| 0 | Flash Descriptor | IFD自身,元数据 |
| 1 | BIOS / UEFI Firmware | 你的固件代码 |
| 2 | Intel ME | Management Engine固件 |
| 3 | GbE | 网卡PXE ROM和配置 |
| 4 | Platform Data | EC固件或其他平台数据 |
一个16MB SPI Flash的真实布局:
0x000000 - 0x000FFF Flash Descriptor (4KB)
0x001000 - 0x6FFFFF ME Region (~7MB)
0x700000 - 0xFEFFFF BIOS Region (~9MB)
0xFF0000 - 0xFFFFFF BIOS Region (顶部) (64KB) ← 复位向量在这里
BIOS Region必须覆盖Flash顶部。 CPU复位向量0xFFFFFFF0经PCH映射后落到Flash偏移0xFFFFF0——必须在BIOS Region内,否则CPU取不到复位向量,直接死。
FREG寄存器
PCH通过FREG寄存器读取Region的Base和Limit:
// Silicon/Intel/CoffeelakeSiliconPkg/Pch/Include/Register/PchRegsSpi.h
#define R_PCH_SPI_FREG0 0x54 // Flash Region 0 (Flash Descriptor)
#define R_PCH_SPI_FREG1 0x58 // Flash Region 1 (BIOS)
#define R_PCH_SPI_FREG2 0x5C // Flash Region 2 (ME)
#define R_PCH_SPI_FREG3 0x60 // Flash Region 3 (GbE)
#define R_PCH_SPI_FREG4 0x64 // Flash Region 4 (Platform Data)
#define B_PCH_SPI_FREG_BASE_MASK 0x7FFF // Base字段:bit[14:0]
#define B_PCH_SPI_FREG_LIMIT_MASK 0x7FFF0000 // Limit字段:bit[30:16]
Base和Limit以4KB为单位。看到Base=0x0070、Limit=0x00FE,实际地址是Base * 0x1000到(Limit + 1) * 0x1000 - 1。
踩坑:FREG1(BIOS Region)的Limit值少算了一个扇区。BIOS Region结束地址落在代码段中间,后面的字节SPI Controller不认,全部返回0xFF。CPU取指正好卡在那个边界上,把0xFFFF解析为非法指令,触发#UD异常。FREG的Base和Limit必须精确对齐到Flash扇区边界。
FRAP寄存器:谁能读、谁能写
FRAP控制访问权限,有两个独立的位掩码:一个控制CPU对各Region的读/写权限,另一个控制ME的权限。CPU能读BIOS Region不代表ME也能读。ME通常被关在ME Region里。
踩坑:拿DediProg烧录器直接把BIOS bin(8MB)烧进16MB Flash。主板亮了,但ME设备挂黄叹号,网络不通。原因:BIOS bin只覆盖了BIOS Region,ME Region和GbE Region是空的。必须用FIT工具把IFD + ME + BIOS + GbE打包成完整Flash image再烧录。
三、SPI Controller核心寄存器
SPI Controller挂在PCI总线B:D:F = 0:1F:5,但寄存器通过专用MMIO窗口访问,基地址约0xFED01000。
| 偏移 | 寄存器 | 作用 |
|---|---|---|
| 0x00 | BFPR | BIOS Flash Primary Region基地址 |
| 0x04 | HSFS | 硬件定序状态和命令(查状态看它) |
| 0x06 | HSFC | 硬件定序控制(发命令看它) |
| 0x08 | FADDR | 软件定序模式下的Flash目标地址 |
| 0x0C | FDATA0 | Flash数据寄存器(低32位) |
| 0x50 | FRAP | Flash Region访问权限 |
| 0x54~0x64 | FREG0~FREG4 | 各Region的Base/Limit |
| 0x74~0x84 | PR0~PR4 | Protected Range寄存器组 |
| 0x90 | SSFS | 软件定序状态 |
| 0x91 | SSFC | 软件定序控制 |
HSFS寄存器调试时最常用:
| 位 | 名称 | 含义 |
|---|---|---|
| 0 | FDONE | Flash操作完成(硬件定序) |
| 1 | FCERR | Flash周期错误 |
| 2 | AEL | Access Error Log——有人访问了不该访问的Region |
| 5 | SCIP | SPI Cycle In Progress——Flash正忙 |
| 15 | FLOCKDN | Flash配置锁定 |
调试第一件事永远是看HSFS寄存器。 SCIP(是否忙)、AEL(是否越界访问)、FCERR(是否命令失败)、FLOCKDN(配置是否已锁)——这四位的状态比你猜代码逻辑有用得多。
四、硬件定序 vs 软件定序
SPI Controller支持两种操作模式,搞不清什么时候用哪种,要么性能拉胯,要么根本写不进去。
硬件定序(Hardware Sequencing)
你只需要告诉Controller:“帮我把Flash偏移0x700000开始的128KB数据搬到内存地址0x80000000”。Controller自己搞定全部底层细节——发Read命令、检查WIP位、等待BUSY清除、读取数据。
PEI阶段加载FV到CAR或DRAM时默认走硬件定序。一条HSFC写命令,PCH自动处理地址转换、SPI命令组装、等待Flash就绪。
软件定序(Software Sequencing)
你直接操控SPI总线的每个时钟周期。发什么命令、什么地址、多少字节、什么时候读——全由代码控制。
Flash的编程(Program)和块擦除(Sector Erase)必须走软件定序。 因为硬件定序不支持这些特殊命令格式。比如Write Enable必须先发,然后才是Erase命令,中间不能断。
// IntelSiliconPkg/Feature/PcieSpiLib/SpiFlashCommon.c
// 硬件定序:一条命令搞定
PchSpiAndThenOr32 (
PCH_SPI_HSFC,
(UINT32) ~B_PCH_SPI_HSFC_FDBC_MASK, // 清空Flash Data Byte Count
(UINT32) (ByteCount - 1) // 设置数据字节数
);
PchSpiOr32 (PCH_SPI_HSFC, B_PCH_SPI_HSFC_FGO); // 发起Flash Cycle Go
// 软件定序:手动控制每个步骤
PchSpiWrite8 (PCH_SPI_SSFC, (UINT8) (command));
PchSpiWrite32 (PCH_SPI_FADDR, flashAddr);
// ... 逐字节读写 FDATA0/FDATA1
| 操作 | 用哪种 | 为什么 |
|---|---|---|
| 读数据 | 硬件定序 | 快,PCH自动处理 |
| 读JEDEC ID | 硬件定序 | 标准操作,硬件支持 |
| 页编程(Program) | 软件定序 | 需要先发Write Enable |
| 扇区擦除(Erase) | 软件定序 | 命令序列硬件不支持 |
| 读SFDP表 | 软件定序 | 非标准命令0x5A |
五、SPI读取时序:从CPU指令到Flash数据
用一次真实的CPU取指过程,把整条链路走一遍。
场景:PEI阶段,CPU要取复位向量0xFFFFFFF0处的指令。
Step 1:CPU取指单元向L1 I-Cache发地址0xFFFFFFF0。Cache Miss(冷启动,Cache是空的)。
Step 2:地址通过Core Ring送到System Agent。
Step 3:System Agent查Memory Map Decoder。地址落在0xFF000000 ~ 0xFFFFFFFF → 路由到DMI/OPI链路 → 发给PCH。
Step 4:PCH收到DMI事务。查表:这个地址属于BIOS Region的映射窗口。
Step 5:PCH查FRAP寄存器,确认CPU对Region 1有读权限。
Step 6:PCH把物理地址0xFFFFFFF0转换成Flash偏移:0xFFFFFFF0 - 0xFF000000 = 0x00FFFFF0。
Step 7:SPI Controller组装Quad I/O Read命令帧:
CS#: ──────┐ ┌────────────
IO0: ────── EB ── A23..A16 ── A15..A8 ── A7..A0 ── M3..M0 ═══ D0 ═══ D4 ═══ ...
IO1: ────── EB ── A23..A16 ── A15..A8 ── A7..A0 ── M3..M0 ═══ D1 ═══ D5 ═══ ...
IO2: ────── EB ── A23..A16 ── A15..A8 ── A7..A0 ── M3..M0 ═══ D2 ═══ D6 ═══ ...
IO3: ────── EB ── A23..A16 ── A15..A8 ── A7..A0 ── M3..M0 ═══ D3 ═══ D7 ═══ ...
- 0xEB:Quad I/O Read命令字节
- 3字节地址:0xFFFFF0
- 1字节Mode + 4个Dummy时钟周期
- 数据输出:4根线同时输出,8个时钟周期出4个字节
Step 8:Flash在Dummy周期结束后吐出第一个字节。SPI Controller把4条数据线拼成字节,放进Prefetch Buffer。
Step 9:Prefetch Buffer连续从Flash拉64字节,再一次性通过DMI返回给CPU。
Step 10:CPU收到数据,填入L1 I-Cache。取指成功,开始执行第一条指令。
在50MHz SPI时钟 + Quad I/O模式下,第一次取指延迟约300~500ns。命中Prefetch Buffer后降到几十纳秒。CPU一旦跳转(Branch),又会回到300~500ns。
这就是为什么固件里会看到大量rep movsb——一次性把Flash里的FV搬到CAR或DRAM,然后直接从内存执行。在Flash里原地执行(XIP)太慢了。
六、SFDP表:Flash的"自我介绍"——能信吗?
符合JESD216标准的SPI Flash都内置了SFDP(Serial Flash Discoverable Parameters)表。发一条Read SFDP命令(0x5A),Flash就把参数完整吐给你:容量、扇区大小、支持的命令集、最大时钟频率、擦除时间、状态寄存器布局。
有了SFDP,理论上不用维护Flash型号表。Linux内核的SPI NOR框架就是这么干的——优先解析SFDP,解析不到才查硬编码型号表。
但固件世界不完全信任SFDP。EDK2里依然维护着芯片型号表:
typedef struct {
UINT8 DeviceId[3]; // JEDEC Manufacturer ID + Device ID
UINT32 DeviceSize; // 总容量
UINT32 BlockSize; // 擦除块大小
UINT32 PageSize; // 页编程大小
UINT8 OpcodeRead; // 读取操作码
UINT8 OpcodeWrite; // 页编程操作码
UINT8 OpcodeErase; // 扇区擦除操作码
UINT8 AddressBytes; // 地址字节数(3或4)
BOOLEAN SupportQuadRead; // 是否支持Quad I/O Read
} SPI_PART_TABLE;
踩坑:国产Flash的SFDP说谎
遇到一颗国产SPI NOR Flash,SFDP表写着"支持1-1-4 Quad I/O Read,最大104MHz"。信了,配成Quad Read模式,结果第3个字节开始全是垃圾。
示波器挂上去看:IO2引脚电平一直不动。这颗Flash的IO2内部被配成了普通GPIO,没接到数据输出缓冲区。SFDP表是厂商烧进去的静态数据——小厂工程师可能直接把竞品的SFDP复制过来改了个ID。SFDP表里的参数可能是"设计规格"而不是"实测规格"。
解决:查Datasheet发现IO2需要先写厂商专用配置寄存器才能切Quad模式。SFDP没说这步。手动加了一段厂商专用命令后正常。
经验:SFDP可以信,但不能全信。 Vendor ID不是Micron/Winbond/Macronix/ISSI这些大厂,就先用Dual Read做数据完整性验证,确认Quad Read输出正确再切。
七、三层写保护——你写不进Flash的原因
SPI Controller的写保护不是"开"或"关"那么简单。有三层,层层递进。写不进Flash,三层都检查一遍。
第一层:Protected Range寄存器(PR0 ~ PR4)
PR寄存器在硬件层面限制可写的Flash地址范围。设好后,任何针对这个范围的SPI Write/Erase命令都被SPI Controller在硬件层直接拦截——不送到Flash芯片,直接返回错误。
PR寄存器本身可以被锁定。PR0FLOCKD位(HSFS第14位)置1后,所有PR寄存器配置再也改不了——直到下次平台复位。BIOS在PEI阶段设好PR0保护Boot Block,然后上锁。
第二层:SMM BIOS Lock Enable(BLE)
HSFS的BLE位一旦置1,只有SMM模式能改写保护相关配置寄存器。非SMM代码(Ring 0的OS内核、DXE驱动)想碰这些寄存器——SPI Controller直接忽略写操作,连异常都不抛。
// IntelSiliconPkg/.../Spi/Smm/PchSpiSmm.c
// SMM模式下锁定Flash配置
VOID EFIAPI PchSpiSetBiosWriteProtect (VOID)
{
// 1. 先设置Protected Range
PchSpiSetProtectedRange (PROTECTED_RANGE_BASE, PROTECTED_RANGE_LIMIT);
// 2. 锁定PR配置
HsfstsValue = PchSpiRead32 (PCH_SPI_HSFS);
HsfstsValue |= B_PCH_SPI_HSFS_PR0FLOCKDN;
PchSpiWrite32 (PCH_SPI_HSFS, HsfstsValue);
// 3. 置BLE位——只有SMM能改配置
HsfstsValue |= B_PCH_SPI_HSFS_BLE;
PchSpiWrite32 (PCH_SPI_HSFS, HsfstsValue);
// 4. 最后锁定整个Flash配置(不可逆)
HsfstsValue |= B_PCH_SPI_HSFS_FLOCKDN;
PchSpiWrite32 (PCH_SPI_HSFS, HsfstsValue);
}
注意顺序:先锁BLE(限制谁可以改),再锁FLOCKDN(彻底不允许改)。反过来就完了——FLOCKDN锁了所有HSFS配置位,BLE再也改不了。
第三层:Boot Guard / BIOS Guard
Intel Boot Guard是PCH硬件级启动验证。不是"防止写Flash",而是**“验证你写的Flash对不对”**。
- 生产线上,OEM把公钥Hash烧入PCH一次性可编程熔丝(OTP Fuse)
- 上电时,PCH硬件自动读取Flash Descriptor,定位Initial Boot Block(IBB)
- 用熔丝里的公钥Hash验证IBB的RSA签名
- 签名不对?PCH拒绝启动。屏幕不亮,风扇转,但什么都看不到
踩坑:BLE位被前一个驱动锁死了
调试UEFI Capsule Update流程。Flash更新在DXE阶段执行,刷入新固件前要临时解除写保护。每次运行到Flash Erase命令时,HSFS的FCERR位跳起来。
排查:前一个DXE驱动(OEM的BIOS Guard初始化模块)在SMM模式下调用了Lock函数,把PR0锁了、设了BLE,但没在退出前恢复。Capsule Update在非SMM模式下运行,无权改动PR寄存器。
解决:在Capsule Update流程里加SMI Handler。需要解锁时触发SMI,让SMM代码临时清除PR0;刷完再触发SMI恢复保护。所有Flash写保护的管理必须收敛到SMM。
八、实战踩坑总结
| 坑 | 现象 | 根因 | 经验 |
|---|---|---|---|
| BIOS bin直接烧Flash | 主板亮但ME挂黄叹号、网络不通 | ME Region和GbE Region是空的 | 必须用FIT打包完整Image |
| FREG边界没对齐 | S3恢复时PEI加载Recovery FV挂死 | Limit少算一个扇区,后面全返回0xFF | Base/Limit必须对齐到扇区边界 |
| 国产Flash SFDP说谎 | Quad Read第3字节开始全是垃圾 | IO2引脚没切到数据模式 | 先Dual Read验证,再切Quad |
| NVRAM写不进去 | BIOS Setup改了保存后重启又回来 | FPRR覆盖了NVRAM variable区域 | Flash保护不会报错,只静默丢弃写命令 |
| Capsule Update失败 | FCERR位置位 | 前一个DXE驱动在SMM里锁了PR0和BLE | Flash写保护管理收敛到SMM |
| Top Swap区太小 | 进了Recovery Shell但没Flash驱动 | 只放了64KB Shell,没塞Flash驱动 | 至少128KB,带完整Flash驱动 |
最后一条经验:Write Protect不报错是设计如此,不是bug。 SPI协议里写保护本身就不该返回错误——它只是silently discard write command。正确做法是Write + Verify:写完了回读确认。
源码路径索引
| 内容 | 关键文件 |
|---|---|
| SPI协议定义 | MdePkg/Include/Protocol/Spi.h |
| SPI Controller寄存器定义 | Silicon/Intel/CoffeelakeSiliconPkg/Pch/Include/Register/PchRegsSpi.h |
| Flash读写库 | IntelSiliconPkg/Feature/PcieSpiLib/SpiFlashCommon.c |
| SMM写保护配置 | IntelSiliconPkg/.../Spi/Smm/PchSpiSmm.c |
| FV Header定义 | MdePkg/Include/Pi/PiFirmwareVolume.h |
| Flash Descriptor结构 | edk2-platforms/Silicon/Intel/*/Pch/Include/FirmwareDescriptor.h |
| 规范 | Intel PCH EDS SPI Controller章节 / JESD216 SFDP标准 |

935

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



