单片机
返回首页

U-Boot Makefile分析(5)主控Makefile分析

2025-01-24 来源:cnblogs

  这次分析源码根目录下的Makefile,它负责读入配置过的信息,通过OBJS、LIBS等变量设置能够参与镜像链接的目标文件,设定编译的目标等等。


 1 HOSTARCH := $(shell uname -m |

 2     sed -e s/i.86/x86/

 3         -e s/sun4u/sparc64/

 4         -e s/arm.*/arm/

 5         -e s/sa110/arm/

 6         -e s/ppc64/powerpc/

 7         -e s/ppc/powerpc/

 8         -e s/macppc/powerpc/

 9         -e s/sh.*/sh/)

10 

11 HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' |

12         sed -e 's/(cygwin).*/cygwin/')

13 

14 # Set shell to bash if possible, otherwise fall back to sh

15 SHELL := $(shell if [ -x '$$BASH' ]; then echo $$BASH;

16     else if [ -x /bin/bash ]; then echo /bin/bash;

17     else echo sh; fi; fi)

18 

19 export  HOSTARCH HOSTOS SHELL


  上述代码通过shell处理字符数据,得到宿主机的架构、操作系统、shell类型等变量,Makefile中调用shell命令的方式有两种,一种是$(shell cmd_name),另一种是命令前后加上反引号``,如$(shell uname -m)或者`uname -m`。这里可以看到,sed的选项可以是多个,之前不知道还能这么用,学习了。


 1 ifdef O

 2 ifeq ('$(origin O)', 'command line')

 3 BUILD_DIR := $(O)

 4 endif

 5 endif

 6 

 7 ifneq ($(BUILD_DIR),)

 8 saved-output := $(BUILD_DIR)

 9 

10 # Attempt to create a output directory.

11 $(shell [ -d ${BUILD_DIR} ] || mkdir -p ${BUILD_DIR})

12 

13 # Verify if it was successful.

14 BUILD_DIR := $(shell cd $(BUILD_DIR) && /bin/pwd)

15 $(if $(BUILD_DIR),,$(error output directory '$(saved-output)' does not exist))

16 endif # ifneq ($(BUILD_DIR),)

17 

18 OBJTREE     := $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR))

19 SPLTREE     := $(OBJTREE)/spl

20 SRCTREE     := $(CURDIR)

21 TOPDIR      := $(SRCTREE)

22 LNDIR       := $(OBJTREE)

23 export  TOPDIR SRCTREE OBJTREE SPLTREE

24 

25 MKCONFIG    := $(SRCTREE)/mkconfig

26 export MKCONFIG

27 

28 ifneq ($(OBJTREE),$(SRCTREE))

29 REMOTE_BUILD    := 1

30 export REMOTE_BUILD

31 endif


  BUILD_DIR可以通过命令行中O=xxx指定。BUILD_DIR和SRCTREE是相同的,代表native build,REMOTE_BUILD = 0。最后将各个变量输出到全局,TOPDIR在子Makefile中经常使用,它的内容是源码根目录的绝对路径。


  MKCONFIG代表根目录下的mkconfig脚本,这个脚本的内容在前面已经分析过了。


1 all:

2 sinclude $(obj)include/autoconf.mk.dep

3 sinclude $(obj)include/autoconf.mk

  这里出现了all,虽然它的确是我们的目标,但是这里并不是要描述它的依赖和生成命令,而是为了防止sinclude的文件中的某个目标成为默认目标,造成严重的逻辑错误。这是一个小技巧,纠正了我对于Makefile的错误认识:之前认为某个目标的依赖关系必须在一条描述中写完,要是这个目标的多个依赖分散在多处写的话,每一部分依赖都有一条命令与之对应。这是错的,其实目标的依赖关系可以分成多次写完,你只需要在某一处指明要执行的命令就行了,在U-Boot的Makefile中,由源文件生成目标文件的命令都是放在模式规则中描述的,而目标文件对于头文件的依赖是在其他地方描述的,这一点已经在分析具体子Makefile的时候说过了。


  从内容上来说,它从autoconf.mk文件中读入了很多配置信息。


1 include $(obj)include/config.mk

2 export  ARCH CPU BOARD VENDOR SOC

  前面分析mkconfig脚本的时候,我们知道include/config.mk文件的主要内容是指定ARCH CPU SOC VENDOR BOARD等信息,这里又输出到全局,对于后面的配置影响很大。


 1 OBJS  = $(CPUDIR)/start.o

 2 ifeq ($(CPU),x86)

 3 OBJS += $(CPUDIR)/start16.o

 4 OBJS += $(CPUDIR)/resetvec.o

 5 endif

 6 ifeq ($(CPU),ppc4xx)

 7 OBJS += $(CPUDIR)/resetvec.o

 8 endif

 9 ifeq ($(CPU),mpc85xx)

10 OBJS += $(CPUDIR)/resetvec.o

11 endif

12 

13 OBJS := $(addprefix $(obj),$(OBJS))


  重要变量OBJS登场,它表示U-Boot必须要编译链接的那些目标文件,而后面可选的驱动、文件系统、网络配置都放在LIBS变量中描述。OBJS变量中,一定要注意目标文件的顺序,这关系到处理器能否正常工作,倘若你把启动配置cpu的代码放在了镜像文件的后面,而非开始位置,那么一定会出错。


 1 HAVE_VENDOR_COMMON_LIB = $(if $(wildcard board/$(VENDOR)/common/Makefile),y,n)

 2 

 3 LIBS-y += lib/libgeneric.o

 4 LIBS-y += lib/lzma/liblzma.o

 5 LIBS-y += lib/lzo/liblzo.o

 6 LIBS-y += lib/zlib/libz.o

 7 LIBS-$(CONFIG_TIZEN) += lib/tizen/libtizen.o

 8 LIBS-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/lib$(VENDOR).o

 9 LIBS-y += $(CPUDIR)/lib$(CPU).o

10 ifdef SOC

11 LIBS-y += $(CPUDIR)/$(SOC)/lib$(SOC).o

12 endif

13 ifeq ($(CPU),ixp)

14 LIBS-y += drivers/net/npe/libnpe.o

15 endif

16 LIBS-$(CONFIG_OF_EMBED) += dts/libdts.o

17 LIBS-y += arch/$(ARCH)/lib/lib$(ARCH).o

18 LIBS-y += fs/cramfs/libcramfs.o

19     fs/ext4/libext4fs.o

20     fs/fat/libfat.o

21     fs/fdos/libfdos.o

22     fs/jffs2/libjffs2.o

23     fs/reiserfs/libreiserfs.o

24     fs/ubifs/libubifs.o

25     fs/yaffs2/libyaffs2.o

26     fs/zfs/libzfs.o


  首先说一下Makefile中的wildcard函数,它的本意是通配符,但是这里的用途是判断 一个路径是否存在,如果你写的路径不合法,不存在,那么返回空字符串。


  可以看到LIBS-y变量开始收割了,它包含了lib fs arch net drivers等各个目录下的目标文件,因为这些功能都是可选的,因此以lib的形式出现。那么浏览一下它包含了哪些目标文件吧!


  1. lib目录下的通用库文件


  2. arch/arm/cpu/armv7/libarmv7.o        实现armv7的cache等部件的管理。


  3. arch/arm/cpu/armv7/s5pc1xx/libs5pc1xx.o   实现了reset clock等部件的管理。


  4. arch/arm/lib/libarm.o              架构相关的底层库。


  5. fs/下的诸多文件系统


  6. net目录下的libnet.o             网络协议栈相关的库。


  7. drivers/ :block dma fpga gpio i2c input misc mmc mtd net  power pci spi等文件下的libxx.o


1 ifeq ($(SOC),s5pc1xx)

2 LIBS-y += $(CPUDIR)/s5p-common/libs5p-common.o

3 endif

  包含s5p系列soc的公用库,比如timer srom watchdog等。


1 LIBS := $(addprefix $(obj),$(sort $(LIBS-y)))

2 .PHONY : $(LIBS)

4 LIBBOARD = board/$(BOARDDIR)/lib$(BOARD).o

5 LIBBOARD := $(addprefix $(obj),$(LIBBOARD))

  LIBS-y经过排序、清理掉重复目标文件之后,放在新的变量LIBS中,并将它声明为伪目标。


  LIBBOARD := board/samsung/goni/libgoni.o,这也是一个重要的变量。


 1 # Add GCC lib

 2 ifdef USE_PRIVATE_LIBGCC

 3 ifeq ('$(USE_PRIVATE_LIBGCC)', 'yes')

 4 PLATFORM_LIBGCC = $(OBJTREE)/arch/$(ARCH)/lib/libgcc.o

 5 else

 6 PLATFORM_LIBGCC = -L $(USE_PRIVATE_LIBGCC) -lgcc

 7 endif

 8 else

 9 PLATFORM_LIBGCC := -L $(shell dirname `$(CC) $(CFLAGS) -print-libgcc-file-name`) -lgcc

10 endif

11 PLATFORM_LIBS += $(PLATFORM_LIBGCC)

12 export PLATFORM_LIBS


  这里添加LIBGCC,看不明白,暂不研究。


1 __OBJS := $(subst $(obj),,$(OBJS))

2 #__LIBS := $(subst $(obj),,$(LIBS)) $(subst $(obj),,$(LIBBOARD))

4 __LIBS := $(subst $(obj),,$(LIBBOARD)) $(subst $(obj),,$(LIBS))

  重量级变量登场!__OBJS是除去了obj前缀的OBJS,就是start.o。__LIBS则由两部分组成,LIBBOARD在前,LIBS在后,其实官方源码中两者的顺序是相反的。为什么要修改呢?这就跟S5PV210的启动流程有关系了。移植的时候,我们选择修改goni的代码,其中要修改的文件之一是board/samsung/goni/lowlevel_init.S,根据路径可以知道这里的文件都会被链接进libgoni.o中,也就是LIBBOARD变量里,如果它放在LIBS后面,那么lowlevel_init.S中的代码将会在镜像靠后的位置,绝对在16KB以后,而S5PV210在启动的时候会将SD卡的前16KB搬移到iRAM中,这将无法运行我们修改的代码,所以修改。


 1 BOARD_SIZE_CHECK =

 2     @actual=`wc -c $@ | awk '{print $$1}'`;

 3     limit=$(CONFIG_BOARD_SIZE_LIMIT);

 4     if test $$actual -gt $$limit; then

 5         echo '$@ exceeds file size limit:';

 6         echo '  limit:  $$limit bytes';

 7         echo '  actual: $$actual bytes';

 8         echo '  excess: $$((actual - limit)) bytes';

 9         exit 1;

10     fi


  这里就是一个检查镜像大小的函数,如果超出了用户设定的大小就报错,理解起来很简单,不多说,一般可以不用这个功能。


 1 ALL-y += $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map

 2 

 3 ALL-$(CONFIG_NAND_U_BOOT) += $(obj)u-boot-nand.bin

 4 ALL-$(CONFIG_ONENAND_U_BOOT) += $(obj)u-boot-onenand.bin

 5 ALL-$(CONFIG_SPL) += $(obj)spl/u-boot-spl.bin

 6 ALL-$(CONFIG_OF_SEPARATE) += $(obj)u-boot.dtb $(obj)u-boot-dtb.bin

 7 

 8 # enable combined SPL/u-boot/dtb rules for tegra

 9 ifeq ($(SOC),tegra20)

10 ifeq ($(CONFIG_OF_SEPARATE),y)

11 ALL-y += $(obj)u-boot-dtb-tegra.bin

12 else

13 ALL-y += $(obj)u-boot-nodtb-tegra.bin

14 endif

15 endif

16 

17 all:        $(ALL-y) $(SUBDIR_EXAMPLES)


  终于开始描述all的依赖了,all的依赖是ALL-y,可以看到这个变量的内容是u-boot.srec u-boot.bin System.map,另外,我们还可以通过宏来给它增添新的内容:u-boot-nand.bin u-boot-onenand.bin u-boot-spl.bin u-boot.dtb u-boot-dtb.bin。


  上面这段代码的最后就写清楚了all的所有依赖:ALL-y $(SUBDIR_EXAMPLES),看一看这两个变量里面的内容吧。


  ALL-y其实指定的是u-boot镜像的生成格式。u-boot是包含各种调试信息、符号表等元素的镜像文件,可以用于阅读分析,但是不适合在嵌入式平台上烧写,因为文件容量大。u-boot.bin是不包含地址信息的二进制指令数据流,文件容量小,我们一般使用这个进行烧写。u-boot.srec,这是一种S-Record格式的文件,是由Motorola公司推出的一种用于flash烧写的镜像文件格式,每一行都由S字母开头,后面有类型字段、地址字段、数据字段以及校验和等等。u-boot.hex,这也是一种烧写flash的镜像文件格式,它每行都由0x3a这个字节开始,后面有一字节的行长度字段、两字节的地址字段、一字节的数据类型字段、数据以及校验和。u-boot里还支持生成其他格式的镜像文件,但是我暂时用不着,先不分析。


  SUBDIR_EXAMPLES指向了examples目录下的文件,主要生成一些测试文件,暂不深究。


  我们关注的是u-boot.bin文件,因此看一下它的依赖:


1 $(obj)u-boot:   depend

2         $(SUBDIR_TOOLS) $(OBJS) $(LIBBOARD) $(LIBS) $(LDSCRIPT) $(obj)u-boot.lds

3         $(GEN_UBOOT)

  u-boot的依赖有7个:depend SUBDIR_TOOLS OBJS LIBBOARD LIBS LDSCRIPT u-boot.lds,生成的命令是$(GEN_UBOOT),下面逐个看一下。


1 # Explicitly make _depend in subdirs containing multiple targets to prevent

2 # parallel sub-makes creating .depend files simultaneously.

3 depend dep: $(TIMESTAMP_FILE) $(VERSION_FILE)

4         $(obj)include/autoconf.mk

5         $(obj)include/generated/generic-asm-offsets.h

6         $(obj)include/generated/asm-offsets.h

7         for dir in $(SUBDIRS) $(CPUDIR) $(LDSCRIPT_MAKEFILE_DIR) ; do

8             $(MAKE) -C $$dir _depend ; done


  depend、dep两个目标拥有同样的依赖关系,执行相同的命令。不难发现depend的命令中并没有生成depend这个文件,因此它的作用相当于一个伪目标,每次执行make命令,都会发现depend目标不存在,接着就开始检查depend的各个依赖。更新depend目标时,也是更换到其他路径下执行make _depend命令,结合前面对于rules.mk的分析知道,_depend也是一个伪目标,用于生成某个文件夹下的所有文件依赖关系,这里很绕,慢慢体会。下面看一看depend的依赖。


  include/autoconf.mk这个文件中有很多重要的配置信息,它的依赖关系如下所示。


1 $(obj)include/autoconf.mk: $(obj)include/config.h

2     @$(XECHO) Generating $@ ;

3     set -e ;

4     : Extract the config macros ;

5     $(CPP) $(CFLAGS) -DDO_DEPS_ONLY -dM include/common.h |

6         sed -n -f tools/scripts/define2mk.sed > $@.tmp &&

7     mv $@.tmp $@


  include/autoconf.mk文件的依赖是include/config.h,config.h是执行make s5p_goni_config的时mkconfig脚本自动生成的文件,所以说,变更配置才会更新autoconf.mk文件,这也进一步说明autoconf.mk文件确实是用来描述当前配置选项的。


  分析一下上面用到的各条命令,第一句就是echo Generating include/autoconf.mk。第二句,set -e的作用是监测当前进程中命令的执行情况,当有命令执行出错时直接退出,这里的关键字是“当前进程”,如果你以 “./example.sh”方式执行某个脚本时出现的错误并不能被捕捉,因为这种方式的调用是以子进程的方式执行的,所以应该使用“source example.sh”或者“ .  example.sh”的方式。第三句,cpp -dM include/common.h | sed -n -f tools/scripts/define2mk.sed > include/autoconf.mk.tmp && mv include/autoconf.mk.tmp include/autoconf.mk,cpp是gcc的预处理器,-dM选项会输出很多预处理信息,比如宏定义,类型定义等等,而且它会递归地解析你包含的头文件,比如说这里的include/common.h文件中有一句#include ,那么cpp -dM include/common.h不但解析common.h文件中的内容并输出,而且会对config.h文件做相同的处理,显然,这条命令执行之后,就输出了全部的配置信息。接着通过管道操作,将这些信息用sed命令处理成特定的格式,最终输出到autoconf.mk文件中。


  TIMESTAMP_FILE和VERSION_FILE


1 $(TIMESTAMP_FILE):

2         @mkdir -p $(dir $(TIMESTAMP_FILE))

3         @LC_ALL=C date +'#define U_BOOT_DATE '%b %d %C%y'' > $@.tmp

4         @LC_ALL=C date +'#define U_BOOT_TIME '%T'' >> $@.tmp

5         @cmp -s $@ $@.tmp && rm -f $@.tmp || mv -f $@.tmp $@


 1 $(VERSION_FILE):

 2         @mkdir -p $(dir $(VERSION_FILE))

 3         @( localvers='$(shell $(TOPDIR)/tools/setlocalversion $(TOPDIR))' ;

 4            printf '#define PLAIN_VERSION '%s%s'n'

 5             '$(U_BOOT_VERSION)' '$${localvers}' ;

 6            printf '#define U_BOOT_VERSION 'U-Boot %s%s'n'

 7             '$(U_BOOT_VERSION)' '$${localvers}' ;

 8         ) > $@.tmp

 9         @( printf '#define CC_VERSION_STRING '%s'n'

10          '$(shell $(CC) --version | head -n 1)' )>>  $@.tmp

11         @( printf '#define LD_VERSION_STRING '%s'n'

12          '$(shell $(LD) -v | head -n 1)' )>>  $@.tmp

13         @cmp -s $@ $@.tmp && rm -f $@.tmp || mv -f $@.tmp $@


1 TIMESTAMP_FILE = $(obj)include/generated/timestamp_autogenerated.h

2 VERSION_FILE = $(obj)include/generated/version_autogenerated.h

  上述两个变量其实是include/genetated下的两个文件。


  timestamp生成的过程:提取TIMESTAMP_FILE路径,建立路径对应的文件夹,将date命令的输出信息重定位到表示时间戳的、后缀为.tmp的文件中,接着比较原有的时间戳文件和这个tmp文件的内容,如果相同,那么删除刚刚生成的文件,否则以tmp文件更换之前的时间戳文件。


  version生成的过程:提取VERSION_FILE路径,建立路径对应文件夹,将主Makefile开始定义的U-Boot version信息、gcc版本信息、ld版本信息都写到tmp结尾的文件中,接着比较version源文件和之前version文件,相同则删除tmp,否则覆盖原文件。


  其实Makefile中将以上两个变量声明为伪目标,因此每次执行make命令,都会在更新depend的时候更新时间戳文件和版本文件。


  depend的另外两个依赖generic-asm-offsets.h asm-offsets.h不是特别重要,暂不分析。


1 SUBDIR_TOOLS = tools

1 tools: $(VERSION_FILE) $(TIMESTAMP_FILE)

2     $(MAKE) -C $@ all

  SUBDIR_TOOLS是u-boot的第二个依赖,它的内容是tools这个文件夹,依赖是时间戳和版本文件,执行的命令是到tools文件夹下执行make,显然意思是到编译tools下面的各个文件,生成各种工具。


 1 $(OBJS):    depend

 2         $(MAKE) -C $(CPUDIR) $(if $(REMOTE_BUILD),$@,$(notdir $@))

 3 

 4 $(LIBS):    depend $(SUBDIR_TOOLS)

 5         $(MAKE) -C $(dir $(subst $(obj),,$@))

 6 

 7 $(LIBBOARD):    depend $(LIBS)

 8         $(MAKE) -C $(dir $(subst $(obj),,$@))

 9 

10 $(SUBDIRS): depend

11         $(MAKE) -C $@ all

12 

13 $(SUBDIR_EXAMPLES): $(obj)u-boot

14 

15 $(LDSCRIPT):    depend

16         $(MAKE) -C $(dir $@) $(notdir $@)

17 

18 $(obj)u-boot.lds: $(LDSCRIPT)

19         $(CPP) $(CPPFLAGS) $(LDPPFLAGS) -ansi -D__ASSEMBLY__ -P - <$^ >$@


  OBJS就是arch/arm/cpu/armv7/start.o,要生成它,就需要到CPUDIR下面执行make,这很容易理解。说一下makefile的函数notdir吧,它的意思是去除所有的地址信息,仅留下最后一个/符号后面的文件名字,也就是取文件名操作。与之对应的dir函数,则实现去除文件名,取路径名的作用。


  LIBS包含arch cpu soc board等各个层次上的公用库,文件系统库,还有各种驱动库。这其实是一个多目标的依赖关系描述,不过各目标的依赖相同,生成规则统一,都是到目标变量所在路径下去执行make。详细点,就是make -C drivers/bios_emulator,make -C drivers/block... ...


  LIBBOARD指的是board/samsung/goni/libgoni.o,生成的命令很简单,到BOARD_DIR目录下执行make即可。


  LDSCRIPT内容是arch/arm/cpu/u-boot.lds,此条依赖对应的命令是make -C arch/arm/cpu u-boot.lds,在arch/arm/cpu/目录下的确能看到链接文件,但是并没有makefile,这一点不能理解。


  u-boot的依赖文件就分析到这里,下面看一下u-boot的生成过程:


 1 ifeq ($(CONFIG_SANDBOX),y)

 2 GEN_UBOOT =

 3         cd $(LNDIR) && $(CC) $(SYMS) -T $(obj)u-boot.lds

 4             -Wl,--start-group $(__LIBS) -Wl,--end-group

 5             $(PLATFORM_LIBS) -Wl,-Map -Wl,u-boot.map -o u-boot

 6 else

 7 GEN_UBOOT =

 8         UNDEF_SYM=`$(OBJDUMP) -x $(LIBBOARD) $(LIBS) |

 9         sed  -n -e 's/.*($(SYM_PREFIX)__u_boot_cmd_.*)/-u1/p'|sort|uniq`;

10         cd $(LNDIR) && $(LD) $(LDFLAGS) $(LDFLAGS_$(@F)) $$UNDEF_SYM $(__OBJS)

11             --start-group $(__LIBS) --end-group $(PLATFORM_LIBS)

12             -Map u-boot.map -o u-boot

13 endif


  这里只看核心部分:$(LD) $(LDFLAGS) $(__OBJS) --start-group $(__LIBS) --end-group $(PLATFORM_LIBS) -Map u-boot.map -o u-boot,简而言之,就是将__OBJS __LIBS __PLATFORM_LIBS链接成u-boot,写到这里,就很容易理解了。


  现在回到u-boot.bin。


1 $(obj)u-boot.bin:   $(obj)u-boot

2         $(OBJCOPY) ${OBJCFLAGS} -O binary $< $@

3         $(BOARD_SIZE_CHECK)

  u-boot.bin依赖u-boot,生成的命令是arm-linux-objcopy,接着还检查了镜像文件的大小,很简单的过程,不多解释


  顺带提一句u-boot.hex,其实就是使用arm-linux-objcopy的时候改变一下控制参数而已,将binary改成ihex。u-boot.srec的生成就是将控制参数替换成srec。


  对U-Boot Makefile的初步探索终于到头了,这个过程虽然很粗略,但确实纠正了一些makefile不准确的认识,学到了不少makefile以及shell的知识,并对u-boot镜像文件的建立有了全局的了解。


进入单片机查看更多内容>>
相关视频
  • 【TI MSPM0 应用实战】智能小车+工业角度编码器+血氧仪+烟雾探测器!硬核参考设计详解!

  • 2022 Digi-Key KOL 系列: 你见过1GHz主频的单片机吗?Teensy 4.1开发板介绍

  • TI 新一代 C2000™ 微控制器:全方位助力伺服及马达驱动应用

  • MSP430电容触摸技术 - 防水Demo演示

  • 直播回放: Microchip Timberwolf™ 音频处理器在线研讨会

  • 基于灵动MM32W0系列MCU的指夹血氧仪控制及OTA升级应用方案分享

精选电路图
  • 1瓦线性调频增强器

  • 家用电器遥控器

  • 12V 转 28V DC-DC 变换器(基于 LM2585)

  • 红外开关

  • DS1669数字电位器

  • HA1377 桥式放大器 BCL 电容 17W(汽车音频)

    相关电子头条文章