在前面章节中,我们已经了解 LVGL移植要求以及 LVGL图形库下载路径。本章主要讲解裸机移植LVGL到STM32开发板上。本章分为如下几部分内容:
2.1 移植准备工作
2.2 向工程添加文件
2.3 修改工程文件
2.4 移植官方例程
2.5 实验现象
在移植 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 移植的简洁源码。
在移植之前我们需要一个基础工程,前面我们已准备好工程,我们在这个工程的基础上完成本章的 LVGL 移植。
首先我们把“0-移植LVGL基础工程”复制一份,并重命名为“1-LVGL无操作系统移植”,然后在该工程目录下新建 Middlewares 文件夹,并在该文件夹下新建 LVGL 文件夹,其次在该文件夹下再新建 GUI 文件夹和 GUI_APP 文件夹,在这两个文件夹下也分别新建 lvgl文件夹,lvgl 文件夹保存前面所精简后的LVGL源码的文件和文件夹,而 GUI_APP 文件夹保存用户编写的文件。
根据上述所述,可得到一个文件树形图,如下图所示:
我们的工程为什么使用这样的文件结构?因为 LVGL 的源文件内声明 lvgl.h 文件都是以“#include “…/…/lvgl.h””这样的形式声明的,显然这个文件的声明是放在第三层路径下,所以笔者把 LVGL 源码放在上图中的 lvgl 文件夹下,这样就符合了 LVGL 声明路径的风格。
根据上图所示,把下图中的文件和文件夹复制到Middlewaes/LVGL/GUI/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 和触摸驱动相关,这些文件我们在后面具体移植会讲解。
看到上述的步骤,很多人以为移植 LVGL需要添加很多文件路径,其实不然,我们只添加关键路径即可,因为 lvgl.h 文件已经为我们省去这部分的操作,如图所示:
在 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时基等操作,万事俱备,只欠东风。
这里我们只需要“一加两改”。“一加”是指添加定时器提供 LVGL的时基,“两改”是指修改 LVGL 显示驱动文件(lv_port_disp_templ.c/h)和输入设备文件(lv_port_indev_templ.c/h)。
打开上图的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);
在文件 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 */
该文件作用是让自己的显示屏驱动文件与 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是没有该外设资源,所以此处忽略。
该文件的作用是添加输入设备,例如触摸设备、鼠标、键盘、编码器、按键等输入设备,针对我们的例程来讲:只添加触摸输入设备即可,如下源码所示:
/**
* @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即可。然后重新编译,可以看到编译是没有错误的。
将该程序下载到开发板内运行,发现程序运行现象没有,此时可以在启动文件内加大堆栈大小,如下所示:
在前面章节中,笔者已经说过,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,如下所示:
修改后编译无错误。
B站演示视频:https://space.bilibili.com/444388619
将工程编译成功后,可使用仿真器或串口工具将程序下载到开发板内,运行结果如下所示:
如彩屏无显示,可打开tftlcd.h头文件检查彩屏驱动是否为宏定义的值。
B站演示视频:https://space.bilibili.com/444388619
专注于51单片机、STM32、国产32、DSP、Proteus、ardunio、ESP32、物联网软件开发,PCB设计,视频分享,技术交流。