第三章 Bootloader与内核启动:U-Boot移植与配置、Linux内核裁剪与编译、设备树(DTS)基础
各位同学,今天我们来聊聊嵌入式Linux启动过程中最关键的两个环节——Bootloader和内核启动。说实话,我在刚入行那会儿,觉得这部分特别神秘,好像系统一上电就自动跑起来了。后来自己动手做项目,才发现这里面的门道可不少。
嗯,咱们先从U-Boot说起。
3.1 U-Boot移植与配置
U-Boot,全称是Universal Bootloader。说白了,它就是嵌入式系统的"第一段程序"。CPU上电后,最先执行的就是它。它的任务很简单:初始化硬件,加载内核到内存,然后跳转过去执行。
我在项目中遇到过最头疼的事,就是U-Boot移植。不同板子的硬件配置千差万别,你得一个一个去适配。不过别怕,掌握了套路,其实也不难。
3.1.1 U-Boot的目录结构
先看看U-Boot的源码目录,我习惯先了解整体框架:
u-boot/
├── arch/ # 架构相关代码(ARM、x86、RISC-V等)
├── board/ # 板级支持包
├── common/ # 通用功能
├── drivers/ # 驱动代码
├── dts/ # 设备树文件
├── include/ # 头文件
├── net/ # 网络协议栈
├── cmd/ # 命令行命令
└── tools/ # 辅助工具
我个人建议,刚开始接触时重点关注 board/ 和 arch/ 这两个目录。因为移植工作主要就是改这两个地方。
3.1.2 移植U-Boot的基本步骤
移植U-Boot,说白了就是让它在你的板子上能跑起来。我总结了一下,核心就三步:
- 选择参考板:找一个和你硬件最接近的板子作为起点
- 修改板级配置:调整内存、时钟、外设等参数
- 编译验证:烧录到板子上看能不能跑
举个例子,假设我们要为一块基于i.MX6ULL的板子移植U-Boot:
# 1. 配置
make mx6ull_14x14_evk_defconfig
# 2. 修改板级文件
# 编辑 board/freescale/mx6ullevk/mx6ullevk.c
# 修改DDR配置、GPIO初始化等
# 3. 编译
make -j4
小技巧:我刚开始做移植时,经常忘记修改时钟配置。结果U-Boot跑起来后,串口输出全是乱码。后来我养成了一个习惯——先确认时钟树,再改其他配置。你想想看,时钟不对,啥都白搭。
3.1.3 U-Boot的配置系统
U-Boot使用Kconfig配置系统,和Linux内核类似。你可以通过 make menuconfig 来图形化配置:
make menuconfig
这里我建议重点关注几个配置项:
| 配置项 | 说明 | 我的建议 |
|---|---|---|
| CONFIG_BOOTDELAY | 启动延时(秒) | 调试时设为3,量产时设为0 |
| CONFIG_BOOTCOMMAND | 默认启动命令 | 根据你的存储介质设置 |
| CONFIG_SYS_TEXT_BASE | U-Boot加载地址 | 要和硬件设计匹配 |
注意:我曾经因为CONFIG_SYS_TEXT_BASE设置错误,导致U-Boot加载到内存后和别的程序冲突,系统直接死机。这个地址一定要和你的内存布局对应好,别想当然。
3.2 Linux内核裁剪与编译
U-Boot跑起来后,下一步就是加载Linux内核。但内核本身很大,动辄几十兆。对于嵌入式设备来说,存储空间和内存都有限,所以我们需要裁剪内核。
说白了,内核裁剪就是去掉你不需要的功能。比如你的设备没有WiFi,那就把WiFi驱动去掉。没有USB,就把USB子系统去掉。这样内核体积能小很多。
3.2.1 获取内核源码
从kernel.org下载,或者用芯片厂商提供的BSP包。我个人习惯用厂商的BSP,因为已经针对硬件做了优化:
# 以Linux 5.10为例
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.tar.xz
tar -xf linux-5.10.tar.xz
cd linux-5.10
3.2.2 配置内核
内核配置有三种方式:
- make defconfig:默认配置,适合快速上手
- make menuconfig:图形化配置,我最常用
- make savedefconfig:保存最小配置
我建议先用 make defconfig 生成基础配置,再用 make menuconfig 手动裁剪:
# 生成默认配置
make ARCH=arm defconfig
# 图形化裁剪
make ARCH=arm menuconfig
在menuconfig界面里,我通常会重点检查这几个地方:
- General setup:关闭调试选项
- Device Drivers:只保留需要的驱动
- File systems:只保留需要的文件系统
- Networking support:按需裁剪网络协议栈
经验之谈:我做过一个项目,客户要求内核体积控制在2MB以内。一开始觉得不可能,后来把不用的驱动、文件系统、网络协议全去掉,最后内核只有1.8MB。你想想看,裁剪的空间其实很大。
3.2.3 编译内核
配置完成后,就可以编译了:
# 编译内核镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 zImage
# 编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 dtbs
# 编译内核模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 modules
编译完成后,你会得到几个关键文件:
- arch/arm/boot/zImage:压缩后的内核镜像
- arch/arm/boot/dts/xxx.dtb:设备树二进制文件
- 模块文件:.ko文件,需要安装到根文件系统
3.3 设备树(DTS)基础
设备树,英文叫Device Tree,简称DTS。它是什么?说白了,就是描述硬件信息的配置文件。
在早期Linux版本里,硬件信息都硬编码在内核代码里。换一块板子就得改内核代码,非常麻烦。设备树出现后,硬件描述和内核代码分开了。你换板子,只需要改设备树文件就行。
3.3.1 设备树的基本结构
设备树文件以 .dts 为后缀,编译后生成 .dtb 文件。它的结构是树状的:
/dts-v1/;
/ {
model = "My Board";
compatible = "vendor,myboard";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB内存
};
chosen {
bootargs = "console=ttymxc0,115200";
};
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 4>;
status = "okay";
};
gpio1: gpio@0209c000 {
compatible = "fsl,imx6ul-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <0 66 4>;
gpio-controller;
#gpio-cells = <2>;
};
};
};
嗯,这里有几个关键概念:
- 节点(node):用
{}包裹的硬件设备描述 - 属性(property):节点的具体参数,如
compatible、reg - 标签(label):如
uart1:,方便在其他地方引用
3.3.2 常用属性说明
| 属性 | 说明 | 示例 |
|---|---|---|
| compatible | 设备兼容性字符串 | "fsl,imx6ul-uart" |
| reg | 寄存器地址和大小 | <0x02020000 0x4000> |
| interrupts | 中断号、类型 | <0 26 4> |
| status | 设备状态(okay/disabled) | "okay" |
避坑指南:我曾经在写设备树时,把 reg 属性的地址写错了。结果内核启动时,驱动去访问错误的地址,系统直接panic。后来我养成了一个习惯——写设备树时,一定对照芯片手册确认寄存器地址。
3.3.3 设备树的编译与使用
设备树源文件需要编译成二进制文件才能被内核使用:
# 编译设备树
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译(调试用)
dtc -I dtb -O dts -o myboard.dts myboard.dtb
在U-Boot中,加载设备树的命令是这样的:
# 从SD卡加载设备树
fatload mmc 0:1 0x83000000 myboard.dtb
# 启动内核时传递设备树
bootz 0x80800000 - 0x83000000
这里要注意,设备树的加载地址不能和内核镜像重叠。我一般习惯把内核放在 0x80800000,设备树放在 0x83000000,中间留出足够空间。
3.4 启动流程总结
好了,我们把整个启动流程串起来看看:
- 上电:CPU从ROM中执行固化代码
- U-Boot启动:初始化DDR、时钟、串口等
- 加载内核:从存储介质读取zImage到内存
- 加载设备树:读取dtb文件到内存
- 跳转到内核:U-Boot传递启动参数,跳转到内核入口
- 内核启动:解压内核,解析设备树,初始化驱动
- 挂载根文件系统:启动init进程,进入用户空间
嗯,这一章的内容就到这里。说实话,Bootloader和内核启动这部分,刚开始会觉得有点复杂。但只要你动手做一遍,把U-Boot烧到板子上,看着串口输出一行行信息,那种成就感还是很棒的。
下一章我们会讲根文件系统的构建,到时候再聊。