拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LVGL移植实战指南:从底层驱动到FreeRTOS性能调优

LVGL移植实战指南:从底层驱动到FreeRTOS性能调优

如果你是从正点原子或其他开发板厂商的例程开始接触LVGL,你多半会觉得这库也就那样:把源码包丢进工程,编译下载,一个带仪表盘、带按钮和滑动条的demo就亮在屏幕上了。但等你关掉例程、打算把LVGL移植到自己画的板子上,或者换一块尺寸、换一颗触摸芯片,你才会意识到,例程帮你做的那些事才是真正的体力活。这篇文章就把这些体力活拆开,用一块正点原子常见的板子做参照,从底层依赖、工程搭建、显示驱动、输入设备,一路聊到FreeRTOS下的任务配合和性能调优。适合刚买板子想跑LVGL的玩家,也适合做产品原型时被白屏和花屏折磨的工程师。

1. 移植的本质:LVGL想要的东西其实只有四个

1.1 别被源码规模吓到:LVGL与硬件之间只隔一条窄缝

LVGL的源码解压出来动辄几十个源文件,头文件里还满是宏定义,第一眼确实吓人。但你要真去翻一遍,会发现它整个库几乎不依赖任何硬件。它不直接操作寄存器,不调用SPI传输函数,也不知道你的屏幕是RGB接口还是MCU并口。它内部做的是纯粹的上层工作:管理控件树、计算布局、执行绘图算法、调度动画和事件。

我用一个类比说清楚这件事:LVGL像一家装修公司的设计师,他只负责画图纸、估算材料、安排施工顺序,但真正往墙上抹泥、铺瓷砖、接水电的工人,是板子BSP里的人。设计师只需要和几个固定的“工头”对接,这个工头就是移植层。所以移植LVGL这件事,本质上不是去改LVGL源码,而是把这家“装修公司”需要的外部服务备齐、接好。

LVGL对外部服务的要求,收敛到最后就四件事:一份正确的配置头文件、一个显示刷新回调、一个输入设备回调、一个持续递增的时间基准。搞懂这四件事怎么落地,移植就完成了八成。剩下两成是内存和任务调度问题,属于更高一层的系统集成。

1.2 四个接口的职责和对应的配置项

第一个是配置头文件lv_conf.h。这相当于告诉LVGL“你这套房子的面积多大、装修标准多高”。你在这个文件里指定颜色深度是16位还是32位、内部内存池开多大、Enable哪些组件、默认字体和主题样式是什么。这个文件不写对,后面显示出来大概率是花屏或者颜色偏得离谱。

第二个是显示刷新回调。LVGL内部有一个绘图缓冲(draw buffer),它把控件画在这个内存区域里,然后通过flush回调把这块数据交给你的屏幕驱动。你的任务就是把这块数据搬到你屏幕对应的显存或者发送到SPI总线上,搬完以后必须调用lv_display_flush_ready()通知LVGL。这一步不做,整个渲染状态机就会卡死在等待里,屏幕上永远白着。

第三个是输入设备回调。LVGL不关心你是触摸屏、鼠标还是旋转编码器,它只做一个动作:周期性地调用你注册的读取函数,从里面拿到坐标或者按键状态,然后分发给当前的焦点控件。你只需要把触摸芯片读到的坐标灌进一个lv_indev_data_t结构体就行。

第四个是时基。LVGL的动画、长按检测、闪烁光标都依赖一个不断递增的毫秒数。裸机下你可以在SysTick中断里调lv_tick_inc(1),在FreeRTOS下就得考虑用独立定时器或者专门的tick接口,否则动画会一卡一卡的。

1.3 为什么官方例程能跑,自己建工程就不行

你在正点原子板卡上跑官方LVGL例程,下载进去就有漂亮的动画,这并不是因为LVGL多智能,而是因为原子已经把这四件事全部做完了:LCD驱动封装好了、触摸驱动封装好了、时基挂在某个定时器上、内存配置也调过了。你看到的demo只是在上层调用LVGL的API。

所以很多人把例程当成黑盒,直接改UI代码没问题,但一旦要换屏幕分辨率、换触摸芯片、换OS或者从零建工程,就完全乱了。我见过太多人拿着官方例程的lv_port_disp.c和lv_port_indev.c往自己的工程里硬拷,结果编译报一堆错,或者跑起来白屏。原因就是这些文件里的底层调用是针对原子板卡的,不是通用代码。

理解这层关系之后,你自己做移植时就有了主线:先确认板子上的屏幕是什么接口、怎么驱动;再确认触摸芯片是什么型号、怎么读坐标;然后把这些能力包装成LVGL需要的回调;最后把时基接好。一条线走通,界面代码随便写。

2. 版本选型与工程目录:8.3还是9.x,怎么搭骨架

2.1 LVGL 8.3与9.x的核心差异

打开GitHub的LVGL Release页面,你会发现现在有两个大版本在并行:老牌的8.3.x和新架构的9.x。9.x把很多API重命名了,比如lv_disp_draw_buf_t整合进了lv_display_t,LV_MEM_SIZE改成了LV_MEM_POOL_SIZE,输入设备和显示设备的结构体也从lv_disp_drv_t、lv_indev_drv_t换成了统一的lv_display_t和lv_indev_t。整体设计更干净,但对老教程不太友好。

我做个简单对比:

对比项LVGL 8.3.xLVGL 9.x
API命名lv_disp_drv_t / lv_indev_drv_tlv_display_t / lv_indev_t
显示缓冲lv_disp_draw_buf_t独立管理并入display对象
时间基准lv_tick_inc()lv_tick_set_cb()更灵活
线程安全需自己加锁提供lv_lock / lv_unlock
官方资料量非常多在补,但教程还少
正点原子例程以8.x为主少量新板卡开始用9.x

你要是跟着正点原子教程学,或者参考网上大量现成例程,我建议先用8.3.x稳定版。我自己的项目到现在还在用8.3.11,不是因为9.x不好,而是因为它生态成熟,遇到问题一搜就有答案。新项目如果团队有能力消化API变化,直接用9.x当然也可以,但别在刚开始学移植时同时踩版本和新架构两个坑。

2.2 lv_conf.h的坑:模板、名字和三个编译宏

LVGL源码包里不会直接给你lv_conf.h,只会给一个lv_conf_template.h模板。你要把它复制一份,改名为lv_conf.h,放到工程Include路径里。这一步很多人漏掉,漏掉的后果是编译时LVGL会报找不到lv_conf.h,因为源码内部的lv_conf_internal.h会去搜索它。

改完名字之后,编译器还需要几个宏。最麻烦的配置是LV_CONF_INCLUDE_SIMPLE和LV_LVGL_H_INCLUDE_SIMPLE。加了LV_CONF_INCLUDE_SIMPLE,源码里就能用#include "lv_conf.h"这种方式找到你的配置文件;不加的话,它可能会尝试用相对路径../../lv_conf.h,而你的工程目录结构一不对就找不到头文件,编译直接挂。另一个LV_LVGL_H_INCLUDE_SIMPLE则是让各组件之间相互引用时用简单的路径。

以STM32的KEIL工程为例,你需要在C/C++选项卡的Define里加上这两个宏,然后把lvgl的源码目录和lv_conf.h所在目录都加进Include Paths。ARM GCC或CMake工程同理。这个点看着不起眼,但我的经验里,一半的人第一次编译失败都栽在头文件路径上。

2.3 一个最小可编译的LVGL空工程长什么样

一个能跑起来的工程,目录通常长这样:

project/ lvgl/ lv_conf.h lv_conf_template.h src/ bsp/ lcd.c lcd.h touch.c touch.h freertos/ main.c Makefile 或 Keil/MDK工程文件

main函数里最核心的代码其实短得可怜:

#include "lvgl/lvgl.h" #include "bsp.h" int main(void) { bsp_init(); // 硬件初始化,时钟、GPIO、LCD、触摸 lv_init(); // LVGL核心初始化 lv_port_disp_init(); // 注册显示刷新回调 lv_port_indev_init(); // 注册输入设备回调 ui_create(); // 创建你的界面 while (1) { lv_timer_handler(); // 定时驱动LVGL处理事件和重绘 delay_ms(5); } }

你先不要急着往上加复杂的UI,能编译过、能显出背景色,就算骨架通了。之后再逐步添加控件。PC模拟器是个好东西,在板子还没有显示驱动时,先用模拟器验证UI逻辑,能省下很多来回烧Flash的时间。LVGL官方仓库里有lv_sim_*系列模拟器工程,分别支持VS、Eclipse和VSCode,直接拉下来编译就能跑,移植的实际代码和板卡上的逻辑完全一致。

3. 显示驱动接入:从“一个空画布”到“屏幕上有东西”

3.1 先分清你的屏是什么类型

正点原子板卡上常见就这么几类屏,它们的移植复杂度天差地别。先把类型搞清楚,后面所有工作才有方向。

屏类型常见接口典型场景移植要点
SPI屏SPI + DC + CS1.3寸、1.54寸、2.4寸小屏一帧一帧或局部推数据,速度受限于SPI时钟
8080并口屏FSMC/FMC老款MCU屏写入速度快,引脚占用多,带时序要求
RGB屏RGB888/RGB565 + 行场同步STM32F429 LTDC、IMX6ULL LCDIF需要显存,LVGL可以直接画进显存区域
Framebuffer/dev/fb0Linux、RT-Thread等带显示设备系统mmap显存,flush回调里memcpy或直接绘制

RGB屏和SPI屏的移植思路别混。SPI屏的显存一般在屏内部,你用SPI把像素数据发给它,它自己刷新;RGB屏的显存则在主控侧,LVGL画到这块内存,屏幕控制器通过时序从这块内存取数显示。

正点原子比较典型的两块平台,STM32F407/429板子多是LTDC+RGB屏,IMX6ULL板子是LCDIF+RGB屏,这几个都偏后半类。如果你的板子是原子老款9341之类的SPI小屏,那就是第一种。

3.2 flush回调的工作逻辑和一段可直接改的骨架代码

LVGL的flush回调是整个移植层最关键的代码。它的工作方式是这样的:LVGL在内部维持一个绘制缓冲区,当需要更新界面时,它会算出需要更新的矩形区域(x1、y1、x2、y2),把这块区域的像素数据画好,然后回调flush函数,把区域坐标和像素指针交给你。你的责任是把这个矩形区域的像素搬到屏幕对应位置,搬完调用lv_display_flush_ready()告诉LVGL可以继续画下一块了。

下面是一段通用骨架,以RGB屏和LTDC为例,显存首地址是ltdc_framebuf:

static void disp_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); uint16_t *src = (uint16_t *)px_map; uint16_t *dst = (uint16_t *)ltdc_framebuf + area->y1 * screen_width + area->x1; // 以行为单位逐行拷贝到显存 for (uint32_t row = 0; row < h; row++) { memcpy(dst, src, w * 2); src += w; dst += screen_width; } lv_display_flush_ready(disp); }

这段代码里最容易被忽略的是目标地址的计算。dst不是每次都从显存头开始搬,而是根据区域坐标算出在显存里的偏移。如果漏了area->y1 * screen_width,你得到的画面就是错位和撕裂的。screen_width也要和显存实际宽度严格对应,如果显存一行占的字节数和屏幕分辨率不一致,同样会花。

3.3 DMA、色彩转换和显存对齐:花屏的主要来源

不用DMA的话,CPU逐行memcpy在320×240的屏上还好,到了480×272乃至800×480,每一次刷新都要占用大量CPU时间,界面动画会肉眼可见地卡。所以更合理的做法是让DMA或DMA2D来搬运数据,CPU在搬运期间继续跑LVGL的绘图任务。

开启DMA之后,花屏概率会明显上升,原因基本都是这几个。

第一个是内存对齐。很多DMA控制器要求源地址和目的地址按32位对齐,如果你给LVGL分配的draw buffer地址没有对齐,DMA传输就会出错或者只有部分数据正确。MCU工程里可以声明一个自定义内存区域来确保对齐,或者用malloc之后做地址对齐处理。

第二个是D-Cache一致性问题。跑在Cortex-M7等高主频MPU上时,CPU和DMA之间有一层Cache,如果CPU刚写完数据还没回写Cache,DMA直接去内存搬运,搬走的可能是旧数据,显示出来就是花屏或者残影。这时候需要在DMA搬运前做SCB_CleanDCache()之类的处理,搬完后再让Cache失效,麻烦但绕不开。

第三个是颜色格式转换。你的屏幕物理面板如果是RGB888,但LVGL配置成16位RGB565,显露出来的颜色会明显不对。反过来一样。所以移植前先看清楚屏幕面板是什么格式,LVGL的LV_COLOR_DEPTH就设成什么格式。如果屏是18位RGB666,驱动里可能需要做一次从RGB565到RGB666的转换再写显存。

3.4 正点原子平台上的两个典型场景

在STM32F407/429这一类带LTDC的板子上,原子例程已经初始化好了LTDC控制器和一个全屏的显存缓冲。LVGL移植的工作其实就是把LTDC显存地址告诉LVGL的draw buffer,或者提供一个flush函数往那个显存里memcpy。这类屏因为主控直接控制刷新时序,刷屏速度通常不错,瓶颈反而在LVGL软件绘制的速度上。

在IMX6ULL Linux板子上略有不同。你在Linux用户态拿到的是一块/dev/fb0帧缓冲设备,通过open和mmap拿到显存映射,然后改成读触摸坐标、拼LVGL刷新链路。它和裸机LTDC的差异只在于底层的显存来源从物理地址变成了mmap映射指针,flush函数里依旧是memcpy。如果你用的Linux版本比较新,还需要注意显存的稳定性,如果mmap出来的内存被换页了,画面会闪,这种情况通常需要把LVGL的draw buffer分配在连续物理内存里,或者干脆用DMA-BUF。

4. 输入设备接入:触摸回调没写好,界面再漂亮也白搭

4.1 LVGL输入设备模型的本质

LVGL把输入设备抽象成lv_indev,它不关心你用的是GT9147、FT5x06还是XPT2046,也不管你是I2C读取还是SPI读取。它只要求你提供一个读取回调,回调里回答两个问题:当前有没有按下,按下的坐标是多少。只要这个回调能稳定回答,LVGL就负责把事件分发给对应控件。

所以触摸移植基本就是把“读触摸芯片坐标”这个驱动函数和LVGL的回调缝一下。过程本身不难,难的是把坐标的方向、范围、滤波处理好,尤其是把屏幕物理方向搞对。

4.2 触摸驱动接入流程

典型的触摸回调长这样:

static void touchpad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) { int16_t x = 0, y = 0; bool pressed = touch_get_point(&x, &y); if (pressed) { >lv_group_t *g = lv_group_create(); lv_group_add_obj(g, btn1); lv_group_add_obj(g, btn2); lv_indev_t *indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER); lv_indev_set_read_cb(indev, encoder_read_cb); lv_indev_set_group(indev, g);

encoder_read_cb里返回enc_diff(旋转了多少格)和按键状态。LVGL拿到enc_diff就会把焦点在lv_group里移动,按键按下就确认。这个模式在菜单型UI里特别合适,配合lv_list、lv_menu这些控件,一个编码器就能操作完整套设置页面。很多人问为什么LVGL要保留键盘输入,因为工业产品场景里触摸屏成本高、又沾油污,实体按键更可靠。

4.4 方向、坐标映射和校准的常见问题

触摸坐标和屏幕坐标方向不一致是最常见的问题。触摸IC的坐标系有原生方向和旋转方向,屏幕的扫描方向也有起始点,两者一组合就会出现上下倒置、左右翻转的鬼畜画面。解决办法是在回调里做一次坐标变换:

// 例如触摸原点和屏幕原点横向相反>void ui_task(void *arg) { while (1) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } }

任务优先级建议不要给最高。LVGL任务运行期间会占用CPU执行绘图,如果优先级太高,其它实时任务可能得不到调度,尤其是DMA中断或者看门狗喂狗任务会被卡住,最后表现就是系统时不时死机或者复位。我一般把LVGL任务设为普通应用优先级,比硬件驱动低一点,比空闲任务高一点就行。

任务栈大小也要注意。LVGL渲染和事件处理过程中会递归调用一些控件函数,局部变量占用的栈空间不小。我在STM32上给UI任务分配了8KB栈,如果工程里大量使用中文长文本和复杂布局,建议放宽到12KB以上,同时打开FreeRTOS的栈溢出检测,在测试阶段把问题暴露出来,不要留到现场去蓝屏。

5.3 LVGL的内存配置在RTOS下的两种姿势

LVGL自己的内存管理有两种工作模式。一种是不依赖外部malloc,使用内部静态内存池,通过LV_MEM_SIZE指定大小。另一种是设置LV_MEM_CUSTOM=1,让LVGL调用你提供的malloc和free。

在FreeRTOS下,我推荐优先保留LVGL内部内存池。原因是RTOS堆(尤其heap_4)在频繁申请释放小块内存后会产生碎片,LVGL的控件创建、销毁、图片解码都很频繁,碎片一旦累积,运行几个小时后系统可能突然崩溃。内部内存池虽然也会碎片,但它规则简单,可以换用lv_mem_monitor实时查看使用量和碎片率。如果你非要用动态内存,至少别用默认的malloc,而是指定到pvPortMalloc,并且定期监控堆余量。

5.4 线程安全:能不加锁就别加锁

LVGL在8.x时代默认不是线程安全的。多个任务同时调用lv_*API就会产生数据竞争,轻则界面闪烁,重则HardFault。9.x设计了lv_lock()和lv_unlock(),但用起来也有讲究。

我踩过几次坑之后的经验是:能不加锁就别加锁。设计上尽量让所有UI操作都发生在UI任务里,其他任务需要更新界面时,不要直接调用LVGL函数,而是通过FreeRTOS队列发消息给UI任务。UI任务收到消息后再去改标签、换页面、弹窗口,这样整个UI模块是单线程模型,天然安全。真要跨任务调LVGL接口,就在外部包一层互斥锁,但要注意别在中断里调用加锁函数,否则死锁风险极大。

6. 内存、渲染与性能调优:先把数学算清楚再谈优化

6.1 带宽是硬约束:屏幕数据量决定了你的天花板

很多人在调UI流畅度时第一时间想到换主频更高的芯片,其实先算一笔账就能判断瓶颈在哪。以一块常见的480×272、RGB565色深的屏为例:

  • 一帧像素数:480 × 272 = 130,560 像素
  • 一帧数据量:130,560 × 2 字节 = 261,120 字节
  • 如果要跑到30fps:261,120 × 30 ≈ 7.8 MB/s

如果你的屏是SPI接口,SPI时钟40MHz,每像素16bit,理论极限速率是40Mbps / 16bit = 2.5M像素/s,一整屏需要约52ms,换算下来只有19fps左右,这还是理论值,实际SPI带协议开销、GPIO反转、驱动延迟,能跑到15fps已经很不错。RGB屏就不一样,LTDC或者LCDIF直接由DMA搬运显存数据,带宽和内存频率相当,瓶颈就不在数据搬运,而在LVGL软件绘制的效率上。

所以做产品选型时,如果界面复杂而且要求流畅,SPI屏通常不适合大尺寸和复杂动画,这是物理带宽决定的,不是LVGL本身问题。

6.2 draw buffer选多大、要不要双缓冲

LVGL默认采用“局部刷新”策略,它不会每次刷新都画全屏,而是只画变化区域,这个区域在LVGL内部用脏矩形机制标记。因此draw buffer不需要全屏大小,一般设置成屏幕宽度乘以几行就够了。

以480×272、RGB565为例,一个480×10行的buffer占用4800×2 = 9600字节,不超过10KB,这就是一个很均衡的配置。如果你内存充裕,可以再开第二个buffer,LVGL在后台绘制buffer A,同时让第一个buffer的DMA搬运在运行,两个buffer轮流交替,刷新效率会有明显提升,动画撕裂感也会好很多。

draw buffer也不是越大越好。它需要消耗内存,而且如果屏幕本身只有局部区域在变化,buffer再大也不会带来更多收益。我建议先把buffer设成5行跑通,再逐步增大观察效果,找到性价比最高的平衡点。

6.3 色彩深度、图片格式和字体缓存

LVGL的颜色深度直接在lv_conf.h里配置,这个值必须和屏幕物理格式对齐,否则要么颜色发紫,要么显示慢半拍。如果你的屏幕是RGB565,就用LV_COLOR_DEPTH 16;如果是ARGB8888,再用32。千万别为了“视觉效果更细腻”在16位物理屏上配32位颜色,那样不仅颜色不对,带宽还要翻倍。

图片加载是另一个性能杀手。LVGL直接加载PNG或JPEG图片时,需要软件解码,耗内存又慢。建议把UI用到的图片用官方LVGL图片转换工具转成C数组或二进制bin格式,这些格式可以直接被LVGL解码,复杂图片甚至都不需要解压的过程。对于字库也一样,把常用字符集生成成固定字库,运行时按需取模,比加载完整字库省很多内存。

6.4 进阶思路:DMA2D、G2D之类硬件加速到底要不要上

正点原子和一些带GPU的SoC板卡上,会有DMA2D、G2D这类2D图形加速引擎。它们的价值是可以替代CPU做大量像素搬移、颜色格式转换、旋转缩放等操作。比如STM32F429的DMA2D能把RGB565转成RGB888,能省掉CPU软转的时间。

但我要泼一盆冷水:如果你的UI瓶颈在LVGL软件绘制阶段(画圆角矩形、文字渲染、阴影混合),那硬件2D引擎能帮上的忙有限,因为LVGL的绘图函数是CPU跑的,DMA2D只负责搬运。只有当你发现CPU时间大量消耗在这类操作上时,上硬件加速才有明确收益。LVGL 8.x/9.x社区有一些第三方的DMA2D/G2D驱动,可以接管lv_draw_*钩子,但引入它意味着要改很多底层代码,调试成本不可忽视。

我的建议是性能调优按这个顺序做:先关阴影和多余动画,再调draw buffer行数和双缓冲,然后看DMA传输是否占满,最后才考虑硬件加速。多数项目在前两步就解决了问题,不需要继续往下折腾。

7. 从白屏到跑通:一个自用的排查套路

7.1 白屏/无显示:从硬件到软件按顺序查

白屏是最打击人的,但它也最好查,只要按顺序过滤。第一步看背光,背光亮不代表主控在工作,但背光不亮先查电源和背光驱动;第二步看屏幕初始化,LCD控制器上电后要严格按厂商时序配置初始化寄存器,方向、像素时钟、分辨率任何一个错了都白屏;第三步看LVGL有没有真的跑起来,方法是在lv_init()之后加一个printf或者让一个LED闪烁;第四步在flush回调里加一个计数器,每次调用加一,串口打印出来,看一眼数字有没有在涨,如果数字在涨说明LVGL在有数据输出,问题在下游显示链路,如果不涨说明LVGL渲染管线压根没跑起来。

我经常见到的情况是:LVGL任务没有被调度,lv_timer_handler()根本没被调用,于是flush一次也不触发,屏幕干干净净一片白。这种情况和LVGL无关,先查FreeRTOS任务创建是否成功,优先级和栈是否设置正确。

7.2 花屏、偏移、颜色不对:多半不是LVGL的问题

花屏比白屏难查一点,因为它说明LVGL确实在输出数据,但数据没有正确到屏上。优先排查三个地方。

第一个是内存对齐。SPI DMA或者LTDC的DMA读地址如果没按32位对齐,花屏概率极高,检查draw buffer地址是否对齐到4字节甚至32字节边界。

第二个是显存行宽。很多屏驱动里有一个“行宽参数”,如果显存的每行字节数和屏幕分辨率不一致,图形会阶梯状错位。比如实际显存一行240像素,你写的时候按320像素一行算,那第二行开始全歪了。

第三个是D-Cache一致性问题。如果MCU有Cache,CPU写显存后必须clean,DMA搬运后必须invalidate,顺序错了就是花屏或残影。这个坑在Cortex-M7核上尤其多。

最后还有一个容易被忽略的:屏幕扫描方向和LVGL坐标方向不一致。比如屏是从右往左扫描,你直接往显存里写像素,显示出来就是左右镜像。

现象首查项次要查项
白屏有背光LCD初始化/复位序列LVGL是否进入flush回调
花屏缓存对齐/颜色格式D-Cache一致性
颜色偏色LV_COLOR_DEPTH配置屏端RGB顺序
触摸反向/偏移坐标变换触摸IC寄存器配置
界面闪烁draw buffer太小/无双缓冲局部刷新配置
跑一会死机LV_MEM_SIZE不足任务栈溢出

7.3 触摸错位、点击无效:坐标和回调是重灾区

触摸问题一般就是两类,一类是点击位置和实际图标位置有偏差,另一类是点了没反应。

位置偏差先确认坐标系是否一致,触摸IC的坐标原点和屏幕的数据扫描原点是否在一个角,如果不在,做水平或垂直翻转。再用一个简单的UI测试页,在屏幕四角画四个大按钮,逐个点击,看哪个角落错位方向,能快速定位是哪条坐标轴反了。

点击没反应则要查两件事:触摸芯片是否真的读到了有效数据,以及LVGL是否注册了输入设备。很多人只初始化了触摸芯片,忘了调用lv_indev_create注册回调,结果触摸芯片工作正常但UI就是没反应,这个在调试时特别容易造成困惑。还有一个隐蔽的坑是触摸芯片在没有触摸时会返回0xFFFF之类的无效坐标,你的驱动如果不做坐标有效范围判断,LVGL会认为手指一直压在屏幕上,界面会一直处于按下状态。

7.4 跑一会儿卡死/重启:内存和栈也要查

跑几分钟才崩的问题最恼火,因为它不按固定规律重现,通常和内存耗尽有关。LVGL内部内存池不够时会打印lv_mem_alloc: out of memory,但如果你没开串口或者没看log,就只看到系统复位。建议在调试阶段周期性调用lv_mem_monitor()把内存使用率打出来,观察有没有随时间上涨。如果持续上涨,多半是有控件或图片重复创建没有释放,造成内存泄漏。

FreeRTOS任务栈溢出也会导致诡异崩溃。建议开启configCHECK_FOR_STACK_OVERFLOW为2,正常跑一段时间UI操作,串口会打印栈溢出告警。设置UI任务栈时,宁多勿少,多出来的几KB内存成本远低于在线排查崩溃的时间成本。

内存和栈都查过没问题,再回头看是不是在中断里调用了LVGL函数,比如在定时器中断里改控件属性,这在带Cache的处理器上很容易引发偶发崩溃,而且极难定位。UI任务单线程模型能避开大多数这类问题。

我自己后来把LVGL移植整理成一套固定套路,所有和板卡相关的参数全部收敛到一个bsp_lvgl_port.h里:屏幕分辨率、像素时钟、触摸方向、显存宽度、draw buffer行数、颜色深度,换板子时只需要改这个文件。这个习惯帮我省下大量重复劳动,也让我在接手别人半成品工程时能迅速定位问题。如果你正在被白屏折磨,我的建议是先别急着读LVGL源码,先把驱动和回调调通,再回来写UI,顺序对了,后面的路会顺很多。

返回列表