目录
[3.1. 结构化配置描述](#3.1. 结构化配置描述)
[3.2. 目标描述](#3.2. 目标描述)
[3.3. 编译器抽象](#3.3. 编译器抽象)
[3.4. 外部工具](#3.4. 外部工具)
一、摘要
本博客描述了一个使用 CMake 构建 TF-A 的构建系统的方案,并在此基础上开发一个可复用的嵌入式项目 CMake 框架。
二、引言
TF-A 当前基于 Makefile 的构建系统已经变得复杂且难以维护,因此有必要引入一种更加灵活的解决方案。建议使用 CMake 语言来构建新的系统。做出这一决定的主要原因如下:
-
CMake 是一个成熟稳定的工具,被开源项目广泛接受;
-
TF-M 已经在使用 CMake,统一 tf.org 项目使用的工具能减少分裂;
-
CMake 相比 Make 有多个优势,例如:
-
项目对主机与目标系统无感知;
-
支持项目模块化、可扩展性强;
-
支持软件集成;
-
原生支持多种工具集成(例如生成各种 IDE 项目文件、cppcheck 静态分析集成等)。
-
当然也有一些缺点:
-
CMake 的语言存在问题(如变量作用域);
-
并非专为嵌入式场景设计。
为了克服这些问题,我们需要为某些任务创建变通方案,比如封装 CMake 函数等。由于这些功能也可用于其他嵌入式项目,因此将它们收集并抽象为一个可重用框架,存放于独立仓库是有益的。下图展示了该框架的结构:

三、主要特性
3.1. 结构化配置描述
在当前 Makefile 构建系统中,构建配置描述、验证、处理以及目标创建和源文件描述等内容混杂在多个文件中。该框架的目标之一就是对这些内容进行组织与结构化。
框架提供了一种结构化方式来描述输入构建参数、编译标志、宏等内容。它提供了两个工具:
-
Map:简单的键值对实现;
-
Group:一组相关 map 的集合。
相关的参数应被打包到一个 group(或"配置组")中。这些配置组应在配置文件中定义和填充。目前配置文件是手动编写的,但未来可以使用类似 Kconfig 的配置管理工具自动生成这些文件。因此,框架本身不进行参数验证与冲突检查,这些应交由配置工具完成。
3.2. 目标描述
框架提供了一个名为 STGT(simple target) 的 API 用于描述构建目标,即构建输出、使用哪些源文件、链接哪些库等。该 API 对 CMake 的目标函数进行了封装和扩展,并可使用上一节所定义的配置组。
配置组可以应用到目标上,也就是说,一组宏、编译标志等可以应用于特定的输出可执行文件或库。这比当前 Makefile 系统更精细化,后者大多是全局应用到所有目标。
3.3. 编译器抽象
除了 CMake 内置的编译器使用方式外,还有一些常见任务是 CMake 本身无法处理的(例如对文件进行预处理)。对于这些任务,框架使用封装函数代替直接调用编译器,从而不依赖于某个特定编译器。
3.4. 外部工具
TF-A 构建系统中会使用一些外部工具,例如用于镜像生成的 fiptool、用于设备树编译的 dtc。框架需要找到并/或构建这些工具。为此,框架使用 CMake 的 find_package
功能。后续如有其他工具需求也可加入。
四、工作流程
下图展示了使用该框架的开发流程:

流程分为两个主要阶段:
-
准备阶段(provisioning phase)
首先获取所需资源,比如克隆代码仓库及其依赖。接着进行配置,最好使用如 KConfig 之类的配置工具。
-
开发阶段(development phase)
首先运行 CMake,它会使用选定的生成器(目前仅支持 Makefile)生成构建系统。之后运行指定的构建工具,它会调用编译器、链接器、打包工具等。最后可以运行并调试输出的可执行文件。
通常在开发过程中只需要重复第二阶段的步骤,而准备阶段一般只需执行一次(或很少)。
五、示例
下面是一个简要示例,展示了基本框架的使用方法。
首先,我们创建一个名为 mem_conf
的配置组,并填入若干参数。注意 CONFIG
和 DEFINE
类型的区别:前者仅存在于 CMake 域,后者仅为 C 语言宏。
然后,我们创建一个名为 fw1
的构建目标,并将 mem_conf
配置组应用到该目标上。这意味着目标使用的所有源文件和头文件都会继承该配置组的所有参数。
接下来,将目标类型设置为可执行文件,并添加一些源文件。由于目标已包含配置参数,因此可以进行条件添加源文件。例如,dram_controller.c
只有在 MEM_TYPE
等于 dram
时才会被加入。
cpp
group_new(NAME mem_conf)
group_add(NAME mem_conf TYPE DEFINE KEY MEM_SIZE VAL 1024)
group_add(NAME mem_conf TYPE CONFIG DEFINE KEY MEM_TYPE VAL dram)
group_add(NAME mem_conf TYPE CFLAG KEY -Os)
stgt_create(NAME fw1)
stgt_add_setting(NAME fw1 GROUPS mem_conf)
stgt_set_target(NAME fw1 TYPE exe)
stgt_add_src(NAME fw1 SRC
${CMAKE_SOURCE_DIR}/main.c
)
stgt_add_src_cond(NAME fw1 KEY MEM_TYPE VAL dram SRC
${CMAKE_SOURCE_DIR}/dram_controller.c
)