STM32之LVGL移植
- 2.8寸TFT-LCD屏原理与应用
- 基本参数
- 引脚说明
- 硬件接线
- 程序移植
- 2.8寸LCD移植LVGL流程分析
- 基本概念
LVGL的英文全称为Light and Versatile Graphics Library,翻译为中文就是轻便而多功能的图形库,LVGL是目前最流行的免费开源的嵌入式图形库之一,可以为任意一款MCU、MPU以及显示器创建漂亮的UI界面。
- 硬件要求
- 支持平台
- 版本说明
LVGL目前主流的版本有LVGL8和LVGL9,如果打算在STM32上移植LVGL,推荐大家选择LVGL8,原因如下:
- 资源占用
STM32F407系列MCU拥有2MB Flash和192KB RAM,虽然这在嵌入式系统中算是相对丰富的资源,但是与一些更高级别的微控制器相比仍然有限。LVGL 8相较于LVGL 9可会有更低的资源需求,这对于资源受限的设备来说是一个重要的考量点。
- 高稳定性
LVGL 8作为一个成熟的版本,已经被广泛使用,并且经过了时间的考验。对于那些需要高稳定性的应用,选择一个成熟的版本可以减少潜在的风险。
- 源码下载
- 进入源码页面后,选择要下载的源码版本,目前稳定版为8.3.11,选中release/v8.3即可,尝鲜的同学可以选择v8.4或v9.0版本(特别是v9版本要求MCU的主频及内存资源更高,目前流畅度稍低于v8版本)。
- 选中源码版本为8.3后,点击右侧的【< >code】按钮执行源码下载,选择下载ZIP格式即可。
- 移植文件
- 打开移植好的2.8英寸电阻屏的工程,在该工程目录下创建LVGL文件夹,并将LVGL所有源码解压到该文件夹。
- 将带有_template后缀的c与h文件,删除“_template”文字,如lv_conf_template.h的名字修改为lv_conf.h,详细如下:
路径 | 文件名 | 修改后的文件名 |
LVGL\ | lv_conf_template.h | lv_conf.h |
LVGL\examples\porting\ | lv_port_disp_template.c | lv_port_disp.c |
lv_port_disp_template.h | lv_port_disp.h | |
lv_port_fs_template.c | lv_port_fs.c | |
lv_port_fs_template.h | lv_port_fs.h | |
lv_port_indev_template.c | lv_port_indev.c | |
lv_port_indev_template.h | lv_port_indev.h |
- 进入管理项目项界面,选中【Project Items】标签页,创建LVGL_CORE、LVGL_DRAW……LVGL_DEMOKEYPAD等多个Group(组),并为每个组添加对应的c文件,详细如下图:
Groups对应添加文件路径如下:
Groups | 文件路径 |
LVGL_CORE | LVGL\src\core |
LVGL_DRAW | LVGL\src\draw LVGL\src\draw\子文件夹 |
LVGL_EXTRA | LVGL\src\extra LVGL\src\extra\子文件夹 |
LVGL_FONT | LVGL\src\font |
LVGL_HAL | LVGL\src\hal |
LVGL_MISC | LVGL\src\misc |
LVGL_WIDGETS | LVGL\src\widgets |
LVGL_PORTING | LVGL\examples\porting |
LVGL_CONFIG | LVGL\lvgl.h LVGL\lvgl_conf.h |
LVGL_DEMO_BENCHMARK | LVGL\demos\benchmark LVGL\demos\benchmark\子文件夹 |
LVGL_DEMO_STRESS | LVGL\demos\stress |
LVGL_DEMO_WIDGETS | LVGL\demos\widgets LVGL\demos\widgets\子文件夹 |
LVGL_DEMO_KEYPAD | LVGL\demos\keypad_encoder |
所有文件添加完成后,工程目录树结构如下:
- 配置工程
- 修改启动文件中的堆栈大小,在startup_stm32f40_41xxx.s文件中检查栈大小至少为0x1000,若无,请设置。
- 进入Target的options,选中【C/C++】标签页,在Define中添加宏LV_LVGL_H_INCLUDE_SIMPLE,并勾选C99 Mode。
添加宏LV_LVGL_H_INCLUDE_SIMPLE原因是在不少文件都存在这个宏定义,若不使能,则要再新建lvgl目录,为了方便访问到lvgl.h头文件,则在MDK的target的options中添加定义LV_LVGL_H_INCLUDE_SIMPLE该宏 。
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
#include "lvgl.h"
#else
#include "lvgl/lvgl.h"
#endif
另外,LVGL源码的编译需要C99模式的支持,不然会出现大量的报错,所以大家需要启用C99模式。
- 包含头文件路径,点击“魔术棒”,然后选择C/C++,在Include Paths添加新的头文件路径,详细如下。
- 配置文件
- 启用LVGL\lv_conf.h这个头文件,因为默认该文件是不参与编译,必须将#if 0 修改为 #if 1,如下图。
- 根据当前显示设备支持的颜色深度设置宏LV_COLOR_DEPTH,对于SPI TFT屏且每8位的传输,确定是否要交换16位色的高低字节,当前显示设备是高字节优先传输,若使能了DMA传输,要设置宏LV_COLOR_16_SWAP为1。
/*====================
COLOR SETTINGS
*====================*/
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 0
- 需要使能LVGL配置文件中的宏定义LV_USE_DEMO_WIDGETS,这样可以使用LVGL提供的demo验证显示效果。
#define LV_USE_DEMO_WIDGETS 1
#if LV_USE_DEMO_WIDGETS
#define LV_DEMO_WIDGETS_SLIDESHOW 0
#endif
- 显示设备
- 启用LVGL\examples\porting\lv_port_disp.c,默认该文件是不参与编译,必须将#if 0 修改为 #if 1,另外,“lv_port_disp_template.h”要修改为“lv_port_disp.h”,详细如下图。
- 当前文件会涉及到显示设备的初始化、颜色填充等函数,需要引入相关支撑tft函数的头文件,并补充增加"lvgl.h"头文件。
- 启用LVGL\examples\porting\lv_port_disp.h,默认该文件是不参与编译,必须将#if 0 修改为 #if 1,如下图。
- 修改分辨率,根据用到的屏幕分辨率修改宏MY_DISP_HOR_RES(屏幕宽度)与MY_DISP_VER_RES(屏幕高度)。
/*********************
* DEFINES
*********************/
#ifndef MY_DISP_HOR_RES
#warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now.
#define MY_DISP_HOR_RES 320
#endif
#ifndef MY_DISP_VER_RES
#warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen height, default value 240 is used for now.
#define MY_DISP_VER_RES 240
#endif
- 修改lv_port_disp_init函数,lv_port_disp_init 函数是LVGL在特定平台或模拟器上初始化显示驱动的接口函数。在实际嵌入式项目中,需要根据硬件平台编写或修改这个函数来适配显示控制器。
此函数通常包含以下内容:
初始化硬件:配置和打开与显示屏连接的硬件接口(disp_init)。
创建显示器描述符:定义一个 lv_disp_drv_t 结构体实例,该结构体包含了LVGL需要的所有关于显示控制器的信息,例如:一个回调函数用于写像素数据到屏幕(disp_flush)。
注册显示器驱动:调用 lv_disp_drv_register 函数,将创建好的显示器驱动结构体注册给LVGL核心库,使其能够通过该驱动与实际硬件进行通信。
LVGL需要使用缓冲区,用于在内部绘制小部件。此缓冲区将传递给显示器驱动程序的“flush_cb”(copy the buffer's content to the display),以将其内容复制到显示器,另外缓冲区必须大于1个显示行。
LVGL有3种缓冲配置:
(1)创建一个行缓冲区(移植时使用该缓冲配置)
LVGL将在此处绘制显示器的内容,并将其写入显示器。
(2)创建两个行缓冲区(有DMA外设则使用该缓冲配置,能大幅提高显示帧率)
LVGL将绘制显示的内容到缓冲区并将其写入显示,并使用DMA将缓冲区的内容写入显示器,它将使LVGL绘制屏幕的下一部分到另一个缓冲区同时数据从第一个缓冲区发送。它使渲染和并行刷新。
(3)双重帧缓冲(有DMA且大内存则推荐使用该缓冲配置,进一步提高显示帧率)
设置2个屏幕大小的缓冲区,并设置disp_drv.full_refresh=1。这样,LVGL将始终在flush_cb中提供整个渲染屏幕,并且只需要更改帧缓冲区的地址。
使用第一种缓冲配置,需注释掉Example for 2 与 Example for 3代码。
void lv_port_disp_init(void)
{
/*-------------------------
* Initialize your display
* -----------------------*/
disp_init();
/*-----------------------------
* Create a buffer for drawing
*----------------------------*/
/**
* LVGL requires a buffer where it internally draws the widgets.
* Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display.
* The buffer has to be greater than 1 display row
*
* There are 3 buffering configurations:
* 1. Create ONE buffer:
* LVGL will draw the display's content here and writes it to your display
*
* 2. Create TWO buffer:
* 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. Double buffering
* Set 2 screens sized buffers and set disp_drv.full_refresh = 1.
* This way LVGL will always provide the whole rendered screen in `flush_cb`
* and you only need to change the frame buffer's address.
*/
/* 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*/
#if 0
/* Example for 2) */
// static lv_disp_draw_buf_t draw_buf_dsc_2;
// static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/
// static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10]; /*An other buffer for 10 rows*/
// lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
/* Example for 3) also set disp_drv.full_refresh = 1 below*/
// 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]; /*A screen sized buffer*/
// static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*Another screen sized buffer*/
// lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2,
// MY_DISP_VER_RES * LV_VER_RES_MAX); /*Initialize the display buffer*/
#endif
/*-----------------------------------
* Register the display in LVGL
*----------------------------------*/
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;
/*Required for Example 3)*/
// disp_drv.full_refresh = 1;
/* Fill a memory array with a color if you have GPU.
* Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL.
* But if you have a different GPU you can use with this callback.*/
// disp_drv.gpu_fill_cb = gpu_fill;
/*Finally register the driver*/
lv_disp_drv_register(&disp_drv);
}
- 编写disp_init函数,该函数初始化显示器和所需的外围设备,并被lv_port_disp_init函数所调用。
/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
LCD_Init();
LCD_Display_Dir(0);
LCD_Scan_Dir(U2D_R2L);
lcddev.width=320;
lcddev.height=240;
}
- 编写disp_flush函数,将内部缓冲区的内容刷新到显示器上的特定区域,并被disp_drv.flush_cb进行回调。
/*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)
{
int32_t x;
int32_t y;
if(disp_flush_enabled)
{
LCD_Color_Fill(area->x1,area->y1,area->x2,area->y2,(uint16_t *)color_p);
lv_disp_flush_ready(disp_drv);
}
}
- 输入设备
- 启用examples\porting\lv_port_indev.c,默认该文件是不参与编译,必须将#if 0 修改为 #if 1 ,另外,必须把“lv_port_indev_template.h”要修改为“lv_port_indev.h”,详细如下图。
另外额外增加以下头文件,支持触摸检测。
#include "touch.h"
#include "tft.h"
- 启用examples\porting\lv_port_indev.h,默认该文件是不参与编译,必须将#if 0 修改为 #if 1,如下图。
把该头文件中的
#include "lvgl/lvgl.h"
修改为
#include "lvgl.h"
- 在lv_port_indev_init函数中屏蔽鼠标、键盘、编码器等相关代码,只保留Touchpad(触摸板)设备。
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
*/
static lv_indev_drv_t indev_drv;
/*------------------
* Touchpad
* -----------------*/
/*Initialize your touchpad if you have*/
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);
#if 0
/*------------------
* Mouse
* -----------------*/
/*Initialize your mouse if you have*/
mouse_init();
/*Register a mouse input device*/
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);
/*Set cursor. For simplicity set a HOME symbol now.*/
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
* -----------------*/
/*Initialize your keypad or keyboard if you have*/
keypad_init();
/*Register a keypad input device*/
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);
/*Later you should create group(s) with `lv_group_t * group = lv_group_create()`,
*add objects to the group with `lv_group_add_obj(group, obj)`
*and assign this input device to group to navigate in it:
*`lv_indev_set_group(indev_keypad, group);`*/
/*------------------
* Encoder
* -----------------*/
/*Initialize your encoder if you have*/
encoder_init();
/*Register a encoder input device*/
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);
/*Later you should create group(s) with `lv_group_t * group = lv_group_create()`,
*add objects to the group with `lv_group_add_obj(group, obj)`
*and assign this input device to group to navigate in it:
*`lv_indev_set_group(indev_encoder, group);`*/
/*------------------
* Button
* -----------------*/
/*Initialize your button if you have*/
button_init();
/*Register a button input device*/
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);
/*Assign buttons to points on the screen*/
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);
#endif
}
- 修改touchpad_init函数,该函数会被lv_port_indev_init调用,并需增加初始化触摸板设备的代码。
/*Initialize your touchpad*/
static void touchpad_init(void)
{
/*Your code comes here*/
TP_Init();
}
- 修改touchpad_is_pressed函数,该函数会被touchpad_read调用,并需增加触摸板设备按压检测的代码。
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
if(PEN)
return false;
return true;
}
- 修改touchpad_get_xy函数,该函数会被touchpad_read调用,并需增加触摸板设备获取x、y坐标值的代码。
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(lv_coord_t *x, lv_coord_t *y)
{
/*Your code comes here*/
if(TP_Scan(0))
{
(*x) = tp_dev.x[0];
(*y) = tp_dev.y[0];
}
}
- 提供心跳
lv_tick_inc()是LVGL中的一个函数,主要用于模拟系统时钟的滴答(tick)更新。在LVGL中,系统时钟被用于动画、延时处理等定时任务。
该函数的主要作用是将LVGL内部维护的系统 tick 计数器加1,表示系统时间已经向前推进了一个单位的时间间隔(通常是毫秒级别)。在实际使用中,用户需要在合适的时机(例如硬件定时器中断服务程序中如TIM3_IRQHandler)调用此函数,以便LVGL能够正确地进行定时相关的操作,参考代码如下:
void TIM3_IRQHandler(void)
{
/* 检测时间更新中断的标志位是否置位 */
if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
/* 系统时间向前推进一个单位的时间间隔 */
lv_tick_inc(1);
/* 清空标志位 */
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
- 任务处理
要处理 LVGL 的任务,我们需要定期通过以下方式之一调用 lv_task_handler() ,提供以下方案:
- main 函数中设置 while(1) 调用
- 定期定时中断(低优先级然后是 lv_tick_inc()) 中调用
- 定期执行的 OS 任务中调用
计时并不严格,但应保持大约5毫秒以保持系统响应。
while(1)
{
lv_task_handler();
delay_ms(5);
}
- 案例演示
- 在工程中创建includes.h头文件,在该头文件中包含以下lvgl相关头文件,如下图所示:
- 在main函数中,调用lv_init、lv_port_disp_init、lv_port_indev_init、lv_demo_widgets、tim3_init等相关初始化函数,在while(1)不可退出循环中添加lv_task_handler函数,该函数为图形库中的一个核心函数,它负责处理所有LVGL的任务和事件。
#include "includes.h"
// 主函数
int main(void)
{
// 初始化lvgl
lv_init();
// 初始化lvgl显示设备
lv_port_disp_init();
// 初始化lvgl输入设备
lv_port_indev_init();
// 初始化lvgl demo
lv_demo_widgets();
// tim3初始化,定时周期为1ms
tim3_init();
while (1)
{
lv_task_handler();
}
return 0;
}
- 将编译好的程序下载到开发板,可以看到开发板的LCD屏上成功显示LVGL提供的demo。