跳到主要内容STM32 结合 LVGL 实现嵌入式图形界面设计与移植 | 极客日志C
STM32 结合 LVGL 实现嵌入式图形界面设计与移植
介绍在 STM32 微控制器上移植和部署 LVGL 图形库的完整流程。涵盖硬件资源评估(SRAM/SDRAM)、显示与输入驱动初始化、定时器心跳设置、基础 UI 控件创建及动画实现。同时分析了常见开发问题如屏幕闪烁、触摸校准、内存溢出及性能优化方案(DMA2D、部分刷新)。旨在帮助开发者在资源受限环境下构建流畅的嵌入式人机交互界面。
MqEngine11 浏览 STM32 + LVGL:手把手教你打造嵌入式图形界面
LVGL + STM32 是为资源受限设备提供轻量级图形界面的解决方案。
为什么是 LVGL?它真的能在 MCU 上流畅运行吗?
在单片机上做图形界面,本质上是一场和内存、CPU 速度的赛跑。传统 GUI 库(比如 Qt Embedded)动辄几十 MB 内存占用,根本不可能塞进 STM32 这类资源受限的设备里。而 LVGL 的设计哲学很明确:轻量化、模块化、可裁剪。
- 最小配置下,LVGL 可以运行在仅 16KB RAM + 64KB Flash 的系统中;
- 所有功能通过 lv_conf.h 配置开关按需启用;
- 支持裸机运行,也兼容 FreeRTOS 等实时操作系统;
- MIT 开源协议,商业项目随便用,无法律风险。
硬件平台怎么选?我的板子能不能带得动?
我们以常见的 STM32F407ZGT6 为例(主频 168MHz,128KB RAM,1MB Flash),搭配一块 4.3 寸 RGB 接口 LCD(480×272 分辨率)。这是目前最主流的 HMI 方案之一。
关键资源评估
| 资源 | 是否满足 |
|---|
| 主频 ≥ 100MHz | ✅ 是(168MHz) |
| 可用 SRAM ≥ 32KB | ✅ 是(可用约 90KB) |
| 显存需求(双缓冲) | ❌ 不足! |
假设使用 16 位色深(RGB565),每像素占 2 字节:
480 × 272 × 2 × 2(双缓冲) ≈ 518KB 显存
STM32F4 内部 RAM 根本扛不住。那怎么办?
解决方案:外扩 SDRAM 或 使用 CCM RAM
- 外接 SDRAM(推荐)
多数 F4/F7/H7 系列芯片支持 FSMC/FMC 接口,可挂载 IS42S16400J 等常见 SDRAM 芯片,轻松扩展 8MB 以上内存,专门用于存放显示缓冲区。
- 使用 CCM RAM(临时应急)
F4 系列有 64KB 的 CCM RAM(CPU 直连,速度快),可用于存放一帧缓冲,另一帧用普通 SRAM。但注意不能用于 DMA 传输,限制较多。
所以结论很清晰:要做大屏、高分辨率 GUI,必须外扩存储。这不是 LVGL 的问题,而是物理规律。
移植第一步:让 LVGL'看见'你的屏幕
LVGL 本身不知道你在用什么屏幕,它只负责'画图'。要把图画出来,就得告诉它两件事:
- 图像数据往哪写? → 显示驱动
- 用户点哪儿了? → 输入驱动
1. 初始化显示缓冲区
#define LCD_WIDTH 480
#define LCD_HEIGHT 272
#define BUF_SIZE (LCD_WIDTH * LCD_HEIGHT / 10)
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf_1[BUF_SIZE];
static lv_color_t buf_2[BUF_SIZE];
void lvgl_init(void)
{
lv_init();
lv_disp_draw_buf_init(&draw_buf, buf_1, buf_2, BUF_SIZE);
}
这里有个重要技巧:不要一次性分配整屏双缓冲!否则直接爆内存。
LVGL 支持'部分刷新'机制,即每次只更新发生变化的矩形区域。我们将缓冲区设为屏幕面积的 1/10(约 8KB),配合 DMA 传输,既能节省内存,又能保证流畅度。
2. 注册显示驱动:连接 LVGL 与 LCD
核心是一个回调函数 lcd_flush,LVGL 每生成一段图像数据,就会调它来'刷屏'。
void lcd_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
uint32_t width = area->x2 - area->x1 + 1;
uint32_t height = area->y2 - area->y1 + 1;
BSP_LCD_DrawRGBImage(area->x1, area->y1, width, height, (uint8_t *)color_p);
lv_disp_flush_ready(disp);
}
这个 BSP_LCD_DrawRGBImage 是你自己写的 LCD 驱动函数,可以通过 FSMC 或 LTDC 实现高速传输。
⚠️ 注意:如果你用了 RTOS,确保这个函数是非阻塞的,或者运行在独立任务中,避免卡住 LVGL 主线程。
3. 添加触摸输入:让用户能'点'
没有交互的界面就是摆设。LVGL 通过抽象输入设备模型支持多种输入方式,最常见的就是触摸屏。
bool touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data)
{
TS_StateTypeDef ts_state;
BSP_TS_GetState(&ts_state);
if (ts_state.touchDetected)
{
data->point.x = ts_state.touchX[0];
data->point.y = ts_state.touchY[0];
data->state = LV_INDEV_STATE_PRESSED;
}
else
{
data->state = LV_INDEV_STATE_RELEASED;
}
return false;
}
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
lv_indev_drv_register(&indev_drv);
一旦注册成功,LVGL 就知道'有人在用手指操作',所有按钮、滑块立刻具备响应能力。
4. 别忘了定时器:LVGL 的心跳
LVGL 需要知道时间过去了多久,用来驱动动画、处理长按事件、调度任务。
通常做法是在 SysTick 中断 或 硬件定时器中断 中调用:
HAL_TIM_Base_Start_IT(&htim6);
void TIM6_IRQHandler(void)
{
if (__HAL_TIM_GET_FLAG(&htim6, TIM_FLAG_UPDATE) != RESET)
{
lv_tick_inc(1);
__HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE);
}
}
🔥 关键提醒:如果 lv_tick_inc 没有稳定调用,你会发现按钮没反应、动画卡住——这不是 LVGL bug,是你忘了给它'心跳'。
创建第一个 UI:Hello World 动起来!
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Hello LVGL on STM32!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, label);
lv_anim_set_exec_cb(&anim, set_label_angle);
lv_anim_set_values(&anim, 0, 360);
lv_anim_set_duration(&anim, 3000);
lv_anim_set_repeat_count(&anim, LV_ANIM_REPEAT_INFINITE);
lv_anim_start(&anim);
其中 set_label_angle 是一个自定义函数,用来改变标签角度(需配合样式变换实现视觉旋转)。
短短几行代码,你就拥有了一个 持续旋转的文字标签 ——而这只是冰山一角。LVGL 内置超过 30 种控件:按钮、图表、滑块、列表、下拉菜单……全都支持主题美化和动画效果。
常见'翻车'现场与避坑指南
🛑 问题 1:界面闪烁严重,甚至花屏?
原因:lcd_flush 完成后忘记调 lv_disp_flush_ready(disp);
→ LVGL 以为你还卡在刷屏,会不断重试,导致数据混乱。
✅ 正确姿势:只要进入 lcd_flush,无论成败,最终一定要调 lv_disp_flush_ready。
🛑 问题 2:触摸不准,点 A 出 B?
原因:坐标未校准,或 TP 驱动返回的是原始 ADC 值未转换为屏幕坐标。
- 使用 LVGL 自带的校准工具(lv_calibrate 示例);
- 或手动映射:(raw_x - min_x) * width / (max_x - min_x);
🛑 问题 3:内存溢出,程序崩溃?
典型症状:UI 创建一半死机、HardFault。
- 检查 LV_MEM_SIZE 设置是否合理(建议初始设为 32KB);
- 打开 LV_USE_LOG 查看内存分配日志;
- 避免频繁创建销毁对象,尽量复用;
- 使用 LV_DEBUG_ENABLE_MALLOC_STATS=1 监控堆使用情况;
🛑 问题 4:动画卡顿,像幻灯片?
真相往往是:主线程在干别的事,比如串口收数据、文件读写……
- 把阻塞操作移到单独任务(RTOS 环境下);
- 启用 DMA2D 硬件加速填充和拷贝;
- 减少控件层级嵌套(避免过度使用容器);
- 设置 LV_DISP_DEF_REFR_PERIOD 为 16ms(60FPS 上限);
工程结构怎么组织才专业?
别再把所有代码扔进 main.c 了!一个清晰的工程结构能让后期维护省下大量时间。
/Project
├── Core/
│ ├── Src/
│ │ ├── main.c
│ │ └── lvgl_init.c ← LVGL 初始化入口
│ └── Inc/
│ └── lv_conf.h ← 核心配置文件
├── Drivers/
├── BSP/ ← 板级支持包(LCD、TS 驱动)
├── LVGL/
│ ├── src/ ← LVGL 官方源码
│ ├── portable/ ← 移植层代码(display/touch 适配)
│ └── examples/ ← 可选:官方示例
├── Middlewares/
└── GUI/
└── lvgl.h ← 统一头文件引用
特别强调:lv_conf.h 一定要放在编译器能找到的头文件路径中,并且取消所有未使用的功能,例如:
#define LV_USE_FILESYSTEM 0
#define LV_USE_GRIDNAV 0
#define LV_COLOR_DEPTH 16
#define LV_MEM_SIZE (32U * 1024U)
每一项关闭都能为你节省几百到几千字节的 Flash/RAM。
性能还能再榨一榨吗?进阶优化技巧
✅ 技巧 1:启用 DMA2D 进行图形加速
STM32F4/F7/H7 都配有 DMA2D 外设,专用于图像数据搬运和填充。你可以让它代替 CPU 完成以下工作:
- 区域清屏(替代 memset)
- 图像拷贝(ARGB 转 RGB565)
- 填充纯色/渐变背景
只需修改 lcd_flush 中的数据传输部分,调用 HAL 库提供的 HAL_DMA2D_Start() 即可。
效果:CPU 占用率下降 30% 以上,尤其在大区域刷新时非常明显。
✅ 技巧 2:启用部分刷新(Partial Refresh)
默认情况下 LVGL 会尝试刷新整个屏幕。但我们可以通过开启宏定义启用局部刷新:
#define LV_DISP_PARTIAL_REFRESH 1
#define LV_PARTIAL_REFRESH_BUF_SIZE (LCD_WIDTH * 30)
这样 LVGL 只会标记脏区域并分批刷新,大幅降低带宽消耗。
✅ 技巧 3:字体压缩与外部存储
中文字体动辄几 MB,肯定不能放 Flash 里。解决方案:
- 使用 FontForge 工具裁剪常用汉字(如只保留数字 + 常用提示语);
- 转换为 C 数组,启用 LV_USE_FONT_COMPRESSED 压缩;
- 更进一步:把字体文件烧录到 SPI Flash 中,按需加载;
LVGL 支持 freetype 和 file system 接口,结合 W25Q 系列 Flash 芯片,可实现动态资源管理。
写在最后:这不仅仅是个'Hello World'
当你第一次看到那个旋转的'Hello LVGL'出现在屏幕上时,也许会觉得不过如此。但请记住:
- 这背后是完整的 对象管理系统:每个控件都是可继承、可绑定事件的对象;
- 这背后是成熟的 样式与主题引擎:一键换肤不再是梦;
- 这背后是灵活的 跨平台架构:今天你在 F4 上跑,明天就能迁移到 H7 甚至 RISC-V 平台;
掌握这套'STM32 + LVGL'组合拳,意味着你已经具备了独立开发工业 HMI、医疗设备面板、智能家居中控屏的能力。
更重要的是,你学会了如何在一个资源极度受限的环境中,做出接近消费级体验的产品——而这,正是嵌入式工程师的核心竞争力。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online