前言
学习理解Makefile编译构建过程,有助于新手开发者熟悉SDK文件框架构成、配置编译链接选项、增删文件编译、了解各bin文件作用及下载升级流程、进行程序空间优化等。特别是可视化SDK,其即是基于Makefile使用命令行执行编译、下载等操作,默认不使用CodeBlock IDE。
那么本文即以杰理jl701n_soundbox_release_v1.4.2/sdk
为例,讲述杰理SDK-Makefile及目标程序固件的编译构建过程,其它无论是可视化或非可视化SDK均可参考此篇文章。
另外,本文主要为讲述基础原理、Makefile编译构建以及程序烧录升级过程,当然也包括部分环境及相关的配置开发介绍,但并不会详细指导如何搭建开发环境。
Makefile解析
Makefile通用结构简述
PS: 设计 Makefile 的目的是配置通过命令行自动执行程序,代替手动的编译操作,从而实现一键编译、下载等。
通用 Makefile 目标形式:
make
、make all
、make -j
命令行执行
make
表示默认执行第一个目标,等同于make all
(相当于执行预设的编译构建下载全流程,但不是重新编译而是增量编译)make -j
表示使用最大线程数并行执行默认目标,加快编译速度make clean
表示清除所有编译中间文件,先执行此条指令再make
相当于 rebuild在终端输入
make clean;make -j
,指示 Shell 终端串行执行此两条指令 rebuild
Makefile 通常为以下的组织形式:
- 各项变量集合定义,包括编译器路径、编译选项、源文件路径、链接选项等
- 定义构建目标的层层依赖关系,以及达成目标的命令集合(会应用到以上自动化变量)
从 Makefile 内容来看,自上而下,通常的结构为:
1 | 工具链路径/可执行程序路径 |
Makefile 作为一个编译构建工具,其构建原理相当于:定义构建目标作为树形结构的顶点,然后通过层层依赖关系得到最底层的所有命令行集合。执行完所有的命令依赖,即可完成目标。
编译工具链路径设置
在Windows
环境下,通常默认安装杰理工具链路径位于C:/JL
,而701-SDK/br28
内核的编译链可执行文件路径则位于C:/JL/pi32/bin
,工具说明如下:
1 | TOOL_DIR := C:/JL/pi32/bin |
除编译工具外,还有相关的辅助构建工具,其可执行程序位于/SDK/tools/utils
,通常将此部分工具添加到系统环境变量中,便于编译时调用
1 | MKDIR := mkdir_win -p # mkdir_win.exe,用于递归创建多级目录 |
Question:为什么在SDK里指定编译器绝对路径,而不是声明到环境变量中?但辅助构建工具却是可以添加到环境变量中?
因为不同SDK对应的cpu内核可能不一致,其编译链虽都是clang,但存在差异,有pi32或pi32v2的,声明同名编译器到环境变量会导致混乱
而辅助构建工具比如创建目录、删除、打包等,则是通用的命令
工具链自带的库文件、头文件路径如下:
1 | SYS_LIB_DIR := C:/JL/pi32/pi32v2-lib/r3-large |
SYS_LIB_DIR
和SYS_INC_DIR
用于编译链接时,查找系统库和头文件,通常包含了标准C库、数学库的实现以及函数声明,如SYS_LIB_DIR
下包含libc.a
、libm.a
等,而SYS_INC_DIR
下包含stdint.h
、stdio.h
、stdlib.h
等。
以下为设置编译生成的中间文件路径,以及输出sdk.elf文件(elf描述了链接后生成的可执行文件)
1 | # 输出文件设置 |
其中sdk.elf是编译工具链输出的产物,而最终的.ufw固件是由cpu/br28/tools/download.bat
对 sdk.elf 进一步处理得到的
编译参数
编译参数,通常无须过多关注,保持默认即可
1 | CFLAGS := \ |
全局宏定义设置
节选如下,用于定义全局的宏定义,有需要可以自行在此添加,但通常不建议这样做
1 | DEFINES := \ |
头文件搜索路径、源文件设置
二次业务开发时,新建的.h头文件,需要将其路径添加到如下,使得编译器能够找到包含的头文件
1 | INCLUDES := \ |
二次业务开发时,新建的.c源文件,需要将其路径添加到如下,使得编译器能够对源文件进行编译并链接进目标程序
1 | # 需要编译的 .c 文件 |
其它用户新建的源文件,如.s、.S、.cpp等,自行添加即可,但一般不会用到(.cc .cxx .cpp都是属于C++源文件扩展名)
1 | # 需要编译的 .S 文件 |
链接参数设置(添加静态库时用到)
节选如下
1 | # 链接参数 |
核心点是注意--start-group
和--end-group
,此组参数的作用是保证顺序链接、控制不同库文件中的同名函数/符号优先级、避免多个库中同名符号冲突
进行库添加时,规范库的链接顺序
如,打补丁需要添加新的静态库时,须注意。顺序不当,有可能会导致链接报错。
比如如下,链接器会优先从 liba.a 中查找 函数/变量 实现,然后是 libb.a,最后是 libc.a
1 | --start-group |
预构建流程
在Makefile
所在目录终端,输入命令make -j
,即开始编译,其中all
是默认目标,而pre_build
是其依赖的第一个目标,即每开始正式编译前,都会先进行一次预构建过程
1 | all: pre_build $(OUT_ELF) |
预构建流程是杰理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
7all依赖于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 | 单备份: jl_isd.bin + VM区 + 蓝牙配置区 = 程序flash占用空间 |
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
可视化SDK界面点击编译、擦除VM、擦除flash等,具体流程是怎样的?
特别地,在可视化SDK根目录下,也存在一个download.bat文件
如下:
1 | @echo off |
最终都会执行下载流程,区别在于设置当前终端的临时环境变量
Makefile 嵌套调用与递归调用
通过嵌套 Makefile,可以将大型项目分解成多个Makefile文件,提高可维护性和可读性:
- 主Makefile:位于项目的根目录下,负责整体的构建流程
- 子Makefile:可以有多个,位于项目的子目录下,负责特定模块或组件的构建
主Makefile可以通过 include
指令来包含合并子Makefile的内容,也可以通过 $(MAKE)
指令递归调用子Makefile的目标。