最近,工作重心要从裸机开发转移到嵌入式 Linux 系统开发,在之前的博文 Linux 之八 完整嵌入式 Linux 环境、(交叉)编译工具链、CPU 体系架构、嵌入式系统构建工具 中详细介绍了嵌入式 Linux 环境,接下来就是重点学习一下 U-Boot。
文中涉及的代码均放到了我个人的 Github 上:https://github.com/ZCShou/BOARD-STM32F769I-EVAL
,大家可以直接拿来边学习边验证,避免眼高手低。
开发环境
前几天,我将开发环境 Ubuntu 20.04 LTS + Arm GNU Toolchain 10.3 -2021.10 进行了升级,升级后的开发环境及需要注意的问题如下所示(本文后续内容同时对新 / 旧这两个环境进行了验证)。
- 由于 Ubuntu 22.04 LTS 默认是标配 OpenSSL 3.x,而旧版 U-Boot 使用的是 OpenSSL 1.x,因此,该环境编译旧版 U-Boot(从 commit e927e21c07483337ffb63b828d4ddb5e0db342b2 开始添加了相关处理) 将出现一堆警告:
- Arm GNU Toolchain 10.2-2022.02 存在 BUG,导致编译 U-Boot 报错,不要使用这个版本!
- 新版的 Arm GNU Toolchain (10.3 之后的版本)在 Linux 上 GDB 需要 Python3.8。然而,Ubuntu 22.04 默认的 Python 是 3.10。报错如下:
解决方法就是直接手动安装 Python3.8 即可。 旧版的 Arm GNU Toolchain 10.3 -2021.10 不需要 Python 支持sudo add-apt-repository ppa:deadsnakes/ppa -y sudo apt install python3.8
起源
U-Boot 起源于 Magnus Damm 编写的名为 8xxROM 的针对于 8xx PowerPC 的引导加载程序。1999 年 10 月,Wolfgang Denk 将其在 SourceForge.net 开源,由于该网站不允许项目名称以数字开头,因此,更名为 PPCBoot(即 PowerPC Boot 的缩写)。2000 年 7 月 19 日首次公开发布 PPCBoot-0.4.1。
- Wolfgang Denk 是 DENX Software Engineering GmbH(简称 DENX) 的创始人,PPCBoot 实际属于 DENX 公司
- 因为 linus ➔ linux 所以 Denk ➔ DENX ?
- DENX 是一家致力于使用自由软件的公司
随着 PPCBoot 被扩展到了 ARM 架构,DENX 认为,PPCBoot 这个名字已经不再合适,于是,在 PPCBoot−2.0.0 于 2002 年 10 月发布时决定将项目更名(SourceForge.net 新建项目)为 Das U-Boot(Universal Boot Loader 的缩写),其中的 Das 是一个德语定冠词,官方说是为了创建一个双关语(致敬经典的 1981 年德国潜艇电影 Das Boot)。
除了官方有些文档称为 Das U-Boot,外界几乎没人用这个名,大家都是直接称呼为 U-Boot。
PPCBoot−2.0.0 就对应 Das U-Boot 第一版 U−Boot-0.1.0 。并紧接着扩展到了 x86 处理器架构。然后,在接下来的几个月中陆续增加了其他架构功能:2003 年 3 月的 MIPS32、4 月份的 MIPS64、10 月份的 Nios II、12 月的 ColdFire 和 2004 年 4 月的 MicroBlaze。
Das U-Boot
现在,U-Boot 已经成为了是嵌入式设备首选的用于包装指令以引导设备操作系统内核的启动加载程序,并且是基于 GPL 协议开源的,项目地址:https://source.denx.de/u-boot。它可用于许多计算机架构,包括 68k,ARM,Blackfin,MicroBlaze,MIPS,Nios,SuperH,PPC,RISC-V 和 x86。
- 代码仓库:https://source.denx.de/u-boot
- Github 仓库:https://github.com/u-boot/u-boot
U-Boot 的代码结构及开发模式在尽可能的遵循 Linux Kernel 的方式,但在实际过程中也存在差异。例如,与 Linux Kernel 类似,在每个版本发布之后,将立即出现一个通常为 21 天的“合并窗口”,然后,在发布 rc1 后,开始以修复错误为主,但是,由于 U-Boot 相对于 Linux kernel 简单很多,因此,它没有 linux 的 Monorepo(Monotree) 模式。
代码风格也是与 Linux Kernel 一样
与 Linux 不同,从 2008 年 10 月的版本开始,U-Boot 版本的名称从没有更深层次含义的数字版本号更改为基于时间戳的编号,通常格式 U-Boot vYYYY.MM.x
,其中,YYYY
是年份(如 2022);MM
是月份(如 08);.x
可能没有,如果存在,这部分是 bug 修复版本(如 1)或者候选版本(如 rc1)。
- 自 2010 年 8 月起,实际的 U-Boot 源代码树中不再有 CHANGELOG 文件,但是,可以使用
make CHANGELOG
命令从 Git 日志动态创建它- 本文主要是使用 U-Boot v2021.10 和 U-Boot v2022.10 这两个版本
架构
U-Boot 其实就是一个功能复杂一些的裸机程序,这个程序最主要的一个功能就是传递内核参数,跳转内核。当然除了跳转到内核,U-Boot 本身还实现了其他一些功能(U-Boot 命令),以方便大家进行各种操作。
U-Boot 支持多种架构的多种 CPU,在众多支持的架构中,ARM 是最麻烦的一个。因为 ARM 卖 IP 且市场占有率相当高,导致产生了非常多的 ARM 核心的厂商,这些厂商会有自己的改动,进一步导致了 U-Boot 的 ARM 架构文件夹(./arch/arm
)下有非常多的 mach-xxx
文件夹。
此外,在 U-Boot 中, board
这个词随处可见,U-Boot 中一大部分代码都是与 board 相关的。CPU(SoC、MCU )本身内部资源是有限的,绝大多数情况,内部资源都无法运行操作系统内核。所以,U-Boot 必须要使用很多 CPU 外部的资源,这些资源就在 board 上了。
文档
大约在 2021 年 7 月份,U-Boot 将文档统一迁移到了新版本,整个文档系统的章节内容也进行了重新组织,界面也焕然一新。新的在线文档地址是 https://u-boot.readthedocs.io/en/latest/,旧版的文档已经无法访问了!
新的文档采用的 Sphinx 文档系统搭建的,Sphinx 也是基于 Python 的,使用的是 reStructuredText 语言格式,文件扩展名通常是 .rst
。现在,文档系统也是向 Linux 看齐了。
- 个人认为,U-Boot 的文档缺少对于总体架构的介绍,缺少图示等直观的示例
- U-Boot 的文章仍然在逐步完善中,代码仓库中有很多介绍文档,但是在线文档系统中搜不到
U-Boot 的文档就位于源代码的 doc
目录下。在源码根目录下(不支持在 doc
目录下),使用命令 make htmldocs
构建脚本就可在 doc 目录下自动建立 output
文件夹,然后该目录下生成 HTML 格式的文档(直接使用浏览器打开 index.html
即可)。
由于我的 Ubuntu 22.04 LTS 中的 Sphinx 升级到了 5.0.1 版,其配置与旧版有些不兼容!U-Boot 目前还没有适配,导致无法生成 pdf、epub 等格式的文档。
- 最小依赖工具:
sudo apt install python3-pip
、sudo pip install -U Sphinx sphinx_rtd_theme
、sudo apt install imagemagick
,如果构建其他格式的文档(如 PDF),还需要安装texlive*
等依赖- 根据构建规则,必须先
make xxx_defconfig
后才能正常运行后面的 make 命令,否则报错找不到.config
文件
源码
源代码可以直接使用 Git 命令 git clone https://source.denx.de/u-boot/u-boot.git
来获取。也可以通过 U-Boot 在 Github 上的镜像仓库来获取 git clone https://github.com/u-boot/u-boot
。我们通常是使用某个特定的 Tags 版本:git checkout v2020.10
。
U-Boot 的源码的结构基本也是向 Linux 看齐(其中部分代码就来自于 Linux kernel),只不过没有 Linux 代码那么复杂。如今,U-Boot 源码每天都有大量变更,最新的版本有 1 万 4 千多个文件,近 300 万行代码。源码中各文件的层级结构可以参考下图:
目录文件
下面是对 U-Boot 源代码中各个目录的一个简介:
- api: 供外部应用程序使用的与架构或设备无关的 API。例如,标准化输入输出,显示,网络 API、存储 API 等,为 CMD 提供支持
- arch: 特定于架构的源码文件。实现了不同体系结构的 CPU,指令集、设备树底层抽象,利用链接绑定实现了符号入口相对位置保持不变,故才能实现将内核镜像拷贝到内存然后进行引导的功能
- arc: 通用的架构文件
- arm: ARM 架构
- lib: 实现了初始化 C 运行时环境(栈/堆指针等的初始化)
- dts: 实现了设备树的底层体系架构依赖的具体抽象剥离
- cpu: 不同的 ARM 指令集的 CPU 分开处理
- mach-xxx: 由于同样的内核相同,各家芯片外设都不尽相同,所以将各自个性实现剥离实现于此,这主要体系在 ARM 体系的芯片,由于 ARM 公司售卖 IP,各家芯片厂商在内核的基础上延伸出各自不同的芯片,所以需要将差异性剥离实现
- m68k: m68k 架构
- microblaze: microblaze 架构
- mips: MIPS 架构
- nds32: NDS32 架构
- nios2: Altera NIOS2 架构
- powerpc: PowerPC 架构
- riscv: RISC-V 架构
- sandbox: 独立于硬件的 “sandbox” 模式。U-Boot 可以使用“沙盒”板在 Linux 主机上运行。这允许在原生平台上进行不特定于主板或架构的特性开发。沙盒还用于运行 U-Boot 的一些测试。
- sh: SH 架构
- x86: x86 架构
- xtensa: Xtensa 架构
- board: 开发板依赖文件,实现了产业链下游,设备厂商的差异性,对于产品设计而言,需要将各自在 boot 阶段需要严格初始化的实现放在这里,比如 IO 口的初始化,产品中大部分 IO 口必须显式设置其初始状态
- boot: images and booting 文件
- cmd: U-Boot 命令相关接口
- common: 与架构无关的一些通用文件. 是 U-Boot 主体,如系统停留在 U-Boot 阶段,CPU 始终在执行一个死循环:
run_main_loop()
- configs: 开发板默认的配置文件。格式均为:
开发板名_defconfig
- disk: 磁盘驱动器分区处理的代码.实现了轻量级磁盘管理
- doc: 该目录下是 U-Boot 的文档,现在使用的是 Sphinx 文档系统。Sphinx 也是基于 Python 的,使用的是 reStructuredText 语言格式,文件扩展名通常是
.rst
。
Sphinx 文档系统使用 make 命令来生成发布的文档,可以生成 html、pdf 等格式。例如,在 源码目录/documentation/ 下执行 make html 命令,就会生成一个 _build 的目录,其中就包含了生成的文档。 - drivers: 设备驱动,这里实现了boot阶段必要的设备驱动,如网口、显示等
- dts: 实现了设备树.用于构建 内部 U-Boot fdt 的 Makefile
- env: 环境支持
- examples: 示例代码
- fs: 文件系统代码(cramfs、ext2、jffs2等)
- include: 头文件
- lib: 通用于所有架构的库例程。比如 CRC 算法,加密算法,压缩算法,字符串操作等
- Licenses: 各种许可证文件
- net: 网络代码,实现网络协议层
- post: 上电自检
- scripts: 各种构建脚本和 Makefile 文件。跟 make menuconfig 配置界面的图形绘制相关的文件,我们作为使用者无需关心这个文件夹的内容
- test: 各种单元测试文件
- tools: 里面包含一系列构建 U-Boot 使用的工具的源代码
源文件过滤
由于 U-Boot 源码文件众多,而具体到某一平台(开发板)之后,其中的大多数文件我们根本不需要。为了学习的方便,剔除无用文件,仅仅保留我们需要的文件对于我们学习将有很大帮助。如果可以正常整理出需要的源代码,那基本对于 U-Boot 的文件结构掌握差不多了。以下是在 VSCode 查看代码时的过滤配置:
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"u-boot-v2021.10":true,
// arch
"**/mips": true,
"**/powerpc": true,
"**/riscv": true,
"**/ti": true,
"**/x86": true,
"**/sandbox": true,
"**/arch/{arc,m68k,microblaze,mips,nios2,powerpc,riscv,sandbox,sh,x86,xtensa,um,sparc,s390,parisc,openrisc,nds32,ia64,hexagon,h8300,csky,arm64,alpha}": true,
// cpu
"**/arch/arm/cpu/{arm11,arm720t,arm920t,arm926ejs,arm946es,arm1136,arm1176,armv7,armv8}": true,
// machine
"**/arch/arm/mach-[^s]*": true,
"**/arch/arm/mach-s[^t]*": true,
"**/arch/arm/mach-st[^m]*": true,
"**/arch/arm/mach-stm32[$^m]*": true,
// dts
"**/dts/[^s|^M|^i|^a|^d|^.]*": true,
"**/dts/d[^t]*": true,
"**/dts/i[^n]*": true,
"**/dts/in[^c]*": true,
"**/dts/a[^r]*": true,
"**/dts/ar[^m]*": true,
"**/dts/arm[^v]*": true,
"**/dts/s[^t]*": true,
"**/dts/st[^m|^-]*": true,
"**/dts/stm32[^f]*": true,
"**/dts/stm32f[^7]*": true,
"**/dts/stm32f7[^6|4|^-]*": true,
"**/dts/stm32f769-[^e|d|p]*": true,
// configs
"**/configs/[^s]*": true,
"**/configs/s[^t]*": true,
"**/configs/st[^m]*": true,
"**/configs/stm[^3]*": true,
// "**/configs/stm3[^2]*": true,
"**/configs/stm32[^f|^_]*": true,
"**/configs/stm32f[^7]*": true,
// "**/configs/stm32f7[^6|^4]*": true,
// "**/configs/stm32f769-[^e]*": true,
// board
"**/board/[^s]*": true,
"**/board/s[^t]*": true,
"**/board/ste": true,
"**/board/sto*": true,
"**/board/st/st[^m]*": true,
"**/board/st/stm32[^f]*": true,
// "**/board/st/stm32f[^7]*": true,
// "**/board/st/stm32f7[^6|^4]*": true,
}
}
由于 U-Boot 很多文件是编译过程中产生的,如何过滤有效文件是个问题。我在网上看到有个网友搞了一个可以根据编译过程提取源代码的脚本:https://github.com/tonyho/Generate_Kernel_Uboot_Project_forIDE,但是经过我尝试,发现并不是很准确,但基本可以用。
配置
U-Boot 源码的配置使用的是 Linux 系统的 Kconfig 配置系统,详细的配置过程说明见独立博文 U-Boot 之四 构建过程(Kconfig 配置 + Kbuild 编译)详解 。所有 U-Boot 支持的开发板,都会在 ./configs
目录下有个默认的配置文件,其中的配置项介绍见独立博文U-Boot 之六 最全配置项(CONFIG_BOARD、CONFIG_SYS)详解。
U-Boot 对于众多架构的支持已经到达了开发板级别。对于一些常用的开发板,U-Boot 直接实现了对他们的支持,也就意味着 U-Boot 可以直接在这些开发板上运行。需要注意,开发板可能需要改动才能使用所有资源。
构建
U-Boot 源码的构建过程使用的是 Linux 系统的 Kbuild 构建系统,Kbuild 详细说明见独立博文 U-Boot 之四 构建过程(Kconfig 配置 + Kbuild 编译)详解 。详细的构建过程见独立博文 U-Boot 之一 零基础编译 U-Boot 过程详解、Image 镜像介绍及使用说明、DTB 文件使用说明,简要构建过程如下所示:
- 获取源代码:
git clone https://github.com/ZCShou/BOARD-STM32F769I-EVAL.git
- 生成配置:
ARCH=arm CROSS_COMPILE=arm-none-eabi- make O=build_stm32 stm32f769-eval_defconfig
- 裁剪:
ARCH=arm CROSS_COMPILE=arm-none-eabi- make O=build_stm32 menuconfig
- 编译:
ARCH=arm CROSS_COMPILE=arm-none-eabi- make O=build_stm32 -j$(nproc)
- 将生成的 Image 烧录到开发板验证
调试参数
在博文 U-Boot 之一 零基础编译 U-Boot 过程详解、Image 镜像介绍及使用说明、DTB 文件使用说明 中,我有详细的介绍,这里就不多说了。下面重点介绍几个在构建中有用的配置(或者说参数)。
O=<dir>
:在构建过程中指定该参数就可以将构建过程中生成的文件全部(包括.config
)放到<dir>
这个目录下,从而避免对源码文件产生污染。需要注意的是,每个命令都必须加该参数。V=1
:输出构建过程中的详细信息。开启后,输出内容非常多。NO_LTO=1
:禁用 LTO(Link-time optimisation)。U-Boot 支持链接时间优化,这可以减少最终 U-Boot 二进制文件的大小。目前,ARM 板可以通过在 defconfig 文件中添加CONFIG LTO=y
来启用这一功能。不支持其他体系结构。LTO 默认为沙盒启用。
XIP
构建镜像文件时,XIP 也是我们遇到的一个比较重要的概念。XIP 是 Execute In Place 的缩写,表示的含义就是生成的 Image 可以在 Nor FLASH 上直接运行,而无需要复制到内存运行。
此外,在 Linux Kernel 编译的时候,也是有 XIP 配置项的,如果我们选择了 XIP 功能,则会专门有 xipImage 这种 Image 文件。当然,一般芯片都没有足够的内部 FLASH 来存放 Linux Kernel 镜像文件,所以很少用。
启动阶段
为了适用于各种 CPU,U-Boot 本身的启动阶段划分为了多个不同的阶段:TPL
➜ VPL
➜ SPL
➜ U-Boot
。在源码代码设计中,SPL 其实是一个框架,TPL
和 VPL
都属于 SPL 框架中的一部分。
TPL
TPL(Tertiary Program Loader,第三段程序加载器) 用于早期的初始化,并且尽可能的小,它负责加载 VPL 或 SPL。根据官方文档,TPL 本身属于 SPL 的精简,代码就在 SPL 代码中,通过宏 CONFIG_TPL_BUILD
来区分,而且,现在只有 powerpc 的 mpc85xx 有这个要求并将实现它。
VPL
VPL 是一个可选的安全启动验证过程。从裸机功能上来看,VPL 是一个独立的过程,负责校验 A/B 两个 SPL,并选择正确的来执行;在代码实现上,VPL 也是 SPL 中的一部分。目前,VPL 的细节还在设计实现中,现在它会直接跳转到 SPL。
SPL
SPL(Secondary Program Loader,第二段程序加载器),这里的第二段程序其实就是指的 U-Boot,也就是,SPL 是第一段程序,优先执行,然后他再去加载 U-Boot。那么 U-Boot 本身已经是一个bootloader了,为啥要有 SPL 这个东西的存在呢?
这个主要原因是对于一些 MCU 来说,它的内部 SRAM 可能会比较小,小到无法装载下一个完整的 U-Boot 镜像,那么就需要 SPL,它主要负责初始化外部 RAM 运行环境,并加载真正的 U-Boot 镜像到外部 RAM 中来执行。
用不用 SPL 取决于自己的芯片,如果资源充足,则可用可不用!
SPL 的对象文件被独立构建并放置在 spl
目录中。这里需要注意,构建 SPL 时,会使用 fdtgrep
工具过滤掉设备树中的一些属性,从而生成一个比较小的设备树文件 spl/u-boot-spl.dtb
。
Falcon Mode
Falcon Mode 就是指的由 SPL 直接启动操作系统内核这种模式,主要用于减少 Bootloader 阶段的时间。U-Boot 本身提供了很多功能,对于某些芯片(应用环境)来说,U-Boot 的众多功能基本不会被使用。
U-Boot
U-Boot 阶段包含完整的 U-Boot 功能,例如,引导逻辑、各种 U-Boot 命令。
设备树
U-Boot 使用与 Linux Kernel 相同的设备树,但是,引导加载器环境与 Linux Kernel 的需求并不一样,因此,U-Boot 添加了一些必要的东西。添加的方式不是更改原有的 Linux Kernel 设备树,而是引入了 *-u-boot.dtsi
的 U-Boot 专用文件。U-Boot 增加的 *-u-boot.dtsi
并不会被其他任何 *.dts
文件件所引用,而是直接在 Makefile 文件中引入。
在编译某个 *.dts
文件时,U-Boot 会自动在要编译的 *.dts
所在目录下查找并包含一个(只会包含一个) *-u-boot.dtsi
到 *.dts
中,包含的优先级由高到低如下:
<orig_filename>-u-boot.dtsi # <orig_filename> 就是 .dts 对应的名字
<CONFIG_SYS_SOC>-u-boot.dtsi
<CONFIG_SYS_CPU>-u-boot.dtsi
<CONFIG_SYS_VENDOR>-u-boot.dtsi
u-boot.dtsi
在实际使用中,有时候我们必须要为自己的开发板指定一个私有 xxx.dtsi
,而这个 xxx.dtsi
通常不能公开,因此直接编辑 *.dts
包含 #include xxx.dtsi
是不可取的。此时,U-Boot 提供了 CONFIG_DEVICE_TREE_INCLUDES
这个配置项来指定我们自己的私有 xxx.dtsi
。
设备树源文件被最终编译为二进制的 DTB 文件,原始的 DTB 文件位于 arch/arm/dts/xxx.dtb
下面,构建系统会复制到 ./dts/dt.dtb
,进一步重命名为 ./u-boot.dtb
。u-boot 支持两种形式将 dtb 编译到 u-boot 的镜像中:
-
dtb 和 u-boot 的 bin文件分离
- 需要打开
CONFIG_OF_SEPARATE
宏来使能。 - 在这种方式下,u-boot 的编译和 dtb 的编译是分开的,先生成 u-boot 的 bin 文件,然后再另外生成dtb 文件。
- dtb 最终会自动追加到 u-boot 的 bin 文件的最后面。因此,可以通过 u-boot 的结束地址符号,也就是 _end 符号来获取 dtb 的地址。
- 需要打开
-
dtb 集成到 u-boot 的 bin 文件内部
- 需要打开
CONFIG_OF_EMBED
宏来使能。 - 在这种方式下,在编译 u-boot 的过程中,也会编译 dtb。
- 最终 dtb 是包含到了u-boot 的 bin 文件内部的。dtb 会位于 u-boot 的 .dtb.init.rodata 段中,并且在代码中可以通过 __dtb_dt_begin 符号获取其符号。
官方不推荐这种方式,建议仅用于调试
- 需要打开
-
另外,也可以通过
fdtcontroladdr
环境变量来指定 dtb 的地址。可以通过直接把 dtb 加载到内存的某个位置,并在环境变量中设置fdtcontroladdr
为这个地址,达到动态指定 dtb 的目的。
参考
- U-Boot 官方文档