U-Boot移植及分析

去哪下载源码

移植uboot分为2种境界:

  • 1.芯片原厂为自己的demo板修改主线uboot的源码
  • 2.自制开发版时,去芯片原厂维护的uboot仓库下载源码,不要基于主线uboot

我们如果不是在芯片原厂上班,那么就不需要去uboot官方仓库下载源码,直接找到自己使用的芯片厂商所提供的uboot就行了,后续我们的移植都是对其demo板子的uboot源码的修改

芯片原厂一般会对主线u-boot做一些扩展,比如RK就对主线u-boot做了很多扩展,如支持中断,支持使用Kernel的设备树、支持更多的文件系统等…

移植流程

  • 1.下载源码

  • 2.选择原厂开发板的配置文件,编译并下载到开发板,看看哪些硬件初始化失败,这将是我们后面移植工作的重点

image-20250815101048905
  • 3.添加自己的开发板

    image-20240914154917497

    这个图不是很全,由于是比较老的版本,所以少了些东西,以下面的文字为准

    • ①开发板的默认配置文件: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
    • ②开发板的板级头文件:include/configs/xxx.h ,该文件的作用:

      • 通过CONIFG_XXX宏定义来定义开发板的一些硬件参数(通常是时钟频率、内存起始地址之类的不用修改的信息,所以直接写死在头文件里,而不是通过Kconfig来设置)如果某个配置项同时在头文件和.config中出现,则config的优先级更高
      1
      2
      #define CONFIG_SYS_TEXT_BASE    0x87800000  // U-Boot 加载地址
      #define CONFIG_SYS_MMC_ENV_DEV 0 // 默认 MMC 设备号
      • 功能模块的开关
      1
      2
      #define CONFIG_CMD_TFTP          // 启用 tftp 命令
      #undef CONFIG_VIDEO // 禁用 LCD 显示驱动
      • 默认环境变量
      1
      2
      3
      #define CONFIG_EXTRA_ENV_SETTINGS \
      "bootcmd=mmc dev 0; fatload mmc 0 80800000 zImage; bootz 80800000\0" \
      "bootargs=console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait\0"
    • ③开发版的板级文件夹:board/<vendor>/<board_name>,不同厂商的开发板内容差异较大,但通常都会有以下关键文件:

      1
      2
      3
      4
      5
      6
      7
      board/
      └── <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.编译下载进行测试

参考链接

源码编译

uboot编译时一般包含以下几个步骤:

  • 1.清空之前的东西
  • 2.加载默认配置
  • 3.编译

最好把步骤写成Shell脚本,免得后边忘记了

1
2
3
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12

编译产物

这里只列举出比较重要的

  • 根目录

    • 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貌似是一样的,他们大小都一样

image-20250706163521795

  • tpl

    • u-boot-tpl.bin
  • spl

    • u-boot-spl.bin

下载到开发板

  • 方式一:使用NXP官方提供的下载工具imxdownload将**.bin**文件下载到SD卡中:
1
2
chmod 777 imxdownload				# 给予 imxdownload 可执行权限,一次即可
./imxdownload u-boot.bin /dev/sdb # 烧写到 SD 卡,不能烧写到/dev/sda 或 sda1 设备里面
  • 方式二:使用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
2
3
setenv ipaddr 192.168.137.50;
setenv serverip 192.168.137.100;
setenv gatewayip 192.168.137.1;

启动流程分析

不同原厂/指令集架构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
2
3
4
//闭源方案
BootROM => ddr.bin => Miniloader => TRUST => U-Boot => KERNEL
//开源方案
BootROM => TPL => SPL => TRUST => U-Boot => KERNEL
  • 可以看到其实整个启动流程有多级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()

image-20250408100411022

疑问:为什么在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来看到底使用的是什么驱动
image-20250707123918746

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() 中解析这些参数

参考链接