【LVGL应用开发--基于STM32】第2章 LVGL无操作系统移植


前言

在前面章节中,我们已经了解 LVGL移植要求以及 LVGL图形库下载路径。本章主要讲解裸机移植LVGL到STM32开发板上。本章分为如下几部分内容:
2.1 移植准备工作
2.2 向工程添加文件
2.3 修改工程文件
2.4 移植官方例程
2.5 实验现象


2.1 移植准备工作

在移植 LVGL之前,用户需要准备一个裸机的工程,可以直接在基础实验例程内寻找“触摸屏实验”,也可以直接在LVGL实验例程内使用准备好的“0-移植LVGL基础工程”,笔者建议使用准备好的工程。因为 LVGL库内含自己内存管理算法,该管理算法的管理内存是由用户分配给它的,所以用户分配内存方式有两种,一种是内部 SRAM 分配方式,而另一种是外部 SRAM 分配方式。下面我们开始讲解移植准备工作的流程。
(1)准备LVGL源码
LVGL 源码可从 LVGL 官方 GitHub 网址(https://github.com/lvgl/lvgl/)下载。当然我们也可以在资料下获取,该获取路径为:“\4–实验程序\4–GUI人机实验\LVGL实验\LVGL源码及工具\lvgl-master.zip”。该压缩包解压后得到下图的文件和文件夹。
在这里插入图片描述

上图中的文件和文件夹我们在前面章节已经介绍过了,这里笔者无需重复介绍。
(2)准备LVGL源码
把 lv_conf_template.h 文件名修改成 lv_conf.h 文件名。
(3)打开 lv_conf.h 文件,修改条件编译指令,如下源码所示。
修改:
在这里插入图片描述

修改成如下所示:
在这里插入图片描述

前面我们也讲解过,LVGL 移植所需要的文件夹和文件有 examples 文件夹、src 文件夹、lv_conf_template.h 和 lvgl.h 文件,其他文件和文件夹与移植无关,我们可以删除其他文件和文件夹。如下所示:
在这里插入图片描述

(4)打开 examples 文件夹,除了 porting 文件夹外,用户可以删除其他文件和文件夹,如下图所示。
在这里插入图片描述

根据上述步骤的操作,我们可得到 LVGL 移植的简洁源码。


2.2 向工程添加文件

2.2.1. 准备基础工程

在移植之前我们需要一个基础工程,前面我们已准备好工程,我们在这个工程的基础上完成本章的 LVGL 移植。
首先我们把“0-移植LVGL基础工程”复制一份,并重命名为“1-LVGL无操作系统移植”,然后在该工程目录下新建 Middlewares 文件夹,并在该文件夹下新建 LVGL 文件夹,其次在该文件夹下再新建 GUI 文件夹和 GUI_APP 文件夹,在这两个文件夹下也分别新建 lvgl文件夹,lvgl 文件夹保存前面所精简后的LVGL源码的文件和文件夹,而 GUI_APP 文件夹保存用户编写的文件。
根据上述所述,可得到一个文件树形图,如下图所示:
在这里插入图片描述

我们的工程为什么使用这样的文件结构?因为 LVGL 的源文件内声明 lvgl.h 文件都是以“#include “…/…/lvgl.h””这样的形式声明的,显然这个文件的声明是放在第三层路径下,所以笔者把 LVGL 源码放在上图中的 lvgl 文件夹下,这样就符合了 LVGL 声明路径的风格。

2.2.2. 把LVGL源码移植到工程中

根据上图所示,把下图中的文件和文件夹复制到Middlewaes/LVGL/GUI/lvgl 路径下,如下图所示:
在这里插入图片描述

2.2.3. 向工程添加文件

打开“1-LVGL无操作系统移植”工程,添加下图分组:
在这里插入图片描述

打开工程下的 Middlewares/LVGL/GUI/lvgl/src 文件夹,我们会发现该文件夹下的文件夹名称与上图的分组名称相似,如下图所示:
在这里插入图片描述

根据上图所示,这些分组需要添加的文件如下所示:
(1) Middlewares/lvgl/src/core 组添加 core 文件夹下的全部.c 文件,如下图所示:
在这里插入图片描述

(2) Middlewares/lvgl/src/draw 组添加 draw 文件夹下除 nxp_pxp、 nxp_vglite、sdl 和stm32_dma2d 文件夹之外的全部.c 文件,如下图所示:
在这里插入图片描述

(3) Middlewares/lvgl/src/extra 组添加 extra 文件夹下除libs文件夹外的全部.c 文件,如下图所示:
在这里插入图片描述

(4) Middlewares/lvgl/src/font 组添加 font 文件夹下的全部.c 文件,如下图所示:
在这里插入图片描述

(5) Middlewares/lvgl/src/gpu 组添加 draw/stm32_dma2d 和 draw/sdl 文件夹下的全部.c 文件,如下图所示:
在这里插入图片描述

(6) Middlewares/lvgl/src/hal 组添加 hal 文件夹下的全部.c 文件,如下图所示:
在这里插入图片描述

(7) Middlewares/lvgl/src/misc 组添加 misc 文件夹下的全部.c 文件,如下图所示:
在这里插入图片描述

(8) Middlewares/lvgl/src/widgets 组添加 widgets 文件夹下的全部.c 文件,如下图所示:
在这里插入图片描述

(9) Middlewares/lvgl/examples/porting 组添加Middlewares/LVGL/GUI/lvgl/examples/porting目录下的 lv_port_disp_template.c 和 lv_port_indev_template.c 文件,如下图所示:
在这里插入图片描述

上图中的.c 文件与 LCD 和触摸驱动相关,这些文件我们在后面具体移植会讲解。

2.2.4. 添加文件路径

看到上述的步骤,很多人以为移植 LVGL需要添加很多文件路径,其实不然,我们只添加关键路径即可,因为 lvgl.h 文件已经为我们省去这部分的操作,如图所示:
在这里插入图片描述

2.2.5. Keil5忽略特定的警告

在 Misc Controls 中填入“–diag_suppress=68 --diag_suppress=111 --diag_suppress=188 --diag_suppress=223 --diag_suppress=546 --diag_suppress=1295”。并且一定要勾选“C99 Mode”,否则编译会出现一大堆错误。如下所示:
在这里插入图片描述

这个方法有效消除 LVGL 带来的警告。如果我们不加这些命令,可能编译代码时会出现很多个警告,当然,这些警告也不会影响我们工程运行结果。到了这一步,我们可以编译工程,工程编译是没有报错的,这样我们移植完成了吗,显然不是,我们还没有编写显示屏和触摸驱动与 LVGL图形库衔接,更没有提供 LVGL时基等操作,万事俱备,只欠东风。


2.3 修改工程文件

这里我们只需要“一加两改”。“一加”是指添加定时器提供 LVGL的时基,“两改”是指修改 LVGL 显示驱动文件(lv_port_disp_templ.c/h)和输入设备文件(lv_port_indev_templ.c/h)。

2.3.1 添加LVGL时基

在APP分组下添加定时器驱动文件,如下图所示:
在这里插入图片描述

打开上图的time.c 文件,声明 LVGL 的头文件,如下源码所示:

#include "lvgl.h"

在该文件下的 TIM4_IRQHandler()函数添加以下源码:

/*******************************************************************************
* 函 数 名         : TIM4_IRQHandler
* 函数功能		   : TIM4中断函数
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void TIM4_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM4,TIM_IT_Update))
	{
//		LED2=!LED2;
		lv_tick_inc(1);//lvgl的1ms心跳
	}
	TIM_ClearITPendingBit(TIM4,TIM_IT_Update);	
}

上述源码中,定时器中断服务函数调用了 LVGL 的 lv_tick_inc 函数,该函数让 LVGL 内部一个时基参数加 1。在 main 函数中,对定时器进行初始化操作,注意:根据自己的 MCU 开启定时器,定时器周期 1ms。对于STM32F103来说,调用定时器设置1ms函数调用如下:

TIM4_Init(999,71);

2.3.2 向LVGL关联彩屏驱动和触摸

在文件 lv_port_disp_template.c/h 和 lv_port_indev_template.c/h 的条件编译指令#if 0 都修改成#if 1,如下源码所示:
修改:

#if 0 /* lv_port_disp_template.c 或者.h 和 lv_port_indev_template.c 或者.h */

修改为:

#if 1 /* 把#if 0 修改成#if 1 */

2.3.2.1 lv_port_disp_template.c 文件修改

该文件作用是让自己的显示屏驱动文件与 LVGL显示驱动做衔接操作。首先打开官方提供的 lv_port_disp_template.c文件,我们会发现官方提供三种方法定义绘图缓冲区,该绘画缓存区主要渲染屏幕内容,定义缓冲区越大,刷新帧率越快。如下源码所示:

/*-----------------------------
* Create a buffer for drawing
*----------------------------*/
/* LVGL requires a buffer where it internally draws the widgets.
* Later this buffer will passed your display drivers `flush_cb` 
to copy its content to your display.
* The buffer has to be greater than 1 display row
*
* There are three buffering configurations:
* 1. Create ONE buffer with some rows:
* LVGL will draw the display's content here and writes it to your display
*
* 2. Create TWO buffer with some rows:
* 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. Create TWO screen-sized buffer:
* Similar to 2) but the buffer have to be screen sized. 
When LVGL 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.
* */
 
/*-----------------------------
 * 创建绘图缓冲区
 *----------------------------*/
 
 
 /* LVGL 需要一个内部绘制小部件的缓冲区。
 然后这个缓冲区会传递给你的显示驱动' flush_cb '来复制它的内容到你的显示。
 缓冲区必须大于 1 显示行
** 有三种缓冲配置:
* 1. 创建一个缓冲区:
* LVGL 将在这里绘制显示的内容,并将其写入到显示
*
* 2. 创建两个缓冲区:
* LVGL 将绘制显示的内容到缓冲区,并写入显示.
* 应该使用 DMA 将缓冲区的内容写入显示.
* 它将使 LVGL 绘制下一部分的屏幕到另一个缓冲区,而同时
* 数据正在从第一个缓冲区发送。它使呈现和刷新并行.
*
* 3.创建两个屏幕大小的缓冲区:
* 类似于第二种,但缓冲区必须是屏幕大小。当 LVGL 准备好了,它会给整帧显示。
这样你只需要改变帧缓冲区的地址而不是复制像素
* */

根据上述所示,LVGL 定义绘图缓冲区具有三种类型,我们的LVGL例程都是采用第一种方式定义绘图缓冲区,如下源码所示:

/**
 * @file lv_port_disp_templ.c
 *
 */

 /*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/
#if 1

/*********************
 *      INCLUDES
 *********************/
#include "lv_port_disp_template.h"
#include "../../lvgl.h"
/* 导入lcd驱动头文件 */
#include "tftlcd.h"

/*********************
 *      DEFINES
 *********************/
#define USE_SRAM        0       /* 使用外部sram为1,否则为0 */
#ifdef USE_SRAM
//#include "malloc.h"
#endif

#define MY_DISP_HOR_RES (800)   /* 屏幕宽度 */
#define MY_DISP_VER_RES (480)   /* 屏幕高度 */

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/
/* 显示设备初始化函数 */
static void disp_init(void);

/* 显示设备刷新函数 */
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
/* GPU 填充函数(使用GPU时,需要实现) */
//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
//        const lv_area_t * fill_area, lv_color_t color);

/**********************
 *  STATIC VARIABLES
 **********************/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/
/**
 * @brief       LCD加速绘制函数
 * @param       (sx,sy),(ex,ey):填充矩形对角坐标,区域大小为:(ex - sx + 1) * (ey - sy + 1)
 * @param       color:要填充的颜色
 * @retval      无
 */
void lcd_draw_fast_rgb_color(int16_t sx, int16_t sy,int16_t ex, int16_t ey, uint16_t *color)
{
    uint16_t w = ex-sx+1;
    uint16_t h = ey-sy+1;

    LCD_Set_Window(sx, sy, w, h);
    uint32_t draw_size = w * h;

    for(uint32_t i = 0; i < draw_size; i++)
    {
        LCD_WriteData_Color(color[i]);
    }
}

/**
 * @brief       初始化并注册显示设备
 * @param       无
 * @retval      无
 */
void lv_port_disp_init(void)
{
    /*-------------------------
     * 初始化显示设备
     * -----------------------*/
    disp_init();

    /*-----------------------------
     * 创建一个绘图缓冲区
     *----------------------------*/

    /**
     * LVGL 需要一个缓冲区用来绘制小部件
     * 随后,这个缓冲区的内容会通过显示设备的 `flush_cb`(显示设备刷新函数) 复制到显示设备上
     * 这个缓冲区的大小需要大于显示设备一行的大小
     *
     * 这里有3中缓冲配置:
     * 1. 单缓冲区:
     *      LVGL 会将显示设备的内容绘制到这里,并将他写入显示设备。
     *
     * 2. 双缓冲区:
     *      LVGL 会将显示设备的内容绘制到其中一个缓冲区,并将他写入显示设备。
     *      需要使用 DMA 将要显示在显示设备的内容写入缓冲区。
     *      当数据从第一个缓冲区发送时,它将使 LVGL 能够将屏幕的下一部分绘制到另一个缓冲区。
     *      这样使得渲染和刷新可以并行执行。
     *
     * 3. 全尺寸双缓冲区
     *      设置两个屏幕大小的全尺寸缓冲区,并且设置 disp_drv.full_refresh = 1。
     *      这样,LVGL将始终以 'flush_cb' 的形式提供整个渲染屏幕,您只需更改帧缓冲区的地址。
     */

    /* 单缓冲区示例) */
    static lv_disp_draw_buf_t draw_buf_dsc_1;
#if USE_SRAM
    static lv_color_t buf_1 = mymalloc(SRAMEX, MY_DISP_HOR_RES * MY_DISP_VER_RES);              /* 设置缓冲区的大小为屏幕的全尺寸大小 */
    lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * MY_DISP_VER_RES);     /* 初始化显示缓冲区 */
#else
    static lv_color_t buf_1[MY_DISP_HOR_RES * 10];                                              /* 设置缓冲区的大小为 10 行屏幕的大小 */
    lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10);                  /* 初始化显示缓冲区 */
#endif

    /* 双缓冲区示例) */
//    static lv_disp_draw_buf_t draw_buf_dsc_2;
//    static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10];                                            /* 设置缓冲区的大小为 10 行屏幕的大小 */
//    static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10];                                            /* 设置另一个缓冲区的大小为 10 行屏幕的大小 */
//    lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);             /* 初始化显示缓冲区 */

    /* 全尺寸双缓冲区示例) 并且在下面设置 disp_drv.full_refresh = 1 */
//    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];                               /* 设置一个全尺寸的缓冲区 */
//    static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES];                               /* 设置另一个全尺寸的缓冲区 */
//    lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2, MY_DISP_HOR_RES * MY_DISP_VER_RES);/* 初始化显示缓冲区 */

    /*-----------------------------------
     * 在 LVGL 中注册显示设备
     *----------------------------------*/

    static lv_disp_drv_t disp_drv;                  /* 显示设备的描述符 */
    lv_disp_drv_init(&disp_drv);                    /* 初始化为默认值 */

    /* 建立访问显示设备的函数  */

    /* 设置显示设备的分辨率
     * 这里为了适配多款屏幕,采用了动态获取的方式,
     * 在实际项目中,通常所使用的屏幕大小是固定的,因此可以直接设置为屏幕的大小 */
    disp_drv.hor_res = tftlcd_data.width;
    disp_drv.ver_res = tftlcd_data.height;

    /* 用来将缓冲区的内容复制到显示设备 */
    disp_drv.flush_cb = disp_flush;

    /* 设置显示缓冲区 */
    disp_drv.draw_buf = &draw_buf_dsc_1;

    /* 全尺寸双缓冲区示例)*/
    //disp_drv.full_refresh = 1

    /* 如果您有GPU,请使用颜色填充内存阵列
     * 注意,你可以在 lv_conf.h 中使能 LVGL 内置支持的 GPUs
     * 但如果你有不同的 GPU,那么可以使用这个回调函数。 */
    //disp_drv.gpu_fill_cb = gpu_fill;

    /* 注册显示设备 */
    lv_disp_drv_register(&disp_drv);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/**
 * @brief       初始化显示设备和必要的外围设备
 * @param       无
 * @retval      无
 */
static void disp_init(void)
{
    /*You code here*/
//    lcd_init();         /* 初始化LCD */
//    lcd_display_dir(1); /* 设置横屏 */
}

/**
 * @brief       将内部缓冲区的内容刷新到显示屏上的特定区域
 *   @note      可以使用 DMA 或者任何硬件在后台加速执行这个操作
 *              但是,需要在刷新完成后调用函数 'lv_disp_flush_ready()'
 *
 * @param       disp_drv    : 显示设备
 *   @arg       area        : 要刷新的区域,包含了填充矩形的对角坐标
 *   @arg       color_p     : 颜色数组
 *
 * @retval      无
 */
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    /* LVGL 官方给出的一个打点刷新屏幕的例子,但这个效率是最低效的 */

//    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++;
//        }
//    }

//    /* 在指定区域内填充指定颜色块 */
    LCD_Color_Fill(area->x1, area->y1, area->x2, area->y2, (uint16_t *)color_p);
//    lcd_draw_fast_rgb_color(area->x1,area->y1,area->x2,area->y2,(uint16_t*)color_p);

    /* 重要!!!
     * 通知图形库,已经刷新完毕了 */
    lv_disp_flush_ready(disp_drv);
}

/* 可选: GPU 接口 */

/* 如果你的 MCU 有硬件加速器 (GPU) 那么你可以使用它来为内存填充颜色 */
/**
 * @brief       使用 GPU 进行颜色填充
 *   @note      如有 MCU 有硬件加速器 (GPU),那么可以用它来为内存进行颜色填充
 *
 * @param       disp_drv    : 显示设备
 *   @arg       dest_buf    : 目标缓冲区
 *   @arg       dest_width  : 目标缓冲区的宽度
 *   @arg       fill_area   : 填充的区域
 *   @arg       color       : 颜色数组
 *
 * @retval      无
 */
//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
//                    const lv_area_t * fill_area, lv_color_t color)
//{
//    /*It's an example code which should be done by your GPU*/
//    int32_t x, y;
//    dest_buf += dest_width * fill_area->y1; /*Go to the first line*/

//    for(y = fill_area->y1; y <= fill_area->y2; y++) {
//        for(x = fill_area->x1; x <= fill_area->x2; x++) {
//            dest_buf[x] = color;
//        }
//        dest_buf+=dest_width;    /*Go to the next line*/
//    }
//}


#else /*Enable this file at the top*/

/*This dummy typedef exists purely to silence -Wpedantic.*/
typedef int keep_pedantic_happy;
#endif

从上述源码可知,我们知道配置 LVGL 显示屏驱动步骤如下:
根据 USE_SRAM 配置项选择内存类型,内存类型分别选择外部 SRAM 和内部 SRAM(笔者建议使用内部 SRAM 分配)。
(1) 调用函数 disp_init 初始化 LCD 驱动(可以省略,在main开头已调用)。
(2) 调用函数 lv_disp_draw_buf_init 初始化缓冲区(最低标准:显示屏的宽度分配率*10)。
(3) 调用函数 lv_disp_drv_init 初始化显示驱动。
(4) 设置显示的高度与宽度。
(5) 注册显示驱动回调。
(6) 设置显示驱动的绘画缓冲区。
(7) 调用 lv_disp_drv_register 函数注册显示驱动到 LVGL 列表中。
它们的作用就是把 LVGL 显示屏驱动与自己的 LCD 驱动衔接起来,在这个文件中我们只提供两个函数,它们分别为 LCD 初始化函数和LCD 填充函数。
注意:如果用户使用 DMA2D 外设,请大家在 lv_conf.h 文件中的 LV_USE_GPU_STM32_DMA2D 宏定义置 1 并在 LV_GPU_DMA2D_CMSIS_INCLUDE 添加 MCU 的头文件路径,对于STM32F103和STM32407是没有该外设资源,所以此处忽略。

2.3.2.2 lv_port_indev_template.c 文件修改

该文件的作用是添加输入设备,例如触摸设备、鼠标、键盘、编码器、按键等输入设备,针对我们的例程来讲:只添加触摸输入设备即可,如下源码所示:

/**
 * @file lv_port_indev_templ.c
 *
 */

 /*Copy this file as "lv_port_indev.c" and set this value to "1" to enable content*/
#if 1

/*********************
 *      INCLUDES
 *********************/
#include "lv_port_indev_template.h"
#include "../../lvgl.h"
/* 导入屏幕触摸驱动头文件 */
#include "touch.h"

/*********************
 *      DEFINES
 *********************/

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/

/* 触摸屏 */
//static void touchpad_init(void);
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
//static bool touchpad_is_pressed(void);
//static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y);

/* 鼠标 */
//static void mouse_init(void);
//static void mouse_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
//static bool mouse_is_pressed(void);
//static void mouse_get_xy(lv_coord_t * x, lv_coord_t * y);

/* 键盘 */
//static void keypad_init(void);
//static void keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
//static uint32_t keypad_get_key(void);

/* 编码器 */
//static void encoder_init(void);
//static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
//static void encoder_handler(void);

/* 按钮 */
//static void button_init(void);
//static void button_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
//static int8_t button_get_pressed_id(void);
//static bool button_is_pressed(uint8_t id);

/**********************
 *  STATIC VARIABLES
 **********************/
lv_indev_t * indev_touchpad;    // 触摸屏
//lv_indev_t * indev_mouse;       // 鼠标
//lv_indev_t * indev_keypad;      // 键盘
//lv_indev_t * indev_encoder;     // 编码器
//lv_indev_t * indev_button;      // 按钮

/* 编码器相关 */
//static int32_t encoder_diff;
//static lv_indev_state_t encoder_state;

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

/**
 * @brief       初始化并注册输入设备
 * @param       无
 * @retval      无
 */
void lv_port_indev_init(void)
{
    /**
     * 
     * 在这里你可以找到 LittlevGL 支持的出入设备的实现示例:
     *  - 触摸屏
     *  - 鼠标 (支持光标)
     *  - 键盘 (仅支持按键的 GUI 用法)
     *  - 编码器 (支持的 GUI 用法仅包括: 左, 右, 按下)
     *  - 按钮 (按下屏幕上指定点的外部按钮)
     *
     *  函数 `..._read()` 只是示例
     *  你需要根据具体的硬件来完成这些函数
     */

    static lv_indev_drv_t indev_drv;

    /*------------------
     * 触摸屏
     * -----------------*/

    /* 初始化触摸屏(如果有) */
//    touchpad_init();

    /* 注册触摸屏输入设备 */
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;
    indev_drv.read_cb = touchpad_read;
    indev_touchpad = lv_indev_drv_register(&indev_drv);

    /*------------------
     * 鼠标
     * -----------------*/

    /* 初始化鼠标(如果有) */
//    mouse_init();

    /* 注册鼠标输入设备 */
//    lv_indev_drv_init(&indev_drv);
//    indev_drv.type = LV_INDEV_TYPE_POINTER;
//    indev_drv.read_cb = mouse_read;
//    indev_mouse = lv_indev_drv_register(&indev_drv);

    /* 设置光标,为了简单起见,现在设置为一个 HOME 符号 */
//    lv_obj_t * mouse_cursor = lv_img_create(lv_scr_act());
//    lv_img_set_src(mouse_cursor, LV_SYMBOL_HOME);
//    lv_indev_set_cursor(indev_mouse, mouse_cursor);

    /*------------------
     * 键盘
     * -----------------*/

//    /* 初始化键盘(如果有) */
//    keypad_init();

//    /* 注册键盘输入设备 */
//    lv_indev_drv_init(&indev_drv);
//    indev_drv.type = LV_INDEV_TYPE_KEYPAD;
//    indev_drv.read_cb = keypad_read;
//    indev_keypad = lv_indev_drv_register(&indev_drv);

//    /* 接着你需要用 `lv_group_t * group = lv_group_create()` 来创建组
//     * 用 `lv_group_add_obj(group, obj)` 往组中添加物体
//     * 并将这个输入设备分配到组中,以导航到它:
//     * `lv_indev_set_group(indev_keypad, group);` */

    /*------------------
     * 编码器
     * -----------------*/

//    /* 初始化编码器(如果有) */
//    encoder_init();

//    /* 注册编码器输入设备 */
//    lv_indev_drv_init(&indev_drv);
//    indev_drv.type = LV_INDEV_TYPE_ENCODER;
//    indev_drv.read_cb = encoder_read;
//    indev_encoder = lv_indev_drv_register(&indev_drv);

//    /* 接着你需要用 `lv_group_t * group = lv_group_create()` 来创建组
//     * 用 `lv_group_add_obj(group, obj)` 往组中添加物体
//     * 并将这个输入设备分配到组中,以导航到它:
//     * `lv_indev_set_group(indev_keypad, group);` */

    /*------------------
     * 按钮
     * -----------------*/

//    /* 初始化按钮(如果有) */
//    button_init();

//    /* 注册按钮输入设备 */
//    lv_indev_drv_init(&indev_drv);
//    indev_drv.type = LV_INDEV_TYPE_BUTTON;
//    indev_drv.read_cb = button_read;
//    indev_button = lv_indev_drv_register(&indev_drv);

//    /* 为按钮分配屏幕上的点
//     * 以此来用按钮模拟点击屏幕上对应的点 */
//    static const lv_point_t btn_points[2] = {
//            {10, 10},   /*Button 0 -> x:10; y:10*/
//            {40, 100},  /*Button 1 -> x:40; y:100*/
//    };
//    lv_indev_set_button_points(indev_button, btn_points);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/*------------------
 * 触摸屏
 * -----------------*/

/**
 * @brief       初始化触摸屏
 * @param       无
 * @retval      无
 */
//static void touchpad_init(void)
//{
//    /*Your code comes here*/
//    tp_dev.init();
//    /* 电阻屏如果发现显示屏XY镜像现象,需要坐标矫正 */
//    if (0 == (tp_dev.touchtype & 0x80)) {
//        TP_Adjust();
//    }
//}

/**
 * @brief       图形库的触摸屏读取回调函数
 * @param       indev_drv   : 触摸屏设备
 *   @arg       data        : 输入设备数据结构体
 * @retval      无
 */
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    static uint16_t last_x = 0;
	static uint16_t last_y = 0;
	if(tp_dev.sta&TP_PRES_DOWN)//触摸按下了
	{
		last_x = tp_dev.x[0];
		last_y = tp_dev.y[0];
		data->point.x = last_x;
		data->point.y = last_y;
		data->state = LV_INDEV_STATE_PR;
	}else{
		data->point.x = last_x;
		data->point.y = last_y;
		data->state = LV_INDEV_STATE_REL;
	}
}

///**
// * @brief       获取触摸屏设备的状态
// * @param       无
// * @retval      返回触摸屏设备是否被按下
// */
//static bool touchpad_is_pressed(void)
//{
//    /*Your code comes here*/
//    tp_dev.scan(0);

//    if (tp_dev.sta & TP_PRES_DOWN)
//    {
//        return true;
//    }

//    return false;
//}

///**
// * @brief       在触摸屏被按下的时候读取 x、y 坐标
// * @param       x   : x坐标的指针
// *   @arg       y   : y坐标的指针
// * @retval      无
// */
//static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
//{
//    /*Your code comes here*/
//    (*x) = tp_dev.x[0];
//    (*y) = tp_dev.y[0];
//}

/*------------------
 * 鼠标
 * -----------------*/

/**
 * @brief       初始化鼠标
 * @param       无
 * @retval      无
 */
//static void mouse_init(void)
//{
//    /*Your code comes here*/
//    tp_dev.init();
//    /* 电阻屏如果发现显示屏XY镜像现象,需要坐标矫正 */
//    if (0 == (tp_dev.touchtype & 0x80))
//    {
//        tp_adjust();
//        tp_save_adjust_data();
//    }
//}

/**
 * @brief       图形库的鼠标读取回调函数
 * @param       indev_drv   : 鼠标设备
 *   @arg       data        : 输入设备数据结构体
 * @retval      无
 */
//static void mouse_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
//{
//    /* 获取当前的 x、y 坐标 */
//    mouse_get_xy(&data->point.x, &data->point.y);

//    /* 获取是否按下或释放鼠标按钮 */
//    if(mouse_is_pressed()) {
//        data->state = LV_INDEV_STATE_PR;
//    } else {
//        data->state = LV_INDEV_STATE_REL;
//    }
//}

/**
 * @brief       获取鼠标设备是否被按下
 * @param       无
 * @retval      返回鼠标设备是否被按下
 */
//static bool mouse_is_pressed(void)
//{
//    /*Your code comes here*/
//    tp_dev.scan(0);
//    
//    if (tp_dev.sta & TP_PRES_DOWN)
//    {
//        return true;
//    }
//    
//    return false;
//}

/**
 * @brief       当鼠标被按下时,获取鼠标的 x、y 坐标
 * @param       x   : x坐标的指针
 *   @arg       y   : y坐标的指针
 * @retval      无
 */
//static void mouse_get_xy(lv_coord_t * x, lv_coord_t * y)
//{
//    /*Your code comes here*/

//    (*x) = tp_dev.x[0];
//    (*y) = tp_dev.y[0];
//}

/*------------------
 * 键盘
 * -----------------*/

///**
// * @brief       初始化键盘
// * @param       无
// * @retval      无
// */
//static void keypad_init(void)
//{
//    /*Your code comes here*/
//}

///**
// * @brief       图形库的键盘读取回调函数
// * @param       indev_drv : 键盘设备
// *   @arg       data      : 输入设备数据结构体
// * @retval      无
// */
//static void keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
//{
//    static uint32_t last_key = 0;

    /* 这段代码是 LVGL 给出的例子,这里获取坐标好像是多余的 */
    /*Get the current x and y coordinates*/
    mouse_get_xy(&data->point.x, &data->point.y);

//    /* 获取按键是否被按下,并保存键值 */
//    uint32_t act_key = keypad_get_key();
//    if(act_key != 0) {
//        data->state = LV_INDEV_STATE_PR;

//        /* 将键值转换成 LVGL 的控制字符 */
//        switch(act_key) {
//        case 1:
//            act_key = LV_KEY_NEXT;
//            break;
//        case 2:
//            act_key = LV_KEY_PREV;
//            break;
//        case 3:
//            act_key = LV_KEY_LEFT;
//            break;
//        case 4:
//            act_key = LV_KEY_RIGHT;
//            break;
//        case 5:
//            act_key = LV_KEY_ENTER;
//            break;
//        }

//        last_key = act_key;
//    } else {
//        data->state = LV_INDEV_STATE_REL;
//    }

//    data->key = last_key;
//}

///**
// * @brief       获取当前正在按下的按键
// * @param       无
// * @retval      0 : 按键没有被按下
// */
//static uint32_t keypad_get_key(void)
//{
//    /*Your code comes here*/

//    return 0;
//}

/*------------------
 * 编码器
 * -----------------*/

///**
// * @brief       初始化编码器
// * @param       无
// * @retval      无
// */
//static void encoder_init(void)
//{
//    /*Your code comes here*/
//}


///**
// * @brief       图形库的编码器读取回调函数
// * @param       indev_drv : 编码器设备
// *   @arg       data      : 输入设备数据结构体
// * @retval      无
// */
//static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
//{

//    data->enc_diff = encoder_diff;
//    data->state = encoder_state;
//}

///**
// * @brief       在中断中调用此函数以处理编码器事件(旋转、按下)
// * @param       无
// * @retval      无
// */
//static void encoder_handler(void)
//{
//    /*Your code comes here*/

//    encoder_diff += 0;
//    encoder_state = LV_INDEV_STATE_REL;
//}

/*------------------
 * 按钮
 * -----------------*/


///**
// * @brief       初始化按钮
// * @param       无
// * @retval      无
// */
//static void button_init(void)
//{
//    /*Your code comes here*/
//}

///**
// * @brief       图形库的按钮读取回调函数
// * @param       indev_drv : 按钮设备
// *   @arg       data      : 输入设备数据结构体
// * @retval      无
// */
//static void button_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
//{

//    static uint8_t last_btn = 0;

//    /* 获取被按下按钮的ID */
//    int8_t btn_act = button_get_pressed_id();

//    if(btn_act >= 0) {
//        data->state = LV_INDEV_STATE_PR;
//        last_btn = btn_act;
//    } else {
//        data->state = LV_INDEV_STATE_REL;
//    }

//    /* 保存最后被按下按钮的ID */
//    data->btn_id = last_btn;
//}

///**
// * @brief       获取被按下按钮的ID
// * @param       无
// * @retval      被按下按钮的ID
// */
//static int8_t button_get_pressed_id(void)
//{
//    uint8_t i;

//    /* 检查那个按键被按下(这里给出的示例适用于两个按钮的情况) */
//    for(i = 0; i < 2; i++) {
//        /* 返回被按下按钮的ID */
//        if(button_is_pressed(i)) {
//            return i;
//        }
//    }

//    /* 没有按钮被按下 */
//    return -1;
//}

///**
// * @brief       检查指定ID的按钮是否被按下
// * @param       无
// * @retval      按钮是否被按下
// */
//static bool button_is_pressed(uint8_t id)
//{

//    /*Your code comes here*/

//    return false;
//}

#else /*Enable this file at the top*/

/*This dummy typedef exists purely to silence -Wpedantic.*/
typedef int keep_pedantic_happy;
#endif

从上述源码,我们知道配置 LVGL 的 touch 触摸驱动实现步骤,如下所示:
(1) 调用 touchpad_init 函数初始化 touch 驱动。
(2) 调用函数 lv_indev_drv_init 初始化初始化输入设备。
(3) 设置设备的的类型,因为是 touch 触摸所以修改成 LV_INDEV_TYPE_POINTER 为指示器类型。
(4) 设置触摸回调函数为 touchpad_read,该函数是获取触摸屏的坐标。
(5) 调用函数 lv_indev_drv_register 输入设备的登记。
LVGL 官方提供获取触摸的函数是touchpad_get_xy,该函数的x 和y 的值需要用户提供,由于我们已经拥有了 touch 触摸驱动,所以我们可调用 tp_dev.x[0]和 tp_dev.y[0]取 XY 轴的触摸值并传入x 和y 参数中。编译工程无错误。如有错误,请查找工程定位问题。
至此,我们测试一下 LVGL 是否移植成功,在工程中main.c添加以下源码:

#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "tftlcd.h"
#include "key.h"
#include "time.h"
#include "touch.h"

#include "lvgl.h"
#include "lv_port_disp_template.h"
#include "lv_port_indev_template.h"



int main()
{
	u8 i=0;
	u8 key;
	
	SysTick_Init(72);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //中断优先级分组 分2组
	LED_Init();
	USART1_Init(115200);
	TFTLCD_Init();			//LCD初始化
	KEY_Init();
	TP_Init();
	TIM4_Init(999,71);
	
	lv_init();	//lvgl系统初始化
	lv_port_disp_init();	//lvgl显示接口初始化,放在lv_init()的后面
	lv_port_indev_init();	//lvgl输入接口初始化,放在lv_init()的后面
	lv_obj_t *label = lv_label_create(lv_scr_act());
	lv_obj_set_style_text_color(label, lv_palette_main(LV_PALETTE_RED),
	LV_STATE_DEFAULT);
	lv_obj_set_style_bg_color(lv_scr_act(),lv_palette_main(LV_PALETTE_BLUE),
	LV_STATE_DEFAULT);
	lv_obj_set_style_text_font(label,&lv_font_montserrat_14, LV_STATE_DEFAULT);
	lv_label_set_text(label,"Hello World!!!");
	
	while(1)
	{
		lv_timer_handler(); /* LVGL 管理函数相当于 RTOS 触发任务调度函数 */
	}
}

将工程编译,发现有报错,如下:
在这里插入图片描述

从报错信息可以看到,主要是由于内存空间不够,这是什么原因导致的呢?我们打开LVGL的配置文件:lv_conf.h,从代码可以看到里面有一个定义内存空间大小为48KB,虽然STM32F103ZET6有64KB的内存,但程序自身要占用很多内存,所以不足以分配这么多给LVGL,因此减小这个内存,一般分配20KB即可。然后重新编译,可以看到编译是没有错误的。
在这里插入图片描述

将该程序下载到开发板内运行,发现程序运行现象没有,此时可以在启动文件内加大堆栈大小,如下所示:
在这里插入图片描述

再次将程序编译后下载到开发板内运行,现象如下所示:
在这里插入图片描述

2.4 移植官方例程

在前面章节中,笔者已经说过,LVGL 的官方例程是在 demos 文件夹下保存着,接下来笔者介绍一下官方例程的移植流程。
(1) 把 demos 文件夹复制到 Middlewares/LVGL/GUI_APP 路径下,如下图所示:
在这里插入图片描述

(2) 添加文件路径
由于 demos 文件夹有多个官方例程,不同的例程所添加的路径都不一样,这里笔者演示的官方例程keypad_encoder,这个例程需要添加以下路径,如下图所示:
在这里插入图片描述

(3) Middlewares/LVGL/GUI_APP 工程管理项组添加 demos/keypad_encoder路径下的全部.c 文件,如下图所示:
在这里插入图片描述

(4) 在main.c文件中添加如下代码:

#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "tftlcd.h"
#include "key.h"
#include "time.h"
#include "touch.h"

#include "lvgl.h"
#include "lv_port_disp_template.h"
#include "lv_port_indev_template.h"
#include "lv_demo_keypad_encoder.h"





int main()
{
	u8 i=0;
	u8 key;
	
	SysTick_Init(72);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //中断优先级分组 分2组
	LED_Init();
	USART1_Init(115200);
	TFTLCD_Init();			//LCD初始化
	KEY_Init();
	TP_Init();
	TIM4_Init(999,71);
	
	if(KEY_Scan(1)==KEY0_PRESS)
		TP_Adjust();
	
	lv_init();	//lvgl系统初始化
	lv_port_disp_init();	//lvgl显示接口初始化,放在lv_init()的后面
	lv_port_indev_init();	//lvgl输入接口初始化,放在lv_init()的后面
	lv_demo_keypad_encoder();
    
    while(1)
    {
        tp_dev.scan(0);
		lv_timer_handler(); /* LVGL计时器 */
    }
}

将工程编译出现错误,如下所示:
在这里插入图片描述

此时在LVGL配置文件lv_conf.h中将宏LV_USE_DEMO_KEYPAD_AND_ENCODER设置为1,如下所示:
在这里插入图片描述

修改后编译无错误。

2.5 实验现象

B站演示视频:https://space.bilibili.com/444388619

将工程编译成功后,可使用仿真器或串口工具将程序下载到开发板内,运行结果如下所示:
在这里插入图片描述

如彩屏无显示,可打开tftlcd.h头文件检查彩屏驱动是否为宏定义的值。


联系作者

B站演示视频:https://space.bilibili.com/444388619
专注于51单片机、STM32、国产32、DSP、Proteus、ardunio、ESP32、物联网软件开发,PCB设计,视频分享,技术交流。

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
《CSDN RT-Thread应用开发实战-基于STM32智能小车》是一本针对使用RT-Thread操作系统进行STM32智能小车开发的实践教程。本书的目的是帮助读者了解RT-Thread的应用开发流程以及如何借助该操作系统开发智能小车。 首先,本书介绍了RT-Thread的基本概念和原理,在此基础上详细讲解了如何在STM32上搭建RT-Thread开发环境。读者将学到如何下载、安装以及配置RT-Thread的开发工具链和库文件。 接下来,本书逐步引导读者完成基于STM32智能小车的应用开发。其中,读者将学习到如何通过GPIO控制智能小车的驱动器,如何使用PWM控制电机的转速,以及如何通过UART与传感器进行通信。 本书还特别强调了实战与实验的重要性。通过一系列的实例,读者将学习到如何使用RT-Thread进行任务管理、内存管理和外设驱动。同时,本书也提供了丰富的实验代码和实验指导,读者可以亲自动手实践,加深理解。 最后,本书还介绍了一些智能小车应用的扩展方向。例如,读者可以学习到如何通过使用传感器实现自动避障功能,如何利用无线通信模块进行远程控制等。 通过阅读《CSDN RT-Thread应用开发实战-基于STM32智能小车》,读者可以系统地学习到RT-Thread操作系统的开发流程和应用方法,了解如何使用STM32开发智能小车,并且能够自己进行实践、实验和创新。对于对嵌入式开发和智能小车有兴趣的读者来说,这本书是一本非常实用的指南。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值