SPI Flash读写原理详解:从硬件时序到BIOS代码层面

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编号名称内容
0Flash DescriptorIFD自身,元数据
1BIOS / UEFI Firmware你的固件代码
2Intel MEManagement Engine固件
3GbE网卡PXE ROM和配置
4Platform DataEC固件或其他平台数据

一个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=0x0070Limit=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

偏移寄存器作用
0x00BFPRBIOS Flash Primary Region基地址
0x04HSFS硬件定序状态和命令(查状态看它)
0x06HSFC硬件定序控制(发命令看它)
0x08FADDR软件定序模式下的Flash目标地址
0x0CFDATA0Flash数据寄存器(低32位)
0x50FRAPFlash Region访问权限
0x54~0x64FREG0~FREG4各Region的Base/Limit
0x74~0x84PR0~PR4Protected Range寄存器组
0x90SSFS软件定序状态
0x91SSFC软件定序控制

HSFS寄存器调试时最常用:

名称含义
0FDONEFlash操作完成(硬件定序)
1FCERRFlash周期错误
2AELAccess Error Log——有人访问了不该访问的Region
5SCIPSPI Cycle In Progress——Flash正忙
15FLOCKDNFlash配置锁定

调试第一件事永远是看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对不对”**。

  1. 生产线上,OEM把公钥Hash烧入PCH一次性可编程熔丝(OTP Fuse)
  2. 上电时,PCH硬件自动读取Flash Descriptor,定位Initial Boot Block(IBB)
  3. 用熔丝里的公钥Hash验证IBB的RSA签名
  4. 签名不对?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少算一个扇区,后面全返回0xFFBase/Limit必须对齐到扇区边界
国产Flash SFDP说谎Quad Read第3字节开始全是垃圾IO2引脚没切到数据模式先Dual Read验证,再切Quad
NVRAM写不进去BIOS Setup改了保存后重启又回来FPRR覆盖了NVRAM variable区域Flash保护不会报错,只静默丢弃写命令
Capsule Update失败FCERR位置位前一个DXE驱动在SMM里锁了PR0和BLEFlash写保护管理收敛到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标准

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

媳妇爱吃酸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值