联盛德 HLK-W806 (十二): Makefile组织结构和编译流程说明

目录

  • 联盛德 HLK-W806 (一): Ubuntu20.04下的开发环境配置, 编译和烧录说明
  • 联盛德 HLK-W806 (二): Win10下的开发环境配置, 编译和烧录说明
  • 联盛德 HLK-W806 (三): 免按键自动下载和复位
  • 联盛德 HLK-W806 (四): 软件SPI和硬件SPI驱动ST7735液晶LCD
  • 联盛德 HLK-W806 (五): W801开发板上手报告
  • 联盛德 HLK-W806 (六): I2C驱动SSD1306 128x64 OLED液晶屏
  • 联盛德 HLK-W806 (七): 兼容开发板 LuatOS Air103
  • 联盛德 HLK-W806 (八): 4线SPI驱动SSD1306/SSD1315 128x64 OLED液晶屏
  • 联盛德 HLK-W806 (九): 软件SPI和硬件SPI驱动ST7789V液晶LCD
  • 联盛德 HLK-W806 (十): 在 CDK IDE开发环境中使用WM-SDK-W806
  • 联盛德 HLK-W806 (十一): 软件SPI和硬件SPI驱动ST7567液晶LCD
  • 联盛德 HLK-W806 (十二): Makefile组织结构和编译流程说明
  • 联盛德 HLK-W806 (十三): 运行FatFs读写FAT和exFat格式的SD卡/TF卡

Makefile基本概念

下面这些是在项目的Makefile中会用到的, 主要就说一下赋值和$方法

赋值=, :=, ?=, +=

Makefile中的赋值, 可以赋值一个列表, 例如
VAR = dir1 dir2 dir3

= 赋值

make会将整个Makefile展开后, 再决定变量的值, 也就是说变量的值会是整个 Makefile 中最后被指定的值

x = foo
y = $(x) bar
x = xyz

y的值会是 xyz bar ,而不是 foo bar

:= 赋值

表示变量的值等于 Makefile 执行到此处时的值, 而不是整个 Makefile 展开后的最终值

x := foo
y := $(x) bar
x := xyz

y的值将会是 foo bar ,而不是 xyz bar

:=赋值可以用来避免递归问题, 例如下面的赋值

CC = gcc
CC = ${CC}

all:
    @echo ${CC}

会报错

$ make
Makefile:8: *** Recursive variable 'CC' references itself (eventually).  Stop.

如果改成:=赋值就没问题了

CC := gcc
CC := ${CC}

all:
    @echo ${CC}

?= 赋值

  • 如果已经赋值, 就不变
  • 如果没有被赋值过, 就赋予等号后面的值

+= 赋值

添加等号后面的值

Makefile 中$的用法

$在Makefile中是一种重要的符号

$(...) 或 ${...}的方法

$()${}是一样的, 执行括号内的内容, 类似于eval, 将整个$(...)替换为执行的结果字符串

直接执行

例如$(VAR = 4)表示执行shell命令VAR = 4, 因为无返回, 所以不会有任何操作

shell 执行shell命令

$(shell cc $(SDK_TOOLS)/wm_getver.c -Wall -O2 -o $(VER_TOOL)) 这个命令表示, 在shell下执行后面的命令

abspath 返回绝对路径

PATH = $(abspath $(TOP_DIR)) 获取绝对路径

dir 返回目录字符串

SUBDIRS ?= $(patsubst %/,%,$(dir $(wildcard */Makefile))), 获取所有Makefile的目录, 并去除结尾的斜杆

substr 字符串替换

TARGET ?= $(subst FROM, TO, TEXT), 这个命令会将字符串TEXT中的子串FROM变为TO后返回, 然后返回的值赋值给TARGET

patsubst 匹配替代

patsubst是patten substitude的缩写,匹配替代的意思

OBJ = $(patsubst %.c, %.o, $(SRC)) , 在SRC中找到所有.c 结尾的文件,然后把所有的.c换成.o。

与使用OBJ = $(SRC:%.c=%.o)效果一样.

wildcard 列出文件

$(wildcard pattern) 是用于用于匹配列出文件名的方法

  • SRCS := $(wildcard *.c)这个表达式中, 所有以.c结尾的文件名都会被赋值给变量SRCS.
  • CCFILES += $(wildcard src/*.cpp), 将匹配后面通配符的文件都列出, 并追加赋值给CCFILES

$(变量名:% = %) 替代

BINS := $(SRCS:%.c=%) 这种方式属于替代赋值, 在这个表达式中, 如果变量 SRCS 的值为 'foo.c bar.c', 变量 BINS 将被赋值 'foo bar'.

notdir 去除目录信息

SRC = $(notdir wildcard), 去除所有的目录信息,SRC里的文件名列表将只有文件名

foreach 循环处理

格式为 $(foreach var, list, express), 把参数list中的单词逐一取出放到参数var所指定的变量中, 然后再执行express所包含的表达式. 每一次express会返回一个字符串, 循环过程中express的所返回的每个字符串会以空格分隔, 最后当整个循环结束时, express所返回的每个字符串所组成的整个字符串(以空格分隔)就是foreach函数的返回值.

所以: var是一个变量, list可以是一个表达式或者一个列表变量, 而express一般是一个命令表达式, 用于处理var.

例子一
在递归编译中常用的 $(foreach d, $(SUBDIRS), $(MAKE) -C $(d);), 将SUBDIRS中收集到的目录, 值依次执行$(MAKE) -C $(d).

例子二

names := a b c d
files := $(foreach n,$(names),$(n).o)

将names中的值依次加上.o, 再赋值给files, $(files)的值变为a.o b.o c.o d.o

WM-SDK-W806 的 Makefile 分析

Makefile 文件结构

代码地址

  • GitHub wm-sdk-w806
  • Gitee wm-sdk-w806

下面列出了 WM-SDK-W806 中, 与make相关的文件, 可以看到这是一个递归make的结构. 主要的文件都已经在结构中标出

│  Makefile                  # 主Makefile文件, make 执行入口
├─app
│  │  Makefile
│  ├─inc
│  └─src
│          Makefile
├─bin
│  └─build
│      └─W806
│          ├─lib
│          └─obj
├─demo
│     Makefile
│
├─platform
│  ├─arch
│  │  │  Makefile
│  │  └─xt804
│  │      │  Makefile
│  │      ├─bsp
│  │      │      Makefile
│  │      └─libc
│  │              Makefile
│  ├─component
│  │  │  Makefile
│  │  ├─auto_dl
│  │  │      Makefile
│  │  └─FreeRTOS
│  │      │  Makefile
│  │      ├─include
│  │      └─portable
│  │          │  Makefile
│  │          ├─MemMang
│  │          │      Makefile
│  │          └─xt804
│  │                  Makefile
│  └─drivers
│          Makefile
└─tools
    └─W806
        │  .config            # 由menuconfig生成的,被conf.mk包含的配置信息
        │  conf.mk            # 全局make配置文件, 会被每一个Makefile头部包含
        │  inc.mk             # 全局的include配置, 在conf.mk底部包含, 所以实际上也会被每一个Makefile包含
        │  mconfig.sh         # make menuconfig时调起的shell脚本
        │  rules.mk           # 全局的make目标文件, 会被每一个Makefile底部包含
        │  wconfig            # menuconfig的字段配置文件
        ├─projects
        └─utilities

Make 的执行顺序

执行make时,

  1. 先执行主Makefile,
  2. 主Makefile中依次包含conf.mk, inc.mk, rule.mk
  3. 行进到rule.mk, 在其中中执行指定的目标, 如果未指定, 则执行默认的all目标.

Make的规则流程 - RULE.MK

这里重点分析rule.mk文件, 因为这里定义了所有的规则, 以及对应的目标处理关系. 当执行默认的all目标时, 其规则定义为

all: .subdirs $(OBJS) $(OLIBS) $(OIMAGES) $(OBINS)

目标 .subdir

这一步实现了对子目录的递归访问

  1. 在all目标中, 第一个前置目标为.subdirs
  2. .subdirs目标的执行是@set -e; $(foreach d, $(SUBDIRS), $(MAKE) -C $(d);),
    • set -e使得执行的命令返回值非0时, make立即退出
    • $(foreach ...)的功能前面写了, 在这里就是遍历SUBDIRS中收集到的子目录列表, 对每一项依次执行make -C 子目录
    • SUBDIRS的定义SUBDIRS ?= $(patsubst %/,%,$(dir $(wildcard */Makefile))) , 注意, 这里只是列出当前目录的下一级目录中包含的Makefile, 如果是跨一级目录的Makefile是不会包含进来的
    • SUBDIRS除了收集的子目录, 还有一处从主Makefile添加的platform目录下的三个路径
  3. 到这一步, make进行到了下一层, 对下一层同样进行前面的处理
  4. 当进行到最底层时, 已经没有可执行的.subdirs目标, make会开始执行后面的目标$(OBJS)

目标: $(OBJS)

这一步实现了从C, CPP, S文件到对象文件的编译. OBJS的定义如下, 收集了当前目录下的.c, .cpp, .S文件, 产生.o的对象文件列表, 因此这里会处理一系列的.o文件前置目标

OBJS := $(CSRCS:%.c=$(OBJODIR)/$(subdir_path)/%.o) \
        $(CPPSRCS:%.cpp=$(OBJODIR)/$(subdir_path)/%.o) \
        $(ASRCS:%.S=$(OBJODIR)/$(subdir_path)/%.o)

对于.o文件的规则是如下的三条, 这是编译时实际执行的命令

$(OBJODIR)/$(subdir_path)/%.o: %.c
	@mkdir -p $(OBJODIR)/$(subdir_path)
	$(CC) $(if $(findstring $<,$(DSRCS)),$(DFLAGS),$(CFLAGS)) $(COPTS_$(*F)) $(INCLUDES) $(CMACRO) -c "$<" -o "$@" -MMD -MD -MF "$(@:$(OBJODIR)/$(subdir_path)/%.o=$(OBJODIR)/$(subdir_path)/%.o.d)" -MT "$(@)"

$(OBJODIR)/$(subdir_path)/%.o: %.cpp
	@mkdir -p $(OBJODIR)/$(subdir_path)
	$(CXX) $(if $(findstring $<,$(DSRCS)),$(DFLAGS),$(CXXFLAGS)) $(COPTS_$(*F)) $(INCLUDES) $(CMACRO) -c "$<" -o "$@" -MMD -MD -MF "$(@:$(OBJODIR)/$(subdir_path)/%.o=$(OBJODIR)/$(subdir_path)/%.o.d)" -MT "$(@)"

$(OBJODIR)/$(subdir_path)/%.o: %.S
	@mkdir -p $(OBJODIR)/$(subdir_path)
	$(ASM) $(ASMFLAGS) $(INCLUDES) $(CMACRO) -c "$<" -o "$@"

在目标$(OBJS)完成后, 开始执行目标$(OLIBS)

目标:$(OLIBS)

这一步是将对象文件打包成库, 来源定义为

OLIBS := $(GEN_LIBS:%=$(LIBODIR)/%)

其中GEN_LIBS定义在每一层的Makefile中, 是一个层层打包的过程, 为生成库文件, 设计了两套规则

  1. 在rule.mk中定义了名为 ShortcutRule 的规则宏用于展开
define ShortcutRule
$(1): .subdirs $(2)/$(1)
endef

通过以下语句展开为每个库的目标规则

$(foreach lib,$(GEN_LIBS),$(eval $(call ShortcutRule,$(lib),$(LIBODIR))))
  1. 在rule.mk中定义了名为 MakeLibrary 的规则宏用于展开
define MakeLibrary
DEP_LIBS_$(1) = $$(foreach lib,$$(filter %$(LIB_EXT),$$(COMPONENTS_$(1))),$$(LIBODIR)/$$(notdir $$(lib)))
DEP_OBJS_$(1) = $$(foreach obj,$$(filter %.o,$$(COMPONENTS_$(1))),$$(OBJODIR)/$$(notdir $$(obj)))
$$(LIBODIR)/$(1)$(LIB_EXT): $$(OBJS) $$(DEP_OBJS_$(1)) $$(DEP_LIBS_$(1)) $$(DEPENDS_$(1))
	@mkdir -p $$(LIBODIR)
	$$(if $$(filter %$(LIB_EXT),$$?),@mkdir -p $$(OBJODIR)/_$(1))
	$$(if $$(filter %$(LIB_EXT),$$?),@cd $$(OBJODIR)/_$(1); $$(foreach lib,$$(filter %$(LIB_EXT),$$?),$$(AR) $(ARFLAGS_2) $$(UP_EXTRACT_DIR)/$$(notdir $$(lib));))
	$$(AR) $(ARFLAGS) $$@ $$(filter %.o,$$?) $$(if $$(filter %$(LIB_EXT),$$?),$$(OBJODIR)/_$(1)/*.o)
	$$(if $$(filter %$(LIB_EXT),$$?),@$$(RM) -r $$(OBJODIR)/_$(1))
endef

通过以下语句展开为各个库的目标规则, 这些规则将负责处理 ShortcutRule 的前置目标

$(foreach lib,$(GEN_LIBS),$(eval $(call MakeLibrary,$(basename $(lib)))))

这就是$(OLIBS)目标的机制

目标:$(OBINS)

这一步会打包为二进制image, 来源定义为

OBINS := $(GEN_BINS:%=$(BINODIR)/%)

其中GEN_BINS在主Makefile中定义, 就是W806.bin

GEN_BINS = $(TARGET).bin

首先也是通过 ShortcutRule 展开的规则集, 用于处理目标 $(OBINS)

$(foreach bin,$(GEN_BINS),$(eval $(call ShortcutRule,$(bin),$(BINODIR))))

然后通过下面的规则, 去处理 $(ODIR)/$(TARGET)/bin/W806.bin目标

$(BINODIR)/%.bin: $(IMAGEODIR)/%.elf
	@mkdir -p $(FIRMWAREDIR)
	@mkdir -p $(FIRMWAREDIR)/$(TARGET)
	$(OBJCOPY) -O binary $(IMAGEODIR)/$(TARGET).elf $(FIRMWAREDIR)/$(TARGET)/$(TARGET).bin

再通过 ShortcutRule 展开的规则集, 处理目标 $(IMAGEODIR)/%.elf

$(foreach image,$(GEN_IMAGES),$(eval $(call ShortcutRule,$(image),$(IMAGEODIR))))

再通过 MakeImage 展开的规则集, 处理目标 $$(IMAGEODIR)/W806.elf

define MakeImage
DEP_LIBS_$(1) = $$(foreach lib,$$(filter %$(LIB_EXT),$$(COMPONENTS_$(1))),$$(LIBODIR)/$$(notdir $$(lib)))
DEP_OBJS_$(1) = $$(foreach obj,$$(filter %.o,$$(COMPONENTS_$(1))),$$(OBJODIR)/$$(notdir $$(obj)))
$$(IMAGEODIR)/$(1).elf: $$(OBJS) $$(DEP_OBJS_$(1)) $$(DEP_LIBS_$(1)) $$(DEPENDS_$(1))
	@mkdir -p $$(IMAGEODIR)
	$(LINK) -Wl,--gc-sections -Wl,-zmax-page-size=1024 -Wl,--whole-archive $$(OBJS) $$(DEP_OBJS_$(1)) $$(DEP_LIBS_$(1)) $$(if $$(LINKFLAGS_$(1)),$$(LINKFLAGS_$(1))) -Wl,--no-whole-archive $(LINKFLAGS) $(MAP) -o $$@
endef

How Patterns Match 模式匹配的工作机制

$(BUILD_DIR)/%.o: %.cxx这样的 目标:前提 定义, 都是属于模式匹配, 模式匹配仅在目标模式匹配了文件名, 并且此规则所有的前提都存在或可以构建的情况下才生效. A pattern rule can be used to build a given file only if there is a target pattern that matches the file name, and all prerequisites in that rule either exist or can be built.

如果有多个规则都满足上面的条件, 则会有一些优先级的判断. 例如文件存在的就比需要用其它规则满足的优先级高. The rules you write take precedence over those that are built in. Note however, that a rule whose prerequisites actually exist or are mentioned always takes priority over a rule with prerequisites that must be made by chaining other implicit rules.

这时候还是有这样的可能性, 多个规则都满足这个条件, 这时候 make 会选择最短的匹配(匹配最精确的那个). 如果多个规则都是最短, 则选第一个. It is possible that more than one pattern rule will meet these criteria. In that case, make will choose the rule with the shortest stem (that is, the pattern that matches most specifically). If more than one pattern rule has the shortest stem, make will choose the first one found in the makefile.

This algorithm results in more specific rules being preferred over more generic ones; for example:

%.o: %.c
  $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
%.o : %.f
  $(COMPILE.F) $(OUTPUT_OPTION) $<
lib/%.o: lib/%.c
  $(CC) -fPIC -c $(CFLAGS) $(CPPFLAGS) $< -o $@

如果给定上面这些规则要构建 bar.o, 并且文件 bar.c 和 bar.f 存在, make 就会选择第一条规则, 如果 bar.c 不存在, 就会选择选择第二条规则. 如果要构建的是 lib/bar.o, 并且 lib/bar.c 和 lib/bar.f 都存在, 那么就会使用第三条规则, 因为这条规则匹配是最精确的. 但是如果 lib/bar.c 文件不存在, 那么就会选用第二条规则.

新增lib时的Makefile编写

以在component中添加FatFs为例, 可以参看这个提交 feat: add fatfs lib 中的修改.

确定目录结构

对于FatFs, 只有一个目录FatFs, 不区分 inc 和 src, 放置的路径是 platform/component/FatFs

将头文件路径加入 tools/W806/inc.mk

因为头文件所在的目录是platform/component/FatFs, 所以直接将这个目录加到inc.mk
加入的内容为

INCLUDES += -I $(TOP_DIR)/platform/component/FatFs

如果这个lib包含多个头文件目录, 需要将这些目录分别加入.

添加lib的Makefile

因为只有一级目录, 所以添加一个Makefile就可以, 将这个lib命名为libfatfs, 于是Makefile的内容为

TOP_DIR = ../../..
sinclude $(TOP_DIR)/tools/W806/conf.mk

ifndef PDIR
GEN_LIBS = libfatfs$(LIB_EXT)
endif

#DEFINES +=

sinclude $(TOP_DIR)/tools/W806/rules.mk
INCLUDES := $(INCLUDES) -I $(PDIR)include
PDIR := ../$(PDIR)
sinclude $(PDIR)Makefile

以上内容可以直接复制使用, 只需要

  1. 修改 GEN_LIBS = libfatfs$(LIB_EXT) 这句
  2. 如果还有子目录, 则还需要添加 COMPONENTS_libfatfs, 本例没有所以不需要添加

将这个lib添加到上一级的Mikefile

上一级的路径是/platform/component, 在Makefile的COMPONENTS_libwmcomponent中添加libfatfs, 变成

COMPONENTS_libwmcomponent = FreeRTOS/libwmrtos$(LIB_EXT) \
                            FatFs/libfatfs$(LIB_EXT) \
                            auto_dl/libautodl$(LIB_EXT) \
                            ascii_fonts/libasciifonts$(LIB_EXT)

这样就可以了. 如果添加的是顶级lib, 没有更高一级的lib了, 那么需要将这个lib加入到rules.mk中.

在 app 目录下添加用户自定义的lib, 和上面的方法也是一样的

结束

以上就是WM-SDK-W806 的 Makefile 分析. 这里只说明了编译的主体流程, 有问题请留言

你可能感兴趣的:(联盛德 HLK-W806 (十二): Makefile组织结构和编译流程说明)