LVGL发生各种卡死现象的原因分析和排故

前言

LVGL是一个在使用时非常容易产生卡死故障的GUI库,大多数卡死都发生在动态阶段,包括且不仅限于:

  • 启动并初始化第一个页面时
  • 在多个页面间来回切换
  • 触发组件回调时(例如button或timer)
  • 长时间运行后

本文所提供的解决方案就是我自己摸索出来的,不一定适用于所有人,也不一定是最标准的办法。网上关于LVGL的教程很多,包括官方文档,但是很多方法都是不怎么规范的。官方的Github issue中也有很多奇奇怪怪的问题,哪怕是LVGL的作者也无法解决或定位错误。但是LVGL已经迭代到了V9版本,大多数因为自带库问题导致的错误已经得到修复,由LVGL本身带来的致命错误已经很少,大多数错误都来源于用户自身的不规范代码或者是用法。

如果你的LVGL例程发生了卡死,首先应当按照以下步骤尝试解决:

  1. 更新LVGL版本至最新。同时,确保你使用的函数在最新的LVGL中仍然可用。LVGL库函数迭代的很快,且自从v8.3之后几乎每个小版本都有大改,v9之后更是删了一大批我认为很好用的功能(比如lv_label_recolor())。如果你抄了一个旧版本的函数并在新版本的lvgl环境下使用它,可能导致问题
  2. 尝试在模拟器中复现代码,确认是否是LVGL库自身的问题
  3. 如果LVGL工作在OS下,尝试在裸机中复现问题;如果裸机和OS下都发生问题,说明部分代码可能存在问题;如果裸机下工作正常,大概率是线程问题导致的LVGL错误。

如果尝试过上述方法之后问题还没有解决,应考虑是用户代码造成的问题。

排故

在开始前,确保在lv_conf.h中启用内存监视和CPU占用监视,具体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*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*/

启动时卡死、白屏、花屏

非常常见的情况。首先在初始化后移除所有的对象创建函数,仅保留一个最基本的创建空白屏幕的函数,这么做是为了排除其他对象引起的干扰因素。

然后,按照下列顺序检查:

  1. 硬件问题。
  • 确保屏幕和MCU硬件连接正确。最好的方式是先不跑LVGL,而是用一个最基本的填色函数来测试屏幕是否工作正常。
  • 确保供电满足要求
  • 确保时钟源正确配置
  1. 检查MCU启动文件中栈和堆大小是否设置正确。 LVGL正常运行至少需要以下条件:
1
2
3
4
5
6
7
8
9
10
11
12
13
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
  1. 检查lv_conf.h中内存缓存地址及大小是否设置正确:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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。

  1. 检查lv_port_disp.clv_port_indev.c

确认以下项设置正确:

  • 屏幕分辨率:
1
2
3
4
5
6
7
8
9
#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_cleanlv_obj_del删除旧页面,然后lv_scr_load创建新页面。比如:
1
2
3
4
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,比如:
1
2
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及以上版本使用时,大概率导致花屏(很多例子),推测是多个对象叠加带来的内存问题。

如果修改为第一种方法后还出现错误,参照以下步骤:

  1. 确保LVGL版本为最新。LVGL v9中lv_scr_load()函数的执行步骤是:

    1. 获取当前活动屏幕和其关联的显示器对象
    2. 判断当前屏幕是否为待切换的目标屏幕,如果是,退出函数
    3. 判断是否有切换动画正在执行,如果有,立即切换至目标屏幕
    4. 切换后,删除旧屏幕,更新acr_scr指针至目标屏幕

    这里的切换逻辑已经写的非常合理了,如果还是有错误:

  2. 确保使用lv_obj_del()而不是lv_obj_clean()来删除旧屏幕。两个函数的功能有区别,lv_obj_del()会立即删除对象本身和其子项,而lv_obj_clean()仅删除目标对象的子项,而不删除目标对象本身,这可能造成内存泄漏。

  3. 调换lv_obj_del()lv_scr_load()的位置,即先创建新屏幕,再删除旧屏幕。之前有Github issue报告称在创建新屏幕前就删除旧屏幕可能导致内存池问题,lvgl会访问空指针导致hardfault。

  4. 确保旧屏幕(包括其子项)和新屏幕(包括其子项)之间的对象依赖关系正确。具体一点来说:

    • 新屏幕必须已经被创建(父对象必须实际存在且为静态)
    • 新屏幕中的子项不能指向、包含或引用不存在的对象。这么说的意思是,如果你是在切换屏幕时才创建新屏幕中的对象,那么这个对象在切换屏幕前是不存在的。如果子项试图访问一个并不实际存在的对象,即访问一个空指针,那么LVGL就会卡死。举个例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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就已经存在。

  5. 如果启用了定时器(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
    11
    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);
    }
  6. 如果到这里还没有解决问题,应使用IDE的debug功能进行单步调试,定位故障来源。下面是几个例子:

    • 函数卡死在lv_tlsf.c中的某个函数:内存管理出现问题,极大可能是访问或释放了不存在的内存(空指针),确保正确处理了对象之间的依赖关系。
    • 函数卡死在delay函数:函数停在这里是正常现象,这里的卡死指的是一直在delay中循环而不跳出,考虑时钟问题,可能的情况下降低屏幕时钟频率。
    • 函数跳转至hardfault_handler():非LVGL自身原因带来的问题,必定是用户的代码产生了错误。

触发组件回调时卡死

  1. 不要在回调中使用whilefor或长时间的延时
  2. 不要在回调函数中访问已经被删除或无效的对象,参加上一小节中的第5条。这一条看似不起眼,但很多人都不会意识到这个问题
  3. LVGL不是线程安全的。如果在OS中使用LVGL,考虑为回调函数添加标志位或互斥锁,避免竞态条件
  4. 尽可能地优化代码,不要嵌套过多的API或一次性创建大量对象,github issue中有因为这么干而造成栈溢出的。

长时间运行后卡死

99%是内存问题。确保创建新对象后删除旧对象并释放内存。

后话

最后,一些好习惯,能够高效的进行debug:

  • 对LVGL申请的关键内存添加static,尤其是各种缓冲区
  • 发生错误时,从父类到子类、从大到小、从前到后依次定位故障来源,着重关注“动态阶段”,也就是创建对象、删除对象、刷新对象等等的这些操作
  • 善用搜索功能和官方文档

LVGL发生各种卡死现象的原因分析和排故
http://akichen891.github.io/2025/02/20/LVGL发生各种卡死现象的原因分析和排故/
作者
Aki
发布于
2025年2月20日
更新于
2025年2月20日
许可协议