前一篇 说明了配置怎样变成编译参数。除了参数,构建系统还需要知道哪些目录、哪些源文件参与构建。这里继续借助本地 BES SDK,从源码清单查到编译命令,再看目标文件怎样链接成固件。

一、源码选择与编译

1.1 core-y:选择顶层目录

要选择参与构建的源码,先要确定它们所在的目录。这份 SDK 在 target.mk 中通过 core-y 列出顶层构建目录:

正文配图 图 1:target.mk 用 core-y 指定顶层构建目录

当前面没有指定 EXT_SRC_DIR 时,会把 apps/ 和 services/ 加入这部分构建列表。后续再进入这些目录,读取各自的 Makefile。

1.2 obj-y:列出子目录和目标文件

core-y 加入 services/ 后,构建系统会读取 services/Makefile。里面的 obj-y 再决定这个目录下哪些内容参与构建。

在这套构建系统里,obj-y 是当前目录的构建清单,里面既可以写子目录,也可以写目标文件。清单的名称和处理方式由 SDK 的构建脚本约定,Make 本身不会因为变量叫 obj-y 就自动编译它。

(1)子目录条目

在 services/Makefile 的 obj-y 列表中,可以找到:

正文配图 图 2:services/Makefile 用 obj-y 列出子目录

audio_process/ 末尾带 /,表示还要进入这个子目录,继续读取 services/audio_process/Makefile。

(2)目标文件条目

进入 services/audio_process/ 后,里面有这些源文件:

audio_process.c
audio_process_vol.c
audio_spectrum.cpp

这些源码需要经过编译,生成对应的 .o 文件。例如,audio_process.c 编译后生成 audio_process.o。

.o 叫作“目标文件”,是源码编译后的中间产物,后面还要参与汇总和链接,才能形成最终固件。

如果 obj-y 清单中包含 audio_process.o,就表示本次需要构建这个目标,构建系统再按照规则找到对应源码进行编译。

(3)自动收集源码

audio_process/Makefile 没有逐个填写 .o,而是先收集当前目录中的源码,再把对应的目标文件名放入 obj-y:

正文配图 图 3:audio_process/Makefile 收集源码并列出目标文件名

其中:

  • obj_c 保存收集到的 .c 文件名。
  • obj_cpp 保存收集到的 .cpp 文件名。
  • obj_s 保存收集到的 .S 汇编文件名。

接着通过图中第 13 行代码生成目标文件列表:

obj-y := $(obj_c:.c=.o) $(obj_s:.S=.o) $(obj_cpp:.cpp=.o)

例如:

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
    subgraph MAP["清单中的文件名转换:尚未编译"]
        direction LR
        A["audio_process.c"] --> B["audio_process.o"]
        C["audio_process_vol.c"] --> D["audio_process_vol.o"]
        E["audio_spectrum.cpp"] --> F["audio_spectrum.o"]
    end
    class A,C,E blue;
    class B,D,F green;
    style MAP fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
    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;

这里改的是清单里的文件名,还没有真正生成 .o 文件。

(4)手动列出目标文件

同一份工程也有手动清单,以 A2DP 音频解码模块为例。这里分两步:先选中解码目录,再列出目录里需要编译的文件。

前面的 core-y 将 apps/ 纳入构建后,apps/Makefile 又列出了 audioplayers/。继续读取 apps/audioplayers/Makefile 时,会根据 A2DP_DECODER_VER 选择解码实现。当前项目在 target.mk 中把它设为 2,所以会选中 a2dp_decoder/ 这个子目录:

正文配图

图中第 10 行先把 a2dp_decoder/ 加入 obj_cpp,后面再汇入 obj-y。末尾的 / 表示继续进入子目录,读取其中的 Makefile。

进入 apps/audioplayers/a2dp_decoder/Makefile 后,才看到手动清单:直接写出要构建的 .o 文件名。

正文配图

第 4 行把 a2dp_decoder.o 加入清单,对应源码是同目录下的 a2dp_decoder.cpp。第 6~7 行表示:A2DP_SMF 不为 1 时,再加入 SBC 解码文件。

第 12~17 行仍在这个分支内:A2DP_AAC_ON=1 时,追加 a2dp_decoder_aac_lc.o,对应 a2dp_decoder_aac_lc.cpp。因此,源码放在目录里,还需要清单把它选进来;这里就通过开关选择需要的解码文件。

以后新增源码时,可以照所在目录的写法处理:audio_process 这种目录会自动收集源码;a2dp_decoder 这种目录则需要在清单里补上对应的 .o,并按功能需要放进相应的条件分支。

(5)完整构建流程

把前面两种方式放在一起看:先进入对应目录,再生成 obj-y 目标清单,最后由通用规则调用编译器。区别在于清单是自动收集生成,还是直接写出目标文件名。

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 26, "nodeSpacing": 20, "padding": 0}, "themeVariables": {"lineColor": "#71829a"}, "themeCSS": "\n.build-step {width:224px;text-align:center;white-space:nowrap;text-wrap:nowrap;}\n.build-step-box {height:100px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;font-size:18px;line-height:1.45;text-wrap:nowrap;}\n.build-blue {border:1.5px solid #7ba4d8;background:#e8f2ff;color:#23456b;}\n.build-green {border:1.5px solid #78aa8b;background:#e7f4ec;color:#24563a;}\n.build-amber {border:1.5px solid #d5a25b;background:#fff3df;color:#72501e;}\n.build-step-note {padding-top:12px;padding-bottom:4px;font-size:15px;line-height:1.4;font-weight:500;text-wrap:nowrap;}\n.build-note-blue {color:#23456b;}\n.build-note-green {color:#24563a;}\n.build-step span {color:inherit;}\n"}}%%
flowchart TB
    %% 每个节点包含上方代码框和下方步骤说明,均由 Mermaid 渲染。
    subgraph AUTO["① 自动收集:audio_process"]
        direction LR
        A0["<div class='build-step'><div class='build-step-box build-blue'><span>target.mk<br/>core-y 选择 services/</span></div><div class='build-step-note build-note-blue'>① 选定顶层构建目录</div></div>"]
        A1["<div class='build-step'><div class='build-step-box build-blue'><span>services/Makefile<br/>进入 audio_process/</span></div><div class='build-step-note build-note-blue'>② 找到音频处理模块</div></div>"]
        A2["<div class='build-step'><div class='build-step-box build-blue'><span>audio_process/Makefile<br/>wildcard 收集源码<br/>替换后缀,生成 obj-y</span></div><div class='build-step-note build-note-blue'>③ 自动整理目标清单</div></div>"]
        A3["<div class='build-step'><div class='build-step-box build-amber'><span>通用编译规则<br/>源码编译成 .o</span></div><div class='build-step-note build-note-blue'>④ 编译源码,生成 .o</div></div>"]
        A0 --> A1 --> A2 --> A3
    end
    style AUTO fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
    subgraph MANUAL["② 手动清单:a2dp_decoder"]
        direction LR
        M0["<div class='build-step'><div class='build-step-box build-green'><span>target.mk → apps/<br/>apps/Makefile<br/>进入 audioplayers/</span></div><div class='build-step-note build-note-green'>① 找到音频播放模块</div></div>"]
        M1["<div class='build-step'><div class='build-step-box build-green'><span>audioplayers/Makefile<br/>A2DP_DECODER_VER=2<br/>进入 a2dp_decoder/</span></div><div class='build-step-note build-note-green'>② 按版本选择解码目录</div></div>"]
        M2["<div class='build-step'><div class='build-step-box build-green'><span>a2dp_decoder/Makefile<br/>直接列出 .o<br/>按开关追加到 obj-y</span></div><div class='build-step-note build-note-green'>③ 手动填写目标清单</div></div>"]
        M3["<div class='build-step'><div class='build-step-box build-amber'><span>通用编译规则<br/>源码编译成 .o</span></div><div class='build-step-note build-note-green'>④ 编译源码,生成 .o</div></div>"]
        M0 --> M1 --> M2 --> M3
    end
    style MANUAL fill:#f0f8f3,stroke:#b9d7c4,color:#24563a
    AUTO ~~~ MANUAL
    classDef stage fill:transparent,stroke:transparent;
    class A0,A1,A2,A3,M0,M1,M2,M3 stage;

1.3 调用编译器生成 .o

源码清单列好后,就由通用规则调用编译器。模块用 ccflags-y 收集 C 编译参数。这些参数会在 scripts/lib.mk 中与其他编译选项汇总,最终放入编译命令使用的 c_flags 中。

实际编译规则在 scripts/build.mk 中:

正文配图 图 4:scripts/build.mk 中的编译命令和源码匹配规则

看图中第 236~237 行:% 匹配相同的文件名主体。目标是 audio_process.o,就找到 audio_process.c,再按规则调用编译命令。

第 229 行是命令本身:$(CC) 是编译器,$(c_flags) 是整理好的参数,$< 和 $@ 分别是输入源码、输出目标文件。

以第三篇中的 EQ 开关 为例,启用它时添加的 -D__HW_DAC_IIR_EQ_PROCESS__ 会进入这些参数,并随编译命令交给编译器。

二、链接与固件生成

2.1 链接生成 ELF

各个源文件编译成 .o 后,还需要通过链接,将不同文件中的代码、数据和函数引用组织起来。在这份示例工程中,链接先生成 .elf,再从中提取 .bin 固件二进制。

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart LR
    N0["各个源文件"]
    N1["编译成 .o"]
    N2["链接生成 .elf"]
    N3["提取 .bin"]
    N0 --> N1
    N1 --> N2
    N2 --> N3
    class N0,N1,N2,N3 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;

正文配图 图 5:顶层 Makefile 中的 ELF 链接命令

2.2 提取 BIN 固件

ELF 保存链接后的程序以及符号、调试等信息;BIN 是从中提取出的原始二进制内容,供后续处理或烧录流程使用。

链接完成后,构建系统通过 OBJCOPY 将 ELF 中的相关内容提取为 BIN:

正文配图 图 6:顶层 Makefile 从 ELF 提取 BIN 的规则

本例提取出的文件是 out/best1306p/best1306p.bin。如果后续升级还需要补充 CRC 信息,可以接着执行第一篇介绍的后处理步骤 。烧录或升级时,应使用对应流程要求的文件。

上一篇:编译配置(03):Make 变量怎样变成 C 宏