声明
本博客所记录的关于正点原子i.MX6ULL开发板的学习笔记,(内容参照正点原子I.MX6U嵌入式linux驱动开发指南,可在正点原子官方获取正点原子Linux开发板 --- 正点原子资料下载中心 1.0.0 文档),旨在如实记录我在学校学习该开发板过程中所遭遇的各类问题以及详细的解决办法。其初衷纯粹是为了个人知识梳理、学习总结以及日后回顾查阅方便,同时也期望能为同样在学习这款开发板的同学或爱好者提供一些解决问题的思路和参考。我尽力保证内容的准确性和可靠性,但由于个人知识水平和实践经验有限,若存在错误或不严谨之处,恳请各位读者批评指正。
责任声明:虽然我力求提供有效的问题解决办法,但由于开发板使用环境、硬件差异、软件版本等多种因素的影响,我的笔记内容不一定适用于所有情况。对于因参考本笔记而导致的任何直接或间接损失,我不承担任何法律责任。使用本笔记内容的读者应自行承担相关风险,并在必要时寻求专业技术支持。
1. 为什么要用 Makefile?
完成这样一个小工程,通过键盘输入两个整形数字,然后计算他们的和并将结果显示在屏幕上,在这个工程中我们有 main.c、 input.c 和 calcu.c 这三个 C 文件和 input.h、 calcu.h 这两个头文件。其中 main.c 是主体, input.c 负责接收从键盘输入的数值, calcu.c 进行任意两个数相加,
其中 main.c 文件内容如下:

input.c 文件内容如下:

calcu.c 文件内容如下:

文件 input.h 内容如下:

文件 calcu.h 内容如下:

在终端输入如下命令进行编译:
gcc main.c calcu.c input.c o main

使用命令"gcc main.c calcu.c input.c o main"看起来很简单是吧,只需要一行就可以完成编译,但是我们这个工程只有三个文件啊!如果几千个文件呢?再就是如果有一个文件被修改了以,使用上面的命令编译的时候所有的文件都会重新编译,如果工程有几万个文件(Linux 源码就有这么多文件! ),想想这几万个文件编译一次所需要的时间就可怕。最好的办法肯定是哪个文件被修改了,只编译这个被修改的文件即可,其它没有修改的文件就不需要再次重新编译了,为此我们改变我们的编译方法,如果第一次编译工程,我们先将工程中的文件都编译一遍,然后后面修改了哪个文件就编译哪个文件,命令如下:
gcc c main.c
gcc c input.c
gcc c calcu.c
gcc main.o input.o calcu.o o main
上述命令前三行分别是将 main.c、 input.c 和 calcu.c 编译成对应的.o 文件,所以使用了" c"选项,"c"选项我们上面说了,是只编译不链接。最后一行命令是将编译出来的所有.o 文件链接成可执行文件 main。假如我们现在修改了 calcu.c 这个文件,只需要将 caclue.c 这一个文件重新编译成.o 文件,然后在将所有的.o 文件链接成可执行文件。
但是如果修改的文件一多,可能都不记得哪个文件修改过了,然后忘记编译,然后......,为此我们需要这样一个工具:
1、如果工程没有编译过,那么工程中的所有.c 文件都要被编译并且链接成可执行程序。
2、如果工程中只有个别 C 文件被修改了,那么只编译这些被修改的 C 文件即可。
3、如果工程的头文件被修改了,那么我们需要编译所有引用这个头文件的 C 文件,并且链接成可执行文件。
很明显,能够完成这个功能的就是 Makefile 了,在工程目录下创建名为"Makefile"的文件,文件名一定要叫做"Makefile"!!!区分大小写的哦!!!

Makefile内容如下:

上述代码中所有行首需要空出来的地方一定要使用"TAB"键!不要使用空格键!这是Makefile 的语法要求,如果是空格就会显示这种报错

Makefile 编写好以后我们就可以使用 make 命令来编译我们的工程了,直接在命令行中输入"make"即可, make 命令会在当前目录下查找是否存在"Makefile"这个文件,如果存在的话就会按照 Makefile 里面定义的编译方式进行编译

错误来源一般有两点:
1、 Makefile 中命令缩进没有使用 TAB 键!
2、 VI/VIM 编辑器使用空格代替了 TAB 键,修改文件/etc/vim/vimrc,在文件最后面加上如下所示代码: set noexpandtab
我们修改一下 input.c 文件源码,随便加几行空行就行了,保证 input.c 被修改过即可,修改完成以后再执行一下"make"命令重新编译一下工程,结果如图

可以看出因为我们修改了 input.c 这个文件,所以 input.c 和最后的可执行文件 main 重新编译了,其它没有修改过的文件就没有编译。而且我们只需要输入"make"这个命令即可,非常方便。
2. Makefile语法
2.1 Makefile规则格式
Makefile 里面是由一系列的规则组成的,规则格式如下:
目标...... : 依赖文件集合......
命令 1
命令 2
......
比如下面这条规则:
main : main.o input.o calcu.o
gcc o main main.o input.o calcu.o
这条规则的目标是 main, main.o、 input.o 和 calcu.o 是生成 main 的依赖文件,如果要更新目标 main,就必须先更新它的所有依赖文件,如果依赖文件中的任何一个有更新,那么目标也必须更新,"更新"就是执行一遍规则中的命令列表。
命令列表中的每条命令必须以 TAB 键开始,不能使用空格!make 命令会为 Makefile 中的每个以 TAB 开始的命令创建一个 Shell 进程去执行。
示例Makefile,代码如下:
main: main.o input.o calcu.o
gcc o main main.o input.o calcu.o
main.o: main.c
gcc c main.c
input.o: input.c
gcc c input.c
calcu.o: calcu.c
gcc c calcu.c
clean:
rm *.o
rm main
上述代码中一共有 5 条规则
第一条规则依赖于文件 main.o、 input.o 和 calcu.o 这个三个.o 文件,这三个.o 文件目前还都没有,因此必须先更新这三个文件。 make 会查找以这三个.o 文件为目标的规则并执行。以 main.o为例,发现更新 main.o 的是第二条规则,因此会执行第二条规则,第二条规则里面的命令为"gcc --c main.c",这行命令很熟悉了吧,就是不链接编译 main.c,生成 main.o,其它两个.o 文件同理。最后一个规则目标是 clean,它没有依赖文件,因此会默认为依赖文件都是最新的,所以其对应的命令不会执行,当我们想要执行 clean 的话可以直接使用命令"make clean",执行以后就会删除当前目录下所有的.o 文件以及 main,因此 clean 的功能就是完成工程的清理,"make clean"

当执行"make clean"命令以后,前面编译出来的.o 和 main 可执行文件都被删除掉了,也就是完成了工程清理工作。
总结一下 Make 的执行过程:
1、 make 命令会在当前目录下查找以 Makefile(makefile 其实也可以)命名的文件。
2、当找到 Makefile 文件以后就会按照 Makefile 中定义的规则去编译生成最终的目标文件。
3、当发现目标文件不存在,或者目标所依赖的文件比目标文件新(也就是最后修改时间比目标文件晚)的话就会执行后面的命令来更新目标。
这就是 make 的执行过程, make 工具就是在 Makefile 中一层一层的查找依赖关系,并执行相应的命令。编译出最终的可执行文件。Makefile 的好处就是"自动化编译",一旦写好了 Makefile文件,以后只需要一个 make 命令即可完成整个工程的编译,极大的提高了开发效率。Makefile 中规则用来描述在什么情况下使用什么命令来构建一个特定的文件,这个文件就是规则的"目标",为了生成这个"目标"而作为材料的其它文件称为"目标"的依赖,规则的命令是用来创建或者更新目标的。
除了 Makefile 的"终极目标"所在的规则以外,其它规则的顺序在 Makefile 中是没有意义的,"终极目标"就是指在使用 make 命令的时候没有指定具体的目标时, make 默认的那个目标,它是 Makefile 文件中第一个规则的目标,如果 Makefile 中的第一个规则有多个目标,那么这些目标中的第一个目标就是 make 的"终极目标"。
2.2 Makefile变量
跟 C 语言一样 Makefile 也支持变量的
main**:** main**.** o input**.** o calcu**.**o
gcc o main main**.** o input**.** o calcu**.**o
上述 Makefile 语句中, main.o input.o 和 calcue.o 这三个依赖文件,我们输入了两遍,我们这个 Makefile 比较小,如果 Makefile 复杂的时候这种重复输入的工作就会非常费时间,而且非常容易输错,为了解决这个问题, Makefile 加入了变量支持。不像 C 语言中的变量有 int、 char等各种类型, Makefile 中的变量都是字符串!类似 C 语言中的宏。使用变量将上面的代码修改,修改以后如下所示:
这段 Makefile 代码定义了一个变量 `objects`,它包含了几个目标文件。然后通过 `(objects)\` 引用该变量来指定生成 \`main\` 可执行文件所依赖的目标文件,以及链接时使用的目标文件。第 1 行是注释, Makefile 中可以写注释,注释开头要用符号"#",不能用 C 语言中的"//"或者"//"!第 2 行我们定义了一个变量 objects,并且给这个变量进行了赋值,其值为字符串"main.o input.o calcu.o",第 3 和 4行使用到了变量 objects, Makefile 中变量的引用方法是"(变量名)"

在定义变量 objects 的时候使用"="对其进行了赋值, Makefile变量的赋值符还有其它两个":="和"?=",我们来看一下这三种赋值符的区别:
1、赋值符"="
"="主要用来给变量赋值。借助它,你能够把一个值或者一组值赋予一个变量,之后就可以在 Makefile 里通过$(变量名)这种方式引用该变量。
2、赋值符":="
赋值符":="不会使用后面定义的变量,只能使用前面已经定义好的
3、赋值符"?="
如果变量前面没有被赋值,那么此变量为新复制的变量,如果前面已经赋过值了,那么就使用前面赋的值。
4、变量追加"+="
Makefile 中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进去,此时就要使用到符号"+=",比如如下所示代码:
objects = main.o inpiut.o
objects += calcu.o
一开始变量 objects 的值为"main.o input.o",后面我们给他追加了一个"calcu.o",因此变量 objects 变成了"main.o input.o calcu.o",这个就是变量的追加。
2.3 Makefile 模式规则
模式规则中,至少在规则的目标定定义中要包涵"%",否则就是一般规则,目标中的"%"表示对文件名的匹配,"%"表示长度任意的非空字符串,比如"%.c"就是所有的以.c 结尾的文件,类似与通配符, a.%.c 就表示以 a.开头,以.c 结束的所有文件。
当"%"出现在目标中的时候,目标中"%"所代表的值决定了依赖中的"%"值,使用方法如下:
%.o : %.c
命令
之前的Makefile 可以改为如下形式:

命令我们需要借助另外一种强大的变量---自动化变量。
所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,自动化变量只应该出现在规则的命令中,常用的自动化变量如下所示:

7 个自动化变量中,常用的三种: @、 <和$^,我们使用自动化变量来完Makefile

2.4 Makefile伪目标
Makefile 有一种特殊的目标------伪目标,一般的目标名都是要生成的文件,而伪目标不代表真正的目标名,在执行 make 命令的时候通过指定这个伪目标来执行其所在规则的定义的命令。使用伪目标主要是为了避免 Makefile 中定义的执行命令的目标和工作目录下的实际文件出现名字冲突。比如在前面的Makefile代码中有如下代码用来完成清理工程的功能:
clean:
rm *.o
rm main
上述规则中并没有创建文件 clean 的命令,因此工作目录下永远都不会存在文件 clean,当我们输入"make clean"以后,后面的"rm *.o"和"rm main"总是会执行。可是如果我们"手贱",在工作目录下创建一个名为"clean"的文件,那就不一样了,当执行"make clean"的时候,规则因为没有依赖文件,所以目标被认为是最新的,因此后面的 rm 命令也就不会执行,我们预先设想的清理工程的功能也就无法完成。为了避免这个问题,我们可以将 clean 声明为伪目标,声明方式如下:
.PHONY : clean
我们使用伪目标来更改,结果如下

2.5 Makefile 条件判断
Makefile 支持多种条件判断方式,主要包括以下几种形式:
-
ifeq/ifneq判断是否相等/不相等
ifeq ($(VAR), value)
# 如果 VAR 等于 "value" 则执行这里
else
# 否则执行这里
endififneq ($(VAR), value)
# 如果 VAR 不等于 "value" 则执行这里
endif -
ifdef/ifndef 判断变量是否定义
ifdef VAR
# 如果 VAR 已定义则执行这里
endififndef VAR
# 如果 VAR 未定义则执行这里
endif -
shell 命令的条件判断
ifeq ($(shell test -f file.txt && echo yes), yes)
# 如果 file.txt 存在则执行这里
endif -
多条件组合
ifneq ((filter (VAR), value1 value2),)
# 如果 VAR 是 value1 或 value2 则执行这里
endif -
条件赋值
VAR := (if (COND),value_if_true,value_if_false)
注意事项
-
条件语句必须顶格写,不能有缩进
-
条件判断中的变量最好使用 `$(strip)` 处理空白字符
-
`ifeq` 比较的是字面值,不是展开后的值
-
可以使用 `$(error)` 在条件不满足时报错
2.6 Makefile 函数使用
Makefile 提供了丰富的内置函数,用于文本处理、文件名操作、条件判断等。这些函数可以大大增强 Makefile 的灵活性和功能。
一、常用函数分类
- 字符串处理函数
`$(subst from,to,text)` 字符串替换
`$(patsubst pattern,replacement,text)` 模式替换
`$(strip string)` 去除首尾空格
`$(findstring find,in)` 查找子串
`$(filter pattern...,text)` 过滤匹配模式的单词
`$(filterout pattern...,text)` 过滤掉匹配模式的单词
- 文件名函数
`$(wildcard pattern)` 扩展通配符
`$(dir names...)` 提取目录部分
`$(notdir names...)` 提取非目录部分
`$(suffix names...)` 提取后缀
`$(basename names...)` 提取前缀
`$(addsuffix suffix,names...)` 添加后缀
`$(addprefix prefix,names...)` 添加前缀
- 流程控制函数
`$(if condition,thenpart[,elsepart])` 条件判断
`$(or condition1[,condition2...])` 或运算
`$(and condition1[,condition2...])` 与运算
- 其他实用函数
`$(foreach var,list,text)` 循环处理
`$(call variable,param,...)` 调用用户定义函数
`$(value variable)` 获取变量未展开的值
`$(eval text)` 动态生成Makefile内容
`$(shell command)` 执行shell命令
二、函数使用示例
-
字符串替换
SRC := a.c b.c c.c
OBJ := (patsubst %.c,%.o,(SRC))OBJ 现在是 a.o b.o c.o
-
文件名操作
FILES := src/main.c src/util.c include/util.h
DIRS := (dir (FILES))DIRS 现在是 src/ src/ include/
-
条件判断
DEBUG := 1
CFLAGS := (if (DEBUG),-g -O0,-O2) -
循环处理
MODULES := foo bar baz
MODULE_OBJS := (foreach m,(MODULES),$(m).o)MODULE_OBJS 现在是 foo.o bar.o baz.o
-
调用shell命令
CURRENT_DATE := $(shell date +%Y-%m-%d)
-
用户定义函数
reverse = (if (1),(call reverse,(wordlist 2,(words (1)),(1)))) (firstword (1)) REVERSED := (call reverse,a b c d)
REVERSED 现在是 d c b a
三、高级用法
-
组合使用函数
SOURCES := (wildcard src/*.c) OBJECTS := (patsubst src/%.c,build/%.o,$(SOURCES))
-
动态生成规则
define make-object
(1): (2)
gcc -c $$< -o $$@
endef(foreach src,(SOURCES),
(eval (call make-object,(patsubst src/%.c,build/%.o,(src)),$(src))) -
错误检查
ifeq ((wildcard (CONFIG_FILE)),)
(error Config file (CONFIG_FILE) not found)
endif
四、注意事项
-
函数调用时参数之间不要有多余的空格
-
变量引用使用 `(VAR)\` 或 \`{VAR}`
-
`$(shell)` 函数会为每次调用启动新的shell进程
-
`$(eval)` 可以动态生成Makefile内容,但要小心使用
-
复杂的逻辑最好用shell脚本实现,再通过`$(shell)`调用
五、调试技巧
-
使用`$(info ...)`打印调试信息
-
使用`$(warning ...)`发出警告
-
使用`$(error ...)`终止执行并报错
(info SOURCES = (SOURCES))
(warning OBJECTS = (OBJECTS))