1. 为什么要在 SBC2332 上折腾本地 HMI
手里捏着一块 SBC2332 这类单板计算机,第一反应通常是拿它跑个服务、做个网关,或者当个微型服务器用。但实际项目里,尤其是工控、自助终端、仪器面板这些场景,客户往往还想要一块能看、能点、能实时响应的屏幕。这时候问题就来了:用 Qt 吧,资源占用高,启动慢,小内存板子跑起来吃力;用 Web 前端套壳吧,浏览器一开又是几百兆内存没了,而且触摸响应总感觉隔了一层。LVGL 就是在这个夹缝里杀出来的方案——它是一个用 C 写的开源嵌入式图形库,核心目标就是“在资源受限的硬件上跑出流畅的图形界面”。
SBC2332 这类板子通常基于 ARM Cortex-A 系列处理器,跑的是 Linux 系统,内存从 256MB 到 1GB 不等,显示接口可能是 RGB、MIPI DSI 或者 HDMI。它不像 STM32 那种裸机单片机那样资源紧张,但也远没到能随便挥霍的程度。LVGL 在这里的定位很微妙:它比裸机方案灵活得多,因为底下有 Linux 和 framebuffer 撑着;又比 Qt 轻量得多,一个带触摸交互的界面跑起来,内存占用可以控制在几十兆以内,启动时间也能压到一两秒。
我最初接触这个组合,是因为一个工业面板项目。客户要求 7 寸屏、电阻触摸、界面要有按钮、滑块、实时曲线,还要能通过串口跟下位机通信。板子就是 SBC2332 级别的硬件,内存 512MB。一开始试了 Qt,编译出来的程序加上依赖库,根文件系统直接膨胀了快 200MB,启动要七八秒,客户现场等得直皱眉。后来换成 LVGL,整个应用编译出来不到 2MB,加上字体和图片资源也就 10MB 出头,启动时间缩短到一秒多,触摸响应也跟手了。这个对比太直观,从那以后,凡是 SBC2332 这类板子上的本地界面需求,我基本首选 LVGL。
这篇文章就是把这套流程完整拆开,从环境搭建、显示后端对接、输入设备处理,到界面设计、性能调优、踩坑记录,全部讲清楚。适合手里有 SBC2332 或类似 Linux 单板、想跑本地图形界面但又不愿意上 Qt 的开发者。不管你是刚接触嵌入式 Linux 的新手,还是从单片机转过来的老手,都能从里面找到能直接抄的配置和能避开的坑。
2. SBC2332 上跑 LVGL 的底层逻辑与方案选型
2.1 LVGL 在 Linux 环境下的运行方式
LVGL 本身是一个纯软件图形库,它不直接操作硬件,而是通过一层“显示驱动”和“输入驱动”跟底层打交道。在 Linux 系统上,最常见的对接方式有三种:framebuffer、DRM/KMS、以及 SDL 模拟器。SBC2332 这类板子通常内核里已经使能了 framebuffer 设备,也就是/dev/fb0,这是最省事的入口。
framebuffer 的本质是一块内存区域,内核把这块内存映射成屏幕上的像素。你往里面写颜色值,屏幕就显示对应内容。LVGL 的lv_linux_fbdev驱动就是干这个的:打开/dev/fb0,用mmap把显存映射到用户空间,然后把 LVGL 渲染好的画布刷进去。这个过程不需要 X11、Wayland 这些显示服务器,直接在控制台层就能跑,资源开销极低。
另一种方式是 DRM/KMS,它比 framebuffer 更现代,支持硬件加速、多层合成、页面翻转等特性。如果你的 SBC2332 内核支持 DRM,并且你愿意多花点时间配置,DRM 方案在刷新率和撕裂控制上会更好。但代价是代码复杂度上升,而且不是所有板子的 DRM 驱动都稳定。对于大多数中小尺寸、低刷新率的 HMI 场景,framebuffer 完全够用,我建议先从它入手。
2.2 为什么不用 Qt 而选 LVGL
这个问题我被问过很多次。Qt 功能强大、生态成熟、开发工具完善,为什么还要用 LVGL?答案就一个字:轻。但“轻”背后是一系列具体的数字和场景差异。
先看内存占用。一个最小化的 Qt Widgets 应用,加上 QtCore、QtGui、QtWidgets 这几个库,运行时内存通常在 30MB 到 80MB 之间,取决于界面复杂度。如果用到 QML,内存还会更高。而 LVGL 应用,整个进程内存可以控制在 5MB 到 20MB,界面元素多的时候也就 30MB 左右。对于 256MB 内存的 SBC2332,这个差距直接决定了你能不能同时跑其他后台服务。
再看启动时间。Qt 应用启动时要加载动态库、初始化图形后端、创建窗口系统,冷启动通常要 3 到 8 秒。LVGL 直接操作 framebuffer,没有窗口系统这一层,启动时间可以压到 1 秒以内。在工业现场,操作员按下电源开关,屏幕立刻亮起来能操作,这个体验差距是巨大的。
还有编译和部署。Qt 的交叉编译工具链配置复杂,依赖库多,根文件系统里要塞进去一堆.so文件。LVGL 就是一个 C 库,静态链接进你的应用,编译出来一个可执行文件,拷贝到板子上就能跑。部署简单到令人发指。
当然,LVGL 也有短板。它的控件库没有 Qt 那么丰富,复杂布局和动画效果需要自己写更多代码,没有可视化设计器(虽然有一些第三方工具),国际化支持也相对基础。但如果你的界面是典型的 HMI 风格——按钮、标签、滑块、图表、列表——LVGL 完全能胜任,而且性能更好。
2.3 硬件资源评估与显示后端选择
在动手之前,先确认几件事。第一,你的 SBC2332 屏幕分辨率是多少?LVGL 在 800x480 或 1024x600 这种分辨率下跑 framebuffer,单缓冲刷新率可以到 30fps 以上,双缓冲可以到 60fps。如果分辨率上到 1920x1080,framebuffer 的带宽压力就大了,这时候要考虑 DRM 或者降低刷新率。
第二,内存够不够?LVGL 的显示缓冲区大小直接影响流畅度。一个 800x480 的 16 位色屏幕,全屏缓冲区需要 800x480x2 = 768KB。双缓冲就是 1.5MB。再加上 LVGL 内部的对象、样式、字体缓存,整个应用内存占用大概在 10MB 到 30MB。SBC2332 如果有 512MB 内存,跑这个绰绰有余。
第三,触摸屏接口是什么?常见的电阻触摸走 SPI 或 I2C,电容触摸走 I2C 或 USB。Linux 内核会把它们统一成/dev/input/eventX设备。LVGL 的lv_linux_evdev驱动就是读这个设备节点,解析 input 事件,转换成 LVGL 的点击、滑动坐标。
第四,显示接口是 RGB、MIPI DSI 还是 HDMI?这决定了内核里 framebuffer 设备的名称和参数。RGB 屏通常对应/dev/fb0,MIPI DSI 可能也是/dev/fb0,HDMI 可能是/dev/fb0或/dev/fb1。用ls /dev/fb*和cat /proc/fb可以确认。
提示:在正式写代码之前,先用
fbset命令查看 framebuffer 的分辨率、色深和时序参数。如果fbset显示的信息跟屏幕实际不符,说明内核的显示驱动配置有问题,需要先解决这个,再谈 LVGL 对接。
3. 从零搭建 LVGL 开发环境
3.1 交叉编译工具链的准备
SBC2332 通常是 ARM 架构,你需要一套对应的交叉编译工具链。如果板子厂商提供了 SDK,里面一般会包含工具链,直接用那个最稳妥。如果没有,可以用 Linaro 或 ARM 官方发布的 GNU 工具链,比如arm-linux-gnueabihf-前缀的版本。
安装好工具链后,验证一下:
arm-linux-gnueabihf-gcc --version能输出版本信息就说明工具链可用。接下来要确认目标系统的 C 库版本,是 glibc 还是 musl,版本号是多少。用arm-linux-gnueabihf-gcc -print-sysroot可以看到 sysroot 路径,里面包含了目标系统的头文件和库。如果厂商 SDK 里的 sysroot 跟板子上的实际系统不一致,编译出来的程序可能跑不起来,这一点要特别注意。
3.2 LVGL 源码获取与版本选择
LVGL 目前主流版本是 8.x 和 9.x。8.x 稳定成熟,资料多,社区支持好;9.x 引入了新的渲染架构和 API 变化,性能有提升,但部分驱动和示例还在完善中。对于 SBC2332 这种 Linux 平台,我建议先用 8.3 或 8.4 版本,稳定压倒一切。等 9.x 生态更成熟了再迁移。
从 GitHub 克隆源码:
git clone --branch release/v8.3 https://github.com/lvgl/lvgl.git如果你网络环境不方便直接克隆,也可以下载 release 压缩包。拿到源码后,重点看几个目录:src/是核心库,examples/是示例代码,demos/是演示程序,lv_drivers/是驱动库(8.x 里驱动是独立仓库,需要单独克隆)。
驱动库也要一并获取:
git clone --branch release/v8.3 https://github.com/lvgl/lv_drivers.git3.3 工程目录组织与编译系统搭建
LVGL 官方推荐用 CMake 或 Makefile 来组织工程。对于嵌入式项目,我习惯用一个清晰的目录结构:
project/ ├── lvgl/ # LVGL 核心库 ├── lv_drivers/ # LVGL 驱动库 ├── app/ # 自己的应用代码 │ ├── main.c │ ├── ui.c │ └── ui.h ├── lv_conf.h # LVGL 配置文件 └── Makefilelv_conf.h是 LVGL 的核心配置文件,它决定了启用哪些功能、缓冲区大小、颜色深度等。这个文件需要从lvgl/lv_conf_template.h复制过来,然后根据你的硬件修改。关键配置项包括:
LV_COLOR_DEPTH:设为 16 或 32,对应 RGB565 或 ARGB8888。framebuffer 通常是 16 位或 32 位,要跟屏幕一致。LV_MEM_SIZE:LVGL 内部内存池大小,默认 48KB,对于复杂界面建议调到 128KB 或 256KB。LV_HOR_RES_MAX和LV_VER_RES_MAX:屏幕分辨率。LV_USE_GPU:如果板子有 GPU 且驱动支持,可以开启硬件加速,但 framebuffer 方案通常用不上。
Makefile 里要指定交叉编译器、sysroot、头文件路径和链接库。一个简化的例子:
CC = arm-linux-gnueabihf-gcc CFLAGS = -I./lvgl -I./lv_drivers -I./app -O2 -Wall LDFLAGS = -lpthread -lm SRCS = $(wildcard ./lvgl/src/*.c) \ $(wildcard ./lvgl/src/**/*.c) \ $(wildcard ./lv_drivers/*.c) \ ./app/main.c ./app/ui.c OBJS = $(SRCS:.c=.o) target: $(OBJS) $(CC) -o lvgl_app $(OBJS) $(LDFLAGS) clean: rm -f $(OBJS) lvgl_app实际项目中,LVGL 源码文件很多,用wildcard递归匹配要注意路径深度。更稳妥的做法是用 CMake,LVGL 官方提供了CMakeLists.txt,直接add_subdirectory(lvgl)就行。
4. 显示与触摸驱动的对接细节
4.1 framebuffer 初始化的关键步骤
LVGL 的 framebuffer 驱动在lv_drivers/display/fbdev.c里。初始化流程大致是:打开/dev/fb0,用ioctl获取屏幕信息(分辨率、色深、行字节数),然后mmap显存,最后把缓冲区指针和刷新函数注册给 LVGL。
关键代码逻辑是这样的:
int fd = open("/dev/fb0", O_RDWR); struct fb_var_screeninfo vinfo; ioctl(fd, FBIOGET_VSCREENINFO, &vinfo); int screensize = vinfo.yres_virtual * vinfo.xres_virtual * vinfo.bits_per_pixel / 8; char *fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);这里有个容易踩的坑:vinfo.bits_per_pixel可能是 16、24 或 32。LVGL 的LV_COLOR_DEPTH必须跟它匹配。如果屏幕是 24 位色,而 LVGL 配的是 16 位,颜色会错乱。24 位色在 framebuffer 里通常按 32 位对齐存储,所以LV_COLOR_DEPTH要设成 32,然后在刷新函数里做转换。
另一个坑是vinfo.yres_virtual和vinfo.yres的区别。虚拟分辨率可能比实际分辨率大,用于双缓冲或页面翻转。mmap的大小要按虚拟分辨率算,但刷新时只刷实际分辨率区域。
4.2 双缓冲与页面翻转的取舍
framebuffer 支持双缓冲,也就是分配两块显存,一块显示,一块渲染,渲染完后切换。这样可以避免撕裂,提升流畅度。但双缓冲会占用双倍显存,而且需要驱动支持FBIOPAN_DISPLAY或FBIOPUT_VSCREENINFO来切换。
在 SBC2332 上,如果显存充足(比如 512MB 内存,显存分配了 16MB),可以开双缓冲。配置方法是把vinfo.yres_virtual设成vinfo.yres * 2,然后mmap两块区域。LVGL 的lv_disp_draw_buf_init里传入两个缓冲区指针,LVGL 会自动交替使用。
如果显存紧张,或者驱动不支持页面翻转,就用单缓冲加局部刷新。LVGL 支持“脏矩形”机制,只刷新界面上发生变化的区域,而不是全屏重绘。这在静态界面居多的 HMI 场景下,能大幅降低带宽占用。配置方法是把LV_DISP_DEF_REFR_PERIOD设成 30ms 左右,让 LVGL 合并多次小刷新。
注意:有些 SBC2332 的 framebuffer 驱动在
mmap后需要调用ioctl(fd, FBIOPAN_DISPLAY, &vinfo)才能让内容真正显示出来。如果屏幕一直黑屏但程序没报错,先检查这一步。
4.3 触摸输入设备的读取与校准
触摸屏在 Linux 下表现为/dev/input/eventX。用evtest工具可以确认哪个 event 设备对应触摸屏,以及它上报的事件类型。电阻触摸通常上报EV_ABS绝对坐标,电容触摸也是EV_ABS,但可能还上报EV_KEY表示按下和抬起。
LVGL 的lv_drivers/indev/evdev.c驱动会读取这些事件,转换成 LVGL 的输入坐标。初始化时要传入 event 设备路径:
lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = evdev_read; lv_indev_drv_register(&indev_drv);电阻触摸的坐标范围通常跟屏幕分辨率不一致,需要校准。校准的方法有两种:一是在驱动层做线性映射,把触摸坐标缩放到屏幕坐标;二是在 LVGL 层用lv_indev_set_calibration设置校准参数。我一般用第一种,因为更直接,而且可以在evdev.c里根据abs_min和abs_max自动计算缩放比例。
电容触摸一般不需要校准,但要注意坐标原点是否跟屏幕一致。有些屏幕的触摸坐标系是旋转过的,需要在驱动里做坐标变换。
4.4 输入事件的线程安全处理
LVGL 本身不是线程安全的。如果你在单独的线程里读取触摸事件,然后直接调用 LVGL 的 API,可能会出问题。正确的做法是用一个互斥锁保护 LVGL 的调用,或者用 LVGL 的lv_timer_handler在主循环里统一处理输入和刷新。
我的习惯是:主线程跑一个循环,每隔几毫秒调用一次lv_timer_handler(),它内部会处理输入设备的读取、界面刷新和动画更新。触摸事件的读取在read_cb回调里完成,这个回调由lv_timer_handler触发,所以天然在主线程里,不需要额外加锁。
如果非要在其他线程里更新界面(比如串口收到数据后要更新显示),必须用lv_async_call或者加锁。lv_async_call会把函数调用推迟到下一次lv_timer_handler执行,保证线程安全。
5. 用 LVGL 构建 HMI 界面的实战思路
5.1 界面布局与控件选型
HMI 界面的特点是信息密度高、操作直接、反馈及时。典型的布局是顶部状态栏、中间主操作区、底部功能按钮。LVGL 的lv_obj可以作为容器,里面放lv_btn、lv_label、lv_slider、lv_chart等控件。
状态栏通常显示时间、通信状态、报警图标。时间可以用lv_label加定时器更新,通信状态可以用不同颜色的圆点表示,报警图标可以用 LVGL 内置的符号字体。主操作区根据具体应用来定,如果是控制面板,就放按钮和滑块;如果是监控界面,就放图表和数值显示。
LVGL 的布局系统支持 Flex 和 Grid,类似 CSS。对于 HMI 界面,Flex 布局很实用,可以自动排列按钮,适应不同分辨率。比如一排按钮用LV_FLEX_FLOW_ROW,设置LV_FLEX_ALIGN_SPACE_EVENLY,就能均匀分布。
5.2 中文字体与图标资源的处理
LVGL 默认只带 ASCII 字体,中文需要自己生成。官方提供了字体转换工具,可以把 TTF 字体转成 C 数组。但完整中文字库太大,几兆到十几兆,嵌入式设备吃不消。实际项目中,只提取界面用到的汉字,生成一个精简字库。
具体做法是:把所有界面文字整理成一个文本文件,用 LVGL 的字体转换工具(在线版或本地 Python 脚本)生成只包含这些字符的字体文件。一个典型的 HMI 界面,用到的汉字可能就几百个,生成的字体文件只有几十 KB。
图标方面,LVGL 内置了一套符号字体,包含常用图标如设置、返回、警告、电池等。如果不够用,可以把 PNG 或 SVG 图标转成 C 数组,用lv_img显示。注意图片要转成 LVGL 支持的格式,并且考虑色深和内存占用。
5.3 界面刷新性能的实测调优
LVGL 在 SBC2332 上的刷新性能,主要受三个因素影响:缓冲区大小、刷新策略、界面复杂度。
缓冲区方面,如果内存允许,用全屏双缓冲最流畅。800x480 的 16 位色屏幕,单缓冲 768KB,双缓冲 1.5MB。如果内存紧张,可以用 1/10 屏幕大小的缓冲区,LVGL 会分块刷新,但可能会有轻微闪烁。
刷新策略上,LV_DISP_DEF_REFR_PERIOD控制刷新周期,默认 30ms,也就是约 33fps。对于 HMI 界面,这个值够了。如果界面动画多,可以调到 16ms,但 CPU 占用会上升。
界面复杂度方面,减少不必要的透明效果、阴影、渐变,这些都会增加渲染时间。LVGL 的样式系统很灵活,但每多一层样式,渲染就多一步计算。实测下来,一个包含 50 个控件的界面,在 800x480 分辨率下,单缓冲刷新一帧大约 8ms,双缓冲大约 5ms,跑 30fps 毫无压力。
5.4 与下位机通信的数据更新机制
HMI 界面往往要显示下位机的实时数据,比如温度、压力、转速。这些数据通过串口、CAN 或网络传上来,然后更新到界面上。关键问题是:通信线程和 UI 线程怎么协作。
我的做法是:通信线程收到数据后,把数据写入一个共享的环形缓冲区,然后通过lv_async_call通知 UI 线程更新。UI 线程在lv_timer_handler里处理这个异步调用,读取缓冲区数据,更新对应的lv_label或lv_chart。这样既保证了线程安全,又不会阻塞通信。
对于实时曲线,LVGL 的lv_chart控件支持动态添加数据点。但要注意,数据点太多会拖慢渲染。一般保留最近 100 到 200 个点就够了,超出后移除最旧的点。更新频率也不要太高,每秒 10 次左右视觉上就很流畅了。
6. 踩过的坑与排查实录
6.1 屏幕黑屏但程序正常运行的排查链路
第一次在 SBC2332 上跑 LVGL,程序编译通过,运行也没报错,但屏幕就是黑的。排查过程是这样的:
第一步,确认 framebuffer 设备存在。ls /dev/fb*显示/dev/fb0,cat /proc/fb显示驱动名称,说明内核识别了屏幕。
第二步,用fbset查看参数。分辨率 800x480,色深 16,跟屏幕规格一致。
第三步,写了一个最简单的测试程序,直接往 framebuffer 里填红色,屏幕亮了。说明 framebuffer 本身没问题。
第四步,回到 LVGL,检查lv_conf.h里的LV_COLOR_DEPTH,发现设的是 32,但屏幕是 16 位。改成 16 后,屏幕正常显示。
这个坑的根因是:LVGL 按 32 位色渲染,写入 framebuffer 时每个像素占 4 字节,但屏幕按 16 位解析,每个像素只读 2 字节,导致图像错位和颜色错乱。看起来像黑屏,其实是数据错位后刚好显示成黑色。
6.2 触摸坐标偏移与抖动问题的解决
触摸屏能用,但点击位置总是偏,而且手指按住不动时坐标会轻微抖动。偏移的原因是触摸坐标范围跟屏幕分辨率不匹配。用evtest查看触摸事件,发现 X 轴范围是 0 到 4095,Y 轴是 0 到 4095,而屏幕是 800x480。需要做线性映射:
x_screen = x_touch * 800 / 4096; y_screen = y_touch * 480 / 4096;抖动的原因是电阻触摸的 ADC 噪声。解决方法是在驱动层做滑动平均滤波,连续读 5 个点取平均值。或者设置一个死区,坐标变化小于阈值时不更新。
6.3 内存泄漏与长时间运行稳定性
LVGL 应用跑几个小时没问题,但跑一两天后界面变卡,甚至崩溃。用top查看内存占用,发现逐渐上升。这是典型的内存泄漏。
排查发现,每次更新图表数据时,都创建了新的lv_chart_series,但没有删除旧的。LVGL 的对象需要手动删除,或者用lv_obj_clean清理容器。另外,动态创建的样式如果没有lv_style_free,也会泄漏。
修复方法是:图表数据更新时复用已有的 series,只更新数据点,不创建新对象。样式在初始化时创建一次,全局复用。修复后,连续跑一周,内存占用稳定在 15MB 左右,没有增长。
6.4 交叉编译时的库依赖陷阱
交叉编译时遇到undefined reference to 'pthread_create',明明加了-lpthread。原因是链接顺序问题,-lpthread要放在源文件之后。Makefile 里LDFLAGS的位置很关键,放在OBJS后面才行。
另一个坑是 sysroot 里的库版本跟板子上不一致。编译时链接的是 SDK 里的 libc,但板子上是另一个版本,运行时报GLIBC_2.29 not found。解决方法是确保交叉工具链的 sysroot 跟板子上的根文件系统一致,或者用静态链接。
7. 性能与资源占用的实测数据
7.1 不同分辨率下的帧率表现
在 SBC2332(Cortex-A7 双核 1GHz,512MB 内存)上实测,LVGL 的帧率表现如下:
| 分辨率 | 色深 | 缓冲方式 | 平均帧率 | CPU 占用 |
|---|---|---|---|---|
| 480x272 | 16 | 单缓冲 | 60fps | 15% |
| 800x480 | 16 | 单缓冲 | 45fps | 25% |
| 800x480 | 16 | 双缓冲 | 60fps | 30% |
| 1024x600 | 16 | 单缓冲 | 30fps | 40% |
| 1024x600 | 16 | 双缓冲 | 45fps | 50% |
从数据看,800x480 是性价比最高的分辨率,单缓冲就能到 45fps,双缓冲稳 60fps。1024x600 单缓冲只有 30fps,如果界面动画多,建议上双缓冲或者降低刷新率。
7.2 内存占用的构成分析
一个典型的 LVGL HMI 应用,内存占用分布如下:
- LVGL 核心库和对象:约 3MB
- 显示缓冲区(800x480x2 单缓冲):768KB
- 字体和图片资源:约 2MB
- 应用数据结构和缓冲区:约 1MB
- 系统库和运行时:约 5MB
总计约 12MB。如果开双缓冲,增加 768KB。如果界面控件多,对象内存会增加,但一般不超过 5MB。对于 512MB 内存的 SBC2332,这个占用非常轻松。
7.3 启动时间的优化空间
从按下电源到界面可交互,时间构成大致是:内核启动 2 秒,根文件系统挂载 1 秒,应用启动 0.5 秒,LVGL 初始化 0.2 秒,界面渲染 0.3 秒。总计约 4 秒。
优化空间主要在应用启动和界面渲染。把 LVGL 静态链接进应用,减少动态库加载时间。界面初始化时只创建可见控件,不可见的延迟创建。字体和图片资源用二进制嵌入,避免文件 IO。实测下来,优化后可以压到 3 秒以内。
8. 从能跑到好用:几个提升体验的细节
8.1 开机自启与看门狗配合
产品化的时候,应用要开机自启。用 systemd 写一个 service 文件,设置Restart=always,应用崩溃后自动重启。同时配合硬件看门狗,应用定期喂狗,如果卡死,看门狗复位系统。
service 文件示例:
[Unit] Description=LVGL HMI Application After=multi-user.target [Service] Type=simple ExecStart=/opt/hmi/lvgl_app Restart=always RestartSec=1 [Install] WantedBy=multi-user.target看门狗方面,如果 SBC2332 有硬件看门狗,在应用里定期ioctl喂狗。如果没有,可以用软件看门狗,监控应用的心跳。
8.2 界面防烧屏与背光控制
工业 HMI 经常长时间显示同一界面,LCD 可能烧屏。LVGL 支持屏幕休眠,一段时间无操作后关闭背光或显示黑屏。背光控制通常通过 PWM 或 GPIO,写/sys/class/backlight或/sys/class/gpio。
实现方法是:在lv_timer_handler里记录最后操作时间,超过设定值后调用背光关闭函数。触摸事件触发时重新点亮。这样既省电又保护屏幕。
8.3 日志与远程调试通道
产品部署后,出问题需要排查。在应用里加日志系统,把关键操作和错误写到文件或串口。LVGL 本身有日志功能,在lv_conf.h里开启LV_USE_LOG,设置日志级别和输出函数。
远程调试方面,可以开一个 telnet 或 SSH 通道,但要注意安全。更轻量的做法是用串口输出日志,现场人员接上串口线就能看到。如果板子有网络,也可以用 UDP 把日志发到指定端口,开发人员在电脑上接收。
8.4 多语言与主题切换的实现
如果产品要出口,界面需要多语言。LVGL 没有内置的多语言框架,但可以自己实现。把所有界面文字定义成字符串数组,根据语言设置选择对应的索引。切换语言时,遍历所有lv_label,更新文本。
主题切换类似,定义几套样式,切换时把新样式应用到控件上。LVGL 的样式系统支持继承和覆盖,可以定义一个基础样式,然后派生出不同主题的变体。
9. 一些个人经验与后续扩展方向
这套 SBC2332 加 LVGL 的方案,我在三个项目里实际用过,最长的已经连续运行了一年多,稳定性没问题。最大的体会是:LVGL 的文档虽然不算特别完善,但源码可读性很好,遇到问题直接看源码比查文档快。另外,社区很活跃,GitHub 上的 issue 和讨论区经常能找到答案。
如果后续要扩展,有几个方向可以考虑。一是上 DRM/KMS,利用硬件加速提升高分辨率下的流畅度。二是集成更复杂的图表库,比如实时波形、仪表盘,LVGL 的lv_chart比较基础,复杂图表需要自己画。三是加网络配置界面,通过触摸屏设置 IP、端口等参数,省去串口配置的麻烦。
还有一个小心得:LVGL 的模拟器在 PC 上跑,开发效率极高。界面布局、样式调整、逻辑验证都可以在 PC 上完成,然后再交叉编译到板子上。模拟器用 SDL 驱动,跟 framebuffer 的行为基本一致,省去了反复烧录的时间。我现在的流程是:PC 上模拟器开发调试,功能稳定后交叉编译到 SBC2332 上做最终验证,效率比直接在板子上调试高好几倍。