首页 / 资讯中心 / 文章详情

LVGL嵌入式GUI移植实战:STM32显示驱动、内存与FreeRTOS

LVGL嵌入式GUI移植实战:STM32显示驱动、内存与FreeRTOS ★ FEATURED ARTICLE
简介《LVGL开发指南中文V1.3》面向嵌入式GUI开发初学者、STM32项目实践者及需要为资源受限设备搭建触控界面的工程师围绕LVGL图形库的移植与界面开发展开帮助读者从环境准备逐步过渡到实际项目落地。内容由正点原子编写覆盖初识LVGL、源码获取、无操作系统移植、FreeRTOS环境移植、PC模拟器使用和移植相关知识并延伸到控件创建、动画交互、事件响应与性能优化等应用环节既可按基础篇顺序系统学习也适合在项目移植和调试时查阅。资源包共1个PDF文件约9.54MB单文件结构便于在电脑或移动设备上检索阅读。目前已有766人学习教程结合STM32开发板场景与例程验证能帮助读者理解文件添加、配置修改、任务调度与下载验证等关键流程减少从环境搭建到界面落地过程中的试错成本。1. 板子刷屏像幻灯片问题多半不在屏幕在嵌入式设备上做 GUI翻车最集中的地方不是画不出按钮而是画得慢、内存不够、触摸不准最后统统归咎于“MCU 跑不动图形界面”。这份 LVGL 开发指南中文 V1.3 走的就是这条链路从源码裁剪、无操作系统移植、带 FreeRTOS 移植到显示与输入驱动注册、样式与动画、文件系统和中文字库加载整体按“基础篇 部件篇”组织。它的适用面比较宽配套例程按硬件设计、软件设计、下载验证三段给属于能照着抄的类型手上已经有一块 STM32 开发板、想把 LVGL 真正跑起来的人会很对路。下面按移植顺序把关键参数和常见故障拆开讲先在电脑上把界面调顺再回到板子上对齐驱动。2. 移植前先算三笔账刷屏带宽、RAM 和对象数量移植失败的项目八成在动手之前就注定了。原因往往不是代码写错而是选型阶段没算过账屏幕接口带宽不够、绘制缓冲配得太小、LVGL 堆内存按 PC 端经验给。指南开篇就把“LVGL 移植要求”单列一章其实就是提醒这件事——LVGL 本身很轻但它对“刷屏通路”和“可用 RAM”的要求是硬的软不了。2.1 显示接口决定上限SPI、并口和 RGB 屏差在哪LVGL 的渲染是软件绘制真正决定流畅度的是“一帧像素从 MCU 送到屏上要多久”。先算一个最朴素的值分辨率 320×240、颜色深度 16bit一帧原始数据就是 320×240×2 153600 字节约 150KB。如果走 SPI时钟 40MHz、实际有效速率按 80% 估算大约 4MB/s理论上一秒能推 26 帧左右但这只是纯搬运还没算 LVGL 自己的绘制时间、SPI 中断开销和 DMA 启动间隙。实际能到 1215fps 就算不错了做静态页面没问题做滑动列表和动画就会明显掉帧。把几种常见通路摆在一起看更直观接口类型典型带宽适合分辨率在 LVGL 场景下的表现SPI40MHz约 4MB/s240×240、320×240静态界面够用动画和大面积刷新吃力8080 并口FSMC约 1020MB/s320×480、480×272中等分辨率的主力方案配 DMA 更稳RGB 并口 显存按像素时钟几十 MB/s480×272 以上刷新最顺但要求 MCU 有 LTDC 类外设和外部 SDRAMMIPI/高速并口模块视控制器而定800×480 以上一般需要带显存或专用驱动 IC 的模组选型时我一般会先确认一件事屏幕有没有自带显存比如常见的 ILI9341、ST7789 这类控制器都带 GRAM。带显存的屏MCU 只需要把“变化的区域”写进去LVGL 的局部刷新才能真正生效不带显存的 RGB 屏必须由 MCU 持续扫帧一旦 DDR 带宽被其他外设抢占就会出现横条纹撕裂。这一点在指南的“显示接口”一节里有对应描述但很多读者会跳过直接抄例程结果换块屏就跑不动。2.2 内存账本绘制缓冲、LV_MEM_SIZE 和任务栈分别要多少第二笔账是 RAM。LVGL 的内存开销分三块彼此独立混在一起算最容易出错。第一块是绘制缓冲draw buffer也就是lv_disp_draw_buf_init传进去的那片内存。它决定 LVGL 一次能渲染多大面积的像素。常见配置有三种单缓冲小尺寸比如屏幕的 1/10、单缓冲全屏、双缓冲全屏。全屏双缓冲最顺但 320×240×16bit 的两块就是 300KB绝大多数没有外扩 SDRAM 的 STM32 型号直接出局。我的建议是没有外扩 RAM 就老老实实用“屏幕 1/10 大小的双缓冲”比如 320×24 两块总共约 30KB配合局部刷新效果已经够看。第二块是 LVGL 自己的堆由LV_MEM_SIZE控制用来放对象、样式、动画、字库描述符。这个值给小了lv_obj_create会返回 NULL界面直接缺块给大了又挤占别的。经验值是只有几个页面、几十个控件的项目16KB32KB 起步控件多、有列表和图片缓存的64KB 以上。它和绘制缓冲是两片完全不同的内存别指望共用。第三块是任务栈。裸机主循环里跑 LVGL栈需求主要看递归深度和局部变量一旦上了 FreeRTOSGUI 任务的栈要单独给512 字2KB是下限带文件系统和图片解码的给到 24KB 更稳。可以用一段小脚本先把预算算出来避免拍脑袋# 估算 LVGL 项目的 RAM 预算单位字节 W, H 320, 240 BPP 2 # 16bit 2 字节/像素 DRAW_DIV 10 # 绘制缓冲取屏幕高度的 1/10 DOUBLE_BUF True # 是否双缓冲 draw_buf W * (H // DRAW_DIV) * BPP * (2 if DOUBLE_BUF else 1) lv_heap 32 * 1024 # LV_MEM_SIZE按控件规模调 gui_stack 4 * 1024 # GUI 任务栈FreeRTOS 下才需要 other 8 * 1024 # 驱动、字库数组、临时缓冲等杂项 total draw_buf lv_heap gui_stack other print(f绘制缓冲: {draw_buf/1024:.1f} KB) print(f合计约: {total/1024:.1f} KB)这段脚本没有玄学就是把三块内存显式加起来。参数里最值得调的是DRAW_DIV和lv_heap把DRAW_DIV改小缓冲变大能减少 flush 回调次数但吃 RAMlv_heap改动后一定要跑一遍完整界面看有没有对象创建失败。我习惯在调试阶段把LV_USE_ASSERT_MALLOC打开一旦堆不够会直接断言比运行到一半花屏好排查得多。2.3 把工程骨架先立起来目录、include 路径和编译选项第三笔账是“工程结构”它不占资源但决定了后面改配置时会不会一团乱。指南里的做法是把 LVGL 源码放在工程目录下单独的文件夹和 BSP、APP 并列我基本沿用这个思路只是会更明确地分层Project/ ├── Drivers/ # 厂商 HAL / 标准外设库 ├── BSP/ # 屏幕、触摸、SDRAM 等板级驱动 ├── Middlewares/ │ └── LVGL/ │ ├── src/ # LVGL 核心源码整体拷入 │ ├── examples/ # 官方例程只留 porting 和需要的 demo │ ├── lv_conf.h # 配置文件放这一层别放 src 里 │ └── lvgl.h # 入口头文件 └── APP/ └── ui/ # 自己的界面代码这样分层的好处是升级 LVGL 版本时只需要替换src和exampleslv_conf.h和自己的ui目录不受影响。include 路径至少要加三条Middlewares/LVGL为了lvgl.h、Middlewares/LVGL/src、Middlewares/LVGL/examples/porting。编译选项上有两个坑一是lv_conf.h里的LV_COLOR_DEPTH必须和屏幕实际像素格式一致不一致会出现颜色错位二是如果用的是 C99 以下的编译器要把 LVGL 里用到的变长数组和inline相关宏检查一遍老版本 Keil 上尤其容易在这里报错。3. 裸机移植全流程从源码裁剪到第一个 flush 回调工程骨架立好之后裸机移植是整份指南里最值得跟着走一遍的部分因为它把所有依赖都暴露出来了没有 RTOS 帮你兜底tick 要自己给flush 要自己写主循环节奏要自己控。走通这一遍后面上 FreeRTOS 只是把“谁在什么时候调用”换个位置。3.1 添加 LVGL 源码哪些目录必须进工程LVGL 的src目录是全量拷进来的不要试图只挑几个文件。它内部按模块划分core、draw、widgets、font、extra等模块之间有交叉引用手动裁剪很容易漏掉一个lv_xxx.h导致链接报错。真正该裁剪的是examples目录porting必须留里面有显示、输入、文件系统的移植模板demo只留你打算跑的那一个其余全部删掉否则编译时间和 Flash 占用都会翻倍。把文件加进工程后先别急着编译检查两件事lv_conf.h是否被正确找到它默认从lvgl.h里以#include lv_conf.h的方式引入所以路径必须能被搜索到以及LV_CONF_INCLUDE_SIMPLE这个宏有没有定义。很多“编译通过但所有功能都是默认值”的情况就是lv_conf.h压根没被包含进去LVGL 用了内部默认配置表现出来就是颜色不对、字体全无。3.2 lv_conf.h 逐项改九个决定成败的开关lv_conf.h是整份指南里信息密度最高的一节几百个宏但真正影响移植成败的就那么几个。按重要性排/* lv_conf.h 关键项按移植顺序改 */ #define LV_COLOR_DEPTH 16 /* 必须与屏幕像素格式一致16 或 32 */ #define LV_MEM_CUSTOM 0 /* 用 LVGL 自带堆管理简单可靠 */ #define LV_MEM_SIZE (32U * 1024U) /* 按 2.2 的预算给 */ #define LV_DISP_DEF_REFR_PERIOD 30 /* 默认刷新周期 ms影响 lv_timer_handler 频率 */ #define LV_INDEV_DEF_READ_PERIOD 30 /* 输入扫描周期 ms触摸响应快慢看它 */ #define LV_USE_LOG 1 /* 调试期打开正式版可关 */ #define LV_USE_ASSERT_MALLOC 1 /* 堆不够直接断言别让它静默失败 */ #define LV_TICK_CUSTOM 1 /* 用自定义 tick而不是让 LVGL 自己数 */ #define LV_TICK_CUSTOM_INCLUDE bsp_tick.h #define LV_TICK_CUSTOM_SYS_TIME_EXPR (get_ms_tick())逐项说一下为什么这么设。LV_COLOR_DEPTH和屏幕不一致时最常见的是红蓝互换或者整体偏色因为 LVGL 内部按 RGB565 组像素驱动按 BGR 发出去中间少了一次转换。LV_MEM_CUSTOM保持 0 是省事LVGL 自带的lv_mem有碎片整理和断言比自己接malloc更容易定位问题如果项目里已经有成熟的内存池再改成 1 并接自己的实现。LV_DISP_DEF_REFR_PERIOD不要设得太小设成 5ms 只会让lv_timer_handler空转真正的瓶颈在 flush不在定时器。LV_TICK_CUSTOM这一项值得单独提醒。裸机下如果不配置自定义 tickLVGL 就不知道“现在过了多少毫秒”动画、定时器、长按判定全部失效表现是动画不动、按钮长按没反应。接一个 1ms 的 SysTick 计数进去就行不需要额外定时器。3.3 显示驱动注册flush_cb 里到底该做什么显示移植的核心就一个回调flush_cb。LVGL 在渲染完一块区域后调用它把color_p指向的像素数据搬到屏幕对应区域搬完调用lv_disp_flush_ready告诉 LVGL 这块可以复用了。/* 显示驱动注册与 flush 回调基于常见 8080 并口屏 */ static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[320 * 24]; /* 屏幕 1/10 高度的绘制缓冲 */ static lv_color_t buf2[320 * 24]; /* 双缓冲没内存就删掉这块 */ static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w area-x2 - area-x1 1; uint32_t h area-y2 - area-y1 1; /* 1. 设置屏幕的局部刷新窗口 */ lcd_set_window(area-x1, area-y1, area-x2, area-y2); /* 2. 启动 DMA 搬运搬运完成中断里再调 flush_ready */ lcd_start_dma_transfer((uint8_t *)color_p, w * h * 2); /* 若不用 DMA这里同步写完后再调用下一行 */ lv_disp_flush_ready(drv); } void lv_port_disp_init(void) { lv_disp_draw_buf_init(draw_buf, buf1, buf2, 320 * 24); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res 320; disp_drv.ver_res 240; disp_drv.flush_cb disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); }几个参数和逻辑必须说清。lv_disp_draw_buf_init的第三个参数是缓冲能容纳的像素数不是字节数双缓冲时传单块的大小。flush_cb里设置的窗口坐标必须是“物理屏幕坐标”如果屏幕做了旋转要在disp_drv.sw_rotate或驱动层处理别在 flush 里临时换算很容易算错方向。用 DMA 的时候lv_disp_flush_ready一定要放在传输完成中断里调用而不是启动 DMA 之后立刻调否则 LVGL 会以为缓冲空了下一帧直接覆盖正在搬运的数据屏幕上就会出现一条一条的错位——这就是最典型的撕裂来源。3.4 tick 与主循环lv_timer_handler 的调用节奏裸机下的主循环结构很简单但节奏不对就会卡int main(void) { bsp_init(); /* 时钟、屏幕、触摸、SysTick */ lv_init(); lv_port_disp_init(); lv_port_indev_init(); while (1) { lv_timer_handler(); /* 处理 LVGL 的定时任务、刷新、动画 */ bsp_touch_scan(); /* 触摸扫描频率不要高于 30ms 一次 */ /* 其他业务逻辑注意别写阻塞式延时 */ } }lv_timer_handler的返回值是“距离下一次需要处理还有多少毫秒”严格一点的做法是拿这个返回值去决定要不要睡一会儿能省电裸机项目里直接空转也常见但要保证循环里没有delay_ms(100)这种阻塞否则动画和触摸全部跟着卡。很多“按钮点不动”的问题追到最后是主循环里有个几百毫秒的阻塞延时LVGL 根本没机会读触摸。4. 上 FreeRTOS 之后GUI 任务、互斥和模拟器三件事裸机跑通之后带操作系统的移植主要是把“调用位置”从主循环挪到任务里同时多出一条铁律LVGL 不是线程安全的。指南里带 OS 移植那章篇幅不长但配套的接口函数和任务划分是重点实际项目里出问题基本都出在这两处。4.1 任务怎么切GUI 任务优先级与栈大小我的习惯是只开一个 GUI 任务所有lv_*调用都在这个任务里完成其他任务通过消息队列把“要改界面”的请求发过来。这样最省事也最不容易出并发问题。GUI 任务的优先级一般设在中低档比通信、电机控制这些实时任务低但比日志、显示统计高栈给 24KB。#define GUI_TASK_STACK 4096 #define GUI_TASK_PRIO 3 #define GUI_PERIOD_MS 10 static void gui_task(void *pv) { lv_init(); lv_port_disp_init(); lv_port_indev_init(); for (;;) { lv_timer_handler(); /* 内部按刷新周期决定干活多少 */ vTaskDelay(pdMS_TO_TICKS(GUI_PERIOD_MS)); /* 让出 CPU别霸占 */ } } xTaskCreate(gui_task, gui, GUI_TASK_STACK, NULL, GUI_TASK_PRIO, NULL);GUI_PERIOD_MS设 10ms 意味着每秒最多处理 100 次对 30ms 的默认刷新周期绰绰有余设成 1ms 只会增加任务切换开销界面并不会更快因为瓶颈还是在 flush 和屏幕带宽。任务栈别抠LVGL 里样式计算和字库解析会有一定递归深度栈溢出表现出来就是随机死机非常难查。4.2 LVGL 不是线程安全的互斥量的正确包法如果确实有多个任务要碰 LVGL比如一个任务刷数据、一个任务处理按键就必须加锁。写法不复杂难的是“包在哪一层”static SemaphoreHandle_t lv_mutex; /* 所有跨任务的 LVGL 调用都套这一对 */ void lv_lock(void) { xSemaphoreTakeRecursive(lv_mutex, portMAX_DELAY); } void lv_unlock(void) { xSemaphoreGiveRecursive(lv_mutex); } /* 在非 GUI 任务里更新界面必须这样写 */ void ui_set_value_safe(lv_obj_t *bar, int32_t v) { lv_lock(); lv_bar_set_value(bar, v, LV_ANIM_OFF); lv_unlock(); }用递归互斥量而不是普通互斥量是因为 LVGL 内部函数之间会互相调用普通互斥量在嵌套场景下会自己把自己锁死。另外要注意lv_timer_handler本身也要在锁里跑否则它正在遍历对象树时另一个任务删了个对象就是标准的野指针。代价是临界区变长所以跨任务的界面更新要尽量合并能一次发完的别拆成十次。4.3 先在 PC 模拟器上把界面调完再烧板子指南里 PC 模拟器那一章容易被跳过但它其实是省时间的关键。模拟器工程基于 SDL在电脑上直接编译运行界面布局、颜色、字体、动画全部可以先调好不用每次改一行代码就烧一次板子。移植到板子上时只需要替换显示和输入两个 port 文件ui目录原样拷过去就行。模拟器的价值在排查上更明显当板子上出现花屏、错位、颜色不对先在模拟器里跑同一份界面代码。模拟器正常、板子不正常问题一定在驱动层flush 窗口、DMA、像素格式两边都不正常问题在界面代码或配置。这一步能把排查范围直接砍一半比在板子上盲猜快得多。5. 移植后常见问题排查五类现象从根因倒推这一节按“现象 → 原因 → 解决”记都是移植阶段真实会撞上的东西按出现频率从高到低排。看到相似现象时先对照原因不要急着改代码。5.1 显示类故障撕裂、闪烁、局部花屏第一类屏幕出现横向撕裂或整屏闪烁。现象是滑动列表时屏幕中间有一条明显的错位线静态界面反而正常。原因基本是两个DMA 还在搬运时 LVGL 已经复用了绘制缓冲或者 flush 窗口设置有误导致相邻两次刷新区域重叠。解决方式是确认lv_disp_flush_ready在 DMA 完成中断里调用同时检查lcd_set_window的坐标是否包含端点x2、y2要减 1 还是不减取决于屏幕控制器手册ILI9341 系列通常需要减 1。另外把绘制缓冲从单缓冲改成双缓冲也能消掉大部分撕裂。第二类局部花屏只有某一块区域颜色错乱。现象是界面大部分正常某个矩形区域反复闪。原因通常是该区域的像素格式和LV_COLOR_DEPTH不一致或者绘制缓冲的像素数被写成了字节数导致越界写入。解决方式是核对lv_disp_draw_buf_init第三个参数是不是像素数并确认屏幕初始化时设置的像素格式是 RGB565。5.2 输入类故障触摸坐标漂移与方向反了第三类触摸点偏了几十像素越往屏幕边缘偏得越厉害。现象是按钮点不中但用调试器打印坐标发现数值是有的。原因是触摸芯片的原始坐标范围没有映射到屏幕分辨率常见于换了不同尺寸的屏但校准参数没改。解决方式是在read_cb里做一次线性映射把触摸 IC 的x_min/x_max/y_min/y_max映射到hor_res/ver_res映射公式写成宏换屏时只改四个端点值。第四类方向反了手指往右划光标往左走。原因是屏幕安装方向和触摸 IC 的坐标系不一致或者屏幕做了 180 度旋转但触摸没跟着转。解决方式是在输入驱动的read_cb里对调 x/y 或做镜像不要试图在 LVGL 层面用lv_indev_set_...绕过那样换一块屏又会错。5.3 资源类故障对象创建失败、运行中死机、字库乱码第五类运行一段时间后死机或者某个页面进去就白屏。现象是lv_obj_create返回 NULL、界面缺控件或者直接进 HardFault。原因按概率排LV_MEM_SIZE不够、任务栈溢出、对象删除后又被访问。解决方式是先把LV_USE_ASSERT_MALLOC打开复现时看断言停在哪再看 FreeRTOS 的栈高水位uxTaskGetStackHighWaterMark低于 20% 就加栈。删除对象之后所有保存过该对象指针的变量都要置空这是血泪经验野指针导致的死机最难定位。字库乱码单独提一句中文显示成方块或者乱码九成是源文件编码不是 UTF-8或者字库里根本没包含那个字。指南在字库那章专门讲了启用 UTF-8 编码编译器选项里也要加-finput-charsetUTF-8两边对不上就会出现“看起来编译通过了屏幕上全是问号”的玄学现象。6. 字库落地内置 C 数组和外部文件系统的取舍与验证中文字库是整个移植里最后一块硬骨头。英文 26 个字母加符号一个几 KB 的字库就够中文常用字 3500 个16×16 点阵下光是位图数据就接近 200KB塞进内部 Flash 往往放不下。指南给了两条路C 语言数组字库编译进固件和文件系统读取字库放在外部 Flash 或 SD 卡。取舍不复杂看两件事——Flash 余量和是否需要动态换字。对比项C 数组字库文件系统字库存放位置内部 Flash随固件烧录外部 Flash / SD 卡占用 RAM只有描述符位图在 Flash 里直接读需要读缓冲按需加载加载速度快直接寻址慢受文件系统读取速度限制换字库要重新编译烧录换文件即可适合场景字固定、量小、产品化多语言、字库大、需要现场升级我一般先用内置方案把界面跑通确认编码和字体渲染没问题再把大字号的中文字库挪到外部。生成字库用 lv_font_conv只裁项目里真正用到的字符集比如界面文案加上数字和标点一个 24 号的中文字库能压到几十 KB# 用 lv_font_conv 生成只含指定字符集的字库输出 C 数组 lv_font_conv --font SourceHanSans.ttf \ --size 24 \ --bpp 4 \ --format lvgl \ --symbols 温度湿度设置返回主界面0123456789 \ -o ui_font_24.c参数里--bpp是每像素位数4bpp 在中文小字号下抗锯齿效果和体积比较平衡1bpp 体积小但有锯齿8bpp 更细腻但体积翻倍。--symbols一定要显式列出来别图省事用--range把整个 CJK 区段都塞进去那样生成的文件能有几 MBFlash 直接爆掉。验证方法很简单但很容易漏在界面上把所有用到的汉字打印一遍看有没有方块或错字再故意切到另一个字号确认LV_FONT_DECLARE声明的字体都被实际引用到没被链接器当垃圾回收掉。从那以后我每次加界面文案都会先跑一遍字库生成脚本把新字补进去再编译省得上板之后满屏问号返工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站