ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LVGL移植正点原子开发板实战:从驱动到UI全流程解析

LVGL移植正点原子开发板实战:从驱动到UI全流程解析 搞过嵌入式界面开发的朋友应该都有体会最头疼的不是业务逻辑而是 UI 框架怎么选、怎么搬到自己板子上。LVGL 作为当前嵌入式领域最主流的开源图形库几乎成了这类需求的默认答案。所以这次我把 LVGL 移植到正点原子开发板上的完整过程整理出来从环境准备到底层驱动接口再到常见坑点逐个拆开讲一遍希望对刚接触 LVGL 或者准备把它搬到新平台的你有所帮助。很多人以为移植 LVGL 就是把源码下载下来、添加到工程里编译通过就算完事其实远远不够。真正跑起来之后你还要面对显示接口对接、触摸输入适配、刷新效率调优、内存分配策略这些现实问题。这篇文章以正点原子系列开发板为主线覆盖 STM32 裸机、带 FreeRTOS 以及 Linux 三类典型环境完整讲述一套从零到能跑 demo 的移植路径并解释每一步背后的原理。1. LVGL 是什么移植到底在干什么1.1 先搞清楚 LVGL 的定位LVGL 全称 LittlevGL现在官方叫 Light and Versatile Graphics Library是一个专门为嵌入式系统设计的开源图形库。它的定位非常明确在资源受限的 MCU 或者入门级 MPU 上提供接近桌面级 UI 的体验。跟 PC 上的 Qt 这种重量级框架不同LVGL 可以在只有几十 KB RAM 的单片机上运行也可以跑在带 LCD 控制器的高性能处理器上这也是它这几年成为行业默认选项的原因。用一句话去理解 LVGL它长着一张标准 API 的脸你不需要关心底层像素怎么刷只要调用lv_label_set_text()这种接口就能搞定文本显示调用lv_btn_create()就能创建一个按钮。实际上 LVGL 本身并不直接操作硬件它只负责维护内部的控件树、计算布局、绘制图形然后通过你提供的一组底层接口把绘制结果丢给屏幕这种设计天然就是为了方便移植到不同平台。从实际应用看LVGL 适合解决这些场景设备状态监控面板、HMI 人机交互界面、仪器仪表显示、智能家居控制屏、甚至一些轻量级游戏界面。比起传统的直接操作 framebuffer 自己维护 UI 状态机LVGL 的控件系统和事件驱动模型能帮你省掉大量重复劳动而且由于官方已经内置了丰富的组件库做出来的界面一致性很高。1.2 为什么选正点原子作为移植载体很多初学者会问网上 LVGL 移植教程不少为什么专门强调正点原子平台其中一个很现实的原因是教学资源的完整度。正点原子长期深耕嵌入式开发板领域无论是 STM32F103/F407、IMX6ULL、RK3568 还是最近热门的 RK3588都能找到详尽的原理图、例程和数据手册。这意味着你在移植过程中遇到问题至少能查到板载 LCD 的具体型号、触摸芯片的通信协议、GPIO/SPI 的接线定义这些信息在纯野板子上很难拿到。另外正点原子大多数板子都标配了 LCD 模块和触摸屏硬件链路是完整的省去自己飞线点屏的麻烦。它的出厂例程里还会提供基本的 LCD 底层驱动这正好可以作为 LVGL 移植的基础。如果你用的是正点原子 STM32 系列开发板出厂代码里的lcd.c、touch.c等文件基本可以直接复用如果是 IMX6ULL 这类 Linux 板文件系统里也带有直接可用的 framebuffer 设备节点移植 LVGL 就变成了如何对接/dev/fb0思路和单片机不太一样但目标一致。1.3 LVGL 版本怎么选一个不起眼但影响巨大的决定LVGL 目前有几个大的分支7.x、8.x 和 9.x不同版本 API 差异不小。8.x 是过去几年应用最广、资料最多的版本特别适合初学者入门因为大多数网上教程、论坛案例都基于 8.x 编写。9.x 是较新的大版本在渲染架构、内部对象管理、输入设备模型上有明显调整API 变化也比较大如果你现在直接拿官方 demo 跑有很多接口名称和调用方式要对着一遍遍查手册。从稳妥角度我的建议是如果你是新学优先选择 8.3.x 的最新维护版本。原因主要有三个第一8.3 的 API 足够稳定且资料丰富出了问题搜索引擎能直接告诉你答案第二很多第三方库、显示驱动适配都是基于 8.x 封装的不需要自己重写太多适配层第三9.x 在内存模型上做了优化但如果你没有特别需求8.3 的性能和体验已经足够优秀。等你在 8.x 上完整跑通一遍理解了 LVGL 的架构思想再迁移到 9.x 也就从容很多。2. 移植前的准备硬件评估和软件环境搭建2.1 不同硬件平台对移植工作的影响很多人一听到移植就会下意识觉得是代码层面的工作实际上在这之前有一个更重要的环节是硬件资源评估。LVGL 对硬件的最小需求官方给了一个参考值ROM 至少 64KB、RAM 至少 16KB、建议有 framebuffer 或者片内 TFT 控制器。但这是跑一个空白界面的最低标准真正要想跑出流畅的动画效果最好能有几百 KB 级别的 RAM 或者外部 SDRAM主频也尽量在 100MHz 以上。对照正点原子常见板卡来看STM32F103ZET6 这一档芯片主频 72MHzRAM 64KB 左右外扩了 512KB SRAM 的板子跑 LVGL 是入门级别可以做但别追求太复杂的效果。STM32H743 这类芯片主频跑到 480MHz有 1MB 以上 RAM跑 LVGL 就流畅得多能做比较丰富的动效。IMX6ULL 是一个 ARM Cortex-A7 核心、主频 800MHz 的 MPU跑 Linux 系统LVGL 只是它上面的一个应用资源和性能都不是瓶颈限制它的是你要用怎么样的人机交互逻辑。RK3568/RK3588 更是高端玩法甚至能用 GPU 做渲染加速。所以在动手之前建议先明确你的目标平台是哪一类因为后面所有驱动适配逻辑、刷新方式、内存分配策略都会因平台而异。STM32 这类 MCU 平台LVGL 直接编译进裸机工程或者 RTOS 任务里通过 SPI/MCU 并口刷屏Linux 平台则是把 LVGL 作为用户态程序通过/dev/fb0或者 DRM/KMS 接口写屏幕。2.2 开发环境准备正点原子用户最常见的开发环境是 Keil MDK特别是 STM32 系列。Keil 的好处是工程管理直观、调试方便缺点是编译速度慢、对内存使用情况不够直观。也有不少朋友用 IAR操作习惯和 Keil 稍有不同但底层原理一致。如果你是 Linux 平台那就直接用交叉编译工具链加 Makefile 或 CMake 管理工程这也是 IMX6ULL 这类 MPU 上面的主流做法。为了少走弯路这里我强烈建议先做一个最小验证步骤在 PC 上跑一遍 LVGL 的模拟器工程。LVGL 官方提供了基于 SDL2 的 PC 模拟器你可以在 Windows 或者 Linux 上直接编译运行不依赖任何真实硬件。这一步能帮你建立起LVGL 工程长什么样的直觉也能用来验证你的配置项有没有改错毕竟在 PC 上调试比在板子上打日志方便太多了。整个移植过程中有一个官方提供的配置头文件lv_conf.h你一定会反复修改里面定义了颜色深度、内存池大小、使能哪些控件、是否启用字体、是否支持触摸等等。第一次使用 LVGL 时请务必把这个文件完整读一遍而不是直接跳过。2.3 源码获取和工程骨架怎么搭LVGL 源码可以直接从 GitHub 克隆稳定版会打 tag。下载之后源码目录里最重要的两个部分是src和examplessrc下面按照核心功能分为多个子目录core是核心对象和事件循环draw是绘图引擎widgets是各种控件实现misc是工具类函数hal是硬件抽象层接口定义。把整个src目录添加进工程之后你在自己的应用代码里只需要 include 一个头文件#include lvgl.h一切对外接口都通过它暴露。需要特别提醒的是LVGL 的移植不是把src加进去就能跑。编译阶段你可能还会遇到缺少配置头文件、缺少lv_conf.h这种问题。官方做法是把lv_conf_template.h复制为lv_conf.h然后根据硬件资源修改宏定义。同时工程里还需要提供一个lv_port_disp.c和lv_port_indev.c这两类文件负责把 LVGL 的绘图结果和输入事件真正接到你的硬件上。3. 认识 LVGL 移植的三要素显示、输入、心跳3.1 为什么核心就这三件事LVGL 的工作模型其实是围绕两个产品和用户的互动过程展开的它要画东西到屏幕所以要一个显示输出接口它要接收人的操作所以需要一个输入设备接口它要驱动动画和界面刷新所以需要一个精准的时间基准。这三个需求抽象出来就是lv_disp_drv_t、lv_indev_drv_t以及lv_tick_inc()函数。你只要把这三件事对接好了整个 LVGL 就跑得起来。用生活里的例子来类比LVGL 像是一个画室显示驱动是画布输入驱动是画笔和颜料而心跳就是画室的时钟。画室知道什么时间该画哪一笔但它不直接接触画布需要通过画布管理员你的驱动函数把画好的内容放到画布上。作为移植者你的核心工作就是当好这个管理员。3.2 显示驱动到底在干什么显示驱动是移植 LVGL 最核心的部分。你要实现的是flush_cb这个回调函数LVGL 内部绘制完一帧画面后会把这个回调丢给你告诉你我这有一块区域的颜色数据你帮我把它刷到屏幕上去。从代码层面看你需要这样初始化一个显示驱动static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[LV_HOR_RES_MAX * 10]; static lv_disp_drv_t disp_drv; lv_disp_draw_buf_init(draw_buf, buf, NULL, LV_HOR_RES_MAX * 10); lv_disp_drv_init(disp_drv); disp_drv.hor_res 480; disp_drv.ver_res 272; disp_drv.flush_cb my_disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv);真正的flush_cb实现根据屏幕接口不同写法差异非常大。如果你的屏幕是 SPI 接口那 flush 函数里就是把颜色数据通过 SPI 写入屏幕控制器的显存如果是并口屏那就通过 FSMC/FMC 总线写数据如果是 RGB 屏这块的 flush 方式则是直接把数据写到 framebuffer完全不需要额外操作硬件这也是为什么 Linux 平台上 LVGL 对接 framebuffer 看起来特别简洁。flush_cb实现完还有一个细节不能漏当你的硬件把数据真正发送完成之后需要调用lv_disp_flush_ready()告诉 LVGL这帧数据我已经处理完了你可以继续绘制下一块了。如果你漏掉这个调用界面会卡死或者只显示一部分内容。3.3 输入设备驱动输入设备这块最常见的就是触摸屏。LVGL 支持三种输入类型定点输入鼠标/触摸、键盘和编码器。触摸屏对应的是LV_INDEV_TYPE_POINTER你需要实现一个read_cb回调。LVGL 会周期性调用这个函数去问你的触摸驱动现在有没有人按在屏幕上按在哪里。static void my_touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) { static lv_coord_t last_x, last_y; if (touch_scan() TOUCH_PRESSED) { last_x touch_get_x(); last_y touch_get_y(); >void my_disp_flush(lv_disp_drv_t *disp, 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); lcd_set_window(area-x1, area-y1, area-x2, area-y2); lcd_write_data((uint8_t *)color_p, w * h * sizeof(lv_color_t)); lv_disp_flush_ready(disp); }有几个地方需要注意。lv_color_t的大小是跟随LV_COLOR_DEPTH变化而变化的当颜色深度是 16 时这个类型是两个字节。当你的底层 LCD 写数据接口是按字节传的传参的时候就要仔细算总字节数。另外不同屏幕在设置窗口set_window之后发送数据的字节序有可能不同有的需要调换高低字节否则颜色出现红蓝互换的情况。如果你用的是带 DSISI 接口的 MCUflush 回调的逻辑几乎一样只是底层变成了并口总线时序。正点原子例程里LCD_Show_Image这类函数能给你一个很好的参考但 LVGL 用的是裁剪区域刷新所以你最好自己用set_window 写数据的方式实现否则每次都刷全屏性能会明显肉。4.4 第四步实现输入驱动和心跳触摸驱动的对接比显示简单得多。正点原子板载的触摸芯片比如 GT9147、FT5426、NS2009在出厂例程里都有现成的驱动函数你只需要在read_cb里调用它们把坐标和按下状态填进lv_indev_data_t结构体。如果是 I2C 接口的电容触摸屏通常还会涉及一个中断引脚可以用查询方式或者中断方式来做。中断方式响应更及时但代码复杂度高一点。入门阶段先用查询方式完全可行只不过在 LVGL 的read_cb里频繁查询 I2C 总线会占用一些 CPU 时间。一个折中的办法是在触摸中断里设置一个标志位read_cb里只读取最新一次扫描保存下来的坐标。心跳这块最标准的做法是在一个定时器中断里调用lv_tick_inc(1)。如果你用的是正点原子带的SysTick定时器注意别和系统其他功能冲突。要是你在裸机环境直接在while(1)循环里用HAL_GetTick()的差值来驱动也可以但精度和稳定性都不如固定中断调用。4.5 第五步初始化并运行 demo到了这一步主函数里需要做的事情已经很清晰了lv_init(); my_disp_init(); // 你的显示驱动注册 my_indev_init(); // 你的触摸驱动注册 lv_tick_inc_config(); // 启动定时器心跳 while (1) { lv_timer_handler(); // 核心处理每 5~10ms 调用一次 delay_ms(5); }lv_timer_handler()是 LVGL 的核心驱动函数它负责处理所有的事件、刷新任务和动画必须被周期性调用而且调用间隔不能太长。如果你发现界面卡顿可以优先检查它被谁阻塞了。你还可以看一下任务结构切分是否合理不要让耗时操作和lv_timer_handler挤在同一个循环里。然后创建一个简单的 demo比如官方自带lv_demo_widgets()注册进lv_init()之后编译烧录屏幕上会显示一个控件齐全的界面。如果这一步能跑通说明核心移植基本完成剩下的事情就是在上面搭你自己的业务界面。4.6 IMX6ULL/Linux 平台上的特殊情况如果你用的是正点原子 IMX6ULL 这类 Linux 开发板移植思路需要做一点转换。LVGL 不再直接去操作寄存器或 SPI 写屏而是把 framebuffer 设备/dev/fb0当作绘图目标。你同样注册一个flush_cb但里面的实现变成ioctl(fd, FBIOPAN_DISPLAY)这类操作或者直接mmap内存后把 LVGL 的数据拷贝过去。比较常见的做法是用lv_drivers库里的 fbdev 驱动它已经把 framebuffer 的 mmap 和 copy 逻辑封装好了你只需要在系统启动后确保 framebuffer 被初始化、分辨率设置正确即可。在 Linux 上你甚至可以直接用lv_lib_png、lv_lib_bmp之类的扩展库读写文件也方便很多。需要注意的是如果你想让界面渲染效率更高用 DRM/KMS 接口替代老的 framebuffer 是更现代的选择但复杂度会上升入门阶段建议还是先跑通 fbdev 再逐步优化。5. 移植路上的常见坑与解决实录5.1 移植中典型问题速查为了让你少踩我踩过的坑我把几个高频问题整理成了一张表按现象、原因、解决三步写好。建议先收藏真用到的时候直接拿过来对照。现象常见原因排查与解决编译报错lv_conf.h找不到头文件路径没添加或LV_CONF_INCLUDE_SIMPLE未开启检查 Keil/IAR 的头文件路径配置确认lv_conf.h位于工程根目录屏幕花屏或颜色不对颜色深度设置与屏规格不一致或者字节序不对核对LV_COLOR_DEPTH在flush_cb里做高低字节交换测试画面只有部分刷新flush_cb没有正确调用lv_disp_flush_ready()确保发送完成、DMA 传输完成后再调用lv_disp_flush_ready()触摸可以点击但位置不对触摸原始坐标未映射或者驱动读数坐标轴方向不对在read_cb里打印坐标与实际触点的差异做线性映射动画卡顿、帧率很低lv_timer_handler()被长时间阻塞或刷新脏矩形太大用逻辑分析仪/打印统计耗时考虑使用 DMA 传输或者把绘制缓存增大界面运行一会儿停止LVGL 内存池耗尽或者有内存碎片用lv_mem_monitor()追踪内存曲线调高LV_MEM_SIZE检查循环里是否不断创建控件32 位平台上指针编译警告lv_color_t大小不同或强转指针不对齐检查LV_COLOR_DEPTH与屏驱动字节序尽量使用官方推荐的转换函数5.2 花屏问题的快速定位法花屏是移植初期出现概率最高的现象很多人一看到花屏就慌其实定位办法很简单。第一步判断是 LVGL 的问题还是你底层 LCD 的问题可以先用你自己的 LCD 驱动函数画一个纯色测试如果纯色都花说明底层驱动或接线有问题跟 LVGL 还没关系。如果纯色正常而 LVGL 跑起来花那大概率是颜色深度或字节序不匹配。这时候可以通过实验来确认把flush_cb里面打印第一个像素点的color_p[0]值理论上如果你设置窗口正确、传输正常屏幕左上角应该出现预期的颜色。常见的红蓝互换问题只需在flush_cb里对每个像素的字节做交换再发送即可解决。这不是最优方案但用来验证字节序问题最直观。5.3 性能瓶颈从刷新机制说起LVGL 的性能瓶颈往往不在绘制计算本身而在屏幕输出这一环。以 SPI 屏为例假设屏幕是 480x272RGB565 格式一帧裸数据大概是 480 x 272 x 2 261120 字节。如果你的 SPI 时钟是 40MHz理论传完一帧也要 52ms 左右也就是最多跑到 20 帧每秒。这还是在纯传数据的理想状态下还没算 LVGL 绘制和等待的时间。正点原子很多板子提供的 LCD 接口是并口或者 RGB 接口带宽比 SPI 高很多但如果使用的还是慢速 GPIO 模拟并口时序效果可能还不如高速 SPI。所以移植时你要先算清自己的屏接口带宽天花板才知道后续优化空间在哪。合理使用 DMA 可以显著减少 CPU 占用在flush_cb里启动 DMA 后立刻返回然后再在 DMA 传输完成中断里调用lv_disp_flush_ready这种“异步冲刷”的方式能让 LVGL 的绘制和像素传输并行起来帧率提升非常明显。6. 加餐与进阶从跑通 demo 到真正做产品6.1 FreeRTOS 环境下的资源协调正点原子不少用户不是用裸机而是用 FreeRTOS 来做任务调度。LVGL 本身是线程安全的吗官方说法是非线程安全这意味着如果多个 RTOS 任务同时调用 LVGL API会存在竞态风险。官方推荐的做法是用一个专门的lvgl_task在这个任务里执行初始化以及所有 LVGL API 调用。你可以在其他任务里通过队列或信号量把UI 更新请求发给这个任务由其统一处理。比较典型的写法是这样的lvgl_task里设置一个循环每隔 5ms任务延时调用一次lv_timer_handler()同时在该任务内部完成所有控件的创建和更新。其他任务想要更新界面时不要直接操作界面对象而是把数据封装成一个结构体写入消息队列UI 任务收到后解析队列再调用 LVGL API 改界面。这样既避开了线程安全问题也让界面逻辑和业务逻辑解耦清晰。如果你必须要在一个高优先级中断或任务里更新界面那也要用lv_timer机制去注册一个回调让 LVGL 在它的主循环里去执行而不是直接调 LVGL API。6.2 中文字体与图片资源的处理嵌入式界面的中国特色需求之一就是中文字体。LVGL 官方默认只带了少量英文字体和符号不包含中文字库你需要把中文字体文件转换成 C 数组或者自定义字体格式。网上有 LVGL 字体转换工具可以导入 TTF 字体文件选择用到的字符范围生成一个.c文件。做产品的时候不要图省事直接塞一个完整 GB2312 字库因为那可能占用数百 KB 的 Flash你应该按实际需求选中用到的几百个汉字添加到字体里。正点原子很多开发板外扩了 SPI Flash 或者 SD 卡也可以把字体放到外部存储动态加载但复杂度会高一些。图片资源的处理逻辑类似可以通过在线转换工具把 PNG/JPG 图片转成 C 数组也可以使用lv_img控件直接引用。图片格式建议统一整理成 RGB565 或者带透明通道的 ARGB8888这样在渲染时不需要额外转换。6.3 效果优化动画流畅度与触摸响应界面跑起来之后实际体验还是要打磨的。动画流畅度的关键有两个刷新率和帧率稳定性。刷新率就是屏幕面板本身的刷新能力MPU 接口屏可以支持高刷SPI 屏往往刷新慢。帧率稳定性则靠程序设计保证lv_timer_handler()被均匀调用同时避免在回调里做太繁重的业务逻辑。触摸响应方面建议把触摸驱动的中断优先级设置得合理一些。不要高到抢占关键时序也不要低到被人忽略。读取触摸数据后可以做一个简单的滤波处理比如连续读取两次坐标差值过大就丢弃但不要过滤得太激进否则滑动会不跟手。6.4 实际项目里的一些经验总结走得多了自然踩过不少坑说两个我觉得最有价值的。第一个就是永远不要在flush_cb回调里去执行复杂逻辑这个函数是在 LVGL 的绘制流程里被调用的它执行的时长直接决定了刷新上限你应该尽量把它写成拷贝数据 启动 DMA的轻量函数。第二是LVGL 的内存池不要贪大也不要过小过大的内存池会在一些没有 MMU 的单片机上导致堆碎片化反而更容易出现问题合理的做法是跑完一段压力测试后检查内存监测数据留出 20% 的余量即可。如果你手上正好有块正点原子开发板照着文中的步骤动手做一遍从点屏到交互跑通一次之后你会对整个嵌入式 UI 开发链路有个非常清晰的认识。后面再去做产品级的界面设计难点就不在迁移上而在于如何把交互体验做得真正贴合用户需求了。
返回列表