makefile简单初探索_2 结合bsp

makefile简单初探索_2 结合bsp

Keruone
系统 ubuntu20.04
参考正点原子

只是学习中自己的小记

某种意义上来说也是上一篇的后续


在上篇文章中,我们编写了一个很小的 makefile 也算是作为了 makefile 的小小入门。
但是在上篇文章中,还有几个不可避免的问题:

    1. 编译器可能也会更换,尤其是在交叉编译场景下,不同架构需要不同工具链
    1. 文件不可能永远只有那么几个,随着你的工程项目变大,文件肯定也越来越多,手动列举每个文件会非常繁琐
    1. 汇编过程后的 .o 文件不可能这么随意的放在当前工作环境的根目录,这样太杂乱了,不利于项目管理和清理
    1. 头文件和源文件可能分散在不同目录,需要统一指定搜索路径才能避免编译错误

1. 工具链统一配置:打造通用 “烹饪工具”

首先,为了解决编译器更换的问题,我们可以通过变量统一管理编译工具链,尤其是在嵌入式开发中常用的交叉编译场景。

CROSS_COMPILE ?= arm-linux-gnueabihf-
TARGET ?= keyc

CC 		:=	$(CROSS_COMPILE)gcc
LD 		:=	$(CROSS_COMPILE)ld
OBJCOPY	:=	$(CROSS_COMPILE)objcopy
OBJDUMP	:=	$(CROSS_COMPILE)objdump

:=:在 Makefile 中,:= 表示 “立即赋值”,即变量的值在定义时就会被确定,后续即使修改了赋值语句中依赖的其他变量,该变量的值也不会再改变,这种方式可以避免变量值的延迟解析导致意外问题。

这里使用 ?= 赋值,意味着如果外部已经定义了 CROSS_COMPILE(比如通过命令行传入),就会使用外部定义的值,否则使用默认的 arm-linux-gnueabihf-。这样一来,当需要更换编译器(比如切换到其他架构的工具链)时,只需修改 CROSS_COMPILE 变量即可,无需逐个修改后面的编译工具定义,极大提升了灵活性。
TARGET 变量则用于统一指定最终生成的目标文件名,后续所有生成文件(如 .bin.elf.dis)都会基于这个变量命名,方便整体修改项目名称。


2. 路径集中管理:打点食材来源

前面我们已经配置好编译工具链这些 “烹饪工具”,接下来就要整理项目所需的 “食材”—— 头文件和源文件的路径。实际项目中,文件常按功能模块分散存放,统一管理路径能让编译过程更有序,也便于后续维护。

  • 首先是头文件路径定义:

    INCDIRS	:=	imx6u		\
    		bsp/clk		\
    		bsp/delay	\
    		bsp/led		\
    		bsp/beep	\
    		bsp/key		\
    		bsp/gpio	\
    		project
    

    INCDIRS 变量集中存放所有头文件(.h)的所在目录:imx6u 目录通常存放芯片相关的底层头文件,bsp 下的子目录(clk、delay 等)对应时钟、延时、LED 等外设的驱动头文件,project 目录则用于存放项目业务逻辑相关的头文件。

    每行末尾的 \ 是续行符,用于将长路径列表拆分成多行,提升代码可读性。

  • 接着是源文件路径定义:

    SRCDIRS	:=	project		\
    		bsp/clk		\
    		bsp/delay	\
    		bsp/led		\
    		bsp/beep	\
    		bsp/key		\
    		bsp/gpio
    

    SRCDIRS 变量记录所有源文件(.s 汇编文件、.c C 语言文件)的所在目录,与头文件路径一一对应,按功能模块分类存放。这样后续编译时,Makefile 能根据这些路径自动查找需要编译的文件,无需手动逐个指定;若后续新增功能模块,只需在该变量中补充对应目录即可,扩展性极强。

通过这种集中管理方式,我们将分散的文件路径 “收纳” 整齐,为后续自动搜索文件、生成编译列表做好了铺垫。


3. 食材预处理:自动生成编译所需的文件列表

有了头文件和源文件的路径(即 "食材存放位置"),接下来需要对这些 "食材" 进行预处理 —— 也就是通过 Makefile 函数自动生成编译过程中需要的文件列表和路径参数,避免手动逐个指定文件的繁琐操作。

3.1 头文件路径格式转换

首先,我们在使用 gcc 编译时,引用头文件是常态,但编译器默认只会在当前目录和系统默认目录找头文件。如果头文件在其他目录(比如我们定义的 imx6ubsp/clk),就必须通过 -I 选项告诉编译器 "去这个目录找头文件"。
到目前为止,我们只有 INCDIRS 里的相对路径,没有 -I 前缀,编译器无法识别。其实解决这个问题不用绕弯,只要给每个路径前面加上 -I 即可 —— 而 Makefile 的 patsubst 函数正好能帮我们实现批量替换。

  • 其中 patsubst 函数语法如下
    $(patsubst <pattern>,<replacement>,<text>)
    
    • 首先,括号内的 patsubst 为该函数的函数名。
    • <text> 为 需要处理的 集合
    • <pattern> 为 对 <text> 集合中各元素的匹配条件
    • <replacement> 为 对 符合条件的元素 的替换方案

对此,我们就可以很好的利用这个函数,来达到批量添加 -I 的操作:

INCLUDE :=	$(patsubst %, -I %, $(INCDIRS))
  • 处理逻辑:% 是通配符,匹配 INCDIRS 中的每一个路径(比如 imx6u、bsp/clk);-I % 表示将匹配到的每个路径替换为 "-I + 该路径"。
  • 举个例子:如果 INCDIRS 里有 imx6ubsp/clk,最终 INCLUDE 会变成 -I imx6u -I bsp/clk,后续传给 gcc 后,编译器就知道要去这两个目录搜索头文件了。

3.2 源文件列表自动收集

接下来,项目中源文件会分散在 SRCDIRS 的各个目录下,随着文件数量增多,手动列举每个 .s(汇编文件)和 .c(C 语言文件)既繁琐又容易遗漏。我们需要一个能自动遍历所有指定目录、批量收集目标文件的方案 —— 这就需要结合 Makefile 的 foreachwildcard 两个函数来实现。

  • 首先介绍 foreach 函数,语法如下
    $(foreach <var>,<list>,<text>)
    
    • 括号内的 foreach 为该函数的函数名。
    • <list> 为需要遍历的集合(比如我们定义的 SRCDIRS 路径列表)。
    • <var> 为循环变量,用于接收 <list> 中逐一取出的元素。
    • <text> 为循环体,每次遍历会将 <var> 代入 <text> 中执行表达式。
  • 再介绍 wildcard 函数,语法如下
    $(wildcard <pattern>)
    
    • 括号内的 wildcard 为该函数的函数名。
    • <pattern> 为文件匹配模式(支持通配符 *)。
    • 核心作用:匹配指定模式的所有文件路径,返回以空格分隔的文件名列表。

结合这两个函数,我们就能实现源文件的自动收集,具体代码如下:

# 包含路径的各个源码文件
SFILES	:=	$(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.s))
CFILES	:=	$(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
  • 处理逻辑:
    1. 先执行 foreach dir, $(SRCDIRS), ...:遍历 SRCDIRS 中的每个目录,将当前目录赋值给循环变量 dir(比如第一次取 project,第二次取 bsp/clk)。
    2. 再执行 wildcard $(dir)/*.s:针对当前 dir,匹配该目录下所有 .s 后缀的文件($(dir)/*.c 同理匹配 .c 文件),返回带完整路径的文件名(比如 project/start.sbsp/led/led.c)。
    3. 循环结束后,将所有目录下的目标文件汇总成 SFILES.s 文件列表)和 CFILES.c 文件列表)。
  • 举个例子:如果 SRCDIRS 包含 projectbsp/keySFILES 会自动收集 project/*.sbsp/key/*.s 所有文件,无需手动添加新文件路径。

3.3 去除文件路径:提取文件名

现在我们已经通过 SFILESCFILES 整理好了所有源文件的"来源路径"(比如 bsp/key/key.cproject/start.s),但后续编译生成的 .o 文件需要统一放在 obj 目录集中管理,此时文件的路径前缀反而成了多余——我们只需要文件名本身,就能对应生成 obj/key.o 这样的目标文件。

因此,我们需要剥离文件路径中的目录部分,只保留纯文件名——这可以通过 Makefile 的 notdir 函数实现。

  • 其中 notdir 函数语法如下
    $(notdir <names>)
    
    • 括号内的 notdir 为该函数的函数名。
    • 为需要处理的文件路径列表(可以是单个路径或多个路径集合)。
    • 核心作用:剥离文件路径中的目录部分,只保留纯文件名。

具体代码实现如下:

# 无路径的各个源码文件
SFILES_NO_DIR	:=	$(notdir $(SFILES))
CFILES_NO_DIR	:=	$(notdir $(CFILES))
  • 处理逻辑:将 SFILESCFILES 中带路径的文件列表传入 notdir 函数,该函数会自动剥离所有目录前缀,只保留文件名。
  • 举个例子:bsp/key/key.c 会被剥离成 key.cproject/start.s 会被剥离成 start.s,最终得到只包含纯文件名的列表 SFILES_NO_DIRCFILES_NO_DIR,为后续统一存放 .o 文件做好准备。

3.4 生成 .o 文件路径:统一存放目标文件

我们已经提取出了源文件的文件名,接下来编译过程会生成 .o 目标文件。这些目标文件不能再随意散放在根目录,否则会让项目结构杂乱无章。因此,我们需要将所有目标文件统一放在 obj 目录下。

要实现这个需求,需要结合 patsubst 函数(语法同 3.1)和 Makefile 自带的后缀替换语法,既完成文件名后缀的转换,又添加统一的目录前缀。

  • 其中 patsubst 函数语法如下(复习)
    $(patsubst <pattern>,<replacement>,<text>)
    
    • 括号内的 patsubst 为该函数的函数名。
    • 为需要处理的集合(这里是纯文件名列表)。
    • 为对 集合中各元素的匹配条件(用通配符 % 匹配所有文件名)。
    • 为对符合条件的元素的替换方案(这里是添加 obj/ 目录前缀)。

具体代码实现如下:

# 源码文件对应的目标文件 .o
SOBJS	:=	$(patsubst %, obj/%, $(SFILES_NO_DIR:.s=.o))
COBJS	:=	$(patsubst %, obj/%, $(CFILES_NO_DIR:.c=.o))
OBJS	:= 	$(SOBJS) $(COBJS)
  • 处理逻辑:
    1. 先做后缀替换:SFILES_NO_DIR:.s=.o 表示将纯文件名列表中所有 .s 后缀(汇编文件)替换为 .o 后缀(比如 start.sstart.o);CFILES_NO_DIR:.c=.o 同理,将 .c 后缀(C语言文件)替换为 .o 后缀(比如 led.cled.o)。
    2. 再用 patsubst 函数添加路径前缀:patsubst %, obj/%, ... 中,% 通配符匹配每个转换后的 .o 文件名,将其替换为 obj/ + 文件名(比如 start.oobj/start.o)。
    3. 最后用 OBJS 变量汇总所有目标文件的完整路径,后续链接时直接调用这个列表即可。
  • 举个例子:SFILES_NO_DIR 中的 start.s 最终会变成 obj/start.oCFILES_NO_DIR 中的 led.c 会变成 obj/led.o,所有 .o 文件都集中存放在 obj 目录。

3.5 配置依赖文件搜索路径:告诉 Make 去哪里找源文件

现在我们已经整理好了源文件的存放路径(SRCDIRS)、收集了带路径的源文件列表(SFILESCFILES),也提取了文件名本身,但 Make 工具默认只会在当前目录查找源文件(.s.c)。如果源文件不在当前目录,直接编译会提示"找不到文件"。

因此,我们需要明确告诉 Make 工具源文件的具体存放位置,让它能在 SRCDIRS 的各个目录中找到需要的源文件,无需手动写完整路径。这可以通过 Makefile 的内置变量 VPATH 实现。

这里需要特别说明一个关键区别:前面定义的 INCLUDE 变量和现在的 VPATH 看似都是"路径配置",但作用对象和场景完全不同,缺一不可:

  • INCLUDE(带 -I 前缀的头文件路径):是给 编译器(gcc) 用的。编译器在编译 .c/.s 文件时,会遇到 #include "xxx.h" 这样的头文件引用,此时需要通过 -I 路径告诉编译器"去哪里找这些 .h 头文件"。
  • VPATH(源文件搜索路径):是给 Make 工具 用的。Make 在执行编译规则时,需要找到依赖的 .c/.s 源文件(比如编译 obj/led.o 时需要依赖 led.c),此时需要通过 VPATH 告诉 Make"去哪里找这些 .c/.s 源文件"。

简单说:INCLUDE 解决"编译器找头文件(.h)"的问题,VPATH 解决"Make 找源文件(.c/.s)"的问题,二者针对不同工具、不同文件类型,是两个完全独立的路径配置,不能相互替代。

  • 其中 VPATH 变量的用法如下
    VPATH := <路径列表>
    
    • VPATH 是 Make 的内置变量,专门用于指定依赖文件的搜索路径,无需额外定义函数。
    • <路径列表> 为多个目录的集合(用空格分隔,这里直接复用 SRCDIRS 的路径集合)。
    • 核心作用:当 Make 查找某个依赖文件时,先在当前目录搜索;若未找到,会按照 <路径列表> 中的目录顺序依次查找,直到找到文件或遍历结束。

具体代码实现如下:

# makefile 依赖文件可选的路径
VPATH	:=	$(SRCDIRS)
  • 处理逻辑:将 SRCDIRS 中所有源文件目录直接赋值给 VPATH,Make 会自动识别这份路径清单。
  • 举个例子:当需要编译 led.c 时,Make 先在当前目录查找;若未找到,会按照 SRCDIRS 的顺序,自动去 bsp/led 目录搜索。找到 led.c 后,就能正常执行编译操作,后续编写编译规则时,直接用纯文件名(如 led.c)即可。

通过配置 VPATH,Make 工具彻底明确了源文件的查找范围,结合之前的 INCLUDE 配置,既解决了源文件分散导致的"找不到文件"问题,也解决了头文件引用导致的"编译报错"问题,为后续的编译、链接流程扫清了障碍。


4. 实际编译汇编部分:烧大菜

接着到了我们实际的汇编部分,其中绝大部分只是在原来简单的makefile文件(指名道姓的预处理、编译、汇编、链接)上将对应的名称更换为了前文定义的变量和通配符。
为一需要注意的是静态模式

静态模式的核心语法为 ::,具体逻辑是:先通过 [:] 从目标集合 中,筛选出符合 格式的目标文件;再将目标模式中通配符 % 所占的部分 “打包”,同步映射到依赖模式 的 % 位置,从而自动建立 “目标文件 - 源文件” 的一一对应关系。这种方式无需为每个源文件单独编写编译规则,高效解决了文件数量增多时的规则冗余问题。

最终此处代码如下:

$(TARGET).bin: $(OBJS)
	$(LD) -Timx6u.lds -o $(TARGET).elf $^
	$(OBJCOPY) -O binary -S $(TARGET).elf $@
	$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis

$(SOBJS):obj/%.o:%.s
	$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<

$(COBJS):obj/%.o:%.c
	$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<


5. 辅助操作:清理与调试检测

为了方便项目管理和 Makefile 调试,我们还定义了 cleanprint 两个伪目标,分别用于清理编译产物和检测变量配置是否正确。

.PHONY:clean
clean:
	rm -rf $(TARGET).bin $(TARGET).elf $(TARGET).dis $(OBJS)
print:
	@echo INCLUDE = $(INCLUDE)					# @是静默符号
	@echo SFILES = $(SFILES)					# @是静默符号
	@echo CFILES = $(CFILES)					# @是静默符号
	@echo SFILES_NO_DIR = $(SFILES_NO_DIR)		# @是静默符号
	@echo CFILES_NO_DIR = $(CFILES_NO_DIR)		# @是静默符号
	@echo SOBJS = $(SOBJS)						# @是静默符号
	@echo COBJS = $(COBJS)						# @是静默符号
	@echo OBJS = $(OBJS)						# @是静默符号
	@echo $(TARGET).bin
  1. clean 伪目标:用于一键清理所有编译生成的文件,包括最终二进制文件($(TARGET).bin)、ELF 文件、反汇编文件和所有 .o 目标文件。执行 make clean 即可快速清空编译产物,保持项目目录整洁。

  2. print 伪目标(重点调试工具):核心作用是通过 echo 命令打印前文定义的所有关键变量值,方便检测 Makefile 配置是否正确。

    • 每行命令前的 @ 是静默符号,作用是执行时不显示命令本身,只输出 echo 后的变量内容,避免输出杂乱。
    • 实际用途:当 Makefile 出现“文件找不到”“路径错误”等问题时,执行 make print 可直观查看 INCLUDE(头文件路径)、SFILES/CFILES(源文件列表)、OBJS(目标文件列表)等变量的实际生成结果,快速定位是否存在路径拼接错误、文件遗漏等问题,是调试 Makefile 的实用工具。

6. 最终完整代码,含注释

CROSS_COMPILE ?= arm-linux-gnueabihf-
TARGET ?= keyc

CC 		:=	$(CROSS_COMPILE)gcc
LD 		:=	$(CROSS_COMPILE)ld
OBJCOPY	:=	$(CROSS_COMPILE)objcopy
OBJDUMP	:=	$(CROSS_COMPILE)objdump

# 头文件路径
INCDIRS	:=	imx6u		\
			bsp/clk		\
			bsp/delay	\
			bsp/led		\
			bsp/beep	\
			bsp/key		\
			bsp/gpio	\
			project

# 源码文件路径
SRCDIRS	:=	project		\
			bsp/clk		\
			bsp/delay	\
			bsp/led		\
			bsp/beep	\
			bsp/key		\
			bsp/gpio	\

# 用于编译的头文件路径
INCLUDE :=	$(patsubst %, -I %, $(INCDIRS))		# $(patsubst <pattern>,<replacement>,<text>) 将 <text> 中符合 <patern> 的部分,替换为 <replacement>

# 包含路径的各个源码文件
SFILES	:=	$(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.s))	# $(foreach <var>,<list>,<text>) 把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。
CFILES	:=	$(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))	# $(wildcard <pattern>) 用于匹配指定<pattern>的文件路径,返回所有符合模式的文件名列表(以空格分隔)。[这里通配符是 "*"]

# 无路径的各个源码文件
SFILES_NO_DIR	:=	$(notdir $(SFILES))
CFILES_NO_DIR	:=	$(notdir $(CFILES))

# 源码文件对应的编译后文件 .o
SOBJS	:=	$(patsubst %, obj/%, $(SFILES_NO_DIR:.s=.o))	# 此外还将所用的 .s 后缀更换为 .o 后缀
COBJS	:=	$(patsubst %, obj/%, $(CFILES_NO_DIR:.c=.o))
OBJS	:= 	$(SOBJS) $(COBJS)

# makefile 依赖文件可选的路径
VPATH	:=	$(SRCDIRS)	# ,make 使用“VPATH”变量来指定“依赖文件”的搜索路径。


# 编译汇编链接部分
$(TARGET).bin: $(OBJS)
	$(LD) -Timx6u.lds -o $(TARGET).elf $^
	$(OBJCOPY) -O binary -S $(TARGET).elf $@				# objcopy [选项] 源文件 目标文件
	$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis

$(SOBJS):obj/%.o:%.s	# 静态模式<list>:<pattern>:<prereq-pattern>,[<list>:<pattern>]先筛选出<list>中符合<pattern>的目标文件,然后将对应的通配符"%"所占位置打包,输出到<prereq-pattern>的"%"中
	$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<

$(COBJS):obj/%.o:%.c	# 这里由于不能再通配符"%"里添加"obj/",不然对应的 .c .s 文件不存在
	$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<


.PHONY:clean
clean:
	rm -rf $(TARGET).bin $(TARGET).elf $(TARGET).dis $(OBJS)
print:
	@echo INCLUDE = $(INCLUDE)					# @是静默符号
	@echo SFILES = $(SFILES)					# @是静默符号
	@echo CFILES = $(CFILES)					# @是静默符号
	@echo SFILES_NO_DIR = $(SFILES_NO_DIR)		# @是静默符号
	@echo CFILES_NO_DIR = $(CFILES_NO_DIR)		# @是静默符号
	@echo SOBJS = $(SOBJS)						# @是静默符号
	@echo COBJS = $(COBJS)						# @是静默符号
	@echo OBJS = $(OBJS)						# @是静默符号
	@echo $(TARGET).bin

参考资料

  1. 正点原子参考资料
  2. 跟我一起写makefile
posted @ 2025-11-19 15:09  Keruone  阅读(0)  评论(0)    收藏  举报