一:引言
LVGL 想必大家都不陌生了,是一个非常优秀的,适合嵌入式的GUI框架。刚好我最近在折腾LVGL,下面就将我折腾的内容记录下来,于是就有了这一个系列,教你如何优雅的通过CLion使用lvgl~
这是一个系列,我打算按照下面的列表顺序,一步步教你优雅的使用lvgl:
- (其一)裸机下移植lvgl~
- (其二)让我们加上Freertos !
- (其三)让我们优化一下rtos的内存分配!
- (其四)让我们加上Fatfs文件系统,并链接LVGL!
既然你点进来了,我就默认你已经有了基础的使用STM32的能力,像STM32CubeMX我是默认你以及安装好且会使用了,还有就是你已经可以正常点亮你的屏幕了~
下面是我使用的平台&配置:
- 系统:Windows 11 x64
- 平台:STM32F407VET6 (128K sram + 64K CCMram + 512K rom)
- LVGL:V8.3.10
- CLion: 2025.3
至于为什么选用lvgl 8.3.10 而不选用最新的V9.x.x,有如下考虑:
- lvgl的V8版本相对于V9来说,rom/ram的占用是明显较少的,更适合STM32F4这类资源受限的MCU
- V8版本发布了很长时间,大部分的UI设计软件像GUI-Guider等都支持V8.3.10,更方便开发
二:准备工作
下面我们来做一些准备工作,这是非常重要的喵!
2.1 CLion
对于STM32的编译与下载,我使用的是ST-Link V2,对于CLion的安装与配置,可以看一下下面的链接:
链接——–
2.2 LVGL
2.2.1 下载
我们可以直接前往lvgl的github的release界面:https://github.com/lvgl/lvgl/releases?,找到你所需要的版本,我这里就用8.3.10版本做演示。选择你需要的版本,展开Assets,单击下载.zip后缀的即可。

嘛,访问github可能需要梯子,如果你实在访问不了的话,可以点击下面的链接下载~
2.2.2 预处理
1. 我们将刚刚下载的压缩包解压到随便一个文件夹中,并将文件夹的名称命名为“lvgl”(很重要,不要改这个名字)

2. 别看东西很多,但其实我们需要的就只有如下5个文件(夹):
| 文件(夹)名称 | 作用 |
| demos | 官方的示例文件夹,用于快速检验以及性能测试(可不需要) |
| porting | lvgl的接口文件夹,实现显示设备注册,文件系统,外设输入功能(重要) |
| src | lvgl的源文件,存放lvgl的渲染逻辑、控件等文件(重要) |
| lvgl_conf.h | 配置文件,用于配置/裁剪lvgl的功能(重要) |
| lvgl.h | lvgl的头文件(重要) |
其中,porting文件夹在lvgl\examples\下,我们将它拷贝到lvgl\下,再删去examples文件夹即可。
demos文件夹不是必须的,它存放着lvgl官方的一些示例,我们保留下来方便快速验证。

3. 我们打开porting文件夹,去掉其中6个文件的文件名末尾的“_template”字样,当然别忘了lvgl\lv_conf_template.h这个文件也要改:

这些 *_template 文件是官方提供的移植/配置参考模板。要在项目中生效,必须改名,以便参与编译并能让 LVGL 找到你定制的配置与驱动。
至此,我们的准备工作就完成了,下面我们正式开始移植!
三:开始移植
这里强烈建议你先看一遍完整的流程,再回来一步一步跟着学~
下面让我们开始吧~ o(* ̄▽ ̄*)ブ
3.1 调整STM32工程
既然你都来学习lvgl的移植了,那么我就默认你已经通过STM32创建好了工程,并且可以通过CLion打开~
如果你以前用的是keil5/Cube IDE也很简单,看下面操作:
3.1.1 调整CubeMX
先打开你的工程,在“Projexct Manager”栏目下,我们“
- 修改“Toolchain / IDE”为“CMake”;
- 调整堆大小“Minimum Heap Size”为“0x2000”;
- 栈大小“Minimum Stack Size”为“0x4000”。
LVGL 是一个图形库,使用了大量的局部变量、栈内存分配(如控件结构体、绘图缓冲处理等),栈空间不足将导致程序运行异常,如 HardFault、死机、显示异常。

然后点击右上角的”GENERATE CODE“按钮,CubeMX就自动帮我们生成好了代码~
3.1.2 CLion打开工程
接下来我们创建一个CLion工程,并做一些设置,让CLion可以解析我们的STM32工程。
CLion已经内置了对STM32工程的支持,按下面的操作就可以很方便的使用CLion进行代码编写:
1.我们打开CLion并新建一个工程,随后我们:
- 在左侧找到”嵌入式开发“里的”STM32 CubeMX“
- 点击界面右上角的文件夹图标,选择你项目所在的根目录
- 点击继续即可

2. 等待CLion解析后,在弹出的界面中,我们这样:
- 勾选“在编辑CMakeLists.txt时……”的选项框
- 只启用”Debug-Debug“和“Release-Release”的配置文件
- 不用改其他的,确定即可

这样我们就完成了CLion的基本配置~
具体细节比如CLion的安装/配置,可以看2.1的链接,讲的很清楚~
3.1.3 将lvgl加入工程
这里没什么要做的,只需要将lvgl的文件夹,直接移动到工程根目录下就行~

ok,我们完成了很简单的一部分,接下来是很重要的部分!认真看!!!
3.2 引入lvgl的文件
CLion在文件管理方面,跟Keil5、STM32CubeIDE很不一样——CLion使用CMake管理项目,而我们如果想将.c/.h文件添加进编译项目中,就需要修改“CMakeLists.txt”文件。
你也许看过其他人移植lvgl的教程,他们还在一个个的将.c文件添加进组,一个个将.h头文件添加进查找目录,大半个教程都在干这浪费时间、还容易出错的事情。
那么有没有快捷高效还不容易出错的办法呢?有的孩子!有的!请看下面的教程~
3.2.1 编辑CMakeLists.txt
CMakeLists.txt在你项目的根目录下,我们双击打开:

关于CMakeLists.txt的结构解析,你可以上网搜索一大堆,这里就不多做解释了,因为我们只要会用就行了!(CMake的用户量超级大,资料特别多~)
复制下面的两个代码函数到CMakeLists.txt的 ”开头注释“ 的下方,“Setup compiler settings”上方:
1.这个函数用于将某个文件夹及其所有子文件夹的目录添加进一个变量中:
# 得到所有子文件夹目录的函数:
add_definitions(-DLV_LVGL_H_INCLUDE_SIMPLE)
function(collect_subdirectories OUTPUT_VAR ROOT_DIR)
set(SUBDIRS "${ROOT_DIR}") # 优先把根目录放进去
function(_recurse dir)
file(GLOB CHILDREN RELATIVE ${dir} ${dir}/*)
foreach(CHILD ${CHILDREN})
set(CHILD_PATH "${dir}/${CHILD}")
if(IS_DIRECTORY ${CHILD_PATH})
list(APPEND SUBDIRS ${CHILD_PATH})
_recurse(${CHILD_PATH})
endif()
endforeach()
set(SUBDIRS ${SUBDIRS} PARENT_SCOPE)
endfunction()
_recurse(${ROOT_DIR})
list(REMOVE_DUPLICATES SUBDIRS)
set(${OUTPUT_VAR} ${SUBDIRS} PARENT_SCOPE)
endfunction()
2.这个函数用于得到某个文件夹下所有.c文件的地址,并存储在一个变量中
# 得到子文件夹所有.c文件的函数:
function(collect_all_c_files OUTPUT_VAR ROOT_DIR)
file(GLOB_RECURSE C_FILES "${ROOT_DIR}/*.c")
list(REMOVE_DUPLICATES C_FILES)
set(${OUTPUT_VAR} ${C_FILES} PARENT_SCOPE)
endfunction()
具体效果如下图所示:

当然,STM32F4系列是带有FPU的,可以加速浮点的运算,我们可以在CMakeLists末尾添加下面代码,即可开启浮点计算的支持~
# 添加浮点数支持
set(CMAKE_SYSTEM_PROCESSOR cortex-m4)
set(MCU_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mfloat-abi=hard" )
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -u _printf_float")
add_compile_options(
${MCU_FLAGS}
-Wall
-Wextra
-ffunction-sections
-fdata-sections
-O3
)
add_link_options(
${MCU_FLAGS}
-Wl,--gc-sections
-Wl,-Map=${PROJECT_NAME}.map
)
3.2.2 添加lvgl的文件进编译
通过3.2.1的函数,我们现在就可以非常方便的添加文件啦~
将下面的代码复制到刚刚粘贴的那两个函数的下方:
# 整备lvgl文件夹
set(LVGL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lvgl) # 设定根目录地址
collect_subdirectories(LVGL_SUBDIRS ${LVGL_DIR}) # 得到所有子文件夹
collect_all_c_files(LVGL_SOURCE_FILES ${LVGL_DIR}) # 得到所有文件名
这样,lvgl下所有的.c文件的地址就在变量${LVGL_SOURCE_FILES}中,而所有的子文件夹的地址就在变量${LVGL_SUBDIRS}中。
下面我们将这两个变量添加进编译/查找列表当中:
- 将${LVGL_SOURCE_FILES}添加到add_executable(${CMAKE_PROJECT_NAME})下。
- 将${LVGL_SUBDIRS}添加到target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE)下。
如图所示:

这两个项也是你添加其他.c/.h时操作的地方,需要要注意的是:
- .c文件需要完整的路径,如:“xxx/xxx/abc.c”
- .h文件只需要到它所在的上一级文件夹即可,如:“xxx/xxx”
其次,对于.c文件,你可以在左侧文件栏,右键.c文件,点击”将xxx.c添加进编译项目“,直接确定即可。
需要注意的是,我们用的这种方案,CMake只会在加载CMakeLists.txt时更新一次变量!也就是说,你如果 新建/删除 了文件,你就需要重新加载一次CMake!而这也很简单:在左侧文件栏空白处右键,点击”重新加载CMake项目“即可!
OK喵!接下来我们重新加载一次项目,然后在CLion界面右上角,点击🔨图标编译!
如果编译没有错误,但有一堆警告就ok!因为我们还没有对lvgl进行配置~如下图:

恭喜你!到这里就已经成功了80%了!接下来就只需要做一些配置即可~
四:配置lvgl
lvgl的配置简单又不简单。如果你想深度调整lvgl,那么还挺折腾的,这可以看我后续的文章;但如果你只是想让它跑起来,就按照下面的步骤来吧!
4.1 开启宏定义
lvgl大量使用#if #endif对来开关某些功能,所以我们需要调整一些文件的配置:
4.1.1 lv_conf.h
lv_conf.h文件在 lvgl\ 下,我们打开它,将代码开始的 “#if 0” 改为 “#if 1” 启用此文件。如图:

4.1.2 lv_port_disp.c/h
lv_port_disp.c/h 在 lvgl\porting\ 下,同理,我们打开它们,将开头的 “#if 0” 改为 “#if 1” 来启用。
其次,对于lv_port_disp.c,我们还需要将”#include lv_port_disp_template.c“ 改成 ”#include lv_port_disp_template.c“,如图:

4.2 配置lv_port_disp.c
我们配置一下lv_port_disp.c,这个文件是lvgl的显示接口,通过它来注册你的屏幕信息。
我们需要配置下面这些内容:
- 屏幕的分辨率
- lvgl的显示缓冲区的方案及大小
- lvgl的屏幕刷新函数
我这里是默认你有你的屏幕的驱动的,至少你得有一个画点的函数!
4.2.1 配置屏幕分辨率
本喵使用的屏幕是ST7789驱动的2.8寸屏幕,分辨率是320*240的,那么修改如下图:
需要注意的是:本喵默认是横屏使用,如果你是竖屏,这里要对调一下

4.2.2 配置显示缓冲区
lvgl需要一块显示缓冲区,相当于显存,用于渲染画面像素。
lvgl提供了三种缓冲区方案:
单缓冲/行缓冲 (Single Buffer) (此处用这种方案)
- LVGL将在此处绘制显示的内容,并将其渲染到屏幕。
- 较省内存,但 CPU 和 LCD 显存“争用”这块内存
- 如果刷新不及时,可能会卡顿
双缓冲(Double Buffer / Flushing) (有DMA外设则推荐该配置,能大幅提高帧率)
- 两块缓冲轮流用,一块画、一块刷,更平滑,内存开销是单缓冲的 2 倍
- 当一块缓冲刷新时,另一块同时画,所以必须使用非阻塞方案!否则不如单缓冲方案
全帧缓冲(Full Frame Buffer) (有DMA且大内存则推荐,进一步提高显示帧率)
- 把整个屏幕一帧都缓存在内存中
- 优点:一次刷新整屏,有很强的兼容性(动画、透明、旋转等最好)
- 缺点:占用大约 153.6KB(320x240x2 字节)内存,适合H7这类高端型号用
好了,概念介绍完了,我们来看lvgl的代码~
我们将第一种方案的缓冲区改大点,然后直接注释掉第二和第三种方案,如图:

4.2.3 配置disp_flush()函数
lvgl本质上是一个图形库,他肯定不会包含任何屏幕的驱动,那么他该怎么将像素显示到屏幕上呢?而这就是disp_flush()的作用!它链接这lvgl与你的屏幕!
我们来看看disp_flush()是怎么工作的:
- lvgl会将要显示的内容渲染到刚刚我们配置的缓冲区中(draw_buffer)。
- 随后lvgl会调用flush_cb函数,我们这里就是disp_flush()函数。
- disp_flush()会传入刷新区域以及像素数据,你的驱动就需要将像素显示到对应区域。
- 刷完后,你需要调用lv_disp_flush_ready()来告诉lvgl我刷完了!
下面我们来看看disp_flush()函数,在第166行:
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
if(disp_flush_enabled) {
//逐点刷,非常慢!
for(int32_t y = area->y1; y <= area->y2; y++) {
for(int32_t x = area->x1; x <= area->x2; x++) {
LCD_DrawPoint(x, y, color_p->full); // 逐点刷新
color_p++;
}
}
}
// 一次性刷新整个区域的代码示例,速度会快很多
// LCD_SetWindows(area->x1, area->y1, area->x2, area->y2); // 设定显示区域
// uint32_t size = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1); 计算像素数量
// LCD_WR_DATA_16bit_Multiple((uint16_t*)color_p, size); // 一次性刷完
lv_disp_flush_ready(disp_drv); // 告知LVGL刷新完成!!!!!!!
}
这里根据你的屏幕驱动来改!
如果你是同步刷新(就是阻塞式),那么在disp_flush()里调用lv_disp_flush_ready()即可
但若用了DMA/IT,那么就在中断回调里调用lv_disp_flush_ready()
4.3 让lvgl能运行起来
为了让lvgl能够正常跑起来,我们需要做两件事情:
- 为lvgl提供心跳
- 让lvgl处理事件
下面我们具体来讲讲~
4.3.1 为lvgl提供心跳
lv_tick_inc(uint32_t time_ms) 是LVGL中的一个函数,通过外部周期性调用来模拟系统时钟的滴答(tick)更新。LVGL 所有动画、过渡效果、事件延迟、定时器等功能,都依赖内部 tick!
- lvgl没有内部定时器,不会自己生成时间,因此需要你来提供。
- 调用lv_tick_inc()会将lvgl内部的一个计数器+你传入的数值
- 这个入参是ms级别的!最好是1ms调用一次!
因此,在裸机中最好的方式就是配置一个定时器,配置为1ms一次中断,并在中断回调函数中调用lv_tick_inc(1)来提供lvgl的心跳!
如果你只是想让lvgl跑起来,也可以在main.c的while循环里1ms调用一次。(当然并不建议这样,受程序运行影响太大,很可能导致卡顿等问题~)
4.3.2 任务处理
要处理 LVGL 的任务,我们需要定期调用 lv_task_handler() 这个函数。
这个函数会处理以下任务:
- 执行注册的 LVGL 任务(lv_task)
- 处理输入设备事件,如触摸、按键输入之类的
- 更新动画与过渡效果
- 处理显示刷新逻辑,如检查脏区,渲染缓冲区,调用disp_flush()等
- 调度定时器任务 等
- 调用的时间间隔建议是5~10ms,这样就可以处理上述事件啦~
对于调用的方案,最简单的当然也是在while循环里调用,当然更推荐定时器~
下面是在while中调用的示范:
// 此处在main()中
lv_init(); // 初始化lvgl
lv_port_disp_init(); // 初始化显示设备
/* USER CODE BEGIN WHILE */
uint8_t tick_count = 0;
while (1)
{
lv_tick_inc(1); // LVGL时钟更新
tick_count++; // 滴答计数
if (tick_count > 5) {} // 每5ms处理一次
tick_count = 0; // 重置计数
lv_task_handler(); // LVGL任务处理
}
/* USER CODE END WHILE */
非常简单对吧?我们编译!下载!一气呵成!如果没有报错的话~
恭喜你!至此你已经完成了lvgl的移植! ヽ(✿゚▽゚)ノ
下面让我们跑一个demo例程看看吧!
五:跑个例程~
我们来跑一个benchmark例程吧,顺便开启一下lvgl的性能监视器(显示内存占用,帧率之类的数据)
启用上面这些功能,我们需要配置lvgl_conf.h,我们打开它:
- 我们来到第282行,将 #define LV_USE_PERF_MONITOR 0 改成 1 来启用FPS显示。
- 来到第289行,将#define LV_USE_MEM_MONITOR 0 改成 1 来启用内存占用监控。
- 来到第747行,将#define LV_USE_DEMO_BENCHMARK 0 改成 1 来启用Demo。
ok,这样我们就开启了上述功能。由此你可以看见,lv_conf.h非常长,能配置的东西非常的,很值得你去慢慢看看哦~
来吧,让我们开启这个demo!很简单,在 lv_port_disp_init() 之后调用 lv_demo_benchmark() 即可!
如下图所示:

ok!编译下载一气呵成!让我们看看成果!

(拍的不好见谅哈哈哈)
六:总结
至此LVGL移植已经结束了,但我们在移植过程中把所有控件都移植进去了,实际上我们是用不到这么多控件的,加上单片机本来资源就受限,因此我们还得进行裁剪,将没用到的控件、字体等删除,节省空间。后面会单独出一篇文章介绍一下。
还有就是,我们并没有移植输入设备,这点以后也会出一篇文章介绍喵~