1. 为什么要在 SBC2332 上折腾本地 HMI
手里有一块 SBC2332 单板计算机,跑着 Linux,串口、网口、GPIO 都通了,但每次调试都要 SSH 上去敲命令,现场操作的人根本不会用。这种场景下,加一块屏幕做本地人机界面(HMI)就成了刚需。SBC2332 这类板子算力不算强,内存也有限,跑 Qt 会显得笨重,启动慢、占用高,而 LVGL 恰好填补了这个空档——它是一个轻量级开源图形库,C 语言编写,对硬件要求低,能在资源受限的嵌入式 Linux 上跑出流畅的界面。
我最初接触这个组合,是因为一个环境监控的小项目:SBC2332 采集温湿度、控制继电器,需要一块 7 寸屏实时显示数据并支持触摸操作。试过用 Web 界面,浏览器一开内存就吃紧;试过 Qt,编译出来的程序几十兆,启动要好几秒。换成 LVGL 之后,整个 GUI 程序编译出来不到 2MB,启动几乎瞬间完成,触摸响应也很跟手。这就是 LVGL 在嵌入式 HMI 场景里的核心价值:用最小的资源代价,换来一个可交互的本地界面。
这篇文章面向的是有一定 Linux 基础、想在 SBC2332 或类似单板计算机上做本地界面的开发者。我会从环境搭建讲起,把 LVGL 在 Linux 上的运行机制、显示与输入设备的对接、界面开发流程、以及实际踩过的坑都摊开来说。不管你是刚接触 LVGL 的新手,还是从 STM32 裸机移植过来的老手,在 Linux 单板机上跑 LVGL 的思路和裸机是有区别的,这些区别正是本文要重点讲清楚的地方。
需要先明确一点:SBC2332 上跑的是完整 Linux,LVGL 在这里不是直接操作寄存器,而是通过 Linux 的帧缓冲(framebuffer)或 DRM 接口来显示,通过 evdev 接口来读取触摸事件。理解这一层,后面所有配置就都顺了。
2. SBC2332 上 LVGL 的运行底座:显示与输入怎么打通
2.1 Linux 下 LVGL 和裸机移植的本质区别
很多从 STM32 转过来的朋友,习惯了自己写disp_flush往 LCD 控制器里灌数据,自己写触摸扫描函数。到了 Linux 上,这套思路要换。Linux 已经把显示和输入抽象成了标准设备节点,LVGL 要做的是对接这些节点,而不是直接碰硬件。
具体来说,显示侧有两个选择:framebuffer(/dev/fb0)和DRM(/dev/dri/card0)。framebuffer 是老接口,简单直接,往/dev/fb0写数据就能显示,适合小屏和简单场景。DRM 是新接口,支持硬件加速、多图层,但配置复杂。SBC2332 这类板子通常 framebuffer 就能满足需求,我建议先用 fbdev 跑通,再考虑要不要上 DRM。
输入侧统一走evdev,触摸屏在系统里表现为/dev/input/eventX。LVGL 官方提供了evdev驱动,直接读事件就行,不用自己解析触摸协议。
这个架构的好处是:LVGL 完全运行在用户空间,不碰内核,编译调试都方便,换块板子只要设备节点对得上,程序基本不用改。
2.2 确认 SBC2332 的显示设备节点
上手第一步,先确认板子上的显示设备。SSH 登录后执行:
ls /dev/fb* cat /sys/class/graphics/fb0/virtual_sizevirtual_size会输出类似1024,600的内容,这就是当前 framebuffer 的分辨率。如果屏幕没点亮或者没接,这个节点可能不存在,需要先检查屏幕的供电和排线。
触摸设备这样找:
cat /proc/bus/input/devices输出里找Handlers那一行,带eventX的就是触摸设备。也可以用evtest工具直接测试:
evtest /dev/input/event1手指点屏幕,终端里会刷出坐标事件,说明设备正常。这一步很关键,设备节点找错了,后面 LVGL 怎么配都不会有反应。
2.3 framebuffer 的像素格式与 LVGL 颜色配置要对齐
framebuffer 有固定的像素格式,常见的是 RGB565(16 位)和 ARGB8888(32 位)。用这个命令查:
cat /sys/class/graphics/fb0/bits_per_pixel如果是 16,对应 LVGL 的LV_COLOR_DEPTH 16;如果是 32,对应LV_COLOR_DEPTH 32。这两个必须一致,否则显示出来的颜色会错乱,比如红色变蓝色、图像花屏。
我在一个项目里就吃过这个亏:板子默认是 RGB565,我按 32 位配的 LVGL,结果界面颜色全反了,排查了半天才发现是颜色深度没对齐。所以这一步别偷懒,先查清楚再动手。
2.4 用 lv_port_linux 快速搭起骨架
LVGL 官方维护了一个lv_port_linux仓库,专门用于 Linux 平台的移植,里面已经把 fbdev 和 evdev 的对接代码写好了。直接克隆下来:
git clone https://github.com/lvgl/lv_port_linux.git cd lv_port_linux git submodule update --init --recursive这个仓库的结构很清晰:lvgl/是图形库本体,main.c是入口,显示和输入的驱动在lv_drivers里。编译用 CMake:
mkdir build && cd build cmake .. make -j4编译产物是一个可执行文件,直接跑就能看到 LVGL 的默认界面。如果屏幕亮了、触摸有反应,说明底座通了,接下来才是真正的界面开发。
提示:交叉编译时要在 CMake 里指定工具链文件,把
CMAKE_C_COMPILER指向 SBC2332 对应的交叉编译器,否则编出来的是 x86 程序,板子上跑不了。
3. 从零写一个能用的界面:LVGL 开发流程拆解
3.1 先理解 LVGL 的对象树模型
LVGL 的界面是由一个个"对象"(lv_obj)堆起来的,对象之间是父子关系,形成一棵树。屏幕(screen)是根,按钮、标签、容器都挂在它下面。父对象移动,子对象跟着动;父对象删除,子对象一起销毁。这个模型和前端开发里的 DOM 树很像,理解了这一点,布局和事件处理就都好办了。
创建对象的基本套路是:先lv_obj_create(parent)创建,再设置位置、大小、样式,最后挂事件回调。比如一个按钮:
lv_obj_t *btn = lv_btn_create(lv_scr_act()); lv_obj_set_size(btn, 120, 50); lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0); lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);lv_scr_act()返回当前活动屏幕,所有顶层对象都挂在它下面。这套 API 在 LVGL 8.x 和 9.x 之间有些变化,9.x 里lv_btn_create被lv_button_create替代,写代码前先确认版本。
3.2 用容器和布局做自适应排版
硬编码坐标在小屏上还行,屏幕一大就乱。LVGL 提供了 Flex 和 Grid 两种布局,配合容器使用,能让界面自适应。
Flex 布局适合横向或纵向排列:
lv_obj_t *cont = lv_obj_create(lv_scr_act()); lv_obj_set_size(cont, LV_PCT(100), LV_PCT(100)); lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP); lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);这样往容器里加子对象,它们会自动排列、自动换行,不用手动算坐标。Grid 布局更适合表格状的界面,比如参数设置页,行列对齐很规整。
我的经验是:能用布局就别用绝对坐标。项目后期改需求,加一个按钮、调一下顺序,用布局改一行代码就行,用绝对坐标得重算一遍。
3.3 中文字体是绕不过去的坎
LVGL 默认只带英文字体,中文要自己生成。官方有个在线字体转换工具,选好字体文件、字号、需要的汉字范围,导出成 C 文件,在代码里声明后就能用。
这里有个坑:别把整个中文字库都转进去,几万个汉字转出来 C 文件几十兆,嵌入式设备根本放不下。正确做法是只转界面上实际用到的字,比如"温度""湿度""设置""返回"这些,几十个字就够了,文件才几 KB。
如果界面文字会动态变化(比如显示传感器名称),那就把可能出现的字都列出来一起转。实在拿不准,可以转常用的一级字库(约 3500 字),文件大小在可接受范围内。
3.4 事件回调里别做耗时操作
LVGL 是单线程的,所有界面刷新和事件处理都在一个循环里跑。如果在事件回调里做耗时操作,比如读串口、等网络响应,整个界面就会卡住。
正确的做法是:回调里只做状态标记,耗时操作放到单独的线程或定时器里,处理完再通过lv_async_call回到主线程更新界面。LVGL 提供了线程安全的机制,但用起来要小心,跨线程操作对象必须加锁。
我见过一个项目,在按钮回调里直接sleep(1)等传感器响应,结果点一下按钮界面卡一秒,用户体验极差。改成异步之后,界面立刻响应,数据到了再刷新,流畅多了。
3.5 定时器驱动数据刷新
界面上的数据要实时更新,比如温度每秒变一次。用 LVGL 的定时器最方便:
lv_timer_t *timer = lv_timer_create(update_sensor_cb, 1000, NULL);update_sensor_cb每秒被调用一次,在里面读数据、更新标签。定时器的周期根据实际需求定,温度变化慢,1 秒够了;如果是实时曲线,可能要 100ms 甚至更快。
注意定时器回调也是在主线程跑的,同样不能做耗时操作。读传感器如果慢,就在定时器里发个信号给采集线程,让线程去读。
4. 实测中那些让人抓狂的坑
4.1 屏幕不亮:先查背光和 framebuffer 控制台
屏幕接上了,程序也跑了,但屏幕一片黑。这种情况先别怀疑代码,按顺序排查:
第一,背光有没有开。很多屏幕的背光需要单独控制,可能是一个 GPIO,也可能是通过 sysfs 调亮度:
echo 255 > /sys/class/backlight/backlight/brightness第二,framebuffer 控制台有没有占用显示。Linux 启动时会把 console 输出到 fb0,如果 LVGL 程序在跑但控制台还在刷,画面会乱。可以在启动参数里加console=tty1把控制台切到别的 tty,或者用fbset关掉。
第三,确认程序真的在往 fb0 写。可以在disp_flush里加打印,看有没有被调用。如果没调用,说明 LVGL 的刷新机制没跑起来,检查lv_timer_handler有没有在主循环里定期调用。
4.2 触摸坐标偏移或反向
触摸能响应,但点 A 处 B 处动,或者上下左右反了。这是触摸校准的问题。evdev 驱动读出来的是原始坐标,需要做一次线性映射到屏幕坐标。
LVGL 的 evdev 驱动支持校准参数,在初始化时设置:
evdev_set_calibration(1, 0, 0, 0, 1, 0);这六个参数是变换矩阵,具体值要根据实际偏移量算。简单的方法是:点屏幕左上角和右下角,记录 evdev 输出的坐标和屏幕实际坐标,算出缩放和偏移。
如果方向反了,把对应的缩放系数改成负数。比如 X 轴反向,第一个参数从 1 改成 -1,再调整偏移量。
4.3 内存不够导致界面卡顿
SBC2332 内存有限,LVGL 默认的缓冲区如果开太大,会挤占系统内存;开太小,刷新会撕裂、卡顿。缓冲区大小要权衡。
LVGL 的显示缓冲区建议至少是屏幕宽度的 1/10,比如 1024 宽的屏,缓冲区至少 102 行。如果内存紧张,可以用双缓冲但每块小一点,或者用单缓冲。
在lv_conf.h里配置:
#define LV_MEM_SIZE (48U * 1024U)这是 LVGL 自己的内存池,界面对象多的时候要调大。如果程序跑着跑着崩溃,多半是这个值太小了。
4.4 程序退出后屏幕残留画面
LVGL 程序退出后,framebuffer 里还是最后一帧的画面,屏幕不会自动清空。如果程序要重启,最好在退出前把屏幕刷成黑色:
lv_obj_clean(lv_scr_act()); lv_timer_handler();或者直接往 fb0 写零。这个细节在正式产品里要注意,不然重启瞬间会闪一下旧画面。
4.5 交叉编译时的库依赖问题
在 PC 上编译好的程序拷到板子上跑,报错找不到库。这是因为链接的库版本和板子上的不一致。解决办法是用交叉编译工具链自带的库,或者把依赖库一起拷过去。
用ldd查程序依赖:
arm-linux-gnueabihf-ldd your_program把列出来的库都确认板子上有。静态链接能省掉这个麻烦,但程序会大一些。对于 SBC2332 这种存储不紧张的板子,静态链接反而省心。
5. 让界面真正好用:性能与体验优化
5.1 减少重绘面积
LVGL 默认会重绘整个屏幕,但实际变化的可能只是一个小区域。开启局部刷新能显著降低 CPU 占用:
#define LV_DISP_DEF_REFR_PERIOD 30这个值控制刷新周期,30ms 约等于 33 帧,够用了。再快对嵌入式设备是浪费。
另外,把不常变化的背景和常变化的数值分开,背景用静态图片,数值用标签,这样刷新时只重绘标签区域。
5.2 图片资源用合适的格式
界面上的图标、背景图,如果直接用 PNG 解码,运行时开销大。LVGL 支持把图片转成 C 数组,编译进程序,运行时直接读,速度快很多。
转换工具用官方的在线转换器,选好颜色格式(和屏幕一致),导出 C 文件。注意图片尺寸别太大,一张全屏背景图转成 C 数组可能几百 KB,要权衡。
如果图片多,可以用文件系统存,LVGL 支持从文件加载图片,但读取速度比内存慢,适合不常显示的图。
5.3 用样式表统一管理外观
界面上的按钮、标签如果一个个设颜色、字体,代码又乱又难维护。LVGL 的样式(style)机制可以把外观抽出来:
static lv_style_t style_btn; lv_style_init(&style_btn); lv_style_set_bg_color(&style_btn, lv_color_hex(0x2196F3)); lv_style_set_radius(&style_btn, 8);然后把这个样式加到所有按钮上。改外观只改一处,全局生效。这是写出可维护界面的关键。
5.4 启动速度优化
LVGL 程序启动时,如果界面复杂,创建对象会花时间。优化方法:把不立即显示的页面延迟创建,先显示主界面,用户点进去再创建子页面。
另外,字体和图片如果从文件加载,启动时会慢。编译进程序虽然占空间,但启动快。这是个取舍,看项目需求。
5.5 看门狗与异常恢复
嵌入式设备要长期运行,程序崩溃了得能自动恢复。可以加一个看门狗,主循环里定期喂狗,程序卡死时自动重启。
LVGL 本身比较稳定,但如果有内存泄漏,跑几天就会崩。用lv_mem_monitor定期检查内存使用,发现持续增长就排查哪里没释放。
6. 这套方案还能怎么扩展
SBC2332 加 LVGL 的组合,跑通之后能做的事情不少。往深了做,可以接 Modbus 采集工业设备数据,界面上做实时曲线;可以接摄像头,用 LVGL 显示视频帧;可以加网络模块,把数据传到服务器,本地界面做配置和监控。
LVGL 9.x 版本对硬件加速的支持更好了,如果 SBC2332 有 GPU,可以开启 DRM 加速,界面会更流畅。不过对于大多数 HMI 场景,fbdev 加软件渲染已经够用。
从 STM32 裸机转过来的朋友,在 Linux 上跑 LVGL 最大的思维转变就是:不要直接碰硬件,用系统提供的设备节点。这个转变适应了,后面就是纯应用开发,和写 PC 程序差不多。
我在实际项目里最大的体会是:LVGL 的学习曲线前期陡,对象树、样式、事件这几个概念理清了,后面就是查文档拼积木。真正花时间的不是写界面,而是调试显示和触摸的对接,以及处理各种环境差异。把底座搭稳,上层开发其实很快。