
LVGL8.1笔记1--显示移植(2022-0515)
参考了LVGL的官方文档、很多的CSDN上的文章、正点原子的LVGL文档等 总结出来的比较详细的移植说明
LVGL8.1笔记1--显示移植
转载请注明出处:https://blog.csdn.net/qq_38685043/article/details/124786944?spm=1001.2014.3001.5501
前言
LVGL是目前比较流行的一款嵌入式GUI图形库,特点是高度可裁剪,占用资源低,界面简洁美观。详细介绍可以看LVGL官方介绍。理论上来说LVGL属于上层程序,不依赖特点的硬件平台,只要是硬件配置(Flash、RAM)满足要求都可以运行LVGL,比如STM32、ESP32。
以前稍微学习过LVGL6.0,但是很长时间没有使用上,而且现在已经发展的8.0以上了,所以再从头学习一下,并且做一些详细的学习笔记。
一、移植前准备
- 硬件(带屏幕的STM32F407VE核心板)
- Keil工程,实现屏幕显示、按键或触摸等
- LVGL-8.1源码(点击这里下载)
二、LVGL-8.1目录简介
目录中内容挺多,主要用的的也就是三部分,下面也只简单介绍这三部分,其他的详细介绍可以参考这个链接lvgl 主要文件目录树
主要用的的内容
- examples
- 一些演示例程和可以用到的接口函数
- src
- LVGL的源码,最重要的内容
- lv_conf_template.h
- LVGL配置文件,用来裁剪控制LVGL的功能,通常重命名为lv_conf.h
- lvgl.h
- LVGL用到的一些头文件的声明
examples文件夹内容介绍
examples->porting文件夹中的文件主要是和显示、文件系统、触摸按键相关的接口。
需要将屏幕显示用的画点函数放入lv_port_disp.c
文件中,否则屏幕不会正常显示
需要将触摸或是按键驱动放入lv_port_indev.c
文件中,否则无法使用触摸屏幕或按键操作
文件系统相关的内容有需要才用得到。
src文件夹内容介绍
src目录中存放的是LVGL的源码,是最主要的内容。其中extra/lib中是一些第三方的库,比如生成二维码用的库。这些非必要可以不用添加,后续有需要再添加进工程也可。
font中是一些自带的字库,只支持英文,想要显示中文需要其他方法。后面再介绍。
lv_conf_template.h文件内容介绍
这是LVGL的配置文件,里面都是一些宏定义。可以通过修改这些宏的值,来控制某些功能的开启或关闭。
三、开始移植
其实LVGL移植起来还是很简单的,简单的几步就可以了
- 准备好硬件平台以及这个硬件带屏幕显示的keil工程,最好是彩屏(虽然LVGL支持单色屏幕,但我还不会用)
- 添加LVGL-8.1到工程中
- API接口移植
- 修改配置文件
- 配置LVGL心跳函数
- 启动LVGL
- 写一个函数测试
1. 编译准备好的带屏幕显示的keil工程
确保编译成功,可以正常显示,最重要的是有刷屏函数
2. 添加LVGL-8.1到工程中
-
在keil工程目录中新建文件夹LVGL_GUI
-
将examples文件夹复制到LVGL_GUI
-
将src文件夹复制到LVGL_GUI
-
将lv_conf_template.h复制到LVGL_GUI,并重命名为
lv_conf.h
-
将lvgl.h复制到LVGL_GUI
-
将src目录下的全部添加到工程中LVGL_GUI组下(除了上面介绍过的第三方库)
-
将examples/porting目录下的文件添加到工程中(可以添加到LVGL_Port组下)
注意:添加工程时内容有些多,要注意别漏掉某些文件。别忘了添加头文件路径
添加完后编译一下,可能会出现大量的error,先不要慌,看一下keil的配置,因为LVGL是基于C99编写的,所以keil编译时一定要选择C99模式
编译,不选择的话默认使用的是C89模式
,所以会出很多错误。
改完编译模式之后再次编译应该就没有错误了,但是会有很多警告,比如一些类型转换的警告,文档结尾没有换行的警告。这些警告建议直接忽略就好了,因为一般这种类似于库函数的代码是不建议自行修改的。要是觉得这些警告看着别扭可以在Keil中设置一下忽略指定类型的警告(警告68、111可以这样设置--diag_suppress=68 --diag_suppress=111
)。
如果还有error,可以检查一下有没有添加头文件路径,看看error类型是什么,慢慢解决 不要慌。有时候很多的error,可能只是某个细节疏忽了。
3. API接口移植(重点)
这应该是整个LVGL移植中最重要的,这一步就是把你的程序和LVGL源码连接起来,主要就是两部分,一是输出、一是输入;
输出:说白了就是屏幕显示,所以需要移植一下显示接口函数
输入:可以是触摸板、触摸屏、按键、编码器、甚至是鼠标等都可
-
屏幕显示接口的移植
将examples/porting目录下的
lv_port_disp_template.c和lv_port_disp_template.h
文件改名为lv_port_disp.c
和lv_port_disp.h
,当然不改名也可以用,但是一般都习惯改一下,显得比较规范。打开.c和.h文件,将最开头的
#if 0
改为#if 1
,这样这两个文件编译时就都有效了
主要修改三个函数:
void disp_init(void)
这个函数是屏幕外设初始化用的,比如我用的屏幕,初始化函数是LCD_Init()。可以复制放到这里来初始化,也可以自己在其他地方初始化,但是要确保在lvgl初始化之前调用。/*Initialize your display and the required peripherals.在这里调用屏幕初始化(如果在其他地方调用了,这里可以不写)*/ static void disp_init(void) { /*You code here*/ LCD_Init(); //LCD初始化 }
void disp_flush(....)
这个函数是刷屏用的,需要将我们自己实现的刷屏函数函数放到这里,函数需要实现的功能是在指定位置绘制指定大小和颜色的矩形,最小可以画一个像素点。通常我们用屏幕,厂家都会有配套的资料,资料里面一般有可以点亮屏幕的程序,通常都会有显示一个字符函数LCD_ShowChar、清屏函数LCD_Clear、显示一条直线函数LCD_DrawLine、画一个带颜色的点函数LCD_DrawPoint,画一个填充了颜色的矩形函数LCD_Color_Fill
其中这两个函数必要有一个,没有的话需要自己写一下
void LCD_DrawPoint(u16 x, u16 y, u16 color); //画点 void LCD_Color_Fill(u16 x1, u16 y1, u16 x2, u16 y2, u16 *color); //填充指定颜色
如果使用画点函数,直接放到最里层的那个for循环就行,把参数填充一下就行了
如果想让LVGL运行更流畅可以使用LCD_Color_Fill这个函数,把两个for循环值删掉,放入这个函数,把参数填充一下就行了。
// 这是LVGL接口函数原型,我们需要进行修改 /*Flush the content of the internal buffer the specific area on the display *You can use DMA or any hardware acceleration to do this operation in the background but *'lv_disp_flush_ready()' has to be called when finished.*/ static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/ int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /*Put a pixel to the display. For example:*/ /*put_px(x, y, *color_p)*/ color_p++; } } /*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv); } // 下面是我修改好的函数 /*Flush the content of the internal buffer the specific area on the display *You can use DMA or any hardware acceleration to do this operation in the background but *'lv_disp_flush_ready()' has to be called when finished.*/ static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/ // 以X1,y1为起点 x2,y2为终点画一个颜色为color的矩形 LCD_Color_Fill(area->x1, area->y1, area->x2, area->y2,(uint16_t *)color_p); /*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv); // 这个函数非常重要,千万不能删掉 }
void lv_port_disp_init(void)
这个函数是LVGL_显示接口初始化函数,用来初始化LVGL库用的绘制缓冲区、调用刷屏函数、设置屏幕大小等功能。看函数的声明就会发现,前两个函数都有static修饰,表示函数的使用范围只在这个lv_port_disp.c文件中,而这个函数是需要在main函数中调用的,所以没有static修饰,而且还要我们手动在lv_port_disp.h文件中声明一下。- 这里有3中缓冲区定义方法,注释中写的还是挺清楚的。方式1一个数组做缓冲区,节省空间,速度偏慢。方式2两个数组做缓冲区,空间需要大,速度快点。方式3也是双缓冲区,但是是每次都刷新整个屏幕。这里使用方式2
- 把屏幕的水平和垂直像素点数也写到里面,如果不常换屏幕可以直接写死,不用定义宏
- 把用不到的内容注释掉就OK了
// 在这定义两个宏来表示屏幕的分辨率大小,实际应该把宏放在lv_conf.h文件中 #define LV_HOR_RES_MAX (320)//定义屏幕的最大水平像素点数 #define LV_VER_RES_MAX (240)//定义屏幕的最大垂直像素点数 void lv_port_disp_init(void) { /*------------------------- * Initialize your display * -----------------------*/ disp_init(); /*----------------------------- * Create a buffer for drawing *----------------------------*/ /** * LVGL requires a buffer where it internally draws the widgets. * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display. * The buffer has to be greater than 1 display row * * There are 3 buffering configurations:3种方式 * 1. Create ONE buffer: * LVGL will draw the display's content here and writes it to your display * * 2. Create TWO buffer: * LVGL will draw the display's content to a buffer and writes it your display. * You should use DMA to write the buffer's content to the display. * It will enable LVGL to draw the next part of the screen to the other buffer while * the data is being sent form the first buffer. It makes rendering and flushing parallel. * * 3. Double buffering * Set 2 screens sized buffers and set disp_drv.full_refresh = 1. * This way LVGL will always provide the whole rendered screen in `flush_cb` * and you only need to change the frame buffer's address. */ /* Example for 1) */ //static lv_disp_draw_buf_t draw_buf_dsc_1; //static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/ //lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/ /* Example for 2) */ static lv_disp_draw_buf_t draw_buf_dsc_2; static lv_color_t buf_2_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/ static lv_color_t buf_2_2[LV_HOR_RES_MAX * 10]; /*An other buffer for 10 rows*/ lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*/ /* Example for 3) also set disp_drv.full_refresh = 1 below*/ //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]; /*A screen sized buffer*/ //static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*An other screen sized buffer*/ //lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2, MY_DISP_VER_RES * LV_VER_RES_MAX); /*Initialize the display buffer*/ /*----------------------------------- * Register the display in LVGL *----------------------------------*/ static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/ lv_disp_drv_init(&disp_drv); /*Basic initialization*/ /*Set up the functions to access to your display*/ /*Set the resolution of the display 设置显示的分辨率 */ disp_drv.hor_res = LV_HOR_RES_MAX; //添加屏幕的最大水平像素点数; disp_drv.ver_res = LV_VER_RES_MAX; //添加屏幕的最大垂直像素点数 /*Used to copy the buffer's content to the display*/ disp_drv.flush_cb = disp_flush; // 这就把刷屏函数注册了 /*Set a display buffer*/ disp_drv.draw_buf = &draw_buf_dsc_2; // 这里和我们选的缓冲区方式2要一致 /*Required for Example 3)*/ //disp_drv.full_refresh = 1 /* Fill a memory array with a color if you have GPU. * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL. * But if you have a different GPU you can use with this callback.*/ //disp_drv.gpu_fill_cb = gpu_fill; /*Finally register the driver*/ lv_disp_drv_register(&disp_drv); }
4.修改配置文件lv_conf.h
这个文件顾名思义,就是对LVGL进行配置用的,里面都是一些宏定义,用来打开或关闭某些功能。
首先,还是要打开文件将#if 0
改为 #if 1
, 然后我们自己定义两个宏,用来表示屏幕的像素尺寸,这两个宏在上面的API接口移植中用到了,定义成宏方便我们后续更换大或小的屏幕。
// 在这定义两个宏来表示屏幕的分辨率大小
#define LV_HOR_RES_MAX (320)//定义屏幕的最大水平像素点数
#define LV_VER_RES_MAX (240)//定义屏幕的最大垂直像素点数
这个配置文件中有挺多内容的,注释也挺详细,大家可以直接看着理解。
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16 // 这个宏用来定义屏幕色彩深度,一般用的彩屏深度就是16,单色屏就是1,我还没用过单色屏所以都是默认16
/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 10 /*[ms]*/ //这里控制刷屏的速度,默认是30,改小一点速度更快,但最终限制刷屏速度的是硬件速度,比如SPI的主频。
/*Input device read period in milliseconds*/
#define LV_INDEV_DEF_READ_PERIOD 30 /*[ms]*/ // 这里控制按键扫描的速度,根据实际自己修改就行
以上这些宏直接影响LVGL的运行效率和使用体验,所以应该根据需要具体进行配置。
其他的宏就是一些功能的选配了。所谓的裁剪,其实就是通过宏打开或关闭功能嘛。
还有下面这两个宏在调试阶段也可以打开,这是用来在屏幕上显示帧数和MCU占用、内存占用的。
/*1: Show CPU usage and FPS count in the right bottom corner*/
#define LV_USE_PERF_MONITOR 1
#if LV_USE_PERF_MONITOR
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
#endif
/*1: Show the used memory and the memory fragmentation in the left bottom corner
* Requires LV_MEM_CUSTOM = 0*/
#define LV_USE_MEM_MONITOR 1
#if LV_USE_PERF_MONITOR
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
#endif
基本上这些完成后,LVGL就算移植完了。接下来就可以编译了。当然编译很可能出错。不要心急,慢慢的分析错误,如果出了很多很多错误,可以看一下第一个error是啥,解决了可能全部error就都解决了。
比如可能出现的错误:
- 文件没有添加到工程中,或者有的漏掉了。
- 仔细检查一下,添加上文件、路径就好了
- 在API接口移植中,会有这个头文件#include “lvgl/lvgl.h”,如果编译报error,改成#include "lvgl.h"就好了,这是因为路径引用的不太对,毕竟lv_port_disp文件是LVGL的示例文件,人家用的路径可能和我们的本地路径不同,需要修改一下。
- 再有就是一些C语言语法错误了,函数传参类型不匹配等
四、运行
如果以上那些步骤都完成了,编译也没有错误,那么之差一小步就可以在屏幕上运行LVGL了。
设置心跳
需要调用两个函数
lv_tick_inc(1); // 这个函数设置心跳,参数1代表1ms。通常将他放在1毫秒中断一次的定时器中断处理中
lv_task_handler(); // 这个函数用来处理LVGL的工作,每心跳一次,这里面就执行一次。
如果使用STM32并且用的cubemx来配置的功能,那么一般默认会开启滴答定时器,一般就是1ms中断一次,所以把这个lv_tick_inc(1);
函数放在systick中断中就行了,不用额外的占用定时器。lv_task_handler();
这个函数可不用放在中断里,通常不用RTOS的话,放在main函数里的while循环就行了
在main函数里还需要调用LVGL初始化函数,然后就可以使用了。
lv_init(); //LVGL初始化
lv_port_disp_init(); //LVGL 显示接口初始化,放在 lv_init()的后面
下面这是我写的main函数和定时器中断处理函数
//定时器3中断服务函数,每1ms中断一次
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
lv_tick_inc(1);
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口波特率为115200
W25QXX_Init(); //外部Flash--W25Q16初始化
TIM3_Int_Init(999,83); //定时器配置1ms中断
KEY_Init(); //按键初始化
LED_Init(); //初始化LED
LCD_Init(); //LCD初始化
tp_dev.init(); //触摸屏初始化
lv_init(); //LVGL初始化
lv_port_disp_init(); //LVGL 显示接口初始化,放在 lv_init()的后面
lv_ex_label(); // 测试函数,显示点动态内容测试LVGL是否成功
while(1)
{
tp_dev.scan(0); // 这是触摸扫描函数,大家可以用自己的按键扫描等
lv_task_handler();
}
}
测试使用
写个小函数来测试一下我们移植是否成功,直接把这函数复制到程序中就能用,先不要研究里面这些函数是干啥的。直接用一下测试自己移植成功没。
static void lv_ex_label(void)
{
static char* github_addr = "https://gitee.com/WRS0923";
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_label_set_recolor(label, true);
lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR); /*Circular scroll*/
lv_obj_set_width(label, 120);
lv_label_set_text_fmt(label, "#ff0000 Gitee: %s#", github_addr);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 10);
lv_obj_t * label2 = lv_label_create(lv_scr_act());
lv_label_set_recolor(label2, true);
lv_label_set_long_mode(label2, LV_LABEL_LONG_SCROLL_CIRCULAR); /*Circular scroll*/
lv_obj_set_width(label2, 120);
lv_label_set_text_fmt(label2, "#ff0000 Hello# #0000ff world !123456789#");
lv_obj_align(label2, LV_ALIGN_CENTER, 0, -10);
}
如果一切正常,屏幕上会显示如下
我的完整程序放在了gitee上,https://gitee.com/WRS0923/stm32_little-vgl/tree/dev/ 有需要的可以自行下载,以后分享的笔记也会在这个程序上修改。争取把LVGL玩明白
参考资料
LittleVGL(LVGL) V8版本 干货入门教程一之移植到STM32并运行:https://blog.csdn.net/qq_26106317/article/details/120610353
lvgl 主要文件目录树:https://blog.csdn.net/qq_39567970/article/details/122126329
【LVGL学习之旅 01】移植LVGL到STM32:https://blog.csdn.net/qq_40831286/article/details/107633216
LVGL系列(二)之二 LVGL常见问题解答 整理自官方文档:https://blog.csdn.net/qq1445104593/article/details/119809376
更多推荐
所有评论(0)