前言

在LVGL中,大多情况下是有多个页面的,一般来说页面的切换有两种情况:

  1. 删除当前的页面,创建新页面加载
  2. 保留当前的页面,创建新页面加载

我们来分析一下这两种情况,比如页面1有一个列表框,有三个选项,每个选项对应进入一个页面。假设此时我们的焦点落在第二个选项上,然后点击进入页面2,我们要是想返回,我们是希望焦点还是保留在列表的第二个选项上的。基于这种情况来说,我们希望能够在保留页面1的同时(同时也保留了焦点),创建页面2并加载。同时对于页面2来说,只是想用户选择的时候再加载,每次进入都处于初始状态,所以在从页面2返回页面1时,我们会希望删除页面2,再创建页面1并加载。

所以这两种切换方式我们都需要了解,下面就来看看在LVGL中如何完成页面切换。

页面切换原理和页面管理实现

1 页面切换函数

首先,无论对于哪种情况,我们都需要创建一个新页面进行加载。在LVGL中是采用函数lv_scr_load来加载一个新页面的,它的调用关系如下:

1
2
3
4
lv_scr_load(scr)
lv_disp_load_scr(scr)
lv_scr_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 0, 0, false);

这里我们不对lv_scr_load_anim进行深入分析,我们只需要知道这个函数最终可以用来显示一个页面就行了,其中anim表示animation,即页面加载时可以有一些动画。lv_scr_load函数则是不用任何动画而是直接加载页面。所以我们也可以直接使用lv_scr_load_anim进行加载页面。

有了这个函数,实际上页面切换就很简单了,对于每个页面来说,一个lv_obj_t基础对象表示一个页面,我们只需要让所有的组件都以这个页面lv_obj_t为父类即可,然后用lv_scr_load_anim函数进行加载就行了。

隐藏页面切换法?
隐藏页面切换法就是调用lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN)lv_obj_clear_flag(obj, LV_OBJ_FLAG_HIDDEN)来隐藏/显示页面以实现页面的切换。如果想多个页面显示在一起,即有上下层叠的关系,可以指定所有页面有一个共有的父类。

但对于lv_scr_load_anim函数来说,仅支持单个页面的显示,参数是哪个页面,显示的就是哪个页面,即使上一个页面没有被删除,也不会被显示出来。如果将所有的页面都建立于一个lv_obj上,不仅浪费内存,而且页面之间的管理也非常乱。所以在我看来,没有什么所谓的隐藏页面切换法。

2 页面切换的条件及原因

对于实际的页面切换来说,我们还需要考虑一些事,比如从当前页面切换出去,是要删除还是保留当前页面及其所有子对象。我们就以GUI Guider生成的代码为例,看看它是如何删除页面、如何调用lv_scr_load_anim进行页面的切换的,毕竟这种软件上生成的代码肯定是非常严谨的。


这里插一句嘴,网上很多信息都不太严谨,我之前下了一个LVGL源码学习,在单片机中运行没问题,但在Codeblocks模拟就有可能会崩溃。这并不是CodeBlocks不兼容,而是代码本身就有内存越界访问的问题。

还有比如BootLoader,如果开启了某个外设的中断、Cache、GPIO时钟等,在退出BootLoader前一定要关掉,即恢复之前的状态。我有亲身经历,同事的BootLoader在退出前没有关串口的中断,然后进入APP后一个常量字符串的某一位莫名其妙的被修改了。

网上很多内容都是不严谨的,可能表面上确实可以用,但也留下了一些潜在的错误。我的建议是参考官方SDK或Github中Star很多的代码。


如下图所示,我们先创键两个页面:

我们希望页面一的按钮按下后切换到页面二,页面二的按钮按下后切换到页面一。在GUI Guider中选中按钮,在右侧添加Events,如下图所示,左边为页面一的Events,右边为页面二的Events。

以页面一为例,上图表示按钮按下后,将载入页面二。另外,我们注意到,在GUI Guider中,还有一个Delete current screen的选项,而且选上之后又出现一个Free memory of current screen before loading new screen选项。三种情况下,点击按钮后切换页面的代码如下所示:

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
27
28
29
30
31
32
33
34
/* 未选中Delete current screen */
lv_obj_t * act_scr = lv_scr_act();
lv_disp_t * d = lv_obj_get_disp(act_scr);
if (d->prev_scr == NULL && (d->scr_to_load == NULL || d->scr_to_load == act_scr))
{
if (guider_ui.screen_2_del == true)
setup_scr_screen_2(&guider_ui);
lv_scr_load_anim(guider_ui.screen_2, LV_SCR_LOAD_ANIM_NONE, 100, 100, false);
guider_ui.screen_1_del = false;
}

/* 选中Delete current screen */
lv_obj_t * act_scr = lv_scr_act();
lv_disp_t * d = lv_obj_get_disp(act_scr);
if (d->prev_scr == NULL && (d->scr_to_load == NULL || d->scr_to_load == act_scr))
{
if (guider_ui.screen_2_del == true)
setup_scr_screen_2(&guider_ui);
lv_scr_load_anim(guider_ui.screen_2, LV_SCR_LOAD_ANIM_NONE, 100, 100, true);
guider_ui.screen_1_del = true;
}

/* 选中Delete current screen和Free memory of current screen before loading new screen */
lv_obj_t * act_scr = lv_scr_act();
lv_disp_t * d = lv_obj_get_disp(act_scr);
if (d->prev_scr == NULL && (d->scr_to_load == NULL || d->scr_to_load == act_scr))
{
lv_obj_clean(act_scr);
if (guider_ui.screen_2_del == true)
setup_scr_screen_2(&guider_ui);
lv_scr_load_anim(guider_ui.screen_2, LV_SCR_LOAD_ANIM_NONE, 100, 100, true);
guider_ui.screen_1_del = true;
}

可以看出如果设置待加载的页面为要删除,则需要在每次加载新页面前,都需要调用setup_scr_screen_2来重新初始化页面中的组件,然后lv_scr_load_anim的最后一个参数auto_del设置为true,表示加载后自动删除原页面中的对象。如果想要在加载新页面前就把原页面中的内容释放掉,调用lv_obj_clean即可。

现在再来分析一下上面代码的共同特征:

(1)lv_obj_t * act_scr = lv_scr_act():获取当前屏幕中正在显示的页面的lv_obj_t类型的指针
(2)lv_disp_t * d = lv_obj_get_disp(act_scr);:获取当前屏幕对象关联的显示器对象
(3)d->prev_scr == NULL
LVGL支持页面切换动画,如从左到右切换,prev_scr就用作于此。当prev_scr不为NULL时,表示当前屏幕对象正在进行切换动画,并且可以通过prev_scr引用到前一个屏幕对象。当prev_scrNULL时才能切换页面,这样做是为了防止在动画执行过程中对前一个屏幕对象进行删除操作,以及避免可能的资源冲突或不一致性。
(4)(d->scr_to_load == NULL || d->scr_to_load == act_scr)
只有在scr_to_load 等于NULL或等于act_scr时才能切换页面。NULL表示当前没有在执行切换页面的动画(已经执行完毕),act_scr表示当前正处于从别的页面切换为当前页面的过程中。其它情况下,如当前页面1(act_scr)正准备切换到页面2时,执行切换到页面3是不被允许的。
(5)lv_obj_clean
表示在载入新的页面之前,删除当前页面的所有子对象。但当lv_scr_load_anim的最后一个参数为true时,会在下一次切换屏幕时调用lv_obj_del删除前一个页面及其子对象。但实际上我们先删除其子对象也不影响动画切换的效果,这是因为子对象已经绘制在act_scr中了,我们只需要当前页面的一个显示状态即可完成动画的过渡效果。

这里我仅仅是用文字对函数的执行过程进行了描述,具体的原理请参考lv_disp.c。如果有必要的话,我会写一篇源码分析的文章。

3 页面切换的模块化实现

从上面可以发现,其实页面切换就是调用那几行代码,但是在每个页面的回调函数中都写那几行又感觉有点冗余,所以干脆我们就写一个页面切换的函数,将所有页面管理起来,调用函数进行相互之间的切换。
(1)定义页面结构体PageStruct_t和页面标识符PAGE_ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef enum
{
PAGE_NULL = -1,
/* 假设有两个页面:LOGO和MAIN,增加页面后自行在此添加对应的枚举类型 */
PAGE_LOGO,
PAGE_MAIN,
}PAGE_ID;

typedef void(*page_init_handle)(lv_obj_t* root);
typedef struct
{
uint8_t page_id;
lv_obj_t* root;
page_init_handle init_handler; //页面中所有组件的初始化函数,以root为父对象
}PageStruct_t;

(2)添加和创建页面
这里实现了一个链表,来管理每个界面,链表实现很普遍而代码量很长,就不贴代码了。page_findlist_rpush为链表相关函数。

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
27
28
29
30
31
32
static void page_add(PageStruct_t* page)
{
/* 遍历链表看是否page_id是否被加入过 */
list_node_t* node = page_find(page->page_id);
if(node != NULL)
{
PRINTF("page has been added!\r\n");
return;
}
node = list_rpush(page_list, list_node_new(page));
if(node != NULL)
{
PRINTF("page %d adds successfully!\r\n", page->page_id);
}
}

static PageStruct_t* page_create(uint8_t id)
{
PageStruct_t* page = NULL;
list_node_t* node = page_find(id);
if(node != NULL)
{
page = node->val;
page->root = lv_obj_create(NULL);
/* 每个页面的大小与LCD屏幕大小对应 */
lv_obj_set_size(page->root, DEMO_PANEL_WIDTH, DEMO_PANEL_HEIGHT);
page->init_handler(page->root);
}

return page;
}

(3)页面切换
页面切换实际上就是在刚刚GUI Guider生成的代码的基础上加上了链表的查询操作。这里当del为1时,默认在切换页面前都调用lv_obj_clean把当前页面的子对象清理掉。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* @param:arg 页面ID
* @param:del 页面内存释放标志
*/
void page_callback(PAGE_ID arg, lv_scr_load_anim_t animation, bool del)
{
uint8_t id = (uint8_t)arg;
PageStruct_t *page;
lv_obj_t *act_scr = lv_scr_act();
lv_disp_t *d = lv_obj_get_disp(act_scr);
list_node_t* p_node = page_find_root(act_scr);

if(p_node == NULL)
{
return;
}
page = p_node->val;

if(d->prev_scr == NULL && (d->scr_to_load == NULL || d->scr_to_load == act_scr))
{
if(del)
{
lv_obj_clean(act_scr);
page->root = NULL;
}
/* 根据待切换屏幕的page_id找到其结构体 */
p_node = page_find(id);
if(p_node == NULL) return;
//page_cur = id;
page = p_node->val;
/* 如果屏幕中的组件被删除了,重新创建 */
if(page->root == NULL)
{
page_create(id);
if(page->root == NULL) return;
}
/* 加载新页面:这里固定了animation的时间,如有必要可以在每个页面结构体里添加这些参数 */
lv_scr_load_anim(page->root, animation, 150, 50, del);
}
}


(4)LOGO页面添加例子

1
2
3
4
5
6
7
8
static PageStruct_t page_logo = {
.page_id = PAGE_LOGO,
.init_handler = gui_logo_init, //初始化页面中的各个组件的函数
.root = NULL,
};
page_add(&page_logo);
page_callback(PAGE_LOGO, LV_SCR_LOAD_ANIM_MOVE_LEFT, 0);

本文就不贴完整的代码了,本文主要是介绍页面切换部分的函数。至于页面之间的管理,实际上就缺一个链表的实现。大家自己去github找一个链表实现一下,同时也有助于理解代码。对于页面切换时焦点保存和恢复的问题,可以参考LVGL学习(5):物理按键切换焦点之焦点保存和恢复

界面切换以及显示优化

一 界面切换

利用lvgl框架绘制GUI免不了需要实现多个页面的切换,毕竟把所有功能和接口都放在一页上有些不太优雅,而且对于嵌入式硬件的小屏幕也有些过于困难。因此这里就需要实现多个页面(或者说lvgl里的screen)及其互相切换。

实现方案(参考文章《LVGL V8应用——通过按键切换页面》),有以下两类实现方案:

【1】通过LV_OBJ_FLAG_HIDDEN隐藏属性实现页面切换:

显示窗口1之前给窗口2的容器设置LV_OBJ_FLAG_HIDDEN隐藏属性,显示窗口2之前给窗口1的容器设置LV_OBJ_FLAG_HIDDEN隐藏属性。

【2】切换到窗口2时,删除窗口1的容器:

这边的具体实现也可以分为两种方法:

  1. 切换到窗口2时,可以在回调函数中删除窗口1的容器,这样可以删除窗口1下的所有子对象,这种方式适用于运存较小的设备。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static void win_btn_event_callback1(lv_event_t* event)
    {
    lv_event_code_t code = lv_event_get_code(event);
    if (code == LV_EVENT_CLICKED)
    {
    // 删除窗口1的win1_contanier容器,这样就可以把win1_contanier上的子对象全部删除,然后显示窗口2
    lv_obj_del(win1_contanier);
    show_button_win2();
    }
    }


    static void win_btn_event_callback2(lv_event_t* event)
    {
    lv_event_code_t code = lv_event_get_code(event);
    if (code == LV_EVENT_CLICKED)
    {
    // 删除窗口2的win2_contanier容器,这样就可以把win2_contanier上的子对象全部删除,然后显示窗口1
    lv_obj_del(win2_contanier);
    show_button_win1();
    }
    }
  2. 使用函数lv_scr_load_anim和lv_scr_load加载和切换屏幕:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /* 直接加载该屏幕scr
    参数1:要加载的屏幕 */
    static inline void lv_scr_load(lv_obj_t * scr)
    {
    lv_disp_load_scr(scr);
    }

    /* 通过动画的方式显示
    参数1:要加载的屏幕
    参数2:动画类别
    参数3:切换动画需要的时间
    参数4:为true时则会在切换界面后将旧界面删除,节约内存*/
    void lv_scr_load_anim(lv_obj_t * scr, lv_scr_load_anim_t anim_type, uint32_t time, uint32_t delay, bool auto_del);

我这边用到的是第二类方式,即GUI Guider中自动生成的事件+加载页面动作的方式:

二 显示优化

因为用到界面的切换,涉及整屏(320*240)的刷新,如果绘制较为复杂的画面,界面切换的过程中会有拉窗帘的效果,非常影响美观。所以显示的优化就不得不考虑了。

参考《LVGL显示优化—基本优化》、《LVGL 优化帧率技巧》,有以下几种优化方法以供参考:

【1】disp_flush刷屏方法改进

这一点之前移植的时候也提过(《【LVGL】学习笔记–(1)Keil中嵌入式系统移植LVGL》),在disp_flush中用封装好的LCD_Fill_LVGL刷屏函数替代原先默认的画点函数。

【2】LVGL帧率限制

LVGL是有一个帧率刷新周期的宏定义,在lv_conf.h里。LVGL会通过LVGL内部的tick,定时去刷屏幕,也就是说该宏定义限定了LVGL刷屏帧率的上限,默认满帧33帧。

这里我们直接改成10ms刷新一次,满帧100帧。顺手再把输入设备读取周期也改成10ms。

1
2
3
4
5
/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 10 /*[ms]*/ //默认30

/*Input device read period in milliseconds*/
#define LV_INDEV_DEF_READ_PERIOD 10 /*[ms]*/ //默认30

【3】LV_DPI_DEF配置

在lv_conf.h里配置DPI:

1
2
3
4
5
/*
* LV_DPI_DEF 注意这里,虽然LVGL的作者说这个没这么重要,但他会严重影响到LVGL的动画效果
* 你应该进行DPI的手动计算,例如320*240分辨率3.2英寸的屏幕,那么 DPI = (320*320+240*240)^0.5 / 3.2 = 125
*/
#define LV_DPI_DEF 125 /*[px/inch]*/

【4】编译器版本以及代码优化等级

编译器版本尽量用arm compiler5。代码优化等级可以提高到Level 2:

【5】堆栈设置

  1. 栈设置:

    官方推荐:

    在启动文件startup_hc32f460.s中设置大一点,我这边设置为8K:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Stack_Size      EQU     0x00002000

    AREA STACK, NOINIT, READWRITE, ALIGN=3
    Stack_Mem SPACE Stack_Size
    __initial_sp


    ; <h> Heap Configuration
    ; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
    ; </h>

    Heap_Size EQU 0x00000200

    AREA HEAP, NOINIT, READWRITE, ALIGN=3
    __heap_base
    Heap_Mem SPACE Heap_Size
    __heap_limit


    PRESERVE8
    THUMB
  2. 堆设置:
    lv_conf.h中的LV_MEM_SIZE进行设置,上面推荐大于32K,我这边设为了48K:

    1
    2
    /*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
    #define LV_MEM_SIZE (48U * 1024U) /*[bytes]*/

【6】显存类型

LVGL其实提供了三种显存类型。三种分别对应不同环境,都可以试试 (B站有大神进行过三种类型的测试LVGL 最优配置测试 (STM32),可以参考一下,我自己测了单BUFFER和非全尺寸双BUFFER,区别不大)。先看下结论:

  1. 单BUFFER
    画面会被拆分成buffer的size大小,分块刷新,当只有局部刷新时,比如说点击了一个按钮按钮变高亮,那么他就只会刷局部画面。官方推荐 1/10 screen size,拆分太小,刷屏接口又慢的话,不仅帧率低,还会有感人的拉窗帘。整屏幕刷速度快是快,但是太占内存了。我这边调整为1/4全屏,效果还不错:

    1
    2
    3
    4
    /* 单缓冲区示例) */
    static lv_disp_draw_buf_t draw_buf_dsc_1;
    static lv_color_t buf_1[MY_DISP_HOR_RES * MY_DISP_VER_RES / 4];
    lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * MY_DISP_VER_RES / 4); /*Initialize the display buffer*/
  2. 非全尺寸双BUFFER
    顾名思义,两个buffer,实际上就是PING-PONG模式,这种情况适合有dma或者其他显存加速设备的芯片,在一块buffer使用dma等外设后台刷屏时,lvgl可以再前台渲染显示进另一块显存,也就是说渲染和刷屏并发执行,理论是比单显存要好的,不过跟硬件和屏幕大小有关,具体要实测。

  3. 全尺寸双BUFFER
    两个全尺寸buffer,虽然直觉看上去和非全尺寸双buffer应该区别不大,但实际上,这个是专门为带TFT驱动的MCU准备的。双全尺寸与前两种不同,在任何时候,给出的buffer都是全屏大小的buffer,而不是局部刷新只提供刷新部分的buffer窗口,这么做的的好处是刷屏,只需要更改TFT驱动的显存地址,而不需要将LVGL的buffer再搬运到TFT驱动的显存,就可完成刷屏动作。

以上。


相关链接(侵删)

  1. (这个博主技术厉害!)LVGL学习(3):页面切换原理和页面管理实现
  2. (这个也厉害)【LVGL】学习笔记–(3)界面切换以及显示优化

=================我是分割线=================

欢迎到公众号来唠嗑: