我将此回答限制在Intel架构上,因为我对它们比较熟悉。
您(以及我)正在寻找的文档称为《BIOS编写指南》,不幸的是,该文档是机密的,目前尚未泄漏(据我所知)。
为了在开源社区推广其产品,英特尔发布了
固件支持包。这可以视为固件编写者的库,其中包含用于初始化内存控制器、PCH(Peripheral Controller Hub,非正式称为“芯片组”)和CPU的(二进制)代码。
任何无法承担与英特尔签署保密协议的开发人员,包括开源开发人员,都可以使用FSP编写自己的固件。
可以反向使用FSP(我要做的许多TODO之一),但将其用作参考更快捷。
在CPU从复位向量
2执行之前,会发生很多事情,但重要的是要记住芯片组(即PCH)已经允许CPU访问闪存ROM。
实际上,这就是第一条指令的执行方式,因为CPU只能从内存地址空间中获取指令。
因此,只要固件将执行流保持在映射到闪存ROM的内存区域内(该区域由闪存ROM本身中存在的Flash描述确定,PCH在其重置期间读取它并配置内存请求的路由),其代码就可以执行。
由于内存尚未初始化,且闪存ROM是只读的(相对于内存写入周期),因此无法使用这些功能:
- 调用。因为它们需要一个可写堆栈。
- 内存中的变量。因为它们会改变。
这两个都是很烦人的问题,在汇编语言中,您可以使用跳转和寄存器来解决这些问题,但在C中则不行。
因此,固件通常要做的第一件事是设置“临时RAM”。
这是FSP的
TempRamInit()
例程(顺便说一下,必须使用跳转调用它),实际上设置了Cache-as-RAM(CAR)。
Cache-as-RAM
这个想法是使用缓存作为临时RAM。
基本点是缓存行不会过期,只有当没有空间来存储来自内存的新请求线时才会被替换掉。
因此,只要小心避免访问超过缓存容量的变量,CPU就只会从缓存中读取和写入(当然,这需要Write-back缓存模式)。
然而,这需要仔细地定位变量,而且非常容易出错。更好的方法是启用缓存(通过清除CR0寄存器中的CD(Cache Disable)位),然后从一个与L1^3一样大的内存区域进行虚拟读取(甚至写入)。然后再次禁用缓存。此模式实际上被称为“no-fill mode”,其中不会将新行带入缓存(因此不会丢失现有行),但读取和写入仍然可以命中缓存。这允许几KB的“RAM”。存在适用于CAR环境的C编译器。
现在固件可以初始化RAM,为此必须完成三件事:
告诉内存控制器DIMM定时(所有的CAS、RAS等)。
告诉内存控制器DIMM大小和排名。
设置路由。
内存控制器通过PCI配置空间和MMIO进行配置,您可以在处理器数据手册第2卷中找到具体信息(假设MC在CPU芯片上)。例如,
第8代和第9代Core数据手册第2卷中包含了内存控制器寄存器的描述。以下是固件可以设置tRAS参数的摘录:
![Example of the MC registers](https://istack.dev59.com/tAFsZ.webp)
类比地,您会找到DIMM大小和类型、通道大小等寄存器:
![Yet another](https://istack.dev59.com/XTBRS.webp)
这些寄存器涵盖了点1和点2(根据定义,还有点3的一部分),但是固件如何知道要使用哪些值呢?毕竟,DIMM是可替换的。正如已经提到的,解决方案是
串行预先检测(SPD),这是集成在DIMM上的一个小EEPROM,描述内存定时、拓扑和大小。该EEPROM通过兼容I2C总线进行访问。在Intel架构中,实际使用的总线是SMBus(系统管理总线),它与I2C兼容并被专门创建。SMBus主机位于PCH中,并在相关系列的数据手册卷2中记录。例如
PCH系列200数据手册第2卷。SMBus主机在使用之前必须进行配置,但非常简单。一旦配置完成,就可以用它来读取SPD数据。这与访问任何其他I2C设备的方式完全相同。 SPD EEPROM(当然可以多个,每个DIMM都有一个)保留了0x50到0x57(在系列200 PCH上)的地址。可以对SPD进行写入,并且SMBus主机中存在禁用此类行为的位。
![SPD write disable](https://istack.dev59.com/7ptfn.webp)
一旦读取了SPD数据,MC就可以配置,然后RAM就可以使用了。
这是FSP的FspMemoryInit()例程。
最后一步是配置路由。
这包括在内存地址空间中设置RAM区域的末尾(有关完整信息,请参阅PCH数据表),以及在NUMA系统中通过QPI/UPI链接通过源地址和目标地址解码器路由内存请求。
所有这些都通过PCH集成设备的PCI配置空间完成。
在NUMA系统中,需要启动其他应用处理器(每个插座一个)来配置它们的内存控制器。
这是通过LAPIC发出的互处理器中断(IPI)完成的,LAPIC是每个CPU中的MMIO组件。
总结
固件执行的大致步骤为:
1. 执行任何基本环境初始化(例如,切换到32位模式)。
2. 初始化Cache-As-RAM。
3. 使用PCI枚举初始化PCH中的SMBus主机。
4. 读取每个DIMM的SPD EEPROM。
5. 使用SPD数据配置每个插座的内存控制器。
6. 配置PCH内存映射。
7. 配置NUMA路由。
1 CPU不需要初始化,实际上,在调用FSP初始化例程时,已经执行了很多代码。他们可能指的是对某些已更或未更记录的功能进行“微调”。
2 这里不讨论它们,但简单来说,嵌入式控制器(适用于笔记本电脑,适用于台式机的硬连线逻辑)将被打开,一旦引导(使用其集成的ROM),其固件将使用GPIO来打开板子上必要的电源门。其中一个门会供电PCH,一旦EC固件断言正确的引脚,就会启动自己的固件(因为它与ME代码的其他部分捆绑在一起,位于相同的闪存ROM的ME区域内,其中还包含BIOS代码,但从技术上讲,它是Bring-Up,BUP,模块)并重置芯片组。一旦芯片组准备就绪,它将断言CPU的电源良好引脚,然后是复位/初始化引脚,这将导致CPU开始执行POST,然后假设TXT能力的CPU,微码将从闪存ROM中获取固件接口表,并从中获取SINIT ACM(系统初始化身份验证控制模块,它将设置所需的安全性以进行“测量启动”)和可选的BIOS ACM(它将执行供应商特定的任务,可能包括引导,跳过传统的重置向量)。最终,BIOS ACM(或如果在FIT中未找到BIOS ACM,则是微码)将跳转到复位向量。这是传统的引导流程。请注意,ACM在使用Cache-as-RAM(参见上文)的特殊环境中执行,遵循任何其他TXT启动的语义(请参阅Intel TXT规格)。
3 根据英特尔,当设置CD时,不进行行替换。我认为这也不会将行移回和移动到更高级别的缓存。
ffff:0000
开始启动。BIOS将隐藏很多这些信息不让你看到。 - fuz