2. 移植前的环境准备:交叉编译工具链选择、目标板硬件规格确认与启动流程分析
做系统移植,说白了就是给一块“裸板”注入灵魂。但灵魂不能乱灌,得先搞清楚三件事:用什么工具编译、目标板长什么样、上电后芯片怎么跑起来。这三件事没捋顺,后面调试会非常痛苦。我个人习惯,在动手写任何一行代码之前,先把环境准备清单列清楚。
2.1 交叉编译工具链:GCC 还是 LLVM?
ARMv8 项目移植,第一步就是选编译器。你想想看,目标板是 ARM 架构,开发机是 x86,我们得用交叉编译工具链在 x86 上生成 ARM 能跑的机器码。
| 对比维度 | GCC (GNU Toolchain) | LLVM/Clang |
|---|---|---|
| 成熟度 | ARM 生态的“老大哥”,TF-A、U-Boot、Linux 内核官方都用它 | 近十年才在嵌入式领域站稳,但 Apple Silicon 和 Android 已大量采用 |
| 代码体积 | 默认优化偏保守,体积略大 | LTO(链接时优化)更激进,有时能省 10%-15% 空间 |
| 调试信息 | DWARF 格式兼容性最好,GDB 配合无坑 | Clang 生成的 DWARF 偶尔有版本不匹配问题 |
| 编译速度 | 中等,单文件编译较慢 | 快,尤其增量编译,能省 30% 时间 |
| 许可证 | GPLv3,商用需注意 | Apache 2.0,更友好 |
我的建议: 新手项目、团队协作、需要稳定调试环境,选 GCC。我早期一个项目用 LLVM 编译 TF-A,结果 BL31 的异常向量表对齐出了问题,查了两天才发现是 Clang 的链接脚本处理方式跟 GCC 有细微差异。从那以后,量产项目我默认用 GCC,LLVM 只用在性能调优阶段做对比测试。
具体到 ARMv8 64位,工具链前缀通常是 aarch64-linux-gnu-。安装方式很简单:
# Ubuntu/Debian
sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
# 或者从 ARM 官网下载预编译的 GNU-A 工具链
wget https://developer.arm.com/-/media/Files/downloads/gnu-a/...
注意: 不要用
aarch64-linux-gnu-gcc 去编译裸机固件(如 TF-A、U-Boot SPL)。裸机环境没有操作系统支持,必须用 aarch64-none-elf- 或 aarch64-elf- 前缀的 bare-metal 工具链。我曾经见过有人用 Linux 工具链编译 BL1,结果链接时找不到 __aeabi_uldivmod 符号,折腾半天才发现是工具链选错了。
2.2 目标板硬件规格确认
拿到一块新板子,别急着上电。先对照原理图和 datasheet,把下面这张表填清楚。我习惯把它打印出来贴在工位上。
| 硬件项 | 确认内容 | 常见坑点 |
|---|---|---|
| SoC 型号 | ARMv8-A 还是 ARMv8-R?Cortex-A53/A72/A76? | A53 是顺序执行,A72 是乱序,cache 和 MMU 配置完全不同 |
| 内存映射 | DDR 起始地址、大小、片选数量 | 很多板子 DDR 地址不是从 0x80000000 开始,比如某些 NXP 平台从 0x40000000 |
| 启动介质 | eMMC、SD卡、NAND Flash、SPI NOR? | 启动顺序由 BootROM 决定,不同介质对应不同的 BL1 加载方式 |
| 调试接口 | UART 串口号、JTAG/SWD 是否引出 | UART 波特率、电平(1.8V vs 3.3V)不匹配会导致完全无输出 |
| 电源域 | 核心电压、IO 电压、DDR 电压 | 上电时序不对,芯片可能锁死或损坏 |
核心原则: 移植工作 80% 的 bug 都来自硬件规格理解错误。不要相信“参考设计”,一定要看你自己板子的原理图。我有个同事,照着官方开发板的内存地址配,结果自己画的板子 DDR 片选接法不同,系统跑起来就随机死机,查了整整一周。
2.3 启动流程分析:BL1 → BL2 → BL31 → BL32 → BL33
ARMv8 的启动流程,是 Trusted Firmware-A (TF-A) 的核心设计。它把启动过程分成了多个阶段,每个阶段都有明确的职责和权限等级。说白了,就是一层层“交棒”,从最底层的安全固件一直交到操作系统。
每个阶段在内存中的位置和权限等级是严格定义的。我整理了一个常用表格:
| 阶段 | 运行位置 | 异常等级 | 典型大小 | 加载者 |
|---|---|---|---|---|
| BL1 | 芯片内部 SRAM (通常 64KB-128KB) | EL3 | ~32KB | BootROM 自动加载 |
| BL2 | SRAM 或 DDR (早期) | S-EL1 | ~64KB | BL1 从 Flash 加载 |
| BL31 | DDR 中保留区域 | EL3 | ~128KB | BL2 加载并验证 |
| BL32 | DDR 中安全区域 | S-EL1 | ~2MB | BL2 加载 |
| BL33 | DDR 起始地址 (通常 0x80000000) | EL2 | ~512KB | BL2 加载 |
避坑指南: 我曾经在移植时,BL2 加载 BL31 后直接跳转,结果 BL31 死机。查了两天,发现是 BL2 阶段 DDR 初始化不完整——BL31 代码里用到了 DDR 的某个 bank,但 BL2 只初始化了前 64MB。记住:BL2 必须确保 BL31/BL32/BL33 所需的全部内存区域都已初始化并可用。
2.4 Trusted Firmware-A (TF-A) 基础
TF-A 是 ARM 官方维护的参考实现,实现了上面说的 BL1-BL33 整个流程。它不是一个“库”,而是一套完整的固件框架。你把它下载下来,配置好平台文件,编译,就能生成 BL1、BL2、BL31 的二进制。
TF-A 的源码结构,我建议重点关注这几个目录:
- bl1/ — BL1 源码,通常不需要修改,除非你要定制 BootROM 行为
- bl2/ — BL2 源码,主要关注镜像加载和验证逻辑
- bl31/ — BL31 源码,核心是
runtime_svc.c和context_mgmt.c - plat/ — 平台相关代码,这是移植工作的主战场。每个平台一个子目录,比如
plat/arm/board/fvp/ - include/ — 头文件,定义了所有数据结构、宏、接口
我的经验: 刚开始接触 TF-A,不要试图通读所有代码。先找到你目标板对应的
plat/ 目录,看 platform.mk 和 plat_setup.c。这两个文件定义了板级配置的核心:内存布局、时钟、UART 地址、中断控制器。把这两个文件读懂,移植工作就完成了一半。
编译 TF-A 的命令也很直接:
# 以 FVP 平台为例
make PLAT=fvp DEBUG=1 ARCH=aarch64 CROSS_COMPILE=aarch64-none-elf-
# 编译产物
# build/fvp/debug/bl1.bin
# build/fvp/debug/bl2.bin
# build/fvp/debug/bl31.bin
这里 DEBUG=1 会打开所有日志输出,对调试非常有用。量产时再改成 DEBUG=0。
核心要点: TF-A 的移植,本质上就是填写一份“平台信息问卷”——你的 SoC 有多少个核心?每个核心的电源域怎么控制?GIC 版本是 v2 还是 v3?UART 基地址是多少?把这些信息填对,TF-A 就能跑起来。剩下的工作,就是调试和优化。