
1. 为什么要在 SBC2332 上跑 LVGL 而不是 Qt1.1 从一块 128x160 的屏说起手里拿到 SBC2332 这块板子的时候我第一反应是翻它的显示接口。SBC2332 这类单板计算机通常带 RGB 或 SPI 接口的 LCD 排针板载内存和存储都比较克制跑完整 Linux 桌面发行版加 Qt 那套东西开机慢、内存吃紧做个小设备的人机界面实在不划算。我试过在类似规格的板子上跑 Qt光一个 QApplication 初始化就要占掉几十兆内存界面还没画出来系统已经喘上了。LVGL 就完全是另一个路子。它是一个用 C 写的开源嵌入式图形库整个核心可以裁剪到几十 KB 的 ROM 占用运行时内存也能压到十几 KB 起步。它不依赖操作系统裸机就能跑也能挂在 FreeRTOS 或者嵌入式 Linux 上。对于 SBC2332 这种定位低成本 HMI 的场景LVGL 几乎是默认答案。这篇文章我想聊的是怎么在 SBC2332 上从零把 LVGL 跑起来驱动一块 LCD做出一个能实际用的人机界面。包括显示驱动怎么接、LVGL 怎么移植、输入设备怎么配、界面怎么组织、性能怎么调。适合正在做嵌入式 HMI 的同行也适合刚接触 LVGL 想找个完整落地案例的朋友。1.2 LVGL 和 Qt 在嵌入式 HMI 里的取舍先把选型这件事说透因为很多人卡在第一步。嵌入式 HMI 常见的图形方案就那么几个Qt、LVGL、emWin、TouchGFX还有各家芯片厂自带的 GUI 库。SBC2332 这类板子通常跑的是精简 Linux 或者 RTOS选型主要看三件事资源占用、开发效率、生态支持。Qt 的优势是控件丰富、有 Designer 可视化工具、C 生态成熟缺点是重。一个最小 Qt 应用在嵌入式 Linux 上跑起来内存占用轻松上百兆启动时间按秒算。如果你的 SBC2332 只有 128MB 或 256MB 内存还要同时跑业务逻辑Qt 会把预算吃光。LVGL 的优势正好反过来轻、快、纯 C、可裁剪。它的控件库其实相当完整按钮、标签、列表、图表、滑块、键盘、Tab 控件都有还内置了一批图标。缺点是它没有官方的可视化设计器虽然有第三方工具界面基本靠代码搭复杂界面写起来代码量不小。我个人的判断标准很简单如果屏幕小于 7 寸、分辨率在 800x480 以下、内存预算紧张、启动速度要求高选 LVGL如果屏幕大、界面复杂、团队是 C 背景、不在乎资源选 Qt。SBC2332 配 1.8 寸到 4 寸的 LCD妥妥的 LVGL 场景。提示LVGL 的版本迭代比较快v8 和 v9 的 API 有不小差异。新项目建议直接上 v9老项目迁移要留意 lv_obj 相关接口的变化。本文以 v9 为主线关键处会标注 v8 的差异。1.3 这套方案最终能做成什么样说个具体的预期免得大家跑偏。在 SBC2332 上用 SPI 接口的 1.8 寸 TFT128x160 或 240x320LVGL 跑起来后界面刷新率做到 30fps 以上是没问题的如果开了双缓冲和 DMA 传输60fps 也能摸到。内存占用方面LVGL 核心加几个常用控件静态占用大概 40 到 80KB帧缓冲按分辨率算128x160 的 16 位色单缓冲只要 40KB。这个资源量级意味着什么意味着你可以在 SBC2332 上同时跑 LVGL 界面、串口通信、传感器采集、数据存储整个系统还能留出大量余量。这就是低成本 HMI 该有的样子——不是把 PC 那套搬过来而是用最小的资源做出够用的交互。2. 硬件链路与显示驱动的底层逻辑2.1 SBC2332 的显示接口怎么选SBC2332 一般会引出几类显示接口SPI、RGB也叫 MCU 接口或 8080 并口、MIPI DSI。选哪个直接决定后面的驱动写法。SPI 接口最省引脚一根时钟、一根数据、一根片选加个 DC 和复位五六根线就能点亮一块屏。缺点是带宽有限128x160 分辨率刷新还行上到 480x320 就会感觉刷屏有拖影。RGB 并口带宽大得多适合大屏但占引脚多16 位色要 16 根数据线加若干控制线。MIPI DSI 是高速差分带宽最大但驱动复杂一般 SoC 原生支持才好用。我的建议是小屏3.5 寸以下优先 SPI接线简单、调试容易中大屏4 寸以上用 RGB 或 MIPI。SBC2332 做低成本 HMI多数人配的是 1.8 到 2.8 寸的 SPI 屏所以下面以 SPI 为主线讲。2.2 SPI 屏的初始化序列为什么不能照抄拿到一块 SPI TFT卖家通常会给你一份初始化代码里面是一长串寄存器写入。很多人直接复制粘贴能亮就行。但这里有个坑不同批次的屏初始化序列可能不一样尤其是伽马校正和电源相关的寄存器。我踩过的坑是同一型号的屏第一批用卖家的初始化序列正常第二批换了个供应商颜色偏得厉害红色发暗。后来对比 datasheet 才发现伽马校正寄存器的值不一样。所以初始化序列一定要对着屏的 datasheet 核对尤其是 0xE0 到 0xE1 这类伽马寄存器还有 0xB1 到 0xB6 的电源控制寄存器。初始化序列里几个关键寄存器的作用寄存器作用常见取值说明0x36内存访问控制控制扫描方向、RGB/BGR 顺序0x3A像素格式0x05 是 16 位色0x06 是 18 位色0xB1帧率控制影响刷新率取值不当会闪0xC0电源控制 1设置 GVDD 电压0xE0/0xE1正/负伽马校正影响色彩还原必须按屏调0x36 这个寄存器特别重要它决定了你画出来的图像方向对不对、颜色通道顺序对不对。如果显示出来是镜像的或者红蓝反了先查这个寄存器。2.3 帧缓冲与刷屏机制LVGL 的工作方式是它在内存里维护一块或多块帧缓冲draw buffer界面变化时先在缓冲里画好再通过显示驱动把缓冲内容推到屏上。这个“推”的过程就是刷屏。刷屏方式有两种全屏刷新和局部刷新。全屏刷新每次把整屏数据推过去简单但费带宽。局部刷新只推变化区域省带宽但逻辑复杂。LVGL 默认支持局部刷新它会告诉你哪块区域变了你只需要把这块区域的数据推过去。对于 SPI 屏局部刷新尤其重要因为 SPI 带宽本来就紧张。128x160 的 16 位色全屏是 40KBSPI 跑 40MHz 的话理论上一秒能推 1000 次全屏但实际受限于命令开销和 CPU 搬运能到 30fps 就不错了。局部刷新能把大部分帧的数据量降到几 KB帧率立刻上去。帧缓冲的大小也有讲究。LVGL 允许你只分配屏幕的十分之一甚至更小作为缓冲它分多次把整屏画完。缓冲越小内存越省但刷新次数越多。我一般建议缓冲至少是屏幕的十分之一128x160 的话就是 128x16 像素16 位色下 4KB这个量级很舒服。注意如果用了 DMA 传输帧缓冲必须是 DMA 能访问的内存区域。有些 SoC 的 DMA 不能访问某些内存段分配缓冲时要留意否则会出现数据传输出错或者干脆传不动。3. LVGL 移植到 SBC2332 的完整流程3.1 移植前要准备什么移植 LVGL 之前先把这几样东西备齐LVGL 源码从官方仓库拉建议用 v9 的稳定 tag、一个能编译 SBC2332 的交叉工具链、一个能点亮的 LCD 驱动、一个能用的定时器给 LVGL 提供心跳。LVGL 本身不依赖任何硬件它需要你提供三样东西显示接口、输入接口、心跳。显示接口负责把画面推出去输入接口负责把触摸或按键事件喂进来心跳负责驱动 LVGL 的内部计时和动画。这三样对接好LVGL 就跑起来了。源码目录结构大致是这样src是核心src/core是对象系统src/widgets是控件src/draw是绘图src/display是显示相关src/indev是输入设备。移植时你主要改的是lv_conf.h配置文件和对接层。3.2 lv_conf.h 里必须改的几个配置lv_conf.h是 LVGL 的总配置从lv_conf_template.h复制过来改。里面几百个宏但真正影响移植的没几个。首先是颜色深度LV_COLOR_DEPTH。SPI 屏大多是 16 位色RGB565所以设成 16。如果你的屏是 24 位色设成 32但内存占用会翻倍。然后是内存相关。LV_MEM_SIZE是 LVGL 自己管理的内存池大小默认 48KB 左右。如果界面控件多要往上加我一般设 64KB 到 128KB。LV_MEM_CUSTOM如果设成 1LVGL 就用系统的 malloc/free适合有完整堆管理的系统设成 0 就用 LVGL 自带的分配器适合裸机。刷新相关LV_DEF_REFR_PERIOD是默认刷新周期单位毫秒默认 30ms对应约 33fps。LV_DPI_DEF是默认 DPI影响字体和控件尺寸的默认值小屏可以设小一点。控件裁剪LVGL 允许你关掉不用的控件来省空间。比如不用图表就关掉LV_USE_CHART不用键盘就关掉LV_USE_KEYBOARD。这个在资源紧张的板子上很有用能省下不少 ROM。/* lv_conf.h 关键配置示例 */ #define LV_COLOR_DEPTH 16 #define LV_MEM_SIZE (64U * 1024U) #define LV_MEM_CUSTOM 0 #define LV_DEF_REFR_PERIOD 30 #define LV_DPI_DEF 130 #define LV_USE_LOG 1 #define LV_USE_ASSERT_NULL 13.3 显示接口对接flush_cb 怎么写LVGL 通过一个回调函数把画面交给你的驱动这个回调叫flush_cb。它的原型是void (*lv_display_flush_cb_t)(lv_display_t * disp, const lv_area_t * area, uint8_t * px_map)。参数里area是这次要刷新的区域px_map是这块区域的像素数据。你要做的就是把px_map里的数据写到屏的对应区域。对于 SPI 屏流程是设置列地址范围、设置行地址范围、发写内存命令、把数据推过去。static void disp_flush(lv_display_t * disp, const lv_area_t * area, uint8_t * px_map) { int32_t x1 area-x1; int32_t y1 area-y1; int32_t x2 area-x2; int32_t y2 area-y2; /* 设置显示窗口 */ lcd_set_window(x1, y1, x2, y2); /* 发送像素数据 */ uint32_t px_count (x2 - x1 1) * (y2 - y1 1); lcd_send_pixels((uint16_t *)px_map, px_count); /* 通知 LVGL 刷新完成 */ lv_display_flush_ready(disp); }这里有个关键点lv_display_flush_ready必须在数据传输真正完成后调用。如果你用的是阻塞式 SPI 传输直接在函数末尾调用就行。如果用了 DMA要在 DMA 完成中断里调用否则 LVGL 会以为刷新还没结束界面就卡住了。3.4 输入设备对接触摸和按键输入设备对接比显示简单。LVGL 支持多种输入设备触摸屏、鼠标、键盘、编码器、按键。SBC2332 做 HMI最常见的是电阻触摸屏或电容触摸屏。触摸屏对接的核心是提供一个读点函数LVGL 会周期性调用它来获取触摸状态。函数里你要读触摸控制器返回坐标和按下状态。static void touch_read(lv_indev_t * indev, lv_indev_data_t * data) { uint16_t x, y; bool pressed touch_get_xy(x, y); if (pressed) { >/* 创建一个 Flex 布局的容器 */ lv_obj_t * cont lv_obj_create(lv_screen_active()); lv_obj_set_size(cont, 200, 150); lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); /* 往容器里加控件自动纵向排列 */ lv_obj_t * btn1 lv_button_create(cont); lv_obj_t * btn2 lv_button_create(cont);样式系统是 LVGL 比较有特色的地方。一个样式可以定义背景色、文字色、边框、阴影、内边距等一堆属性然后挂到控件上。样式可以继承子控件默认继承父控件的样式所以你在容器上设个字体里面所有控件都跟着变。我一般会定义几套基础样式主色调、次色调、警告色、禁用态然后在界面里复用。这样改配色的时候只改一处全局生效。4.2 中文字体怎么处理LVGL 默认只带 ASCII 字体显示中文要自己生成字体文件。官方有个在线字体转换工具可以把 TTF 转成 LVGL 的 C 数组。但中文字库动辄几 MB全字库塞进 SBC2332 不现实。我的做法是按需裁剪。先统计界面上要用到的所有汉字去重后可能就几百个用工具只生成这几百个字的字库体积能压到几十 KB。如果界面文字会动态变化比如显示传感器名称那就把可能出现的字都列进去。字体大小也要控制。小屏上 12 到 16 像素的字就够看了再大就占地方。LVGL 支持多字体标题用大字体正文用小字体按需加载。/* 声明外部字体 */ LV_FONT_DECLARE(my_font_16); LV_FONT_DECLARE(my_font_12); /* 应用字体 */ lv_obj_set_style_text_font(label, my_font_16, 0);有个细节LVGL 的字体是位图字体不是矢量字体所以缩放会糊。要不同大小就生成不同大小的字库别指望一个字体缩放用。4.3 刷新率上不去的几个原因界面跑起来后如果感觉卡先别急着优化代码按这个顺序排查。第一看刷屏方式。如果每次都是全屏刷新那带宽肯定不够。检查flush_cb里传进来的area是不是整屏如果是说明 LVGL 没启用局部刷新或者你的缓冲设得太小导致它每次都重画整屏。第二看 SPI 时钟。SPI 时钟太低是刷屏慢的头号原因。很多初始化代码里 SPI 只跑 10MHz 甚至更低把它提到 40MHz 或 50MHz看屏的支持帧率立刻翻几倍。但要注意SPI 时钟太高可能导致数据出错要实测稳定性。第三看有没有用 DMA。CPU 搬运 SPI 数据是纯耗时操作用 DMA 可以让 CPU 去干别的。开了 DMA 之后刷屏和业务逻辑能并行体感流畅度提升明显。第四看有没有不必要的重绘。LVGL 会在对象属性变化时标记重绘区域如果你频繁改一个不影响显示的属性或者在一个循环里反复设同一个值会造成多余重绘。用 LVGL 的日志功能能看到重绘区域排查起来方便。现象可能原因排查方向整体卡顿SPI 时钟低提高 SPI 时钟局部拖影未用局部刷新检查 flush 区域刷新时 CPU 占用高未用 DMA启用 DMA 传输偶发花屏SPI 时钟过高降低时钟或加延时界面元素错位布局未生效检查 Flex/Grid 配置4.4 内存不够时的裁剪策略SBC2332 的内存有限LVGL 跑起来后如果发现内存吃紧可以从这几个地方省。关掉不用的控件。LVGL 的每个控件都会占 ROM 和 RAMlv_conf.h里把不用的关掉能省不少。比如不用图表、不用日历、不用颜色选择器全关。减小内存池。LV_MEM_SIZE如果设得太大会浪费设得太小控件创建会失败。可以先设小一点跑起来看日志有没有分配失败再往上加。降低颜色深度。16 位色够用就别用 32 位内存直接省一半。减少帧缓冲。缓冲从屏幕的十分之一降到二十分之一内存省一半代价是刷新次数增加。如果 SPI 带宽够这个交换是划算的。用外部 RAM。如果 SBC2332 有外部 SDRAM把帧缓冲放到外部 RAM内部 RAM 留给业务逻辑。但要注意外部 RAM 的访问速度太慢的话刷屏会受影响。注意LVGL 的内存池是在编译时确定的静态数组不是动态增长的。如果运行时发现内存不够只能改配置重新编译不能在线扩容。所以前期评估内存需求时要留足余量。5. 常见问题排查与避坑清单5.1 屏幕点不亮怎么办屏幕点不亮是最常见的问题排查要按信号链一步步来。先查供电。屏的 VCC 和背光供电是不是正常背光有没有限流电阻电流够不够。很多屏点不亮其实是背光没开仔细看能看到很暗的图像。再查复位。屏的复位时序对不对复位脉冲宽度够不够。有些屏要求复位后延时一段时间才能发初始化命令延时不够会初始化失败。然后查 SPI 通信。用逻辑分析仪抓 SPI 波形看时钟、数据、片选、DC 信号是否正常。DC 信号特别关键它区分命令和数据如果 DC 接错或时序不对屏收到的全是乱码。最后查初始化序列。对照 datasheet 逐条核对尤其是电源和伽马相关的寄存器。有些屏需要先发一个软件复位命令延时后再发初始化序列。5.2 显示颜色不对怎么调颜色不对有几种表现红蓝反了、整体偏色、颜色发暗。红蓝反了通常是 0x36 寄存器的 RGB/BGR 位设反了改一下就好。整体偏色多半是伽马校正的问题对着 datasheet 调 0xE0 和 0xE1。颜色发暗可能是电源电压不够调 0xC0 和 0xC1 的 GVDD 值。还有一种情况是像素格式设错了。0x3A 寄存器如果设成 18 位色但实际发的是 16 位数据颜色就会错乱。确认屏支持哪种格式两边对齐。5.3 触摸不准怎么校准电阻触摸屏的校准是绕不开的。原理是触摸控制器输出的 ADC 值和屏幕像素坐标之间是线性关系但有个偏移和缩放。采两个点就能算出缩放采四个点能同时算出偏移和旋转。校准流程在屏幕四个角显示十字光标让用户依次点击记录每个点的 ADC 值然后用最小二乘法算出变换矩阵。这个矩阵存到 Flash 里下次开机直接加载。电容触摸屏一般不需要校准因为它的坐标是直接输出的。但如果贴了厚保护膜可能会有偏移这时候用软件做个偏移补偿就行。5.4 LVGL 常见报错速查报错/现象原因解决界面不刷新flush_ready 未调用检查 flush_cb 末尾控件创建失败内存池不足增大 LV_MEM_SIZE触摸无响应输入设备未注册检查 indev 创建字体显示方块字库缺字重新生成字库动画卡顿刷新周期太长减小 LV_DEF_REFR_PERIOD编译报错找不到头文件包含路径未配检查 include 路径5.5 几个我踩过的坑第一个坑SPI 的 DC 信号用普通 GPIO 控制但 GPIO 翻转速度跟不上 SPI 时钟导致命令和数据错位。解决办法是用硬件 SPI 的 DC 功能或者降低 SPI 时钟。第二个坑帧缓冲用了局部变量函数返回后内存被回收LVGL 画出来的全是乱码。帧缓冲必须是全局或静态的生命周期要覆盖整个运行期。第三个坑在中断里调用 LVGL 的 API。LVGL 不是线程安全的中断里改控件会导致对象树损坏。正确做法是在中断里设个标志主循环里再处理。第四个坑忘了开背光。调试了半天以为屏坏了结果是背光引脚没拉高。这种低级错误在赶工的时候特别容易犯建议做个检查清单。6. 从能跑到好用几个提升体验的细节6.1 开机动画和启动优化SBC2332 从通电到界面出现中间要经过 bootloader、内核、应用初始化。如果这段时间屏幕是黑的用户会以为设备坏了。加个开机动画能显著提升体验。最简单的做法是在应用启动时先显示一张 logo 图等 LVGL 初始化完成再切到主界面。logo 图可以放在 Flash 里直接推给屏。如果 bootloader 支持显示可以在更早的阶段就点亮屏幕。启动优化方面LVGL 的初始化其实很快主要耗时在字体加载和界面构建。如果界面复杂可以考虑把界面构建放到后台线程先显示个加载提示。6.2 界面响应速度的体感优化同样的刷新率为什么有的界面感觉流畅有的感觉卡差别在响应延迟。按钮按下后如果视觉反馈延迟超过 100ms用户就会觉得卡。LVGL 的按钮默认有按下态但如果你在按下事件里做了耗时操作反馈就会延迟。正确做法是按下事件里只改视觉状态耗时操作放到释放事件或者异步执行。动画也是同理。LVGL 的动画是帧驱动的如果刷新率不够动画会跳帧。小屏上动画别做太复杂简单的位移和透明度变化就够了。6.3 长期运行的稳定性HMI 设备通常要 7x24 运行稳定性比什么都重要。LVGL 本身很稳但外围代码容易出问题。内存泄漏是头号杀手。LVGL 的对象如果创建了不删除内存会慢慢耗尽。界面切换时旧界面的对象要删干净。用lv_obj_delete删父对象子对象会自动删。看门狗要开。LVGL 的主循环如果卡住看门狗能复位系统。但要注意刷屏是耗时操作看门狗喂狗要放在刷屏之后别在刷屏中间喂。日志要留。LVGL 有日志功能能输出警告和错误。长期运行前把日志级别调低只留错误避免日志刷屏影响性能。出问题时再调高。6.4 后续可以扩展的方向这套方案跑通之后能扩展的地方很多。比如加个文件系统把界面配置和字库存到外部 Flash方便升级。比如加个网络模块做远程监控。比如加个数据记录功能把传感器数据存到 SD 卡。LVGL 本身也在进化v9 之后对多显示器和多语言的支持更好了。如果产品要出海多语言切换用 LVGL 的翻译功能能省不少事。我个人在实际操作中的体会是嵌入式 HMI 的难点不在图形库本身而在整个链路的打通。从硬件接线到驱动调试从 LVGL 移植到界面优化每一步都有坑。但只要按信号链一步步排查把每个环节都验证过最后跑起来的那一刻成就感是很足的。SBC2332 加 LVGL 这套组合成本低、资源省、可控性强做小设备的人机界面值得一试。