03 U boot移植及分析
U-Boot移植及分析
去哪下载源码
移植uboot分为2种境界:
- 1.芯片原厂为自己的demo板修改主线uboot的源码
- 2.自制开发版时,去芯片原厂维护的uboot仓库下载源码,不要基于主线uboot
我们如果不是在芯片原厂上班,那么就不需要去uboot官方仓库下载源码,直接找到自己使用的芯片厂商所提供的uboot就行了,后续我们的移植都是对其demo板子的uboot源码的修改
芯片原厂一般会对主线u-boot做一些扩展,比如RK就对主线u-boot做了很多扩展,如支持中断,支持使用Kernel的设备树、支持更多的文件系统等…
移植流程
1.下载源码
2.选择原厂开发板的配置文件,编译并下载到开发板,看看哪些硬件初始化失败,这将是我们后面移植工作的重点
3.添加自己的开发板
这个图不是很全,由于是比较老的版本,所以少了些东西,以下面的文字为准
①开发板的默认配置文件:
configs/xxx_defconfig,用于配置某功能的开/关- 和Linux内核以及其他用了Kconfig系统的编译体系一致,首先生成
.config文件,再生成一些头文件(config/目录中)这些头文件中的宏定义才是实际控制条件编译的东西
1
2
3
4
5
6
7//.config
CONFIG_CMD_MMC=y
CONFIG_USB_HOST=n
// xxx.h
#define CONFIG_CMD_MMC 1
#undef CONFIG_USB_HOST- 和Linux内核以及其他用了Kconfig系统的编译体系一致,首先生成
②开发板的板级头文件:
include/configs/xxx.h,该文件的作用:- 通过
CONIFG_XXX宏定义来定义开发板的一些硬件参数(通常是时钟频率、内存起始地址之类的不用修改的信息,所以直接写死在头文件里,而不是通过Kconfig来设置)如果某个配置项同时在头文件和.config中出现,则config的优先级更高
1
2- 功能模块的开关
1
2- 默认环境变量
1
2
3- 通过
③开发版的板级文件夹:
board/<vendor>/<board_name>,不同厂商的开发板内容差异较大,但通常都会有以下关键文件:1
2
3
4
5
6
7board/
└── <vendor>/ # 厂商名(如 freescale、ti、rockchip)
└── <board_name>/ # 开发板名(如 mx6ullevk、rpi)
├── Makefile # 控制该板子的编译规则
├── Kconfig # 定义板级配置选项(menuconfig 可见)
├── MAINTAINERS # 维护者信息(可选)
├── <board_name>.c # 板级初始化代码(关键)<board_name>.c:实现板级硬件初始化函数(如 DDR 配置、外设引脚复用、环境变量设置),这些函数会被board_init_f/r()调用1
2
3
4
5
6
7
8
9
10
11
12
13// 典型函数
int board_init(void) {
// 初始化 GPIO、时钟、DDR 等
}
int dram_init(void) {
// 设置内存大小(如 gd->ram_size = 512MB;)
}
int board_late_init(void) {
// 后期初始化(如设置 MAC 地址、环境变量等)
}
④设备树:在
arch/arm/dts下
4.删掉原厂uboot中一些不要的驱动
5.编译下载进行测试
参考链接
- i.MX6ULL嵌入式Linux开发2-uboot移植实践 - 知乎 (zhihu.com)
- u-boot移植:详细讲解移植u-boot.2022.10版本到imx6ull开发板(没用设备树)
- i.MX6ULL - 从零开始移植uboot-imx_v2020.04_5.4.70_2.3.0(基于设备树)
- 移植NXP官方uboot到IMX6ULL开发板–以及过程中遇到的疑问和错误记录
- IMX6ULL2025年最新部署方案
源码编译
uboot编译时一般包含以下几个步骤:
- 1.清空之前的东西
- 2.加载默认配置
- 3.编译
最好把步骤写成Shell脚本,免得后边忘记了
1 | make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean |
编译产物
这里只列举出比较重要的
根目录
u-boot.lds:链接脚本,给出了uboot的各部分放在什么地址
u-boot.bin:这是编译后的 U-Boot 核心程序的二进制文件,可以使用imxdownload工具下载到SD卡
u-boot.cfg:保存了ddr的配置信息,供ROM读取来初始化内存
u-boot.imx:对于u-boot.bin进一步封装,在头部加上了u-boot.cfg。使用imxdownload工具下载时,实际上是先转成了这种格式,再下载到SD卡里的。如果是使用其他方法下载到SD卡,就得用这种格式
u-boot.dtb:编译后的设备树二进制文件
u-boot-nodtb.bin:编译后的u-boot核心程序
u-boot-dtb.bin:编译后的u-boot核心程序 + 设备树的二进制文件
u-boot-dtb.imx:编译后的u-boot核心程序 + 设备树的二进制文件,并转换成了imx6ull支持的格式
经测试,u-boot-dtb.bin和u-boot.bin貌似是一样的,他们大小都一样

tpl
- u-boot-tpl.bin
spl
- u-boot-spl.bin
下载到开发板
- 方式一:使用NXP官方提供的下载工具imxdownload将**.bin**文件下载到SD卡中:
1 | chmod 777 imxdownload # 给予 imxdownload 可执行权限,一次即可 |
- 方式二:使用Ubuntu自带的指令下载**.imx**文件到SD卡中
1 | sudo dd if=u-boot-dtb.imx of=/dev/sdb bs=512 seek=2 conv=sync |
U-boot的使用
常见命令
| 命令 | 作用 |
|---|---|
| printenv | 打印环境变量 |
| setenv | 设置某个环境变量 |
| saveenv | 保存环境变量 |
| boot | 执行bootcmd环境变量,加载内核 |
| bootz <内核地址> [ |
从内存中指定地址启动Linux内核 |
环境变量的设置
bootcmd(启动命令)
功能:定义了在U-Boot启动时自动执行的命令(手动执行boot指令也是运行这个). 通常用于自动引导操作系统或加载内核镜像
作用:U-Boot启动后,会执行bootcmd中定义的命令,通常这包括从某个存储介质(如Flash、SD卡或网络)加载内核镜像、设备树文件和根文件系统镜像,然后引导内核启动
示例:
1 | setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000;' |
可以看到,I.MX6ULL在启动时,首先从tftp服务器下载zImage和设备树文件到指定的内存中,然后使用
bootz命令启动Linux内核
bootargs(启动参数)
功能:传递给内核的启动参数,通常用于指定根文件系统、控制台、日志级别等。内核启动时会解析这些参数并根据配置做出相应设置
作用:==影响内核启动时的行为==。例如,指定根文件系统的位置、传递命令行参数给内核控制台、设置日志输出的详细级别等
示例:
1 | setenv bootargs = ‘console=tty1 console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.137.100:/home/lrq/linux/nfs/qtrootfs,proto=tcp,v3 rw ip=192.168.137.50:192.168.137.100:192.168.137.1:255.255.255.0::eth0:off’ |
内核是如何获取bootargs
- 并不是直接读的uboot的
bootargs环境变量,而是uboot会把他的bootargs环境变量先写入设备树的chosen节点,内核通过该节点获取启动参数的 - 内核启动后,可以通过
cat /proc/cmdline查看启动参数!
网络参数
当使用tftp、nfs时,在uboot中还要设置一些与网络相关的环境变量
示例:
1 | setenv ipaddr 192.168.137.50; |
启动流程分析
不同原厂/指令集架构SoC的启动流程都不太一样,甚至同一个SoC因为所用的启动介质不同,启动流程也可能不同。所以我们应该针对某个具体的SoC和来分析启动流程,而不是宽泛地分析uboot的启动流程
面试回答
1.上电&BootROM:
- 上电之后首先运行SoC内部的BootROM
- BootROM首先初始化最基本的外设(如时钟、片上SRAM),接着根据boot引脚的电气属性选择启动介质,并把后级bootloader(SPL)加载到片上SRAM,然后跳转执行
2.SPL:
- 初始化DDR,把后级的bootloader(Uboot main)搬运到DDR,然后跳转执行
3.Uboot main:
- 硬件初始化:时钟、串口、定时器、DDR 校准、板级外设初始化
- 代码重定向:把Uboot移动到内存的高段地址
- 加载内核:首先将内核和设备树加载到内存中,之后通过
bootz命令启动内核 bootz对应uboot中的do_bootz()函数,在其中会对内核镜像进行解压缩和校验,之后调用do_bootm_linux()在里面使用函数指针kernel_entry保存解压后镜像的内核入口,并进行跳转
4.Linux内核启动阶段
- 硬件初始化(head.S):
- 打开MMU,建立page table
- 初始化中断控制器,设置异常向量表
- 初始化堆栈,提供C语言的运行环境
- 内核初始化(start_kernel()):解析设备树,初始化各种内核子系统,挂载根文件系统
- 用户进程启动:内核启动init进程,由init进程启动其他用户进程
RK平台
RK平台主要有2种启动流程,其主要区别在于使用的前级Loader
1 | //闭源方案 |
- 可以看到其实整个启动流程有多级bootloader,不仅仅是uboot
- TPL/ddr.bin:运行在SRAM,负责DDR的初始化
- SPL/Miniloader:运行在DDR,负责完成系统lowlevel初始化、后级固件的加载(trust.img、uboot.img)
- U-Boot proper:运行在DDR,指的是我们通常所说的U-Boot,负责kernel的引导
- TRUST:Armv8引入的安全相关的固件
NXP平台
NXP平台SoC的多级bootloater的启动顺序如下所示
1 | BootROM => (SPL) => U-Boot => KERNEL |
1.BOOT ROM阶段
执行主体:芯片内部Boot ROM中的代码
功能:
启动介质选择:根据
BOOT_MODE[1:0]引脚或eFUSE配置(如BT_FUSE_SEL)选择从SD卡、eMMC、NAND Flash等设备启动硬件初始化:通过解析镜像(
.imx文件)的DCD表,初始化SRAM、内核/外设时钟,引脚复用等基础外设- DCD表里面存了6ull一些重要寄存器的值,boot rom会利用它来进行赋值,从而初始化硬件(参考链接:正点原子驱动开发9.4.2节)
安全启动支持:NXP的boot rom支持HAB,可验证镜像签名,增强安全性
加载引导代码:初始化启动介质,并将后级Loader(U-Boot)从存储设备加载到SRAM中直接运行,跳转到后级Loader入口点
2.U-Boot阶段
执行主体:
uboot.imx,在先在SRAM中,后在DDR中运行,主要是C语言实现功能:
- 外设初始化:串口、网卡、usb、屏幕之类的硬件
- 内核引导:加载内核镜像、设备树到内存中
- 设置环境变量、启动参数,执行预定义的引导命令(
bootcmd环境变量里的命令)或进入交互式模式来启动内核
2.1第一阶段(SPL)
作用:由于SRAM太小,放不下完整的uboot,所以需要先初始化DDR,然后把uboot主体放到DDR里运行。而SPL本身是在SRAM运行的
注意,如果启动介质是eMMC或者SD卡的话,可能没有显式的
SPL.bin被编译出来,但是还是类似的。bootrom后会加载uboot.imx的前一小部分到SRAM进行DDR的初始化,之后把后一部分加载到DDR并跳转
2.1第二阶段(U-boot proper)
U-boot proper(主体)又可细分为2个阶段:
汇编阶段(
start.S)- CPU模式设置:切换至SVC模式,关闭中断,确保启动过程不被干扰
- Cache与MMU控制:关闭MMU(因直接操作物理地址),清除Cache以避免脏数据影响
- 异常向量表重定位:将向量表从默认地址0x00000000重定位到DDR中
C语言阶段
- 板级前置初始化
board_init_f():初始化早期外设(如串口、定时器),并划分DDR内存区域(如malloc区、重定位地址)。此阶段通过函数数组(init_sequence_f)依次调用外设初始化函数 - 代码重定向:kernel一般是放在内存的低段地址,为了防止拷贝内核到DDR时覆盖掉了uboot,所以把uboot移动到内存的高段地址
- 板级后置初始化
board_init_r():初始化高级外设(如网卡、USB、LCD),加载环境变量,并进入main_loop - 主循环
main_loop()
- 板级前置初始化

疑问:为什么在u-boot的SPL和主体都会完成部分硬件的初始化(比如时钟、串口、flash、DDR),而Linux内核启动的时候其驱动框架还要再次注册这些设备呢?
- u-boot只负责硬件的初始化,比如设置串口的波特率之类的,但不负责硬件如何使用,比如为应用层封装可用的串口发送函数。硬件的操作逻辑(功能抽象)需要内核的驱动框架来提供
面试问题
1.各个外设的初始化顺序是什么?何时加载驱动?
- 外设的初始化顺序由2部分共同决定。首先在
init_sequence[]中定义了许多函数的调用顺序,里面会完成部分外设的初始化,这些外设的初始化顺序可以直接看出来。其次如果启用了DM(u-boot的驱动模型),则会在initr_dm中扫描设备树,根据各节点的出现顺序初始化
2.uboot中设备驱动代码是怎么组织的?
- 以串口为例,
drivers/serrial中首先会定义一个类似Linux中核心层一样的一个抽象层serial-uclass.c,定义出通用API接口,接着各个芯片原厂会实现自己SoC的主机驱动,比如serial_mxc.c就是NXP为他们SoC实现的。可以根据compatible来看到底使用的是什么驱动
3.各种教程提到了uboot启动中的一些关键API,比如board_init_f(),但是实际看源码的时候,不知道去哪找这类函数,并且同一个函数可能在多个地方都有定义,那么如何看某个函数在具体是在哪个文件定义的?
- 在
uboot.map中搜索就行了 - 有的时候会看到某个函数定义在
built-in.o,但是却没built-in.c,因为该文件是为了减少链接耗时而生成的中间文件,可以通过build-in.o.cmd来看它依赖了哪些c文件,再grep搜索
4.如何判断到底是否启用了SPL/TPL阶段
- 看.config中是否有
CONFIG_SPL=y以及有没有生成uboot-spl.bin
5.spl/tpl这些前级loader的固件是单独存在的还是会被链接到uboot.bin中?
- 单独存在
6.spl和主uboot中有同名函数比如board_init_f/r怎么办?
- 首先spl和主uboot是2个不同的程序(不共享符号表),所以有同名函数无所谓
- 其次,可以通过map文件看同名函数到底在哪个文件,基本上也都是不同的文件里定义的,不会出现同一个文件的同一个函数,同时出现在spl和主uboot中的(spl和主uboot的
board_init_f/r所做的事情也不同)
7.SPL的启动流程是什么?
- 通过看链接脚本可以发现spl和uboot主体的入口都是一样的,所以SPL和uboot主体在启动时首先都要运行上面提到的汇编代码,在
_main中都会调用board_init_f,但是2者的代码中该函数的定义是不同的
8.有没有遇到过uboot阶段中出现问题,比如启动内核失败,什么现象,卡在哪里
常见问题:
- uboot的下载地址和启动时的地址不一致
- 网卡驱动没完全适配好,导致TFTP下载内核时失败
- 校验镜像失败,提示 Bad Magic Number
- 内核能启动但VFS挂载失败,报
Unable to mount root fs,一般是bootargs配置或文件系统驱动问题
9.uboot是如何给kernel传递参数的
- uboot在启动内核之前会对传进来的设备树进行更新,在里面插入
/chosen节点,里面会保存uboot的bootargs环境变量,内核启动后在setup_arch()中解析这些参数
