LVGL移植实战:从STM32到IMX6ULL的完整流程与避坑指南
发布时间:2026/10/3 15:25:21 锦皓数字建站

做嵌入式GUI开发LVGL是一个绕不开的名字。这个开源图形库这几年几乎成了中低端MCU上做UI的事实标准我在正点原子好几块板子上都移植过LVGL从最早的STM32F103到后来的IMX6ULL踩了不少坑也积攒了一套可以复用的流程。这篇就把完整的移植思路和关键细节梳理一遍给正准备入手的同学一份能直接照着做的参考。LVGL移植这件事网络上教程很多但大多数只告诉你“把文件加进去、改几个宏”却很少讲清楚为什么这样改、底层到底发生了什么。结果就是照步骤做能跑通一旦换块板子、换个屏幕或者想加个功能就完全懵掉。我会从原理讲到实操再把我实际踩过的坑一并交代清楚尽量让你看完之后能理解整个移植过程的本质。1. 移植前想清楚LVGL到底在移什么1.1 LVGL不是一个“软件包”而是一套与硬件解耦的GUI引擎很多同学第一次接触LVGL是从正点原子例程里看到的目录里放了一堆源码文件像是某种“库”却说不清它和普通外设驱动库有什么区别。简单来说LVGL是一套纯C语言实现的图形用户界面库它本身不依赖特定硬件也不管你是STM32、NXP、ESP32还是Linux它只负责“把图形画出来”这件事。它内部有自己的对象系统、事件系统、动画系统、布局管理器、字体引擎和控件库相当于一台运行在MCU里的“小型操作系统级别的UI框架”。那它靠什么和具体硬件打交道靠的是几个抽象接口。LVGL定义了显示驱动、输入设备驱动和系统时钟三个最底层的接入点你只需要把这几个接口用你手上的硬件去实现LVGL就能在你那块屏幕上跑起来。这就像你买了一个嵌入式“电视盒子”它不认识你家电视是哪个牌子但只要电视有HDMI口、能接收标准HDMI信号插上就能用。LVGL就是这个盒子HDMI接口协议就是它定义好的驱动接口你要做的就是把自家屏幕“翻译”成它认识的信号。1.2 一次移植需要对接的三类底层资源具体到代码层面LVGL需要你提供的底层能力其实只有三类。第一是“把像素点画到屏幕上”。LVGL会计算出某个UI控件应该显示在什么位置、什么颜色但真正把颜色数据写入LCD控制器的寄存器、刷新到屏幕上的操作它不负责需要你写一个flush_cb回调函数。这个函数接收一块矩形区域和一份像素颜色数据你需要把它拷贝到LCD显存或直接发送给屏幕驱动芯片。第二是“获取用户的输入”。无论是触摸屏的坐标还是物理按键的状态LVGL需要通过read_cb回调函数定期读取。你拿到触摸芯片的原始坐标数据后要在回调里通过lv_indev_set_state和lv_indev_set_point等接口把坐标提交给LVGL。第三是“提供一个时间基准”。LVGL内部有动画系统、光标闪烁、长按检测等功能都需要一个毫秒级的时间源。它不会自己起定时器而是要求你周期性调用lv_tick_inc(ms)函数告诉它时间又过去了多少毫秒。搞明白这三点移植的本质就清楚了。什么源码添加、宏定义修改所有工作最终都是围绕让这三类接口能够跑通而展开的。1.3 什么时候选LVGL什么时候不选有一个高频问题经常被问到跑UI到底用LVGL、TouchGFX、emWin还是直接在Linux上搞QT我的经验判断是如果你的主控是Cortex-M系列MCU运行内存只有几十到几百KBFlash在1MB以下那LVGL几乎是最优选择因为它裁剪之后可以做得非常轻量。如果你在主控上跑Linux系统追求复杂炫酷的界面LVGL也可以但并不是唯一解QT和其他方案都能干得更好尤其是有大量复杂交互逻辑时。如果是高端的Cortex-A配大内存大屏幕想快速出产品原型LVGL也有价值因为开发门槛相对QT低很多不过界面复杂度上不占优势。正点原子开箱例程里用LVGL的场景主要是STM32F103、F407、H743这些MCU以及IMX6ULL嵌入式Linux下的显示Demo。这两类场景恰好能覆盖LVGL最常见的使用方式。2. 环境准备硬件平台、编译链与源码版本怎么选2.1 正点原子各开发板的硬件差异与影响正点原子的开发板型号很多LVGL移植在不同板子上的工作量差得还是挺大的。我用过的几款可以这样总结精英板/战舰板/探索者板STM32F103/F407系列屏幕接口多为FSMC并口或SPI串口需要自己初始化LCD驱动芯片移植时显示驱动部分是主要工作量。阿波罗板STM32H743/F429等部分型号使用RGB接口屏幕配合LTDC控制器显示带宽更高LVGL跑起来更流畅适合做中级项目验证。I.MX6ULL系列本质上是在Linux framebuffer基础上跑LVGL此时你不需要直接操作LCD寄存器而是通过/dev/fb0设备节点映射显存驱动接口的写法就变了。RK3568/RK3588平台这些属于高性能应用处理器跑LVGL通常是作为跨平台UI在Linux下通过wayland或直接渲染的方式运行涉及的是编译工具链和系统库适配。所以动手之前先确认你的硬件平台这决定了后续移植的代码路径。正点原子的资料包里通常包含不同屏幕的驱动例程第一步不是去找LVGL移植教程而是先确保你手上那块屏幕能在裸机环境下正常显示颜色和字符这一步如果没做好后面LVGL移植根本无从谈起。2.2 开发环境Keil、IAR、STM32CubeMX还是交叉编译链如果你用的是STM32系列开发环境基本就是Keil MDK或IAR两种。正点原子例程默认是Keil工程很多同学跟着例程学习也就习惯了MDK的界面和操作方式。不过我个人建议即使你最终用Keil构建产品也最好先用STM32CubeMX生成一份基础工程再去手动整合LVGL源码。原因是CubeMX可以帮你在GUI界面里配置时钟树、GPIO、FSMC/LTDC、触摸相关的I2C外设等生成代码后你只需要关注LVGL的文件组织逻辑而不是被底层初始化代码所纠缠。如果你移植的目标是I.MX6ULL这类Linux板子那就需要用到交叉编译工具链了。正点原子的Linux开发文档里对交叉编译环境的配置写得很详细你只需要照做即可LVGL那部分源码不做任何修改直接编译就能通过因为LVGL本质上是跨平台的ANSI C代码。2.3 LVGL源码版本选择v8还是v9这个决定很关键LVGL从v8到v9的升级API变化非常大。如果你去看老教程很多代码在v9下直接编译报错。所以版本选择要在项目开始前决定好中途切换是最痛苦的。我的建议是如果你是学习用途跟着正点原子例程做那就用例程自带的LVGL版本通常是v8.3.x因为配套资料最多、踩坑记录最全。如果是从零开始一个新项目且不依赖老代码可以直接用v9界面风格和API设计更现代但你要做好查阅官方文档比百度更有用的心理准备。如果项目周期紧不要追新稳定的v8.3分支大概率比最新的v9分支更能帮你按时交付。v9版本内存管理和显示驱动模型都做了重构内存开销略高但是代码结构更干净对多显示器支持更好。v8.3则相对成熟。具体看项目需求来选。3. 核心配置与接口对接移植的真正难点在这3.1 LVGL源码目录结构与该拷贝哪些文件从GitHub把LVGL源码拉下来之后目录看起来挺唬人但真正需要关心的没几个。lvgl/srcLVGL核心源码你不需要改动里面的代码。lvgl/demos官方写好的示例Demo可以用来快速验证移植是否成功比如音乐播放器Demo、手表Demo等。lvgl/examples更小的示例工程适合学习和测试单个控件。lvgl/lv_conf_template.h配置模板文件。你需要复制一份到你的工程重命名为lv_conf.h。lvgl/lv_drivers或相关组件目录部分版本会有显示驱动和输入驱动的参考实现如果接口碰巧和你的LCD、触摸芯片一致可以直接修改复用否则就参考它的写法。把整个src目录、配置头文件和需要的demos/examples文件夹加进工程即可。有一个细节要特别注意加入工程时文件路径不能有中文或空格某些IDE环境下会莫名其妙报一些让人摸不着头脑的错。3.2 lv_conf.h核心配置项解读lv_conf.h是整个移植过程中最核心的文件。我见过太多人移植不成功其实就是这个头文件里的几个宏没有配置正确。首先要确保LV_CONF_INCLUDE_SIMPLE这个宏被启用否则编译器找不到lv_conf.h。它的含义是“直接在包含路径里寻找lv_conf.h”启用之后LVGL会直接通过#include lv_conf.h找到你放在工程根目录的配置文件。其次是颜色深度配置LV_COLOR_DEPTH。这个值必须和你的屏幕像素格式一致。如果你的LCD是RGB565格式就设置成16是RGB888就设置成32部分平台是24。如果设置不对显示效果就是颜色错乱、花屏。我见过最多的奇怪问题都源于这个参数。然后是显示分辨率LV_HOR_RES和LV_VER_RES对应的就是屏幕宽和高单位是像素。比如4.3寸屏分辨率是800x480宏那就分别填800和480。再一个是内存池大小LV_MEM_SIZE。LVGL内部有一套动态内存分配器默认使用它来管理UI对象和绘图缓冲。内存池太小会导致控件创建失败、显示异常甚至卡死。我通常起步给32KB复杂界面再加到64KB或更高。正点原子F407探索者板子内部SRAM有192KB给LVGL分64KB问题不大。最后是LV_TICK_CUSTOM配置。如果设为0你需要在自己的定时器中断里调用lv_tick_inc如果设为1LVGL会自行获取系统时间但这需要平台提供clock()函数或类似的系统时钟支持。裸机环境下我建议设0由自己控制更直观也更容易排查。3.3 显示驱动对接flush_cb的写法与常见误区LVGL在绘制完一帧的一部分后会调用你注册的flush回调把一块矩形区域的颜色数据刷到屏幕上。这个回调在lv_display_create之后通过lv_display_set_flush_cb注册。一个基本的flush回调模板如下void my_display_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint32_t w lv_area_get_width(area); uint32_t h lv_area_get_height(area); lcd_draw_rgb565(area-x1, area-y1, w, h, (uint16_t *)px_map); lv_display_flush_ready(disp); }很多新手在这里犯错把数据交给LCD驱动发送之后就结束了忽略了最后必须调用lv_display_flush_ready。这个调用本质上是通知LVGL“这块区域已经刷新完成可以继续处理下一块了”。如果不加LVGL会一直等待界面就卡住不动。如果你的屏幕控制器如ILI9341、ST7789、NT35510等支持高效的矩形填充指令那这个flush函数会很有优势。如果不支持就只能逐点写入效率会低很多LVGL帧率上不去就是这个原因。另外一种常见做法是利用MCU的DMA进行异步传输。LVGL允许在DMA传输完成后再调用flush_ready这样绘制和传送可以流水线并行帧率能提升不少。正点原子例程里有些版本就用到了这种方式但要注意DMA缓冲区地址对齐问题否则传输会失败或出现残缺画面。3.4 输入设备对接触摸屏和按键两种方式触摸屏在LVGL里被抽象为指针设备。你需要创建一个输入设备对象并注册一个读取回调。void my_touchpad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) { if (touch_get_point(tp)) { >#define LV_COLOR_DEPTH 16 #define LV_HOR_RES 800 #define LV_VER_RES 480 #define LV_MEM_SIZE (64U * 1024U) #define LV_TICK_CUSTOM 0颜色深度16是因为这块屏幕是RGB565分辨率800x480内存池考虑到后面可能要跑官方demo直接给了64KB。LV_TICK_CUSTOM设为0我自己在定时器中断里调用lv_tick_inc。还有几个可选的宏和特性开关比如LV_USE_DEMO_WIDGETS、LV_USE_DEMO_MUSIC等需要测试demo时才打开。正式项目里建议关闭省内存省Flash。4.3 编写显示驱动和输入驱动适配代码在做完基础的GPIO、LTDC、触摸I2C初始化之后我新增了lv_port.c文件专门放LVGL相关的底层接口实现。里面就干三件事初始化显示、初始化触摸、注册flush和read回调。显示初始化的核心是向LVGL注册一个display对象并把分辨率信息关联进去static lv_display_t *disp; disp lv_display_create(LV_HOR_RES, LV_VER_RES); lv_display_set_flush_cb(disp, my_display_flush_cb);更进一步的优化是使用LVGL提供的buffer。LVGL的绘图过程不是直接画到屏幕上而是先绘制到一块缓冲内存中再由flush函数送去显示。缓冲区可以是单缓冲也可以是双缓冲。双缓冲可以显著减少画面撕裂代价是多用一块内存。static lv_color_t buf1[LV_HOR_RES * 100]; static lv_color_t buf2[LV_HOR_RES * 100]; lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);这个配置表示缓冲区宽度为800、高度为100LVGL会把绘制区域划分成多个100像素高的条带逐条渲染并刷新。这种“部分渲染”模式非常适合内存受限的MCU。flush回调里则通过LTDC控制器把像素数据拷贝进显存地址正点原子例程提供了底层画点、画块函数我直接把这些函数封装进flush里即可。注意数据格式要一致LTDC配置时选择了RGB565像素缓冲也是16位。触摸部分就是读FT5X06的触摸坐标经过必要的坐标变换后通过read_cb上报。4.4 主循环里完成LVGL调度在main函数或一个while循环里每个循环周期都调用一次lv_timer_handler()这个函数是LVGL的“心脏泵”它负责处理定时器任务、重绘界面、处理输入事件。完整的主循环看起来是这样while (1) { lv_timer_handler(); delay_ms(5); }注意不要在两次调用之间间隔太久否则动画帧率会变低触控响应也会显得迟钝。5ms是经验值你可以根据实际需求调整。如果你想在裸机上让LVGL跑得更顺畅可以把主循环里其他无关的延时降到最低比如日志打印、非关键的数据轮询等尽量把CPU时间让给UI渲染。4.5 第一个界面验证移植是否成功在写完底层接口以后先用最简单的方式验证移植是否成功而不是直接上复杂界面。我习惯在main里加上这样一段验证代码lv_obj_t *label lv_label_create(lv_screen_active()); lv_label_set_text(label, LVGL OK); lv_obj_center(label);编译下载后如果屏幕上出现居中的“LVGL OK”文字而且没有花屏、不闪烁说明显示链路已经通了。接下来再测试触摸功能加一个按钮按下时切换颜色看看触摸坐标是否正确如果不准检查触摸坐标方向映射。确认基础功能正常后再逐步引入官方demo验证动画、字体渲染、多控件布局等是否都正常。我用正点原子例程里自带的音乐播放器Demo验证过整机跑起来效果相当不错。5. 常见问题与排查技巧实录5.1 编译报错集中在哪些地方移植LVGL最常见的编译报错有以下几类第一类找不到lv_conf.h头文件。这个通常是因为LV_CONF_INCLUDE_SIMPLE宏被注释掉了或者头文件搜索路径没加对。直接打开lv_conf.h检查宏定义状态再看Keil的C/C选项卡下Include Paths是否包含了lvgl目录。第二类宏定义冲突比如LV_COLOR_DEPTH与某些标准库头文件冲突。这种情况比较少见但碰上了往往是编译选项里有编译器自带的配置宏干扰可以在工程里强制取消那些宏或调整头文件包含顺序。第三类内存溢出不报错但flash溢出。v8.3的LVGL全量编译后能吃掉不小的Flash如果芯片Flash不够Linker会报错。解决办法是裁剪LVGL功能把不需要的字体、控件、特性宏关掉或者更换到容量更大的芯片型号。曾经有人问为什么正点原子F103C8T6移植LVGL总是flash不足其实就是没有裁剪。我把常见编译问题的排查思路整理成了表格报错特征可能原因排查方法fatal error: lv_conf.h: No such file头文件路径或include宏问题检查LV_CONF_INCLUDE_SIMPLE和搜索路径undefined symbol: lv_timer_handler源码文件未添加完整检查src目录下所有.c文件是否加入工程region FLASH overflowed裁剪不足、功能全开按需关闭LV_USE_DEMO等宏region RAM overflowed缓冲区或内存池过大调小LV_MEM_SIZE和绘图缓冲区编译通过但连接后无GUI主循环没调用lv_timer_handler查看源码逻辑5.2 花屏、闪屏与颜色错乱花屏和颜色错乱是我排查最多的问题通常原因有几种颜色错乱先看LV_COLOR_DEPTH是否匹配屏幕格式。RGB565屏配16位、RGB888屏配32位这个不能随意改。如果还是不对检查LTDC的像素格式配置和LVGL的buffer是否一致比如LTDC配成了RGB888但LVGL按RGB565生成数据颜色就会完全不对。花屏往往和绘制缓冲区的管理有关。如果用部分渲染模式要确保LCD的地址窗口设置正确。LCD控制器芯片在收到矩形数据后会自动根据当前地址窗口移动写指针如果窗口设置不对数据就会写到屏幕外面或位置错乱。ILITEK、ST、格科微这些屏驱芯片的地址窗口命令存在差异要参考芯片手册调整。闪屏则通常是双缓冲没配好或flush_ready调用时机不对导致绘制内容和显示内容互相干扰。建议先用单缓冲把画面调出来再改双缓冲做优化。5.3 触摸点击不准和漂移触摸点击不准的问题几乎每个人都会遇到。第一反应不是改LVGL而是在底层检查触摸芯片的原始坐标是否准确。LCD屏幕的x方向很可能和触摸芯片的x方向反了或者坐标原点的位置不同。比如触摸芯片原点在左下角而屏幕原点在左上角那就需要做坐标变换。调试方法很简单在read_cb里用串口打印出坐标值然后点击屏幕四个角落比对实际点到的坐标和打印出来的坐标就能判断出是镜像、旋转还是偏移问题。还有一种轻微偏移多是因为触摸屏本身有校准需求。LVGL没有内置校准机制如果你的硬件一致性不好可以在read_cb里做线性映射或者移植一个触摸校准算法。不过正点原子的屏幕品质一致性还不错大部分情况下验证坐标方向就够了。5.4 界面卡顿和帧率低下的优化思路界面卡顿最直接的原因是绘制效率低。有了LVGL不代表动画就很流畅底层LCD驱动的写入速度是硬瓶颈。优化方向有四个用DMA替代CPU逐点拷贝把像素数据搬运的负担从CPU转移到DMA控制器。尽量使用支持矩形区域填充的LCD控制命令避免全屏刷新。开启LVGL合层功能和GPU加速接口。如果MCU带2D图形加速器比如STM32F429的DMA2D或RT系列内部2D引擎可以实现硬件加速。降低UI复杂度减少透明效果、阴影等需要大量绘制的特性。正点原子H743系列带硬件JPEG和DMA2D在处理图片和图层叠加时优化效果非常明显。不过这些优化要在保证功能正确后再做不要一开始就追求极致性能先跑起来再优化。5.5 工程从裸机迁移到FreeRTOS的注意事项很多人在跑通裸机LVGL后会考虑加FreeRTOS实现更丰富的业务逻辑。正点原子也有不少例程是FreeRTOS和LVGL结合的。这里最核心的坑就是线程安全问题。LVGL的API默认不是线程安全的如果多个任务同时调用LVGL接口界面会闪退、卡死甚至硬件错误。解决办法一般有两种用互斥锁保护LVGL相关操作或者设定一个专属任务来负责所有LVGL调用其他任务通过消息队列和信号量与之通信。第二种方式思路更清晰推荐在实际项目中使用。还有一个容易忽略的地方是内存管理。FreeRTOS和LVGL各自有自己的堆内存管理策略如果用了FreeRTOS的内存分配函数去分配LVGL的缓冲区要特别留意内存池大小配置别让两者互相抢占导致堆耗尽。6. 我能给新手的几条经验性建议6.1 先跑通屏幕驱动再看LVGL文档我见过不少初学者一上来就下载LVGL源码连屏幕是RGB接口还是SPI接口都没搞清楚结果移植好几天最后发现是屏幕初始化代码本身就有问题。所以在动手做LVGL移植之前你应该先用LCD裸机驱动例程点亮屏幕在屏幕上画一个纯色背景或显示一张图片。这一步可以验证LCD的数据总线时序、地址窗口、背光控制和色彩格式是否正常。只要这一步稳定了LVGL那边的活儿其实就是“把像素数据喂给你的屏幕驱动”。正点原子的资料包里有非常详细的裸机LCD例程把这部分吃透后面移植LVGL会感觉非常顺。6.2 多做减法能不用的功能先全部关掉不要一开始就把所有Demo、所有控件、所有特性全部打开。这些功能打开之后编译时间变长、Flash和RAM消耗变大出了问题也难排查。我在每次移植时都会先把lv_conf.h里所有LV_USE_DEMO_*和LV_BUILD_EXAMPLES相关的宏全部关掉只保留最基本的功能等界面跑起来了再逐步打开需要的特性。这样好处非常明显第一问题定位范围小如果崩了大概率是显示或内存问题第二编译时间短调试效率高。6.3 保留一个“可回退点”方便随时复盘建议在完成每一步关键配置后都保留一个可编译通过的工程备份。比如屏幕点亮算一个里程碑LVGL显示文本算一个里程碑触摸可用算一个里程碑。这样如果你后续配置遇到了奇怪的问题可以直接回退到最近正常的状态再一步一步往前进而不是从头再来。正点原子的例程也是这样组织的先LCD裸机例程然后加入LVGL点灯例程最后是LVGL综合Demo。这种工程管理方式看起来很基础但真的能帮你节省大量排查时间。LVGL的移植过程说到底就是搞清楚“三层关系”LVGL内核抽象层、底层硬件驱动层、以及它们之间的接口粘合层。只要思路清晰不管用什么芯片、什么屏幕遇到问题都能快速定位到具体环节。希望这篇从头到尾的操作记录能让你少走几步弯路。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。