ARTICLE DETAIL

资讯详情

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

LVGL在RTOS下线程安全实战:从卡死到流畅的完整指南

LVGL在RTOS下线程安全实战:从卡死到流畅的完整指南 作为一个常年把LVGL往各种RTOS里塞的嵌入式开发者我太清楚标题里“从卡死到流畅”这六个字的分量了。无论是刚把LVGL 8.x 移植到FreeRTOS上的新手还是正在折腾GD32F103、ESP32这类资源受限MCU的老鸟几乎每个人都会在某个深夜被诡异的现象折磨界面偶尔切换不流畅、显示撕裂、动画卡住甚至整个系统直接硬死机。很多人第一反应是GPU不够快或者移植代码有Bug但排查到最后往往发现问题出在一个基础却极其关键的地方——线程安全。LVGL本身并不是线程安全的库当你把它放在RTOS环境下多个任务同时操作显示缓冲区或内部状态时各种匪夷所思的问题就接踵而至了。这篇文章我不会只贴一堆官方文档说过的东西而是基于我实际踩坑、修复、重构的经验把LVGL与RTOS结合的线程安全实践从原理到代码再到排查思路做一个系统性的复盘。如果你正被界面卡死、任务挂起或者动画异常折磨这篇文章就是为你准备的。1. 项目概述与问题溯源为什么LVGL跑在RTOS上会卡死1.1 核心矛盾LVGL的单线程假设与RTOS的多任务现实先理清一个根本问题LVGL在设计之初核心渲染循环lv_timer_handler是被设计成在单线程环境中运行的。它内部维护着大量的全局状态对象树、事件队列、动画状态机、显示缓冲区的读写指针。通常我们只在一个地方调用lv_timer_handler()比如放在while(1)主循环里或者单独开一个lvgl_task。而RTOS环境下我们的业务逻辑往往散布在多个任务里比如一个任务负责读取传感器数据并通过LVGL的图表控件刷新曲线另一个任务处理按键输入并切换页面还有一个任务通过网络收到数据后更新标签文本。这些任务如果直接调用lv_label_set_text()或lv_obj_add_flag()等API瞬间就会跟lv_timer_handler()的渲染过程产生数据竞争。两个任务同时操作一个链表节点结果是灾难性的——轻则显示错乱重则直接硬错误进入死循环。这里需要明确一点LVGL虽然不是线程安全的但它并非完全没有线程安全机制。在lv_conf.h中有一个LV_USE_OS配置项。在LVGL 8.3及之后的版本中如果在移植时开启了系统层支持LVGL会尝试用操作系统提供的互斥锁封装一些内部操作。但这层封装远没有覆盖全部API而且很多开发者甚至根本没有开启这个选项或者对它的工作原理一知半解以为开启了就万事大吉。我见过很多项目配置里LV_USE_OS 1但系统层文件lv_os只是placeholder并没有真正对接FreeRTOS的互斥锁API等于白搭。1.2 卡死现场还原信号量、优先级翻转与脏数据卡死是最终现象但成因往往不止一个。我把实际项目中遇到的几类典型场景整理出来基本能覆盖大部分案例第一类是死锁。任务A持有了LVGL内部锁或者它自己加的锁去执行某个LVGL API此时时间片耗尽高优先级任务B抢占CPU任务B也尝试调用LVGL API并尝试获取同一把锁发现锁被任务A持有于是进入阻塞等待。恰好任务A又需要等待任务B释放某个资源才能继续执行并释放锁这就构成了典型的循环等待。在FreeRTOS中如果没有合理配置互斥锁的优先级继承属性还可能引发优先级翻转进一步加剧死锁概率。第二类是看门狗复位。这类问题容易被误判为硬件不稳定。我排过一个故障按键任务里做了防抖处理在200ms内要调用多次lv_btn_set_state()每次调用都会触发lv_event_send()导致多个事件回调里有耗时操作。整个lv_timer_handler()循环被拖得极长低优先级的GUI任务迟迟无法让出CPU高优先级的通信任务饿死最终通信看门狗超时复位。第三类是界面闪烁或撕裂。这个不会卡死但体验极差。如果两个任务同时往显示缓冲区里写数据或者一个任务在DMA传输过程中修改了缓冲区内容画面就会出现半幅更新、撕裂甚至花屏。这在切换页面或者刷新大尺寸图片时非常明显。如果把这些问题的根源总结成一句话就是不加锁地共享了LVGL的上下文并且没有建立一个清晰的“谁可以碰LVGL”的访问模型。2. 方案选型建立GUI专属任务与统一访问模型2.1 架构设计思路一切UI访问都走GUI队列解决线程安全最简单粗暴的办法是全局大锁所有调用LVGL API的地方在调用前加锁调用后解锁。这个方案简单、直接、不容易出错但效率非常低锁竞争严重特别是当你在中断或高频任务中操作UI时极易引发低优先级任务长时间得不到调度。我在实际项目中更推荐另一种做法建立GUI专属任务 消息队列。系统的UI操作只允许在专属的GUI任务中执行比如gui_task。其他业务任务如果需要更新界面不直接调用LVGL API而是把需求封装成一条私有消息通过RTOS的消息队列发送给gui_task。gui_task收到消息后在任务上下文中安全地调用LVGL API然后处理lv_timer_handler()。这样做的优势非常明显从架构上根除了多线程并发访问的问题。因为所有LVGL API的调用都发生在同一个任务上下文中天然就是串行的。业务任务和GUI任务之间只存在一个生产者和消费者的关系只需要保证消息队列本身线程安全即可FreeRTOS的队列天然保证这一点。看似绕了一圈多写了不少代码但稳定性和可维护性提升巨大。这个模式其实就是把“锁”从API层面转化成了“队列”层面的同步代价是增加了些许延迟但对绝大多数人机交互需求来说换来的可靠性绝对值回票价。2.2 在FreeRTOS上实现GUI任务与消息分发下面给出我实际在STM32 FreeRTOS上验证过的代码骨架。请务必注意这不是完整工程而是一个可复用的模式// gui_task.h #ifndef __GUI_TASK_H #define __GUI_TASK_H #include cmsis_os.h #include lvgl.h #include stdint.h typedef enum { GUI_MSG_SET_LABEL, GUI_MSG_UPDATE_CHART, GUI_MSG_LOAD_PAGE, GUI_MSG_KEY_PRESS, // ... 其他消息类型 } gui_msg_type_t; typedef struct { gui_msg_type_t type; union { struct { lv_obj_t *obj; char text[32]; } label; struct { lv_obj_t *obj; int16_t value; } chart; struct { uint8_t page_id; } page; struct { uint8_t key_code; } key; } param; } gui_msg_t; void gui_task_init(void); void gui_send_msg(gui_msg_t *msg); void gui_task_entry(void *arg); #endif// gui_task.c #include gui_task.h #include FreeRTOS.h #include task.h #include queue.h static QueueHandle_t s_gui_queue; void gui_send_msg(gui_msg_t *msg) { if (s_gui_queue ! NULL) { BaseType_t need_yield pdFALSE; // 中断中使用FromISR版本任务中使用普通版本 xQueueSendFromISR(s_gui_queue, msg, need_yield); portYIELD_FROM_ISR(need_yield); } } void gui_task_entry(void *arg) { gui_msg_t msg; lv_init(); // 初始化显示驱动和输入驱动这里省略 s_gui_queue xQueueCreate(16, sizeof(gui_msg_t)); while (1) { // 轮询处理消息确保UI事件不会堆积太多 while (xQueueReceive(s_gui_queue, msg, 0) pdTRUE) { handle_gui_msg(msg); } // 耗时最短的间隔轮询LVGL处理器 lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); // 5ms } } static void handle_gui_msg(const gui_msg_t *msg) { switch (msg-type) { case GUI_MSG_SET_LABEL: if (msg-param.label.obj) { lv_label_set_text(msg-param.label.obj, msg-param.label.text); } break; case GUI_MSG_LOAD_PAGE: // 页面切换逻辑 break; // ... 其他消息处理 } } void gui_task_init(void) { xTaskCreate(gui_task_entry, gui_task, 4096, NULL, 5, NULL); }这段代码有几个细节需要说明。队列长度我一般设为16或者32太短容易丢消息太长浪费RAM。消息体用联合体压缩体积这对小内存MCU很重要毕竟一个gui_msg_t如果不用联合体动不动就是几十上百字节队列再乘以16RAM压力就上来了。xQueueSendFromISR的使用是因为有些传感器任务可能在中断上下文中直接发送UI消息比如编码器计数中断。如果你确认所有业务任务都是普通任务也可以直接用xQueueSend。另外handle_gui_msg最好只做LVGL API调用和轻量逻辑绝对不要把耗时操作、文件读写、复杂计算放进去否则会卡住lv_timer_handler()的执行。2.3 为什么我不推荐仅依赖LV_USE_OS先说明一下LVGL 8.3 官方引入了LV_USE_OS配置可以对接FreeRTOS、CMSIS-RTOS2等它提供了lv_lock()/lv_unlock()这样的全局锁宏理论上你可以在多个任务中直接调用lv_lock()包住LVGL API调用。这个机制是有效的而且官方推荐在“需要在多线程中直接调用LVGL API”时使用它。但我在项目里对它的评价是能用但不要作为主力方案。原因有三第一锁的粒度太粗。lv_lock()锁住的是整个LVGL全局上下文。如果业务任务A和GUI任务同时要操作UI业务任务A持锁后执行一个耗时的样式计算GUI任务必须等待这直接影响渲染节奏。第二容易忘记开配置或者配置错误。很多修改过的第三方面板驱动、SDK生成的工程里LV_USE_OS是默认关闭的即使开了如果lv_os目录下没有正确的lv_os_freertos.c文件链接时可能不会报错运行时锁是个空操作你会误以为有保护实际上没有。第三中断安全不好处理。在中断上下文里调用lv_lock()是非常危险的操作。互斥锁如果在中断里获取不到会尝试阻塞这可是崩溃级别的错误。虽然LV_USE_OS内部有lv_lock_is_in_isr之类的检查但只要你有在中断里操作UI的冲动这条路径就不值得冒险。所以我的结论是LV_USE_OS适合作为额外的保险丝但架构上还是要靠“单任务访问 队列通信”来兜底。两者结合既保证效率又保证安全。3. 核心细节解析与实操要点3.1 LVGL显示缓冲区的正确配置与双缓冲策略既然聊到界面流畅就不能绕过显示缓冲区。LVGL的每次刷新都要访问内部缓冲区这个缓冲区的设计直接影响线程安全和渲染流畅度。LVGL的刷新机制是这样的应用层的控件绘制先绘制到内部缓冲区内部的lv_refr机制把脏区域的像素数据拷贝到用户提供的显示缓冲区然后通过flush_cb回调发送到屏幕。LV_MEM_SIZE和LV_DISP_BUF_SIZE是两个关键参数尤其后者。我强烈推荐使用双缓冲模式。具体做法是定义两个屏幕大小的缓冲区如果内存不够至少两个局部缓冲区#define DISP_BUF_SIZE (LV_HOR_RES * LV_VER_RES) static lv_disp_draw_buf_t draw_buf; static lv_color_t buf_1[DISP_BUF_SIZE]; static lv_color_t buf_2[DISP_BUF_SIZE]; void lv_port_disp_init(void) { lv_disp_draw_buf_init(draw_buf, buf_1, buf_2, DISP_BUF_SIZE); // ... display driver init }为什么双缓冲在这种情况下如此重要因为这能让flush_cb中的DMA传输和LVGL的绘制工作重叠起来。LVGL在向一个缓冲写数据的同时DMA可以把另一个缓冲的数据发送到LCD。在这种模式下flush_cb里通常会等待DMA传输完成再调用lv_disp_flush_ready()通知LVGL缓冲区已释放。这里有一个线程安全的隐患DMA传输期间如果另一个任务也尝试访问这个缓冲区就会造成“一个在写一个在发”的冲突。在单任务模型下这个问题不太明显因为LVGL会等到flush_ready回调后才继续刷新但在多任务下如果业务任务直接操作了缓冲区比如直写显存就破坏了时序。所以在GUI单任务模型下所有对缓冲区的访问必须限定在GUI任务内部业务任务只能通过消息队列请求GUI任务去更新UI绝对不要直接访问任何LVGL相关的底层数组。3.2 帧率控制与lv_timer_handler的调用节奏很多开发者对lv_timer_handler()的调用策略很随意有的放在while(1)里死循环跑有的用vTaskDelay(1)高频轮询有的甚至放在SysTick中断里。这些都存在隐患。lv_timer_handler()每次调用会执行定时器、动画、刷新模块等所有内部任务。它的执行时间并不是固定的取决于当前有多少对象需要刷新、动画是否在运行。如果在高优先级任务里死循环调用它会浪费大量CPU时间如果在低优先级任务里调用又可能因为被频繁抢占而卡顿。我的经验是GUI任务优先级建议介于业务中等优先级与硬件驱动高优先级之间用vTaskDelay(5)或者vTaskDelayUntil固定周期调用根据屏幕刷新率需求一般5ms到10ms为一个周期比较平衡。如果你追求更精细的控制可以分析lv_timer_handler内部返回的下次到期时间。LVGL在内部维护了timer的到期列表并在lv_timer_handler()的返回值里暗示了“下一次还需要多久再调用”。在LVGL 8.x中lv_timer_handler()的返回值是一个uint32_t代表“建议距下次调用的毫秒数”。你可以根据这个值动态调整vTaskDelay的时间uint32_t idle_ms lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(idle_ms 5 ? 5 : idle_ms));注意这个时间并不包含刷新需要的额外延迟所以我们通常仍然限制在一个最大值和最小值之间。这样做的收益是当界面静止时lv_timer_handler()返回较大值我们可以多睡一会儿省电又减CPU占用当动画运行时返回值较小任务就高频运行流畅度有了保障。3.3 信号量与互斥锁的实际应用场景虽然我们建议业务任务不直接调用LVGL API但总有一些场景绕不开需要简单加锁。典型就是多个任务都需要刷新状态但不想引入队列的复杂逻辑。这时候正确的做法是定义一个专门给GUI用的互斥锁而不是随手用一个二值信号量。互斥锁和二值信号量的区别非常关键。二值信号量常常被用来做同步一个任务发另一个任务等它虽然有“锁”的外观但没有优先级继承机制。互斥锁在FreeRTOS中则专门解决了优先级翻转问题当高优先级任务等待互斥锁时系统会临时提升持有锁的低优先级任务的优先级让它可以快速执行完并释放锁从而避免长时间被中优先级任务打断。LVGL在打开LV_USE_OS后默认就为lv_lock/unlock实现了互斥锁FreeRTOS下是xSemaphoreCreateMutex。如果你要自己在业务任务中加锁我的建议是使用同样的互斥锁接口而不要新建一堆信号量来模仿锁的行为。3.4 输入设备的线程安全问题按键、触摸与编码器输入设备驱动是线程安全的重灾区。大多数项目里按键扫描或者触摸读取是由独立任务或定时器中断执行的然后把坐标或键值交给LVGL。如果不加任何处理直接在扫描任务里调用lv_indev_drv_read_cb或者lv_event_send很可能在LVGL处理上一帧事件时写入新的输入事件导致事件队列混乱。更安全的做法是扫描任务只负责读取原始状态写入一个RTOS队列或FIFOGUI任务在lv_timer_handler()之前统一从队列取出输入事件然后调用LVGL的底层输入API如lv_indev_set_button_points()或直接调用lv_event_send()。这样输入数据和UI刷新就都在同一上下文。我在实际项目中触摸的I2C读取仍然放在独立的触摸任务里因为I2C带延时但拿到坐标后只是存到全局变量并置一个 “数据有效” 标志。GUI任务在每帧启动时读取这个标志和坐标然后调用lv_indev相关的API。如果不这样处理直接在I2C中断回调里调用LVGL API很容易导致LVGL内部结构被并发修改。4. 实操过程与核心环节实现4.1 用LVGL官方LV_USE_OS对接FreeRTOS既然前面提到LVGL内置了OS集成那就实际演示一下怎么把它配好。首先在lv_conf.h中确认以下配置#define LV_USE_OS 1然后确保你的工程里有对应的系统层文件。LVGL 8.3版本的源码目录结构里有一个env_support目录里面会有freertos文件夹对应lv_os_freertos.c和lv_os_freertos.h。如果你的源码包没有这个目录可以去GitHub拉取最新的 release 分支或者根据LVGL的文档手动创建这个文件。// lv_os_freertos.c (简化) #include lvgl.h #if LV_USE_OS #include FreeRTOS.h #include semphr.h static SemaphoreHandle_t lv_lock_ptr; void lv_lock_init(void) { if (lv_lock_ptr NULL) { lv_lock_ptr xSemaphoreCreateMutex(); LV_ASSERT(lv_lock_ptr ! NULL); } } void lv_lock(void) { if (lv_lock_ptr ! NULL) { xSemaphoreTake(lv_lock_ptr, portMAX_DELAY); } } void lv_unlock(void) { if (lv_lock_ptr ! NULL) { xSemaphoreGive(lv_lock_ptr); } } #endif配置正确后LVGL内部在lv_timer_handler()以及事件处理等关键入口会自动调用lv_lock()和lv_unlock()。但请注意这只是保护LVGL内部核心操作并不是说你可以肆无忌惮地在中断里调用LVGL API。我还是建议业务任务尽量通过消息队列。4.2 多任务页面切换的完整代码参考下面演示一个实际场景任务A是传感器采集任务采集完数据后需要更新曲线任务B是按键扫描任务检测到页面切换键后需要切换页面。任务B通过消息队列给GUI任务发消息GUI任务在收到消息后切换到新页面。// 业务任务按键扫描中 void button_scan_task(void *arg) { for (;;) { uint8_t key read_key(); if (key KEY_NEXT_PAGE) { gui_msg_t msg {0}; msg.type GUI_MSG_LOAD_PAGE; msg.param.page.page_id get_current_page() 1; gui_send_msg(msg); } vTaskDelay(pdMS_TO_TICKS(20)); } } // GUI任务中 static void handle_gui_msg(const gui_msg_t *msg) { switch (msg-type) { case GUI_MSG_LOAD_PAGE: { lv_obj_t *new_page create_page(msg-param.page.page_id); lv_obj_t *current lv_scr_act(); lv_scr_load_anim(new_page, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, false); // 清理旧页面放到动画结束后处理更安全这里简化 if (current ! NULL) { // lv_obj_del(current); // 注意动画期间删除旧屏可能有问题 // 建议用 lv_obj_clean 或延迟删除 } break; } default: break; } }一个小细节lv_scr_load_anim是一个带动画的页面切换API它内部会开启一个动画定时器在动画执行期间旧的屏幕会被保留直到动画结束才被LVGL自动删除如果旧屏幕设置了LV_OBJ_FLAG_DEL_ON_RELEASE或者你在动画回调里删除。如果在动画进行中再次调用lv_scr_load_anim可能造成资源竞争。所以页面切换请求也需要做同步比如在消息里携带一个IDgui_task在处理时判断lv_anim_count_running()是否为零否则延迟到动画完成之后再做下一次切换。4.3 任务栈大小与内存对齐的调试心得LVGL本身占用内存大户主要是LV_MEM_SIZE堆缓存以及显示缓冲区、各个控件对象。除此之外每个RTOS任务还需要自己的任务栈。GUI任务因为要执行各种LVGL API调用链会比较深特别是涉及到字符串格式化、动画回调、容器布局计算时栈消耗不小。我踩过一次比较隐蔽的坑GUI任务栈分配了2048字节平时一切正常但一旦连续快速切换多个页面特别是某个界面包含大量lv_label动态创建时系统会在动画回调里硬错误。排查到最后发现是任务栈溢出。任务栈溢出在FreeRTOS里有时并不会立即崩溃它可能悄无声息地踩坏了相邻的内存区域然后在某个犄角旮旯的地方爆雷。排查手段是把configCHECK_FOR_STACK_OVERFLOW设为2或者用uxTaskGetStackHighWaterMark()定期观察剩余量。我的建议是GUI任务栈至少给4096字节如果LVGL版本较新且使用了较多容器、Flex布局等高级特性建议给到8192字节。不要迷信手册里的最小值实际跑一遍页面切换压力测试后再减。5. 常见问题与排查技巧实录5.1 卡死问题排查HardFault还是任务疯抢卡死分两种一种是进入HardFault一种是没有进HardFault但任务调度像冻住一样。前者多半是内存越界、野指针、栈溢出后者多半是死锁或优先级配置问题。面对HardFault第一步就是在调试器里查看PC寄存器和调用栈定位到出错函数。如果是LVGL相关函数再看是不是因为多个任务同时调用了lv_*API。这里有一个很实用的技巧在lv_conf.h里开启LV_USE_DEBUG和LV_USE_LOGLVGL会在出问题前打印警告信息比如对象指针无效、内存分配失败等。这些日志信息往往比HardFault信息更能说明问题。如果卡死但没有异常优先检查是不是死锁。暂停调试查看所有任务的状态有几个任务处于Blocked状态等待某个信号量谁持有这些信号量在FreeRTOS的调试视图里可以清晰看到每个互斥锁的持有者是哪个任务。如果发现多个任务互相等待而且持有锁的任务优先级较低基本可以判定是优先级翻转导致的死锁。解决方案是确保所有共享LVGL资源的锁都是互斥锁并且合理配置各任务的优先级避免低优先级任务长期持有高优先级任务需要的锁。5.2 显示撕裂花屏DMA传输与缓冲区冲突花屏通常与显示缓冲区有关尤其常见于flush_cb内部使用DMA传输的情况。如果在DMA传输过程中LVGL已经向同一个缓冲区写入了新的显示数据屏幕就会呈现“一半前一帧、一半后一帧”的撕裂效果。解决办法无外乎两种一是使用真正的双缓冲让LVGL始终在一个缓冲区绘制DMA从另一个缓冲区发送交替使用。这要求你的底层flush_cb支持等待上一次传输完成后再返回。如果你没有使用双缓冲而是单缓冲那么必须在flush_cb中等待DMA传输完成后再调用lv_disp_flush_ready()也就是说绘制和传输在时间上是串行的这能保证正确性但不能提高帧率。还有一点在GUI单任务模型下业务任务即使只是读一下buf_1的内容比如做截屏功能也需要防止与刷新冲突。这种操作应该通过队列请求GUI任务执行而不是直接去访问数组。5.3 动画不执行、界面切换不完全动画不执行最常见的原因是tick源没喂。LVGL的动画和超时机制依赖系统tick通过lv_tick_inc(1)提供。许多RTOS移植中lv_tick_inc是在一个定时器中断里调用的。如果你在FreeRTOS下的SysTick中断里做了其他耗时处理可能导致tick丢失LVGL误以为时间没到动画自然不执行。确认lv_tick_inc被正确调用的方法在lv_conf.h里开启LV_TICK_CUSTOM可以在一个RTOS tick hook中调用void vApplicationTickHook(void) { lv_tick_inc(1); }但要注意优先级高的中断不要做太多事情lv_tick_inc本身非常轻量可以在中断里调用。如果使用的是LVGL 9.xtick配置方式略有不同具体看官方文档但原理是相同的。另一个导致动画异常的是lv_timer_handler 调用频率不够。理想情况下LVGL内部的定时器周期默认是2msLV_DISP_DEF_REFR_PERIOD如果你把GUI任务延迟设置得太久比如100ms才调用一次动画自然会卡成PPT。5.4 实测优化效果任务栈调节与锁粒度收缩我最近在一个Cortex-M4主频168MHz的项目上做优化原来的代码是多个任务直接调用LVGL API并加上全局锁CPU占用高且偶发卡顿。重构后的方案是GUI单任务 队列通信把业务消息串行化同时给LVGL配置双缓冲帧率从原先的20fps提升到60fps屏幕尺寸480x320。其中锁粒度收缩是最直观的优化。原本业务任务频繁调用lv_label_set_text必须等待全局锁而现在只是往队列里塞一条消息耗时几乎是固定的几十微秒。GUI任务在空闲时统一处理这些消息更新UI不至于一股脑全挤在某个时刻渲染节奏更均匀。此外把原来的一次全屏刷新拆成小范围脏区域刷新LVGL本身支持只刷新脏区域配合双缓冲效果更好。6. 结尾一点个人经验补充最后再分享一个我在多个项目里都受益的小习惯在GUI任务中显式区分“同步UI操作”和“异步UI操作”。比如某些关键设置页面用户点击开关后必须立即视觉反馈我会在按键处理中直接调用对应的LVGL API加锁完成因为这属于单一任务内部调用在GUI任务里不会引起并发问题而不那么紧急的比如日志刷新、传感器曲线追加则全部走队列。这样既保障了交互实时性又避免了消息风暴导致动画卡顿。另外一个细节是RTOS下LVGL工程的文件组织一定要清晰。LVGL源码本身是跨平台的但底层驱动、锁封装、缓冲配置这些一定要单独分目录不要到处散落。我在调试一个GD32F103项目时就发现工程里有两个不同的lv_tick_inc调用一个来自系统定时器一个来自板级驱动两个都在喂tick导致LVGL的时间戳飞速前进动画速度快得离谱。这类问题浪费了我将近一个晚上查出来之后真的很想摔键盘。如果你正在经历从“能跑”到“跑得稳”的阶段希望你读完这篇文章后能少走几步我之前走过的弯路。稳定流畅的界面切换不完全是靠优化渲染算法很多时候靠的是合理的RTOS线程安全设计。
返回列表