LittleVGL (LVGL)入门教程一之移植到stm32芯片
前言:
阅读前,请确保你拥有以下条件:
- Keil下的项目的基本创建能力。
- stm32(或其他平台) 的开发经验。(不是这么重要,因为最低要求有画点函数就行)
- 你已经实现了一个屏幕的驱动(至少要有画点函数,因为要对接接口)。
LVGL有三大种需要对接的API,可以互相独立使用
- 显示API(这个必须有,不然上lvgl就没意义了)
- 输入设备API(比如触摸屏、按键 等)
- 文件系统API(如FatFs等)
这篇文章只讲“显示API”的移植。其他两个后面出其他文章讲。
(重要) 编译LVGL至少需要c99标准
目录:
移植和启动LVGL思路:
- 实现画点函数(绘制越快越好)。
- 为Keil项目添加LVGL文件。
- 在几个port文件里对接API。(port文件就是API对接的端口文件,如lv_port_disp.c)
- 使用LVGL文档的例程(不是demo)。
- 循环调用lv_tick_inc()和lv_task_handler()(如果上了OS则分别为这两个创建任务)。
- 调用lvgl的初始化函数(一个主初始化和三个port初始化)。
一、为项目添加LVGL源文件
(一) 下载源码:
1.使用git命令:
git clone https://github.com/lvgl/lvgl.git
2.去LVGL的github页:https://github.com/lvgl/lvgl 下载。
(二) 在keil添加文件:
- lvgl/src :源码,所有源码都在项目根目录的src文件夹里,自行添加即可。(比较多,不列出来)
- lvgl/port :port文件,对接API的地方,在 lvgl/examples/porting/中
- lvgl/user:用户文件,放你自己的东西,我放了lvgl头文件和配置文件,方便编辑。
下载到的源码里,只用下图红圈的文件,其他不用管:
- 其中 lv_conf_template.h 文件名改为 lv_conf.h 并移动到上级目录。(必须)
- 把lvgl/examples/里的 porting文件夹复制出来
- lvgl.h 不动
整理好后的文件结构如下:

(三) 改port文件的文件名并使能port的使用:
1.改名
Q:为什么要改文件名?
A:因为默认名有_tamplate后缀,当然不改也能用,编译器也能编,但代码是人看的,改掉比较好,把后缀_tamplate删除,文件内也要改。
默认名:
改后名:
2.使能port的使用 (重要)
Q:如何使能?
A:在port文件中把“#if 0”改为“#if 1”,c文件和h文件都有这个,其实就是预处理。
(四) 显示API 对接(重要):
我们打开文件 “lv_port_disp.c”,其中的函数:lv_port_disp_init() 就是我们后面要调用的显示设备初始化函数,先记着,我们来看这个函数的内容:
void lv_port_disp_init(void)
{
/*-------------------------
* Initialize your display
* -----------------------*/
// 这个就是我们的显示器初始化函数应该放的地方,函数定义在下面
disp_init();
/*-----------------------------
* Create a buffer for drawing
*----------------------------*/
/* LittlevGL requires a buffer where it draws the objects. The buffer's has to be greater than 1 display row
*
* There are three buffering configurations:
* 1. Create ONE buffer with some rows:
* LittlevGL will draw the display's content here and writes it to your display
*
* 2. Create TWO buffer with some rows:
* LittlevGL 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 LittlevGL 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. Create TWO screen-sized buffer:
* Similar to 2) but the buffer have to be screen sized. When LittlevGL is ready it will give the
* whole frame to display. This way you only need to change the frame buffer's address instead of
* copying the pixels.
* */
#define EXAMPLE 2
#if ( EXAMPLE == 1 )
/* Example for 1) */
static lv_disp_buf_t disp_buf_1;
static lv_color_t buf1_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/
lv_disp_buf_init(&disp_buf_1, buf1_1, NULL, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*/
#endif
#if ( EXAMPLE == 2 )
/* Example for 2) */
static lv_disp_buf_t disp_buf_2;
static lv_color_t buf2_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/
static lv_color_t buf2_2[LV_HOR_RES_MAX * 10]; /*An other buffer for 10 rows*/
lv_disp_buf_init(&disp_buf_2, buf2_1, buf2_2, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*/
#endif
#if ( EXAMPLE == 3 )
/* Example for 3) */
static lv_disp_buf_t disp_buf_3;
static lv_color_t buf3_1[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*A screen sized buffer*/
static lv_color_t buf3_2[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*An other screen sized buffer*/
lv_disp_buf_init(&disp_buf_3, buf3_1, buf3_2, LV_HOR_RES_MAX * LV_VER_RES_MAX); /*Initialize the display buffer*/
#endif
/*-----------------------------------
* Register the display in LittlevGL
*----------------------------------*/
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 = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = disp_flush;
/*Set a display buffer*/
#if ( EXAMPLE == 1 )
disp_drv.buffer = &disp_buf_1;
#endif
#if ( EXAMPLE == 2 )
disp_drv.buffer = &disp_buf_2;
#endif
#if ( EXAMPLE == 3 )
disp_drv.buffer = &disp_buf_3;
#endif
#if LV_USE_GPU
/*Optionally add functions to access the GPU. (Only in buffered mode, LV_VDB_SIZE != 0)*/
/*Blend two color array using opacity*/
disp_drv.gpu_blend_cb = gpu_blend;
/*Fill a memory array with a color*/
disp_drv.gpu_fill_cb = gpu_fill;
#endif
/*Finally register the driver*/
lv_disp_drv_register(&disp_drv);
}
/* Initialize your display and the required peripherals. */
static void disp_init(void)
{
/*You code here*/
scr_init(); // 你的屏幕的初始化函数
}
首先我们第一个需要改的地方就是EXAMPLE的地方,添加了一些预处理语句,分别是三种不同的缓冲方式,源码是没有这些宏定义的,如果不想自己改,直接复制上面我的即可,调整宏使用不同的缓冲方式。
第二就是下面的代码,LV_HOR_RES_MAX和LV_VER_RES_MAX定义在“lv_conf.h”中,
原port文件没有使用这两个宏定义,分别代表显示器的宽和高。
/*Set the resolution of the display*/
disp_drv.hor_res = LV_HOR_RES_MAX;
disp_drv.ver_res = LV_VER_RES_MAX;
我们继续往下看,找到disp_flush函数的定义,参考我下面的改法。
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*/
u16 x, y;
u16 x1 = area->x1;
u16 y1 = area->y1;
u16 x2 = area->x2;
u16 y2 = area->y2;
// 设置显示区域,函数是你自己实现的,这是我改后的
// 例如ST7735的驱动就可以设置显示绘制区域
set_region( x1, y1, x2, y2 );
for(y = y1; y <= y2; y++) {
for(x = x1; x <= x2; x++) {
/* Put a pixel to the display. For example: */
/* put_px(x, y, *color_p)*/
// 画点函数,例如常见的一些屏用的是16位颜色,你把16位数据输出到屏幕即可
send_pixel_dat( color_p->full );
color_p++;
}
}
/* IMPORTANT!!!
* Inform the graphics library that you are ready with the flushing*/
// 这个很重要,不用改也不用删
lv_disp_flush_ready(disp_drv);
}
最后还有比较重要的一步:在他的同名头文件里声明:
void lv_port_disp_init(void)
他默认没声明,也不知道为什么。
那么做到这里,你已经完成了显示API的对接了,接下来可以启动LVGL了。
(五) 启动LVGL:
1.启动
到了这步就很简单了,按照下面顺序在main函数中调用几个初始化函数即可。但是光这样还不能使用LVGL,因为他没有“心跳”。
lv_init();
lv_port_disp_init(); // 显示器初始化
lv_port_indev_init(); // 输入设备初始化(如果没有实现就注释掉)
lv_port_fs_init(); // 文件系统设备初始化(如果没有实现就注释掉)
2. 让他心跳
我们还需要调用两个函数
1. lv_tick_inc( tick ); // tick单位是ms,设置为5ms即可,一般只要有值就行。
2. lv_task_handler(); // 这个比较重要,从名字就能知道他是用来运行lvgl的task的
有两种方式让他心跳:
(1) 不使用OS:
在main函数弄while死循环
#include <rtthread.h>
#include <lvgl.h>
#define LVGL_TICK 5
static void lvgl_init( void )
{
lv_init();
lv_port_disp_init(); // 显示器初始化
// lv_port_indev_init(); // 输入设备初始化
// lv_port_fs_init(); // 文件系统设备初始化
}
int main()
{
lvgl_init();
while(1) {
// 先调用 lv_tick_inc 再调用 lv_task_handler
lv_tick_inc(LVGL_TICK);
lv_task_handler();
delay_ms(LVGL_TICK); // 可以省略,lvgl并不是OS的真正任务
}
}
(2) 使用OS(以rt-thread os为例,其他OS类似):
代码比较长,主要就是创建两个任务分别运行就可以。
#include <rtthread.h>
#include <lvgl.h>
#define APP_THREAD_NUM 5
// 动态线程堆
static rt_thread_t u_threadx[APP_THREAD_NUM] = {
RT_NULL};
// 静态线程堆
static struct rt_thread u_static_threadx[APP_THREAD_NUM];
/* lvgl tick线程 */
#define LVGL_TICK 10
#define LVGL_TICK_THREAD_NAME "lvgl_tick" // 线程名
#define LVGL_TICK_STACK_SIZE 256 // 线程栈大小
#define LVGL_TICK_TIME_SLICE 5 // 线程时间片
#define LVGL_TICK_PRIOROTY 10 // 线程优先级
static rt_thread_t *lvgl_tick_th = &u_threadx[1]; // 从线程堆分配线程
static void lvgl_tick_thread( void *param )
{
param = param;
while ( 1 ) {
lv_tick_inc(LVGL_TICK);
rt_thread_mdelay(LVGL_TICK);
}
}
/* lvgl task handler线程 */
#define LVGL_TASK_THREAD_NAME "lvgl_task" // 线程名
#define LVGL_TASK_STACK_SIZE 2048 // 线程栈大小
#define LVGL_TASK_TIME_SLICE 10 // 线程时间片
#define LVGL_TASK_PRIOROTY 10 // 线程优先级
ALIGN(RT_ALIGN_SIZE) static u8 lvgl_task_stk[LVGL_TASK_STACK_SIZE]; // 线程栈
static struct rt_thread *lvgl_task_th_s = &u_static_threadx[3]; // 从线程堆分配线程
static void lvgl_task_thread( void *param )
{
param = param;
while ( 1 ) {
lv_task_handler();
rt_thread_mdelay(LVGL_TICK);
}
}
static void lvgl_init( void )
{
lv_init();
lv_port_disp_init(); // 显示器初始化
// lv_port_indev_init(); // 输入设备初始化
// lv_port_fs_init(); // 文件系统设备初始化
}
/**************************************************/
int main()
{
lvgl_init();
/* 创建lvgl tick动态线程 */
*lvgl_tick_th = rt_thread_create(
LVGL_TICK_THREAD_NAME, /*线程名字*/
lvgl_tick_thread, /*线程入口函数*/
RT_NULL, /*线程入口函数参数*/
LVGL_TICK_STACK_SIZE, /*线程栈大小*/
LVGL_TICK_PRIOROTY , /*线程优先级*/
LVGL_TICK_TIME_SLICE /*线程时间片*/
);
if(lvgl_tick_th !=RT_NULL)
rt_thread_startup (*lvgl_tick_th);
else
return -1;
/* 创建lvgl task 静态线程 */
err = rt_thread_init(
lvgl_task_th_s,
LVGL_TASK_THREAD_NAME, /*线程名字*/
lvgl_task_thread, /*线程入口函数*/
RT_NULL, /*线程入口函数*/
lvgl_task_stk, /*线程栈*/
LVGL_TASK_STACK_SIZE, /*线程栈大小*/
LVGL_TASK_PRIOROTY,
LVGL_TASK_TIME_SLICE
);
if ( err == RT_EOK )
rt_thread_startup (lvgl_task_th_s);
else
rt_kprintf( "create thread \"%s\" error. (%d)\n", LVGL_TASK_THREAD_NAME, err );
return 0;
}
(六) 简单的配置lv_conf.h文件:
lv_conf.h 中把“#if 0”改为“#if 1”
里面比较重要的地方有三处:
1. 屏幕尺寸宏定义
// 改为你的屏幕尺寸
#define LV_HOR_RES_MAX (128) // 水平 (X)
#define LV_VER_RES_MAX (128) // 垂直 (Y)
2. memory宏定义
找到下面这两个宏定义
#define LV_MEM_SIZE (48U * 1024U) // 定义LVGL使用的内存大小,我定义了48k
#define LV_MEM_ADR 0 // 定义LVGL使用的内存地址,设置为0或手动设置
3. 兼容宏定义
/*1: Use the functions and types from the older API if possible */
#define LV_USE_API_EXTENSION_V6 0 // 兼容v6版本的API(默认关闭)
#define LV_USE_API_EXTENSION_V7 1 // 兼容v7版本的API(默认开启)
至此,我们完成了LVGL最基本的移植工作,我们接下来可以使用一些官方文档里的例程,看看效果。
(七) 使用文档例程(不是demo)
前面提到“官方文档”,其实就是lvgl的数据手册,不同版本的数据手册不通用,如lvgl v7和v6有大部分函数不一样。
lv_ex_label_1例程创建了一个长滚动条,当你的字符总长度大于屏幕宽度就会滚动。
#include <rtthread.h>
#include <lvgl.h>
#define LVGL_TICK 5
static void lv_ex_label_1(void)
{
lv_obj_t * label2 = lv_label_create(lv_scr_act(), NULL);
lv_label_set_recolor(label2, true);
lv_label_set_long_mode(label2, LV_LABEL_LONG_SROLL_CIRC); /*Circular scroll*/
lv_obj_set_width(label2, 120);
// Hello world ! Trisuborn.
lv_label_set_text(label2, "#ff0000 Hello# #00ff00 world ! Trisuborn.#");
lv_obj_align(label2, NULL, LV_ALIGN_CENTER, 0, 0);
}
static void lvgl_init( void )
{
lv_init();
lv_port_disp_init(); // 显示器初始化
// lv_port_indev_init(); // 输入设备初始化
// lv_port_fs_init(); // 文件系统设备初始化
}
int main()
{
lvgl_init();
lv_ex_label_1();
while(1) {
// 先调用 lv_tick_inc 再调用 lv_task_handler
lv_tick_inc(LVGL_TICK);
lv_task_handler();
delay_ms(LVGL_TICK); // 可以省略,lvgl并不是OS的真正任务
}
}
(八) 效果图GIF
可以参考我的项目:
https://github.com/Trisuborn/mp3-lvgl-stm32f405rgt
本篇完
其他:
LittleVGL (LVGL)干货入门教程一之移植到stm32芯片
LittleVGL (LVGL)干货入门教程二之LVGL的输入设备(indev)API对接。
LittleVGL (LVGL)干货入门教程三之LVGL的文件系统(fs)API对接。
LittleVGL (LVGL)干货入门教程四之制作和使用中文汉字字库