系统移植基础:交叉编译环境搭建、Bootloader流程解析与设备树原理
系统移植,说白了就是让一套软件能在不同的硬件平台上跑起来。我做了这么多年移植,最深的体会就是——环境搭不好,后面全是坑。今天咱们就把交叉编译、Bootloader、设备树这三块硬骨头啃下来。
交叉编译环境搭建
你想想看,ARM开发板性能有限,不可能在上面直接编译Linux内核。所以我们需要在PC上编译,生成ARM能执行的代码。这就是交叉编译。
工具链的选择
我个人习惯用Linaro提供的GCC工具链,稳定且社区支持好。以ARMv8/AArch64为例:
# 下载aarch64-linux-gnu工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz
# 解压到指定目录
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz -C /opt/
# 配置环境变量
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH
export CROSS_COMPILE=aarch64-linux-gnu-
export ARCH=arm64
~/.bashrc 里,避免每次打开终端都要重新export。另外,记得检查工具链版本是否与内核版本匹配,不匹配会出现莫名其妙的编译错误。
验证工具链
搭好环境后,先写个简单的C程序验证一下:
# hello.c
#include <stdio.h>
int main(void) {
printf("ARMv9移植实战\n");
return 0;
}
# 交叉编译
aarch64-linux-gnu-gcc -o hello hello.c
# 查看生成的文件类型
file hello
# 输出: hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV)
看到"ARM aarch64"就对了。我曾经遇到过工具链装好了,但编译出来的却是x86_64的可执行文件——后来发现是PATH里同时存在多个工具链,优先级搞混了。嗯,这里要注意环境变量的顺序。
Bootloader流程解析:TF-A与U-Boot
ARMv9平台的启动流程比传统ARM复杂得多。我画了一张图,帮你理清整个链条:
TF-A (Trusted Firmware-A)
TF-A是ARM官方提供的安全固件,负责从EL3启动。我刚开始接触时觉得它很神秘,其实核心就三件事:
- BL1:固化在ROM里的代码,验证并加载BL2。这部分基本不用改。
- BL2:初始化DDR、时钟、串口等基础外设,然后加载BL31/BL32/BL33。
- BL31:运行在EL3,提供运行时服务(PSCI、SMC等)。
关键配置:TF-A的编译参数决定了平台行为。我常用的配置项:
make PLAT=qemu DEBUG=1 \
ARM_ARCH_MAJOR=9 \
SPD=opteed \
BL33=../u-boot/u-boot.bin
注意 ARM_ARCH_MAJOR=9 是ARMv9特有的,不加这个编译出来的固件可能不支持新特性。
U-Boot流程
U-Boot作为BL33,负责加载内核和设备树。它的启动分两个阶段:
| 阶段 | 功能 | 关键代码 |
|---|---|---|
| SPL (Secondary Program Loader) | 初始化DDR、时钟,加载U-Boot主程序 | arch/arm/mach-xxx/spl.c |
| U-Boot主程序 | 解析设备树、加载内核、启动系统 | common/board_r.c |
我曾经在移植时遇到一个坑:SPL阶段DDR初始化参数写错了,导致U-Boot主程序加载到内存后跑飞。排查了两天才发现是时序参数不对。所以,DDR参数一定要对照芯片手册仔细核对。
设备树(Device Tree)原理与编写
设备树,说白了就是描述硬件信息的配置文件。Linux内核不再硬编码硬件信息,而是通过设备树动态获取。这样做的好处是——换一块板子,只需要改设备树,不用重新编译内核。
设备树语法基础
一个最简单的设备树文件长这样:
/dts-v1/;
/ {
model = "ARMv9 Development Board";
compatible = "arm,armv9-board";
#address-cells = <2>;
#size-cells = <2>;
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>;
};
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a710";
reg = <0x0 0x0>;
enable-method = "psci";
};
uart@1c090000 {
compatible = "arm,pl011";
reg = <0x0 0x1c090000 0x0 0x1000>;
interrupts = <0 38 4>;
clock-frequency = <24000000>;
};
};
#address-cells 和 #size-cells 搞混了。ARMv9平台地址空间大,通常用2个cell表示64位地址。如果设成1个cell,内核解析内存范围时会截断高位地址,导致系统只能识别4GB以下的内存。
设备树绑定规则
每个外设节点都有对应的绑定文档,在Linux内核源码的 Documentation/devicetree/bindings/ 目录下。我建议你养成习惯:写设备树前先看绑定文档。比如PL011 UART的绑定要求:
compatible必须包含 "arm,pl011"reg必须包含地址和长度interrupts必须指定中断号和触发类型clock-frequency可选,但建议写上
设备树编译与反编译
设备树源文件(.dts)需要编译成二进制(.dtb)才能被内核使用:
# 编译
dtc -I dts -O dtb -o board.dtb board.dts
# 反编译(调试用)
dtc -I dtb -O dts -o board_decompiled.dts board.dtb
反编译特别有用。我经常用它来检查生成的dtb是否正确,或者分析别人板子上的设备树配置。
设备树覆盖(Overlay)
ARMv9平台支持设备树覆盖,允许在运行时动态添加或修改设备节点。这个特性在FPGA加速卡场景下特别实用:
// fpga-accel.dts
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = <&fpga_region>;
__overlay__ {
#address-cells = <2>;
#size-cells = <2>;
accel@a0000000 {
compatible = "vendor,accel-v1";
reg = <0x0 0xa0000000 0x0 0x10000>;
interrupts = <0 42 4>;
};
};
};
};
-@ 选项生成符号表:dtc -@ -I dts -O dtb -o fpga-accel.dtbo fpga-accel.dts。加载覆盖的命令是 configfs 接口,具体路径在 /sys/kernel/config/device-tree/overlays/。
本章小结
系统移植的三板斧——交叉编译环境、Bootloader、设备树,环环相扣。环境搭不好,编译出来的东西跑不了;Bootloader流程不清楚,系统起不来;设备树写不对,外设认不到。我建议你动手实践时,先从QEMU模拟器开始,把这三步走通,再上真实硬件。这样出了问题,至少知道是哪个环节的锅。