前一篇 讨论了 Make 变量从哪里来、怎样覆盖。接下来还要弄清楚:配置文件中的开关,怎样影响 C 源码里的 #ifdef?

一、从变量到宏的传递过程

程序主要读取什么在构建中负责什么
MakeMakefile、.mk 文件根据配置安排构建,并组织传给编译器的参数
C 编译器.c、.h 等源码文件根据宏和源码内容,生成目标文件

写在 .mk 文件中的变量,是给 Make 使用的。C 编译器不会自动读取这些变量。

先用一个简单的 MY_FEATURE 开关说明。

1.1 定义 Make 变量

假设 .mk 文件里写了:

MY_FEATURE = 1

MY_FEATURE 是 Make 变量,Make 用 $(MY_FEATURE) 取值。例如,判断这个开关是否等于 1:

ifeq ($(MY_FEATURE),1)
# 在这里安排开启功能时的构建操作
endif

1.2 export:传递给子 Make

如果顶层 Make 另外启动子 Make 来构建模块,后面的子 Make 也可能需要这个开关。这时可以前面加 export 将变量传过去:

export MY_FEATURE = 1

export 会把变量带给后续子进程,子 Make 因而也能读取 MY_FEATURE。

这里说的子 Make,是另外启动的 Make 进程。如果是同一个 Make, 通过 include 读入多个配置文件时,变量可以在该进程内使用,无需为了 include 单独加 export。

注意:变量传到了子 Make,C 编译器仍然不会自动得到同名宏,所以 cpp文件仍然无法使用mk 中定义的宏

1.3 -D:定义编译宏

要让 .mk 中的开关影响 C 编译,需要把对应的宏定义选项加入模块编译参数。这个 SDK 会用 ccflags-y 收集这些参数,例如:

ifeq ($(MY_FEATURE),1)
ccflags-y += -DMY_FEATURE
endif

开关为 1 时,模块参数 ccflags-y 就会多一个 -DMY_FEATURE。

这里的 -D 才是编译器识别的宏定义选项。-DMY_FEATURE 表示本次编译时定义 MY_FEATURE,效果相当于:

#define MY_FEATURE 1

ccflags-y 是这套 SDK 用来收集模块编译参数的变量;构建规则会把它的内容并入编译命令,再交给编译器。实际命令可以简化看成下面这样:

arm-none-eabi-gcc -DMY_FEATURE -c demo.c -o demo.o

其中 demo.c 是输入源码,demo.o 是编译生成的目标文件。编译器处理 demo.c 时会带着 MY_FEATURE 这个宏,C 源码就可以用条件编译判断是否保留相关代码:

#ifdef MY_FEATURE
    /* 这部分功能代码参与编译 */
#endif
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart LR
    N0["Make 变量<br/>MY_FEATURE=1"]
    N1["模块添加<br/>-DMY_FEATURE"]
    N2["条件分支参与编译<br/>#ifdef MY_FEATURE"]
    N0 -->|"ifeq 条件成立"| N1
    N1 -->|"编译器定义宏"| N2
    class N0,N1,N2 blue;
    classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
    classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
    classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;

二、在 BES 中追踪开关与宏的对应关系

回到工程里的 EQ 开关,Make 变量和 C 宏用的是两个名字:

正文配图 图 1:audio_process/Makefile 第 113~114 行添加编译宏

第 113 行判断 HW_DAC_IIR_EQ_PROCESS,第 114 行添加 -D__HW_DAC_IIR_EQ_PROCESS__。开关为 1,编译器就会定义后一个宏。

C 源码中的 #ifdef此时就要用后一个名字,Makefile 只是负责把两边对应起来。

三、检查配置是否生效

这里要确认的是:前面加到 ccflags-y 的 -D__HW_DAC_IIR_EQ_PROCESS__,有没有真的出现在编译 audio_process.c 的命令里。

执行下面的构建命令:

make T=best1306p V=1 -j8

其中,T=best1306p 选择本次构建目标;V=1 让构建过程打印完整的编译命令;-j8 表示并行使用 8 个任务,只影响构建速度。

在输出中找到编译 audio_process.c 的那条命令,确认里面有 -D__HW_DAC_IIR_EQ_PROCESS__。省略其他编译参数后,大致会看到:

arm-none-eabi-gcc ... -D__HW_DAC_IIR_EQ_PROCESS__ ... -c audio_process.c -o audio_process.o

判断时看这三种情况:

  • 有 audio_process.c 的编译命令,且带有 -D__HW_DAC_IIR_EQ_PROCESS__:宏参与了这次编译。
  • 有该文件的编译命令,但没有这个 -D 参数:这条配置添加的宏参数没有传入本次编译。
  • 没有该文件的编译命令:这次构建没有重新编译 audio_process.c。增量构建会跳过输入文件和编译命令都没有变化的目标;

如果想从头编译 best1306p 这个目标,可以先清理它的构建输出,再重新编译:

make T=best1306p allclean
make T=best1306p V=1 -j8

allclean 会删除 out/best1306p 中该目标的生成文件,第二条命令会重新构建。完整构建会比增量构建花更长时间,V=1 也会打印大量编译命令。

这一步只能确认宏参数进入了编译命令。要确认功能在设备上实际运行,还要烧录本次生成的文件,再结合初始化、触发条件和设备日志判断。

上一篇:编译配置(02):构建目标与配置覆盖

下一篇:编译配置(04):源码怎样编译成固件