概述
本文介绍目前LVGL的应用小知识,希望对采用MCU设计UI界面的用户有所启发,开发出界面更友好的消费品或者工业产品,造福大众。
01.LVGL系统架构
LVGL系统框架
应用程序创建GUI并处理特定任务的应用程序。
LVGL本身是一个图形库。我们的应用程序通过调用LVGL库来创建GUI。它包含一个HAL(硬件抽象层)接口,用于注册显示和输入设备驱动程序。
驱动程序除特定的驱动程序外,它还有其他的功能,可驱动显示器到GPU (可选)、读取触摸板或按钮的输入。
根据MCU,有两种典型的硬件设置。一个带有内置LCD/TFT驱动器的外围设备,而另一种是没有内置LCD/TFT驱动器的外围设备。在这两种情况下,都需要一个帧缓冲区来存储屏幕的当前图像。
1.集成了TFT/LCD驱动器的MCU如果MCU集成了TFT/LCD驱动器外围设备,则可以直接通过RGB接口连接显示器。在这种情况下,帧缓冲区可以位于内部RAM(如果MCU有足够的RAM)中,也可以位于外部RAM(如果MCU具有存储器接口)中。
2.如果MCU没有集成TFT/LCD驱动程序接口,则必须使用外部显示控制器(例如SSD1963、SSD1306、ILI9341 )。在这种情况下,MCU可以通过并行端口,SPI或通过I2C与显示控制器进行通信。帧缓冲区通常位于显示控制器中,从而为MCU节省了大量RAM。
**02.**建立一个LVGL项目
要在我们的项目中使用 lvgl ,我们起码需要获取到官方的这两个库:
lvgl(lvgl)核心图形库的官方 GitHub 仓库地址:https://github.com/lvgl/lvgl。
lvgl(lv_drivers)输入输出设备驱动官方 GitHub 仓库地址:https://github.com/lvgl/lv_drivers
我们可以克隆或下载这两个库的最新版本,将它们复制到我们的项目中,然后进行适配。
目录 lvgl 就是 lvgl 的官方图形库
目录 lv_drivers 是 lvgl 输入输出设备驱动官方示例配置
目录 lv_examples 是 lvgl 的官方demo(可选,但不要直接使用到实际项目中)
配置文件
上面的三个库中有一个类似名为 lv_conf_template.h 的配置头文件(template就是模板的意思)。通过它可以设置库的基本行为,裁剪不需要模块和功能,在编译时调整内存缓冲区的大小等等。
-
将 lvgl/lv_conf_template.h 复制到 lvgl 同级目录下,并将其重命名为 lv_drv_conf.h 。打开文件并将开头的 #if 0 更改为 #if 1 以使能其内容。
-
将 lv_drivers/lv_drv_conf_template.h 复制到 lv_drivers 同级目录下,并将其重命名为 lv_conf.h 。打开文件并将开头的 #if 0 更改为 #if 1 以使能其内容。
-
(可选)将 lv_examples/lv_ex_conf_template.h 复制到 lv_examples 同级目录下,并将其重命名为 lv_ex_conf.h 。打开文件并将开头的 #if 0 更改为 #if 1 以使能其内容。
准备lvgl配置文件
使能配置文件
lv_conf.h 也可以复制到其他位置,但是应该在编译器选项中添加 ``LV_CONF_INCLUDE_SIMPLE``定义(例如,对于gcc编译器为``-DLV_CONF_INCLUDE_SIMPLE`` ) 并手动设置包含路径。
在配置文件中,注释说明了各个选项的含义。我们在移植时至少要检查以下三个配置选项,其他配置根据具体的需要进行修改:
-
LV_HOR_RES_MAX 显示器的水平分辨率。
-
LV_VER_RES_MAX 显示器的垂直分辨率。
-
LV_COLOR_DEPTH 颜色深度,其可以是:
8 - RG332
16 - RGB565
32 - (RGB888和ARGB8888)
初始化LVGL
准备好这三个库:lvgl、lv_drivers、lv_examples 后,我们就要开始使用lvgl带给我们的功能了。使用 lvgl 图形库之前,我们还必须初始化 lvlg 以及相关其他组件。初始化的顺序为:
-
调用 lv_init() 初始化 lvgl 库;
-
初始化驱动程序;
-
在 LVGL 中注册显示和输入设备驱动程序;
-
在中断中每隔 x毫秒 调用 lv_tick_inc(x) 用以告知 lvgl 经过的时间;
-
每隔 x毫秒 定期调用 lv_task_handler() 用以处理与 LVGL相关的任务。
**03.**显示接口
要设置显示,必须初始化 lv_disp_buf_t 和 lv_disp_drv_t 变量。
-
lv_disp_buf_t 保存显示缓冲区信息的结构体
-
lv_disp_drv_t HAL要注册的显示驱动程序、与显示交互并处理与图形相关的结构体、回调函数。
显示缓存区
关于缓冲区大小,有 3 种情况:
一个缓冲区 LVGL将屏幕的内保存到缓冲区中并将其发送到显示器。缓冲区可以小于屏幕。在这种情况下,较大的区域将被重画成多个部分。如果只有很小的区域发生变化(例如按下按钮),则只会刷新该部分的区域。
两个非屏幕大小的缓冲区 具有两个缓冲区的 LVGL 可以将其中一个作为显示缓冲区,而另一缓冲区的内容发送到后台显示。应该使用 DMA 或其他硬件将数据传输到显示器,以让CPU同时绘图。这样,渲染和刷新并行处理。与 一个缓冲区 的情况类似,如果缓冲区小于要刷新的区域,LVGL将按块绘制显示内容
两个屏幕大小的缓冲区 与两个非屏幕大小的缓冲区相反,LVGL将始终提供整个屏幕的内容,而不仅仅是块。这样,驱动程序可以简单地将帧缓冲区的地址更改为从 LVGL 接收的缓冲区。因此,当MCU具有 LCD/TFT 接口且帧缓冲区只是 RAM 中的一个位置时,这种方法的效果很好。
显示驱动器
一旦缓冲区初始化准备就绪,就需要初始化显示驱动程序。在最简单的情况下,仅需要设置 lv_disp_drv_t 的以下两个字段:
buffer 指向已初始化的 lv_disp_buf_t 变量的指针。
flush_cb 回调函数,用于将缓冲区的内容复制到显示的特定区域。刷新准备就绪后,需要调用lv_disp_flush_ready()。LVGL可能会以多个块呈现屏幕,因此多次调用flush_cb。使用 lv_disp_flush_is_last() 可以查看哪块是最后渲染的。
其中,有一些可选的数据字段:
hor_res 显示器的水平分辨率。(默认为 lv_conf.h 中的 LV_HOR_RES_MAX )
ver_res 显示器的垂直分辨率。(默认为 lv_conf.h 中的 LV_VER_RES_MAX )
color_chroma_key 在 chrome 键控图像上将被绘制为透明的颜色。(默认为 lv_conf.h 中的 LV_COLOR_TRANSP )
user_data 驱动程序的自定义用户数据。可以在 lv_conf.h 中修改其类型。
anti-aliasing 使用抗锯齿(anti-aliasing)(边缘平滑)。缺省情况下默认为 lv_conf.h 中的 LV_ANTIALIAS 。
rotated 如果 1 交换 hor_res 和 ver_res 。两种情况下 LVGL 的绘制方向相同(从上到下的线条),因此还需要重新配置驱动程序以更改显示器的填充方向。
screen_transp 如果为 1 ,则屏幕可以具有透明或不透明的样式。需要在 lv_conf.h 中启用 LV_COLOR_SCREEN_TRANSP 。
要使用GPU,可以使用以下回调:
gpu_fill_cb 用颜色填充内存中的区域。
gpu_blend_cb 使用不透明度混合两个内存缓冲区。
gpu_wait_cb 如果在 GPU 仍在运行 LVGL 的情况下返回了任何 GPU 函数,则在需要确保GPU渲染就绪时将使用此函数。
注意,这些功能需要绘制到内存(RAM)中,而不是直接显示在屏幕上。
其他一些可选的回调,使单色、灰度或其他非标准RGB显示一起使用时更轻松、优化:
rounder_cb 四舍五入要重绘的区域的坐标。例如。2x2像素可以转换为2x8。如果显示控制器只能刷新特定高度或宽度的区域(对于单色显示器,通常为8 px高),则可以使用它。
set_px_cb 编写显示缓冲区的自定义函数。如果显示器具有特殊的颜色格式,则可用于更紧凑地存储像素。(例如1位单色,2位灰度等)。这样,lv_disp_buf_t中使用的缓冲区可以较小,以仅保留给定区域大小所需的位数。set_px_cb不能与两个屏幕大小的缓冲区一起显示缓冲区配置。
monitor_cb 回调函数告诉在多少时间内刷新了多少像素。
clean_dcache_cb 清除与显示相关的所有缓存的回调
要设置 lv_disp_drv_t 变量的字段,需要使用 lv_disp_drv_init(&disp_drv) 进行初始化。最后,要为 LVGL 注册显示设备,需要调用lv_disp_drv_register(&disp_drv)。
**04.**输入设备接口
(一)、输入设备的类型
要设置输入设备,必须初始化 lv_indev_drv_t 变量:
类型 (indev_drv.type)可以是:
-
LV_INDEV_TYPE_POINTER 触摸板或鼠标
-
LV_INDEV_TYPE_KEYPAD 键盘或小键盘
-
LV_INDEV_TYPE_ENCODER 带有左,右,推动选项的编码器
-
LV_INDEV_TYPE_BUTTON 外部按钮按下屏幕
read_cb (indev_drv.read_cb)是一个函数指针,将定期调用该函数指针以报告输入设备的当前状态。它还可以缓冲数据并在没有更多数据要读取时返回 false ,或者在缓冲区不为空时返回 true 。
进一步了解有关 输入设备 的更多信息。
(二)、触摸板,鼠标或任何指针
可以单击屏幕点的输入设备属于此类别。
即使状态为 LV_INDEV_STATE_REL ,触摸板驱动程序也必须返回最后的 X/Y 坐标。
要设置鼠标光标,请使用 lv_indev_set_cursor(my_indev,&img_cursor) 。( my_indev 是 lv_indev_drv_register 的返回值)键盘或键盘
(三)、触摸板或键盘
带有所有字母的完整键盘或带有一些导航按钮的简单键盘均属于此处。
要使用键盘/触摸板:
-
注册具有 LV_INDEV_TYPE_KEYPAD 类型的 read_cb 函数。
-
在 lv_conf.h 中启用 LV_USE_GROUP
-
必须创建一个对象组:lv_group_t * g = lv_group_create(),并且必须使用 lv_group_add_obj(g,obj) 向其中添加对象
-
必须将创建的组分配给输入设备:lv_indev_set_group(my_indev,g)( my_indev 是 lv_indev_drv_register 的返回值)
-
使用 LV_KEY _... 在组中的对象之间导航。有关可用的密钥,请参见 lv_core/lv_group.h。
(四)、编码器
可以通过下面四种方式使用编码器:
-
按下按钮
-
长按其按钮
-
转左
-
右转
简而言之,编码器输入设备的工作方式如下:
-
通过旋转编码器,可以专注于下一个/上一个对象。
-
在简单对象(如按钮)上按下编码器时,将单击它。
-
如果将编码器按在复杂的对象(如列表,消息框等)上,则该对象将进入编辑模式,从而转动编码器即可在对象内部导航。
-
长按按钮,退出编辑模式。
要使用编码器(类似于键盘),应将对象添加到组中。
(五)、使用带有编码器逻辑的按钮
除了标准的编码器行为外,您还可以利用其逻辑来使用按钮导航(聚焦)和编辑小部件。如果只有几个按钮可用,或者除编码器滚轮外还想使用其他按钮,这将特别方便。
需要有3个可用的按钮:
LV_KEY_ENTER 将模拟按下或推动编码器按钮
LV_KEY_LEFT 将向左模拟转向编码器
LV_KEY_RIGHT 将正确模拟转向编码器
其他键将传递给焦点小部件
如果按住这些键,它将模拟indev_drv.long_press_rep_time中指定的时间段内的编码器单击。
(六)、按键
按钮是指屏幕旁边的外部"硬件"按钮,它们被分配给屏幕的特定坐标。如果按下按钮,它将模拟在指定坐标上的按下。(类似于触摸板)
使用 lv_indev_set_button_points(my_indev, points_array) 将按钮分配给坐标。points_array应该看起来像const lv_point_t points_array [] = {{12,30},{60,90},...}
points_array不能超出范围。将其声明为全局变量或函数内部的静态变量。
(七)、其它功能
除了 read_cb 之外,还可以在 lv_indev_drv_t 中指定 feedback_cb 回调。输入设备发送任何类型的事件时,都会调用feedback_cb。(独立于其类型)。它允许为用户提供反馈,例如在LV_EVENT_CLICK上播放声音。
可以在lv_conf.h中设置以下参数的默认值,但可以在lv_indev_drv_t中覆盖默认值:
-
拖拽限制(drag_limit) 实际拖动对象之前要滑动的像素数 drag_throw 拖曳速度降低[%]。更高的价值意味着更快的减速
-
(drag_throw) 拖曳速度降低[%]。更高的价值意味着更快的减速
-
(long_press_time) 按下时间发送 LV_EVENT_LONG_PRESSED (以毫秒为单位)
-
(long_press_rep_time) 发送 LV_EVENT_LONG_PRESSED_REPEAT 的时间间隔(以毫秒为单位)
-
(read_task) 指向读取输入设备的lv_task的指针。可以通过 lv_task_...() 函数更改其参数
每个输入设备都与一个显示器关联。默认情况下,新的输入设备将添加到最后创建的或显式选择的显示设备(使用lv_disp_set_default())。相关的显示已存储,并且可以在驱动程序的显示字段中更改。
(八)、心跳
LVGL 需要系统滴答声才能知道动画和其他任务的经过时间。
为此我们需要定期调用 lv_tick_inc(tick_period) 函数,并以毫秒为单位告知调用周期。例如, lv_tick_inc(1) 用于每毫秒调用一次。
为了精确地知道经过的毫秒数,lv_tick_inc 应该在比 lv_task_handler() 更高优先级的例程中被调用(例如在中断中),即使 lv_task_handler 的执行花费较长时间。
(九)、任务处理器(Task Handler)
要处理 LVGL 的任务,我们需要定期通过以下方式之一调用 lv_task_handler() :
-
mian 函数中设置 while(1) 调用
-
定期定时中断(低优先级然后是 lv_tick_inc()) 中调用
-
定期执行的 OS 任务中调用
计时并不严格,但应保持大约5毫秒以保持系统响应。
**05.**日志记录
LVGL 内置有日志模块,用于记录用户库中正在发生的事情。
(一)、日志级别
要启用日志记录,需要在 lv_conf.h 中将 LV_USE_LOG 设置为 1 ,并将 LV_LOG_LEVEL 设置为以下值之一:
-
LV_LOG_LEVEL_TRACE 记录所有信息
-
LV_LOG_LEVEL_INFO 记录重要事件
-
LV_LOG_LEVEL_WARN 记录是否发生了警告事件
-
LV_LOG_LEVEL_ERROR 记录错误信息,当系统可能发生故障时或致命错误
-
LV_LOG_LEVEL_NONE 不要记录任何东西
级别高于设置的日志级别的事件也将被记录。例如。如果使用 LV_LOG_LEVEL_WARN ,也会记录错误。
(二)、使用printf记录
如果您的系统支持printf,则只需在 lv_conf.h 中启用 **LV_LOG_PRINTF **即可发送带有 printf 的日志。
(三)、自定义日志功能
如果不能使用 printf 或想要使用自定义函数进行日志记录,可以使用 lv_log_register_print_cb() 注册 "logger" 回调。