3、Bootloader与内核启动:U-Boot移植与配置、Linux内核裁剪与编译、设备树解析与修改

好,咱们进入第三章。这一章可以说是整个座舱系统启动的「第一道关卡」。你想想看,芯片上电后,第一段代码是谁跑的?就是 Bootloader。而 U-Boot 几乎是嵌入式 Linux 世界的标配。我这些年经手的项目,从低端单片机到高端车规芯片,无一例外都在跟它打交道。

说白了,Bootloader 的任务就两件:初始化硬件,然后加载内核。但这两件事背后牵扯的东西,远比想象中复杂。尤其是座舱系统这种对启动时间有严苛要求的场景,U-Boot 的移植和配置,直接决定了你的产品能不能在 3 秒内亮屏。

3.1 U-Boot 移植与配置

U-Boot 的全称是 Universal Bootloader,它支持几十种架构。但移植到一块新板子上,核心工作其实就几块:板级初始化、驱动适配、存储布局。

我个人习惯,拿到一块新芯片的参考板后,第一步不是急着改代码,而是先看芯片厂商提供的 board 目录下的参考板配置。比如对于 NXP i.MX8 系列,我会先复制一份 board/freescale/imx8qm_mek 到自己的板子目录下。

核心移植步骤:

  1. 创建板级目录:在 board/ 下新建你的板子文件夹,复制参考板的 Kconfig、Makefile、imx8qm_xxx.c 等文件。
  2. 修改内存初始化:DDR 时序参数是重中之重。我遇到过最坑的一次,就是 DDR 的刷新周期配错了,导致系统跑几分钟就随机死机。
  3. 配置时钟树:座舱芯片通常有复杂的 PLL 和时钟域,必须确保 CPU、DDR、外设总线时钟正确。
  4. 添加设备树文件:U-Boot 本身也使用设备树,需要创建 arch/arm/dts/ 下的 .dts 文件。
  5. 配置存储设备:eMMC、NAND Flash 或 SD 卡,驱动要能正确识别。

配置 U-Boot 用的是 make menuconfig,这个大家应该不陌生。但我要提醒一点:不要贪多。座舱系统对启动时间敏感,U-Boot 里能关的功能尽量关掉。比如网络协议栈、USB 主机模式、文件系统支持,如果启动阶段用不到,就果断去掉。

我的经验:U-Boot 的启动延迟,很多时候不是 CPU 慢,而是驱动初始化时做了太多轮询等待。比如某个 I2C 设备没接,但驱动还在傻等超时。我建议在板级初始化函数 board_init() 里加上超时控制,或者直接跳过不存在的设备。

3.2 Linux 内核裁剪与编译

内核裁剪,说白了就是「瘦身」。座舱系统的内核,不需要跑服务器那一套。你想想看,一个座舱芯片,需要支持几十个 USB 设备吗?需要上千个网络协议吗?不需要。

我刚开始做座舱项目时,直接拿厂商的默认配置编译,结果内核镜像 12MB。后来一查,光是不用的驱动就占了 5MB。裁剪后,内核降到了 4.2MB,启动时间缩短了将近 1 秒。

裁剪的核心思路,我总结成一张表:

裁剪方向 具体操作 典型节省空间
驱动模块 关闭不用的外设驱动(如未使用的网卡、USB 控制器) 1-3 MB
文件系统 只保留 ext4、FAT,去掉 btrfs、xfs 等 0.5-1 MB
网络协议 去掉 IPv6、IPsec、Netfilter 等 0.5-2 MB
调试选项 关闭 DEBUG、PRINTK、LOCKDEP 等 0.3-0.8 MB
架构无关代码 只保留 ARM64,去掉 x86、ARM32 等 0.2-0.5 MB

编译命令很简单,但有几个坑要注意:

# 配置内核
make ARCH=arm64 defconfig
make ARCH=arm64 menuconfig

# 编译内核镜像
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8 Image

# 编译设备树
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8 dtbs

# 编译内核模块
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8 modules

警告:我曾经犯过一个低级错误——编译内核时用了错误的交叉编译器版本。比如用 gcc 7.x 编译 Linux 5.10 内核,结果链接阶段报一堆 undefined reference。后来查了内核文档,才发现 5.10 要求最低 gcc 8.x。所以,编译前一定先看内核源码根目录下的 Documentation/process/changes.rst,确认工具链版本。

3.3 设备树(Device Tree)解析与修改

设备树,是嵌入式 Linux 里用来描述硬件信息的「说明书」。它把 CPU、内存、外设、中断、GPIO 等信息,用一种树形结构写出来。内核启动时,通过解析设备树来知道「我有哪些硬件,它们怎么连接」。

座舱系统的设备树,通常非常庞大。一块 i.MX8QM 的参考设备树,光 .dts 文件就有 3000 多行。但别怕,核心要改的地方其实不多。

我一般把设备树修改分成三类:

  • 内存节点:修改 reg = <0x0 0x80000000>; 来匹配实际 DDR 大小。
  • 外设节点:使能或禁用某个外设,比如 status = "okay"; 或 status = "disabled";。
  • 引脚复用:修改 pinctrl 子节点,配置 GPIO 功能。

举个例子,假设我们要在座舱系统中添加一个 I2C 触摸屏:

&i2c3 {
    status = "okay";
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c3>;

    touch@38 {
        compatible = "edt,edt-ft5406";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
    };
};

这里要注意的是 compatible 属性,它必须和内核驱动里的 of_match_table 完全一致,否则驱动不会 probe。

避坑指南:我曾经在调试一个显示驱动时,发现内核死活不认我的 LCD 面板。查了两天,最后发现是设备树里 display-timings 的 hactive 和 vactive 写反了。嗯,这种低级错误,一旦犯过,这辈子都不会再犯。

3.4 本章知识体系

为了让你更直观地理解这三者之间的关系,我画了一张流程图:

Bootloader与内核启动流程 Bootloader与内核启动流程 芯片上电 U-Boot SPL U-Boot Proper 加载设备树(.dtb) Linux内核启动 关键说明 • SPL:初始化DDR、时钟 • Proper:加载内核与dtb • 设备树:硬件描述信息 内核裁剪要点 • 关闭无用驱动 • 精简文件系统 • 去掉调试选项

从这张图可以清楚看到,整个启动链路是串行的。任何一个环节出问题,系统都起不来。我调试时,习惯在 U-Boot 的 bootcmd 里加上 bootdelay=3,这样可以在启动时按任意键进入 U-Boot 命令行,手动验证每个环节。

本章核心要点:

  • U-Boot 移植的核心是板级初始化和驱动适配,不要贪多,能关的功能就关。
  • 内核裁剪要有的放矢,座舱系统不需要的功能一律去掉,启动时间比功能丰富更重要。
  • 设备树是硬件和内核之间的桥梁,修改时务必确认 compatible 和引脚复用是否正确。
  • 整个启动流程是串行的,建议在 U-Boot 命令行中逐级验证,不要等到内核启动失败才回头查。