嵌入式三方库指南:以FreeRTOS与LVGL为例

众所周知,嵌入式领域有很多优秀的三方开源库。例如 FreeRTOSLVGLCMSIS-DSPLittleFS 等,都是大名鼎鼎的嵌入式三方库。利用这些库,可以大幅提升开发效率,轻松实现任务调度、图形界面等功能,无需再陷入裸机编程的重复造轮子中。

在嵌入式开发中,第三方库的引入方式直接影响项目的可维护性。虽然直接复制源码是最简单的方式,但会带来版本管理、依赖冲突和更新困难等问题。

主流语言生态大多通过包管理器(如 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 版(长期支持版本),如下图所示。

FreeRTOS官网下载页面

这类版本通常是长期支持,不会由于内容更新而引入新的 Bug 。无论是对于商业开发还是个人开发,选择 LTS 版本都有利于长期稳定。

在开发过程中,如果有足够的理由怀疑三方库出现了问题,也可以在相应仓库的 issue 里提出。但如果只是由于自己对于三方库功能使用不当造成的问题,还是建议自行尝试解决之后再提出,不要无效提问。

引入方式的选择

繁琐的话题到此为止,现在开始讲解当拿到一个库之后,如何阅读其中的提示信息,并将库引入到自己的项目。

接下来,以 FreeRTOS 的 LTS 版本为例讲解,请各位可以试着下载一份源码,并按照步骤对照阅读。

首先,当我们拿到一份陌生的开源库,应该首先寻找其中的使用示例或者教学文档。按照文档中提示的内容进行,能够尽可能避开错误和误区。

有些三方开源仓库的构建示例会直接放在根目录当中,也有些会放在 examples 文件夹内,而 FreeRTOS-Kernel 就属于后者。现在打开路径 FreeRTOS-Kernel\examples\cmake_example ,可以看到如下的脚本代码。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
cmake_minimum_required(VERSION 3.15)
project(example)

set(FREERTOS_KERNEL_PATH "../../")

# Add the freertos_config for FreeRTOS-Kernel
add_library(freertos_config INTERFACE)

target_include_directories(freertos_config
INTERFACE
"../template_configuration"
)

if (DEFINED FREERTOS_SMP_EXAMPLE AND FREERTOS_SMP_EXAMPLE STREQUAL "1")
message(STATUS "Build FreeRTOS SMP example")
# Adding the following configurations to build SMP template port
add_compile_options( -DconfigNUMBER_OF_CORES=2 -DconfigUSE_PASSIVE_IDLE_HOOK=0 )
endif()

# Select the heap port. values between 1-4 will pick a heap.
set(FREERTOS_HEAP "4" CACHE STRING "" FORCE)

# Select the native compile PORT
set(FREERTOS_PORT "TEMPLATE" CACHE STRING "" FORCE)

# Adding the FreeRTOS-Kernel subdirectory
add_subdirectory(${FREERTOS_KERNEL_PATH} FreeRTOS-Kernel)

########################################################################
# Overall Compile Options
# Note the compile option strategy is to error on everything and then
# Per library opt-out of things that are warnings/errors.
# This ensures that no matter what strategy for compilation you take, the
# builds will still occur.
#
# Only tested with GNU and Clang.
# Other options are https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_COMPILER_ID.html#variable:CMAKE_%3CLANG%3E_COMPILER_ID
# Naming of compilers translation map:
#
# FreeRTOS | CMake
# -------------------
# CCS | ?TBD?
# GCC | GNU, Clang, *Clang Others?
# IAR | IAR
# Keil | ARMCC
# MSVC | MSVC # Note only for MinGW?
# Renesas | ?TBD?

target_compile_options(freertos_kernel PRIVATE
### Gnu/Clang C Options
$<$<COMPILE_LANG_AND_ID:C,GNU>:-fdiagnostics-color=always>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-fcolor-diagnostics>

$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wall>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wextra>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wpedantic>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Werror>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wconversion>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Weverything>

# Suppressions required to build clean with clang.
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-unused-macros>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-padded>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-missing-variable-declarations>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-covered-switch-default>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-cast-align> )

add_executable(${PROJECT_NAME}
main.c
)

target_link_libraries(${PROJECT_NAME} freertos_kernel freertos_config)

set_property(TARGET freertos_kernel PROPERTY C_STANDARD 90)

如果读者不了解 CMake 的语法细节,第一次看到这样的脚本应该还是比较茫然的。但是没关系, CMake 的可读性没有那么不堪,各位即便是第一次接触 CMakeLists.txt ,也可以照猫画虎,模仿其中的写法。

首先,我们会在第一行看到, CMake 脚本必须指定当前使用的 CMake 最低版本,属于固定写法,照抄即可。

随后会看到脚本指定了 FREERTOS_KERNEL_PATH 的相对路径,这里由于该脚本将自己放在了子路径里,所以需要返回前两级路径,改写的时候就需要将路径指定到当前存储 FreeRTOS 的路径,适当调整。

然后脚本以库的形式创建了 freertos_config 作为接口库,引入了 ../template_configuration 作为头文件路径,而打开相关文件夹, FreeRTOSConfig.h 中就负责定义 FreeRTOS 的配置选项,根据不同的 CPU 类型和架构,进行自定义修改。

下面这一部分如果不提前了解 FreeRTOS 的设计可能读不明白,这里指定的是如果该 CPU 存在多核处理器,就需要额外增加一些特殊的编译选项。如果只是普通的单核 CPU 就无需考虑这个问题。

(出自源文件14行)

1
2
3
4
5
if (DEFINED FREERTOS_SMP_EXAMPLE AND FREERTOS_SMP_EXAMPLE STREQUAL "1")
message(STATUS "Build FreeRTOS SMP example")
# Adding the following configurations to build SMP template port
add_compile_options( -DconfigNUMBER_OF_CORES=2 -DconfigUSE_PASSIVE_IDLE_HOOK=0 )
endif()

接下来需要选择内存管理方案,说白了就是从1~4的四种方案中选择一个作为当前的内存管理。因为FreeRTOS支持动态内存分配,所以需要权衡性能与便利之间的优劣。

也属于提前了解才能知道如何选择的选项,因此在引入自己的项目前真的需要认真通读一遍功能才行。

接下来提到 FREERTOS_PORT 可以暂时忽略,不会影响到引用项目的过程。

而再下一步,就进入到了关于主要代码的引入。

(出自源文件27行)

1
2
# Adding the FreeRTOS-Kernel subdirectory
add_subdirectory(${FREERTOS_KERNEL_PATH} FreeRTOS-Kernel)

首先是一大段的注释信息,提到关于不同编译器构建时的编译策略和编译器选择。这里默认采用 GCC 来作为编译器,具体可以参考笔者以前写过的 stm32-cmake-templete 学习。下面提到的一大段都是非常常见的编译选项,想要了解相关内容可以在 Option Summary 中查找,不过多展开。这里主要做的内容是根据不同的编译器采取不同的编译选项,并将所有警告视为错误。

(出自源文件49行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
target_compile_options(freertos_kernel PRIVATE
### Gnu/Clang C Options
$<$<COMPILE_LANG_AND_ID:C,GNU>:-fdiagnostics-color=always>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-fcolor-diagnostics>

$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wall>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wextra>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wpedantic>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Werror>
$<$<COMPILE_LANG_AND_ID:C,Clang,GNU>:-Wconversion>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Weverything>

# Suppressions required to build clean with clang.
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-unused-macros>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-padded>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-missing-variable-declarations>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-covered-switch-default>
$<$<COMPILE_LANG_AND_ID:C,Clang>:-Wno-cast-align> )

但是在引入到自己的项目时可以直接指定,并不一定完全遵照这个写法,因此虽然看着内容很多很复杂,但其实都可以忽略。

再接下来就是生成可执行文件,将 main.c (以及你项目中的所有源文件)引入到构建系统,并生成代码。然后将前面创建的两个库一并链接到可执行文件的编译过程中,实现模块间解耦。

怎么样,这样就读完了脚本,是不是还是不会写(?

简化引入方式

具体到笔者所写的项目当中,并没有采用这种复杂的方法。所以读完上面的分析,如果还是不会写也没关系,只需要看接下来的内容就能掌握更通用的引用方法。

这次以 LVGL 为例讲解,同样的还是推荐下载一份源代码,自行尝试解读其中的构建示例。

首先先来看 LVGL 引入的时候的文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
third-party/LVGL/
├── lvgl/ # 官方源码
│ ├── src/... # 源文件
│ ├── lv_version.c # 版本号
│ └── lvgl.h # 通用头文件
├── porting/ # 移植代码
│ ├── lv_port_disp.c # 移植屏幕驱动
│ ├── lv_port_disp.h
│ ├── lv_port_indev.c # 移植触屏驱动
│ └── lv_port_indev.h
├── CMakeLists.txt # 子构建脚本
└── lv_conf.h # lvgl设置

注意,其中只有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Set the LVGL root directory
set(LVGL_ROOT_DIR "${CMAKE_SOURCE_DIR}/third-party/LVGL")
set(LVGL_SRC_DIR "${LVGL_ROOT_DIR}/lvgl")
set(LVGL_PORT_DIR "${LVGL_ROOT_DIR}/porting")

# Build LVGL library
add_library(lvgl INTERFACE)

# Set sources used for LVGL components
file(GLOB_RECURSE SOURCES
${LVGL_SRC_DIR}/src/*.c
${LVGL_SRC_DIR}/src/*.S
${LVGL_PORT_DIR}/*.c
)

target_sources(lvgl INTERFACE
${SOURCES}
)

# Include root and optional parent path of LV_CONF_PATH
target_include_directories(lvgl INTERFACE
${LVGL_ROOT_DIR}
)

内容较为简单易懂,主要执行的步骤为建立路径变量、创建接口库、添加源文件内容、添加头文件路径。这样就能快速将三方库文件引入到自己的项目中,而且具有良好的隔离性,每个三方库都拥有自己的子构建脚本,将所有的子构建脚本汇总在根目录下的 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/


嵌入式三方库指南:以FreeRTOS与LVGL为例
https://marisa9961.github.io/2025/05/21/250521-stm32-freertos-lvgl/
作者
Marisa9961
发布于
2025年5月21日
许可协议