如何以正确的姿势引入三方库
众所周知,嵌入式领域有很多优秀的三方开源库。例如 FreeRTOS 、 LVGL 、 CMSIS-DSP 和 LittleFS 等,都是大名鼎鼎的嵌入式三方库。利用这些库,可以大幅提升开发效率,轻松实现任务调度、图形界面等功能,无需再陷入裸机编程的重复造轮子中。
然而由于这些三方库为了适配多种平台,引入了众多设置选项,增加了使用难度。需要开发者对编译设置和构建系统有较深入的研究和理解,才能对于三方库的引入得心应手。
对于这些三方库还有不熟悉的读者可以简单来了解一下:
FreeRTOS 是一个广泛使用的轻量级实时操作系统内核,适用于嵌入式设备。它提供了任务管理、信号量、队列、定时器等功能,使得开发者可以方便地进行多任务编程。
例如,你可以用 FreeRTOS 创建多个任务来分别处理传感器数据、用户交互、蓝牙通信等,实现各个模块的逻辑解耦。LVGL 是一个功能强大、资源占用小的图形界面库,适合在资源有限的 MCU 上运行。它支持丰富的图形控件(按钮、滑块、图表等)和动画效果,可以帮助开发者快速构建人机交互界面。
CMSIS-DSP 是 ARM 官方提供的数字信号处理库,包含了各种常见的 DSP 运算函数,如 FFT、滤波器、向量运算等,且经过高度优化,适用于 Cortex-M 系列处理器。
LittleFS 是为嵌入式设备设计的高可靠性文件系统,特别适合使用 NOR/NAND 闪存的系统。它支持断电保护、均匀磨损和动态挂载机制。
虽然以上提到的所有三方库,都可以简单粗暴的以直接复制粘贴的形式引入项目,但是会带来很重的精神负担。那么如何以正确的姿势引入这些三方库,就变成了一个很有必要的话题。本篇博客就将以我个人开发的嵌入式项目 SmartBand 为例,讲解笔者在开发过程当中是如何实现以统一方式管理三方库的引入。
为什么要用CMake
CMake是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个函数库。 它用配置文件控制建构过程的方式和Unix的make相似,只是CMake的配置文件取名为CMakeLists.txt。
本次项目的构建系统采用了 CMake+Ninja
的形式,虽然 CMake 在C/C++圈子里一直以来广受诟病,且在国内普遍使用 keil 这类 IDE 工具进行编写嵌入式代码的形势下, CMake 的传播度并没有那么广,但其在嵌入式领域的地位依然不容置疑。像 FreeRTOS 和 LVGL 这类成熟的三方库,通常都会提供标准化的 CMakeLists.txt 构建脚本,以便于进行项目的管理和编译。
再加上 Ninja 在编译性能上的优越性,能够大大提高编译速度,提升编写和调试时的幸福感,因此本项目选择使用了 CMake+Ninja
作为构建系统。
除此之外,还有一点值得提起的是, CMake 在构建编译信息时可以选择导出 compile_commands.json
。借助 VSCode 中的 Clangd 插件,再配合 Clang-Tidy 强大的静态检查功能,能够极大提升编写 C/C++ 时的效率和准确性。
在这一点上其实并非只有 CMake 能够做到,其他构建工具例如 Bazel 和 Meson 也都有类似的功能,包括近来新兴的构建工具 Xmake 也都能够生成 compile_commands.json
,实现更好的静态检查。
对于这些其他的构建系统,不在本文的讨论范围内,如果各位读者有兴趣可以自行了解。
工具 | 嵌入式适用性 | 特点 |
---|---|---|
CMake | 跨平台,支持裸机和RTOS | 泛用性强,语法复杂 |
Make | 需手动编写规则,维护困难 | 精神负担重,跨平台性差 |
Keil | 仅限ARM,厂商锁定 | 依赖厂商,无法导出编译数据库 |
如何获取三方库
一般的开源库都会选择在 Github
存储源代码,所以在 Release 页面就可以获取到当前最新的发行版本。如果没有 Release 发布页面,也可以阅读 README 文件来寻找获取方式。
对于由专业组织维护的开源项目(如 FreeRTOS 由亚马逊维护, LVGL 由开源社区主导),建议通过其官方网站获取最新的稳定版本。
虽然在 Github 也能够查找并获取到 FreeRTOS 和 LVGL 的源码仓库,可以直接下载下来,但这类代码常常是处在版本更迭之间,还未修缮完毕或者还未开发完成的状态,因此并不建议直接引入到自己的项目当中。不过仓库页面中也可能包含了 Release 页面,可以获取到稳定版本。
但通常并不建议这样做。因为这类开源代码有更稳定的版本供以选择,通常叫做 LTS 版(长期支持版本),如下图所示。

这类版本通常是具有足够的稳定性,只要在维护年限内出现了问题,官方都会尽快提供修复补丁。总而言之,无论是对于商业开发还是个人开发,选择 LTS 版本都有利于长期稳定。
在开发过程中,如果有足够的理由怀疑三方库出现了 Bug ,也可以在相应仓库的 issue 里提出。但如果只是由于自己对于三方库功能使用不当造成的问题,还是建议三思之后再提出,不然会比较尴尬。
引入方式的选择
繁琐的话题到此为止,现在开始讲解当拿到一个库之后,如何阅读其中的提示信息,并将库引入到自己的项目。
接下来,以 FreeRTOS 的 LTS 版本为例讲解,请各位可以试着下载一份源码,并按照步骤对照阅读。
首先,当我们拿到一份陌生的开源库,应该首先寻找其中的使用示例或者教学文档。按照文档中提示的内容进行,能够尽可能避开错误和误区。
有些三方开源仓库的构建示例会直接放在根目录当中,也有些会放在 examples 文件夹内,而 FreeRTOS-Kernel 就属于后者。现在打开路径 FreeRTOS-Kernel\examples\cmake_example
,可以看到如下的脚本代码。
1 |
|
如果读者不了解 CMake 的语法细节,第一次看到这样的脚本应该还是比较茫然的。但是没关系, CMake 的可读性没有那么不堪,各位即便是第一次接触 CMakeLists.txt ,也可以照猫画虎,模仿其中的写法。
首先,我们会在第一行看到, CMake 脚本必须指定当前使用的 CMake 最低版本,属于固定写法,照抄即可。
随后会看到脚本指定了 FREERTOS_KERNEL_PATH 的相对路径,这里由于该脚本将自己放在了子路径里,所以需要返回前两级路径,改写的时候就需要将路径指定到当前存储 FreeRTOS 的路径,适当调整。
然后脚本以库的形式创建了 freertos_config 作为接口库,引入了 ../template_configuration
作为头文件路径,而打开相关文件夹, FreeRTOSConfig.h 中就负责定义 FreeRTOS 的配置选项,根据不同的 CPU 类型和架构,进行自定义修改。
下面这一部分如果不提前了解 FreeRTOS 的设计可能读不明白,这里指定的是如果该 CPU 存在多核处理器,就需要额外增加一些特殊的编译选项。如果只是普通的单核 CPU 就无需考虑这个问题。
(出自源文件14行)
1 |
|
接下来需要选择内存管理方案,说白了就是从1~4的四种方案中选择一个作为当前的内存管理。因为FreeRTOS支持动态内存分配,所以需要权衡性能与便利之间的优劣。
也属于提前了解才能知道如何选择的选项,因此在引入自己的项目前真的需要认真通读一遍功能才行。
接下来提到 FREERTOS_PORT
可以暂时忽略,不会影响到引用项目的过程。
而再下一步,就进入到了关于主要代码的引入。
(出自源文件27行)
1 |
|
首先是一大段的注释信息,提到关于不同编译器构建时的编译策略和编译器选择。这里默认采用 GCC 来作为编译器,具体可以参考笔者以前写过的 stm32-cmake-templete 学习。下面提到的一大段都是非常常见的编译选项,想要了解相关内容可以在 Option Summary 中查找,不过多展开。这里主要做的内容是根据不同的编译器采取不同的编译选项,并将所有警告视为错误。
(出自源文件49行)
1 |
|
但是在引入到自己的项目时可以直接指定,并不一定完全遵照这个写法,因此虽然看着内容很多很复杂,但其实都可以忽略。
再接下来就是生成可执行文件,将 main.c
(以及你项目中的所有源文件)引入到构建系统,并生成代码。然后将前面创建的两个库一并链接到可执行文件的编译过程中,实现模块间解耦。
怎么样,这样就读完了脚本,是不是还是不会写(?
简化引入方式
具体到笔者所写的项目当中,并没有采用这种复杂的方法。所以读完上面的分析,如果还是不会写也没关系,只需要看接下来的内容就能掌握更通用的引用方法。
这次以 LVGL 为例讲解,同样的还是推荐下载一份源代码,自行尝试解读其中的构建示例。
首先先来看 LVGL 引入的时候的文件结构:
1 |
|
注意,其中只有 porting/
与 lv_conf.h
是需要手动设置的,其他文件内容原则上不应该随意改动。
首先是 lvgl/
目录,这里存放从官网下载的原始代码,包括 src/
子目录里的核心源代码、 lv_version.c
版本文件以及 lvgl.h
主头文件。这些文件组成了 LVGL 的核心功能,一般不应该修改这里面的内容,这样以后升级版本时直接替换整个目录就行。
然后是 porting
目录,这里放的是需要自己编写的硬件适配代码。主要包含两个关键文件: lv_port_disp.c
用于实现屏幕显示驱动,负责初始化屏幕、管理缓冲区和刷新画面; lv_port_indev.c
用于实现输入设备驱动,处理触摸屏或者按键的输入。这两个文件需要根据具体硬件来编写,比如在 STM32 上可能要操作 LTDC 或者 SPI 接口。
还有一个重要的 lv_conf.h
配置文件,这个文件用来设置 LVGL 的各项参数。比如在这里定义屏幕的分辨率、颜色深度,启用或禁用各种功能模块,调整内存池大小等。这个文件直接影响 LVGL 的运行效果和资源占用。
最后是 CMakeLists.txt
构建脚本,它负责把这些部分组织起来。脚本里会明确定义哪些是官方源代码,哪些是自己写的移植代码,并确保所有头文件路径都正确设置。这样构建系统就能把各个部分正确编译链接到一起。
这种组织方式的好处很明显:官方代码和自己写的代码完全分开,升级时互不影响;不同的硬件平台可以通过不同的 porting
实现来支持;通过修改配置文件就能调整功能,不需要动核心代码。
接下来就重点讲解子构建脚本的内容应当如何编写:
1 |
|
内容较为简单易懂,主要执行的步骤为建立路径变量、创建接口库、添加源文件内容、添加头文件路径。这样就能快速将三方库文件引入到自己的项目中,而且具有良好的隔离性,每个三方库都拥有自己的子构建脚本,将所有的子构建脚本汇总在根目录下的 CMakeLists.txt
中使用诸如 add_subdirectory(third-party/LVGL)
这样的函数就能快速引入 LVGL 的库文件。
总而言之,将三方库代码统一存放在 third-party
当中,并在每个库内建立 CMakeLists.txt
来进行管理,能提升项目的条理性。
非常简单的一段脚本对吧,掌握了基本 CMake 语法之后可以轻易读懂。所以我说的简化引入方式并没有什么神秘之处。因此,比起寻找更简洁的构建方式,不如先从项目的文件管理下手,将引入三方库的工作进行解耦设计,这样就能使得代码设计更加简洁。
补充点没讲到的
在 SmartBand 项目仓库中,我总共引入了 LVGL 、 LittleFS ,并没有出现 FreeRTOS 的源文件和构建脚本。这是因为 STM32CubeMX 中提供的 CMSIS-RTOS 兼容层已经包含了这些源代码,而这些源代码又被我的 cmake/stm32cubemx.cmake
所包含,所以实际上可以省去这个步骤。
另一个没有在本篇博客中举例讲解的 LittleFS 可以自行查看,原理大致相同,但是其源代码并不提供接口耦合, LittleFS/porting
是我仿照 LVGL 的接口自行编写的,如有想要借鉴的可以自行研究。
项目中的核心代码部分也值得阅读,可以参考如下目录:
目录路径 | 内容类型 | 典型文件 | 功能说明 | 维护方式 |
---|---|---|---|---|
base/ |
系统基础代码 | init.c |
系统初始化/任务跳转 | 手动开发 |
components/ |
硬件驱动 | lcd.c |
显示屏等外设驱动 | 手动开发 |
func/ |
功能模块 | task.cpp |
数据处理/工具函数 | 手动开发 |
tasks/ |
RTOS任务 | gui.cpp |
FreeRTOS任务实现 | 手动开发 |
ui/ |
LVGL界面 | ui.c |
UI界面代码及控件 | SquareLine Studio生成+手动优化 |
构建或许没那么神秘
到了这里,本文的主题就差不多探讨完了。
整个项目的构建过程虽然重要,但真正让项目活起来的,是这些组件之间的配合。通过合理的任务划分、通信传递和资源共享,各个模块各司其职又相互配合,最终达到总体的目标。这些实践经验,才是最重要的精华,愿大家共勉。
推荐阅读
CMake Tutorial: https://cmake.org/cmake/help/latest/guide/tutorial/