3、Bootloader与内核基础:U-Boot编译与配置、Linux内核编译、设备树(DTS)基础、内核启动参数
好,咱们进入第三章。这一章可以说是整个嵌入式系统的「起跑线」。你想想看,一块开发板拿到手,上电之后,CPU 怎么知道该干什么?谁去把操作系统请进内存?谁告诉内核硬件长什么样?
答案就是:Bootloader(U-Boot)、Linux 内核、设备树(DTS),还有那串神秘的启动参数。我当年刚入行时,觉得 U-Boot 就是个「黑盒子」,敲几个命令能启动就行。直到有一次,板子死活起不来,我对着串口日志看了三天,才发现是内核参数里一个逗号写成了句号……嗯,从那以后,我再也不敢小看这些基础了。
本章核心脉络: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:匹配内核驱动,内核通过这个字符串找到对应的driverreg:描述地址和大小,格式是<起始地址 长度>
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、内核镜像和设备树。
- 上电:U-Boot 启动,打印启动信息
- 自动执行 bootcmd:从 SD 卡加载 zImage 和 DTB 到内存
- 设置 bootargs:告诉内核控制台和根文件系统位置
- 执行 bootz:跳转到内核入口
- 内核启动:解析 DTB,初始化硬件,挂载根文件系统
- 执行 init:启动第一个用户空间进程
如果一切顺利,你会看到串口打印出内核的启动日志,最后出现登录提示符。如果不顺利……嗯,那就回到第一步,检查 U-Boot 的环境变量和 DTB 文件。
核心要点回顾:
- U-Boot 负责加载内核和 DTB,设置启动参数
- 内核编译要选对架构和板级配置
- 设备树是硬件描述文件,必须与内核驱动匹配
- 启动参数控制内核行为,一个字符都不能错
这一章的内容就到这里。记住,嵌入式启动是一个「链条」,任何一个环节断了,系统都跑不起来。我当年踩过的坑,希望你能绕过去。下一章,咱们聊聊根文件系统的构建——那才是真正让系统「活起来」的关键。