Bootloader原理与U-Boot移植
嵌入式Linux系统启动,第一步就是Bootloader。说白了,它就是上电后第一个跑起来的程序。我刚开始接触嵌入式时,总觉得Bootloader不就是个引导程序嘛,有啥好研究的?直到有一次调试一块新板子,串口死活没输出,折腾了两天才发现是Bootloader的时钟配置错了。嗯,从那以后我再也不敢小看它了。
Bootloader启动流程
一个典型的嵌入式系统启动,大致分这么几步:
- 上电复位:CPU从复位向量地址取第一条指令
- 一级Bootloader:初始化最基本的硬件(时钟、DDR、串口)
- 二级Bootloader:加载操作系统内核到内存
- 内核启动:解压并运行内核
你想想看,CPU刚上电时,DDR还没初始化,代码只能在SRAM或内部ROM里跑。所以Bootloader必须把自己分成两个阶段:
Stage 1:汇编代码,初始化CPU、关闭看门狗、设置时钟、初始化DDR。代码量极小,通常在4KB以内。
Stage 2:C语言实现,加载内核、文件系统、提供交互界面。功能丰富,体积较大。
我在项目中遇到过一块板子,DDR布线有问题,Stage 1初始化DDR时总是失败。排查了很久才发现是时序参数配错了。所以啊,Stage 1的代码一定要稳,它出问题你连调试手段都没有。
U-Boot源码结构
U-Boot是目前最主流的Bootloader。它的源码结构,我建议你重点关注这几个目录:
| 目录 | 作用 |
|---|---|
arch/ |
CPU架构相关代码,如arm、x86、riscv |
board/ |
具体板级支持代码,每个板子一个子目录 |
drivers/ |
各种外设驱动,串口、网卡、Flash等 |
common/ |
通用功能,如命令解析、环境变量管理 |
include/ |
头文件,特别是configs/下的板级配置 |
cmd/ |
U-Boot命令行命令的实现 |
我个人习惯,拿到一个新板子,先看board/和include/configs/这两个目录。前者告诉你板子初始化流程,后者告诉你功能开关怎么配。
小技巧:U-Boot的配置系统采用Kconfig,和Linux内核一样。想加新功能,运行make menuconfig即可。
U-Boot配置与编译
配置和编译U-Boot,其实就几条命令。但这里有个坑,我踩过好几次:
# 清理
make distclean
# 配置(以qemu-arm为例)
make qemu_arm_defconfig
# 图形化配置(可选)
make menuconfig
# 编译
make -j4
编译完成后,会在当前目录生成u-boot.bin。但注意,有些平台还需要额外的打包步骤,比如生成u-boot.imx或u-boot-spl.bin。
警告:make distclean会删除所有配置和编译产物。如果你改了配置想保留,用make clean只清理编译产物。
我曾经在给一块i.MX6ULL板子移植U-Boot时,忘了执行make distclean,直接make xxx_defconfig,结果配置没生效,编译出来的镜像还是旧的。折腾了半小时才发现问题。嗯,这种低级错误,犯一次就记住了。
U-Boot常用命令
U-Boot启动后,进入命令行界面。这里列出我最常用的几条命令:
| 命令 | 功能 | 示例 |
|---|---|---|
printenv |
打印所有环境变量 | printenv |
setenv |
设置环境变量 | setenv bootargs console=ttymxc0,115200 |
saveenv |
保存环境变量到存储设备 | saveenv |
tftp |
通过TFTP下载文件 | tftp 0x80800000 uImage |
bootm |
启动内核镜像 | bootm 0x80800000 |
mmc |
MMC/SD卡操作 | mmc dev 0 |
fatload |
从FAT分区加载文件 | fatload mmc 0:1 0x80800000 uImage |
调试时,我习惯先用printenv看看当前环境变量,确认启动参数对不对。然后用tftp从网络加载内核,这样不用反复烧写存储设备,开发效率高很多。
U-Boot环境变量
环境变量是U-Boot的灵魂。它决定了U-Boot怎么启动、从哪里加载内核、传给内核什么参数。核心环境变量有这么几个:
bootcmd:自动启动时执行的命令bootargs:传给内核的命令行参数bootdelay:自动启动前的延时(秒)ipaddr:开发板IP地址serverip:TFTP服务器IP地址loadaddr:加载内核的内存地址
举个例子,一个典型的网络启动配置:
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.10
setenv loadaddr 0x80800000
setenv bootcmd 'tftp ${loadaddr} uImage; bootm ${loadaddr}'
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.10:/nfsroot ip=192.168.1.100'
saveenv
这里要注意,bootcmd里的命令用分号分隔。U-Boot会依次执行这些命令。如果某条命令失败,后面的命令还会继续执行吗?答案是会的。所以调试时,我经常在bootcmd里加echo命令打印状态,方便定位问题。
避坑指南:我曾经在设置bootargs时,把console=ttymxc0写成了console=ttyS0,结果内核启动后串口没输出。排查了半天才发现是设备名不对。不同平台的串口设备名不一样,一定要查芯片手册确认。
环境变量存储在存储设备上(如MMC、NAND Flash)。U-Boot启动时会从存储设备加载环境变量到内存。如果存储设备上的环境变量损坏,U-Boot会使用默认值。我遇到过一块板子,环境变量存储区被意外擦除了,结果U-Boot启动后找不到bootcmd,停在命令行。解决办法是用defaultenv恢复默认环境变量,然后重新配置。
这张图把U-Boot的启动流程和核心组件串起来了。从上电复位到内核启动,每一步都有对应的代码模块。你移植U-Boot时,其实就是照着这个流程,把每个阶段的代码适配到你的板子上。