前言
LVGL是一个在使用时非常容易产生卡死故障的GUI库,大多数卡死都发生在动态阶段,包括且不仅限于:
- 启动并初始化第一个页面时
- 在多个页面间来回切换
- 触发组件回调时(例如button或timer)
- 长时间运行后
本文所提供的解决方案就是我自己摸索出来的,不一定适用于所有人,也不一定是最标准的办法。网上关于LVGL的教程很多,包括官方文档,但是很多方法都是不怎么规范的。官方的Github issue中也有很多奇奇怪怪的问题,哪怕是LVGL的作者也无法解决或定位错误。但是LVGL已经迭代到了V9版本,大多数因为自带库问题导致的错误已经得到修复,由LVGL本身带来的致命错误已经很少,大多数错误都来源于用户自身的不规范代码或者是用法。
如果你的LVGL例程发生了卡死,首先应当按照以下步骤尝试解决:
- 更新LVGL版本至最新。同时,确保你使用的函数在最新的LVGL中仍然可用。LVGL库函数迭代的很快,且自从v8.3之后几乎每个小版本都有大改,v9之后更是删了一大批我认为很好用的功能(比如
lv_label_recolor())。如果你抄了一个旧版本的函数并在新版本的lvgl环境下使用它,可能导致问题 - 尝试在模拟器中复现代码,确认是否是LVGL库自身的问题
- 如果LVGL工作在OS下,尝试在裸机中复现问题;如果裸机和OS下都发生问题,说明部分代码可能存在问题;如果裸机下工作正常,大概率是线程问题导致的LVGL错误。
如果尝试过上述方法之后问题还没有解决,应考虑是用户代码造成的问题。
排故
在开始前,确保在lv_conf.h中启用内存监视和CPU占用监视,具体为:
/*1: Enable system monitor component*//*1: Enable system monitor component*/#define LV_USE_SYSMON 1#if LV_USE_SYSMON /*Get the idle percentage. E.g. uint32_t my_get_idle(void);*/ #define LV_SYSMON_GET_IDLE lv_timer_get_idle
/*1: Show CPU usage and FPS count * Requires `LV_USE_SYSMON = 1`*/ #define LV_USE_PERF_MONITOR 1 /* 启用CPU性能监视 */ #if LV_USE_PERF_MONITOR #define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
/*0: Displays performance data on the screen, 1: Prints performance data using log.*/ #define LV_USE_PERF_MONITOR_LOG_MODE 0 #endif
/*1: Show the used memory and the memory fragmentation * Requires `LV_USE_STDLIB_MALLOC = LV_STDLIB_BUILTIN` * Requires `LV_USE_SYSMON = 1`*/ #define LV_USE_MEM_MONITOR 1 /* 启用内存监视 */ #if LV_USE_MEM_MONITOR #define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT #endif
#endif /*LV_USE_SYSMON*/启动时卡死、白屏、花屏
非常常见的情况。首先在初始化后移除所有的对象创建函数,仅保留一个最基本的创建空白屏幕的函数,这么做是为了排除其他对象引起的干扰因素。
然后,按照下列顺序检查:
- 硬件问题。
- 确保屏幕和MCU硬件连接正确。最好的方式是先不跑LVGL,而是用一个最基本的填色函数来测试屏幕是否工作正常。
- 确保供电满足要求
- 确保时钟源正确配置
- 检查MCU启动文件中栈和堆大小是否设置正确。 LVGL正常运行至少需要以下条件:
Flash/ROM: > 64 kB for the very essential components (> 180 kB is recommended)
RAM:Static RAM usage: ~2 kB depending on the used features and object types
stack: > 2kB (> 8 kB is recommended)
Dynamic data (heap): > 2 KB (> 48 kB is recommended if using several objects).Set by LV_MEM_SIZE in lv_conf.h.
Display buffer: > "Horizontal resolution" pixels (> 10 "Horizontal resolution" is recommended)
One frame buffer in the MCU or in an external display controller- 检查
lv_conf.h中内存缓存地址及大小是否设置正确:
#if LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN /*Size of the memory available for `lv_malloc()` in bytes (>= 2kB)*/ #define LV_MEM_SIZE (300U * 1024U) /*[bytes]*/
/*Size of the memory expand for `lv_malloc()` in bytes*/ #define LV_MEM_POOL_EXPAND_SIZE 0
/*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/ #define LV_MEM_ADR (0XC0000000+1024*600*2) /*0: unused*/ /*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/ #if LV_MEM_ADR == 0 #undef LV_MEM_POOL_INCLUDE #undef LV_MEM_POOL_ALLOC #endif#endif /*LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN*/我这里使用的是外部SDRAM作为lvgl的显存地址,大小为300KB。如果启动时白屏,优先考虑增加LV_MEM_SIZE的大小,至少为64KB。如果增加至64KB还未能解决卡死的问题,如果原本现存位置是MCU内部SRAM,考虑将显存移至外部SDRAM;如果原本是外部SDRAM,考虑移动至内部SRAM。
- 检查
lv_port_disp.c和lv_port_indev.c
确认以下项设置正确:
- 屏幕分辨率:
#ifndef MY_DISP_HOR_RES #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now. #define MY_DISP_HOR_RES 1024#endif
#ifndef MY_DISP_VER_RES #warning Please define or replace the macro MY_DISP_VER_RES with the actual screen height, default value 240 is used for now. #define MY_DISP_VER_RES 600#endif屏幕分辨率如果比实际屏幕大,会导致花屏;如果比实际小,可能导致显示错位。
-
disp_flush()是否在初始化后被正确调用 -
颜色深度是否选择正确,lvgl默认为RGB565
-
是否选择了正确的输入设备,没有用到的输入设备所对应的初始化代码要注释掉。
如果LVGL初始化正确并进入了空白屏幕,那么至少屏幕左下角和右下角会分别显示内存监视和CPU监视的小窗口,且CPU监视窗口的数字应当是在动态变化的。
切换页面时卡死或花屏
通常认为,有两种切换屏幕的方式:
- 通过
lv_obj_clean或lv_obj_del删除旧页面,然后lv_scr_load创建新页面。比如:
lv_obj_t *act_scr = lv_scr_act(); lv_obj_del(act_scr); create_main_scr(); lv_scr_load(main_scr);- 通过向页面添加或移除
LV_OBJ_FLAG_HIDDEN来控制页面的可视化,这么做需要事先一次性创建完所有可能需要显示的页面并添加flag,比如:
lv_obj_add_flag(main_screen, LV_OBJ_FLAG_HIDDEN); /* 隐藏页面 */ lv_obj_clear_flag(main_screen, LV_OBJ_FLAG_HIDDEN); /* 显示页面 */建议:不要使用第二种通过控制FLAG_HIDDEN来实现切换页面的方法,除非你真的确认第一种方法没法使用。因为:
- 初始化时一次性创建大量页面及其子类对象会极大的增加CPU和内存负担。应尽可能降低同时操作多个对象的可能性。
- 浪费内存,造成内存泄漏,页面管理混乱。
- 在LVGL v9及以上版本使用时,大概率导致花屏(很多例子),推测是多个对象叠加带来的内存问题。
如果修改为第一种方法后还出现错误,参照以下步骤:
-
确保LVGL版本为最新。LVGL v9中
lv_scr_load()函数的执行步骤是:- 获取当前活动屏幕和其关联的显示器对象
- 判断当前屏幕是否为待切换的目标屏幕,如果是,退出函数
- 判断是否有切换动画正在执行,如果有,立即切换至目标屏幕
- 切换后,删除旧屏幕,更新
acr_scr指针至目标屏幕
这里的切换逻辑已经写的非常合理了,如果还是有错误:
-
确保使用
lv_obj_del()而不是lv_obj_clean()来删除旧屏幕。两个函数的功能有区别,lv_obj_del()会立即删除对象本身和其子项,而lv_obj_clean()仅删除目标对象的子项,而不删除目标对象本身,这可能造成内存泄漏。 -
调换
lv_obj_del()和lv_scr_load()的位置,即先创建新屏幕,再删除旧屏幕。之前有Github issue报告称在创建新屏幕前就删除旧屏幕可能导致内存池问题,lvgl会访问空指针导致hardfault。 -
确保旧屏幕(包括其子项)和新屏幕(包括其子项)之间的对象依赖关系正确。具体一点来说:
- 新屏幕必须已经被创建(父对象必须实际存在且为静态)
- 新屏幕中的子项不能指向、包含或引用不存在的对象。这么说的意思是,如果你是在切换屏幕时才创建新屏幕中的对象,那么这个对象在切换屏幕前是不存在的。如果子项试图访问一个并不实际存在的对象,即访问一个空指针,那么LVGL就会卡死。举个例子:
static void btn_to_main_page_cb(lv_event_t *e){if (lv_event_get_code(e) == LV_EVENT_CLICKED){lv_obj_t *act_scr = lv_scr_act();create_main_scr();lv_scr_load(main_scr); /* 切换至main_scr() *、`lv_obj_del(act_scr);}}static void create_main_scr(){/* ..... */lv_label_set_text_fmt(Example_label);}这个例子里,我即将切换到的目标屏幕中包含一个对
Example_label的操作,而Example_label可能是在其他屏幕初始化函数中被创建的,那么必须确保在切换前Example_label就已经存在。 -
如果启用了定时器(lv_timer),检查定时器创建位置和回调执行位置。假设
lv_timer_1用于实时更新test_scr中的某个label组件,那么lv_timer_1应当只在进入test_scr后被创建,如果其他屏幕没有用到lv_timer_1,那么lv_timer_1在退出test_scr前就应当被销毁(先销毁定时器,再删除旧屏幕),尤其是当lv_timer_1的回调函数中包括对test_scr中子对象的引用、指向和删除操作时,因为一旦退出test_scr,回调函数所指向的对象就不再存在(被删除),即指向空指针,LVGL会立即卡死。推荐使用这种方式来管理定时器的创建:
static void lvgl_create_timers_1(void){// 如果存在旧的定时器对象if (timer_1 != NULL) {lv_timer_del(timer_1); // 删除旧的定时器timer_1 = NULL; // 清空指针}// 创建新的定时器timer_1 = lv_timer_create(lvgl_timer_1_cb, 500, NULL);} -
如果到这里还没有解决问题,应使用IDE的debug功能进行单步调试,定位故障来源。下面是几个例子:
- 函数卡死在
lv_tlsf.c中的某个函数:内存管理出现问题,极大可能是访问或释放了不存在的内存(空指针),确保正确处理了对象之间的依赖关系。 - 函数卡死在
delay函数:函数停在这里是正常现象,这里的卡死指的是一直在delay中循环而不跳出,考虑时钟问题,可能的情况下降低屏幕时钟频率。 - 函数跳转至
hardfault_handler():非LVGL自身原因带来的问题,必定是用户的代码产生了错误。
- 函数卡死在
触发组件回调时卡死
- 不要在回调中使用
while、for或长时间的延时 - 不要在回调函数中访问已经被删除或无效的对象,参加上一小节中的第5条。这一条看似不起眼,但很多人都不会意识到这个问题
- LVGL不是线程安全的。如果在OS中使用LVGL,考虑为回调函数添加标志位或互斥锁,避免竞态条件
- 尽可能地优化代码,不要嵌套过多的API或一次性创建大量对象,github issue中有因为这么干而造成栈溢出的。
长时间运行后卡死
99%是内存问题。确保创建新对象后删除旧对象并释放内存。
后话
最后,一些好习惯,能够高效的进行debug:
- 对LVGL申请的关键内存添加
static,尤其是各种缓冲区 - 发生错误时,从父类到子类、从大到小、从前到后依次定位故障来源,着重关注“动态阶段”,也就是创建对象、删除对象、刷新对象等等的这些操作
- 善用搜索功能和官方文档