LVGL(轻量级和通用图形库)是一个免费和开源的图形库,它提供了创建嵌入式GUI所需的一切,具有易于使用的图形元素,美丽的视觉效果和低内存占用。
LVGL更多介绍:https://zhuanlan.zhihu.com/p/406294618
本次实验将LVGL移植到STM32F103ZET6中,并编译通过,记录一下移植过程
因为LVGL要求MCU的堆栈最少是2K,2K = 2048 = 0x800,所以要在CubeMX中修改堆栈大小,为什么不直接在代码中修改呢,因为在代码中修改的话,而CubeMX中没有修改,下一次用CubeMX生成工程后又会覆盖掉代码中修改的地方,所以需要在CubeMX中修改
使用上次TFT-LCD触摸的文件,打开后在下图位置中修改,最小堆栈大小设置为0x800,然后生成工程代码
在keil工程里也可查看修改的位置
LVGL要求开启C99模式,在C/C++选项中勾选即可
官网地址:https://lvgl.io/
Github下载地址:https://github.com/lvgl
打开会比较慢,多试几次
下载好源码和例程,这里用V7版本的
LVGL官方代码里是相对路径,所以一定要按步骤建好文件夹
在工程目录下新建一个GUI的文件夹
GUI文件夹里再建三个子文件夹
lvgl:放源码,将下载的源码 lvgl-release-v7压缩包复制到该文件夹,并解压缩
lvgl_driver:放显示和触摸的驱动
在解压完的源码文件中,有个examples的文件夹,点击打开
再打开porting文件夹
这些就是显示和触摸的驱动文件
将显示和触摸的.c和.h文件拷贝到 lvgl_driver文件夹中,因为没有用到文件系统,所以可以不用拷贝,需要将文件名的_template去掉,不然后面加入keil工程后编译会出错
lvgl_example:放例程,将官方例程 lv_examples-release-v7复制到该文件夹并解压缩
打开lvgl的文件夹,找到 lv_conf_template.h文件
剪切粘贴到GUI文件夹的目录下,并修改文件名,将_template去掉
然后打开 lv_conf.h 文件,修改预编译选项,将0改为1,然后保存
打开 lvgl_examples 的文件夹,找到 lv_ex_conf_template.h 文件
剪切粘贴到GUI文件夹的目录下,同样要修改文件名,将_template去掉
同理,将 lv_ex_conf.h 的预编译选项的0改为1,保存
至此,LVGL的工程文件夹已经创建好
打开lvgl->src,将下面文件夹里的.c文件全都添加到keil工程的lvgl文件夹中
添加完的lvgl文件夹如下
然后编译一次,没有出现错误,发现有警告,这些警告是源码里的,尽量不要去修改源码
发现是111的警告,可以在keil里设置,屏蔽这些警告,需要在C/C++选项中的下图位置添加这条语句:–diag_suppress=111
然后再次编译,发现没有出现警告
打开lv_disp.c源文件,找到 lv_conf.h文件,这个就是配置文件,要修改屏幕的最大分辨率,因为手上屏幕是240x320的,所以水平分辨率改为240,垂直分辨率保持默认
往下一点就是颜色格式设置和总线位数设置,颜色格式默认RGB565的,所以不用改,手上屏幕是16位总线的,所以总线位数也不用改,这需要根据具体屏幕硬件来设置
LVGL显示界面需要内存,如果显示的东西多,则需要的内存就大,下面这里就是设置分给LVGL内存的大小,如果显示的内容多,则32可改为其他值,这里使用默认
191行的是设置是否使用GPU,默认是1开启,改为0,选择不使用
211行是设置是否使用文件系统,默认是1使用,改为0,选择不使用
最后编译一次,没有错误则进行下一步
首先包含lvgl的头文件路径
在MyApplication.h头文件中添加 lvgl.h 头文件
在定时器中断回调函数中调用lvgl的心跳函数 lv_tick_inc(),定时器每隔一定时间就调用该函数,控制 lvgl 刷新界面,lv_tick_inc函数需要传入参数,参数就是定时器定时时间,比如定时5ms,那就传入5,定时1ms,那就传入1
在keil工程中新创建一个驱动的文件夹 lvgl_driver,并添加显示驱动源文件 lv_port_disp.c,
打开lv_port_disp.c文件,修改预编译选项,0改为1,修改引入头文件的名称,lv_port_disp_template.h 改为 lv_port_disp.h
打开 lv_port_disp.h头文件,预编译0改为1,"lvgl/lvgl.h"改为 “…/lvgl/lvgl.h”,编译器才能找到该头文件路径
再回到 lv_port_disp.c 源文件中,找到 disp_init() 函数,这里可以放自己写的TFT-LCD屏幕驱动
当然使用自己的函数需要引入对应的头文件
执行完disp_init函数后,会进行缓存的定义,lvgl给出了三种定义的方法,如下图,方法1最简单,方法2使用了双重缓存,方法3是根据屏幕大小定义缓存,这次使用比较简单的第1种方法,把另外两种注释掉即可
第一种方法中的缓存大小 LV_HOR_RES_MAX * 10 改为 LV_HOR_RES_MAX * LV_HOR_RES_MAX / 10,设置缓存大一点
再往下就是设置当前屏幕的尺寸大小的,将水平的480改为240,跟屏幕匹配;前面源码里设置的是屏幕最大的分辨率,这里设置的尺寸大小不能大于前面设置的最大分辨率
接下来修改disp_flush函数,下面是该函数没有被修改过的,可以看出该函数的功能就是设置一个窗口,然后往窗口里写入像素点的值,写入的操作默认被注释掉了,写完一个像素点后,像素点指针加1,继续写下一个像素点
/* 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++; //像素点指针加1
}
}
/* IMPORTANT!!!
* Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
修改后的disp_flush函数如下,挂起STM32的systick时钟是为了提高GUI的刷新速度,在写完像素点数据后,再开启systick时钟
/* 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*/
uint16_t x,y;
//挂起systick,提高GUI刷新速率
HAL_SuspendTick();
//设置窗口,参数:X轴起始位,Y轴起始位,长度,宽度
TFT_LCD.LCD_SetWindows(area->x1,area->y1,area->x2-area->x1+1,area->y2-area->y1+1);
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)*/
LCD_Write_DATA(color_p->full); //调用函数写入像素点数据
color_p++;
}
}
/* IMPORTANT!!!
* Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
//恢复systick
HAL_ResumeTick();
}
在lvgl_driver文件夹中,添加触摸驱动源文件 lv_port_indev.c
然后与显示驱动一样,修改源文件的预编译选项,修改引入头文件的名称
lv_port_indev.h头文件也是修改预编译选项,修改引入的头文件路径
然后回到 lv_port_indev.c源文件中,里面是一些功能的驱动函数,如触摸、鼠标、键盘、编码器和按钮功能,需要什么功能根据实际情况选择,本次移植使用简单点的触摸功能,其他功能函数都可删掉
修改后的lv_port_indev.c源文件,用lvgl自带的初始化函数,然后对touchpad_read函数进行修改
/**
* @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.h"
#include "MyApplication.h"
static bool touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
void lv_port_indev_init(void)
{
/* Here you will find example implementation of input devices supported by LittelvGL:
* - Touchpad
* - Mouse (with cursor support)
* - Keypad (supports GUI usage only with key)
* - Encoder (supports GUI usage only with: left, right, push)
* - Button (external buttons to press points on the screen)
*
* The `..._read()` function are only examples.
* You should shape them according to your hardware
*/
lv_indev_drv_t indev_drv;
/*------------------
* Touchpad
* -----------------*/
/*Register a touchpad input device*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
lv_indev_drv_register(&indev_drv);
}
/**********************
* STATIC FUNCTIONS
**********************/
/* Will be called by the library to read the touchpad */
static bool 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(Touch.Touch_Flag == TRUE)
{
Touch.Touch_Flag = FALSE;
//把新坐标Touch.LCD_X和Touch.LCD_Y赋给lvgl的坐标结构体的x和y
data->point.x = Touch.LCD_X;
data->point.y = Touch.LCD_Y;
//更改状态,lvgl获取坐标
data->state = LV_INDEV_STATE_PR;
//更新当前坐标
last_x = data->point.x;
last_y = data->point.y;
}
else //如果没有触摸
{
//把上一次坐标赋给lvgl坐标结构体的x和y
data->point.x = last_x;
data->point.y = last_y;
//更改状态,lvgl获取坐标
data->state = LV_INDEV_STATE_REL;
}
return false;
}
#else /* Enable this file at the top */
/* This dummy typedef exists purely to silence -Wpedantic. */
typedef int keep_pedantic_happy;
#endif
系统运行主函数中判断屏幕是否被触摸,如果触摸了,则更新标志位
/*
* @name Run
* @brief 系统运行
* @param None
* @retval None
*/
static void Run()
{
//获取坐标板坐标
if(Touch.Scan() == TRUE)
{
//通过该标志位知道屏幕是否被触摸更新坐标
Touch.Touch_Flag = TRUE;
}
}
在自己工程的公共头文件MyApplication.h中添加lvgl的头文件
在设置中添加头文件路径,包括源文件路径和驱动路径
显示驱动和触摸驱动都是有初始化函数的,但它们的头文件都没有声明初始化函数,如果要调用这些函数的话是会有警告的,所以要先进行声明
在自己的初始化函数中调用lvgl的初始化函数lv_init(),显示驱动函数lv_port_disp_init(),触摸驱动函数lv_port_indev_init()
把前面触摸屏扫描函数中在LCD屏幕上显示触摸屏的坐标值的语句删除