需求
1.将LVGL7.11移植到项目工程中。
2.创建一个LVGL任务,在任务中创建两个屏幕,开始运行时加载第一个屏幕。
3.设计一个按键控制,通过按压不用的按键来切换不同的屏幕。
一、LVGL概要
LVGL就是一款免费开源的嵌入式图形库,可为任何MCU、MPU和显示器类型设计好看的用户界面。其实本质上就是一个图形库。开发者通过代码编写调用LVGL库来创建GUI界面。它包含一个HAL(硬件抽象层)接口,用于注册显示和输入设备驱动程序。
驱动程序除特定的驱动程序外,它还有其他的功能,可驱动显示器到GPU(可选)、读取触摸板或按钮的输入。
Lvgl官网:官网地址
Lvgl官方文档:官方文档
Lvgl源码网址:源码地址
个人理解:
LVGL屏幕交互设计就是制作PPT,每个屏幕的创建就是每页PPT的创建。在PPT中你可以制作多个PPT页面,但显示的屏幕只有一个,所以我们通过鼠标进行页面切换。在LVGL中也一样,你可以创建多个屏幕,但给客户显示的屏幕只有一个,所以可以通过代码逻辑来进行按需切换。
二、移植步骤
移植前准备:
1.想要移植的目标工程。
2.下载好的LVGL源码(版本按需选择,版本越高功能越多,占用空间也就越大),本例由于芯片资源有限,所以选择的是LVGL7.11版本。
1.源码复制
在想要移植的目标工程中创建一个LVGL文件夹
在建好的LVGL文件夹中新建app,porting,src文件夹
app:放置以后自己编写的LVGL工程
porting:LVGL接口文件
src:LVGL源码文件
打开下载好的LVGL源码文件夹,将LVGL源码文件夹中的src文件夹中的内容全部copy到上图目标工程的src文件夹中。
打开LVGL源码文件中的examples文件夹
找到该文件夹中的porting接口文件,将该文件中的内容同样复制到目标工程中我们自己创建的porting文件夹中。
在源码文件中找到lvgl/lvgl.h文件以及lvgl/lv_conf_template.h文件,将其复制到目标工程中的LVGL文件中
复制过来后改个名字,将lv_conf_template.h文件更名为lv_conf.h
将porting文件夹中的显示接口文件改名
lv_port_disp_template为屏幕驱动。
lv_port_fs_template为文件系统驱动。
lv_port_indev_template为输入驱动
本项目中我们只使用了屏幕的显示功能,因此我们只修改屏幕驱动的文件名字。
2.添加到工程中
用keil5打开目标工程项目。
在工程目录下新建三个分组,分别为Lvgl/app、Lvgl/porting、Lvgl/src三个目录。
添加文件到工程目录中
porting目录下只添加lv_port_disp.c,以及Lvgl目录下的lv_conf.h文件,这两个文件后面需要修改。
src目录下,添加Lvgl/src目录下除去gpu文件夹外的所有文件夹内的c文件。
之后,点击配置选择
点击C/C++添加头文件路径,把Lvgl文件夹下所有包含h文件的路径全部添加。
3.配置文件的修改
打开lv_port_disp.c/.h文件,将所有的#include "lvgl\lvgl.h"替换为#include “lvgl.h”。
将if置1,开启屏幕接口和lcd初始化
修改lv_conf.h文件如下图所示
编译修改后的代码,如果没有error就代表以上配置,配置成功了。
4.适配屏幕接口
修改lv_conf.h内的宏定义,通过它可以设置库的基本行为,裁剪不需要模块和功能,在编译时调整内存缓冲区的大小等等。
修改屏幕分辨率
修改屏幕DPI
DPI 是将屏幕分辨率与屏幕尺寸结合起来的度量,用于描述在每英寸上的像素密度。
屏幕分辨率:指的是屏幕上横向和纵向像素的总数,通常表示为宽×高的像素数。
屏幕尺寸:指的是屏幕的对角线长度,通常以英寸(inch)为单位。
设置占用内存,最少也要2kb,根据芯片的内存来决定,越大越好。
本例使用的芯片:STM32F103ZET6
512 KB闪存(用于程序存储)。
64 KB SRAM(用于数据存储)
由于该芯片没有GPU,所以还需要把GPU选项关掉
继续适配屏幕接口到lvgl上、修改lv_port_disp.c文件中的显示接口函数,用于适配我们的屏幕与lvgl,包含lcd屏幕显示的头文件。
修改屏幕显示初始化函数lv_port_disp_init,我们用方法一显示,同时修改屏幕的大小。
修改disp_init函数,该函数一般将我们的屏幕初始化放进去,也可以在硬件层初始化屏幕,这里就可以不写底层屏幕的初始化。
修改disp_flush函数,该函数是lvgl绘制界面的关键函数,是用于绘制界面的最基本的函数,也是lvgl与底层屏幕的绘制适配接口函数。
LVGL给的例程中是想让我们使用画点方式进行实现,但画点方式效率比较低,所以此处我们使用LCD_Color_Fill函数实现,该函数通常支持一次性填充整个屏幕或者特定区域,可以大大提高cpu的运行效率。
三、需求实现
1.初始化LVGL
为了给LVGL提供时间基准,告诉LVGL现在过了多久,我们需要写一个系统心跳钩子函数。
如果使用的是裸机开发,那么直接将这个函数放到硬件定时器的1ms中断服务函数内。
此时我们使用的是操作系统,所以我们可以放到系统基础节拍的钩子函数内。
为了使屏幕定期刷新我们还需要每隔x毫秒调用lv_task_handler()用以处理与lvgl相关的任务。
如果是裸机开发,那么我们可以在while(1)中做一个时间点,1ms或者10ms的调用一次这个函数。
此时我们使用的是操作系统,那么我们可以将lv_task_handler()函数放到任务中的while(1)中定期调用。
搞定LVGL时间基准和定期刷新屏幕后,想要进行屏幕的创建和编写,此时我们还需要进行LCD初始化,LVGL初始化以及LVGL接口的初始化。
2.创建屏幕
子类和父类的概念
在LVGL中,有子类和父类的概念,简单来说就是,一个屏幕上的所有部分都是这个屏幕的子类。
在LVGL中,所有的屏幕都没有父类,不需要依赖任何东西。
可以同时创建多个屏幕,在LVGL初始化中,会在后台加载所有的屏幕。想要那个屏幕显示时,只需调用屏幕加载函数调用即可。
了解以上概念后,我们就可以开始创建屏幕了。
先创建lv_obj_t类型的变量。
lv_obj_t类型用来表示图形界面库中的各种对象,如屏幕(screen)、按钮(button)、标签(label)等。
lv_obj_t * scr1;
lv_obj_t * scr2;
lv_obj_t * obj;
lv_obj_t * obj1;
lv_obj_t * label1;
进行屏幕的创建以及其他部件的创建。
//测试代码--显示一个label
scr1 = lv_obj_create(NULL, NULL);
obj = lv_obj_create(scr1, NULL);
obj1 = lv_obj_create(obj, NULL);
lv_obj_set_size(obj, 50, 50); /*Button size*/
lv_obj_set_pos(obj, 0,0);
//
scr2 = lv_obj_create(NULL, NULL);
label1 = lv_label_create(scr2, NULL);
lv_label_set_long_mode(label1, LV_LABEL_LONG_BREAK);
lv_label_set_recolor(label1, true);
lv_label_set_align(label1, LV_LABEL_ALIGN_CENTER);
lv_label_set_text(label1, "#0000ff Re-color# #ff00ff words# #ff0000 of a# label "
"and wrap long text automatically.");
lv_obj_set_width(label1, 150);
lv_obj_align(label1, NULL, LV_ALIGN_CENTER, 0, -30);
lv_scr_load(scr1);
3.按键控制
void KEY_Task(void *p)
{
while(1)
{
switch(key_getvalue())
{
case 1:lv_scr_load_anim(scr2,LV_SCR_LOAD_ANIM_MOVE_LEFT,1000,0,0);break;
case 2:lv_scr_load_anim(scr1,LV_SCR_LOAD_ANIM_MOVE_LEFT,1000,0,0);break;
case 3:;break;
case 4:break;
}
vTaskDelay(10);//MS级别的延时,带有阻塞性质,任务会从运行态变为阻塞态
}
}
本质上就是通过代码控制不同屏幕的加载,后加载的屏幕会覆盖掉前面加载的屏幕。可以理解为PPT的幻灯片。
lv_scr_load()和lv_scr_load_anim()函数都是屏幕加载,只不过前者是直接加载,后者是动画加载。
完整代码
main.c
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
#include "delay.h"
#include "string.h"
#include "pwm.h"
#include "adc.h"
#include "su03t.h"
#include "dht11.h"
#include "kqm.h"
#include "usart.h"
#include "key.h"
#include "aliot.h"
#include "lvgl.h"
#include "lv_port_disp.h"
//使用FreeRtos相关头文件之前,一定要先包含这个#include "FreeRtos.h"
#include "FreeRtos.h"
#include "task.h"
#include "semphr.h"
#include "queue.h"
#include "bsp_lcd.h"
#include "RTC.h"
#include "time.h"
TaskHandle_t Deal_TaskHandle;//数据处理
TaskHandle_t Su03_TaskHandle;//语音播报
TaskHandle_t Ali_pub_TaskHandle;//阿里发布
TaskHandle_t Lcd_TaskHandle;
TaskHandle_t Lvgl_TaskHandle;
TaskHandle_t KEY_TaskHandle;//按键任务
char D_wen[20];
char D_shi[20];
char D_time[20];
struct tm *info;
extern const unsigned char gImage_hengliu[153600];
uint32_t sec=0;
lv_obj_t * scr1;
lv_obj_t * scr2;
lv_obj_t * obj;
lv_obj_t * obj1;
lv_obj_t * label1;
void Lvgl_Task(void *p)
{
LCD_Init();
lv_init();
lv_port_disp_init();
//测试代码--显示一个label
scr1 = lv_obj_create(NULL, NULL);
obj = lv_obj_create(scr1, NULL);
obj1 = lv_obj_create(obj, NULL);
lv_obj_set_size(obj, 50, 50); /*Button size*/
lv_obj_set_pos(obj, 0,0);
//
scr2 = lv_obj_create(NULL, NULL);
label1 = lv_label_create(scr2, NULL);
lv_label_set_long_mode(label1, LV_LABEL_LONG_BREAK);
lv_label_set_recolor(label1, true);
lv_label_set_align(label1, LV_LABEL_ALIGN_CENTER);
lv_label_set_text(label1, "#0000ff Re-color# #ff00ff words# #ff0000 of a# label "
"and wrap long text automatically.");
lv_obj_set_width(label1, 150);
lv_obj_align(label1, NULL, LV_ALIGN_CENTER, 0, -30);
lv_scr_load(scr1);
//
while(1)
{
lv_task_handler();
vTaskDelay(5);
}
}
void KEY_Task(void *p)
{
while(1)
{
switch(key_getvalue())
{
case 1:lv_scr_load_anim(scr2,LV_SCR_LOAD_ANIM_MOVE_LEFT,1000,0,0);break;
case 2:lv_scr_load_anim(scr1,LV_SCR_LOAD_ANIM_MOVE_LEFT,1000,0,0);break;
case 3:;break;
case 4:break;
}
vTaskDelay(10);//MS级别的延时,带有阻塞性质,任务会从运行态变为阻塞态
}
}
int main()
{
RGBpwm_Config();
Kqm_U4Config();
Usart1_Config();
Su03t_U5Config();
DHT11_Config();
Adc_Config();
Led_Init();
key_Init();
BaseType_t Ret = pdPASS;
Ret = xTaskCreate(Lvgl_Task,"Lvgl_Task",500,NULL,2,&Lvgl_TaskHandle);
if(Ret==pdPASS)
{
printf("Lvgl运行成功!\r\n");
}
Ret = xTaskCreate(KEY_Task, //创建任务的任务函数名
"KEY_Task",//任务名字
100,//任务栈深度。32位单片机*4
NULL,//创建任务时传递参数,没有就给NULL
2,//任务优先级
&KEY_TaskHandle);//任务的句柄,用于后边删除,挂起任务
if(Ret == pdPASS){
printf("KEY_Task创建完成\r\n");
}
printf("开始调度!\r\n");
vTaskStartScheduler();
while(1)
{
}
}
void vApplicationStackOverflowHook( TaskHandle_t xTask,char *pcTaskName )
{
printf("任务:%s->栈溢出\r\n",pcTaskName);
printf("任务剩余空间:%d\r\n",(int)uxTaskGetStackHighWaterMark(xTask));
while(1)//栈溢出时卡死到钩子函数中
{}
}
//系统心跳钩子函数
void vApplicationTickHook(void)
{
lv_tick_inc(1);//告诉LVGL时间过了1Ms
}