makefile问题汇总
修改.h文件没有重新编译
- 定义
COMPILE_FLAGS = -MD或者-MMD, 编译器标志,用于生成.d文件 $(BUILD_DIR)为编译文件目录,跟进自身makefile修改,原来的编译规则:
1 | -include $(wildcard $(BUILD_DIR)/*/*.d) # 包含所有生成的依赖文件,避免重复编译、提高效率 |
/*/*.d 为当前目录下的二级所有文件检索。
- 修改为以下编译规则:或者:
1
2
3
4
5# 找到所有的 .d 文件
DEP_FILES := $(shell find $(BUILD_DIR) -type f -name '*.d')# 包含所有生成的依赖文件,避免重复编译、提高效率
# 包含所有的 .d 文件
-include $(DEP_FILES)1
2-include $(wildcard $(BUILD_DIR)/**/*.d)
-include $(wildcard $(BUILD_DIR)/*/*/*.d)/**/*.d它允许你搜索和匹配嵌套在任意深度的目录中的文件。/*/*/*.d为当前目录下的三级所有文件检索,根据具体情况修改。
$< 和 $^ 的区别
在Makefile中,$< 和 $^ 是两个自动变量,它们在规则中用来引用规则的依赖文件,但它们的用途和行为有所不同:
$<- 代表规则的第一个依赖文件。- 当规则有多个依赖文件时,
$<只引用第一个依赖文件。 - 它通常用于指定要编译的源文件,特别是在编译单个目标文件时。
- 当规则有多个依赖文件时,
$^- 代表规则的所有依赖文件。- 无论规则有多少个依赖文件,
$^都会将它们全部列出。 - 它常用于链接阶段,当你需要将多个目标文件链接成最终的可执行文件时。
- 无论规则有多少个依赖文件,
举个例子来说明它们的不同:
1 | # 假设有一个目标文件 main.o 需要两个源文件 main.c 和 utils.c 来生成 |
再看一个示例:
1 | $(BUILD_DIR)/%.o: %.c |
这个规则的意思是,对于$(BUILD_DIR)目录下的每个.o文件,都有一个对应的.c文件。在Makefile中,$< 是一个自动变量,它代表当前规则的第一个依赖文件。然而,$< 并不会直接依赖所有的 .c 文件,而是依赖于当前规则的 第一个依赖文件。
总结一下:
- 使用
$<时,只有第一个依赖文件会被考虑。 - 使用
$^时,所有依赖文件都会被考虑。
在实际编写Makefile时,根据你的需要选择合适的变量。
makefile伪指令
在 Makefile 中,.PHONY 是一个特殊的声明,用来指出一些目标并不是实际的文件,而是一些需要执行的命令序列。这样做可以让 Make 工具在遇到同名文件时,不会误认为这些目标是要操作的文件,而是要执行的命令。
1 | .PHONY : clean all copy mix download |
Makefile默认执行的是 第一个目标(不包括以点开头的目标)all,而不是.PHONY后的第一个目标。.PHONY宏的作用是防止Make错误地将伪目标与文件系统中的文件混淆。make会加载依赖$(TARGET).bin$(TARGET).list$(TARGET).hex$(SZ) $(TARGET).elf。@符号使得Make工具在执行命令时不会打印该命令本身,如make copy。- 拓展:
$(OC) -I binary -O ihex --change-addresses 0x8000000 mix.bin mix.hex表示将mix.bin的二进制文件转换为名为mix.hex的Intel HEX格式文件,并将所有地址偏移设置为0x8000000
n32g452rc的makefile解析
makefile源码:
1 | # C编译器的宏定义 |
下面是对这段代码的逐句分析:
宏定义 (
C_DEFS):-DN32G45X和-DUSE_STDPERIPH_DRIVER是编译器的宏定义,用于在编译时定义特定的预处理变量。
头文件搜索路径 (
INCLUDE):-ICMSIS/core,-ICMSIS/device,-Istd_periph_lib/inc指定了编译器搜索头文件的路径。
应用层源文件 (
C_APP_SOURCES):- 列出了应用层的
C源文件。
- 列出了应用层的
中间层驱动源文件 (
C_DRV_SOURCES):- 使用 $(wildcard rtt-nano/src/*.c) 来匹配 rtt-nano/src 目录下的所有 .c 文件。
底层驱动源文件 (
C_LIB_SOURCES):- 列出了底层驱动的 C 源文件。
源文件汇总 (
C_SOURCES):- 将应用层、中间层和底层驱动的源文件汇总到
C_SOURCES。
- 将应用层、中间层和底层驱动的源文件汇总到
汇编源文件 (
ASM_SOURCES):- 列出了汇编语言的源文件。
交叉编译工具链 (
CROSS_COMPILE,CC,LD,AR,AS,OC,OD,SZ):- 定义了交叉编译工具链的前缀和各个工具(编译器、链接器、库管理器等)的命令。
目标硬件的架构和浮点运算单元 (
MCU):-mcpu=cortex-m4:- 编译器目标处理器是
Cortex-M4CPU -mcpu=后面跟的是具体的目标 CPU 型号
- 编译器目标处理器是
-mthumb:- 编译器生成
Thumb指令集的代码。Thumb指令集是ARM架构的一种16位指令集,用于嵌入式系统以减少内存占用和提高代码密度。
- 编译器生成
-ffunction-sections:- 允许编译器将 每个函数 分别放入程序的 单独段 中。这可以使得链接器在最终的链接阶段丢弃未使用的函数,从而减小最终固件的大小。
-fdata-sections:- 类似于
-ffunction-sections,这个选项允许编译器将 不同的数据 放入程序的 不同段 中。这同样有助于链接器优化,移除未使用的数据段。
- 类似于
--specs=nano.specs:- 这个选项指定使用新
libnano规格,这是针对小型嵌入式系统的 C 标准库的缩减版本。它提供了比标准C库 更小的占用空间,适合资源受限的系统。
- 这个选项指定使用新
--specs=nosys.specs:- 这个选项指定编译器使用
nosys作为系统调用的默认返回值。在嵌入式系统中,系统调用可能并不总是可用,这个选项 允许编译器生成不依赖系统调用的代码。
- 这个选项指定编译器使用
-Os:- 这个选项指示编译器
优化大小。编译器会尝试在不牺牲太多性能的情况下,生成尽可能小的代码。还有-O0(无优化),-O1,-O2,-O3,-Og(优化调试,不是优化性能或大小)
- 这个选项指示编译器
-ggdb:- 这个选项用于生成调试信息。
-g让编译器在对象文件中包含调试信息,使得开发者可以使用调试器(如GDB)来调试程序。
- 这个选项用于生成调试信息。
-mfpu=fpv4-sp-d16(未使用):- 符合
FPU 版本 4(FPv4)的浮点单元,它支持单精度(single-precision)和双精度(double-precision)浮点运算。-sp-d16表示该FPU拥有16个双精度寄存器,它们被组织为32个单精度寄存器。
- 符合
-mfloat-abi=hard(未使用):- 这个选项指定使用“硬”浮点
ABI,意味着浮点运算将使用目标硬件的 FPU 执行,并且浮点函数(如sin、cos、sqrt等)将被实现为直接调用硬件支持的浮点指令。这通常可以提高性能,因为 浮点运算更快,但可能会 增加固件的大小,因为需要包含 FPU 的指令集。
- 这个选项指定使用“硬”浮点
-mfloat-abi=soft(未使用):- soft 浮点 ABI 指定浮点运算将通过软件库实现,而不是直接使用硬件 FPU。这种方式可以 生成更小的代码,因为不需要包含 FPU 的指令集,但运行时的 浮点运算会较慢,因为它们需要通过软件模拟。
注意:
-mfpu=fpv4-sp-d16与-mfloat-abi=hard或-mfloat-abi=soft选项一起使用。
- 编译选项 (
CFLAGS):
定义了编译C源文件时使用的选项,$(MCU)和$(C_DEFS)上面有解释,不过多阐述。-c:- 这个选项告诉编译器 仅编译源代码 文件而 不进行链接。编译器会为每个源文件生成一个目标(
object)文件,这些目标文件 随后可以被链接 器链接成可执行文件或库。
- 这个选项告诉编译器 仅编译源代码 文件而 不进行链接。编译器会为每个源文件生成一个目标(
-fno-common:- 在一些系统中,
-fcommon是默认行为,它允许存在多个未初始化的相同大小的全局变量或静态变量,它们在链接时合并为一个。-fno-common选项禁用了这一行为,要求每个全局变量或静态变量都有其自己的内存位置。这在嵌入式系统或某些特定系统中可能是必需的。
- 在一些系统中,
--specs=rdimon.specs:- 这个选项指定使用
rdimon.specs文件作为编译器的规格说明。规格说明文件包含了特定于系统的编译和链接规则,rdimon.specs可能是指某个特定实时操作系统(RTOS)或嵌入式平台的规格。
- 这个选项指定使用
-std=gnu99:- 这个选项指定编译器遵循
ISO C99标准,同时包括 GNU 的扩展。gnu99 意味着除了标准的C99特性外,编译器还会接受 GNU C 的特定扩展。
- 这个选项指定编译器遵循
-mabi=aapcs:- 这个选项指定使用
ARM架构程序调用标准(AAPCS,ARM Architecture Procedure Call Standard)。AAPCS定义了函数调用时的参数传递、返回值以及寄存器使用等规则。
- 这个选项指定使用
-Wall:- 这个选项告诉编译器打开大多数警告信息。虽然
-Wall并不打开所有的警告选项,但它会启用大量标准警告,帮助开发者发现潜在的问题。
- 这个选项告诉编译器打开大多数警告信息。虽然
链接器脚本和链接选项 (
LDSCRIPT,LDFLAGS):LDSCRIPT=n32g452_flash.ld- 链接器使用的脚本
-Wl,--gc-sectionsWl是告诉GCC后面的选项是传递给链接器的--gc-sections告诉链接器在最终的可执行文件中删除未使用的代码段,减小程序大小。
--data-sections:- 这个选项类似于
--gc-sections,但它专门用于数据段。它允许链接器移除未使用的数据段,进一步减小程序大小。
- 这个选项类似于
-mabi=aapcs:- 这个选项指定了应用程序二进制接口(
ABI)。aapcs代表ARM 架构过程调用标准,它定义了ARM 架构中函数调用的规则,包括如何传递参数、如何管理堆栈等。
- 这个选项指定了应用程序二进制接口(
-T$(LDSCRIPT):-T是链接器选项,用来指定链接脚本文件。链接脚本包含了有关如何链接程序的附加信息,比如内存布局、各种段的位置等。$(LDSCRIPT)是一个Makefile变量,它的值是链接脚本文件的名称,通常是一个文本文件,告诉链接器如何组织内存中的段。
-x assembler-with-cpp:- 这个选项告诉链接器将所有输入文件视为预处理过的汇编源文件。这通常用于确保链接器正确处理由 C 预处理器生成的汇编代码。
-Wa,-mimplicit-it=thumb:- 这是传递给汇编器的选项(通过
-Wa前缀)。-mimplicit-it=thumb指定默认的指令集为Thumb模式,这是ARM 架构的一种16 位指令集,用于减小代码大小。
- 这是传递给汇编器的选项(通过
- 目标文件格式转换选项 (
OCFLAGS,ODFLAGS):
-OCFLAGS = -Obinary
-ODFLAGS = -S
- 输出目录和目标文件名 (
BUILD_DIR,TARGET):- 定义了构建输出目录和最终目标文件的名称。
- 目标文件的生成 (
C_OBJECTS,ASM_OBJECTS,OBJECTS):C_OBJECTS = $(addprefix $(BUILD_DIR)/, $(C_SOURCES:.c=.o))$(C_SOURCES:.c=.o)将所有的C源文件列表$(C_SOURCES)中的每个.c扩展名替换为.o扩展名,得到目标文件(object files)列表。$(addprefix $(BUILD_DIR)/, ...)为每个.o文件加前缀加上构建目录$(BUILD_DIR)/的路径。C_OBJECTS变量包含了所有C源文件对应的、带有完整路径的目标文件列表。
ASM_OBJECTS = $(addprefix $(BUILD_DIR)/, $(ASM_SOURCES:.S=.o)):- 这行代码的逻辑与
C_OBJECTS类似,但是针对汇编源文件$(ASM_SOURCES)。 - 它将所有
.S源文件扩展名替换为.o,然后添加构建目录的前缀,生成汇编语言目标文件的列表。
- 这行代码的逻辑与
OBJECTS += $(ASM_OBJECTS) $(C_OBJECTS):- 这行代码使用
+=操作符将$(ASM_OBJECTS)和$(C_OBJECTS)列表中的所有目标文件添加到OBJECTS变量中。 - OBJECTS 变量通常用于表示所有需要链接的文件的列表,它可能已经包含了一些其他目标文件,这里通过
+=操作符将汇编和C语言的目标文件列表追加进去。 - 最终,
OBJECTS包含了所有需要被链接器用来生成最终可执行文件或库文件的目标文件。
- 这行代码使用
- 伪目标 (
.PHONY):- 定义了
clean,all,copy,mix,download等伪目标,用于执行特定的命令序列。
- 定义了
- 条件命令 (
SYS,ifeq):SYS := $(shell uname -a):- 这行使用
uname -a命令来获取当前系统的内核信息,并将输出赋值给SYS变量。$(shell ...)命令会在shell中执行括号内的内容,并返回其输出。
- 这行使用
ifeq ($(findstring Microsoft,$(SYS)),Microsoft):- 这是一个条件语句,用于检查
SYS变量中是否包含字符串 “Microsoft“。$(findstring ...)函数在SYS的值中搜索 “Microsoft“ 字符串。如果找到,返回 “Microsoft“;否则返回空字符串。 ifeq检查$(findstring Microsoft,$(SYS))的结果是否等于 “Microsoft“。如果是,将执行下面的命令
- 这是一个条件语句,用于检查
wsl.exe -d Ubuntu-20.04 cmd.exe /c "C:\\\Users\\\BREO\\\Desktop\\\iap-tools\\\linux_download\\\program452RC.bat":- 使用
wsl.exe调用WSL环境。 -d Ubuntu-20.04中,-d选项允许你选择一个特定的发行版来启动,这里是Ubuntu-20.04。cmd.exe:这是Windows的命令行解释器,来运行一个Windows 批处理脚本。/c: 告诉cmd.exe执行指定的命令,然后关闭窗口。"C:\\\...bat": 需要执行脚本的路径。- **不知道为什么要用
\\\**。
- 使用
- 生成目标文件的规则 (
$(TARGET).list,$(TARGET).bin,$(TARGET).elf,$(TARGET).hex):$(TARGET).list: $(TARGET).elf:(以下规则类似)$(TARGET).list:$@(目标文件)$(TARGET).elf:$<(输入文件)
$(OD) $(ODFLAGS) $< > $(TARGET).lst:- 这是上述规则的命令部分。
$(OD)是objdump工具的变量,$(ODFLAGS)是objdump的相关参数。$<是自动变量,表示第一个依赖文件,也就是$(TARGET).elf。 - 命令使用
objdump提取.elf文件的汇编内容,并重定向输出到$(TARGET).lst文件。
- 这是上述规则的命令部分。
$(OC) $(OCFLAGS) $(TARGET).elf $(TARGET).bin:- 这是上述规则的命令部分。
$(OC)是objcopy工具的变量,$(OCFLAGS)是objcopy的相关参数。 - 命令使用
objcopy从.elf文件生成二进制格式的.bin文件。
- 这是上述规则的命令部分。
$(CC) $(ASM_OBJECTS) $(C_OBJECTS) $(LDFLAGS) -Wl,-Map=$(TARGET).map -o $(TARGET).elf:- 这是 链接命令,使用
$(CC)编译器(实际上是链接器)来链接对象文件$(ASM_OBJECTS)和$(C_OBJECTS),以及链接器标志$(LDFLAGS)。 -Wl,-Map=$(TARGET).map选项告诉链接器生成一个内存映射文件$(TARGET).map。-o $(TARGET).elf指定了输出的 可执行文件名。
- 这是 链接命令,使用
$(OC) -O ihex $< $@:- 这是上述规则的命令部分,使用
objcopy的-O ihex选项将输入文件($<)转换为Intel HEX格式,并命名为$@,即$(TARGET).hex。
- 这是上述规则的命令部分,使用
mkdir -p $(dir $@)- 这条命令确保目标文件的目录存在。
$(dir $@)获取目标文件的目录路径,mkdir -p命令 创建这个目录(如果它还不存在)。
- 这条命令确保目标文件的目录存在。
$(CC) $(INCLUDE) $(CFLAGS) -MMD -c $< -o $@- 这是编译单个
C源文件的命令。它使用$(CC)编译器,$(INCLUDE)包含路径,$(CFLAGS)编译选项,-c选项来编译不链接,-MMD生成依赖文件,$<是源文件,$@是目标文件。
- 这是编译单个
$(BUILD_DIR)/%.o: %.S:- 这是一个模式规则,用于从
.S汇编源文件生成.o目标文件,其命令与上面编译C文件的命令类似,但是通常 不包含编译选项,因为汇编语言的处理方式与C语言不同。
- 这是一个模式规则,用于从
-include $(wildcard $(BUILD_DIR)/**/*.d):$(wildcard *.c)会列出当前目录下所有的.c文件。$(wildcard $(BUILD_DIR)/**/*.d)会列出$(BUILD_DIR)目录下的所有.d文件。
DEP_FILES := $(shell find $(BUILD_DIR) -type f -name '*.d'),-include $(DEP_FILES):find这是一个find命令,用于在文件系统中搜索文件。$(BUILD_DIR)是之前定义的Makefile变量,表示构建目录的路径。-type f指定find命令只搜索文件(不包括目录)。-name '*.d'定义了搜索的文件名模式,*.d匹配所有以.d结尾的文件,这通常是由编译器生成的依赖文件。-include $(DEP_FILES):make将会包含这些.d,这些文件包含了头文件的依赖信息,从而确保make能够正确地检测到源文件的依赖关系,并且在源文件或其头文件发生变化时重新编译相应的目标文件。
编译C源文件和汇编源文件的规则 (
$(BUILD_DIR)/%.o: %.c,$(BUILD_DIR)/%.o: %.S):- 定义了如何编译
C和汇编源文件为.o目标文件。
- 定义了如何编译
- 包含生成的依赖文件
(-include $(wildcard $(BUILD_DIR)/**/*.d), DEP_FILES):- 使用
find命令找到所有的.d文件,然后使用-include指令包含这些文件,以确保Make能够跟踪源文件的变化并避免不必要的重新编译。
- 使用
标准makefile基本语法
[参考文档]:https://seisman.github.io/how-to-write-makefile/index.html
书写规则
规则的语法
1 | targets : prerequisites |
或是这样:
1 | targets : prerequisites ; command |
如果命令太长,你可以使用反斜杠( \ )作为换行符
在规则中使用通配符
make支持三个通配符: * , ? 和 ~ 。这是和Unix的B-Shell是相同的。
*:通配符代替了你一系列的文件,如*.c表示所有后缀为c的文件。一个需要我们注意的是,如果我们的文件名中有通配符,如:*,那么可以用转义字符\,如\*来表示真实的*字符,而不是任意长度的字符串。1
2clean:
rm -f *.oclean是操作系统Shell所支持的通配符。1
objects = *.o
objects的值就是*.o, 并不是说 *.o 会展开, 如需展开进行以下操作。示例1:1
objects := $(wildcard *.o)
1
var = $(shell echo "Hello, World!")
:=和=区别:=(递归展开):示例1每次引用$(var)时,Make都会执行echo "Hello, World!":=(直接展开):示例1每次引用$(var)的值在Makefile解析时设置为"Hello, World!"
示例2
可写出编译并链接所有 .c 和 .o 文件:
1 | objects := $(patsubst %.c,%.o,$(wildcard *.c)) |
patsubst函数用于将第一个参数中匹配第二个参数模式的部分替换为第三个参数中的相应模式patsubst函数用于将*.c替换为*.o,wildcard函数用于获取当前目录下所有.c文件。
文件搜索
1 | VPATH = src:../headers |
Makefile文件中的特殊变量VPATH,当make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。- “
src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)。
1 | vpath <pattern> <directories> |
vpath使用方法中的<pattern>需要包含%字符。%的意思是匹配零或若干字符,(需引用%,使用\)例如,%.h表示所有以.h结尾的文件。<pattern>指定了要搜索的文件集,而<directories>则指定了<pattern>的文件集的搜索的目录。
伪目标
1 | .PHONY : clean |
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。
1 | all : prog1 prog2 prog3 |
Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于默认目标的特性是,总是被执行的,但由于“all”又是一个伪目标,伪目标只是一个标签不会生成文件,所以不会有“all”文件产生。于是,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。.PHONY : all声明了“all”这个目标为“伪目标”。(注:这里的显式“.PHONY : all” 不写的话一般情况也可以正确的执行,这样make可通过隐式规则推导出, “all” 是一个伪目标,执行make不会生成“all”文件,而执行后面的多个目标。建议:显式写出是一个好习惯。)
多目标
Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。
1 | bigoutput littleoutput : text.g |
上述规则等价于:
1 | bigoutput : text.g |
其中,-$(subst output,,$@)中的 $ 表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数,将在后面讲述。这里的这个函数是替换字符串的意思, $@ 表示目标的集合,就像一个数组, $@ 依次取出目标,并执于命令。
$(subst from,to,text)from是您想要替换的字符串。to是您想要替换成的新字符串。text是原始文本。
generate text.g -$(subst output,,$@) > $@中的”>“ 是一个shell命令,用于将generate text.g -$(subst output,,$@)的输出重定向到右侧$@文件中。
静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法:
1 | <targets ...> : <target-pattern> : <prereq-patterns ...> |
我们的“目标模式”或是“依赖模式”中都应该有 % 这个字符,如果你的文件名中有 % 那么你可以使用反斜杠 \ 进行转义,来标明真实的 % 字符。看一个例子:
1 | objects = foo.o bar.o |
- 我们的目标从
$object中获取 %.o表明要所有以.o结尾的目标,也就是foo.obar.o,也就是变量$object集合的模式- 依赖模式
%.c则取模式%.o的%,也就是foobar,并为其加下.c的后缀,于是,我们的依赖目标就是foo.cbar.c - 命令中的
$<和$@则是自动化变量,$<表示 第一个依赖文件,$@表示 目标集(也就是“foo.obar.o”)
上面的规则展开后等价于下面的规则:
1 | foo.o : foo.c |
如果我们的 %.o 有几百个,使用“静态模式规则”很方便,再看一个例子:
1 | files = foo.elc bar.o lose.o |
$(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$files”集,只要其中模式为“%.o”的内容。
自动生成依赖性
在Makefile中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的main.c中有一句 #include "defs.h" ,那么我们的依赖关系应该是:
main.o : main.c defs.h
但是,如果是一个比较大型的工程,你必需清楚哪些C文件包含了哪些头文件,例如,如果我们执行下面的命令:
cc -M main.c
其输出是:
main.o : main.c defs.h
需要提醒一句的是,如果你使用GNU的C/C++编译器,你得用 -MM 参数,不然,-M 参数会把一些 标准库的头文件也包含进来。
gcc -M main.c的输出是:1
2
3
4
5
6
7
8
9main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \
/usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \
/usr/include/bits/sched.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/include/bits/wchar.h /usr/include/gconv.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
/usr/include/bits/stdio_lim.hgcc -MM main.c的输出则是:1
main.o: main.c defs.h
GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个name.c的文件都生成一个name.d的Makefile文件,.d文件中就存放对应.c文件的依赖关系。- 我们可以写出
.c文件和.d文件的依赖关系,并让make自动更新或生成.d文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。
1 | %.d: %.c |
- 规则的意思是,所有的
.d文件依赖于.c文件 rm -f $@的意思是删除所有的目标,也就是.d文件- 第二行的意思是,为每个依赖文件
$<,也就是.c文件生成依赖文件,$@表示模式%.d文件,如果有一个C文件是name.c,那么%就是name,$$$$意为一个随机编号,第二行生成的文件有可能是“name.d.12345” - 第三行使用
sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档 - 第四行就是删除临时文件
总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖,即把依赖关系:
main.o : main.c defs.h
转成:
main.o main.d : main.c defs.h
于是,我们的 .d 文件也会自动更新了,并会自动生成了, 你还可以在这个 .d 文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个 .d 文件都包含一个完整的规则, 例如:
1 | sources = foo.c bar.c |
$(sources:.c=.d)中的.c=.d的意思是做一个替换,把变量$(sources)所有.c的字串都替换成.d- 因为
include是按次序来载入文件,最先载入的.d文件中的目标会成为默认目标。
书写命令
显示命令
1 | @echo 正在编译XXX模块...... |
当make执行时,会输出“正在编译XXX模块……”字串,但不会输出命令,如果没有“@”,那么,make将输出:
1 | echo 正在编译XXX模块...... //输出命令 |
- 如果
make执行时,带入make参数-n或--just-print,那么其只是显示命令,但不会执行命令。 make参数-s或--silent或--quiet则是全面禁止命令的显示。
命令执行
当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。
- 示例一:
1
2
3exec:
cd /home/hchen
pwd
1 | exec: |
当我们执行 make exec 时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出“/home/hchen”。
命令出错
忽略命令的出错,我们可以在Makefile的命令行前加一个减号 - (在Tab键之后),标记为不管命令出不出错都认为是成功的。如:
1 | clean: |
- 还有一个全局的办法是,给
make加上-i或是--ignore-errors参数,那么,Makefile中所有命令都会忽略错误。如果一个规则是以.IGNORE作为目标的,那么这个规则中的所有命令将会忽略错误。 - 还有一个要提一下的
make的参数的是-k或是--keep-going,这个参数的意思是,如果某规则中的命令出错了,那么就 终止该规则 的执行,但 继续执行其它规则。
嵌套执行make
在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile
例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:
1 | subsystem: |
其等价于:
1 | subsystem: |
Makefile的-C选项,可以指定编译目录,-f选项可以指定编译文件。定义
$(MAKE)宏变量的意思是,也许我们的make需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录,然后执行make命令。我们把这个
Makefile叫做“总控Makefile”,总控Makefile的变量可以传递到下级的Makefile中(如果你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了-e参数。如果你要传递变量到下级
Makefile中,那么你可以使用这样的声明:export <variable ...>;如果你不想让某些变量传递到下级
Makefile中,那么你可以这样声明:unexport <variable ...>;如果你要传递所有的变量,那么,只要一个
export就行了。后面什么也不用跟,表示传递所有的变量。
示例一:export variable = value
// 其等价于:
variable = value
export variable
// 其等价于:
export variable := value
// 其等价于:
variable := value
export variable
示例二:
export variable += value
// 其等价于:
variable += value
export variable
- 需要注意的是,有两个变量,一个是
SHELL,一个是MAKEFLAGS,这两个变量不管你是否export,其总是要 传递到下层Makefile中,特别是MAKEFLAGS变量,其中包含了make的 参数信息,如果我们执行“总控Makefile”时有make参数或是在上层Makefile中定义了这个变量,那么MAKEFLAGS变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量。 - 但是
make命令中的有几个参数并不往下传递,它们是-C,-f,-h,-o和-W(有关Makefile参数的细节将在后面说明),如果你不想往下层传递参数,那么,你可以这样来:1
2subsystem:
cd subdir && $(MAKE) MAKEFLAGS= - 如果你定义了环境变量
MAKEFLAGS,那么你得确信其中的选项是大家都会用到的,如果其中有-t,-n和-q参数,那么将会有让你意想不到的结果,或许会让你异常地恐慌。 - 还有一个在“嵌套执行”中比较有用的参数,
-w或是--print-directory会在make的过程中输出一些信息,让你看到目前的工作目录。比如,如果我们的下级make目录是“/home/hchen/gnu/make”,如果我们使用make -w来执行,那么当进入该目录时,我们会看到:而在完成下层make后离开目录时,我们会看到:1
make: Entering directory `/home/hchen/gnu/make'.
1
make: Leaving directory `/home/hchen/gnu/make'
- 当你使用
-C参数来指定make下层Makefile时,-w会被自动打开的。如果参数中有-s(--slient)或是--no-print-directory,那么,-w总是失效的。
定义命令包
如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以 define 开始,以 endef 结束,如:
1 | define run-yacc |
- 这里,“run-yacc”是这个命令包的名字,其不要和Makefile中的变量重名。在 define 和 endef 中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc程序,因为Yacc程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看吧。
1 | foo.c : foo.y |
我们可以看见,要使用这个命令包,我们就好像使用变量一样。在这个命令包的使用中,命令包“run-yacc”中的 $^ 就是 foo.y , $@ 就是 foo.c (有关这种以 $ 开头的特殊变量,我们会在后面介绍),make在执行命令包时,命令包中的每个命令会被依次独立执行。
使用函数
函数的调用语法
函数调用,很像变量的使用,也是以 $ 来标识的,其语法如下:
$(<function> <arguments>)
// 或者
${<function> <arguments>}
字符串处理函数
subst
1 | $(subst <from>,<to>,<text>) |
- 名称:字符串替换函数
- 功能:把字串
<text>中的<from>字符串替换成<to>。 - 返回:函数返回被替换过后的字符串。
patsubst
1 | $(patsubst <pattern>,<replacement>,<text>) |
- 名称:模式 字符串替换函数。
- 功能:查找
<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。这里,<pattern>可以包括通配符%,表示任意长度的字串。如果<replacement>中也包含%,那么,<replacement>中的这个%将是<pattern>中的那个 % 所代表的字串。(可以用\来转义,以\%来表示真实含义的%字符) - 返回:函数返回被替换过后的字符串。
- 示例:把字串
1
$(patsubst %.c,%.o,x.c.c bar.c)
x.c.cbar.c符合模式%.c的单词替换成%.o,返回结果是x.c.obar.o
strip
$(strip <string>)
- 名称:去空格函数。
- 功能:去掉
<string>字串中开头和结尾的空字符。 - 返回:返回被去掉空格的字符串值。
- 示例:把字串
1
$(strip a b c )
a b c去掉开头和结尾的空格,结果是a b c。
findstring
$(findstring <find>,<in>)
- 名称:查找字符串函数
- 功能:在字串
中查找 字串。 - 返回:如果找到,那么返回
,否则返回空字符串。 - 示例:第一个函数返回
1
2$(findstring a,a b c)
$(findstring a,b c)a字符串,第二个返回空字符串
混合总结
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
如果我们的 $(VPATH) 值是 src:../headers ,那么 $(patsubst %,-I%,$(subst :, ,$(VPATH))) 将返回 -Isrc -I../headers ,这正是cc或gcc搜索头文件路径的参数。
注:函数太多,不做概述。详见:https://seisman.github.io/how-to-write-makefile/functions.html
例程:https://github.com/XUAN9527/linux_test/tree/main/make_demo
Makefile搭配Kconfig使用
话不多说,我们一般使用menuconfig+Kconfig的方式进行版本配置,这里简单的笔记一个Python解析器版本的实现Kconfiglib。
[参考链接1]:https://cstriker1407.info/blog/kconfiglib-simple-note/
[参考链接2]:https://juejin.cn/post/7101836149915648030
环境搭建
- 安装必须组件:
Python3+kconfiglib
1 | sudo apt update |
- 验证安装:
1
2
3
4
5
6
7python3 --version
Python 3.10.14
pip3 show kconfiglib
Name: kconfiglib
Version: 14.1.0
...
实现示例
在跟目录下创建
Kconfig文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59mainmenu "N32l40x 128K MCU, Flash Configuration"
config SPI_FLASH_ENABLE
bool "spi flash enable"
default n
help
config spi flash enable/disable
menu "Internal Flash Configuration"
depends on !SPI_FLASH_ENABLE
config INTER_BOOTLOAD_FIRMWARE_SIZE
int "bootloader fireware size (K)"
range 10 32
default 10
help
config bootloader fireware size
config INTER_FACTORY_FIRMWARE_SIZE
int "factory fireware size (K)"
range 56 96
default 56
help
config factory fireware size 56K/96K
config INTER_APPLICATION_FIRMWARE_SIZE
int "application fireware size (K)"
range 56 96
default 56
help
config application fireware size 56K/96K
config INTER_DOWNLOAD_AREA_SIZE
int "download fireware size (K)"
range 56 96
default 56
help
config download area size 56K/96K
config INTER_UPGRADE_DATA_SIZE
int "upgrade data size (K)"
range 2 4
default 2
help
config upgrade data size 2K/4K
config INTER_DCD_DATA_SIZE
int "dcd data size (K)"
range 2 4
default 2
help
config dcd data size 2K/4K
config INTER_USER_DATA_SIZE
int "user data size (K)"
range 2 408
default 2
help
config user data size 2K/408K; 2k - inter flash; 408K - outerflash
endmenu
...shell执行menuconfig指令:


- 选择好需要的参数后,保存退出,生成
.config配置文件。 shell执行genconfig指令,将.config文件生成config.h文件,可供程序调用。- 如需搭配
makefile使用,则需要将config.h文件添加到Makefile中,添加以下依赖规则。执行1
2
3
4
5
6
7
8
9
10
11
12all: genconfig ...
...
...
menuconfig:
menuconfig
@echo "menuconfig running!"
genconfig:
genconfig
@echo "genconfig .config > config.h complete!"make menuconfig进行配置,make编译生成即可。
makefile隐藏打印输出
以下是
makefile使用make编译的shell输出信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22xuan@DESKTOP-A52B6V9:~/work/wireless-charging/app$ make
mkdir -p build/application/
arm-none-eabi-gcc -ICMSIS/core -ICMSIS/device -Istd_periph_lib/inc -Iuser -Imsp -Idriver -Iapplication/inc -Icomponents/letter_shell -Icomponents/iap -Icomponents/ntc -Icomponents/soft_timer -Icomponents/comp_misc_lib -Icomponents/flexibleButton -Icomponents/aw9523b -c -fno-common --specs=rdimon.specs -std=gnu99 -mabi=aapcs -Wall -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -lm --specs=nosys.specs --specs=nano.specs -Os -ggdb -DN32L40X -DUSE_STDPERIPH_DRIVER -D__FPU_PRESENT=1 -MMD -c application/main.c -o build/application/main.o
arm-none-eabi-gcc build/CMSIS/device/startup/startup_n32l40x_gcc.o build/CMSIS/device/system_n32l40x.o build/std_periph_lib/src/n32l40x_rcc.o build/std_periph_lib/src/n32l40x_tim.o build/std_periph_lib/src/n32l40x_adc.o build/std_periph_lib/src/n32l40x_dma.o build/std_periph_lib/src/n32l40x_usart.o build/std_periph_lib/src/n32l40x_iwdg.o build/std_periph_lib/src/n32l40x_flash.o build/std_periph_lib/src/n32l40x_gpio.o build/std_periph_lib/src/n32l40x_spi.o build/std_periph_lib/src/misc.o build/application/main.o build/application/n32l40x_it.o build/application/system_work.o build/user/module_battery.o build/user/module_botton.o build/user/module_led.o build/user/module_power.o build/user/module_storage.o build/user/shell_debug.o build/user/user_board.o build/msp/board.o build/msp/drv_msp.o build/driver/drv_adc.o build/driver/drv_flash.o build/driver/drv_gpio.o build/driver/drv_iwdg.o build/driver/drv_pwm_gpio.o build/driver/drv_pwm_input.o build/driver/drv_usart.o build/driver/drv_spi_led.o build/driver/drv_i2c.o build/driver/drv_i2c_bit_ops.o build/components/comp_misc_lib/comp_misc_lib.o build/components/flexibleButton/flexible_button.o build/components/iap/af_utils.o build/components/iap/dcd_port.o build/components/iap/dcd_user.o build/components/iap/iap.o build/components/iap/ymodem.o build/components/letter_shell/log.o build/components/letter_shell/shell.o build/components/letter_shell/shell_cmd_list.o build/components/letter_shell/shell_companion.o build/components/letter_shell/shell_ext.o build/components/letter_shell/shell_port.o build/components/ntc/ntc.o build/components/soft_timer/soft_timer.o build/components/aw9523b/aw9523b.o -Wl,--gc-sections --data-sections -mabi=aapcs -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -ffunction-sections -fdata-sections -lm --specs=nosys.specs --specs=nano.specs -Os -ggdb -Tn32l40x_flash.ld -x assembler-with-cpp -Wa,-mimplicit-it=thumb -Wl,-Map=build/app.map -o build/app.elf
arm-none-eabi-objcopy -Obinary build/app.elf build/app.bin
arm-none-eabi-objdump -S build/app.elf > build/app.lst
arm-none-eabi-objcopy -O ihex build/app.elf build/app.hex
arm-none-eabi-size build/app.elf
text data bss dec hex filename
29280 292 11888 41460 a1f4 build/app.elf
make[1]: Entering directory '/home/xuan/work/wireless-charging/app'
cp build/app.bin app.bin
cp ../bootloader/bootloader.bin bootloader.bin
make[1]: Leaving directory '/home/xuan/work/wireless-charging/app'
make[1]: Entering directory '/home/xuan/work/wireless-charging/app'
./tools/papp_up
./tools/mix_10K
arm-none-eabi-objcopy -I binary -O ihex --change-addresses 0x8000000 mix.bin mix.hex
rm bootloader.bin
rm app.bin
rm mix.bin
make[1]: Leaving directory '/home/xuan/work/wireless-charging/app'以下是
makefile的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40.PHONY : clean all
all: $(TARGET).bin $(TARGET).list $(TARGET).hex
$(SZ) $(TARGET).elf
@make copy
@make mix
copy: $(TARGET).bin
cp $(TARGET).bin app.bin
cp ../bootloader/bootloader.bin bootloader.bin
mix:
./tools/papp_up
./tools/mix_10K
$(OC) -I binary -O ihex --change-addresses 0x8000000 mix.bin mix.hex
rm bootloader.bin
rm app.bin
rm mix.bin
clean:
rm -rf $(OUTPUT_DIR)
rm papp.bin
rm mix.hex
SYS := $(shell uname -a)
ifeq ($(findstring Microsoft,$(SYS)),Microsoft)
COPY_CMD:
cp $(OUTPUT_DIR)/app.hex "/mnt/c/Users/Breo/Desktop/iap-tools/linux_download/"
wsl.exe -d Ubuntu-20.04 cmd.exe /c "C:\\\Users\\\BREO\\\Desktop\\\iap-tools\\\linux_download\\\program.bat"
else
COPY_CMD:
echo "当前系统不是 WSL,跳过拷贝文件指令"
endif
download:
@make all
@$(MAKE) COPY_CMD使用
@和--no-print来隐藏打印信息,改进后:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42.PHONY : clean all
all: $(TARGET).bin $(TARGET).list $(TARGET).hex
$(SZ) $(TARGET).elf
@make --no-print copy
@make --no-print mix
copy: $(TARGET).bin
@cp $(TARGET).bin app.bin
@cp ../bootloader/bootloader.bin bootloader.bin
mix:
@./tools/papp_up
@./tools/mix_10K
@$(OC) -I binary -O ihex --change-addresses 0x8000000 mix.bin mix.hex
@rm bootloader.bin
@rm app.bin
@rm mix.bin
clean:
-rm -rf $(OUTPUT_DIR)
-rm papp.bin
-rm mix.hex
SYS := $(shell uname -a)
ifeq ($(findstring Microsoft,$(SYS)),Microsoft)
COPY_CMD:
cp $(TARGET).hex "/mnt/c/Users/Breo/Desktop/iap-tools/linux_download/"
wsl.exe -d Ubuntu-20.04 cmd.exe /c "C:\\\Users\\\BREO\\\Desktop\\\iap-tools\\\linux_download\\\program.bat"
else
COPY_CMD:
echo "当前系统不是 WSL,跳过拷贝文件指令"
endif
download:
@make all
@$(MAKE) COPY_CMD
编译路径问题
下面一段是
makefile在工程目录下的.o生成规则,makefile是跟Libraries同级目录,规则是生成到BUILD_DIR文件夹下,保持子目录结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21C_SOURCES = \
Libraries/core/system.c \
Libraries/CMSIS/Device/YICHIP/YC3122/Source/Templates/system_yc3122.c \
Libraries/sdk/yc_gpio.c \
Libraries/sdk/yc_uart.c \
Libraries/sdk/yc_timer.c \
main.c
ASM_SOURCES = \
Libraries/CMSIS/Device/YICHIP/YC3122/Source/Templates/gcc/startup_yc3122.S
# 将源文件映射到 obj 目录,保持子目录结构
OBJECTS := $(patsubst %, $(BUILD_DIR)/%, $(C_SOURCES:.c=.o)) \
$(patsubst %, $(BUILD_DIR)/%, $(ASM_SOURCES:.S=.o))
# 编译 C 文件
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(dir $@)
@echo "[CC] $<"
@$(CC) -c $(CFLAGS) -MMD -MP -MF"$(@:.o=.d)" \
$< -o $@如果
makefile跟Libraries不是同级目录,需要建立PROJECT_PATH开头,下面通配构建:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# 定义一个工程目录,C_SOURCES源文件链接地址统一开头,$(OBJ_DIR)/%.o: $(PROJECT_PATH)/%.c生成规则才能构建编译区目录
PROJECT_PATH = ../../../../..
OUTPUT_DIR = output
OBJ_DIR = $(OUTPUT_DIR)/obj
# 源文件
C_SOURCES = \
$(PROJECT_PATH)/Libraries/core/system.c \
$(PROJECT_PATH)/Libraries/CMSIS/Device/YICHIP/YC3122/Source/Templates/system_yc3122.c \
$(PROJECT_PATH)/ModuleDemo/GPIO/gpio_toggle/user/main.c \
$(PROJECT_PATH)/Libraries/sdk/yc_gpio.c \
$(PROJECT_PATH)/Libraries/sdk/yc_uart.c \
$(PROJECT_PATH)/Libraries/sdk/yc_timer.c
# 将源文件映射到 obj 目录,保持子目录结构
OBJ_FILES := $(patsubst $(PROJECT_PATH)/%, $(OBJ_DIR)/%, $(C_SOURCES:.c=.o)) \
$(patsubst $(PROJECT_PATH)/%, $(OBJ_DIR)/%, $(ASM_SOURCES:.S=.o))
# 编译 C 文件
$(OBJ_DIR)/%.o: $(PROJECT_PATH)/%.c
@mkdir -p $(dir $@)
@echo "[CC] $<"
$(GCC) $(CFLAG) -o $@ $<如果
C_SOURCES路径比较混乱,又用的是通配$(OBJ_DIR)/%.o: $(PROJECT_PATH)/%.c,生成的编译文件.o就会比较混乱:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18C_SOURCES = \
../../../../../Libraries/core/system.c \
../../../../../Libraries/CMSIS/Device/YICHIP/YC3122/Source/Templates/system_yc3122.c \
../../user/main.c \
../../../../../Libraries/sdk/yc_gpio.c \
../../../../../Libraries/sdk/yc_uart.c \
../../../../../Libraries/sdk/yc_timer.c
# 下面生成规则会出文件,找不到文件路径,因为通用的规则不能识别不了不同的相对路径
OBJECTS := $(patsubst %, $(BUILD_DIR)/%, $(C_SOURCES:.c=.o)) \
$(patsubst %, $(BUILD_DIR)/%, $(ASM_SOURCES:.S=.o))
# 编译 C 文件
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(dir $@)
@echo "[CC] $<"
@$(CC) -c $(CFLAGS) -MMD -MP -MF"$(@:.o=.d)" \
$< -o $@
总结:
- 当
makefile与待编译文件(文件夹)同级目录时,可直接写通用规则。 - 当
makefile与待编译文件(文件夹)不为同级目录时,需找到相对路径最远端的文件夹,设为PROJECT_PATH,其他的文件以此目录开始相对路径,编译文件就能用通用编译规则,编译文件也会带目录。