第五章 LVGL移植的相关知识
在前面的章节里面,我们已经成功地移植了LVGL到开发板当中,但并没有介绍移植过程中的一些知识点,所以本章主要讲解LVGL移植相关的一些知识点。
本章节将分为以下几个小节:
5.1 LVGL初始化流程
5.2 lv_conf.h文件解析
5.3 显示接口
5.4 输入设备
5.5 LVGL时基
5.6 LVGL任务处理
5.7 拓展知识
5.1 LVGL初始化流程
在学习LVGL移植相关知识之前,我们需要先简单地了解一下LVGL的初始化流程,这样即可知道整个初始化过程中所涉及的一些关键配置,而这些关键配置是在移植过程中需要重点关注的。LVGL初始化流程分为以下几个步骤:
第一步:调用 lv_init函数,初始化LVGL图形库。 在这一步的初始化中,涉及的内容非常的多,包括:内存管理、文件系统、定时器,等等。
第二步:调用lv_port_disp_init函数和lv_port_indev_init函数,注册显示设备和输入设备。注意: 在注册显示设备和输入设备之前,必须先初始化LVGL图形库(调用lv_init函数)。
第三步:为LVGL提供时基。用户可以使用定时器,在其中断里面定时调用lv_tick_inc函数,为LVGL提供时基。如果工程中带有OS操作系统,则可以使用相应的时钟函数来为LVGL提供时基。
第四步:定时处理LVGL任务。用户需要每隔几毫秒调用一次lv_timer_handler函数,以处理LVGL 相关的任务,该函数可以放在while循环中,但延时不宜过大,需要确保5 毫秒以内。
5.2 lv_conf.h文件解析
首先,我们需要明确一个点:lv_conf.h是一个用户级别的文件,它不属于内核的部分,因此,在不同的工程中,该文件有可能存在差异。lv_conf.h文件具有两大功能:
(1) 配置功能:内存、屏幕刷新周期、输入设备的读取周期,等等;
(2) 裁剪功能:使能/失能某些功能,有效地优化Flash的分配。
lv_conf.h文件的内容可划分为10个板块,如下表所示:

表5.2.1 lv_conf.h文件内容描述
接下来,我们重点讲解lv_conf.h文件中几个比较重要板块内容。
1. 颜色设置
/* 颜色设置 */ /* 颜色深度: 1(每像素1字节), 8(RGB332), 16(RGB565), 32(ARGB8888) */ #define LV_COLOR_DEPTH 16 /* 交换2字节的RGB565颜色。如果显示有8位接口(例如SPI) */ #define LV_COLOR_16_SWAP 0 /* 1: 启用屏幕透明. * 对OSD或其他有重叠的gui很有用. * 要求' LV_COLOR_DEPTH = 32 '颜色和屏幕的样式应该被修改: `style.body.opa = ...`*/ #define LV_COLOR_SCREEN_TRANSP 0 /* 调整颜色混合功能四舍五入。gpu可能会以不同的方式计算颜色混合。 * 0:取整,64:从x.75取整,128:从half取整,192:从x.25取整,254:从half取整 */ #define LV_COLOR_MIX_ROUND_OFS (LV_COLOR_DEPTH == 32 ? 0: 128) /* 如果使用色度键,将不会绘制这种颜色的图像像素) */ #define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00) /* 纯绿 */
在上述源码中,我们一般情况下只会用到以下两个宏定义:
① LV_COLOR_DEPTH宏定义,可用于设置颜色的深度,用户根据实际的显示屏颜色深度进行配置即可
② LV_COLOR_16_SWAP宏定义,可用于交换2字节的RGB565颜色,如果用户使用的是SPI的屏幕,发现颜色出现异常,可以尝试将该宏置1。
2. 内存设置
/* 0: 使用内置的 `lv_mem_alloc()` 和 `lv_mem_free()`*/ #define LV_MEM_CUSTOM 0 #if LV_MEM_CUSTOM == 0 /* `lv_mem_alloc()`可获得的内存大小(以字节为单位)(>= 2kB) */ #define LV_MEM_SIZE (100U * 1024U)/*[字节]*/ /* 为内存池设置一个地址,而不是将其作为普通数组分配。也可以在外部SRAM中。 */ #define LV_MEM_ADR 0 /*0: 未使用*/ /* 给内存分配器而不是地址,它将被调用来获得LVGL的内存池。例如my_malloc */ #if LV_MEM_ADR == 0 //#define LV_MEM_POOL_INCLUDE your_alloc_library //#define LV_MEM_POOL_ALLOC your_alloc #endif #else /*LV_MEM_CUSTOM*/ #define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /* 动态内存函数的头 */ #define LV_MEM_CUSTOM_ALLOC malloc #define LV_MEM_CUSTOM_FREE free #define LV_MEM_CUSTOM_REALLOC realloc #endif /*LV_MEM_CUSTOM*/ /* 在渲染和其他内部处理机制期间使用的中间内存缓冲区的数量。 * 如果没有足够的缓冲区,你会看到一个错误日志信息. */ #define LV_MEM_BUF_MAX_NUM 16 /* 使用标准的 `memcpy` 和 `memset` 代替LVGL自己的函数。(可能更快,也可能不会更快) */ #define LV_MEMCPY_MEMSET_STD
在上述的源码中,LVGL为用户提供了四种内存分配的方式,接下来我们分别介绍这四种内存分配 方式的实现逻辑。
①方式一(最常用):在lv_conf.h文件中把LV_MEM_CUSTOM和LV_MEM_ADR设置为0,然后注释LV_MEM_POOL_INCLUDE和LV_MEM_POOL_ALLOC配置项,如下源码所示(标红的部分):
/* 0: 使用内置的 `lv_mem_alloc()` 和 `lv_mem_free()`*/ #define LV_MEM_CUSTOM 0 #if LV_MEM_CUSTOM == 0 /* `lv_mem_alloc()`可获得的内存大小(以字节为单位)(>= 2kB) */ #define LV_MEM_SIZE (50U * 1024U) /*[字节]*/ /* 为内存池设置一个地址,而不是将其作为普通数组分配。也可以在外部SRAM中。 */ #define LV_MEM_ADR 0 /*0: 未使用*/ /* 给内存分配器而不是地址,它将被调用来获得LVGL的内存池。例如my_malloc */ #if LV_MEM_ADR == 0 //#define LV_MEM_POOL_INCLUDE your_alloc_library //#define LV_MEM_POOL_ALLOC your_alloc #endif #else /*LV_MEM_CUSTOM*/ #define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /* 动态内存函数的头 */ #define LV_MEM_CUSTOM_ALLOC malloc #define LV_MEM_CUSTOM_FREE free #define LV_MEM_CUSTOM_REALLOC realloc #endif /*LV_MEM_CUSTOM*/
该内存分配 方式是最为便捷的,它将用一个大数组来存储动态分配的数据,申请的内存大小由LV_MEM_SIZE配置项所决定,而内存的管理则由LVGL内部的内存管理算法来实现。
②方式二:在lv_conf.h文件中把LV_MEM_CUSTOM和LV_MEM_ADR配置项设置为0,然后配置LV_MEM_POOL_INCLUDE和LV_MEM_POOL_ALLOC选项,如下源码所示(标红的部分):
/* 0: 使用内置的 `lv_mem_alloc()` 和 `lv_mem_free()`*/ #define LV_MEM_CUSTOM 0 #if LV_MEM_CUSTOM == 0 /* `lv_mem_alloc()`可获得的内存大小(以字节为单位)(>= 2kB) */ #define LV_MEM_SIZE (50U * 1024U) /*[字节]*/ /* 为内存池设置一个地址,而不是将其作为普通数组分配。也可以在外部SRAM中。 */ #define LV_MEM_ADR 0 /*0: 未使用*/ /* 给内存分配器而不是地址,它将被调用来获得LVGL的内存池。例如my_malloc */ #if LV_MEM_ADR == 0 #define LV_MEM_POOL_INCLUDE "./MALLOC/malloc.h" #define LV_MEM_POOL_ALLOC lv_mymalloc #endif #else /*LV_MEM_CUSTOM*/ #define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /* 动态内存函数的头 */ #define LV_MEM_CUSTOM_ALLOC malloc #define LV_MEM_CUSTOM_FREE free #define LV_MEM_CUSTOM_REALLOC realloc #endif /*LV_MEM_CUSTOM*/
使用此内存分配 方式时,内存的分配将由LV_MEM_POOL_ALLOC 配置项所指向的lv_mymalloc函数接管,LV_MEM_POOL_INCLUDE配置项指向lv_mymalloc函数的声明路径。
注意:该函数所的申请内存大小将由LV_MEM_SIZE配置项决定,而内存的管理则由LVGL内部的内存管理算法来实现。
③方式三:在lv_conf.h文件中把LV_MEM_CUSTOM配置项设置为0,然后注释LV_MEM_POOL_INCLUDE和LV_MEM_POOL_ALLOC配置项,最后设置LV_MEM_ADR为所需要分配的内存首地址,如下源码所示(标红的部分):
/* 0: 使用内置的 `lv_mem_alloc()` 和 `lv_mem_free()`*/ #define LV_MEM_CUSTOM 0 #if LV_MEM_CUSTOM == 0 /* `lv_mem_alloc()`可获得的内存大小(以字节为单位)(>= 2kB) */ #define LV_MEM_SIZE (50U * 1024U) /*[字节]*/ /* 为内存池设置一个地址,而不是将其作为普通数组分配。也可以在外部SRAM中。 */ #define LV_MEM_ADR 0x68000000 /*0: 未使用*/ /* 给内存分配器而不是地址,它将被调用来获得LVGL的内存池。例如my_malloc */ #if LV_MEM_ADR == 0 //#define LV_MEM_POOL_INCLUDE your_alloc_library //#define LV_MEM_POOL_ALLOC your_alloc #endif #else /*LV_MEM_CUSTOM*/ #define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /* 动态内存函数的头 */ #define LV_MEM_CUSTOM_ALLOC malloc #define LV_MEM_CUSTOM_FREE free #define LV_MEM_CUSTOM_REALLOC realloc #endif /*LV_MEM_CUSTOM*/
使用此内存分配 方式,用户可以指定一个内存的首地址,该地址可以指向内部SRAM或者外部SRAM, LV_MEM_SIZE配置项将决定分配内存的大小,而内存的管理则由LVGL内部的内存管理算法来实现。
④方式四:在lv_conf.h文件中把LV_MEM_CUSTOM配置项设置为1,然后配置自研的内存管理算法,如下源码所示(标红的部分):
/* 0: 使用内置的 `lv_mem_alloc()` 和 `lv_mem_free()`*/ #define LV_MEM_CUSTOM 1 #if LV_MEM_CUSTOM == 0 /* `lv_mem_alloc()`可获得的内存大小(以字节为单位)(>= 2kB) */ #define LV_MEM_SIZE (100U * 1024U) /*[字节]*/ /* 为内存池设置一个地址,而不是将其作为普通数组分配。也可以在外部SRAM中。 */ #define LV_MEM_ADR 0 /*0: 未使用*/ /* 给内存分配器而不是地址,它将被调用来获得LVGL的内存池。例如my_malloc */ #if LV_MEM_ADR == 0 //#define LV_MEM_POOL_INCLUDE your_alloc_library //#define LV_MEM_POOL_ALLOC your_alloc #endif #else /*LV_MEM_CUSTOM*/ #define LV_MEM_CUSTOM_INCLUDE "./MALLOC/malloc.h" /*动态内存函数的头路径*/ #define LV_MEM_CUSTOM_ALLOC lv_mymalloc /*申请内存函数*/ #define LV_MEM_CUSTOM_FREE lv_myfree /*释放内存函数*/ #define LV_MEM_CUSTOM_REALLOC lv_myrealloc /*重新分配内存函数*/ #endif /*LV_MEM_CUSTOM*/
在上述源码中,当LV_MEM_CUSTOM宏定义设置为1,则系统不再使用LVGL内部的内存管理算法,而是由自研的内存管理算法接管,所以用户必须配置相关的宏定义。这里我们以正点原子的内存管理算法为例,为大家介绍如何配置自研的内存管理算法。
首先打开正点原子malloc.c文件,添加以下源码:
/**
* @brief 分配内存(外部调用)
* @param size : 要分配的内存大小(字节)
* @retval 分配到的内存首地址.
*/
void * lv_mymalloc(uint32_t size)
{
return (void *)mymalloc(SRAMIN,size);
}
/**
* @brief 释放内存(外部调用)
* @param ptr : 内存首地址
* @retval 无
*/
void lv_myfree(void *ptr)
{
myfree(SRAMIN,ptr);
}
/**
* @brief 重新分配内存(外部调用)
* @param memx : 所属内存块
* @param *ptr : 旧内存首地址
* @param size : 要分配的内存大小(字节)
* @retval 新分配到的内存首地址.
*/
void * lv_myrealloc(void *ptr, uint32_t size)
{
return (void *)myrealloc(SRAMIN,ptr,size);
}
在上述源码中,我们对底层的内存分配、内存释放和内存重新分配函数进行了封装,以适配LVGL的内存管理接口函数格式。
接下来,在malloc.h文件中声明这些函数,即可在LVGL中进行调用,具体的配置方式请回顾上文,查看内存分配 方式四的相关源码(标红的部分)。
3. HAL(硬件抽象层)设置
/* 默认的显示刷新周期。LVGL使用这个周期重绘修改过的区域 */ #define LV_DISP_DEF_REFR_PERIOD 4 /*[ms]*/ /* 输入设备的读取周期(以毫秒为单位) */ #define LV_INDEV_DEF_READ_PERIOD 4 /*[ms]*/ /* 使用自定义tick源,以毫秒为单位告诉运行时间。它不需要手动更新 `lv_tick_inc()` */ #define LV_TICK_CUSTOM 1 #if LV_TICK_CUSTOM #define LV_TICK_CUSTOM_INCLUDE "FreeRTOS.h" /* 系统时间函数头 */ /* 计算系统当前时间的表达式(以毫秒为单位) */ #define LV_TICK_CUSTOM_SYS_TIME_EXPR (xTaskGetTickCount()) #endif /* LV_TICK_CUSTOM */ /* 默认每英寸的点数量。用于初始化默认大小,例如小部件大小,样式填充。 * (不是很重要,你可以调整它来修改默认大小和空格) */ #define LV_DPI_DEF 130 /*[px/inch]*/
在上述源码中,LV_DISP_DEF_REFR_PERIOD宏定义用于配置显示设备的刷新周期,LV_INDEV_DEF_READ_PERIOD宏定义用于配置输入设备读取周期,当LV_TICK_CUSTOM宏定义为1时,可使用RTOS为LVGL提供时基,用户只需要配置好头文件以及节拍获取的函数即可。
4. 特征设置
/*------------- * 3. 日志 *-----------*/ /* 启用日志模块 */ #define LV_USE_LOG 0 #if LV_USE_LOG /*应该添加多重要的日志: *LV_LOG_LEVEL_TRACE 大量的日志给出了详细的信息 *LV_LOG_LEVEL_INFO 记录重要事件 *LV_LOG_LEVEL_WARN 如果发生了一些不想要的事情但没有引起问题,则记录下来 *LV_LOG_LEVEL_ERROR 只有在系统可能出现故障时才会出现关键问题 *LV_LOG_LEVEL_USER 仅用户自己添加的日志 *LV_LOG_LEVEL_NONE 不要记录任何内容*/ #define LV_LOG_LEVEL LV_LOG_LEVEL_WARN /*1: 使用'printf'打印日志; *0: 用户需要用' lv_log_register_print_cb() '注册回调函数 */ #define LV_LOG_PRINTF 0 /* 在产生大量日志的模块中启用/禁用LV_LOG_TRACE */ #define LV_LOG_TRACE_MEM 1 #define LV_LOG_TRACE_TIMER 1 #define LV_LOG_TRACE_INDEV 1 #define LV_LOG_TRACE_DISP_REFR 1 #define LV_LOG_TRACE_EVENT 1 #define LV_LOG_TRACE_OBJ_CREATE 1 #define LV_LOG_TRACE_LAYOUT 1 #define LV_LOG_TRACE_ANIM 1 #endif /* LV_USE_LOG */
该板块中我们一般只用到日志部分,它可以在LVGL运行时,打印相关的信息,这对于用户调试程序,了解程序的运行情况非常有用。需要开启日志功能,请把LV_USE_LOG宏定义设置为1,然后在LV_LOG_LEVEL选项中设置日志过滤等级,最后设置LV_LOG_PRINTF宏定义为1,即可在串口中打印日志信息。注意:使用LV_LOG_LEVEL_TRACE过滤等级,将会有大量的日志被输出,这有可能会影响整个工程的运行,建议大家慎用!
5. 字体设置
/* 可以在这里声明您的自定义字体。 你也可以使用这些字体作为默认字体 它们将在全局范围内提供。如 * #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) \ * LV_FONT_DECLARE(my_font_2) */ #define LV_FONT_CUSTOM_DECLARE
该宏定义用于声明自定义的字体,如果用户需要使用自己生成的字体,可以在此声明。
6. 部件设置
/*------------- * 部件设置 *-----------*/ #define LV_USE_ARC 1 #define LV_USE_ANIMIMG 1 #define LV_USE_BAR 1 #define LV_USE_BTN 1 #define LV_USE_BTNMATRIX 1 #define LV_USE_CANVAS 1 #define LV_USE_CHECKBOX 1 #define LV_USE_DROPDOWN 1 /* 依赖: lv_label */ #define LV_USE_IMG 1 /* 依赖: lv_label */ #define LV_USE_LABEL 1 #if LV_USE_LABEL #define LV_LABEL_TEXT_SELECTION 1 /* 启用标签的选择文本*/ /* 在标签中存储一些额外的信息,以加快绘制非常长的文本 */ #define LV_LABEL_LONG_TXT_HINT 1 #endif #define LV_USE_LINE 1 #define LV_USE_ROLLER 1 /* 依赖: lv_label */ #if LV_USE_ROLLER #define LV_ROLLER_INF_PAGES 7 /* 当滚轮无限时,额外的“页数” */ #endif #define LV_USE_SLIDER 1 /* 依赖: lv_bar*/ #define LV_USE_SWITCH 1 #define LV_USE_TEXTAREA 1 /* 依赖: lv_label*/ #if LV_USE_TEXTAREA != 0 #define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /*ms*/ #endif #define LV_USE_TABLE 1
上述源码用于控制相应部件的使能与失能,当某个部件的宏定义设置为1时,则使能该部件;设置为0则失能该部件。这些配置项能有效地优化Flash的分配。
5.3 显示接口
在绘制UI之前,LVGL 必须要注册一个绘制缓冲区以及显示驱动,该显示驱动可以把像素阵列(在缓冲区中)复制到显示器的指定区域。
5.3.1 绘制缓冲区
绘制缓冲区是 LVGL 用来渲染屏幕内容的简单数组。一旦渲染好,绘制缓冲区的内容就会使用flush_cb中的回调函数发送到显示器。
接下来,我们结合源码分析绘制缓冲区的三种初始化方式,具体源码如下所示:
#define MY_DISP_HOR_RES (240) /* 屏幕宽度 */ #define MY_DISP_VER_RES (240) /* 屏幕高度 */ /* 第一种初始化方式:单缓冲 */ static lv_disp_draw_buf_t draw_buf_dsc_1; /* 设置缓冲区的大小为 10 行屏幕的大小 */ static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /* 初始化显示缓冲区 */ lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /* 第二种初始化方式:双缓冲 */ static lv_disp_draw_buf_t draw_buf_dsc_2; /* 设置缓冲区的大小为 10 行屏幕的大小 */ static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10]; /* 设置另一个缓冲区的大小为 10 行屏幕的大小 */ static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10]; /* 初始化显示缓冲区 */ lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10); /* 第三种初始化方式:全尺寸双缓冲区 */ static lv_disp_draw_buf_t draw_buf_dsc_3; /* 设置一个全尺寸的缓冲区 */ static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /* 设置另一个全尺寸的缓冲区 */ static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /* 初始化显示缓冲区 */ lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2, MY_DISP_HOR_RES * MY_DISP_VER_RES);
①单缓冲区:此时只有一个绘制缓冲区,其大小可以小于屏幕的总像素,但不建议小于“屏幕水平像素*10”。如果缓冲区较小,不足以一次性刷新全部内容,在这种情况下,整块区域将被重绘成多个部分,此时的屏幕刷新速度会变得不理想。而LVGL为了尽可能地提高刷新速度,当屏幕只有很小的区域发生变化时,它只会刷新那部分的区域。
注意:绘制缓冲区越大,对内存的占用就越多,所以在设置缓冲区大小的时候,需要考虑实际的内存是否够用。
②双缓冲区:当我们拥有两个缓冲区时,LVGL可以在一个缓冲区中进行绘制,与此同时,将另一个缓冲区的内容从后台发送到显示器(需要硬件支持,例如DMA2D),这样即可实现渲染和刷新的并行。
③全尺寸双缓冲区:全尺寸的双缓冲区对内存的占用非常高,以上述代码为例,屏幕的分辨率为240*240,颜色深度为16位(2字节),此时全尺寸双缓冲区所占的内存为:240*240*2*2= 230400字节。
5.3.2 注册显示驱动
缓冲区初始化完成后,LVGL将会注册显示驱动,具体源码如下所示:
/*----------------------------------- * 在 LVGL 中注册显示设备 *----------------------------------*/ static lv_disp_drv_t disp_drv; /* 显示设备的描述符 */ lv_disp_drv_init(&disp_drv); /* 初始化为默认值 */ /* 设置显示设备的分辨率 * 这里为了适配正点原子的多款屏幕,采用了动态获取的方式, * 在实际项目中,通常所使用的屏幕大小是固定的,因此可以直接设置为屏幕的大小 */ disp_drv.hor_res = lcddev.width; disp_drv.ver_res = lcddev.height; /* 用来将缓冲区的内容复制到显示设备 */ disp_drv.flush_cb = disp_flush; /* 设置显示缓冲区 */ disp_drv.draw_buf = &draw_buf_dsc_1; /* 注册显示设备 */ lv_disp_drv_register(&disp_drv);
从上述源码可知,显示驱动的注册流程分为以下几个步骤:
第一步:调用lv_disp_drv_init函数,初始化显示设备。
第二步:设置显示设备的分辨率、显示缓冲区以及刷屏函数。
第三步:调用lv_disp_drv_register函数注册显示设备。
注意:lv_disp_drv_t需要是静态、全局或动态分配的变量。
5.3.3 屏幕旋转
当我们硬件显示器的X和Y轴方向调转且无法通过硬件方式来改变时,为了不重新设计硬件电路,可使用LVGL的屏幕旋转功能(纯软件),示意效果如下图所示:

图5.3.3.1 X、Y轴调转
需要注意的是,纯软件实现屏幕旋转需要大量的内存开销,并且刷新效果会大打折扣。接下来,我们介绍屏幕旋转的配置步骤:
/* 第一步:设置 sw_rotate 标志设置为 1 */ disp_drv. sw_rotate = 1; /* 第二步:调用旋转函数,对屏幕方向进行旋转 */ lv_disp_set_rotation(lv_disp_get_default(), LV_DISP_ROT_180);
用户可以在注册显示设备的时候进行屏幕旋转,旋转角度相关的枚举如下:
① LV_DISP_ROT_NONE:无需旋转;
② LV_DISP_ROT_90:旋转90度;
③ LV_DISP_ROT_180:旋转180度;
④ LV_DISP_ROT_270:旋转270度;
5.3.4 显示接口API函数
LVGL官方提供了很多与显示接口相关API,如下表所示:

表5.3.4.1 显示接口相关API函数
接下来,我们介绍一些LVGL显示接口的常用函数:
(1) lv_disp_drv_init函数
初始化显示驱动,其函数原型如下所示:
void lv_disp_drv_init(lv_disp_drv_t *driver);
该函数的形参,如下表所示:

表5.3.4.2 lv_disp_drv_init函数形参描述
返回值:无。
(2) lv_disp_draw_buf_init函数
初始化显示缓冲区,其函数原型如下所示:
void lv_disp_draw_buf_init(lv_disp_draw_buf_t *draw_buf, void *buf1, void *buf2, uint32_t size_in_px_cnt);
该函数的形参,如下表所示:

表5.3.4.3 lv_disp_draw_buf_init函数形参描述
返回值:无。
(3) lv_disp_drv_register函数
注册一个显示设备,其函数原型如下所示:
lv_disp_t *lv_disp_drv_register(lv_disp_drv_t *driver);
该函数的形参,如下表所示:

表5.3.4.4 lv_disp_drv_register函数形参描述
返回值:成功时返回相应的指针,错误时为 NULL。
(4) lv_disp_remove函数
移除显示器,其函数原型如下所示:
void lv_disp_remove(lv_disp_t *disp);
该函数的形参,如下表所示:

表5.3.4.5 lv_disp_remove函数形参描述
返回值:无。
(5) lv_disp_get_default函数
获取默认显示设备,其函数原型如下所示:
lv_disp_t *lv_disp_get_default(void);
返回值:指向默认显示设备的指针。
(6) lv_disp_set_rotation函数
设置屏幕的旋转,其函数原型如下所示:
void lv_disp_set_rotation(lv_disp_t *disp , lv_disp_rot_t rotation);
该函数的形参,如下表所示:

表5.3.4.6 lv_disp_set_rotation函数形参描述
返回值:无。
5.4 输入设备
输入设备可以让用户和计算机系统之间进行信息交换,在LVGL中,支持的输入设备包括触摸屏、键盘、鼠标、编码器以及按钮,它们都是在 lv_port_indev_template.c文件中进行配置的。我们需要成功地驱动自己的输入设备,就必须在LVGL中将其注册。
5.4.1 注册输入设备
这里我们结合源码来介绍LVGL输入设备的注册流程,源码如下所示(以按键为例):
static lv_indev_drv_t indev_drv; keypad_init(); /* 第一步 初始化用户按键(用户层) */ lv_indev_drv_init(&indev_drv); /* 第二步 初始化输入设备*/ indev_drv.type = LV_INDEV_TYPE_KEYPAD; /* 第三步 配置输入设备类型 */ indev_drv.read_cb = keypad_read; /* 第四步 设置输入设备读取回调函数 */ /* 第五步 在LVGL中注册驱动程序,并保存创建的输入设备对象 */ indev_keypad = lv_indev_drv_register(&indev_drv);
由上述源码可知,输入设备的注册流程一共有五个步骤,不同类型输入设备的配置方法有所区别,但注册的流程都是相同的。
5.4.2 输入设备相关API
LVGL官方提供输入设备相关API,如下表所示:

表5.4.2.1 输入设备相关API函数
接下来,我们介绍LVGL输入设备的一些常用函数:
(1) lv_indev_drv_init函数
初始化输入设备,其函数原型如下所示:
void lv_indev_drv_init(struct _lv_indev_drv_t *driver);
该函数的形参,如下表所示:

表5.4.2.2 lv_indev_drv_init函数形参描述
返回值:无。
(2) lv_indev_drv_register函数
注册输入设备,其函数原型如下所示:
lv_indev_t *lv_indev_drv_register(struct _lv_indev_drv_t *driver);
该函数的形参,如下表所示:

表5.4.2.3 lv_indev_drv_register函数形参描述
返回值:指向新输入设备的指针或 NULL 错误。
(3) lv_indev_delete函数
移除输入设备,其函数原型如下所示:
void lv_indev_delete(lv_indev_t *indev);
该函数的形参,如下表所示:

表5.4.2.4 lv_indev_delete函数形参描述
返回值:无。
(4) _lv_indev_read函数
读取输入设备数据,其函数原型如下所示:
void _lv_indev_read(lv_indev_t *indev, lv_indev_data_t *driver);
该函数的形参,如下表所示:

表5.4.2.5 _lv_indev_read函数形参描述
返回值:无。
5.5 LVGL时基
在RTOS中,任务的切换需要依赖系统定时器,而LVGL同样也需要一个时基,这样它才可以知道动画以及任务所经过的时间。
关于LVGL的时基配置,请大家回顾第二、三章节。下面我们来讲解LVGL时钟相关的函数:
(1) lv_tick_get函数
该函数可以获取自启动以来经过的毫秒数,其原型如下所示:
uint32_t lv_tick_get(void);
返回值:经过的毫秒数。
(2) lv_tick_elaps函数
该函数可以获取自上一个时间戳以来经过的毫秒数,其原型如下所示:
uint32_t lv_tick_elaps(uint32_t prev_tick);
该函数具有一个参数,如下表所示:

表5.5.1 lv_tick_elaps函数描述
返回值:经过的毫秒数。
5.6 LVGL任务处理
LVGL的任务处理函数(lv_timer_handler)类似于RTOS切换任务的任务调度函数。lv_timer_handler函数对于定时的要求并不严格,一般来说,用户只需要确保在5 毫秒以内调用一次该函数即可,以保持系统响应的速度。值得注意的是,LVGL的任务处理并不是抢占式的,而是采用轮询的方式。
要处理 LVGL 的任务或者回调函数,用户只需要将lv_timer_handler函数定时调用即可,该函数可放在以下地方:
① main函数的循环。
② 定时器的中断(优先级需要低于lv_tick_inc)。
③ 定时执行的OS 任务。
5.7 拓展知识
1. LVGL睡眠模式
睡眠模式即低功耗模式。当没有触发信号时,我们可以让系统进入睡眠模式,以减少系统的功耗,具体的代码实现如下:
while(1)
{
/* 正常操作(无睡眠)< 1秒不活动 */
if(lv_disp_get_inactive_time(NULL) < 1000)
{
lv_task_handler();
}
/* 静止1秒后睡觉 */
else
{
timer_stop(); /* 停止调用lv_tick_inc()的定时器 */
sleep(); /* 调用自己的MCU睡眠函数*/
}
delay_ms(5);
}
注意:如果需要使用输入唤醒(按下、触摸等),应该在输入设备的读取中添加以下几行代码,如下所示:
lv_tick_inc(LV_DISP_DEF_REFR_PERIOD); /* 强制唤醒任务执行 */ timer_start(); /* 在调用lv_tick_inc()时重新启动计时器 */ lv_task_handler(); /* 调用lv_task_handler()来处理唤醒事件 */
2. 操作系统
在默认情况下,LVGL不是线程安全的,其主要原因是:操作系统是抢占式的,而不同的任务之间,可能会发生资源的抢占,这将导致LVGL的运行出现异常,出现卡顿或者显示不全等问题。值得注意的是,在LVGL事件和定时器回调中部分,它们是安全的。
如果用户需要使用实际任务或线程,则需要一个互斥锁,以防止任务翻转,该互斥锁应在lv_timer_handler函数之前被调用,并在它之后释放,除此之外,用户在调用 LVGL相关的函数和代码时,其相关的任务和线程中也需要使用相同的互斥锁。
以下是官方所提供的模板,源码如下:
void lvgl_thread(void)
{
while(1)
{
xSemaphoreTake(MutexSemaphore,portMAX_DELAY);
lv_task_handler();
xSemaphoreGive(MutexSemaphore); /* 释放互斥信号量 */
thread_sleep(10); /* sleep for 10 ms */
}
}
void other_thread(void)
{
/* 使用LVGL api时,你必须始终持有互斥对象 */
xSemaphoreTake(MutexSemaphore,portMAX_DELAY);
lv_obj_t *img = lv_img_create(lv_scr_act());
xSemaphoreGive(MutexSemaphore); /* 释放互斥信号量 */
while(1)
{
xSemaphoreTake(MutexSemaphore,portMAX_DELAY);
/* 切换到下一个图像 */
lv_img_set_src(img, next_image);
xSemaphoreGive(MutexSemaphore); /* 释放互斥信号量 */
thread_sleep(2000);
}
}
3. 中断
用户应该尽量避免在中断里调用 LVGL函数(lv_tick_inc 和lv_disp_flush_ready函数除外),如果必须执行此操作,则需要在 lv_timer_handler函数运行时,禁用相关的中断。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3171579/ ,如需转载,请注明出处,否则将追究法律责任。