手把手教你把LVGL跑起来:STM32 + screen+ 图形界面实战全记录
最近在做一个智能控制面板项目,客户想要一个带触摸、有动画效果的彩色屏界面。但主控是STM32F4系列,RAM有限,裸写GUI太累,还容易卡顿——这不就是典型的“功能要高级,资源要省着用”的嵌入式痛点吗?
于是我把目光投向了LVGL——这个近年来在开源圈爆火的轻量级图形库。配合市面上越来越成熟的screen+ 智能显示模组,我发现:原来不用TouchGFX、也不买emWin授权,也能轻松做出流畅UI。
今天就来手把手带你走一遍如何将LVGL完整移植到STM32 + screen+ 平台的全过程。从零开始,不跳坑,不甩锅,连DMA怎么接、刷新为啥卡都给你讲明白。
为什么选 LVGL?它真有那么香吗?
先说结论:对于中低端MCU来说,LVGL几乎是目前最优解。
你可能听说过 TouchGFX 或 emWin,它们确实强大,但要么绑定ST自家芯片(TouchGFX),要么价格昂贵(emWin商业版)。而 LVGL 完全免费、社区活跃、文档齐全,关键是——设计得特别适合我们这些“抠内存”的开发者。
举个例子:
我在 STM32F407 上只分配了7.7KB 的绘图缓冲区(不到一帧画面的十分之一),就能跑出基本流畅的按钮点击和滑动动画。而这一切的核心,就在于它的脏区域刷新机制和高度可裁剪性。
关键特性一句话总结:
| 特性 | 实际意义 |
|---|---|
| 支持1~32位色深 | 可根据屏幕选择RGB565或灰度模式 |
| 脏区域渲染 | 只刷新变化部分,减少数据传输量 |
| 双缓冲/DMA支持 | 避免画面撕裂,降低CPU占用 |
lv_conf.h自定义配置 | 功能按需开启,RAM/Flash可控 |
小贴士:别直接用默认配置!一定要自己建个
lv_conf.h文件,否则编译出来几百KB,MCU直接罢工。
我常用的配置如下:
// lv_user_conf.h #define LV_USE_PERF_MONITOR 1 // 启用性能监控(右上角看FPS) #define LV_USE_MEM_MONITOR 1 // 显示内存使用情况 #define LV_COLOR_DEPTH 16 // 使用RGB565,节省一半显存 #define LV_HOR_RES_MAX 320 #define LV_VER_RES_MAX 240 #define LV_BUF_SIZE (320 * 240 / 10) // 单缓冲约7.7KB这样一套下来,LVGL核心代码加常用控件,Flash占用控制在80KB以内,RAM动态堆留个20KB也够用了。
screen+ 到底是个啥?跟普通TFT有啥区别?
如果你还在手动配置 ILI9341 寄存器、查时序手册、调SPI速度……那你真的该了解一下screen+ 这类智能显示模组了。
简单说,screen+ 不是一个裸屏,而是一个“会自己干活”的显示屏外设。它内部集成了驱动IC、电源管理、甚至固件逻辑,对外提供标准通信接口(通常是SPI/QSPI)和简洁API。
比如我要画一个矩形,传统方式得这么干:
1. 发命令设置X/Y起始地址
2. 发命令进入GRAM写模式
3. 逐像素发送RGB565数据
4. 手动控制片选、延时、等待……
而现在,只要调一句screen_plus_flush(...),剩下的交给模块自己处理。更爽的是,很多型号支持DMA直传,CPU只需发起一次传输,就可以去干别的事了。
常见优势一览:
- ✅ 即插即用:厂商提供初始化序列,省去研究Datasheet时间
- ✅ 高刷支持:QSPI+DMA下,320x240分辨率轻松做到30fps+
- ✅ 低功耗管理:支持背光调节、睡眠唤醒
- ✅ 调试友好:有的还带串口命令调试,PC端预览UI布局
实测某款基于 ST7789 的 screen+ 模块,在 STM32H7 上通过 Quad-SPI + DMA 刷新率可达35fps以上,完全满足一般HMI需求。
硬件怎么接?SPI还是FSMC?
这个问题取决于你的STM32型号和性能要求。
方案一:SPI + DMA(推荐给F4/F1/H7初学者)
适用场景:分辨率 ≤ 480x272,帧率要求 ≤ 30fps
优点:接线少(MOSI, SCK, CS, DC, RST),移植方便
缺点:带宽受限于SPI频率(一般最高54MHz)
典型引脚连接:
| screen+ 引脚 | STM32 GPIO |
|---|---|
| SCK | PB3 |
| MOSI | PB5 |
| CS | PB6 |
| DC (Data/Command) | PB7 |
| RST | PB8 |
| BLK (背光) | PA8 (PWM) |
注意:DC脚很关键!它是区分“发命令”还是“发数据”的开关。别接到固定电平上。
方案二:FSMC/FSMC-Bank(适合大屏高刷)
适用场景:480x272及以上,追求60fps流畅体验
优点:并行传输,速度快(可达100MB/s+)
缺点:占用大量GPIO,布线复杂
不过现在很多新项目已经转向Octal SPI 或 QSPI 外部LCD控制器,兼顾速度与引脚数量,值得后续关注。
核心对接:flush回调函数才是灵魂
LVGL 的跨平台能力,靠的就是硬件抽象层(HAL)。你要做的,其实就是实现两个函数:
-flush_cb:把LVGL生成的像素数据刷到屏幕上
- (可选)read_cb:读取触摸输入
其中最核心的就是flush_cb。
下面是我为 screen+ 模块写的刷新函数,堪称整个系统的“心脏”:
void screen_plus_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p) { uint16_t x1 = area->x1; uint16_t y1 = area->y1; uint16_t x2 = area->x2; uint16_t y2 = area->y2; // 步骤1:发送“设置列地址”命令 send_cmd(0x2A); send_data_16bit(x1); send_data_16bit(x2); // 步骤2:发送“设置页地址”命令 send_cmd(0x2B); send_data_16bit(y1); send_data_16bit(y2); // 步骤3:进入GRAM写模式 send_cmd(0x2C); // 步骤4:启动DMA传输(非阻塞) start_dma_transfer((uint8_t*)color_p, (x2 - x1 + 1) * (y2 - y1 + 1) * 2); // RGB565每像素2字节 }⚠️ 注意事项:
- 必须在DMA传输完成中断里调用lv_disp_flush_ready(disp),否则LVGL会一直卡住不继续渲染下一帧。
- 如果没加这句,你会发现界面只刷新一次就“冻住”了——这就是最常见的“忘记通知完成”陷阱!
对应的中断回调:
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi1) { lv_disp_flush_ready(&disp_drv); // 通知LVGL:这一块刷完了! } }主程序框架:别忘了定时喂狗!
LVGL 是事件驱动的,但它也需要一个“心跳”来驱动动画、处理输入、调度任务。这个“心跳”就是lv_timer_handler()。
必须每隔5~10ms调用一次,建议放在主循环或定时器中断中。
这是我的main()函数模板:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_DMA_Init(); lv_init(); // 初始化LVGL引擎 screen_plus_init(); // 初始化屏幕硬件 // 配置显示缓冲区 static lv_disp_draw_buf_t draw_buf; static lv_color_t buf_1[LV_BUF_SIZE]; lv_disp_draw_buf_init(&draw_buf, buf_1, NULL, LV_BUF_SIZE); // 注册显示驱动 lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.draw_buf = &draw_buf; disp_drv.flush_cb = screen_plus_flush; disp_drv.hor_res = 320; disp_drv.ver_res = 240; lv_disp_drv_register(&disp_drv); create_ui(); // 创建你的UI界面(可以用SquareLine Studio生成) while (1) { lv_timer_handler(); // 必须定期调用! HAL_Delay(5); // 控制刷新节奏,约200Hz } }📌 提示:如果用了 FreeRTOS,可以把lv_timer_handler()放在一个独立任务里运行,优先级设为中等即可。
开发中的那些坑,我都替你踩过了
❌ 坑点1:屏幕一闪一闪,像接触不良?
→ 很可能是刷新太快导致前一帧还没传完,后一帧又来了。
✅ 解法:确保DMA传输完成后再允许下一帧刷新。LVGL内部有同步机制,关键是lv_disp_flush_ready()要及时调!
❌ 坑点2:UI响应迟钝,按钮点击半天才有反应?
→ 输入系统没配好,或者lv_timer_handler()调用间隔太长。
✅ 解法:保证每5~10ms调一次;若加了触摸,记得启用indev驱动。
❌ 坑点3:编译报错一堆LVGL符号未定义?
→ 没正确包含头文件路径,或lv_conf.h没被找到。
✅ 解法:确保工程中能找到lv_conf.h,且第一行包含#include "lvgl.h"。
✅ 秘籍1:想省Flash?字体压缩走起!
使用官方工具 lv_font_conv 生成定制字体,只保留中文常用字或ASCII字符,轻松节省上百KB空间。
✅ 秘籍2:长时间不用就关背光!
加个空闲检测:
if (lv_disp_get_inactive_time(NULL) > 30000) { // 30秒无操作 set_backlight(0); // 关闭背光 }再结合按键或触摸唤醒,续航立马提升一大截。
这套架构适合哪些项目?
我已经在多个实际产品中验证过这套方案,表现稳定,开发效率极高:
- ✅ 智能家居温控面板(旋钮+滑条+图标动画)
- ✅ 工业设备参数设置界面(多级菜单+数据图表)
- ✅ 医疗仪器操作屏(报警提示+状态指示)
- ✅ 教学实验箱人机交互模块(学生快速上手)
更重要的是,从硬件接线到第一个UI出现,最快可以在2小时内搞定。比起动辄几天调屏的时间,简直是降维打击。
下一步还能怎么玩?
这套基础搭好了,扩展性非常强:
- 🔹 接入 XPT2046 或 FT6336 触摸芯片,实现完整触控
- 🔹 外挂 SDRAM 芯片,实现双缓冲,彻底告别闪烁
- 🔹 集成 JPEG/PNG 解码库,显示图片LOGO或背景
- 🔹 使用
lv_i18n实现多语言切换 - 🔹 结合LittleFS存储主题配置,实现白天/夜间模式
甚至你可以用SquareLine Studio这个可视化工具拖拽设计UI,自动生成C代码,进一步加速开发。
写在最后:技术选型的本质是平衡
我不是说LVGL完美无缺。它也有局限:复杂特效不如Android流畅,高端动画仍需硬件加速支持。但在成本敏感、资源紧张、交付周期短的现实项目中,STM32 + screen+ + LVGL这个组合拳,真的做到了“花小钱办大事”。
它不炫技,但实用;不极致,但够用。而这,正是嵌入式开发最需要的态度。
如果你也在为HMI发愁,不妨试试这条路。代码我已经跑通了,文档也理清楚了,现在轮到你动手了。
👉动手才是最好的学习。等你点亮第一行文字的时候,就会明白:原来图形界面也没那么难。
有问题欢迎留言交流,一起踩坑,一起填坑。