LVGL的学习
前言
提示:本博客中的大部分学习内容参考正点原子LGVL开发指南以及一些他人博客内容,相关文件可以去正点原子官方网站下载或者CSDN自查,请大家在使用时:
自用为好,切勿商用
正点原子资料下载中心(LVGL新版学习资料)
编写本博客主要是回顾在个人项目中对于LVGL的学习历程,并且对于学习LVGL这一新技术进行总结。
一、LVGL是什么
简单来说,LVGL就是一款免费的轻量级开源图形库。我个人的看法就是对于嵌入式开发,可以利用LVGL设计出自己所想要的显示控制界面,能够满足自己的需求与创意想法。而其所具备的一系列优缺点,都可以在官网或者其他论坛中找到,此处就不做赘述。
二、LVGL的移植
在进行移植之前,我们需要准备一些文件:
1、LVGL的源码。LVGL 相关的源码和工程都是存放在 GitHub 远程仓库中,该 GitHub 远程仓库地址如下, 用户可以在该仓库中下载 LVGL 图形库的源码。 以下文档中我以lvgl-release-v8.3版本为例。
LVGL源代码
2、基础工程:LCD显示驱动、触摸屏显示驱动、基本定时器驱动。我使用的是正点原子官方探索者STM32F407开发板的官方例程《触摸屏实验》以及《定时器中断实验》。
3、将《定时器中断实验》中定时器中断实验\HARDWARE路径下的TIMER文件夹复制到《触摸屏实验》的HARDWARE文件夹下,并且修改《触摸屏实验》的文件名,以下以《LVGL》文件为例。
1、无操作系统
1.前期准备
1、解压从官网下载的源代码压缩包,lvgl-release-v8.3解压后如下图:
2、在源代码文件中我们需要的只有examples文件夹、src文件夹、lv_conf_template.h文件、lvgl.h文件,将lv_conf_template.h文件重命名为lv_conf.h其他文件及文件夹均与移植无关,但是其中的demos文件夹存放的是官方提供的演示例程,需要者可以保留。精简后如下图:
3、打开examples文件夹,仅保留porting文件夹,其余的都删除掉。
2.源码移植
1、打开刚才准备好的《LVLG》文件夹,新建LVGL文件夹,打开,新建GUI文件夹与GUI_APP文件夹,其中GUI文件夹用于存放lvgl官方源代码,GUI_APP文件夹用于存放后期自己编写的界面代码。
2、将刚才精简后的 lvgl-release-v8.3文件夹 移至 GUI文件夹 下,并且重命名为 lvgl 。
此处说明以下为何要如此命名文件夹路径:为了兼容 LVGL源码中包含头文件的格式,不然在后期对LVGL源码进行修改时会很繁琐,需要修改多个文件内容
3、以上步骤做完、文件路径如下图:
4、打开《lvgl》工程,填加以下如图工程分组:
其实该工程分组对应了LVGL源码中src子目录的文件路径:
5、按照以下步骤,将相关路径将文件添加至对应分组中:
- 往 LVGL/GUI/lvgl/src/core 分组中添加 core 文件夹下的全部.c 文件
- 往 LVGL/GUI/lvgl/src/draw 组中添加 draw 文件夹下除 nxp_pxp、nxp_vglite、sdl 和stm32_dma2d 文件夹之外的全部.c 文件
- 往 LVGL/GUI/lvgl/src/extra 组中添加 extra 文件夹下除了 lib 文件夹之外的全部.c 文件
- 往 LVGL/GUI/lvgl/src/font 组中添加 font 文件夹下的全部.c 文件
- 往 LVGL/GUI/lvgl/src/gpu 组中添加 draw/stm32_dma2d 和 draw/sdl文件夹下的全部.c文件
- 往 LVGL/GUI/lvgl/src/hal 组中添加 hal 文件夹下的全部.c 文件
- 往 LVGL/GUI/lvgl/src/misc 组中添加 misc 文件夹下的全部.c 文件
- 往 LVGL/GUI/lvgl/src/widgets 组中添加 widgets 文件夹下的全部.c 文件
- 往 LVGL/GUI/lvgl/examples/porting 组添加 LVGL/GUI/lvgl/examples/porting目录下的lv_port_disp_template.c 和 lv_port_indev_template.c 文件
6、在工程中填加对应头文件路径
7、往 HARDWARE 分组填加定时器驱动文件 timer.c ,并且填加对应的头文件路径
提醒,在填加定时器驱动文件之后,需要在 FWLIB 分组下填加stm32f4xx_tim.c文件,否则工程会对定时器进行报错
8、屏蔽 MDK 的警告(引用正点原子官方教程
)
移植至此,如果我们编译代码,则会出现很多警告,这些警告都是 LVGL源码所带来的,大家如果想屏幕这些警告,可以采用以下方法(非必须,慎用):点击 图标,选中C/C++选项 卡 , 在 Misc Controls 框 中 填 入 以 下 内 容 : --diag_suppress=68 --diag_suppress=111 --diag_suppress=188 --diag_suppress=223 --diag_suppress=546 --diag_suppress=1295。
3.修改相关文件代码
(1)为LVGL提供时基(即心跳)
- 打开timer.c文件,声明LVGL头文件#include “lvgl.h”
- 修改定时器3中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
LED1=!LED1;//DS1翻转
lv_tick_inc(1); //为lvgl提供1ms的心跳
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
- 打开main.c文件,在初始化中填加定时器的初始化代码,本人使用的是stm32f407开发板,代码为
TIM3_Int_Init(1000-1,84-1); //提供1ms的定时
(2)配置显示屏、触摸输入驱动
-
先将 LVGL/GUI/lvgl/examples/porting 分组下的两个文件及对应头文件中的条件编译指令#if 0 都修改成#if 1
-
1.对lv_port_disp_template.c进行修改
该文件用于配置显示屏,它可以将用户的底层显示驱动与 LVGL的显示驱动衔接起来。
主要需要修改的地方为- 包含lcd驱动头文件 #include “lcd.h”
- 设置自己的屏幕宽度 #define MY_DISP_HOR_RES 320
- 设置自己的屏幕高度 #define MY_DISP_VER_RES 480
- 根据需求选择缓冲方式,然后将其他不需要的配置注释或者删除
- 在static void disp_init(void)函数中填加自己的lcd初始化函数
- 修改static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)函数
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { /* 重要!!!将源码的LCD 驱动函数更换为自己lcd.c文件中在指定区域内填充指定颜色块的函数 */ LCD_Color_Fill(area->x1,area->y1,area->x2,area->y2,(uint16_t*)color_p); lv_disp_flush_ready(disp_drv); }
精简之后的lv_port_disp_template.c代码如下,如果所使用的开发板是相同的可以直接复制粘贴使用
#if 1 #include "lv_port_disp_template.h" #include <stdbool.h> #include "lcd.h" /*包含lcd驱动头文件*/ #ifndef MY_DISP_HOR_RES #define MY_DISP_HOR_RES 320 /*设置自己的屏幕宽度*/ #endif #ifndef MY_DISP_VER_RES #define MY_DISP_VER_RES 480 /*设置自己的屏幕高度*/ #endif /*显示设备初始化函数*/ 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); void lv_port_disp_init(void) { /*------------------------- * 初始化显示设备 * -----------------------*/ disp_init(); /*----------------------------- * 创建一个绘图缓冲区 *----------------------------*/ /** * 1. 单缓冲区: * LVGL 会将显示设备的内容绘制到这里,并将他写入显示设备。 /* Example for 1) */ static lv_disp_draw_buf_t draw_buf_dsc_1; static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/ lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/ static 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 = MY_DISP_HOR_RES; disp_drv.ver_res = MY_DISP_VER_RES; /*Used to copy the buffer's content to the display*/ disp_drv.flush_cb = disp_flush; /*Set a display buffer*/ disp_drv.draw_buf = &draw_buf_dsc_1; lv_disp_drv_register(&disp_drv); } /*Initialize your display and the required peripherals.*/ static void disp_init(void) { /*You code here*/ LCD_Init(); /* 初始化 LCD ,填加自己的lcd初始化函数*/ } volatile bool disp_flush_enabled = true; /* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL */ void disp_enable_update(void) { disp_flush_enabled = true; } /* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL */ void disp_disable_update(void) { disp_flush_enabled = false; } /*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) { /* 重要!!!将源码的LCD 驱动函数更换为自己lcd.c文件中在指定区域内填充指定颜色块的函数 */ LCD_Color_Fill(area->x1,area->y1,area->x2,area->y2,(uint16_t*)color_p); /*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv); } #else /*Enable this file at the top*/ /*This dummy typedef exists purely to silence -Wpedantic.*/ typedef int keep_pedantic_happy; #endif
-
2.对lv_port_indev_template.c进行修改
该文件用于配置输入设备,例如:触摸屏、鼠标、键盘、编码器、按键等,它可以将用户的底层输入设备驱动与 LVGL的输入驱动衔接起来。
主要需要修改的地方为- 包含lcd驱动头文件 #include “lcd.h”
- 包含触摸驱动头文件 #include “touch.h”
- 包含按键驱动头文件 #include “key.h”
- 删除不需要的输入设备相关代码,本人只使用到了触摸屏,如需使用其他的输入设备请浏览官网相关文档
- 在static void touchpad_init(void)函数中插入自己使用的触摸屏驱动初始化函数
tp_dev.init(); //触摸驱动函数 if (KEY_Scan(0) == KEY0_PRES) /* KEY0 按下,则执行校准程序 */ { LCD_Clear(WHITE); /* 清屏 */ TP_Adjust(); /* 屏幕校准 */ TP_Save_Adjdata(); }
- 在static bool touchpad_is_pressed(void)函数中编写获取触摸屏设备的状态,返回 true 是触摸板被按下
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; }
- 在static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)函数中编写在触摸屏被按下的时候读取 x、y 坐标
精简之后的lv_port_indev_template.c代码如下,如果所使用的开发板是相同的可以直接复制粘贴使用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]; }
#if 1 /********************* * INCLUDES *********************/ #include "lv_port_indev_template.h" #include "../../lvgl.h" #include "touch.h" //填加自己的屏幕触摸驱动头文件 #include "lcd.h" //填加自己的屏幕驱动头文件 #include "key.h" //填加自己的按键驱动头文件 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); lv_indev_t * indev_touchpad; void lv_port_indev_init(void) { static lv_indev_drv_t indev_drv; /*------------------ * Touchpad * -----------------*/ touchpad_init(); /* 第一步 初始化触摸屏 */ /*Register a touchpad input device*/ 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); /* 注册输入设备 */ } /*插入自己使用的触摸屏驱动初始化函数*/ static void touchpad_init(void) { tp_dev.init(); //触摸驱动函数 if (KEY_Scan(0) == KEY0_PRES) /* KEY0 按下,则执行校准程序 */ { LCD_Clear(WHITE); /* 清屏 */ TP_Adjust(); /* 屏幕校准 */ TP_Save_Adjdata(); } /*Your code comes here*/ } /*Will be called by the library to read the touchpad*/ static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data) { static lv_coord_t last_x = 0; static lv_coord_t last_y = 0; /*Save the pressed coordinates and the state*/ if(touchpad_is_pressed()) { touchpad_get_xy(&last_x, &last_y); data->state = LV_INDEV_STATE_PR; } else { data->state = LV_INDEV_STATE_REL; } /*Set the last pressed coordinates*/ data->point.x = last_x; data->point.y = last_y; } /*获取触摸屏设备的状态,返回 true 是触摸板被按下*/ 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; } /*在触摸屏被按下的时候读取 x、y 坐标*/ 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]; } #else /*Enable this file at the top*/ #endif
(3)检测代码是否移植成功
1.在进行检测之前,需要将kiel开启C99模式,否则编译代码会出现很多报错,C99 模式的方法如下图所示:
2.开启 C99模式之后,接下来就可以在 main.c中编写测试代码,检测 LVGL是否移植成功,具体源码如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "lcd.h"
#include "key.h"
#include "touch.h"
#include "timer.h"
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口波特率为115200
LED_Init(); //初始化LED
LCD_Init(); //LCD初始化
KEY_Init(); //按键初始化
tp_dev.init(); //触摸屏初始化
TIM3_Int_Init(1000-1,84-1); //初始化定时器1ms
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_label_set_text(label,"Hello Alientek!!!");
lv_obj_center(label);
while (1)
{
lv_task_handler(); // lvgl的事务处理
delay_ms(5);
}
}
3.移植成功的效果如下:
如果编译工程时出现如图 的报错(不一定出现),则说明内存不足,用户可以尝试在 startup_stm32f40_41xxx.s 文件中修改这两个参数来增加栈空间,注意要选择合适的参数,
Stack_Size EQU 0x00000400
Heap_Size EQU 0x00000200
三、GUI-Guider创建代码的移植
GUI-Guider是恩智浦为LVGL开发了一个上位机GUI设计工具,可以通过拖放控件的方式设计LVGL GUI页面,加速GUI的设计。设计完成的GUI页面可以在PC上仿真运行,确认设计完毕之后可以生成C代码,再整合到MCU项目中。关于GUI-Guider的使用,后期有时间的话会再出一个博客,本篇博客中只介绍如何将生成的代码移植到自己的工程里面。我使用的是当前GUI-Guider最新版本1.7.2版本。以下以我自己生成的工程为例:
1、GUI-Guider代码生成
- 打开自己的GUI项目,按照自己的需求设计相关界面及控件事件。完成之后,点击运行-编译部署C或者点击下图箭头所指即可
—— - 打开自己保存该项目的文件夹,文件夹内容如下图所示,我们所需要的只有custom和generated两个文件夹,其余的不用管:
2、移植工作
- 将custom和generated两个文件夹复制粘贴到刚才移植成功的工程的GUI_APP文件下
- 打开该工程
- 将custom和generated两个文件夹下的所有**.c**文件全部都填加到LVGL/GUI_APP分组下
- 填加custom和generated两个文件夹的文件路径,如下图所示:
- 在main.c文件下填加两个头文件
#include "events_init.h" //自己设计的一系列事件都存放在该文件对应的.c文件中 #include "gui_guider.h" //是对自己所设计的所有控件的引用
- 在头文件引用下方定义lv_ui的一个变量
lv_ui guider_ui;
- 在main函数中,while(1)之前填加两行代码:
setup_ui(&guider_ui); events_init(&guider_ui);
- 编译代码,判断是否移植成功,无错误烧录到单片机上观察效果即可。