4、硬件初始化协同:UEFI与ATF如何分工完成CPU、内存、外设的初始化,避免资源冲突
这一章,我们来聊聊硬件初始化这块硬骨头。
很多做固件的朋友都问过我一个问题:UEFI和ATF都在初始化硬件,它们到底怎么分工?会不会打架?
说实话,我刚接触这个领域时也踩过坑。有一次,ATF把GIC配成了group0,UEFI又去改成了group1,结果中断全乱套了。从那以后,我养成了一个习惯——先画清楚谁管什么,再动手写代码。
4.1 为什么需要协同?
你想想看,系统启动就像接力赛。ATF跑第一棒,UEFI跑第二棒。如果交接不清楚,棒子掉了,整个系统就崩了。
具体来说,硬件初始化有三个关键领域:
- CPU:异常等级、页表、GIC、定时器
- 内存:DDR控制器、MMU、内存映射
- 外设:UART、SPI、PCIe、DMA
每个领域都有明确的归属。ATF负责最底层的安全相关初始化,UEFI负责非安全侧的系统级初始化。两者通过SMC(Secure Monitor Call)通信,就像两个部门通过正式文件对接。
核心原则:谁先碰,谁负责到底。ATF初始化过的硬件,UEFI原则上只读不写。非要写,必须通过SMC请求ATF授权。
4.2 CPU初始化分工
CPU初始化是启动的第一步。ATF跑在EL3,UEFI跑在EL2或EL1。这个层级关系决定了谁该做什么。
4.2.1 ATF负责的部分
ATF在EL3完成以下工作:
- 异常向量表:设置EL3的异常向量,确保SMC、IRQ、FIQ能正确路由
- GIC配置:初始化GIC的distributor和redistributor,配置中断分组和优先级
- 系统寄存器:配置SCR_EL3、HCR_EL2等关键寄存器,控制异常路由和虚拟化
- PSCI:实现电源管理接口,供UEFI和OS调用
我记得有一次调试,发现UEFI启动后中断响应特别慢。查了半天,原来是ATF把GIC的优先级掩码设成了0xFF,所有中断都被屏蔽了。后来我们在ATF的初始化代码里加了个配置项,让UEFI可以动态调整优先级掩码。
4.2.2 UEFI负责的部分
UEFI在EL2/EL1完成以下工作:
- 页表初始化:建立非安全世界的页表,映射DDR、MMIO、UEFI运行时服务区域
- 定时器配置:设置通用定时器的频率和比较值,用于UEFI的计时服务
- CPU特性检测:通过ID寄存器检测CPU支持的特性,如SVE、RAS、AMU等
- 多核启动:通过PSCI接口唤醒从核,分配启动栈和入口地址
个人经验:UEFI在初始化页表时,一定要预留安全世界的内存区域。我见过有人把ATF的代码段映射成非安全可读写,结果ATF运行时数据被意外篡改,系统直接挂掉。
4.3 内存初始化分工
内存初始化是启动中最容易出问题的环节。ATF和UEFI对内存的视角完全不同。
| 阶段 | ATF视角 | UEFI视角 |
|---|---|---|
| DDR控制器 | 只初始化安全世界需要的部分 | 初始化全部DDR控制器,配置时序和频率 |
| MMU | EL3页表,只映射安全内存和MMIO | EL2/EL1页表,映射全部非安全内存 |
| 内存映射 | 固定映射,通常很小(几MB) | 动态映射,支持UEFI内存描述符 |
| 安全内存 | 独占,UEFI不可访问 | 不映射,通过SMC请求访问 |
这里有个关键点:DDR控制器的初始化到底谁来做?
我的建议是:UEFI做。因为DDR控制器的初始化需要复杂的时序训练和校准,这部分逻辑通常放在UEFI的硬件初始化阶段(DXE)。ATF只需要在启动时使用一段固定的、已知良好的DDR配置即可。
但要注意,ATF在EL3阶段可能也需要访问DDR(比如加载BL33)。这时候,ATF要么使用内部SRAM,要么使用一个非常保守的DDR配置。等UEFI完成DDR训练后,再通过SMC通知ATF更新DDR配置。
避坑指南:我曾经在一个项目上,ATF和UEFI同时初始化DDR控制器,结果导致DDR总线冲突,系统随机死机。后来我们改成UEFI独占DDR初始化,ATF只读不写,问题才解决。
4.4 外设初始化分工
外设初始化是资源冲突的重灾区。为什么?因为很多外设既可以被安全世界访问,也可以被非安全世界访问。
4.4.1 安全外设
以下外设通常由ATF独占:
- TrustZone控制器:配置外设的安全属性
- 安全UART:用于ATF的调试输出
- 安全定时器:用于ATF的时间管理
- 密钥存储:OTP、efuse等
4.4.2 非安全外设
以下外设由UEFI初始化:
- 调试UART:UEFI的串口输出
- SPI控制器:用于访问固件存储(NOR Flash)
- PCIe控制器:枚举总线,配置设备
- DMA控制器:用于大块数据传输
4.4.3 共享外设
有些外设需要ATF和UEFI协同管理:
- GIC:ATF配置中断路由,UEFI配置中断处理函数
- 通用定时器:ATF设置物理计数器,UEFI设置虚拟计数器
- 电源管理:ATF实现PSCI,UEFI调用PSCI
对于共享外设,我建议采用“谁初始化,谁管理”的原则。比如GIC,ATF初始化后,UEFI只能通过SMC修改中断配置,不能直接写GIC寄存器。
4.5 资源冲突的典型场景与解决方案
说了这么多理论,我们来看几个实际案例。
场景一:MMIO地址冲突
ATF和UEFI都映射了同一个MMIO区域,但映射属性不同(比如ATF映射为安全,UEFI映射为非安全)。结果就是UEFI访问时触发异常。
解决方案:在固件设计阶段,就定义好MMIO的归属。ATF独占的MMIO,UEFI的页表里直接留空。UEFI需要访问时,通过SMC委托ATF操作。
场景二:中断配置冲突
ATF把某个中断配置为FIQ,UEFI又把它改成了IRQ。结果中断路由混乱,谁都没收到。
解决方案:ATF在初始化时,把所有中断都配置为IRQ,并设置好group。UEFI只能修改非安全中断的配置,安全中断的配置通过SMC请求ATF修改。
场景三:DMA缓冲区冲突
UEFI分配了一个DMA缓冲区,但ATF的安全世界也在使用同一段物理内存。DMA操作导致安全数据泄露。
解决方案:UEFI在分配DMA缓冲区时,必须避开安全世界使用的内存区域。ATF通过SMC向UEFI报告安全内存的地址范围,UEFI在内存描述符中标记为“不可用”。
核心思路:避免资源冲突的关键,是建立清晰的“所有权”模型。每个硬件资源在启动阶段都有且只有一个所有者。所有者负责初始化、配置和释放。其他模块通过标准接口(SMC、UEFI协议)访问,而不是直接操作硬件。
4.6 协同初始化流程图
下面这张图展示了ATF和UEFI在硬件初始化阶段的完整协同流程。
4.7 实战建议
最后,分享几个我在实际项目中总结的经验:
- 提前定义接口:在项目启动阶段,就定义好ATF和UEFI之间的SMC接口。包括哪些功能通过SMC调用,参数格式是什么,返回值怎么处理。
- 使用共享数据结构:在安全内存中定义一个共享数据结构,存放ATF初始化完成后的硬件状态。UEFI启动时直接读取,避免重复探测。
- 日志分级:ATF和UEFI使用不同的UART输出日志。ATF用安全UART,UEFI用非安全UART。这样调试时一眼就能看出问题出在哪个阶段。
- 模拟验证:在QEMU或FPGA上先跑一遍协同初始化流程。我习惯在模拟环境里故意制造资源冲突,看看系统会不会崩溃。提前暴露问题,总比在真机上死机强。
小技巧:在ATF的启动日志里,打印出所有初始化的硬件资源列表。UEFI启动时,先读取这个列表,和自己要初始化的资源做对比。如果有重叠,直接报错。这个检查逻辑帮我抓到了好几次资源冲突的bug。
嗯,硬件初始化协同这块,说白了就是划清界限、各司其职。ATF管好安全世界,UEFI管好非安全世界,共享资源通过SMC协商。只要把这个模型想清楚,代码实现反而简单了。