Bootloader原理与U-Boot移植

嵌入式Linux系统启动,第一步就是Bootloader。说白了,它就是上电后第一个跑起来的程序。我刚开始接触嵌入式时,总觉得Bootloader不就是个引导程序嘛,有啥好研究的?直到有一次调试一块新板子,串口死活没输出,折腾了两天才发现是Bootloader的时钟配置错了。嗯,从那以后我再也不敢小看它了。

Bootloader启动流程

一个典型的嵌入式系统启动,大致分这么几步:

  1. 上电复位:CPU从复位向量地址取第一条指令
  2. 一级Bootloader:初始化最基本的硬件(时钟、DDR、串口)
  3. 二级Bootloader:加载操作系统内核到内存
  4. 内核启动:解压并运行内核

你想想看,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启动流程与核心组件 上电复位 Stage 1:汇编初始化 时钟 / DDR / 串口 / 看门狗 Stage 2:C语言主逻辑 命令解析 / 环境变量 / 设备驱动 加载内核到内存 启动内核 核心组件 • arch/:CPU架构 • board/:板级支持 • drivers/:外设驱动 • cmd/:命令实现 • include/configs/:配置 关键环境变量 • bootcmd:启动命令 • bootargs:内核参数 • loadaddr:加载地址

这张图把U-Boot的启动流程和核心组件串起来了。从上电复位到内核启动,每一步都有对应的代码模块。你移植U-Boot时,其实就是照着这个流程,把每个阶段的代码适配到你的板子上。