ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入解析TF-A BL2:Arm系统启动链的承上启下关键阶段

2026/8/5 9:39:26 拓冰建站 浏览量
深入解析TF-A BL2:Arm系统启动链的承上启下关键阶段 1. 从BL1到BL2系统启动的“第二棒”接力在嵌入式系统特别是基于Arm架构的复杂SoC片上系统的启动世界里ATFArm Trusted Firmware现在更常被称为TF-A扮演着至关重要的角色。它是一套开源的、由Arm主导开发的固件负责从芯片上电到将控制权安全地交给操作系统内核之间的所有关键任务。如果你接触过嵌入式开发尤其是涉及安全启动、多核启动或者TrustZone技术的项目那么TF-A几乎是绕不开的一环。整个TF-A的启动流程被精心设计成多个阶段每个阶段称为一个“Boot Loader”阶段简称BL。BL1通常是固化在芯片ROM中的代码负责最基础的硬件初始化和加载下一阶段代码。而我们今天要深入探讨的BL2正是这个接力赛中的“第二棒”。如果说BL1是那个从沉睡中唤醒芯片、点亮第一盏灯的“唤醒者”那么BL2就是接过火炬开始真正搭建系统运行环境、为后续更复杂的软件如BL31、BL32、BL33通常是U-Boot或Hypervisor铺平道路的“奠基者”。理解BL2不仅仅是知道它“做了什么”更重要的是理解它“为什么这么做”以及“如何安全、高效地做到”。这涉及到内存布局的规划、镜像的加载与验证、安全状态的配置等一系列底层且关键的细节。对于开发者而言无论是进行系统定制、调试启动失败问题还是实现自己的安全启动方案对BL2的透彻理解都是不可或缺的基本功。接下来我们就抛开那些笼统的概念深入到BL2的具体职责、实现机制和那些容易踩坑的细节中去。2. BL2的核心使命承上启下的关键枢纽BL2在TF-A启动链中的位置非常明确它由BL1加载并验证执行完毕后它负责加载并验证后续所有阶段的固件BL31, BL32, BL33等最终将控制权移交。它的核心使命可以概括为三个关键词初始化、加载、过渡。2.1 初始化搭建初级运行环境BL1通常运行在一个非常受限的环境下如芯片内部的SRAM它只完成了最最基础的初始化例如使能必要的时钟、初始化用于加载BL2的存储控制器如eMMC、QSPI Flash的控制器。BL2接过控制权时系统的“舞台”还非常简陋。BL2的首要任务就是扩展这个舞台。这包括更完整的内存控制器初始化BL1可能只初始化了用于加载自身和BL2的那一小块内存。BL2需要初始化所有的DDR内存控制器配置时序参数进行内存训练Memory Training对于DDR4/LPDDR4等尤其重要使得整个系统可用的DRAM区域能够被正确访问。这是后续所有大型镜像如U-Boot、Linux内核能够被加载运行的前提。控制台初始化为了输出调试信息BL2需要初始化一个串口控制台UART。这不仅仅是配置波特率还包括引脚复用Pin Mux的设置。这里一个常见的坑是芯片参考设计中的UART引脚可能和你的具体板子设计不同如果BL2的串口初始化失败你将陷入“黑屏”状态只能通过更底层的调试器如JTAG来查看问题。必要的时钟与电源管理BL2可能会进行一些更细致的时钟树配置或者为后续阶段准备特定的电源域。安全配置的继承与深化BL1已经开启了Arm TrustZone将世界划分为安全世界Secure World和非安全世界Normal World。BL2运行在安全世界它会进一步配置安全相关的硬件例如设置安全内存区域如使用TZC-400 TrustZone Controller划分DRAM的安全/非安全区域为BL31安全监控器的运行做好准备。2.2 加载与验证启动链的守护者这是BL2最核心的职责之一。TF-A采用了一种叫做“可信启动”Trusted Boot的链式验证模型。简单说每一阶段只信任加载它的上一阶段并且负责验证它要加载的下一阶段。BL1验证BL2BL1从可靠的存储介质如ROM中约定的Flash地址读取BL2镜像使用内置的公钥或哈希值对其进行身份验证可能是RSA签名校验也可能是简单的SHA256哈希校验。验证通过后才跳转到BL2执行。BL2验证后续所有阶段BL2需要从存储设备如eMMC、Nor Flash或网络如TFTP上找到并加载BL31安全监控器、BL32可选的安全服务如OP-TEE OS、BL33非安全世界软件通常是U-Boot等镜像。对于每一个镜像BL2都必须使用它持有的公钥或哈希值去验证其签名确保镜像未被篡改、来源可信。这个过程依赖于一个重要的数据结构FIPFirmware Image Package。在实际部署中我们很少将BL31、BL32、BL33等镜像单独烧录。而是用一个叫做fiptool的工具将这些镜像、它们的证书链和必要的元数据打包成一个单一的fip.bin文件。BL2的代码里“知道”如何解析这个FIP格式从中提取出各个组件并按照镜像头部的描述信息如加载地址、入口点、验证方式进行加载和验证。注意BL2自身通常也是FIP的一部分或者与FIP一起打包。但BL1加载的BL2镜像可能是一个独立的“BL2镜像”也可能是一个包含了BL2引导头用于BL1直接加载的“BL2镜像”。具体格式取决于平台配置PLAT。2.3 过渡世界切换的准备工作BL2运行在安全世界的EL3异常等级3或EL1取决于配置。它的最终任务不是一直运行下去而是为切换到下一个阶段做好一切准备然后优雅地退出。准备参数BL2会准备一个叫做“入口点信息”的结构体里面包含了它要跳转到的下一个阶段的入口地址、是安全世界还是非安全世界、CPU状态如SPSR寄存器值等信息。对于BL31还会传递一个指向“BL32和BL33镜像描述符”等信息的指针。清理现场BL2可能会关闭自己使用过的外设或将缓存中的数据刷回内存Cache Clean and Invalidate确保下一个阶段看到的是一个一致的内存视图。执行跳转最后BL2通过一条特殊的指令如ERET进行异常返回将CPU的控制权、以及准备好的参数一并交给下一个阶段通常是BL31。从此BL2的生命周期结束。3. BL2的代码执行流与平台移植关键点要真正驾驭BL2必须理解它的代码执行流并知道在为你自己的硬件平台移植TF-A时需要在哪些地方动手术刀。3.1 标准执行流程剖析TF-A的代码结构清晰BL2的通用主流程位于bl2/bl2_main.c的bl2_main()函数中。让我们拆解这个流程bl2_early_platform_setup()这是平台相关的早期初始化。在这里SoC厂商需要实现自己芯片的内存控制器初始化。这是整个BL2乃至后续所有阶段能正常工作的基石。如果这里DDR初始化失败系统会在后续访问内存时直接挂死。bl2_platform_setup()稍晚一些的平台初始化可以初始化控制台串口等。这样之后的流程就可以打印日志了。bl2_arch_setup()架构相关的设置例如配置MMU内存管理单元。使能MMU后CPU可以使用虚拟地址访问效率更高并且可以设置内存的访问属性如可缓存、不可缓存、只读等。加载并验证FIP镜像这是核心。代码会调用load_auth_image()等函数尝试从PLAT定义的各种存储设备如MTD设备中定位fip.bin。找到后会解析其内部结构。验证并加载各个组件遍历FIP中的镜像对每一个需要加载的镜像通过bl2_plat_handle_pre_image_load()和bl2_plat_handle_post_image_load()平台钩子函数判断进行密码学验证。验证通过后将其内容从存储介质拷贝到指定的内存加载地址。准备并执行跳转所有镜像加载验证完毕后调用bl2_prepare_next_image()来填充下一个阶段BL31的入口点信息结构体。最后调用smc(BL1_SMC_RUN_IMAGE, ...)或直接设置寄存器后调用bl2_run_next_image()将控制权移交。3.2 平台移植你必须实现的接口TF-A通过“平台抽象层”来支持不同的SoC。当你为一块新的芯片移植TF-A时大部分工作集中在plat/your_platform/目录下。对于BL2以下几个接口至关重要plat_get_ns_image_entrypoint()告诉BL2BL33非安全世界镜像如U-Boot的入口地址是什么。这个地址通常是BL33镜像被加载到内存后的地址。bl2_platform_setup()如前所述初始化串口等平台关键外设。一个实用的技巧是尽量早地初始化串口哪怕只是输出一个简单的字符如B这对于判断BL2是否成功执行到某个点非常有帮助。bl2_early_platform_setup()重中之重实现DDR初始化。这里你需要调用芯片厂商提供的底层库可能是一些封闭的二进制库也可能是开源代码按照你板子上使用的具体DDR颗粒的型号配置正确的时序参数。参数配错轻则性能不稳重则无法启动。存储驱动接口BL2需要读取FIP。你需要实现io_storage层对应的驱动。例如如果你的FIP放在eMMC上你需要实现eMMC的驱动如果放在Nor Flash上则需要实现SPI Flash的驱动。TF-A已经提供了很多常见控制器如Cadence MMCSTM32 QSPI的驱动框架你通常需要适配的是底层引脚和时钟配置。踩坑实录DDR初始化参数。这是我早期移植时踩过的最大的坑。我们直接使用了芯片原厂参考板的DDR配置但自己的板子换了不同型号的DDR颗粒。系统在BL2阶段随机性地死机。最后用示波器抓取DDR信号线发现有时序不满足要求。解决方案是1仔细对比新旧DDR颗粒的数据手册重点关注时序参数如tRCD, tRP, tRAS2使用原厂提供的配置工具如果有重新生成初始化代码3在BL2初始化DDR后增加一个简单的内存读写测试循环例如写-读比较全内存范围尽早暴露问题。4. BL2镜像的构建与配置从源码到二进制了解了BL2做什么和怎么做我们来看看如何得到它。TF-A使用Makefile和CMake两种构建系统这里以更常见的Makefile为例。4.1 编译配置选项编译BL2不是简单的一个make命令你需要指定大量的配置。一个典型的编译命令如下make CROSS_COMPILEaarch64-linux-gnu- \ PLATyour_platform \ DEBUG1 \ BL21 \ all fip关键参数解析PLATyour_platform指定你的平台目录名对应plat/your_platform。DEBUG1启用调试符号和优化等级-O0。这在开发阶段至关重要否则代码被高度优化单步调试时会非常痛苦。切记生成最终生产固件时必须去掉DEBUG1以启用优化如-Os并减小体积。BL21显式编译BL2。实际上all目标通常会依赖它但明确指定是个好习惯。all fipall目标编译出各个阶段的ELF文件fip目标则调用fiptool将编译好的BL2、BL31、BL33等打包成最终的fip.bin。其他重要选项SPDopteed如果你要包含BL32OP-TEE需要指定此选项。ARM_BL31_IN_DRAM1将BL31加载到DRAM而非SRAM中运行。当BL31代码较大时需要使用。TRUSTED_BOARD_BOOT1启用可信启动这会引入密码学验证增加BL2的代码体积和启动时间。4.2 BL2镜像的最终形态编译完成后在build/your_platform/debug/或release/目录下你会找到多个文件bl2.bin纯二进制格式的BL2镜像。在某些启动流程中BL1需要直接加载这个文件。bl2.elf包含调试符号的ELF文件用于调试。fip.bin我们最终要烧录到板子Flash中的固件包。这里有一个非常重要的细节bl2.bin和fip.bin里的BL2组件可能不是同一个东西在一些平台设计中BL1直接从Flash固定偏移处加载一个“BL2镜像”这个镜像可能是一个带有自解压头或特殊格式的bl2.bin。而这个bl2.bin执行起来后它的任务之一就是去Flash的另一个固定偏移处或通过分区表查找加载并解析fip.bin然后从fip.bin里取出BL31、BL33等继续执行。在另一些设计中fip.bin本身就包含了BL2BL1加载的“第一级”镜像就是一个能解析FIP格式的Loader它从FIP中解出BL2并执行。你必须根据你的芯片文档和平台移植层代码搞清楚你板子上的BL2到底以何种形式存在以及bl2.bin和fip.bin的烧录地址是什么。烧错地址是导致启动失败的常见原因。5. 调试BL2当启动卡住时怎么办BL2阶段没有操作系统没有成熟的调试工具链。当板子上电后串口没有任何输出或者输出到某个特定字符串如“B”或“BL2: ...”后就停止时问题很可能出在BL2。以下是我常用的排查链路5.1 第一阶段确认BL1已交接首先你需要确认BL1确实成功跳转到了BL2。如果BL1和BL2之间有一个简单的串口输出约定例如BL1打印ABL2打印B那么这很容易。如果没有你需要在BL2的入口函数通常是bl2_entrypoint最开头强制插入一条汇编指令让某个GPIO引脚翻转电平然后用示波器或逻辑分析仪探测这个引脚。如果看到翻转证明BL2已开始执行。使用JTAG调试器在BL2的入口点设置断点。如果断点能命中同样证明交接成功。5.2 第二阶段串口“失声”排查如果BL2开始执行了但没有串口输出问题集中在bl2_platform_setup()的串口初始化部分。检查引脚复用确认代码中UART TX/RX引脚配置与板级原理图一致。一个引脚可能被复用于多种功能UART、I2C、GPIO等配置错模式就会导致无输出。检查时钟源UART模块的时钟是否使能波特率计算的基础时钟频率是否正确有时需要追溯好几层时钟树。简化测试尝试在初始化UART前先将其配置为最简单的GPIO输出模式驱动一个LED闪烁以证明CPU确实执行到了这里。5.3 第三阶段内存初始化失败这是最棘手的问题之一症状可能是输出部分信息后死机或者访问特定内存地址时产生数据中止异常。启用MMU前死机问题很可能在DDR初始化。检查bl2_early_platform_setup()中的DDR配置代码。使用JTAG在初始化前后读取DDR控制器的状态寄存器看是否有错误标志。启用MMU后死机问题可能在MMU的页表配置。TF-A为BL2配置的页表通常比较简单比如1:1映射。检查bl2_plat_arch_setup()中页表描述符的设置确保你想要访问的物理内存区域如代码段、数据段、设备寄存器段都被正确映射并且具有正确的属性如设备内存应标记为不可缓存。增加内存测试在BL2的DDR初始化完成后MMU启用前插入一个简单的内存测试函数。写一个已知的模式如0xAA55AA55到DRAM的起始和结束地址然后读回来比较。如果测试失败立即通过某种方式报告比如让一个LED闪烁特定的错误码而不是继续执行。5.4 第四阶段镜像加载与验证失败如果BL2能正常打印日志但在加载FIP或验证镜像时失败日志通常会给出线索如“Failed to load image”或“Authentication failed”。存储设备访问失败检查BL2的存储驱动如eMMC/SD卡驱动是否正常。确认驱动识别到了你的存储设备可以通过读取CID/CSD寄存器验证。确认FIP镜像被烧录到了驱动代码期望的偏移地址。FIP解析失败确认你烧录的fip.bin是使用当前编译的fiptool打包的且打包时包含了所有必要的镜像。可以用fiptool info fip.bin命令查看其内容。验证失败如果启用了TRUSTED_BOARD_BOOT检查用于验证的证书链和公钥是否与镜像签名时使用的私钥匹配。这是一个经典的“密钥对不匹配”问题。确保在项目中使用一致的密钥对。6. 性能与尺寸优化让BL2更快更小BL2在启动时间敏感的场景中至关重要。优化BL2可以从以下几个方面入手编译器优化发布版本务必使用DEBUG0。GCC的-Os优化尺寸通常比-O2或-O3更适合引导程序因为能减少二进制体积从而可能减少从Flash加载到RAM的时间。减少初始化范围BL2不需要初始化所有外设。只初始化启动所必需的DDR、用于加载的存储设备、串口用于调试。其他外设如USB、以太网等留给后续阶段如U-Boot去初始化。镜像压缩考虑对BL2自身或FIP进行压缩。BL1先加载一个小的解压程序再由解压程序解压出真正的BL2。这可以节省Flash空间但会增加一点解压时间的开销。TF-A支持LZMA等压缩算法。缓存策略在MMU启用后合理配置内存区域的缓存属性。将代码和只读数据区域设置为可缓存Cacheable能显著提升执行速度。将设备寄存器区域设置为不可缓存Device-nGnRnE保证访问的原子性和顺序性。并行加载如果硬件支持如有多总线或DMA可以尝试让BL2在验证当前镜像的同时通过DMA预加载下一个镜像实现流水线操作减少总的加载等待时间。但这需要比较精细的代码设计。对BL2的优化是一个权衡的艺术需要在启动速度、代码大小、功能完整性和可维护性之间找到平衡点。最好的方法是使用高精度计时器如果芯片有或GPIO翻转配合示波器实际测量每个初始化步骤的耗时找到瓶颈所在再进行针对性优化。理解BL2就像掌握了一把打开Arm系统底层启动世界的钥匙。它不再是一个黑盒而是由一系列明确职责、可配置、可调试的代码模块组成。从内存初始化的硬件细节到镜像验证的密码学流程再到平台移植的具体实践每一步都要求开发者兼具硬件思维和软件工程能力。当你成功驯服了BL2看着它稳稳地将系统交接给后续阶段时那种对系统全局的掌控感正是底层开发的魅力所在。