转载请注明出处: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以上了,所以再从头学习一下,并且做一些详细的学习笔记。


一、移植前准备

  1. 硬件(带屏幕的STM32F407VE核心板)
  2. Keil工程,实现屏幕显示、按键或触摸等
  3. 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到工程中
  1. 在keil工程目录中新建文件夹LVGL_GUI

  2. 将examples文件夹复制到LVGL_GUI

  3. 将src文件夹复制到LVGL_GUI

  4. 将lv_conf_template.h复制到LVGL_GUI,并重命名为lv_conf.h

  5. 将lvgl.h复制到LVGL_GUI

  6. 将src目录下的全部添加到工程中LVGL_GUI组下(除了上面介绍过的第三方库)

  7. 将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源码连接起来,主要就是两部分,一是输出、一是输入;

输出:说白了就是屏幕显示,所以需要移植一下显示接口函数

输入:可以是触摸板、触摸屏、按键、编码器、甚至是鼠标等都可

  1. 屏幕显示接口的移植

    将examples/porting目录下的lv_port_disp_template.c和lv_port_disp_template.h文件改名为lv_port_disp.clv_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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐