做嵌入式Linux开发这几年,我接触过的图形方案不算少。从早些年用DirectFB做简单界面,到后来接触Qt、GTK,再到最近两年LVGL在ARM开发板、工业HMI项目里频繁被点名。说句实在话,LVGL能在嵌入式Linux上流行起来,不是因为它功能比Qt全面,而是因为它把"轻量"和"易上手"这两件事做到了极致。这篇文章是我基于一个实际量产项目的完整复盘记录——LVGL在嵌入式Linux上的移植过程、性能调优、问题排查,以及一些在官方文档里不太容易找到的经验。项目最终跑在Cortex-A7双核1.2GHz的平台上,内存256MB,屏幕是1024x600的RGB屏,系统是BusyBox裁剪后的精简Linux,LVGL版本从8.3升级到了9.2。整个过程踩了不少坑,也总结了不少可复用的方法,希望能给正在做类似方案的朋友一些参考。
1. 移植前必读:为什么在Linux上要选LVGL
1.1 从MCU到Linux的图形方案演进
很多团队走上LVGL这条路,其实是从MCU开发转过来的。早期的STM32、GD32等MCU上跑LVGL非常成熟,工程师们已经习惯了LVGL的控件风格和开发节奏。到了Linux平台,资源从"捉襟见肘"变成了"相对宽裕",但这不是说就能随意铺张——工业产品对成本极度敏感,很多主控板用的还是老一代Cortex-A7/A9,内存就128MB到512MB,还要跑业务逻辑、通信协议、数据库,留给图形的资源并不多。
在这个环境下,常见的图形方案有几种:Qt、GTK、AWTK、LVGL。Qt全家桶功能最强,但也最重,一个最小的Qt Widgets应用内存占用轻松飙到80MB以上,启动时间按秒算;GTK在嵌入式Linux上的定位更偏向桌面或复杂应用,依赖组件多,裁剪起来费劲;AWTK是国内团队维护的跨平台GUI,也很轻,但生态相对窄;LVGL的优势在于,它本身就是一个为资源受限场景设计的图形库,1MB内存的MCU都能跑,到了Linux上更是游刃有余。
我用一个表格简单对比一下:
| 方案 | 最小内存占用 | 启动速度 | 开发门槛 | 界面效果 | 典型场景 |
|---|---|---|---|---|---|
| Qt/Widgets | 80MB+ | 秒级 | 较高 | 丰富 | 复杂桌面级HMI |
| GTK | 50MB+ | 秒级 | 较高 | 丰富 | 特定Linux桌面/工业 |
| AWTK | 20MB左右 | 百毫秒级 | 中等 | 良好 | 国产化HMI项目 |
| LVGL | 5-15MB | 百毫秒级 | 低 | 良好 | 设备面板/中控屏 |
LVGL在Linux上还有一个隐性优势:它和MCU端的代码结构几乎一样。团队里熟悉MCU开发的人可以直接复用底层逻辑,不需要像Qt那样重新学习一套对象模型和信号槽机制。如果你所在的团队之前在单片机项目里积累过LVGL代码,那搬到Linux上几乎是平滑过渡。
1.2 项目场景与选型依据
什么样的产品适合用LVGL做Linux端界面?我根据自己的项目经验总结为四类:
第一类是设备控制面板类,比如工业触摸屏、电力监控终端、医疗设备操作界面,特点是界面层级浅、控件数量有限、强调响应速度与稳定性。第二类是家电和智能家居中控,比如带屏的智能音箱、变频空调控制面板,界面简单且动画需求不高,LVGL内置的动画完全够用。第三类是仪器仪表界面,偏数据展示,需要大量图表和仪表盘,LVGL有chart控件和仪表控件,稍微美化一下就很专业。第四类是对启动速度极度敏感的设备,比如汽车后装设备,开机要在几百毫秒内出画面,LVGL这这种轻量库能快速点亮屏幕,而Qt要考虑大量动态库加载,很难做到同样速度。
反过来,如果你的产品需要复杂的非规则窗口、高密度表格交互、网页风格排版,或者要嵌入视频播放器和浏览器,那LVGL并不是好选择。它本质上还是面向"图块化界面"的控件库,做不到浏览器那种布局灵活性。遇到这种需求,别硬扛,换Qt或直接上Web技术栈更靠谱。
1.3 显示后端选型:fbdev还是DRM/KMS
这是移植第一步就要面对的问题。Linux下让LVGL把画面输出到屏幕,有两条主流路径:传统framebuffer(fbdev)和现代DRM/KMS。
fbdev是历史悠久的显示子系统,通过/dev/fb0这样的设备节点向用户空间暴露显存。好处是简单——打开设备、mmap内存、写入像素数据,屏幕就亮了;坏处是内核社区早已把fbdev标记为过时,新平台尤其是带GPU的SoC,fbdev多是用DRM模拟出来的,性能和功能都有限制。如果你的目标平台是树莓派、RK系列这类现代芯片,只用fbdev会浪费掉硬件合成、多平面显示这些能力。
DRM/KMS是现代Linux的标准显示方案。KMS负责管理显示模式、分辨率、连接器等,DRM则提供渲染与提交接口。用DRM/KMS需要处理更复杂的ioctl调用和对象模型,比如connector、encoder、crtc、plane,初期学习曲线比fbdev陡。但换来的是更好的可控性:能利用硬件plane做图层合成,能配合VBlank信号做垂直同步防撕裂,能使用GPU的DMA-BUF直接提交渲染结果。
我的建议是:如果只是快速验证UI逻辑,先用fbdev跑通,成本最低,驱动代码加显示注册总共不到200行;如果产品要量产,或者你的平台有GPU/G2D加速单元,直接用DRM/KMS走,省得后面重新适配。我这次项目最终使用的是DRM/KMS方案,因为目标平台主控芯片内部带了2D硬件加速器,想物尽其用。fbdev版本我留了分支代码,用来做单元测试和模拟器调试。
2. 一步步完成LVGL在嵌入式Linux上的移植
2.1 获取源码与目录结构
LVGL的源码管理非常成熟,GitHub官方仓库分为LVGL核心库(lvgl)和显示/输入驱动库(lv_drivers)。不过到了9.x版本,驱动库的维护重心已经迁移,很多平台直接建议开发者自己实现display和input后端,不再强依赖lv_drivers。我建议你也按这个思路来:用官方核心库,自己写Linux下的display/input适配层。
版本选择上,LVGL目前有两条主流分支:8.3.x是最后的稳定大版本,API非常稳定,网上资料最丰富,很多老项目都停留在这条线;9.x是当前迭代版本,API变化较大,但加入了更多现代特性,比如更灵活的样式系统、新的事件机制、更好的缩放支持。如果你参考的教程和示例代码大多是8.x的,建议先用8.3版本上手;如果项目周期长、想保持前瞻性,可以选9.x,但一定要去官网看迁移文档。
我这次项目因为要长时间维护,选择了9.2版本。拉取代码很简单:
git clone https://github.com/lvgl/lvgl.git --branch release/v9.2仓库内部结构清晰,核心目录是src,里面有display、indev、widgets、draw、misc等子目录。移植时不需要改核心库源码,只需要在项目里新建一个lv_port目录,放自己的平台适配代码。
2.2 配置lv_conf.h:正确启停特性
LVGL的裁剪配置集中在lv_conf.h文件里。第一次使用要先从lvgl目录复制模板出来:
cp lvgl/lv_conf_template.h lv_conf.h这个文件至关重要,它决定了LVGL编译后的大小和运行时的功能集。对Linux平台,有几个关键宏必须正确设置。
#define LV_COLOR_DEPTH 32 #define LV_MEM_CUSTOM 1 #define LV_DPI_DEF 130 #define LV_FONT_MONTSERRAT_14 1 #define LV_USE_PERF_MONITOR 1 #define LV_FPS_MONITOR 1LV_COLOR_DEPTH必须和屏幕实际像素格式匹配。如果你的屏幕是RGB888,就用32;如果是RGB565,用16。颜色深度不匹配会导致画面颜色错乱,这一点在调试时特别容易让人抓狂。
LV_MEM_CUSTOM在Linux平台强烈建议设为1,这样LVGL会使用系统标准库的malloc/free来管理内部内存,省去手动配置LVGL内部内存池的麻烦。MCU端因为堆空间有限才需要用内部内存池,Linux上根本没有这个必要。
另外几个宏根据需求打开:需要内嵌英文字体就启用对应字号,需要中文字体就去lv_font模块里打开LV_FONT_SIMPLIFIED_CHINESE。尺寸控制上,如果产品只用少量控件,可以关掉不用的扩展组件,比如LV_USE_FLEX和LV_USE_GRID如果项目用不到就关闭,能省不少内存。
2.3 显示驱动实现:framebuffer初始化与刷新流程
显示适配的核心是让LVGL能把渲染好的像素数据拷贝到屏幕的显存区域。在Linux下,无论是fbdev还是DRM/KMS,最终逻辑都殊途同归:获取一个可写的显存指针,然后定义一个flush回调函数,LVGL在需要刷新时调用它。
先看DRM/KMS模式下如何获取显存指针。关键步骤是打开DRM设备文件(通常是/dev/dri/card0),然后查询可用的connector和crtc,最后通过drmModeAddFB和drmModeSetCrtc设置显示模式。这段代码比较长,我简化核心流程:
int drm_fd = open("/dev/dri/card0", O_RDWR); drmModeRes *res = drmModeGetResources(drm_fd); // 遍历connector、encoder、crtc,找到第一个已连接的connector drmModeConnector *conn = drmModeGetConnector(drm_fd, res->connectors[0]); // 挑选匹配当前分辨率的mode drmModeModeInfo mode = conn->modes[0]; // 创建framebuffer uint32_t fb_id; int ret = drmModeAddFB(drm_fd, mode.hdisplay, mode.vdisplay, 24, 32, stride, (uint32_t)framebuffer_addr, &fb_id); // 设置显示模式 drmModeSetCrtc(drm_fd, crtc->crtc_id, fb_id, 0, 0, &conn->connector_id, 1, &mode);之后用mmap将显存映射到用户空间,得到一个void *buf指针。到这里,屏幕已经点亮,我们可以自由地向这块内存写入像素。
LVGL端显示驱动的初始化也很直接。核心是创建一个display对象并绑定flush回调:
lv_display_t *disp = lv_display_create(1024, 600); lv_display_set_flush_cb(disp, flush_cb); lv_display_set_buffers(disp, buf1, buf2, buf_size, LV_DISPLAY_RENDER_MODE_DIRECT);flush_cb是真正的渲染核心函数,LVGL会把需要刷新的区域和渲染好的颜色数据传进来,我们只需要拷贝到显存并在结束后调用lv_display_flush_ready告诉LVGL这块已经搞定。实现如下:
static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_p) { int w = lv_area_get_width(area); int h = lv_area_get_height(area); // area->x1, area->y1是刷新区域左上角坐标 for (int y = 0; y < h; y++) { memcpy(fb_base + (area->y1 + y) * fb_stride + area->x1 * bytes_per_pixel, color_p + y * w * bytes_per_pixel, w * bytes_per_pixel); } lv_display_flush_ready(disp); }这里有几个坑要注意。第一是行字节数必须对齐,很多显示控制器要求每行字节数按照16或32字节对齐,代码里要用fb_stride而不是简单的宽乘字节数。第二是颜色格式转换,如果LVGL的LV_COLOR_DEPTH是32而屏幕实际是24位或16位,需要做像素格式转换,这一步最耗CPU,后面优化部分会细说。第三是lv_display_flush_ready一定要在数据真正写到显存之后调用,否则LVGL认为显存已经可读,下一个帧可能直接读旧数据导致闪烁。
2.4 输入设备接入:触摸屏与按键
显示通了以后,输入设备是第二个必须处理的环节。嵌入式Linux的触摸屏和按键大多通过输入子系统上报事件,用户空间通过/dev/input/eventX读取。LVGL自带的lv_indev框架可以对接键盘、鼠标、触摸屏、编码器等多种输入设备。
触摸屏适配时,逻辑上要做两件事:从evdev读原始触摸坐标,然后把这个坐标映射为LVGL的坐标。evdev读数据的核心代码很短:
int fd = open("/dev/input/event1", O_RDONLY); struct input_event ev; while (read(fd, &ev, sizeof(ev)) > 0) { if (ev.type == EV_ABS) { if (ev.code == ABS_X) touch_x = ev.value; else if (ev.code == ABS_Y) touch_y = ev.value; } else if (ev.type == EV_KEY && ev.code == BTN_TOUCH) { touch_pressed = (ev.value == 1); } }拿到坐标以后,喂给LVGL的方式有两种。一种是自己解析事件并调用lv_indev_set_state和lv_indev_set_point去更新一个lv_indev;另一种是更常见的做法——在LVGL的输入设备read_callback里返回当前的坐标和按键状态。后者能让LVGL内部的事件循环更统一,我推荐用这种方式。
static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data) { >scale_x = 1024.0 / 4095.0; scale_y = 600.0 / 4095.0;如果产品遇到过触摸不准问题,之后可以在采样时再叠加校准参数。
按键输入方面,如果产品只有物理按键,没有触摸屏,LVGL的LV_INDEV_TYPE_ENCODER可以同时处理按键和旋钮事件。我在项目里做过一版纯按键控制,配置一个encoder输入设备,把一个GPIO短按映射为LV_EVENT_CLICKED,加一个旋转编码器映射为focus切换,用户的操控体验完全不输触摸屏。
3. 性能优化实战:从30帧到60帧的调优记录
3.1 渲染缓冲策略与双缓冲实现
图形库最影响帧率的往往是渲染路径上几个点——缓冲大小、刷新方式、像素拷贝效率。LVGL在Linux端跑不高帧率,很多时候不是LVGL本身慢,而是我们的缓冲策略没有用好。
LVGL支持三种渲染模式:LV_DISPLAY_RENDER_MODE_PARTIAL(部分刷新)、LV_DISPLAY_RENDER_MODE_DIRECT(直接模式)、LV_DISPLAY_RENDER_MODE_FULL(全屏模式)。在Linux平台上,如果屏幕不大,推荐用DIRECT模式,配合双缓冲能有效减少撕裂。
我用的是双缓冲方案。在DRM模式下,创建两个framebuffer对象,LVGL绘制时轮流提交,等垂直同步信号到来时切换显示源。这样用户在屏幕上看到的始终是完整的一帧,不会出现画面中间断裂的撕裂现象。
static lv_display_t *disp; static void *buf1, *buf2; buf1 = malloc(1024 * 600 * 4); buf2 = malloc(1024 * 600 * 4); disp = lv_display_create(1024, 600); lv_display_set_buffers(disp, buf1, buf2, 1024 * 600 * 4, LV_DISPLAY_RENDER_MODE_DIRECT);在flush回调里,不是急着memcpy,而是用DRM的drmModePageFlip或drmModeSetCrtc切换显示指针到渲染好的buffer。这一招能极大提升流畅度,代价是要能守住显存指针的同步逻辑。
如果你不想用DRM层面的双缓冲,也可以在LVGL层面做双缓冲,即上面代码里的buf1和buf2都作为LVGL的绘制目标,LVGL内部轮流渲染,flush时才拷贝到屏幕。这样也能减少撕裂,但多一次内存拷贝会耗掉一些性能,适合对代码复杂度比较敏感的团队。
3.2 局部刷新与脏矩形原理
LVGL渲染不是整屏重画,而是只绘制"脏矩形"——也就是内容发生变化的区域。这跟很多UI框架的思路一样。如果你不理解这个概念,可能在flush回调里傻乎乎地每次都整屏memcpy,那性能就废了。
在flush回调里,area参数就是LVGL认为需要更新的脏区域。我一开始的代码是整屏拷贝,结果帧率非常低,CPU飙到80%。后来改成只拷贝area指定的区域,渲染负载立刻降低一个量级。
局部刷新还有一个隐藏收益:可以配合显示控制器的"部分显示"功能。比如有些RGB屏支持设置显示窗口(partial window),只需要把变化区域告诉LCD控制器,它就能只刷新那一块,这对低速总线接口(如SPI屏)尤其有效。在RGB屏加DRM方案下,虽然没有窗口收益,但memcpy的数据量减少了,缓存命中率提高了,效果同样明显。
3.3 显存访问与DMA/硬件加速
在嵌入式Linux上,显存访问比MCU更复杂,因为涉及CPU缓存一致性。很多ARM平台上,外部DMA控制器和CPU看到的物理内存视图是同一块内存,但CPU写完后,数据还在cache里,没有真正落到内存。这时候如果显示控制器直接从内存读,就会读到旧数据,画面变成"花屏"。
解决办法有几种。最简单的是在flush回调完成后调用dma_cache_sync或者msync刷新缓存,但这很拖性能。更好的方案是在DRM模式下,使用DRM_IOCTL_MODE_MAP_DUMB创建带DMA-BUF能力的buffer,并通过DRM_IOCTL_SYNCOBJ或显式的fence机制保证数据就绪后再提交显示,这个路径完全绕开了用户空间手动同步的麻烦。
如果平台带了2D硬件加速器(比如Rockchip的RGA、Allwinner的DE2),那还抱着memcpy优化就太浪费了。这类加速器提供了blit和rotate的硬件实现,LVGL的flush回调可以改为把数据交给硬件加速器处理。我在RK平台上的实践是,使用libdrm提供的drmPrimeHandleToFD拿到DMA-BUF文件描述符,直接提交给RGA做色彩格式转换和缩放,CPU基本空闲下来,帧率从35帧直接跳到60帧。
不过硬件加速并不是一上来就要搞的东西。开发阶段先用CPU memcpy把功能调通,等确认真有性能瓶颈再上硬件加速,否则会平白增加调试复杂度。我的建议是分两步走:先软加速跑通,再用perf工具测量哪里热点最高,最后针对热点做定向优化。
3.4 内存占用与CPU占用平衡
LVGL在Linux下的内存优化策略和MCU不太一样。MCU要考虑静态内存池限制,Linux则可以用系统malloc,看起来没有上限,但如果界面写得臃肿,内存照样会被吃爆。我在项目里做了两件事控制内存:一是精简动画资源,LVGL的动画其实很耗内存,因为它要保存插值计算的中间状态,动画多而杂的话内存蹭蹭涨;二是对位图素材做了取舍,很多图标用字体图标替代,而不是存成一张张PNG,节省了大量内存。
CPU占用方面,LVGL的main loop通常是一个while(1)循环里反复调用lv_timer_handler()。这个函数的调用频率直接决定帧率上限。默认刷新周期是33ms,也就是30帧左右。如果你的界面动画不多,可以把刷新周期调大,降低CPU占用;如果是展示数据变化频繁的图表,可以调小。我这边把LV_DEF_REFR_PERIOD从33调整到20,动画流畅度明显提升,CPU占用也只增加了3%。
LVGL还提供了性能监控功能,显示左上角的帧率和CPU占用。开发阶段务必开起来,调优时数据说话,比凭感觉靠谱得多。
4. 移植与优化中的常见问题速查
4.1 屏幕闪烁、撕裂如何解决
屏幕闪烁大多数情况是因为LVGL绘制时,数据直接写到了当前正在显示的显存里。用户看到的是"边擦边画"的效果,视觉上就是闪。撕裂则是屏幕刷新一半时显存内容被替换,画面上半帧和下半帧来自不同的渲染帧。
这两类问题的通用解法就是双缓冲加垂直同步。在DRM/KMS方案下,用drmModePageFlip切换显示buffer,并等待VBlank事件,可以从机制上确保用户看到的永远是完整的一帧。fbdev方案下,需要自己控制两个缓冲区的切换,并且尽量在屏幕的防撕裂等待期间完成拷贝,逻辑上麻烦不少。
我在项目里遇到过一种特殊情况:LVGL的flush回调还没执行完,显示控制器已经开始扫描扫描线,导致屏幕顶部正常、底部闪烁。解决方法是把flush回调里的memcpy拆成上下两半,先拷完上半帧再让LVGL继续,但这引入了额外复杂度。最后还是切到了DRM的PageFlip方案才彻底解决。
4.2 触摸漂移、点击位置不对
触摸不准是嵌入式Linux上最常见的输入问题,尤其是杂牌触摸屏。表现形式有两种:点击位置整体偏移,或者屏幕边缘区域点击误差大。
第一种问题通常是因为触摸坐标未做缩放匹配。LINUX触摸设备通过ABS_X和ABS_Y上报的通常不是像素坐标,而是模数转换值,需要根据设备报告的axis范围做比例换算。可以用ioctl(fd, EVIOCGABS(ABS_X), &absinfo)拿到轴的范围,代码里做映射。如果映射后还是偏,那就是触摸屏本身有旋转或者贴合偏差,需要通过tslib或者手动校准。
第二种边缘误差大的情况,往往是触摸屏的触控IC线性度一般,四周区域响应不准确。可以在LVGL层面做区域过滤,把靠近边缘的坐标做一定的拉伸校正。不过这个属于治标不治本,最好还是换好一些的触摸屏模组。
4.3 启动卡死、显示异常
移植过程中最容易遇到的三类启动问题:
一是打开/dev/fb0或/dev/dri/card0时提示权限拒绝。这是Linux的ACL权限问题,把运行LVGL程序的用户加入video组,或者给设备文件加0666权限即可。
二是屏幕没有信号,黑屏但程序不报错。DELL排查顺序是:先确认设备节点存在,再确认连接器是connected状态,然后用modetest工具测试DRM能否正常出图,最后才轮到LVGL。
三是LVGL初始化后立即崩溃或死循环。多半是lv_conf.h里定义的颜色深度和显存格式不匹配,或者LV_MEM_CUSTOM设置为0导致内部内存池不够用。排查方法是打开LV_USE_LOG,观察日志输出到哪一步卡住。
4.4 中文字体显示为方块
LVGL默认不带中文字体,如果你直接用一个自定义字符串显示"你好",屏幕上是不会出现字形的,要么是空白要么是方块。解决方法是让LVGL使用自定义字体,有两种路径:
一种是在代码里加载字体文件,比如lv_font_load("path/to/font.bin"),这需要先把TTF字体转换成LVGL格式的bin文件,转换工具是lv_font_conv。另一种是转换成C数组,编译时直接嵌入可执行文件,省去文件系统依赖。我常用后者,因为很多精简Linux没有完整的文件系统,直接编译进固件最稳妥。
中文显示还有两个细节:一是字体文件通常很大,一个完整的中文字库动辄几MB,如果只需要常用字,可以在转换时指定--range参数只提取需要的字符,比如0x4E00-0x9FA5的前三千个常用字,体积能缩小到几百KB;二是LVGL渲染大字体时内存占用可观,建议优先用内置字体,实在要自定义再加载外部字体。
5. 实践复盘:移植优化中的几条个人经验
5.1 我踩过的几个值得说的坑
第一个坑在双缓冲和DRM切换逻辑上。我一开始参考了一些简化示例,在flush_cb里直接切换显存地址,没有等VBlank信号,结果画面撕裂非常厉害。后来加了线程等待drmWaitVBlank,情况好很多,但也引入了延迟。最终改为在VBlank事件中提交PageFlip,这才算稳定下来。
第二个坑是区域坐标转换。LVGL在DIRECT模式下,坐标是相对于display左上角的,但DRM的framebuffer可能因为对齐策略多了一些边缘填充,导致拷贝到显存时坐标总是不对。后来我仔细看DRM的stride参数,发现它可能比实际显示宽度大很多,于是所有行地址计算都改用stride而不是width * 4,问题迎刃而解。
第三个坑更隐蔽。在fbdev调试时,我发现手动修改一个像素测试显示正常,但LVGL刷上来就是花屏。后来查了很多资料才明白,有些fb驱动默认是RGB565格式,而LVGL按RGB888渲染,两者不匹配。这种问题用fbset查一下屏幕depth就能定位,但我当时绕了不少弯路。
5.2 移植完成后的团队协作经验
移植LVGL到Linux不只是底层适配工程师一个人的事,UI和应用开发同样需要配合。我发现一个很有效的协作模式:底层适配完成后,先把LVGL跑在PC模拟器上(LVGL官方支持SDL模拟器),UI设计师和前端工程师在模拟器里开发界面;底层工程师在真机上做性能和稳定性测试。两边通过统一的lv_conf.h和代码仓库同步,等UI在模拟器上验收通过后,再合并到真机分支做联调。
这个方法特别适合团队中有多个开发人员的场景,避免了真机资源争抢,也加快了UI迭代速度。模拟器模式还能输出日志和异常栈,调试起来比真机方便得多。
我在实际项目中还发现一个容易被忽视的点:LVGL的线程安全问题。由于LVGL 9.x开始支持多线程渲染(通过LV_USE_OS配置),如果你的业务代码会从别的线程里调用LVGL接口,必须加锁保护。我这边选择的是单线程模型,所有LVGL调用都集中在主循环里,避免了很多竞态问题。业务线程通过队列和信号量与主循环通信,虽然代码稍多,但稳定性和可排查性非常好。
5.3 后续可以继续扩展的方向
这次项目做完,我给自己留了一个关于LVGL性能优化的long-running任务:把更多绘制工作从CPU搬到GPU。虽然平台有2D加速器,但目前只是用在了flush阶段的memcpy上,LVGL自身的光栅化(绘制alpha混合、边框、圆角等)仍然由CPU完成。如果能把LVGL的draw接口重定向到GPU的绘制队列,整个界面的渲染成本会进一步下降。这个方向官方已经有实验性的GPU加速支持,应用在复杂界面时提升幅度很可观。
另外一个可扩展的方向是显示内容分层。很多HMI产品其实可以分成背景层、业务层、警告层,分别对应DRM的多个plane,让硬件完成合成。这样LVGL只需要管业务层,警告层可以通过系统级机制直接点亮,响应时间能做到很低。这个思路在医疗或工控产品里价值很大,我准备在下一个项目里重点验证。
5.4 给后来者的三点真诚建议
第一,不要一上来就想把所有功能都跑起来。先用fbdev的最小demo点亮屏幕,再用模拟器跑通UI,最后再上DRM和双缓冲。底层适配和界面开发解耦,能让团队并行工作,整体节奏反而更快。
第二,lv_conf.h里的宏不是越多越好,相反,没用的功能模块最好全关掉。LVGL的代码是模块化的,哪些宏没有定义,编译器就会连对应的代码都不编译。我见过一个团队把所有控件都开着,编译出来的库比我这边多了200KB不止,内存占用也高了不少。
第三,做性能优化时一定把perf工具用起来,不要凭感觉猜瓶颈。先用top看看CPU占用,再用perf top定位具体函数,确认是memcpy慢还是LVGL内部绘制慢,再决定开不开放硬件加速。我调试过程中,有两次以为是显示刷新问题,结果却是中断风暴导致系统长时间处理中断,与LVGL本身没关系。拿数据说话,是最稳妥的调优方式。