SBC2332嵌入式Linux上LVGL本地HMI开发实战指南
2026/9/20 12:04:55 网站建设 项目流程

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_size

virtual_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_createlv_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 的学习曲线前期陡,对象树、样式、事件这几个概念理清了,后面就是查文档拼积木。真正花时间的不是写界面,而是调试显示和触摸的对接,以及处理各种环境差异。把底座搭稳,上层开发其实很快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询