0%

【杰理】杰理SDK-Makefile-编译构建下载流程分析

前言

学习理解Makefile编译构建过程,有助于新手开发者熟悉SDK文件框架构成、配置编译链接选项、增删文件编译、了解各bin文件作用及下载升级流程、进行程序空间优化等。特别是可视化SDK,其即是基于Makefile使用命令行执行编译、下载等操作,默认不使用CodeBlock IDE。

那么本文即以杰理jl701n_soundbox_release_v1.4.2/sdk为例,讲述杰理SDK-Makefile及目标程序固件的编译构建过程,其它无论是可视化或非可视化SDK均可参考此篇文章。

另外,本文主要为讲述基础原理、Makefile编译构建以及程序烧录升级过程,当然也包括部分环境及相关的配置开发介绍,但并不会详细指导如何搭建开发环境。

Makefile解析

Makefile通用结构简述

PS: 设计 Makefile 的目的是配置通过命令行自动执行程序,代替手动的编译操作,从而实现一键编译、下载等。

通用 Makefile 目标形式:

  • makemake allmake -j

    命令行执行make表示默认执行第一个目标,等同于make all(相当于执行预设的编译构建下载全流程,但不是重新编译而是增量编译)
    make -j表示使用最大线程数并行执行默认目标,加快编译速度

  • make clean表示清除所有编译中间文件,先执行此条指令再make相当于 rebuild

    在终端输入make clean;make -j,指示 Shell 终端串行执行此两条指令 rebuild


Makefile 通常为以下的组织形式:

  • 各项变量集合定义,包括编译器路径、编译选项、源文件路径、链接选项等
  • 定义构建目标的层层依赖关系,以及达成目标的命令集合(会应用到以上自动化变量)

从 Makefile 内容来看,自上而下,通常的结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
工具链路径/可执行程序路径
编译参数
全局宏定义
头文件搜索路径集合
源文件路径集合
链接参数

编译依赖声明集合

---

默认目标(all):依赖目标1 ...依赖目标n(如prebuild、固件sdk.elf等)
命令...

其它目标(clean):
命令...(rm -rf *)

依赖目标1(prebuild):
命令...

依赖目标2(sdk.elf):依赖目标2-1 ...依赖目标2-n
命令...

依赖目标n-n:
命令...

Makefile 作为一个编译构建工具,其构建原理相当于:定义构建目标作为树形结构的顶点,然后通过层层依赖关系得到最底层的所有命令行集合。执行完所有的命令依赖,即可完成目标。


编译工具链路径设置

Windows环境下,通常默认安装杰理工具链路径位于C:/JL,而701-SDK/br28内核的编译链可执行文件路径则位于C:/JL/pi32/bin,工具说明如下:

1
2
3
4
5
TOOL_DIR := C:/JL/pi32/bin
CC := clang.exe #
CXX := clang.exe # 定义C、C++编译器为clang,用于生成.o目标文件
LD := pi32v2-lto-wrapper.exe # 定义链接器,用于生成可执行文件
AR := llvm-ar.exe # 定义归档工具,用于生成.a静态库

除编译工具外,还有相关的辅助构建工具,其可执行程序位于/SDK/tools/utils,通常将此部分工具添加到系统环境变量中,便于编译时调用

1
2
3
4
MKDIR := mkdir_win -p   # mkdir_win.exe,用于递归创建多级目录
RM := rm -rf # rm.exe,用于删除文件/目录
FIXBAT := tools\utils\fixbat.exe # 用于处理 utf8->gbk 编码问题
...

Question:为什么在SDK里指定编译器绝对路径,而不是声明到环境变量中?但辅助构建工具却是可以添加到环境变量中?

因为不同SDK对应的cpu内核可能不一致,其编译链虽都是clang,但存在差异,有pi32或pi32v2的,声明同名编译器到环境变量会导致混乱
而辅助构建工具比如创建目录、删除、打包等,则是通用的命令


工具链自带的库文件、头文件路径如下:

1
2
SYS_LIB_DIR := C:/JL/pi32/pi32v2-lib/r3-large
SYS_INC_DIR := C:/JL/pi32/pi32v2-include

SYS_LIB_DIRSYS_INC_DIR用于编译链接时,查找系统库和头文件,通常包含了标准C库、数学库的实现以及函数声明,如SYS_LIB_DIR下包含libc.alibm.a等,而SYS_INC_DIR下包含stdint.hstdio.hstdlib.h等。


以下为设置编译生成的中间文件路径,以及输出sdk.elf文件(elf描述了链接后生成的可执行文件)

1
2
3
4
5
# 输出文件设置
OUT_ELF := cpu/br28/tools/sdk.elf
OBJ_FILE := $(OUT_ELF).objs.txt
# 编译路径设置
BUILD_DIR := objs

其中sdk.elf是编译工具链输出的产物,而最终的.ufw固件是由cpu/br28/tools/download.bat对 sdk.elf 进一步处理得到的

编译参数

编译参数,通常无须过多关注,保持默认即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CFLAGS := \
-target pi32v2 \
-mcpu=r3 \
-integrated-as \
-flto \
-Wuninitialized \
-Wno-invalid-noreturn \
-fno-common \
-integrated-as \
-Oz \ # 表示最大限度的编译优化
-g \ # 生成调试信息,g0 g1 g2 g3 g,其中-g等于g3,表示最大优化
-flto \
-fallow-pointer-null \
-fprefer-gnu-section \
-Wno-shift-negative-value \
-Wundef \
-Wframe-larger-than=256 \
-Wincompatible-pointer-types \ # 警告不兼容的指针类型转换
-Wreturn-type \ # 警告缺少返回值的函数
-Wimplicit-function-declaration \ # 缺失函数警告
-mllvm -pi32v2-large-program=true \
-fms-extensions \
-fdiscrete-bitfield-abi \
-w \ # 默认禁用所有编译警告

全局宏定义设置

节选如下,用于定义全局的宏定义,有需要可以自行在此添加,但通常不建议这样做

1
2
3
4
5
6
7
8
9
10
DEFINES := \
-DSUPPORT_MS_EXTENSIONS \
-DCONFIG_RELEASE_ENABLE \
-DCONFIG_CPU_BR28 \
...
-DCONFIG_SOUNDBOX \
-DEVENT_HANDLER_NUM_CONFIG=2 \
-DEVENT_TOUCH_ENABLE_CONFIG=0 \
-DEVENT_POOL_SIZE_CONFIG=256 \
...

头文件搜索路径、源文件设置

二次业务开发时,新建的.h头文件,需要将其路径添加到如下,使得编译器能够找到包含的头文件

1
2
3
4
5
6
7
8
9
INCLUDES := \
-Iinclude_lib \
-Iinclude_lib/driver \
......
-Iapps/common \
-Iapps/common/device \
-Iapps/common/audio \
-Iapps/common/audio/live_audio \
......

二次业务开发时,新建的.c源文件,需要将其路径添加到如下,使得编译器能够对源文件进行编译并链接进目标程序

1
2
3
4
5
6
7
8
# 需要编译的 .c 文件
c_SRC_FILES := \
apps/common/audio/amplitude_statistic.c \
apps/common/audio/audio_digital_vol.c \
apps/common/audio/audio_export_demo.c \
apps/common/audio/audio_utils.c \
apps/common/audio/decode/audio_key_tone.c \
......

其它用户新建的源文件,如.s、.S、.cpp等,自行添加即可,但一般不会用到(.cc .cxx .cpp都是属于C++源文件扩展名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 需要编译的 .S 文件
S_SRC_FILES := \
apps/soundbox/sdk_version.z.S \

# 需要编译的 .s 文件
s_SRC_FILES :=

# 需要编译的 .cpp 文件
cpp_SRC_FILES :=

# 需要编译的 .cc 文件
cc_SRC_FILES :=

# 需要编译的 .cxx 文件
cxx_SRC_FILES :=

链接参数设置(添加静态库时用到)

节选如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 链接参数
LFLAGS := \
--plugin-opt=-pi32v2-always-use-itblock=false \
--plugin-opt=-enable-ipra=true \
--gc-sections \ # 删除未使用的section,减少程序占用
--start-group \ #
cpu/br28/liba/cpu.a \
cpu/br28/liba/system.a \
...
# 添加静态库时,按需求添加在--start-group和--end-group之间
...
cpu/br28/liba/res.a \
--end-group \ # 与--start-group搭配
-Tcpu/br28/sdk.ld \ # 指定链接脚本 sdk.ld,控制内存布局和段定位
-M=cpu/br28/tools/sdk.map \ # 配置生成map文件,记录链接信息,用于调试分析
--plugin-opt=mcpu=r3 \
--plugin-opt=-mattr=+fprev1 \

核心点是注意--start-group--end-group,此组参数的作用是保证顺序链接、控制不同库文件中的同名函数/符号优先级、避免多个库中同名符号冲突

进行库添加时,规范库的链接顺序
如,打补丁需要添加新的静态库时,须注意。顺序不当,有可能会导致链接报错。


比如如下,链接器会优先从 liba.a 中查找 函数/变量 实现,然后是 libb.a,最后是 libc.a

1
2
3
4
5
--start-group
liba.a
libb.a
libc.a
--end-group

预构建流程

Makefile所在目录终端,输入命令make -j,即开始编译,其中all是默认目标,而pre_build是其依赖的第一个目标,即每开始正式编译前,都会先进行一次预构建过程

1
2
3
4
5
6
7
8
9
10
11
12
all: pre_build $(OUT_ELF)
$(info +POST-BUILD)
$(QUITE) $(RUN_POST_SCRIPT) sdk

# 预构建
pre_build:
$(info +PRE-BUILD)
$(QUITE) $(CC) $(CFLAGS) $(DEFINES) $(INCLUDES) -D__LD__ -E -P cpu/br28/sdk_used_list.c -o cpu/br28/sdk_used_list.used
$(QUITE) $(CC) $(CFLAGS) $(DEFINES) $(INCLUDES) -D__LD__ -E -P cpu/br28/sdk_ld.c -o cpu/br28/sdk.ld
$(QUITE) $(CC) $(CFLAGS) $(DEFINES) $(INCLUDES) -D__LD__ -E -P cpu/br28/tools/download.c -o $(POST_SCRIPT)
$(QUITE) $(FIXBAT) $(POST_SCRIPT)
$(QUITE) $(CC) $(CFLAGS) $(DEFINES) $(INCLUDES) -D__LD__ -E -P cpu/br28/tools/isd_config_rule.c -o cpu/br28/tools/isd_config.ini

预构建流程是杰理SDK编译前的必要步骤,其主要作用包括以下:

  • 处理sdk_used_list.c得到sdk_used_list.used,在链接参数中会引用此文件,主要是起到记录、声明作用

    链接参数--plugin-opt=-used-symbol-file=cpu/br28/sdk_used_list.used
    表示链接器会去匹配寻找该文件里的符号(函数/变量声明),将这个符号在库中的实现链接到sdk.ld指定的地址。


  • 处理cpu/br28/tools/sdk_ld.c得到sdk.ld,主要是生成链接脚本,控制内存布局

  • 处理cpu/br28/tools/download.c得到download.bat,生成顶层下载脚本

    从sdk.elf提取不同节,得到相应的二进制文件,并组合成app.bin等操作
    调用子脚本download.bat,进行二进制文件打包和程序下载流程


  • $(FIXBAT) $(POST_SCRIPT) 调用fixbat.exe对download.bat处理编码转换问题

  • 处理 cpu/br28/tools/isd_config_rule.c 得到 isd_config.ini

    子脚本download.bat 调用 isd_download.exe 进行实际的flash下载(isd_config.ini为下载配置文件)
    如:..\..\isd_download.exe ..\..\isd_config.ini -tonorflash -dev br28 -boot 0x120000 -div8 -wait 300 -uboot ..\..\uboot.boot -app ..\..\app.bin -res tone.cfg ..\..\cfg_tool.bin ..\..\eq_cfg_hw.bin p11_code.bin -uboot_compress


源文件构建流程

  • 使用通配符和转换规则,使c_OBJS, S_OBJS, s_OBJS, cpp_OBJS, cxx_OBJS, cc_OBJS分别映射包含所有源文件编译生成对应的 .o 目标文件
  • 定义OBJS包含所有类型的目标文件;定义DEP_FILES包含所有的.d依赖文件,用于增量编译判断用。
  • 修改 OBJS 和 DEP_FILES 的值,增加BUILD_DIR前缀,使得所有构建过程输出都位于BUILD_DIR目录下
  • VERBOSE控制编译过程输出详细日志,默认为0,可设置为1,如make VERBOSE=1; LINK_AT用于决定是否使用file函数
  • 定义构建伪目标all、pre_build、clean,规则、编译、依赖文件包含等
    1
    2
    3
    4
    5
    6
    7
    all依赖于pre_build和OUT_ELF(即sdk.elf)(层层目标依赖,直至源文件->目标文件的编译)
    直接输入make时,默认执行make all。
    其执行完编译链接流程后,最后调用脚本`cpu\br28\tools\download.bat`进行下载

    pre_build用于执行预处理步骤,如生成配置文件、链接脚本、下载脚本

    clean用于清理构建过程产生的中间文件,如rm -rf BUILD_DIR xxx

链接脚本、固件打包程序、下载脚本解析

sdk_ld.c分析

maskrom_stubs.ld

涉及到 update_flag VM区 等的地址链接定位

涉及到 配置 代码放RAM 的链接配置

cpu/brxx/tools/download.c分析

暂无

说明如何打包固件等

子脚本download.bat分析

在可视化SDK中,子脚本download.bat 已经合并进 download.c 了

执行实际的下载流程

下载/升级固件构成分析

由download.c以及子脚本download.bat可得到,实际下载到芯片的flash构成,以及各固件的关系

1
2
3
4
5
6
7
单备份: jl_isd.bin + VM区 + 蓝牙配置区 = 程序flash占用空间

jl_isd.bin = (isd_download.exe) uboot.boot + app.bin + tone_zh.cfg + cfg_tool.bin + p11_code.bin + stream.bin

app.bin = text.bin + data.bin + data_code.bin + aec.bin + aac.bin + ps_ram_data_code.bin

update.ufw = jl_isd.ufw = ufw_make.exe (jl_isd.fw + ota.bin)

VM区大小须满足 大于 提示各种升级所需的最小空间
其中 VM区 和 蓝牙配置区 的大小,可以在isd_config.ini中查看。

压缩代码方法: 配置能关则关,音频编解码能关尽关, 提示音文件可以改成 16bit-16KHz-单声道的mp3文件

哪些文件是烧录的,哪些文件是用于ota的,哪些文件是干嘛的?

思考与拓展

杰理CodeBlock工程是如何进行构建编译的?与SDK根目录的Makefile构建方式的具体异同?哪个效率更优?

暂无解答

make与codeblock-IDE的构建工具 差异对比?

归根到底无论是IDE还是Makefile,都是调用同一个编译器对源文件进行编译链接,编译速度理论上无差异。但构建组织速度存在差异

Code::Blocks可以配置使用Makefile,也可以使用其内置的构建系统。另,可以通过配置ccache工具加速编译构建

Maskrom的程序跟下载的固件有相关性吗?

Maskrom是出厂自带的只读程序,主要用于引导启动、集成固件烧录功能等?

暂不了解 Maskrom uboot app 的详细流程

可以用C++编写杰理程序吗?

理论上可以,只要编译器支持即可

毕竟编程语言只是程序流程描述,编译器负责将其解析其适合于芯片cpu的机器码。但越是高级的语言,其附带的特性就越多,因此会占用更多的资源。不建议用于资源紧张的嵌入式开发

什么情况下需要rebuild?

全局宏配置、功能的开启/关闭,有可能会影响到预构建相关的文件sdk_used_list.c、sdk_ld.c、download.c、isd_config_rule.c里预编译条件变化,需要rebuild。
当链接静态库有更新时,必须要 rebuild。如果静态库有引用到相关的配置宏的,也需要 rebuild。

常规的.c源文件、.h头文件修改,不需要rebuild。
因为在Makefile中,包含进编译源文件的路径集合,已经被全部声明了依赖关系。

但静态库文件、预构建文件这部分没有被声明依赖关系的文件,即使有更新,make也不会检测到。需要rebuild,清除已有的编译中间文件,重新进行编译链接

什么情况下需要擦除flash和VM?

需要清除记忆的配置信息,如蓝牙配对信息、用户信息等,则擦除VM

OTA升级前后的程序VM区大小配置不一致,会导致升级失败?

旧程序配置了VM为48K,新程序配置为64K,验证了仅有此部分差异情况下,OTA升级不成功。

可以通过分析链接脚本,查看程序的内存排布。

VM区的大小配置,会影响到全局程序区的地址偏移链接,导致对指定地址区域的升级文件校验失败?

配置 Makefile 下载时默认擦除所有: 在download.bat增加 -format all ?

编译工具链 addr2line 的使用

由上文解释可知,sdk.elf是链接后生成的elf文件,其包含所有编译的源文件、编译的函数、编译的变量等,通过addr2line工具,可以直接反编译出指定地址的函数名、变量名等。

ps: map、lst 文件都是由elf文件衍生的

addr2line工具用于将内存地址转换为源代码中的函数名和行号

参数: -e 指定 elf文件,-a 显示地址,-f 显示函数名,-i 显示行号

在终端(如 PowerShell )输入命令执行示例如下:addr2line -e sdk.elf -a -f 0x0011BE9E 0x00119A40 0x00119A9A 0x06003AF8
alt text

可视化SDK界面点击编译、擦除VM、擦除flash等,具体流程是怎样的?

特别地,在可视化SDK根目录下,也存在一个download.bat文件

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@echo off
:: 将命令的第一个参数作为xx路径
SET PROJ_DOWNLOAD_PATH=%1

:: 第二个参数用于定义环境变量是否擦除VM、FLASH
if %2==format_flash (
SET FORMAT_ALL_ENABLE=1
SET FORMAT_VM_ENABLE=0
)
if %2==format_vm (
SET FORMAT_VM_ENABLE=1
SET FORMAT_ALL_ENABLE=0
)
if %2==download (
SET FORMAT_ALL_ENABLE=0
SET FORMAT_VM_ENABLE=0
)

cpu\br27\tools\download.bat

最终都会执行下载流程,区别在于设置当前终端的临时环境变量

Makefile 嵌套调用与递归调用

通过嵌套 Makefile,可以将大型项目分解成多个Makefile文件,提高可维护性和可读性:

  • 主Makefile:位于项目的根目录下,负责整体的构建流程
  • 子Makefile:可以有多个,位于项目的子目录下,负责特定模块或组件的构建

主Makefile可以通过 include 指令来包含合并子Makefile的内容,也可以通过 $(MAKE) 指令递归调用子Makefile的目标。

参考站点