2. Bootloader与系统启动流程:U-Boot移植与调试、ATF启动流程、内核启动参数与设备树调试

车载SoC的启动流程,说白了就是一段「从复位向量到用户态」的接力赛。每一棒跑砸了,系统就起不来。我这些年调过的板子,十有八九的启动问题都出在Bootloader、ATF或者设备树这三个环节上。今天咱们就把这条链路彻底捋清楚。

2.1 U-Boot移植与调试

U-Boot是嵌入式Linux的事实标准Bootloader。移植U-Boot到一块新板子,核心工作就三件:配置DDR时序、适配Flash/NAND/eMMC驱动、搞定网络/串口等基础外设。

2.1.1 移植第一步:板级目录与配置

我个人习惯从一颗已经支持的参考板开始。比如你的SoC是NXP i.MX8QM,那就先看 board/freescale/imx8qm_mek/ 这个目录。复制一份,改名字,然后修改 Kconfig 和 MAINTAINERS。

# 创建板级目录
cp -r board/freescale/imx8qm_mek board/freescale/my_car_board
# 创建板级配置
cp configs/imx8qm_mek_defconfig configs/my_car_board_defconfig

然后执行 make my_car_board_defconfig,再 make menuconfig 微调。这里有个坑——DDR参数必须跟硬件原理图严格对应。我曾经因为一个时序参数抄错了,板子死活跑不到DDR4的标称频率,查了三天才发现是寄存器配置里少写了一个bit。

注意:DDR初始化代码通常在 board/<vendor>/<board>/ddr.c 或 drivers/ddr/ 下。不要直接改通用驱动,要新建板级文件。

2.1.2 调试手段:串口与JTAG

U-Boot早期阶段(SPL/TPL)还没有完整的驱动栈,调试基本靠串口打印。我建议你在 arch/arm/mach-<soc>/lowlevel_init.S 里加几个 putc 宏,确认CPU、时钟、DDR是否正常。

调试阶段 常用手段 典型问题
SPL阶段 串口打印、GPIO电平 DDR初始化失败、时钟配置错误
U-Boot proper 串口、网络tftp、mmc 环境变量损坏、分区表错误
内核启动前 bootm/booti命令、设备树检查 内核地址不对、设备树语法错误
小技巧:在U-Boot命令行里执行 bdinfo 可以查看板级信息,mmc info 看存储设备,printenv 看环境变量。这些命令在调试初期非常有用。

2.2 ATF(ARM Trusted Firmware)启动流程

ARMv8架构引入了异常级别(EL0-EL3)。ATF运行在最高权限的EL3,负责安全启动、PSCI电源管理、以及切换到下一个阶段。车载SoC对安全要求极高,ATF是绕不过去的坎。

2.2.1 ATF的启动顺序

ATF的启动流程可以概括为:BL1 → BL2 → BL31 → BL32(可选)→ BL33(U-Boot)。

  1. BL1(Boot ROM):固化在SoC内部,加载BL2到SRAM。
  2. BL2(Trusted Boot Firmware):验证并加载BL31和BL32。
  3. BL31(EL3 Runtime Firmware):提供PSCI、SMC等服务,然后跳转到BL33。
  4. BL33(Non-secure World):就是U-Boot或直接是内核。

嗯,这里要注意:BL2和BL31的链接地址必须跟硬件设计一致。我遇到过一块板子,BL31的基地址跟ATF源码默认值差了0x1000,结果每次启动到PSCI初始化就挂。后来用JTAG单步跟踪才发现是地址对齐问题。

核心配置项:在ATF的 plat/<vendor>/<platform>/platform_def.h 里,重点关注 PLAT_PHY_ADDR_SPACE_SIZE、BL31_BASE、BL32_BASE 这几个宏。

2.2.2 PSCI与CPU热插拔

PSCI(Power State Coordination Interface)是ATF提供给内核的电源管理接口。车载SoC经常需要动态开关CPU核心来省电。如果PSCI实现有问题,内核执行 cpu_suspend 或 cpu_off 时就会卡死。

// 在ATF的psci_statistics.c里,可以加打印确认状态机
void psci_print_state(unsigned int cpu_id) {
    INFO("CPU%d: current state = %d\n", cpu_id, psci_cpu_state[cpu_id]);
}

我曾经在调试一个ADAS域控时,发现系统休眠后唤不醒。查了两天,最后定位到是ATF的 psci_validate_power_state 函数里,对电源域ID的校验写错了。修完那一行代码,问题解决。

2.3 内核启动参数与设备树调试

设备树(Device Tree)是描述硬件信息的结构化数据。内核通过它来知道「我这块板子上有哪些外设、它们怎么连接、寄存器地址是多少」。设备树写错了,驱动就加载不对。

2.3.1 内核启动参数

U-Boot通过 bootargs 环境变量传给内核。车载系统常用的参数包括:

  • console=ttyS0,115200:指定调试串口
  • root=/dev/mmcblk0p2:根文件系统分区
  • rw:以读写方式挂载
  • quiet:减少内核打印(量产时用)
  • isolcpus=2,3:隔离CPU核心给实时任务
调试建议:开发阶段不要加 quiet,把 loglevel=8 加上,能看到最详细的内核打印。量产时再改回 loglevel=4。

2.3.2 设备树调试三板斧

设备树调试,我总结了三步走:

  1. 检查语法:用 dtc -I dts -O dtb -o test.dtb test.dts 编译,看有没有报错。
  2. 查看实际加载:在U-Boot里执行 fdt addr $fdt_addr 然后 fdt list,确认设备树被正确加载到内存。
  3. 对比寄存器:用 devmem2 或 mmc read 读出硬件寄存器值,跟设备树里的 reg 属性比对。

为什么第三步很重要?因为有时候硬件设计改了,但设备树没同步更新。我见过一个案例,某款车载以太网PHY的复位GPIO从GPIO3_12改到了GPIO4_05,设备树里还是旧的,结果PHY死活不工作。查了整整一天,最后拿示波器量才发现复位引脚没拉高。

2.3.3 设备树覆盖(DT Overlay)

车载SoC经常有多个硬件变体。比如同一款域控制器,低配版用4GB内存,高配版用8GB。用设备树覆盖可以避免维护多份完整的dts文件。

// memory_overlay.dts
/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &memory;
        __overlay__ {
            reg = <0x0 0x80000000 0x0 0x80000000>; // 2GB
        };
    };
};

在U-Boot里用 fdt apply 命令加载覆盖文件。注意:覆盖文件的 target 必须跟基树里的节点标签完全一致。

常见错误:设备树里的 interrupts 属性写错,会导致中断无法触发。用 cat /proc/interrupts 可以快速确认中断号是否匹配。

2.4 本章知识结构图

下面这张图展示了从SoC上电到内核启动的完整流程,以及每个阶段的关键交付物。

Bootloader与系统启动流程 车载SoC启动流程与关键交付物 ROM Code (BL1) ATF BL2 ATF BL31 U-Boot (BL33) Linux Kernel BL2关键交付物 • 平台配置(平台_def.h) • 安全启动证书 • BL31/BL32镜像加载地址 U-Boot关键交付物 • 板级配置(defconfig) • DDR时序参数 • 环境变量(bootargs) 内核关键交付物 • 设备树(.dts/.dtb) • 内核启动参数 各阶段调试手段 BL1/BL2:JTAG + 串口打印 BL31:SMC调用日志 + PSCI状态 U-Boot:bdinfo + mmc + tftp 内核:loglevel=8 + devmem2 设备树:dtc编译 + fdt list 常见问题定位 • 启动卡死 → 检查DDR/时钟 • 内核panic → 检查设备树 • 外设不工作 → 检查reg/中断 • 休眠唤醒失败 → 检查PSCI • 网络不通 → 检查PHY地址

这张图把启动流程和调试手段对应起来了。你拿到一块新板子,按这个流程走一遍,基本能快速定位问题出在哪个阶段。

总结一下:U-Boot移植的核心是DDR和板级配置;ATF调试的关键是BL31的基地址和PSCI状态机;设备树调试要善用dtc编译和fdt命令。这三块搞定了,车载SoC的启动流程就稳了。