上一节U-Boot学习(1):简介及命令行指令详解中,介绍了如何使用U-Boot。我们知道一个U-Boot可能要适配不同的硬件,所以不同的硬件就有不同的配置,配置后就可以编译U-Boot,最终生成镜像。U-Boot如何编译,以什么规则编译,编译后下载到内存中的哪里呢,这一切都在Makefile中。所以本节课就来分析一下U-Boot的Makefile的结构。
1、官方源码
U-Boot官方的源码在Github上有对应的镜像:https://github.com/u-boot/u-boot,大家可以在这里下载源码。
2、半导体产商适配源码
前面说了,U-Boot主要是一个引导的操作,但是对于不同半导体产商的芯片来说,它们的接口类型和初始化方式都有差异,虽然U-Boot源码中做了一定的适配,但是不如半导体产商自身做的适配完善,比如NXP就提供了I.MX系列Cortex-A核芯片的U-Boot适配,可以在下面的地址中下载:https://github.com/nxp-imx/uboot-imx。
这里面提供的U-Boot基本上就是基于官方的开发板进行了适配,也就是说我们使用同一款芯片的话,假设Flash可以接到多个Flash接口上,可能官方开发板接在接口1,而你的产品接到了接口2,这时候你就可以在官方源码的基础上更改一下这个接口。如果需要网络启动,那对于以太网接口的初始化也是一样的,要适配你自己的开发板的硬件连接。
解压U-Boot源码后,常见的文件夹的作用如下:
目录 | 描述 |
---|---|
/arch | 体系结构特定的文件 |
/arch/arm | 适用于ARM体系结构的通用文件(该目录下除了ARM架构外还有其他架构的,这里省略) |
/api | 面向外部应用的机器/体系结构无关的API |
/board | 依赖于板级别的文件 |
/boot | 支持镜像和引导 |
/cmd | U-Boot命令函数 |
/common | 各体系结构通用的杂项函数 |
/configs | 板默认配置文件 |
/disk | 处理磁盘驱动分区的代码 |
/doc | 文档(一组ReST和README文件的混合) |
/drivers | 设备驱动程序 |
/dts | 用于构建内部U-Boot fdt的Makefile |
/env | 环境支持 |
/examples | 独立应用程序示例代码等 |
/fs | 文件系统代码(cramfs,ext2,jffs2等) |
/include | 头文件 |
/lib | 所有体系结构通用的库例程 |
/Licenses | 各种许可文件 |
/net | 网络代码 |
/post | 自检 |
/scripts | 各种构建脚本和Makefile |
/test | 各种单元测试文件 |
/tools | 用于构建和签名FIT镜像等的工具 |
这里我们以NXP官方提供的U-Boot源码为例进行分析,我这就下载了当前这个仓库的默认分支if_v2022.04
。下载解压后,目录结构如下:
(1)不同开发板的配置
我们现在想要编译U-Boot,首先我们来看看README中怎么说:
也就是说在configs
目录下有对应不同的配置文件,不同的配置文件对应不同的开发板,我们根据自己的需求选择一个配置文件进行make
,这里我就选择目录下的imx6ul_isiot_emmc_defconfig
。在执行指令之前,需要安装以下四个库
sudo apt install make
sudo apt install gcc
sudo apt install bison
sudo apt install flex
现在我们执行make imx6ul_isiot_emmc_defconfig
,看一下执行的结果:
此时目录下会生成一个.config
文件:
实际上就是一些宏定义,一会编译U-Boot的时候肯定会用到这些宏定义。
(2)交叉编译器的配置
继续看README:
也就是说我们要指定用什么交叉编译器来编译U-Boot。
这里我下载了gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf.tar.xz
,把交叉编译器解压到了/usr/local/arm/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf
,然后需要在/etc/profile
的最后添加环境变量:export PATH=$PATH:/usr/local/arm/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf/bin
,最后输入source /etc/profile
让环境变量立即生效(每次打开终端都要输入一次,要一直生效需要重启系统)。
现在我们就直接在U-Boot的Makefile中修改CROSS_COMPILE
变量为我们交叉编译器的执行名前缀:
现在就可以直接在U-Boot目录下输入make
进行编译,由于编译过程比较久,我们可以指定-j12
,表示使用12个线程编译以加快编译速度。输入make -j12
,但是会报错:
这是因为U-Boot的部分文件使用了OpenSSL协议,我们需要安装相关库:
sudo apt install libssl-dev
然后我们还会遇到问题:
这里我们不使用LDO旁路检测,直接在.config
文件中注释掉CONFIG_LDO_BYPASS_CHECK
即可。
现在就编译成功了,目录下多了很多的文件
前面我们虽然编译出来了u-boot.bin,但是这个文件的链接地址是否和我们开发板的RAM相对应呢,还有刚刚的make XXXconfig
完成了什么操作,.config
中的内容代表什么呢。这些都与Makefile有关,所以现在我们就来分析一下Makefile。
由前面的介绍我们知道,在configs
目录下有对应不同的配置文件,前面我们选择了make imx6ul_isiot_emmc_defconfig
这个配置,我们来看一下Makefile中对应的生成规则:
%
为通配符,即匹配了所有make xxxdeconfig
展开来实际上是:
make -f ./scripts/Makefile.build obj=scripts/kconfig imx6ul_isiot_emmc_defconfig
这句make
指令使用了-f
选项指定了要使用的Makefile文件的路径,即./scripts/Makefile.build
。然后,通过obj=scripts/basic
传递了一个变量obj
和我们的目标名。
1、outputmakefile
PHONY += outputmakefile
outputmakefile:
ifneq ($(KBUILD_SRC),)
$(Q)ln -fsn $(srctree) source
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile $(srctree)
endif
可以看到如果KBUILD_SRC
为空的话,才会执行下面的指令,但是实际上KBUILD_SRC
是为空的,文档中有以下两句:
# KBUILD_SRC is set on invocation of make in OBJ directory
# KBUILD_SRC is not intended to be used by the regular user (for now)
其中第一句指出KBUILD_SRC
是在调用make时设置的(如果不设置的话就使用当前目录)。
2、FORCE
PHONY += FORCE
FORCE:
这里FORCE
被声明为.PHONY
(伪目标),所以如果FORCE
是其他目标的依赖,那么无论FORCE
是否实际上被修改,这些目标都将始终被认为是需要重新生成的。因此,make将忽略文件时间戳,总是认为.PHONY
目标需要重新生成。
3、scripts_basic
# Basic helpers built in scripts/
PHONY += scripts_basic
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
$(Q)rm -f .tmp_quiet_recordmcount
# To avoid any implicit rule to kick in, define an empty command.
scripts/basic/%: scripts_basic ;
我们在Makefile中搜索Q
、MAKE
、build
等变量,展开后就是:
make -f ./scripts/Makefile.build obj=scripts/basic
rm -f .tmp_quiet_recordmcount
这里的make
和前面的%config
的语句很像,最终还是执行./scripts/Makefile.build
这个Makefile。
这里就不继续往下分析了,Makefile.build
和Linux内核的Makefile.build
很像,可以参考前面分享的Makefile介绍的文章。这里就直接说结论:scripts_basic
会生成fixdep
应用。
这个应用是用来生成头文件依赖的,下面来介绍一下相关的知识。
在编译过程中,使用-MD
选项可以生成依赖关系文件。这些文件包含了源代码文件和它们之间的依赖关系,通常以 Makefile 的规则格式保存,这样可以在后续的编译过程中更好地管理文件的依赖关系。这在大型项目中是很有用的,因为它可以确保在修改一个源文件后,只重新编译与之相关的文件,而不是整个项目。
下面是一个简单的例子,假设有一个C语言项目,包含三个源文件:main.c
,utils.c
,和header.h
。main.c
包含了main
函数,utils.c
包含了一些工具函数,而header.h
包含了函数的声明。
假设我们使用GCC编译器,可以通过以下命令使用-MD
选项生成依赖关系文件:
gcc -MD -o main main.c utils.c
这个命令会生成main.d
文件,内容可能如下:
main.o: main.c header.h
utils.o: utils.c header.h
这表示在编译main.c
时,依赖于main.c
和header.h
;在编译utils.c
时,依赖于utils.c
和header.h
。这样,如果修改了header.h
,只有依赖于它的文件会重新编译,而不是整个项目。
这对于Makefile来说非常有用,因为它们可以根据这些依赖关系文件来判断哪些文件需要重新编译,从而提高编译效率。
fixdep
是一个用于生成Makefile依赖关系的工具。它主要用于处理头文件之间的依赖关系,以确保在构建U-Boot时,当某个头文件发生变化时,只重新编译与之相关的文件,而不是整个项目。具体来说,fixdep
主要完成以下任务:
#include
指令:fixdep
分析源代码文件,找到其中包含的头文件。#include
指令,fixdep
生成一个包含依赖关系的 Makefile 规则。这些规则通常包含了源文件和其所依赖的头文件,以及它们之间的关系。fixdep
将生成的 Makefile 规则输出到标准输出或指定的文件中,以便后续的构建工具(如Make)使用。例如,在U-Boot的Makefile中可能包含类似如下的使用fixdep
的命令:
depend:
$(SRCTREE)/scripts/fixdep $(CFLAGS) $(CPPFLAGS) $(SRCARCH) $(SRC) > .depend
这个命令使用fixdep
生成头文件依赖关系,并将结果保存到.depend
文件中。在Makefile的其他地方可以包含这个文件,以便Make工具能够使用这些依赖关系。
前面的%config
目标最终会执行:
scripts/kconfig/conf --defconfig=arch/../configs/imx6ul_isiot_emmc_defconfig Kconfig
我们可以通过执行make imx6ul_isiot_emmc_defconfig V=1
看到:
在U-Boot中,scripts/kconfig/conf
是Kconfig工具的配置脚本,用于处理配置文件和生成配置信息。Kconfig是Linux内核中使用的配置系统,也被U-Boot采用,用于管理项目的配置选项。conf
脚本会根据imx6ul_isiot_emmc_defconfig
中的配置生成一个配置文件.config
,其中包含了用户所做的配置选项。这个文件会被后续的构建过程使用,以根据用户的配置生成相应的代码。