LittleVGL (LVGL)干货入门教程一之移植到stm32芯片

LittleVGL (LVGL)入门教程一之移植到stm32芯片


前言:

阅读前,请确保你拥有以下条件:

  • Keil下的项目的基本创建能力。
  • stm32(或其他平台) 的开发经验。(不是这么重要,因为最低要求有画点函数就行)
  • 你已经实现了一个屏幕的驱动(至少要有画点函数,因为要对接接口)。

LVGL有三大种需要对接的API,可以互相独立使用

  1. 显示API(这个必须有,不然上lvgl就没意义了)
  2. 输入设备API(比如触摸屏、按键 等)
  3. 文件系统API(如FatFs等)

这篇文章只讲“显示API”的移植。其他两个后面出其他文章讲。

重要) 编译LVGL至少需要c99标准



移植和启动LVGL思路:

  • 实现画点函数(绘制越快越好)。
  • 为Keil项目添加LVGL文件。
  • 在几个port文件里对接API。(port文件就是API对接的端口文件,如lv_port_disp.c)
  • 使用LVGL文档的例程(不是demo)。
  • 循环调用lv_tick_inc()和lv_task_handler()(如果上了OS则分别为这两个创建任务)。
  • 调用lvgl的初始化函数(一个主初始化和三个port初始化)。

一、为项目添加LVGL源文件

(一) 下载源码:

1.使用git命令:

git clone https://github.com/lvgl/lvgl.git

2.去LVGL的github页:https://github.com/lvgl/lvgl 下载。

(二) 在keil添加文件:

在这里插入图片描述

  • lvgl/src :源码,所有源码都在项目根目录的src文件夹里,自行添加即可。(比较多,不列出来)
  • lvgl/port :port文件,对接API的地方,在 lvgl/examples/porting/中
  • lvgl/user:用户文件,放你自己的东西,我放了lvgl头文件和配置文件,方便编辑。

下载到的源码里,只用下图红圈的文件,其他不用管:
在这里插入图片描述

  1. 其中 lv_conf_template.h 文件名改为 lv_conf.h 并移动到上级目录。(必须)
  2. 把lvgl/examples/里的 porting文件夹复制出来
  3. lvgl.h 不动

整理好后的文件结构如下:

在这里插入图片描述


(三) 改port文件的文件名并使能port的使用:
1.改名

Q:为什么要改文件名?
A:因为默认名有_tamplate后缀,当然不改也能用,编译器也能编,但代码是人看的,改掉比较好,把后缀_tamplate删除,文件内也要改。


默认名:
在这里插入图片描述
改后名:
在这里插入图片描述

2.使能port的使用 (重要

Q:如何使能?
A:在port文件中把“#if 0”改为“#if 1”,c文件和h文件都有这个,其实就是预处理。


(四) 显示API 对接(重要):

我们打开文件 “lv_port_disp.c”,其中的函数:lv_port_disp_init() 就是我们后面要调用的显示设备初始化函数,先记着,我们来看这个函数的内容:


void lv_port_disp_init(void)
{
    
    
    /*-------------------------
     * Initialize your display
     * -----------------------*/
    // 这个就是我们的显示器初始化函数应该放的地方,函数定义在下面
    disp_init();		

    /*-----------------------------
     * Create a buffer for drawing
     *----------------------------*/

    /* LittlevGL requires a buffer where it draws the objects. The buffer's has to be greater than 1 display row
     *
     * There are three buffering configurations:
     * 1. Create ONE buffer with some rows: 
     *      LittlevGL will draw the display's content here and writes it to your display
     * 
     * 2. Create TWO buffer with some rows: 
     *      LittlevGL 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 LittlevGL 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. Create TWO screen-sized buffer: 
     *      Similar to 2) but the buffer have to be screen sized. When LittlevGL is ready it will give the
     *      whole frame to display. This way you only need to change the frame buffer's address instead of
     *      copying the pixels.
     * */

#define EXAMPLE 2
    
#if  ( EXAMPLE == 1 )
    /* Example for 1) */
    static lv_disp_buf_t disp_buf_1;
    static lv_color_t buf1_1[LV_HOR_RES_MAX * 10];                      /*A buffer for 10 rows*/
    lv_disp_buf_init(&disp_buf_1, buf1_1, NULL, LV_HOR_RES_MAX * 10);   /*Initialize the display buffer*/
#endif

#if  ( EXAMPLE == 2 )
    /* Example for 2) */
    static lv_disp_buf_t disp_buf_2;
    static lv_color_t buf2_1[LV_HOR_RES_MAX * 10];                        /*A buffer for 10 rows*/
    static lv_color_t buf2_2[LV_HOR_RES_MAX * 10];                        /*An other buffer for 10 rows*/
    lv_disp_buf_init(&disp_buf_2, buf2_1, buf2_2, LV_HOR_RES_MAX * 10);   /*Initialize the display buffer*/
#endif

#if  ( EXAMPLE == 3 )
    /* Example for 3) */
    static lv_disp_buf_t disp_buf_3;
    static lv_color_t buf3_1[LV_HOR_RES_MAX * LV_VER_RES_MAX];            /*A screen sized buffer*/
    static lv_color_t buf3_2[LV_HOR_RES_MAX * LV_VER_RES_MAX];            /*An other screen sized buffer*/
    lv_disp_buf_init(&disp_buf_3, buf3_1, buf3_2, LV_HOR_RES_MAX * LV_VER_RES_MAX);   /*Initialize the display buffer*/
#endif

    /*-----------------------------------
     * Register the display in LittlevGL
     *----------------------------------*/

    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*/
#if  ( EXAMPLE == 1 )
    disp_drv.buffer = &disp_buf_1;
#endif
#if  ( EXAMPLE == 2 )
    disp_drv.buffer = &disp_buf_2;
#endif
#if  ( EXAMPLE == 3 )
    disp_drv.buffer = &disp_buf_3;
#endif

#if LV_USE_GPU
    /*Optionally add functions to access the GPU. (Only in buffered mode, LV_VDB_SIZE != 0)*/

    /*Blend two color array using opacity*/
    disp_drv.gpu_blend_cb = gpu_blend;

    /*Fill a memory array with a color*/
    disp_drv.gpu_fill_cb = gpu_fill;
#endif

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

/* Initialize your display and the required peripherals. */
static void disp_init(void)
{
    
    
    /*You code here*/
    scr_init();		// 你的屏幕的初始化函数
}

首先我们第一个需要改的地方就是EXAMPLE的地方,添加了一些预处理语句,分别是三种不同的缓冲方式,源码是没有这些宏定义的,如果不想自己改,直接复制上面我的即可,调整宏使用不同的缓冲方式。

第二就是下面的代码,LV_HOR_RES_MAX和LV_VER_RES_MAX定义在“lv_conf.h”中,
原port文件没有使用这两个宏定义,分别代表显示器的宽和高。

/*Set the resolution of the display*/
    disp_drv.hor_res = LV_HOR_RES_MAX;
    disp_drv.ver_res = LV_VER_RES_MAX;

我们继续往下看,找到disp_flush函数的定义,参考我下面的改法。

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*/

    u16 x, y;
    
    u16 x1 = area->x1;
    u16 y1 = area->y1;
    u16 x2 = area->x2;
    u16 y2 = area->y2;
    
    // 设置显示区域,函数是你自己实现的,这是我改后的
    // 例如ST7735的驱动就可以设置显示绘制区域
    set_region( x1, y1, x2, y2 );
    
    for(y = y1; y <= y2; y++) {
    
    
        for(x = x1; x <= x2; x++) {
    
    
            /* Put a pixel to the display. For example: */
            /* put_px(x, y, *color_p)*/
            // 画点函数,例如常见的一些屏用的是16位颜色,你把16位数据输出到屏幕即可
            send_pixel_dat( color_p->full );
            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)

他默认没声明,也不知道为什么。
那么做到这里,你已经完成了显示API的对接了,接下来可以启动LVGL了。


(五) 启动LVGL:
1.启动

到了这步就很简单了,按照下面顺序在main函数中调用几个初始化函数即可。但是光这样还不能使用LVGL,因为他没有“心跳”。

	lv_init();
    lv_port_disp_init();        // 显示器初始化
    lv_port_indev_init();       // 输入设备初始化(如果没有实现就注释掉)
    lv_port_fs_init();          // 文件系统设备初始化(如果没有实现就注释掉)
2. 让他心跳

我们还需要调用两个函数

 1. lv_tick_inc( tick );		 // tick单位是ms,设置为5ms即可,一般只要有值就行。
 2. lv_task_handler();		 	// 这个比较重要,从名字就能知道他是用来运行lvgl的task的

有两种方式让他心跳:

(1) 不使用OS:
在main函数弄while死循环

#include <rtthread.h>
#include <lvgl.h>

#define LVGL_TICK 	5

static void lvgl_init( void ) 
{
    
    
    lv_init();
    lv_port_disp_init();        // 显示器初始化
    // lv_port_indev_init();       // 输入设备初始化
    // lv_port_fs_init();          // 文件系统设备初始化
}

int main()
{
    
    
	lvgl_init();

	while(1) {
    
    
		// 先调用 lv_tick_inc 再调用 lv_task_handler
		lv_tick_inc(LVGL_TICK);
		lv_task_handler();
		delay_ms(LVGL_TICK);	// 可以省略,lvgl并不是OS的真正任务
	}
}

(2) 使用OS(以rt-thread os为例,其他OS类似):
代码比较长,主要就是创建两个任务分别运行就可以。

#include <rtthread.h>
#include <lvgl.h>

#define APP_THREAD_NUM 	5
// 动态线程堆
static rt_thread_t u_threadx[APP_THREAD_NUM] = {
    
    RT_NULL};
// 静态线程堆
static struct rt_thread u_static_threadx[APP_THREAD_NUM];

/* lvgl tick线程 */
#define LVGL_TICK   10
#define LVGL_TICK_THREAD_NAME   "lvgl_tick"         // 线程名
#define LVGL_TICK_STACK_SIZE    256                // 线程栈大小
#define LVGL_TICK_TIME_SLICE    5                   // 线程时间片
#define LVGL_TICK_PRIOROTY      10                  // 线程优先级
static rt_thread_t *lvgl_tick_th = &u_threadx[1];      // 从线程堆分配线程
static void lvgl_tick_thread( void *param )
{
    
    
    param = param;
    while ( 1 ) {
    
    
        lv_tick_inc(LVGL_TICK);
        rt_thread_mdelay(LVGL_TICK);
    }
}

/* lvgl task handler线程 */
#define LVGL_TASK_THREAD_NAME   "lvgl_task"                         // 线程名
#define LVGL_TASK_STACK_SIZE    2048                                // 线程栈大小
#define LVGL_TASK_TIME_SLICE    10                                  // 线程时间片
#define LVGL_TASK_PRIOROTY      10                                  // 线程优先级
ALIGN(RT_ALIGN_SIZE) static u8 lvgl_task_stk[LVGL_TASK_STACK_SIZE]; // 线程栈
static struct rt_thread *lvgl_task_th_s = &u_static_threadx[3];     // 从线程堆分配线程
static void lvgl_task_thread( void *param )
{
    
    
    param = param;
    while ( 1 ) {
    
    
        lv_task_handler();
        rt_thread_mdelay(LVGL_TICK);
    }
}

static void lvgl_init( void ) 
{
    
    
    lv_init();
    lv_port_disp_init();        // 显示器初始化
    // lv_port_indev_init();       // 输入设备初始化
    // lv_port_fs_init();          // 文件系统设备初始化
}

/**************************************************/
int main()
{
    
    
	lvgl_init();

	/*  创建lvgl tick动态线程 */
    *lvgl_tick_th = rt_thread_create( 
        LVGL_TICK_THREAD_NAME,        /*线程名字*/                    
        lvgl_tick_thread,             /*线程入口函数*/
        RT_NULL,                      /*线程入口函数参数*/
        LVGL_TICK_STACK_SIZE,         /*线程栈大小*/
        LVGL_TICK_PRIOROTY ,          /*线程优先级*/
        LVGL_TICK_TIME_SLICE          /*线程时间片*/
    );               
    if(lvgl_tick_th !=RT_NULL)
        rt_thread_startup (*lvgl_tick_th);
    else
        return -1;

    /*  创建lvgl task 静态线程 */
    err = rt_thread_init(  
        lvgl_task_th_s,                 
        LVGL_TASK_THREAD_NAME,          /*线程名字*/ 
        lvgl_task_thread,               /*线程入口函数*/
        RT_NULL,                        /*线程入口函数*/
        lvgl_task_stk,                  /*线程栈*/
        LVGL_TASK_STACK_SIZE,           /*线程栈大小*/
        LVGL_TASK_PRIOROTY,
        LVGL_TASK_TIME_SLICE
    );
    if ( err == RT_EOK ) 
        rt_thread_startup (lvgl_task_th_s);
    else
        rt_kprintf( "create thread \"%s\" error. (%d)\n", LVGL_TASK_THREAD_NAME, err );

	return 0;
}

(六) 简单的配置lv_conf.h文件:

lv_conf.h 中把“#if 0”改为“#if 1”

里面比较重要的地方有三处:

1. 屏幕尺寸宏定义
// 改为你的屏幕尺寸
#define LV_HOR_RES_MAX          (128)	// 水平	(X)
#define LV_VER_RES_MAX          (128)	// 垂直	(Y)
2. memory宏定义

找到下面这两个宏定义

#define LV_MEM_SIZE (48U * 1024U)	// 定义LVGL使用的内存大小,我定义了48k
#define LV_MEM_ADR  0				// 定义LVGL使用的内存地址,设置为0或手动设置
3. 兼容宏定义
/*1: Use the functions and types from the older API if possible */
#define LV_USE_API_EXTENSION_V6  0		// 兼容v6版本的API(默认关闭)
#define LV_USE_API_EXTENSION_V7  1		// 兼容v7版本的API(默认开启)

至此,我们完成了LVGL最基本的移植工作,我们接下来可以使用一些官方文档里的例程,看看效果。


(七) 使用文档例程(不是demo)

前面提到“官方文档”,其实就是lvgl的数据手册,不同版本的数据手册不通用,如lvgl v7和v6有大部分函数不一样。

lv_ex_label_1例程创建了一个长滚动条,当你的字符总长度大于屏幕宽度就会滚动。

#include <rtthread.h>
#include <lvgl.h>

#define LVGL_TICK 	5

static void lv_ex_label_1(void)
{
    
    
    lv_obj_t * label2 = lv_label_create(lv_scr_act(), NULL);
    lv_label_set_recolor(label2, true);
    lv_label_set_long_mode(label2, LV_LABEL_LONG_SROLL_CIRC); /*Circular scroll*/
    lv_obj_set_width(label2, 120);
    // Hello world ! Trisuborn.
    lv_label_set_text(label2, "#ff0000 Hello# #00ff00 world ! Trisuborn.#");
    lv_obj_align(label2, NULL, LV_ALIGN_CENTER, 0, 0);
}

static void lvgl_init( void ) 
{
    
    
    lv_init();
    lv_port_disp_init();        // 显示器初始化
    // lv_port_indev_init();       // 输入设备初始化
    // lv_port_fs_init();          // 文件系统设备初始化
}

int main()
{
    
    
	lvgl_init();

	lv_ex_label_1();

	while(1) {
    
    
		// 先调用 lv_tick_inc 再调用 lv_task_handler
		lv_tick_inc(LVGL_TICK);
		lv_task_handler();
		delay_ms(LVGL_TICK);	// 可以省略,lvgl并不是OS的真正任务
	}
}

(八) 效果图GIF

lvgl


可以参考我的项目:
https://github.com/Trisuborn/mp3-lvgl-stm32f405rgt


本篇完


其他:

LittleVGL (LVGL)干货入门教程一之移植到stm32芯片

LittleVGL (LVGL)干货入门教程二之LVGL的输入设备(indev)API对接。

LittleVGL (LVGL)干货入门教程三之LVGL的文件系统(fs)API对接。

LittleVGL (LVGL)干货入门教程四之制作和使用中文汉字字库


下一篇:

LittleVGL (LVGL)干货入门教程二之LVGL的输入设备(indev)API对接

猜你喜欢

转载自blog.csdn.net/qq_26106317/article/details/109666444