3、ATF源码结构:ATF仓库结构、BL1/BL2/BL31/BL32/BL33概念、平台移植框架、构建系统
好,咱们进入正题。这一章我打算把ATF的源码结构彻底讲透。很多初学者一打开ATF仓库就懵了——文件夹一大堆,不知道从哪看起。我当年第一次接触时也差不多,对着docs/和plat/发了半天呆。
说白了,ATF的源码结构并不复杂。它遵循一套清晰的启动流程,每个阶段对应一个BL(Boot Loader)组件。你只要搞懂这几个BL是干嘛的,整个仓库的脉络就清楚了。
3.1 ATF仓库结构概览
先看看ATF仓库的顶层目录。我习惯用tree -L 1扫一眼,大概长这样:
.
├── bl1/ # 第一级Boot Loader
├── bl2/ # 第二级Boot Loader
├── bl31/ # EL3 Runtime Firmware
├── bl32/ # TEE相关(可选)
├── bl33/ # 非安全世界的Boot Loader(如U-Boot)
├── common/ # 通用代码
├── plat/ # 平台移植代码
├── drivers/ # 驱动
├── include/ # 头文件
├── lib/ # 库函数
├── make_helpers/ # 构建辅助脚本
├── docs/ # 文档
└── tools/ # 工具
嗯,这里要注意:bl32/目录并不是每个平台都有。它只在需要TEE(Trusted Execution Environment)支持时才存在。我在项目中遇到过,有些客户只做安全启动,不需要TEE,那bl32/就直接删掉了。
3.2 BL1/BL2/BL31/BL32/BL33概念
这几个BL是ATF启动流程的核心。我画了一张图,帮你理清它们的关系:
这张图我画了好几次才满意。它清晰地展示了启动顺序:BL1 → BL2 → BL31 → (BL32) → BL33。每个阶段都有明确的职责和运行特权级。
3.2.1 BL1 - 第一级Boot Loader
BL1是上电后第一个执行的代码。它通常存放在ROM里,不可修改。它的任务很简单:
- 初始化最基本的硬件(比如时钟、内存控制器)
- 验证BL2的签名(安全启动)
- 加载BL2到SRAM并跳转
我在项目中遇到过一个问题:BL1的ROM空间非常有限,通常只有几十KB。有一次我试图在BL1里加一个完整的UART驱动,结果编译后超了ROM大小。后来只能精简到最简轮询模式,才勉强塞进去。
3.2.2 BL2 - 第二级Boot Loader
BL2运行在SRAM里,比BL1灵活得多。它的主要工作是:
- 加载并验证后续的BL31、BL32、BL33镜像
- 配置内存控制器(DDR初始化)
- 传递平台参数给后续阶段
你想想看,BL2其实就像一个「搬运工」。它把各个BL镜像从存储设备(比如eMMC、NAND Flash)搬到DDR里,然后跳转过去。我建议你在调试时,可以在BL2里加一些打印,确认每个镜像的加载地址是否正确。
3.2.3 BL31 - EL3 Runtime Firmware
BL31是ATF的核心。它运行在EL3,提供运行时服务:
- 处理SMC(Secure Monitor Call)异常
- 管理安全世界和非安全世界的切换
- 提供PSCI(电源管理接口)
说白了,BL31就是整个系统的「看门人」。所有从非安全世界到安全世界的调用,都要经过BL31。我记得有一次调试一个TEE相关的bug,发现SMC调用总是返回错误。最后定位到是BL31里没有注册对应的服务ID。嗯,这种问题排查起来很费时间。
3.2.4 BL32 - TEE(可选)
BL32是TEE OS的镜像,比如OP-TEE、Trusty等。它运行在S-EL1,提供安全服务。不是所有平台都需要BL32——如果你的系统不需要TEE,可以跳过这个阶段。
我个人习惯在项目初期先不集成BL32,等BL31和BL33调通后再加。这样能减少调试的复杂度。
3.2.5 BL33 - 非安全世界的Boot Loader
BL33就是非安全世界的启动引导程序,通常是U-Boot或UEFI。它运行在EL2(虚拟化)或EL1(非虚拟化)。ATF把控制权交给BL33后,就进入非安全世界了。
3.3 平台移植框架
ATF的移植性做得很好。你只需要关注plat/目录下的代码。每个平台都有一个独立的文件夹,比如:
plat/
├── arm/ # ARM官方开发板
│ ├── juno/
│ ├── fvp/
│ └── ...
├── hisilicon/ # 海思平台
├── mediatek/ # 联发科平台
├── nvidia/ # 英伟达平台
├── qemu/ # QEMU模拟器
└── ...
移植一个新平台,你需要实现以下接口:
| 接口 | 说明 | 所在文件 |
|---|---|---|
plat_get_my_entrypoint() |
获取当前CPU的入口地址 | plat/common/plat_psci_common.c |
plat_my_core_pos() |
获取当前CPU的ID | plat/common/plat_psci_common.c |
plat_get_syscnt_freq() |
获取系统计数器频率 | plat/common/plat_psci_common.c |
plat_get_console() |
获取控制台驱动 | plat/common/plat_console.c |
platform_setup() |
平台初始化 | plat/common/plat_setup.c |
我曾经移植ATF到一个国产芯片上,最头疼的就是时钟配置。芯片手册写得不清楚,我反复试了好几种配置才让系统计数器跑起来。后来我总结了一个经验:先拿QEMU验证逻辑,再上真硬件。
3.4 构建系统
ATF使用Makefile构建。它的构建系统设计得很巧妙,支持多平台、多配置。我常用的构建命令是:
make PLAT=qemu DEBUG=1 bl31
这条命令会编译QEMU平台的BL31镜像,并开启调试模式。常用的构建变量有:
PLAT:目标平台,比如qemu、juno、fvpDEBUG:是否开启调试(0或1)LOG_LEVEL:日志级别,从0(无日志)到50(最详细)BL32:指定BL32镜像路径(如果使用TEE)BL33:指定BL33镜像路径(如U-Boot)
LOG_LEVEL=40,能看到详细的启动流程。但正式发布时记得改回LOG_LEVEL=20,否则日志太多会影响性能。
构建系统还支持make_helpers/目录下的辅助脚本。比如checkpatch.pl可以检查代码风格是否符合ATF规范。我每次提交代码前都会跑一遍,省得review时被怼。
嗯,这一章的内容就到这里。ATF的源码结构其实不复杂,关键是要理解每个BL的职责和它们之间的协作关系。下一章我们会深入BL1的启动细节,看看芯片上电后到底发生了什么。
核心要点回顾:
- ATF仓库按BL组件划分目录,
bl1/到bl33/对应启动阶段 - 启动顺序:BL1 → BL2 → BL31 → (BL32) → BL33
- 平台移植主要修改
plat/目录下的代码 - 构建系统通过
make PLAT=<platform>指定目标