LVGL发生各种卡死现象的原因分析和排故
前言
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 |
|
启动时卡死、白屏、花屏
非常常见的情况。首先在初始化后移除所有的对象创建函数,仅保留一个最基本的创建空白屏幕的函数,这么做是为了排除其他对象引起的干扰因素。
然后,按照下列顺序检查:
- 硬件问题。
- 确保屏幕和MCU硬件连接正确。最好的方式是先不跑LVGL,而是用一个最基本的填色函数来测试屏幕是否工作正常。
- 确保供电满足要求
- 确保时钟源正确配置
- 检查MCU启动文件中栈和堆大小是否设置正确。 LVGL正常运行至少需要以下条件:
1 |
|
- 检查
lv_conf.h
中内存缓存地址及大小是否设置正确:
1 |
|
我这里使用的是外部SDRAM作为lvgl的显存地址,大小为300KB。如果启动时白屏,优先考虑增加LV_MEM_SIZE
的大小,至少为64KB。如果增加至64KB还未能解决卡死的问题,如果原本现存位置是MCU内部SRAM,考虑将显存移至外部SDRAM;如果原本是外部SDRAM,考虑移动至内部SRAM。
- 检查
lv_port_disp.c
和lv_port_indev.c
确认以下项设置正确:
- 屏幕分辨率:
1 |
|
屏幕分辨率如果比实际屏幕大,会导致花屏;如果比实际小,可能导致显示错位。
-
disp_flush()
是否在初始化后被正确调用 -
颜色深度是否选择正确,lvgl默认为RGB565
-
是否选择了正确的输入设备,没有用到的输入设备所对应的初始化代码要注释掉。
如果LVGL初始化正确并进入了空白屏幕,那么至少屏幕左下角和右下角会分别显示内存监视和CPU监视的小窗口,且CPU监视窗口的数字应当是在动态变化的。
切换页面时卡死或花屏
通常认为,有两种切换屏幕的方式:
- 通过
lv_obj_clean
或lv_obj_del
删除旧页面,然后lv_scr_load
创建新页面。比如:
1 |
|
- 通过向页面添加或移除
LV_OBJ_FLAG_HIDDEN
来控制页面的可视化,这么做需要事先一次性创建完所有可能需要显示的页面并添加flag,比如:
1 |
|
建议:不要使用第二种通过控制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就会卡死。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13static 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会立即卡死。推荐使用这种方式来管理定时器的创建:
1
2
3
4
5
6
7
8
9
10
11static 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
,尤其是各种缓冲区 - 发生错误时,从父类到子类、从大到小、从前到后依次定位故障来源,着重关注“动态阶段”,也就是创建对象、删除对象、刷新对象等等的这些操作
- 善用搜索功能和官方文档