3、Bootloader与内核基础:U-Boot编译与配置、Linux内核编译、设备树(DTS)基础、内核启动参数

好,咱们进入第三章。这一章可以说是整个嵌入式系统的「起跑线」。你想想看,一块开发板拿到手,上电之后,CPU 怎么知道该干什么?谁去把操作系统请进内存?谁告诉内核硬件长什么样?

答案就是:Bootloader(U-Boot)、Linux 内核、设备树(DTS),还有那串神秘的启动参数。我当年刚入行时,觉得 U-Boot 就是个「黑盒子」,敲几个命令能启动就行。直到有一次,板子死活起不来,我对着串口日志看了三天,才发现是内核参数里一个逗号写成了句号……嗯,从那以后,我再也不敢小看这些基础了。

本章核心脉络:U-Boot 负责引导 → 内核负责运行 → 设备树描述硬件 → 启动参数传递策略。四者环环相扣,缺一不可。

嵌入式系统启动流程 U-Boot 引导加载程序 Linux 内核 操作系统核心 设备树 DTS 硬件描述语言 启动参数 bootargs U-Boot 加载内核 → 内核解析设备树 → 设备树描述硬件资源 启动参数贯穿全程,控制内核行为 💡 任何一个环节出错,系统都无法正常启动

3.1 U-Boot 编译与配置

U-Boot,全称 Universal Bootloader。说白了,它就是嵌入式世界的「BIOS」。我习惯把它看作一个微型操作系统——有自己的命令行、文件系统、网络协议栈。只不过它的使命只有一个:把内核请进内存,然后跳转过去。

3.1.1 获取源码与配置

从官方仓库或芯片厂商的 SDK 中获取 U-Boot 源码。我个人建议用厂商提供的版本,因为里面已经包含了板级配置。比如对于某款 ARM 开发板:

git clone https://github.com/u-boot/u-boot.git
cd u-boot
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- <board_name>_defconfig

这里的 _defconfig 是板级默认配置。我在项目中遇到过,有人直接 make menuconfig 乱改一通,结果网口驱动没了……嗯,建议先看 configs/ 目录下有没有现成的配置。

3.1.2 编译与烧录

配置完成后,直接编译:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4

编译产物通常是 u-boot.bin 或 u-boot.imx(不同平台后缀不同)。烧录方式取决于硬件——有的用 SD 卡,有的用 SPI Flash,有的用 JTAG。

我的小技巧:第一次烧录 U-Boot 时,先用 md 命令读一下内存,确认硬件能正常访问。我曾经在一块新板子上烧了三天,最后发现是 DDR 初始化参数不对——U-Boot 根本没跑起来。

3.2 Linux 内核编译

内核编译,说白了就是把你需要的功能「编译进去」或「编译成模块」。我刚开始学的时候,总觉得内核配置选项多到让人崩溃。后来我总结了一个原则:用不到的,坚决不编译。这样内核小、启动快、还安全。

3.2.1 获取内核源码

同样,建议从芯片厂商的仓库获取。比如:

git clone https://github.com/raspberrypi/linux.git
cd linux
make ARCH=arm bcm2711_defconfig  # 以树莓派4为例

3.2.2 配置内核

内核配置有三种常用方式:

  • make menuconfig:基于 ncurses 的图形界面,最常用
  • make xconfig:基于 Qt 的图形界面,更直观
  • make defconfig:使用默认配置,适合快速验证

我个人习惯先用 defconfig 生成基础配置,再用 menuconfig 微调。比如去掉蓝牙驱动(如果板子没有蓝牙),加上自己写的 SPI 驱动。

3.2.3 编译与安装

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 zImage
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 modules
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=./modules modules_install

这里 zImage 是压缩后的内核镜像。如果是 64 位 ARM,用 Image 或 Image.gz。

注意:内核版本必须与 U-Boot 兼容。我遇到过内核太新,U-Boot 不支持新的设备树绑定,结果启动到一半就挂了。建议先查一下 U-Boot 的 doc/ 目录下的兼容性说明。

3.3 设备树(DTS)基础

设备树,英文叫 Device Tree,缩写 DTS(源文件)或 DTB(二进制)。它的作用是什么?说白了,就是告诉内核:「你的硬件长什么样」。以前的内核把硬件信息硬编码在 arch/arm/mach-xxx 里,换一块板子就得重新编译内核。有了设备树,内核和硬件描述就解耦了。

3.3.1 DTS 文件结构

一个典型的 DTS 文件长这样:

/dts-v1/;
#include "imx6ul.dtsi"

/ {
    model = "My Custom Board";
    compatible = "mycompany,imx6ul-board", "fsl,imx6ul";

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB RAM
    };

    &uart1 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
    };
};

这里有几个关键点:

  • /dts-v1/;:版本声明,必须写
  • #include:可以包含 .dtsi 文件(类似头文件),.dtsi 里放 SoC 公共部分
  • compatible:匹配内核驱动,内核通过这个字符串找到对应的 driver
  • reg:描述地址和大小,格式是 <起始地址 长度>

3.3.2 编译设备树

dtc -I dts -O dtb -o myboard.dtb myboard.dts

dtc 是设备树编译器。内核源码里也有编译脚本,可以直接:

make ARCH=arm dtbs

避坑指南:我曾经在 DTS 里把 GPIO 的 reg 写错了,结果 LED 死活不亮。排查了两天,最后用 dtc -I dtb -O dts 反编译,才发现地址偏移了一位。所以,写完 DTS 一定要反编译检查一遍。

3.4 内核启动参数

启动参数,就是 U-Boot 传给内核的那一串字符串。它告诉内核:根文件系统在哪、控制台用哪个、内存多大等等。这串参数通常存在 U-Boot 的环境变量 bootargs 里。

3.4.1 常用启动参数

参数 说明 示例
console 指定控制台设备 console=ttymxc0,115200
root 指定根文件系统设备 root=/dev/mmcblk0p2
rootfstype 根文件系统类型 rootfstype=ext4
rw 以读写方式挂载根文件系统 rw
init 指定第一个用户空间进程 init=/sbin/init
mem 限制可用内存大小 mem=256M

3.4.2 在 U-Boot 中设置

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rw'
setenv bootcmd 'mmc dev 0; fatload mmc 0:1 0x80800000 zImage; fatload mmc 0:1 0x83000000 myboard.dtb; bootz 0x80800000 - 0x83000000'
saveenv
boot

这里 bootz 是启动 zImage 内核的命令。如果是 64 位,用 booti。参数中的 - 表示没有 initramfs。

注意:启动参数里不要有空格!U-Boot 会把空格当成参数分隔符。我见过有人写 root = /dev/mmcblk0p2(多了空格),结果内核根本找不到根文件系统。正确的写法是 root=/dev/mmcblk0p2。

3.5 实战:从 U-Boot 到内核的完整启动

好了,理论说完了。咱们来走一遍实际流程。假设你有一块 i.MX6UL 开发板,SD 卡里已经烧好了 U-Boot、内核镜像和设备树。

  1. 上电:U-Boot 启动,打印启动信息
  2. 自动执行 bootcmd:从 SD 卡加载 zImage 和 DTB 到内存
  3. 设置 bootargs:告诉内核控制台和根文件系统位置
  4. 执行 bootz:跳转到内核入口
  5. 内核启动:解析 DTB,初始化硬件,挂载根文件系统
  6. 执行 init:启动第一个用户空间进程

如果一切顺利,你会看到串口打印出内核的启动日志,最后出现登录提示符。如果不顺利……嗯,那就回到第一步,检查 U-Boot 的环境变量和 DTB 文件。

核心要点回顾:

  • U-Boot 负责加载内核和 DTB,设置启动参数
  • 内核编译要选对架构和板级配置
  • 设备树是硬件描述文件,必须与内核驱动匹配
  • 启动参数控制内核行为,一个字符都不能错

这一章的内容就到这里。记住,嵌入式启动是一个「链条」,任何一个环节断了,系统都跑不起来。我当年踩过的坑,希望你能绕过去。下一章,咱们聊聊根文件系统的构建——那才是真正让系统「活起来」的关键。

可可精品资料库