【0基础嵌入式学习日志】Day05:Makefile 自动化、通配符、模式规则与增量编译 Day06:头文件依赖追踪、.d 文件与完整增量编译

📅 2026/6/28 6:59:56
【0基础嵌入式学习日志】Day05:Makefile 自动化、通配符、模式规则与增量编译 Day06:头文件依赖追踪、.d 文件与完整增量编译
【0基础嵌入式学习日志】Day05Makefile 自动化、通配符、模式规则与增量编译一、前言今天继续进行嵌入式 C 语言基础学习。前面几天已经完成了 C 工程结构、结构体、故障码、函数封装、多文件拆分、.h/.c配合以及.c → .o → 可执行文件的分步编译流程。Day04 中Makefile 已经可以将多个.c文件分别编译成.o文件再链接生成最终可执行程序src/main.c → build/main.o src/system.c → build/system.o src/sensor.c → build/sensor.o src/fault.c → build/fault.o build/main.o build/system.o build/sensor.o build/fault.o → build/day04_test但是 Day04 的 Makefile 仍然存在一个问题每个.o文件和对应的编译规则都需要手动写出来。随着工程文件数量增加这种写法会越来越麻烦。因此Day05 的重点是学习 Makefile 的自动化写法包括wildcard patsubst 模式规则 自动变量 $ 和 $ 增量编译目标是让 Makefile 自动查找src目录下所有.c文件并自动生成对应的.o文件。二、Day05 学习目标本次 Day05 主要学习以下内容理解为什么要进一步优化 Makefile学会使用wildcard自动查找.c文件学会使用patsubst自动生成.o文件列表理解模式规则build/%.o: src/%.c理解自动变量$和$的含义验证 Makefile 的增量编译机制理解修改单个.c文件后只重新编译对应.o文件的过程。三、Day05 工程结构Day05 工程是在 Day04 的基础上复制并修改得到的整体结构如下day05 ├── Makefile ├── include │ ├── fault_code.h │ ├── fault.h │ ├── sensor.h │ ├── system.h │ └── system_type.h ├── src │ ├── fault.c │ ├── main.c │ ├── sensor.c │ └── system.c └── build ├── fault.o ├── main.o ├── sensor.o ├── system.o └── day05_test其中include存放头文件src存放源文件build存放编译生成的.o文件和可执行文件Makefile管理自动化编译、运行和清理README.md记录项目说明。需要注意的是build目录中的.o文件和day05_test是本地编译产物一般不需要上传到 GitHub。四、为什么要优化 Day04 的 MakefileDay04 的 Makefile 中手动写出了每一个目标文件OBJS build/main.o build/system.o build/sensor.o build/fault.o并且每一个.c文件都要单独写一条编译规则build/main.o: src/main.c $(CC) $(CFLAGS) -c src/main.c -o build/main.o build/system.o: src/system.c $(CC) $(CFLAGS) -c src/system.c -o build/system.o build/sensor.o: src/sensor.c $(CC) $(CFLAGS) -c src/sensor.c -o build/sensor.o build/fault.o: src/fault.c $(CC) $(CFLAGS) -c src/fault.c -o build/fault.o这种方式虽然清楚但是不够灵活。如果后面新增一个源文件例如src/uart.c就需要手动修改 Makefile增加build/uart.o以及对应的编译规则。真实嵌入式工程中.c文件可能很多如果每个文件都手动维护效率会很低也容易出错。因此Day05 使用更加自动化的 Makefile 写法让它自动完成自动查找 src 目录下所有 .c 文件 自动生成 build 目录下对应的 .o 文件 使用一条模式规则完成所有 .c 文件的编译五、Day05 Makefile 内容Day05 的 Makefile 内容如下CC gcc CFLAGS -Wall -Wextra -Iinclude TARGET build/day05_test SRCS $(wildcard src/*.c) OBJS $(patsubst src/%.c, build/%.o, $(SRCS)) all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) build/%.o: src/%.c $(CC) $(CFLAGS) -c $ -o $ run: $(TARGET) ./$(TARGET) clean: rm -f build/*.o $(TARGET)这里和 Day04 最大的区别是下面三部分SRCS $(wildcard src/*.c) OBJS $(patsubst src/%.c, build/%.o, $(SRCS)) build/%.o: src/%.c $(CC) $(CFLAGS) -c $ -o $这几句是 Day05 的核心。六、Makefile 逐句理解1. 指定编译器CC gccCC表示 C Compiler也就是 C 语言编译器。后面使用$(CC)就等价于gcc这样写的好处是如果以后想换成其他编译器只需要修改CC的值。2. 指定编译参数CFLAGS -Wall -Wextra -Iinclude其中-Wall开启常见警告-Wextra开启更多警告-Iinclude告诉 GCC 到include目录查找头文件。因为代码中包含了自己写的头文件#includesystem.h#includesensor.h#includefault.h这些头文件都放在include目录下所以需要使用-Iinclude3. 指定最终可执行文件TARGET build/day05_test这表示最终生成的可执行文件是build/day05_test运行时可以使用./build/day05_test七、wildcard自动查找源文件SRCS $(wildcard src/*.c)wildcard的作用是自动查找符合条件的文件。这里$(wildcard src/*.c)表示查找src目录下所有.c文件。如果src目录中有src/fault.c src/main.c src/sensor.c src/system.c那么SRCS $(wildcard src/*.c)最终相当于SRCS src/fault.c src/main.c src/sensor.c src/system.c这样以后如果新增一个src/uart.cMakefile 会自动找到它不需要手动修改SRCS。八、patsubst自动生成目标文件列表OBJS $(patsubst src/%.c, build/%.o, $(SRCS))patsubst是模式替换函数。它的作用是把SRCS中的.c文件路径转换成对应的.o文件路径。例如src/main.c → build/main.o src/system.c → build/system.o src/sensor.c → build/sensor.o src/fault.c → build/fault.o也就是说OBJS $(patsubst src/%.c, build/%.o, $(SRCS))最终等价于OBJS build/fault.o build/main.o build/sensor.o build/system.o这样就不需要手动写每一个.o文件了。九、模式规则一条规则编译所有 .c 文件Day04 中每个.c文件都需要单独写规则。Day05 中使用一条模式规则build/%.o: src/%.c $(CC) $(CFLAGS) -c $ -o $这句表示所有 build/xxx.o 都可以由 src/xxx.c 编译得到例如build/main.o 由 src/main.c 生成 build/system.o 由 src/system.c 生成 build/sensor.o 由 src/sensor.c 生成 build/fault.o 由 src/fault.c 生成这里的%可以理解为匹配相同的文件名。例如build/sensor.o: src/sensor.c其中%就对应sensor。十、自动变量 $ 和 $在模式规则中使用了两个自动变量$ $它们的含义是$ 表示当前规则中的第一个依赖文件 $ 表示当前规则中的目标文件例如当前规则是build/sensor.o: src/sensor.c那么$ src/sensor.c $ build/sensor.o所以$(CC) $(CFLAGS) -c $ -o $就等价于gcc-Wall-Wextra-Iinclude-csrc/sensor.c-obuild/sensor.o这样一条规则就可以适用于所有.c → .o的编译过程。十一、生成最终程序$(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET)这句表示要生成 build/day05_test 必须先生成所有 .o 文件展开后大概等价于gcc build/fault.o build/main.o build/sensor.o build/system.o-obuild/day05_test这一步是链接过程也就是把多个.o文件链接成最终可执行程序。十二、运行与清理1. 运行程序run: $(TARGET) ./$(TARGET)执行makerun等价于./build/day05_test2. 清理编译产物clean: rm -f build/*.o $(TARGET)执行makeclean会删除build/*.o build/day05_test也就是删除所有.o目标文件和最终可执行程序。十三、编译与运行进入 Day05 目录cd/root/Embedded_14Days/day05清理旧编译产物makeclean编译make查看build目录lsbuild可以看到day05_test fault.o main.o sensor.o system.o运行程序makerun运行结果如下LED state:0Voltage:9.50V Current:2.50A temperature:72.00C Fault code: 0x0007 Fault: Low voltage Fault: Over current Fault: Over temperature说明 Makefile 已经可以自动完成查找源文件 生成目标文件 链接可执行程序 运行程序十四、增量编译验证Day05 另一个重点是验证增量编译。首先在没有修改任何文件的情况下再次执行make终端输出make: Nothing to bedoneforall.这说明当前所有目标文件都是最新的不需要重新编译。然后修改src/sensor.c中的温度值sys-temperature72.0f;改成sys-temperature75.0f;保存后再次执行make此时终端只重新编译了gcc-Wall-Wextra-Iinclude-csrc/sensor.c-obuild/sensor.o然后重新链接gcc build/fault.o build/main.o build/sensor.o build/system.o-obuild/day05_test这说明 Makefile 判断出只有sensor.c被修改所以只重新生成了build/sensor.o而没有重新编译main.c system.c fault.c这就是增量编译。十五、修改后的运行结果再次执行makerun运行结果如下LED state:0Voltage:9.50V Current:2.50A temperature:75.00C Fault code: 0x0007 Fault: Low voltage Fault: Over current Fault: Over temperature可以看到温度已经从72.00 C变成了75.00 C说明修改后的sensor.c已经重新编译并生效。十六、Day04 和 Day05 的区别Day04 的 Makefile 是手动列出目标文件和规则OBJS build/main.o build/system.o build/sensor.o build/fault.o并且每个.c文件都要单独写一条规则。Day05 的 Makefile 使用自动化写法SRCS $(wildcard src/*.c) OBJS $(patsubst src/%.c, build/%.o, $(SRCS)) build/%.o: src/%.c $(CC) $(CFLAGS) -c $ -o $也就是说Day04手动列出每个 .o 文件和编译规则 Day05自动查找 .c 文件自动生成 .o 文件并用一条模式规则统一编译Day05 的 Makefile 更适合后续扩展工程。十七、遇到的问题和解决方法1. 目录路径不能直接当命令执行学习过程中曾经直接输入/root/Embedded_14Days/day05终端提示Is a directory原因是目录路径本身不是命令不能直接执行。正确进入目录的方式是cd/root/Embedded_14Days/day052. git add 的路径要看当前所在目录在上传 GitHub 时如果当前已经在day05目录中再执行gitaddday05Git 会去找day05/day05自然找不到。更清楚的做法是先回到总目录cd/root/Embedded_14Days然后执行gitaddday05这样路径就不会出错。十八、今日总结通过 Day05 的学习主要掌握了以下内容学会了使用wildcard自动查找src目录下所有.c文件学会了使用patsubst自动将.c文件列表转换成.o文件列表理解了模式规则build/%.o: src/%.c理解了自动变量$和$的作用学会了使用一条规则编译多个.c文件验证了 Makefile 的增量编译机制理解了修改单个.c文件后只需要重新编译对应.o文件进一步理解了真实 C 工程中的自动化构建流程。Day05 的核心可以总结为一句话Makefile 自动查找源文件 自动生成目标文件列表 使用模式规则统一编译 并支持增量编译。这一步对于后续学习嵌入式工程、驱动模块拆分、复杂 Makefile 和交叉编译非常重要。十九、项目源码本次 Day05 学习代码已上传至 GitHubhttps://github.com/jdai10590-afk/Embedded-C-Learning-Projects/tree/main/day05【0基础嵌入式学习日志】Day06头文件依赖追踪、.d 文件与完整增量编译一、前言今天继续进行嵌入式 C 语言基础学习。前面几天已经逐步完成了 C 工程结构、函数封装、多文件拆分、Makefile 自动化、.c → .o → 可执行文件的分步编译流程以及通过wildcard、patsubst和模式规则实现自动化编译。Day05 中Makefile 已经可以自动完成自动查找 src 目录下所有 .c 文件 自动生成 build 目录下对应的 .o 文件 使用一条模式规则编译所有 .c 文件 修改某个 .c 文件后只重新编译对应的 .o 文件但是 Day05 还存在一个问题它主要追踪.c文件的变化。如果修改的是.h头文件Makefile 不一定知道哪些.o文件需要重新编译。因此Day06 的重点是学习头文件依赖追踪也就是让 Makefile 能够知道某个 .o 文件不仅依赖对应的 .c 文件 还依赖它所包含的 .h 头文件。本次学习通过 GCC 的-MMD -MP参数自动生成.d依赖文件并在 Makefile 中引入这些.d文件从而实现更完整的增量编译。二、Day06 学习目标本次 Day06 主要学习以下内容理解为什么.h文件变化也会影响编译理解.d依赖文件的作用学会使用-MMD -MP自动生成头文件依赖信息学会通过DEPS自动生成.d文件列表学会使用-include $(DEPS)将依赖文件引入 Makefile验证修改头文件后相关.o文件能够自动重新编译进一步理解真实 C 工程中的完整增量编译机制。三、Day06 工程结构Day06 工程是在 Day05 基础上复制并修改得到的工程结构如下day06 ├── Makefile ├── include │ ├── fault_code.h │ ├── fault.h │ ├── sensor.h │ ├── system.h │ └── system_type.h ├── src │ ├── fault.c │ ├── main.c │ ├── sensor.c │ └── system.c └── build ├── fault.d ├── fault.o ├── main.d ├── main.o ├── sensor.d ├── sensor.o ├── system.d ├── system.o └── day06_test其中include存放头文件src存放源文件build存放.o目标文件、.d依赖文件和最终可执行程序Makefile管理编译、链接、运行和清理过程README.md记录项目说明。相比 Day05Day06 的build目录中多出了.d文件例如fault.d main.d sensor.d system.d这些.d文件就是今天学习的重点。四、为什么需要头文件依赖追踪在 C 工程中.h头文件通常会存放宏定义 结构体定义 函数声明 枚举类型 寄存器地址 配置参数 故障码定义例如本项目中的fault_code.h用来定义故障码#ifndefFAULT_CODE_H#defineFAULT_CODE_H#defineFAULT_NONE0x0000#defineFAULT_LOW_VOLTAGE0x0001#defineFAULT_OVER_CURRENT0x0002#defineFAULT_OVER_TEMP0x0004#defineFAULT_SENSOR_ERROR0x0008// sensor fault#endif这个头文件会被多个.c文件间接或直接使用。例如system.c 使用 fault_code.h 中的 FAULT_NONE、FAULT_LOW_VOLTAGE 等宏 fault.c 使用 fault_code.h 中的故障码宏定义如果修改了fault_code.h那么依赖它的.c文件应该重新编译。但是 Day05 的规则主要是build/%.o: src/%.c $(CC) $(CFLAGS) -c $ -o $这条规则只告诉 Makefilebuild/xxx.o 依赖 src/xxx.c并没有告诉 Makefilebuild/system.o 还依赖 include/system.h、include/system_type.h、include/fault_code.h所以 Day06 要解决的问题就是让 Makefile 自动知道每个 .o 文件到底依赖哪些 .c 和 .h 文件。五、.d 文件是什么.d文件是依赖文件。它的作用是记录某个 .o 文件依赖哪些 .c 文件和 .h 文件。例如编译system.c后会生成build/system.o build/system.d其中system.o是目标文件system.d是依赖文件。查看system.dcatbuild/system.d可以看到类似内容build/system.o: src/system.c include/system.h include/system_type.h \ include/fault_code.h include/system.h: include/system_type.h: include/fault_code.h:这表示build/system.o 依赖 src/system.c build/system.o 依赖 include/system.h build/system.o 依赖 include/system_type.h build/system.o 依赖 include/fault_code.h因此如果这些文件中的任何一个发生变化Makefile 就知道build/system.o需要重新编译。这就是.d文件的作用。六、Day06 Makefile 内容Day06 的 Makefile 内容如下CC gcc CFLAGS -Wall -Wextra -Iinclude DEPFLAGS -MMD -MP TARGET build/day06_test SRCS $(wildcard src/*.c) OBJS $(patsubst src/%.c, build/%.o, $(SRCS)) DEPS $(patsubst src/%.c, build/%.d, $(SRCS)) all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) build/%.o: src/%.c $(CC) $(CFLAGS) $(DEPFLAGS) -c $ -o $ run: $(TARGET) ./$(TARGET) clean: rm -f build/*.o build/*.d $(TARGET) -include $(DEPS)相比 Day05Day06 主要新增了三部分DEPFLAGS -MMD -MP DEPS $(patsubst src/%.c, build/%.d, $(SRCS)) -include $(DEPS)并且编译规则中增加了$(DEPFLAGS)七、Makefile 关键语句解释1. 编译器设置CC gccCC表示 C Compiler也就是 C 编译器。后面写$(CC)就等价于gcc2. 编译参数CFLAGS -Wall -Wextra -Iinclude其中-Wall开启常见警告-Wextra开启更多警告-Iinclude告诉 GCC 到include目录查找头文件。因为本项目的头文件都放在day06/include所以需要通过-Iinclude指定头文件搜索路径。3. 依赖生成参数DEPFLAGS -MMD -MP这是 Day06 的新增内容。其中-MMD让 GCC 自动生成项目头文件依赖信息 -MP为头文件生成伪目标避免头文件被删除后 Makefile 报错简单理解-MMD -MP 的作用就是让 GCC 在编译 .c 文件时同时生成 .d 依赖文件。例如编译gcc-Wall-Wextra-Iinclude-MMD-MP-csrc/system.c-obuild/system.o会同时生成build/system.o build/system.d4. 最终目标文件TARGET build/day06_test表示最终生成的可执行程序是build/day06_test5. 自动查找源文件SRCS $(wildcard src/*.c)wildcard用于自动查找src目录下所有.c文件。例如src/fault.c src/main.c src/sensor.c src/system.c都会被自动加入SRCS。6. 自动生成 .o 文件列表OBJS $(patsubst src/%.c, build/%.o, $(SRCS))这句会把.c文件路径转换成.o文件路径。例如src/main.c - build/main.o src/system.c - build/system.o src/sensor.c - build/sensor.o src/fault.c - build/fault.o最终OBJS就是build/fault.o build/main.o build/sensor.o build/system.o7. 自动生成 .d 文件列表DEPS $(patsubst src/%.c, build/%.d, $(SRCS))这句和OBJS很像。区别是OBJS 生成 .o 文件列表 DEPS 生成 .d 文件列表例如src/main.c - build/main.d src/system.c - build/system.d src/sensor.c - build/sensor.d src/fault.c - build/fault.d最终DEPS包含build/fault.d build/main.d build/sensor.d build/system.d8. 编译目标all: $(TARGET)当输入make时默认会执行第一个目标all。这句表示make 的目标是生成 build/day06_test。9. 链接最终程序$(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET)这表示要生成 build/day06_test必须先生成所有 .o 文件。展开后大概等价于gcc build/fault.o build/main.o build/sensor.o build/system.o-obuild/day06_test这一步叫链接也就是把多个.o文件链接成最终可执行程序。10. 模式规则生成 .o 和 .dbuild/%.o: src/%.c $(CC) $(CFLAGS) $(DEPFLAGS) -c $ -o $这句表示所有 build/xxx.o 都可以由 src/xxx.c 编译得到。例如build/system.o 由 src/system.c 编译得到 build/fault.o 由 src/fault.c 编译得到其中$ 表示输入文件也就是 src/xxx.c $ 表示输出目标也就是 build/xxx.o因为加入了$(DEPFLAGS)所以每次编译.c文件时不仅生成.o还会生成.d。11. 运行程序run: $(TARGET) ./$(TARGET)执行makerun会运行./build/day06_test12. 清理编译产物clean: rm -f build/*.o build/*.d $(TARGET)执行makeclean会删除build/*.o build/*.d build/day06_test也就是删除目标文件、依赖文件和最终可执行程序。13. 引入依赖文件-include $(DEPS)这是 Day06 最关键的一句。它的作用是把所有 .d 文件包含进 Makefile 让 Makefile 知道每个 .o 文件还依赖哪些 .h 文件。前面的-表示如果 .d 文件暂时不存在也不要报错。因为第一次执行make时.d文件还没有生成。如果直接写include $(DEPS)第一次可能会因为找不到.d文件而报错。所以更稳妥的写法是-include $(DEPS)八、编译与运行进入 Day06 目录cd/root/Embedded_14Days/day06清理旧文件makeclean编译make查看build目录lsbuild可以看到day06_test fault.d fault.o main.d main.o sensor.d sensor.o system.d system.o运行程序makerun运行结果如下LED state:0Voltage:9.50V Current:2.50A temperature:75.00C Fault code: 0x0007 Fault: Low voltage Fault: Over current Fault: Over temperature这说明程序编译和运行都正常。九、查看 .d 文件内容执行catbuild/system.d可以看到类似build/system.o: src/system.c include/system.h include/system_type.h \ include/fault_code.h include/system.h: include/system_type.h: include/fault_code.h:这一段说明build/system.o 依赖 src/system.c build/system.o 依赖 include/system.h build/system.o 依赖 include/system_type.h build/system.o 依赖 include/fault_code.h因此如果修改include/fault_code.hMakefile 就知道system.o需要重新编译。后面这些内容include/system.h: include/system_type.h: include/fault_code.h:是-MP自动生成的伪目标主要用于避免头文件被删除后 Makefile 报错。十、头文件依赖验证为了验证头文件依赖是否生效修改day06/include/fault_code.h将#defineFAULT_SENSOR_ERROR0x0008改成#defineFAULT_SENSOR_ERROR0x0008// sensor fault保存后再执行make终端输出中可以看到gcc-Wall-Wextra-Iinclude-MMD-MP-csrc/fault.c-obuild/fault.o gcc-Wall-Wextra-Iinclude-MMD-MP-csrc/system.c-obuild/system.o gcc build/fault.o build/main.o build/sensor.o build/system.o-obuild/day06_test这说明修改fault_code.h后Makefile 自动判断出fault.o 依赖 fault_code.h system.o 依赖 fault_code.h所以重新编译了src/fault.c - build/fault.o src/system.c - build/system.o没有重新编译src/main.c src/sensor.c这说明头文件依赖追踪已经生效。十一、Day05 和 Day06 的区别Day05 主要解决的是.c文件变化修改 sensor.c ↓ 重新编译 sensor.o ↓ 重新链接 day05_testDay06 进一步解决的是.h文件变化修改 fault_code.h ↓ Makefile 根据 .d 文件判断哪些 .o 依赖它 ↓ 重新编译 fault.o 和 system.o ↓ 重新链接 day06_test也就是说Day05支持源文件级别的增量编译 Day06支持头文件依赖级别的增量编译Day06 更接近真实 C 工程和嵌入式工程中的构建方式。十二、遇到的问题和解决方法1. -MD 和 -MMD 的区别一开始编译命令中使用过-MD-MP后来改成-MMD-MP两者区别是-MD会记录系统头文件和项目头文件 -MMD主要记录项目头文件不记录系统标准库头文件例如stdio.h这类系统头文件一般不需要在项目依赖文件中追踪。所以本项目最终使用DEPFLAGS -MMD -MP这样更加适合当前工程。2. .d 文件第一次不存在怎么办第一次执行make时.d文件还没有生成。如果直接写include $(DEPS)可能会因为找不到.d文件而报错。因此使用-include $(DEPS)前面的-表示即使文件暂时不存在也不要报错。3. build 文件夹中的 .o 和 .d 不需要上传 GitHubbuild目录中的文件属于编译产物例如fault.o main.o sensor.o system.o fault.d main.d sensor.d system.d day06_test这些文件可以通过源码和 Makefile 重新生成一般不需要上传 GitHub。GitHub 上主要保留Makefile README.md include/*.h src/*.c十三、今日总结通过 Day06 的学习主要掌握了以下内容理解了.h头文件变化也会影响.o文件理解了.d文件用于记录目标文件依赖关系学会了使用-MMD -MP自动生成.d文件学会了通过DEPS自动生成依赖文件列表学会了使用-include $(DEPS)将依赖文件引入 Makefile验证了修改头文件后相关.o文件会自动重新编译进一步理解了真实 C 工程中的完整增量编译机制。Day06 的核心可以总结为一句话.d 文件记录每个 .o 文件依赖哪些 .c 和 .h 文件 Makefile 通过包含 .d 文件实现头文件级别的增量编译。这一步对于后续学习嵌入式工程、驱动开发、模块化编程、复杂 Makefile 和交叉编译非常重要。十四、项目源码本次 Day06 学习代码已上传至 GitHubhttps://github.com/jdai10590-afk/Embedded-C-Learning-Projects/tree/main/day06