STM32移植LVGL 8.3.10:显示驱动、显存规划与避坑指南
简介:面向STM32开发者的LVGL 8.3.10移植完整资料包,聚焦嵌入式GUI开发中的图形库适配与硬件设计,适合有一定单片机基础、希望快速上手LVGL的软硬件工程师。压缩包内共2000个文件,以1175个C源码与453个头文件为主,涵盖LVGL库源码、系统初始化、显示驱动、输入驱动及工程链接脚本,并含原理图文件,便于直接打开工程对照硬件连接。资源约46.85MB,已有1255人学习下载。代码模块划分清晰:从时钟、内存与外设初始化,到SPI/I2C显示屏驱动、触摸输入、LVGL渲染,再到应用层API调用,均有完整实现;所附STM32F412原理图进一步展示了MCU与显示、输入设备的接口定义,能帮助开发者快速完成移植验证,减少排错时间,搭建流畅的嵌入式图形界面。
1. STM32 移植 LVGL 8.3.10:从「能跑」到「流畅」的距离
把 LVGL 8.3.10 源码下载下来、往 STM32 工程里塞,这本身不难。难的是塞完之后,屏幕到底是白屏、花屏,还是能正常刷出界面。我见过不少人折腾一整天,最后发现只是 SPI 速率配太快,或者 lv_tick_inc 压根没人喂。这个标题里的“完整代码及原理图”,说的不是某个神秘压缩包,而是一套可复现的方案:屏幕芯片驱动怎么写、显存怎么规划、触摸坐标怎么校准,以及 STM32 和屏幕之间的物理接线怎么画。
下面这条路径就是把这三件事串起来:先认清硬件,再调好配置,最后解决白屏、花屏、触摸乱跳这类老熟人。适合第一次在 STM32 上跑图形库的嵌入式工程师,也适合那些在 FreeRTOS 里移植 LVGL 遇到显示问题后想回头查根因的人。
2. 移植前先别急着抄代码:显示芯片、显存规划与版本选型
LVGL 移植看起来是“把源码加进工程再编译”,其实一半的坑在工程之外。我一般动手前会先花半小时,把屏幕主控、内存预算和版本选型定下来,心里有数再开工。
2.1 屏幕主控:ILI9341 与 ST7789 的驱动思路
市面上 2.8 寸、3.2 寸的 SPI 屏模块,主控以 ILI9341 为主;再大一点的屏会用 ST7789 或国产兼容芯片。LVGL 完全不关心屏幕是谁家的,它只看到一块宽 320、高 240 的矩形画布。你要做的,就是把“往画布某个区域写一批像素”这件事,翻译成屏幕芯片自己的命令序列。
我一般会为屏幕写两个底层函数:set_window 用于设定写入矩形范围,push_pixels 用于把像素数据通过 SPI 连续发过去。这两个函数最终被 LVGL 的显示刷新回调调用,所以它们慢了整个界面就慢了,它们错了整个界面就是花的。
有个现象值得提前说:很多国产 ILI9341 模块,读 ID 寄存器会返回 0xA1A1,而不是标准 0x9341。这不是你的 SPI 时序有毛病,是屏厂没按标准做或者用了替代芯片。我就遇到过“读 ID 是 A1A1 但初始化序列完全正常”的模块,所以现在干脆不再读 ID,直接把固定初始化序列写上,屏幕能亮就继续往下走。这个判断能帮你省下大量排查时间。
2.2 显存规划:LV_COLOR_DEPTH 与 LV_MEM_SIZE 是两个概念
LVGL 默认颜色深度是 16 位,也就是 RGB565,一个像素占 2 字节。以 320×240 为例,一帧全屏缓冲是 320×240×2 = 153600 字节,约 150KB。这让带 128KB SRAM 的 F407 并不宽裕,让只有 20KB SRAM 的 F103C8T6 完全装不下。
很多人把 LV_MEM_SIZE 当成“显示缓冲区大小”,往大了调,结果控件还是不正常。实际上,LV_MEM_SIZE 是给 LVGL 内部对象(按钮、标签、容器)用的堆内存;显示缓冲区是单独在 lv_disp_draw_buf_init 里声明的,两个变量要分开算。F103 这类小内存芯片,常见做法是 LV_MEM_SIZE 取 12KB 到 16KB,显示缓冲区只开 1/10 屏那样的小块,让 LVGL 分块刷新。
不同芯片的经验配置大致如下,起步可以照抄:
| 芯片 | SRAM 规模 | LV_MEM_SIZE | 显示缓冲策略 |
|---|---|---|---|
| STM32F103C8T6 | 20KB | 12–16KB | 单缓冲 1/10 屏(约 32 行) |
| STM32F407VE | 128KB | 64KB | 双缓冲 1/4 屏(约 60 行) |
| STM32F429/439 | 256KB | 80–96KB | 双缓冲 + DMA2D,全屏可试 |
严格说,显示缓冲占多少和刷新率、刷新区域有关,但这组数字够你做第一版验证。小内存芯片宁可把缓冲开小,也别让系统空间被吃掉导致 HardFault。
2.3 版本选型:为什么 8.3.10 是当前最稳的选择
LVGL 9.x 已经发布,API 有变动。9.x 里显示驱动结构体改了,刷新回调的参数和状态管理也变了,很多老教程、老厂商代码根本对不上。淘宝屏幕模块里附带的驱动,大多数还是基于 8.3 写的。标题指定 8.3.10,就是奔着“资料多、坑已被踩完”去的。
如果你听人推荐直接上 9.6,那就要有对着 migration guide 逐条改代码的耐心,而且很多示例和第三方控件还没跟上。关键是,裸机和 FreeRTOS 都能跑 8.3.10;FreeRTOS 下主要是把 lv_tick_inc 放到一个 1ms 定时器或高优先级任务里,别在低优先级任务里用 vTaskDelay(1) 来凑时基。我一般用裸机 SysTick 喂 tick,再把 LVGL 的周期任务放到 while(1) 里跑,简单可靠。
3. 把 lvgl-8.3.10 并入 STM32 工程:源码裁剪、lv_conf.h 与驱动回调
硬件和内存预算算清楚后,这一步开始动手接入源码。完整流程分四步:拿源码、改配置、写显示回调、写触摸回调,每一步都有对应代码。
3.1 从 release 包中裁剪 LVGL 源码目录
LVGL 8.3.10 在仓库 Release 里有打包好的源码,解压后是一个 lvgl 主目录,里面放着 src、demos、examples、lv_conf_template.h 这些内容。真正跑起来只需要 src,demos 和 examples 只是验证参考。
工程里我一般这么放:
# 复制核心源码目录 cp -r lvgl/src ./App/lvgl/src # 复制配置模板为实际使用的 lv_conf.h cp lvgl/lv_conf_template.h ./App/lvgl/lv_conf.h # 可选: 带上 benchmark 和 widgets 的 demo 源码, 用于第6章验证 cp -r lvgl/demos ./App/lvgl/demos复制完之后,工程头文件路径里要能搜到 lvgl 这个目录,在 Keil 或 CMake 里加一条 include 路径即可。demos 目录里有很多参考代码,但默认模板里标志位大多为 0,用到哪个 demo 就把对应宏开成 1,否则对应源文件不被编译,也不占 Flash。
注意,LVGL 8.3 对编译器的 C 标准有要求,STM32 的 Keil 工程优先用 AC6 编译器,AC5 在部分环境下会因为 C99/C11 特性支持不全而报错。第一次编译时间长是正常的,别在首次编译失败时就怀疑源码被破坏。
3.2 lv_conf.h 必改项:一张配置清单
lv_conf.h 开头第一行是条件编译控制,要确认启用状态,否则整个文件里的配置不生效。接着打开几个关键项:
#define LV_COLOR_DEPTH 16 #define LV_MEM_SIZE (32U * 1024U) /* 内部对象内存池,按芯片资源调整 */ #define LV_DISP_DEF_REFR_PERIOD 33 /* 毫秒,约 30fps 的刷新上限 */ #define LV_TICK_CUSTOM 0 /* 由用户自己喂 lv_tick_inc */ #define LV_USE_LOG 1 /* 调试期打开,量产前关掉 */ #define LV_FONT_MONTSERRAT_14 1 /* 至少开一个默认字体 */ #define LV_USE_DEMO_WIDGETS 1 /* 验证用;量产关掉 */LV_COLOR_DEPTH 16 是 RGB565,与绝大多数 SPI 屏一致,不用改。LV_MEM_SIZE 在 F407 上建议 64KB,F103 上不要超过 16KB,否则堆不够用。LV_DISP_DEF_REFR_PERIOD 是 LVGL 内部周期刷新任务的间隔,33ms 对应约 30 帧刷新上限,改小不会让屏幕响应更快,只会让 CPU 更忙。LV_TICK_CUSTOM 保持 0,由你定时调用 lv_tick_inc(1),动画和刷新的时基才可控。
LV_USE_LOG 打开后运行日志走串口,能直接看到内存分配失败、参数错误等问题,调试期强烈建议开。字体最少开 MONTSERRAT_14,否则控件上的文字会显示成方块。如果编译时发现缺字体,回到这些 LV_FONT_MONTSERRAT_* 宏里检查。
3.3 显示刷新回调 flush_cb 的正确写法
LVGL 把画布上的改动汇总成“脏矩形”,然后通过注册的回调,把这块区域的像素交给你送屏。标准写法如下:
static void lvgl_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); uint16_t x1 = area->x1, y1 = area->y1; uint16_t x2 = area->x2, y2 = area->y2; /* 1. 告诉屏幕:接下来写入哪个矩形区域 */ LCD_SetWindow(x1, y1, x2, y2); /* 2. 把整个矩形的像素连续发给屏幕 */ LCD_PushPixels((uint16_t *)color_p, w * h); /* 3. 通知 LVGL:刷新完成,可以继续下一块 */ lv_disp_flush_ready(disp_drv); }这个回调被 LVGL 的刷新任务调用,不是中断上下文,所以可以放心在里面做 SPI 发送。第三步 lv_disp_flush_ready 必须在数据真正发完后调用,否则 LVGL 以为发送结束,会把同一块缓冲复用来画下一帧,造成花屏或撕裂。
如果屏幕支持 DMA,推荐把 SPI 发送改成 DMA 方式,在 DMA 传输完成中断里调用 lv_disp_flush_ready。这样刷屏期间 CPU 可以干别的活,界面操作更跟手。如果不用 DMA,保持同步阻塞发送也能跑通,只是刷新率受 SPI 速率限制,具体提升在第 6 章对比。
3.4 注册触摸输入设备:read_cb 与坐标映射
触摸流程比显示简单,LVGL 周期性调用注册好的读回调,查询按压状态和坐标。一个最小实现:
static void lvgl_touchpad_read_cb(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) { int16_t x, y; uint8_t pressed = TP_GetXY(&x, &y); /* 自己实现的触摸读取 */ if (pressed) { >lv_demo_benchmark();这是 8.3.10 自带的一个 demo,它会在屏幕上循环绘制各种形状,并在角落或串口输出实时 FPS。跑 30 秒,如果 FPS 稳定在 25 以上,说明刷新链路合格;如果只有十几帧,就去查 SPI 分频和是否用了阻塞发送。我习惯在验证时把 LV_DISP_DEF_REFR_PERIOD 临时改为 16ms,看极限帧率,但正式工程里还是回到 33ms,给 CPU 留余量。
触摸校验更简单:benchmark 里有交互项,点一下能切换页面,说明 read_cb 和坐标映射是对的。如果点了没反应,先回头检查第 5.3 节。
6.2 用 DMA2D 把 CPU 从刷屏中解放出来
F4 系列自带 DMA2D,可以做内存搬运、颜色格式转换和填充。常见做法是:在 LCD_PushPixels 里不直接做 SPI 循环发送,而是先用 DMA2D 把某块像素从一个显示缓冲搬到另一个 DMA 发送缓冲,同时触发 SPI DMA 发送。这个过程有两个注意点:DMA2D 的源地址和目的地址最好 4 字节对齐;SPI DMA 发送完成中断里要记得调 lv_disp_flush_ready,否则刷新回调一直处于等待状态。
等 benchmark 帧率上来之后,再考虑加过渡动画和容器布局。LVGL 8.3 的动画默认跑在时基上,tick 准了动画就顺了,这一步通常不用改代码。
最后说个习惯:我做这个方向时,永远是先用最土的方式把纯色屏点亮,再进 LVGL;先开一个 demo 跑通,再开动画和控件。有时候屏幕半天没反应,不是 LVGL 的问题,是初始化序列或者供电的问题。这套“先砍到最小再叠加”的方法,陪我避开了绝大多数翻车,希望帮到你。
本文还有配套的精品资源,点击获取