一、LVGL简介
LVGL(Light and Versatile Graphics Library)轻量级通用型图形库,是一个免费的开源图形库,提供了创建嵌入式 GUI 所需的一切,具有易于使用的组件,美观的视觉效果和低内存占用等特点。支持触摸屏操作,移植简单方便,开发者一直在不断完善更新。
特点:
- 丰富且强大的模块化图形组件:按钮 (buttons)、图表 (charts)、列表 (lists)、滑动条 (sliders)、图片 (images) 等
- 高级的图形引擎:动画、抗锯齿、透明度、平滑滚动、图层混合等效果
- 支持多种输入设备:触摸屏、 键盘、编码器、按键等
- 支持多显示设备
- 不依赖特定的硬件平台,可以在任何显示屏上运行
- 配置可裁剪(最低资源占用:64 kB Flash,16 kB RAM)
- 基于UTF-8的多语种支持,例如中文、日文、韩文、阿拉伯文等
- 可以通过类CSS的方式来设计、布局图形界面(例如:Flexbox、Grid)
- 支持操作系统、外置内存、以及硬件加速(LVGL已内建支持STM32 DMA2D、NXP PXP和VGLite)
- 即便仅有单缓冲区(frame buffer)的情况下,也可保证渲染如丝般顺滑
- 全部由C编写完成,并支持C++调用
- 支持Micropython编程,参见:LVGL API in Micropython
- 支持模拟器仿真,可以无硬件依托进行开发
- 丰富详实的例程
- 详尽的文档以及API参考手册,可线上查阅或可下载为PDF格式
二、FSMC配置LCD屏显示和触摸
查看 STM32CubeMX学习笔记(38)——FSMC接口使用(TFT-LCD屏显示) 查看 STM32CubeMX学习笔记(39)——FSMC接口使用(TFT-LCD屏触摸)
三、TIM6基本定时器(可跳过,看LVGL心跳的配置方式选择)
3.1 参数配置
在 Timers 中选择 TIM6 设置,并勾选 Activated 激活
在 Parameter Settings 进行具体参数配置。
Tclk 即内部时钟CK_INT,经过APB1预分频器后分频提供,如果APB1预分频系数等于1,则频率不变,否则频率乘以2,库函数中APB1预分频的系数是2,即PCLK1=36M,如图所以定时器时钟Tclk=36*2=72M。
定时器溢出时间:
Tout = 1 / (Tclk / (psc + 1)) ? (arr + 1)
- 定时器时钟Tclk:72MHz
- 预分频器psc:71
- 自动重装载寄存器arr:999
即 Tout = 1/(72MHz/(71+1))?(999+1) = 1ms
- Prescaler(时钟预分频数):72-1
则驱动计数器的时钟 CK_CNT = CK_INT(即72MHz)/(71+1) = 1MHz - Counter Mode(计数模式):Up(向上计数模式)
基本定时器只能是向上计数 - Counter Period(自动重装载值):1000-1
则定时时间 1/CK_CLK*(999+1) = 1ms - auto-reload-preload(自动重装载):Enable(使能)
- TRGO Parameters(触发输出):不使能
在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换)
3.2 配置NVIC
使能定时器中断
四、工程管理
4.1 增大栈空间
将最小栈空间改到 0x1000
注意:由于LVGL运行的硬件要求,故需要加大项目的栈空间到 2KB 和使能 C99 编译器功 能,修改 Stack_Size 的值大于 2KB(0x00000800),。
4.2 生成代码
输入项目名和项目路径 选择应用的 IDE 开发环境 MDK-ARM V5 每个外设生成独立的 ’.c/.h’ 文件 不勾:所有初始化代码都生成在 main.c 勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。 点击 GENERATE CODE 生成代码
五、移植LVGL
5.1 下载源码
我用的是 LVGL 8.1 版本
5.2 新建文件夹
- 首先在工程根目录下新建两个文件夹,命名
GUI 和 GUI_APP
GUI 目录是用来存放跟LVGL库相关的所有文件的。 GUI_APP 是用来放我们自己的GUI应用代码的,因为现在才刚开始移植,还来不及自己写GUI应用,所以GUI_APP目录里面先留空。
- 然后在
GUI 文件夹下新建三个空文件夹,lvgl 、lvgl_port
lvgl 官方各类控件的源程序。 lvgl_port 用于存放LVGL显示屏驱动、输入设备驱动及文件系统驱动。
5.3 拷贝LVGL src文件和根目录lvgl.h到lvgl文件夹
src 所有源码都在项目根目录的src文件夹里。 lvgl.h 包含了LVGL库中的所有头文件。
5.4 拷贝LVGL examples/porting文件到lvgl_port文件夹
复制"lvgl-8.1.0\examples\porting"中的文件到工程目录下的"GUI\lvgl_port"目录下。并将他们改名 去掉template 。
lv_port_disp 为LVGL显示驱动。 lv_port_fs 为LVGL文件系统驱动。 lv_port_indev 为LVGL输入设备驱动。
5.5 拷贝配置文件lv_conf_template.h到GUI文件夹
复制LVGL库根目录下的"lv_conf_template.h"文件到工程文件夹“GUI”下,并将"lv_conf_template.h"重命名为"lv_conf.h"。
lv_conf.h 是LVGL库的配置文件,里面有各种宏。
5.6 添加源码到工程组文件夹
接下来我们在 mdk 里面新建 GUI/lvgl 和 GUI/lvgl_port 两个组文件夹,其中 GUI/lvgl 用于存放 src 文件夹的内容,GUI/lvgl_port 用于存放 examples\porting 文件夹的内容。
在GUI/lvgl组中添加以下文件夹中所有的.c文件:
GUI/lvgl/src/lv_core
GUI/lvgl/src/lv_draw
GUI/lvgl/src/lv_extra(除了lib外,除非你用到了相关功能)
GUI/lvgl/src/lv_font
GUI/lvgl/src/lv_hal
GUI/lvgl/src/lv_misc
GUI/lvgl/src/lv_themes
GUI/lvgl/src/lv_widgets
在GUI/lvgl_port组中添加以下.c文件:
GUI/lvgl_port/lv_port_disp.c
GUI/lvgl_port/lv_port_indev.c
在User组中添加lvgl_conf.h配置文件:
5.7 指定头文件路径
LVGL 的源码已经添加到开发环境的组文件夹下面,编译的时候需要为这些源文件指定头文件的路径,不然编译会报错。只需要将 GUI 、GUI/lvgl 、GUI/lvgl_port 的路径在开发环境里面指定即可。
5.8 设置编译参数
C99:LVGL要求 C99 或更新的编译器,否则编译是会报错的。
5.9 修改FreeRTOSConfig.h
#if 1
#define LV_COLOR_DEPTH 16
- 内存设置
给LVGL分配动态内存RAM的大小,至少需要2k,资源允许的情况下可以稍微设大些,这个设置过小的话,在跑一些稍微复杂的demo时界面就会刷不出来。
#define LV_MEM_CUSTOM 0
#if LV_MEM_CUSTOM == 0
# define LV_MEM_SIZE (32U * 1024U)
#define LV_TICK_CUSTOM 1
#if LV_TICK_CUSTOM
#define LV_TICK_CUSTOM_INCLUDE "stm32f1xx_hal.h"
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (HAL_GetTick())
#endif
#define LV_DPI_DEF 130
#define LV_USE_PERF_MONITOR 0
#if LV_USE_PERF_MONITOR
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
#endif
#define LV_USE_MEM_MONITOR 0
#if LV_USE_PERF_MONITOR
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
#endif
六、修改显示驱动接口
使能文件及添加头文件
#if 1
#include "lv_port_disp.h"
#include "lvgl.h"
#include "bsp_ili9341_lcd.h"
添加及修改屏幕像素高度和宽度,根据实际屏幕尺寸
#define MY_DISP_HOR_RES (320)
#define MY_DISP_VER_RES (240)
···
···
disp_drv.hor_res = MY_DISP_HOR_RES;
disp_drv.ver_res = MY_DISP_VER_RES;
找到 disp_init() 函数,将显示屏初始化驱动 ILI9341_Init() 放到这里
static void disp_init(void)
{
ILI9341_Init();
}
修改 lv_port_disp_init() 函数
void lv_port_disp_init(void)
{
disp_init();
static lv_disp_draw_buf_t draw_buf_dsc_1;
static lv_color_t buf_1[MY_DISP_HOR_RES * 40];
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 40);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = MY_DISP_HOR_RES;
disp_drv.ver_res = MY_DISP_VER_RES;
disp_drv.flush_cb = disp_flush;
disp_drv.draw_buf = &draw_buf_dsc_1;
lv_disp_drv_register(&disp_drv);
}
修改 disp_flush() 函数,将自己显示屏对应的填充颜色块函数放到这里,这个函数是用来刷新显示区域的,速度越快越好
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x;
int32_t y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
ILI9341_DrawPixel((uint16_t)x, (uint16_t)y, (uint16_t)color_p->full);
color_p++;
}
}
lv_disp_flush_ready(disp_drv);
}
红色标注部分的函数也就是以单个像素点填充屏幕的函数,这个函数野火写的不满足调用要求,稍微将原来的驱动代码进行了更改,实现了如下所示的单个像素点填充函数:
void ILI9341_DrawPixel ( uint16_t usX, uint16_t usY, uint16_t uColor )
{
if ( ( usX < LCD_X_LENGTH ) && ( usY < LCD_Y_LENGTH ) )
{
ILI9341_SetCursor ( usX, usY );
ILI9341_FillColor ( 1, uColor );
}
}
#if 1
···
···
void lv_port_disp_init(void);
七、修改输入设备驱动接口
使能文件及添加头文件
#if 1
#include "lv_port_indev.h"
#include "lvgl.h"
#include "bsp_xpt2046_lcd.h"
找到 touchpad_init() 函数,将触摸屏初始化驱动 XPT2046_Init_Init() 放到这里
static void touchpad_init(void)
{
XPT2046_Init();
}
修改 lv_port_indev_init() 函数,这里是初始化输入设备驱动和在LVGL中注册一个输入设备。输入设备可以是触摸屏、鼠标、键盘、编码器、按键,这里我们只使用触摸屏,其余的删除。
void lv_port_indev_init(void)
{
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);
}
修改 touchpad_is_pressed() 和 touchpad_get_xy() 函数
static bool touchpad_is_pressed(void)
{
if(TOUCH_PRESSED == XPT2046_TouchDetect())
{
return true;
}
return false;
}
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
{
static strType_XPT2046_Coordinate info = {-1,-1,-1,-1};
XPT2046_Get_TouchedPoint(&info, strXPT2046_TouchPara);
(*x) = info.x;
(*y) = info.y;
}
- lv_port_indev.h
使能文件及声明函数
#if 1
···
···
void lv_port_indev_init(void);
八、修改main.c
8.1 包含头文件
#include "main.h"
#include <stdio.h>
#include "bsp_ili9341_lcd.h"
#include "bsp_xpt2046_lcd.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
#include "lvgl.h"
8.2 初始化LVGL
lv_init();
lv_port_disp_init();
lv_port_indev_init();
8.3 配置LVGL心跳
8.3.1 配置LV_TICK_CUSTOM方式(推荐,选择其中一种)
过 x 毫秒调用 lv_tick_inc(x) 函数一次(1 ≤ x ≤ 10),这个函数是LittlevGL运行所需的时钟源。
如果定义LV_TICK_CUSTOM 为1 的话,就无须在应用程序中主动调用 lv_tick_inc(x) 函数,而是需要定义一个获取当前系统已运行时间的函数(例如HAL_GetTick())并使用宏定义LV_TICK_CUSTOM_SYS_TIME_EXPR 表示该函数,这个函数会在调用 lv_task_handler() 函数的时候自动调用并获取当前时间戳。
int main(void)
{
···
···
while (1)
{
lv_task_handler();
}
}
8.3.1 基本定时器方式(选择其中一种)
修改中断回调函数 打开 stm32f1xx_it.c 中断服务函数文件,找到 TIM6 中断的服务函数 TIM6_IRQHandler() 中断服务函数里面就调用了定时器中断处理函数 HAL_TIM_IRQHandler()
打开 stm32f1xx_hal_tim.c 文件,找到定时器中断处理函数原型 HAL_TIM_IRQHandler() ,其主要作用就是判断是哪个定时器产生哪种事件中断,清除中断标识位,然后调用中断回调函数 HAL_TIM_PeriodElapsedCallback() 。
/* NOTE: This function Should not be modified, when the callback is needed, the HAL_GPIO_EXTI_Callback could be implemented in the user file */ 这个函数不应该被改变,如果需要使用回调函数,请重新在用户文件中实现该函数。
HAL_TIM_PeriodElapsedCallback() 按照官方提示我们应该再次定义该函数,__weak 是一个弱化标识,带有这个的函数就是一个弱化函数,就是你可以在其他地方写一个名称和参数都一模一样的函数,编译器就会忽略这一个函数,而去执行你写的那个函数;而 UNUSED(htim) ,这就是一个防报错的定义,当传进来的定时器号没有做任何处理的时候,编译器也不会报出警告。其实我们在开发的时候已经不需要去理会中断服务函数了,只需要找到这个中断回调函数并将其重写即可而这个回调函数还有一点非常便利的地方这里没有体现出来,就是当同时有多个中断使能的时候,STM32CubeMX会自动地将几个中断的服务函数规整到一起并调用一个回调函数,也就是无论几个中断,我们只需要重写一个回调函并判断传进来的定时器号即可。
接下来我们就在 stm32f1xx_it.c 这个文件的最下面添加 HAL_TIM_PeriodElapsedCallback()
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint32_t time = 0;
if(htim->Instance == TIM6)
{
lv_tick_inc(1); lv_tick_inc(1);
}
}
添加定时器启动函数 现在进入 main 函数并在 while 循环前加入开启定时器函数 HAL_TIM_Base_Start_IT() ,这里所传入的 htim6 就是刚刚定时器初始化后的结构体。
int main(void)
{
···
···
HAL_TIM_Base_Start_IT(&htim6);
while (1)
{
lv_task_handler();
}
}
8.4 执行demo
实现 btn_event_cb() 按键事件回调,实现 lvgl_first_demo_start() 并在 main() 中调用。
static void btn_event_cb(lv_event_t * event)
{
lv_obj_t *btn = lv_event_get_target(event);
if(event->code == LV_EVENT_CLICKED)
{
static uint8_t cnt = 0;
cnt++;
lv_obj_t * label = lv_obj_get_child(btn, NULL);
lv_label_set_text_fmt(label, "Button: %d", cnt);
}
}
static void lvgl_first_demo_start(void)
{
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_set_pos(btn, 10, 10);
lv_obj_set_size(btn, 120, 50);
lv_obj_add_event_cb(btn, (lv_event_cb_t)btn_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Yeah");
lv_obj_t * label1 = lv_label_create(lv_scr_act());
lv_label_set_text(label1, "Hello world!");
lv_obj_align(label1, LV_ALIGN_CENTER, 0, 0);
lv_obj_align_to(btn, label1, LV_ALIGN_OUT_TOP_MID, 0, -10);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
MX_FSMC_Init();
MX_TIM6_Init();
lv_init();
lv_port_disp_init();
lv_port_indev_init();
ILI9341_GramScan(3);
lvgl_first_demo_start();
while (1)
{
lv_timer_handler();
}
}
查看效果:
九、工程代码
链接:https://pan.baidu.com/s/1mmcFnxAvYHJT-WxbNh3spQ?pwd=0htb 提取码:0htb
十、注意事项
用户代码要加在 USER CODE BEGIN N 和 USER CODE END N 之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。
? 由 Leung 写于 2022 年 1 月 27 日
? 参考:【LVGL学习之旅 01】移植LVGL到STM32 STM32移植LittleVgl(LVGL)嵌入式开源图形库 LittleVGL(LVGL) V8版本 干货入门教程一之移植到STM32并运行 野火指南者开发板移植 lvgl 库
|