系统移植基础:交叉编译环境搭建、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复杂得多。我画了一张图,帮你理清整个链条:

ARMv9 启动流程总览 ROM Boot (固化代码) BL1 (TF-A BootROM) BL2 (TF-A平台初始化) BL31 (EL3 Runtime) BL32 (OPTEE安全OS) BL33 (U-Boot) 说明:ARMv9强制要求EL3安全监控模式,TF-A的BL31负责管理 异常等级切换。U-Boot作为BL33运行在EL2,加载内核到EL1。

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模拟器开始,把这三步走通,再上真实硬件。这样出了问题,至少知道是哪个环节的锅。