赞
踩
本文的移植参考的是正点原子的课程《手把手教你学LVGL图形界面编程》
基于该课程和《LVGL开发指南_V1.3》“第二章 LVGL 无操作系统移植”,然后结合自身的实际情况进行整理。
先根据自己的习惯,创建基础的单片机工程,然后在APP业务层和DRIVER驱动层之间加上MIDDLEWARE层,在这一层中加入lvgl。
另外,这里还有句话:
所以如果用到lvgl,尽可能使用裸机开发。
基于LVGL版本V8.2。
注意
移植lvgl之前应先保证LCD和定时器的驱动是正常的。
移植本身并不难,跟着教程走一遍即可。
难点是,如何优化LCD本身的驱动,以及如何优化LVGL,来让刷屏的速度够快。
先看下移植要求
几个问题说明下:
1、
这个图形缓冲区的意义是,不会来一个点就刷新一次,而是至少缓冲到一行之后再刷新,防止画面不断闪烁卡顿,效率更高。所以才要求>水平分辨率像素。缓冲区其实就是个数组,数组大小,至少是一行像素所占用字节的大小,假如屏幕大小是300*240,所用的颜色是16位的,也就是一个像素点占两个字节,那么数组大小就要求至少是300*2=600字节大小。
如果想要更流畅,就建议大于1/10屏幕总像素。也就是300*240/10*2=14400字节,也就是15k字节左右。对内存还是有一些要求的。
2、
上面的移植要求中,前面的是最低要求,后面的建议是一般使用的设置,当然,在条件允许的情况下,肯定是大些好。
3、
这里说的图形缓冲区,指的是lvgl管理的缓冲区,后面正式移植时会在相应文件中进行设置。另外,如果我们LCD的驱动里使用DMA,可能也有个双缓冲的使用,二者并不是同一回事,不要搞混了。LCD屏幕本身是怎么刷新的我们暂且不管,当做一个整体来看待即可,LVGL会先将要显示的内容放在它自己管理的缓冲区内,也就是这里说的图形缓冲区,里面存放的是即将要刷新的像素的颜色数据,然后LVGL会通过调用底层的发送接口(需要我们手动添加底层的区域打点函数),将数据发送给LCD。所以,LVGL的缓冲区是它自己要显示的内容的缓冲,而DMA的缓冲是硬件层面的缓冲,是为了加速发送过程而设置的缓冲机制。类似于,LVGL的缓冲是把要运送的水给准备好储存好,然后搬运过程中用几个人来搬,用什么方式来搬会更快,这是硬件层面的驱动要考虑的问题,LVGL是面对应用的。
4、
另外注意要勾选C99,要不然编译时会报很多错误。
5、
8位的51单片机无法运行LVGL
优化LVGL运行效果的方法
关键点其实就是:缩短图像刷新所需要的时间。
接下来分别进行说明
1、
提高芯片主频,可以提高程序的运行速度,提高外设的处理速度;
2、
增大SRAM容量,可以提高一次刷新屏幕的像素点数,一次刷新10行,和一次刷新50行,肯定是有区别的,但是一次刷新的行数越多,就表示越需要更大的空间,如果SRAM的内存空间不够用,那这点就没法实现;万一内部SRAM不够,也可以根据情况考虑外部SRAM。
注意:内部SRAM会比外部SRAM快很多,原则上只有内部SRAM不够用的情况下才会使用外部的SRAM。
什么情况下使用外部SRAM呢?
根据这里可知,LVGL管理的内存空间和绘图缓冲区不是等价的概念。
LVGL管理的内存空间是用来给LVGL各种使用的空间,绘图缓冲区只是其中的一个方面。
3、
上面说过,增大图形缓冲区可以让一次刷新的像素点数更多,从而加快刷新速度。
使用双缓冲可以让数据一边接收一边发送,省去了等待的时间,让速度加倍。
单缓冲和双缓冲到底有何区别?
单缓冲,接收数据时不能发送,发送数据时不能接收,总有一端是处于等待状态。
双缓冲,可以同时接收和同时发送,A缓存接收满了就发送,同时B缓存接收,B接收满了就发送,刚才A发完的就又可以接收了,如此循环往复,期间交换指针即可,这样其实是利用了A在发送的时候,B也可以接收的特点,就是说数据时时刻刻都是准备好的。其实就是一种用空间换时间的机制。
打个比方,一个池塘里准备好了水,现在想要把池塘里的水运送到水缸里去,如果打一桶水需要t时长,倒一桶水也需要t时长,如果让一个人来干,那么这个人就无法同时打水和倒水,所以他得先花t时间打水,再花t时长倒水,运送一桶水就得2t的时长,n桶水就得2nt的时间,但是如果有两个人,一个人花t时长打水,然后花t时间倒水,在这个人倒水的同时,另一个人就可以装水,这时候,只用把刚才倒完水的空桶和另一个人装满水的桶调换一下,就可以接着倒水了,如此往复循环,n桶水就只需要n+1的时长,中间调换桶的时间可以忽略不计。
(n+1)t对比2nt,效率提高了两倍。
这就是双缓冲的机制,可以省去等待的时间,DMA如果使用双缓冲,那么一次刷新的数组就不必非得设置得那么大,因为数据是源源不绝的,影响的只有交换指针的次数。
4、
减少屏幕刷新需要的总像素点,就是说能局部刷新就不要全局刷新。
5、
提高像素的传输速度,指的是底层LCD驱动的通信速度,有DMA2D就使用DMA2D,能使用并口就使用并口,如果使用的是SPI,就使用SPI+DMA的双缓冲模式,根据情况尽可能提高SPI的传输速度。
初始化流程
接下来开始移植……
1、获取处理好的源码
不要重复造轮子,无需自己精简文件。
直接下载正点原子的A盘里的“程序源码”,打开“扩展例程”里的“LVGL例程”,打开以下路径:LVGL例程1 无操作系统移植\Middlewares,将内部的LVGL拿过来放在自己所建的基础工程的MIDDLEWARE目录下,并且,删除LVGL中的GUI_APP,因为我们实际开发中不必使用例程来查看效果。
2、添加到工程
直接查看《LVGL开发指南_V1.3》“第二章 LVGL 无操作系统移植”这里开始的一段内容:
第31页,搜索关键字:3. 添加工程分组、LVGL 源文件
只需到这里即可结束。
注意,keil只有两级目录,新建分组时可以直接输入全路径
最后的结果显示如下:这里写下来,后面直接拿来复制即可。
Middlewares/lvgl/example/porting
Middlewares/lvgl/src/core
Middlewares/lvgl/src/draw
Middlewares/lvgl/src/extra
Middlewares/lvgl/src/font
Middlewares/lvgl/src/gpu
Middlewares/lvgl/src/hal
Middlewares/lvgl/src/misc
Middlewares/lvgl/src/widgets
之后在对应目录中添加对应的C文件即可,注意,有些路径下有多个目录需要添加,添加时细心一些就可以了。
注意:头文件里有些可能要指明路径,路径中一个点表示当前目录,两个点表示上级目录。
3、配置显示屏驱动
该部分配置步骤如下:
注意,因为上面使用的是正点原子整理过后的文件,所以,很多地方已经是现成的,我们要做的就是在调用我们自己函数的地方给替换掉即可。
不过,为了加深印象,还是做个简单的过程记录吧。
第一步:
修改条件编译指令,对应的头文件也要改
第二步:
将lcd的驱动头文件包含到刚才的lvgl显示文件lv_port_disp_template.c中
第三步:
在lv_port_disp_template.c里找到lv_port_disp_init函数,里面调用了disp_init函数,跳转到disp_init函数中,写入我们自己的屏幕初始化函数,并设置为横屏(可选)
注意,这里的设置横屏也属于LCD驱动的一部分,并不是LVGL的要求。
另外,初始化函数不放在这里也可以,直接在main函数中初始化也行。
第四步(容易搞混):
配置图形单缓冲、双缓冲或者全缓冲;
注意这里配置的是LVGL的图形缓冲区。
单缓冲为最常用的,V8.2版本双缓冲相对单缓冲提升效果不明显,全缓冲对内存要求太高。
缓冲策略是:缓冲区大小是屏幕横向分辨率*10,也就是一次刷新10行。如果效果不太理想,可以尝试增大一次刷新的行数,也就是修改10这个参数。
lv_port_disp_template.c文件中,提供了三种缓冲,对于不需要的缓冲方式,直接删掉或者屏蔽掉即可。
图形缓冲区已经写好的只是个形式参考,具体还是看自己写的,比如我把双缓冲写成全尺寸的双缓冲,只需要把对应数组的大小改变下即可。
注意,这里的MY_DISP_HOR_RES宏定义表示水平分辨率大小,有的版本没有给我们定义,所以需要我们在该文件前面自行定义一下。
比如:
#define MY_DISP_HOR_RES 320
另外还有个垂直分辨率的宏MY_DISP_VER_RES,也可以一并定义了。
分辨率一般在LCD的驱动里也会定义,不要重复了。
三种缓冲方式说明
关于这里的双缓冲,在使用DMA时,尤其是使用STM32F4系列单片机时,很容易搞混。
为什么呢?
因为F4系列单片机的DMA就有个双缓冲模式,即发送时利用双缓冲循环发送数据,省去了大量数据时分批进行DMA传输等待的问题,其实最大的好处是内存不够用,没办法开辟大的缓存时,可以用来个小的缓存来实现流水式数据传输,传输时需要利用传输完成中断,然后在中断里对缓存数据做处理,从而实现乒乓操作,这是底层刷新层面的双缓冲。
参考:
DMA 双缓冲模式_标准库函数dma双指针缓冲-CSDN博客
APM32F4xx DMA双缓冲:超级效率的数据传输策略 - - 21ic电子技术开发论坛
另外LVGL本身也有个双缓冲,这个双缓冲是显存的双缓冲,也就是渲染阶段的双缓冲,可以实现渲染和刷新同时进行,一边渲染一边刷新。二者没有必然联系,就像F103的DMA没有双缓冲,但是也不影响LVGL图像缓冲区的双缓冲。
这两个概念很容易搞混,千万要注意。
我们常规使用时,可以使用LVGL的双缓冲+DMA来实现渲染和刷新的同步进行,这样,比单缓冲渲染和刷新同时只能进行一个效率要高。
这里的图形缓冲区就是显存的作用
显存,也被叫做帧缓存,它的作用是用来存储显卡芯片处理过或者即将提取的渲染数据。如同计算机的内存一样,显存是用来存储要处理的图形信息的部件。
更多参考:
什么是图像的渲染?
我们在LCD开发时,会在LCD屏幕上绘制很多图案,在我们看来是各种各样图案的叠加,但是对于LVGL来说,其实就是一帧画面的不同像素点的组合,渲染就是把我们要绘制的图案通过算法给转化成一帧像素数据,然后直接把这帧像素数据往LCD上刷新。
第五步:
将刚才的那个函数再往下拉就能找到分辨率设置的地方
分别对应水平分辨率和垂直分辨率,直接填入即可。
第六步,配置打点函数(关键的步骤)
在刚才的文件中,找到函数disp_flush,在里面调用我们自己的打点函数。
打点函数通常有5个参数,也就是起始坐标,结束坐标,要打点的颜色。
从disp_flush的形参中获取当前的参数值即可,如下所示:
lcd_color_fill(area->x1, area->y1, area->x2, area->y2, (uint16_t *)color_p);
这里是正电原子提供的函数,根据自己的工程不同进行替换。
最后,在main函数中初始化时调用函数lv_port_disp_init进行初始化即可。
这是影响刷新速度最关键的一个步骤!!!!!!
其实这里调用的是区域填充的函数,LVGL会提供绘制坐标,以及要写入的颜色数据的数组。
这里最重要的就是那个color_p,里面存放的就是图形缓冲区的数据,我们先看上图红框中LVGL本身提供的打点函数,就是把区域里所有像素点一个一个地打到屏幕。
这是效率最低的一种方式,也是原理最直白的一种方式,假如要刷新一个320*240个像素点的屏幕,那么就需要打点76800次,如果使用的是SPI的单字节发送函数,每次循环就需要先发送两次坐标信息,发送一次写GRAM指令,再发送两次颜色值信息才能打一个点,即每打一个点需要发送5次数据,也就是总共需要发送76800*5=384000次数据,就算发送一次字节是1us,那么刷新一次屏幕也得384ms,大约一秒3帧,我们的电视是24帧,这么一想,刷新不拉窗帘才怪。
所以,我们一般都会利用LCD的GRAM自增的特性,先设置一次开始和结束坐标,然后通过更快速的硬件方式将颜色数据批量发送出去,比如使用SPI+DMA的双缓冲方式。
注意,这里的color_p是个数组,里面存放的就是像素点的颜色信息。
使用DMA时直接就把这个指针给DMA不就行了?可以尝试一下。
注意:调用一次disp_flush函数的刷新量就是设置的显存大小,如果是单缓冲,一次刷10行,则调用一次disp_flush函数就会传入10行的像素数据,传完后通过lv_disp_flush_ready函数来通过lvgl,然后接着下一波缓冲的发送。
这里的颜色,有的地方用的是color_p,有的地方用的是color_p->full,其实,跳转到lv_color_t这个结构体去看下就知道了,这两个其实是等价的。color_p是结构体的指针,也就是结构体首元素的指针,即color_p->full的地址;
取这个地址的值,也就是full的值,二者是等价的
可以配合LVGL的双缓存+DMA来进行数据刷新
4、配置输入设备(可选)
如果不需要输入设备,直接保持条件编译指令为#if 0即可。
如果需要,则按步骤配置即可。
本人因开发工程时暂不需要输入设备,所以此步骤暂且略过。
5、 为LVGL提供任务处理的时基
为保证任务调度的准确性,时间的精确度把控十分重要,所以需要一个时基。
一般都是1ms,但是是其他的时长貌似也没啥问题,类似个时间的分辨率,越小则分辨率越高,但是也别比1ms更低了。
在自己的定时器驱动中创建一个1ms的定时器,然后在自己的app_timer的中断处理函数中,调用LVGL的 lv_tick_inc 函数:lv_tick_inc(1); /* lvgl 的 1ms 心跳 */
该函数需要包含如下头文件:#include "lvgl.h"
直接调用即可,无需其他内容。
6、main函数中需要的内容
包含必要的头文件。
#include "lvgl.h"
#include "lv_port_indev_template.h"(可选)
#include "lv_port_disp_template.h"在main函数中初始化时对lvgl进行初始化;
lv_init(); /* lvgl系统初始化 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
lv_port_indev_init(); /* lvgl输入接口初始化,放在lv_init()的后面 */定时处理 LVGL 任务。用户需要每隔几毫秒调用一次 lv_timer_handler 函数,以处理 LVGL 相关的任务,该函数可以放在 while 循环中,但延时不宜过大,需要确保 5 毫秒以内。
这里有个问题,那就是正点原子教程中直接delay延时了5ms,我们实际使用时,不可能直接在主循环里面进行延时的,所以我们就直接调用即可。
7、编写测试代码
lv_obj_t *switch_obj = lv_switch_create(lv_scr_act());
lv_obj_set_size(switch_obj, 120, 60);
lv_obj_align(switch_obj, LV_ALIGN_CENTER, 0, 0);
如果没问题,可以看到界面中会有个切换按钮。
另外还可以自己编写个刷颜色块的程序
//lvgl的任务
void LvglTask(void)
{
// lv_obj_t *switch_obj = lv_switch_create(lv_scr_act());// lv_obj_set_size(switch_obj, 120, 60);
// lv_obj_align(switch_obj, LV_ALIGN_CENTER, 0, 0);
// lv_demo_stress();
//画一个纯色块来调试
/* 创建一个对象 */
lv_obj_t * obj1 = lv_obj_create(lv_scr_act());
/* 设置对象背景颜色 */
lv_obj_set_style_bg_color(obj1, lv_palette_main(LV_PALETTE_GREEN), LV_STATE_DEFAULT);
lv_obj_set_style_width(obj1, 320, LV_STATE_DEFAULT);
lv_obj_set_style_height(obj1, 240, LV_STATE_DEFAULT);// //画一个纯色块来调试
// /* 创建一个对象 */
// lv_obj_t * obj2 = lv_obj_create(lv_scr_act());
// /* 设置对象背景颜色 */
// lv_obj_set_style_bg_color(obj2, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
// lv_obj_set_style_width(obj2, 320, LV_STATE_DEFAULT);
// lv_obj_set_style_height(obj2, 240, LV_STATE_DEFAULT);
}
有个尴尬的问题,移植完之后,不知道lvgl的程序应该要写到哪里,就比如上面的那个测试代码。如果放在主循环里,不合理,因为不可能会循环执行这个图形任务的,如果放在主循环外面,执行一次,不是就没了?
其实就是放在主循环外面。
lv_timer_handler一直在监测着lvgl的活动,只要有待处理的lvgl任务,就会通过lv_timer_handler来处理。
压力测试的demo如何移植?
只用添加一个头文件即可。
遇到一个比较麻烦的问题,程序一进入lv_timer_handler函数就会卡死出不来了。
导致程序没法正常显示,一开始我没发现这个问题,也没往这上面想,就总以为是其他地方的问题,到处调试了半天,最后才发现是这个函数阻塞了。
搞不懂为啥,网上也没有类似的资料,就好像没有人会遇到这种问题一样。
待解决,解决思路有几个:
将lv_timer_handler改成lv_task_handler试试看;
将正点原子的配置文件挪过来试试看;
将STM32的栈大小改大些看看;
将lv_tick_inc放到systick的中断里看看;
后续可行的解决方案如下:
将单片机的堆栈从默认的0x00000400改到0x00000800,就不会卡死了。
那么,LVGL对堆栈的大小有什么要求吗?
这一点正点原子的课程里貌似没有提到,也有可能是我漏看了。
官方说明文档中推荐我们堆、栈大小设置为8kB
由此,我们索性直接将栈和堆都设置成8kB,确保LVGL运行的流畅度
LVGL 的任务处理函数(lv_timer_handler)类似于 RTOS 切换任务的任务调度函数。
lv_timer_handler 函数对于定时的要求并不严格,一般来说,用户只需要确保在 5 毫秒以内调用一次该函数即可,以保持系统响应的速度。值得注意的是,LVGL 的任务处理并不是抢占式的,而是采用轮询的方式。
要处理 LVGL 的任务或者回调函数,用户只需要将 lv_timer_handler 函数定时调用即可,该函数可放在以下地方:
① main 函数的循环。
② 定时器的中断(优先级需要低于 lv_tick_inc)。
③ 定时执行的 OS 任务。
用户应该尽量避免在中断里调用 LVGL函数(lv_tick_inc 和lv_disp_flush_ready函数除外)
如果必须执行此操作,则需要在 lv_timer_handler 函数运行时,禁用相关的中断。
注意:
编译时,如果提示内存问题,可以尝试调整LVGL管理的内存空间。
因为LVGL默认管理的内存空间是48K字节,这其实是蛮大的,如果你单片机的内存都没有48K,那LVGL就肯定会报错。
如何调整?
在lv_conf.h中可以找到MEMORY SETTINGS部分的内容,将48的值改小一些,至少保证自己的单片机能提供这么多内存空间。
配置文件lv_conf.h
lv_conf.h 是一个用户级别的文件,它不属于内核的部分,因此,在不同的工程中,该文件有可能存在差异。
lv_conf.h 文件具有两大功能:
(1) 配置功能:内存、屏幕刷新周期、输入设备的读取周期,等等;
(2) 裁剪功能:使能/失能某些功能,有效地优化 Flash 的分配。
lv_conf.h 文件的内容可划分为 10 个板块,如下表所示:
配置项还是蛮多的,这里记录下常用的配置项
总开关
#if 1 /*Set it to "1" to enable content*/
颜色设置
/* 颜色深度: 1(每像素1字节), 8(RGB332), 16(RGB565), 32(ARGB8888) */
#define LV_COLOR_DEPTH 16内存设置
/* 0: 使用内置的 `lv_mem_alloc()` 和 `lv_mem_free()`*/
#define LV_MEM_CUSTOM 0HAL设置
/* 输入设备的读取周期(以毫秒为单位) */
#define LV_INDEV_DEF_READ_PERIOD 4 /*[ms]*/字库设置
/* 始终设置默认字体 */
#define LV_FONT_DEFAULT &lv_font_montserrat_14后续控件和特别功能等等,如果不需要就置0.
这几部分正点原子为了教学方便,好多都开了,实际中可选择性关闭。
更多详细内容直接参考《LVGL开发指南_V1.3》“第五章 LVGL 移植的相关知识”
补充::::::::
外部SRAM、自定义的内存管理算法
需要时再来研究
DMA2D
DAM2D需要硬件支持,如果板子不支持DMA2D,则无法使用。
首先了解下什么是DMA2D:STM32的“GPU”——DMA2D实例详解 - 知乎
根据上面的文章可知,DMA2D可以理解成专门用来处理2D图像的DMA,类似于“GPU”。
因为STM32F103和STM32F407没有该外设,所以此处不赘述。
F429单片机就支持:STM32F429的图形加速器DMA2D的基础知识
以后有需要再研究。
第11讲 基础篇-LVGL移植(DMA2D)_哔哩哔哩_bilibili
在竖屏状态下,LVGL 使用 DMA2D 的方式刷新显示屏,会出现显示混乱的问题,该问题目前还没有很好的解决方案。如果用户想使用 RGB 屏,并且以竖屏的方式运行 LVGL,可以使用原始的画点方式刷新显示屏。
补充:lv_timer_handler()具体是干嘛的?
根据官方给出的移植指南,我们知道移植步骤如下:
To use the graphics library you have to initialize it and setup required components. The order of the initialization is:
1. Call lv_init().
2. Initialize your drivers.
3. Register the display and input devices drivers in LVGL. Learn more about Display and Input device registration.
4. Call lv_tick_inc(x) every x milliseconds in an interrupt to report the elapsed time to LVGL. Learn more.
5. Call lv_timer_handler() every few milliseconds to handle LVGL related tasks.首先,调用lv_init方法,初始化LVGL。
然后,初始化我们的驱动。再把显示和输入设备驱动注册到LVGL。
再然后,周期性调用lv_tick_inc,用以报告已经过去的时间(其实就是给LVGL提供一个时间基准)。
最后,周期性调用lv_timer_handler()用以处理LVGL内部的任务。
有个疑惑,lv_timer_handler()具体是干嘛的?
直接参考这篇文章:LVGL开源图形库学习总结_KAGUYA233的博客-CSDN博客
/* LVGL 管理函数相当于 RTOS 触发任务调度函数 */
这个也可以参考:LVGL-V8框架简析(3) - 哔哩哔哩
LVGL 的绘制,不是直接绘制到屏幕,首先是往内部缓冲区绘制,当绘图(渲染)准备好时,该缓冲区被刷到屏幕;
与直接绘制到屏幕相比,这种方法有两个主要优点:
1、避免绘制UI层时闪烁。例如,如果 LVGL 直接绘制到显示中,那么在绘制 *背景 + 按钮 + 文本 * 时,每个“阶段”都会在短时间内可见。
2、修改内部 RAM 中的缓冲区并最终仅写入一帧像素一次比在每个像素访问时直接读取/写入显示更快。 (例如,通过带有 SPI 接口的显示控制器)。
请注意,此概念与“传统”双缓冲不同,后者有 2 个屏幕大小的帧缓冲区: 一个保存当前图像以显示在显示器上,渲染发生在另一个(非活动)帧缓冲区中,渲染完成后它们会被交换。 主要区别在于,使用 LVGL,不必存储 2 个帧缓冲区(通常需要外部 RAM),而只需存储更小的绘图缓冲区,也可以轻松装入内部 RAM。总结就是:先把要绘制的整个图形的数据存入图形缓冲区,这就是渲染,然后调用底层函数去刷新,也就是把数据发送给LCD。
LVGL移植后,编译特别慢。
可以进行如下设置:
1、取消勾选Browse Information
此时,就无法使用跳转查看了,可以使用全局搜索替代使用。
2、取消勾选微库
验证
勾选微库和浏览时的编译时长为45秒左右。
去掉微库勾选基本没啥改善;
去掉浏览勾选约35秒左右,其实改善也不是太明显;
换个添加了较多素材的程序,原来的编译时长为1分35秒;
取消勾选浏览后约为1分10秒;
可见,取消勾选微库没啥作用,取消勾选Browse Information有点作用,但是节省的时间也很有限,觉得没啥取消勾选的必要。
务必先移植好再调优,如果先调优,后续移植出问题都不知道是啥原因,所以,先用最简单的方式给移植好即可。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。