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在硬件初始化阶段的完整协同流程。

ATF与UEFI硬件初始化协同流程 阶段1:ATF EL3初始化 CPU:异常向量、GIC、系统寄存器 内存:安全SRAM/保守DDR配置 外设:安全UART、TrustZone控制器 SMC跳转 阶段2:UEFI DXE初始化 CPU:页表、定时器、多核启动 内存:DDR训练、完整内存映射 外设:UART、SPI、PCIe、DMA 协同区域:共享外设初始化 GIC:ATF配置路由,UEFI注册处理函数 电源管理:ATF实现PSCI,UEFI调用PSCI 安全内存:ATF报告范围,UEFI标记保留 启动完成,进入OS 说明:ATF完成安全侧初始化后,通过SMC跳转到UEFI;UEFI在初始化共享外设时,通过SMC请求ATF协同操作

4.7 实战建议

最后,分享几个我在实际项目中总结的经验:

  1. 提前定义接口:在项目启动阶段,就定义好ATF和UEFI之间的SMC接口。包括哪些功能通过SMC调用,参数格式是什么,返回值怎么处理。
  2. 使用共享数据结构:在安全内存中定义一个共享数据结构,存放ATF初始化完成后的硬件状态。UEFI启动时直接读取,避免重复探测。
  3. 日志分级:ATF和UEFI使用不同的UART输出日志。ATF用安全UART,UEFI用非安全UART。这样调试时一眼就能看出问题出在哪个阶段。
  4. 模拟验证:在QEMU或FPGA上先跑一遍协同初始化流程。我习惯在模拟环境里故意制造资源冲突,看看系统会不会崩溃。提前暴露问题,总比在真机上死机强。

小技巧:在ATF的启动日志里,打印出所有初始化的硬件资源列表。UEFI启动时,先读取这个列表,和自己要初始化的资源做对比。如果有重叠,直接报错。这个检查逻辑帮我抓到了好几次资源冲突的bug。

嗯,硬件初始化协同这块,说白了就是划清界限、各司其职。ATF管好安全世界,UEFI管好非安全世界,共享资源通过SMC协商。只要把这个模型想清楚,代码实现反而简单了。