嵌入式三方库指南:以FreeRTOS与LVGL为例
众所周知,嵌入式领域有很多优秀的三方开源库。例如 FreeRTOS 、 LVGL 、 CMSIS-DSP 和 LittleFS 等,都是大名鼎鼎的嵌入式三方库。利用这些库,可以大幅提升开发效率,轻松实现任务调度、图形界面等功能,无需再陷入裸机编程的重复造轮子中。
在嵌入式开发中,第三方库的引入方式直接影响项目的可维护性。虽然直接复制源码是最简单的方式,但会带来版本管理、依赖冲突和更新困难等问题。
主流语言生态大多通过包管理器(如 Python 的 pip、Rust 的 Cargo)解决这类问题。然而在 C/C++ 领域,虽然已有 vcpkg、Conan 等优秀工具,但对嵌入式平台(特别是资源受限的 MCU)的支持仍不够完善,许多库缺乏预编译的嵌入式版本。
因此,我们在很多情况下不得不手动引入三方库来进行开发,本篇博客就将以我个人开发的嵌入式项目 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 文件来寻找线索。
不过虽然在 Github 可以直接获取当前的源码,但这类代码常常是处在版本更迭之间,还未修缮完毕或者还未开发完成的状态,因此并不建议直接引入到自己的项目当中。
对于维护较为完善的开源项目,他们通常都有更稳定的版本供以选择,叫做 LTS 版(长期支持版本),如下图所示。

这类版本通常是长期支持,不会由于内容更新而引入新的 Bug 。无论是对于商业开发还是个人开发,选择 LTS 版本都有利于长期稳定。
在开发过程中,如果有足够的理由怀疑三方库出现了问题,也可以在相应仓库的 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 的接口自行编写的,如有想要借鉴的可以自行研究。
在本文结束后的半年时间里,笔者偶然了解到了 RT-Thread 对于嵌入式包管理的尝试
各位如果有兴趣的话可以自行尝试了解 https://github.com/RT-Thread/env
推荐阅读
CMake Tutorial: https://cmake.org/cmake/help/latest/guide/tutorial/