一、前言
在上文中,我们成功的移植进了FreeRTOS,接下来我们在此基础上,移入我们的LVGL图形界面库。
二、LVGL
-
一款用于绘制界面UI的开源库,让硬件资源更少的MCU跑出显示效果理想的界面。实际效果可以参考官方或者视频网站上开发者公布出来的界面效果。
-
从官方的Github上来看,最早的公开为V5版本,V6和V7版本已经确定不再更新,目前LVGL官方团队仍在更新V8版本,我们可以看到V8里还有不少小版本。
-
截至本文编写发布,LVGL正式发布的版本已至V8.3.7。
-
从V8版本开始由于LVGL官方团队对API做了很大的调整,并不是很兼容V7及以前的版本。这里我们做个区分。
-
使用的版本越新,意味着LVGL自带的插件更丰富,这也同样意味着对硬件的资源要求越来越高,可以看看下面V7版本和V8.1版本下官方对硬件资源的推荐配置
-
不同版本间存在多少差异这里不作详细赘述,可自行阅读官方对版本差异的说明。为了让资源更少的MCU也跑上LVGL,本文选择V7版本,作为LVGL的移植源码。
三、移植准备
移植前请准备好以下内容:
- GD32F303+FreeRTOS的keil工程
工程可按照GD32F303调试小记(九)之FreeRTOS移植创建。 - LVGL源码
我使用的是V7.11,源码在Github上。 - 一块由GD32F303主控的硬件板子,并包含对应的输入输出控件。
四、移植步骤
一、LVGL源码并入KEIL工程
1. 解压文档
2. 解压后的文件夹里重点关注红框选中的部分,examples文件夹是lvgl库对外的接口文件、src文件夹是库本身的核心源码、lv_conf_template.h是对lvgl库的一些配置而lvgl.h是对版本的说明。
3. 在工程目录中新建GUI文件夹并分好类别
4. 分好类别后,将源码中src文件夹和最外头的lvgl.h文件无增减的复制到工程中GUI->core_library文件夹
5. 将源码中examples->porting文件夹无增减的复制到工程中GUI->deivers文件夹
- lv_port_display_template.c代表我们的显示接口
- lv_port_fs_template.c代表文件系统接口
- lv_port_indev_template.c代表输入设备接口
- 以上文件有用到的需将文件名中的template去掉,如下:
6. 最后将lvgl-release-v7 -> lv_conf_template.h文件复制到 GUI 文件夹下
- 将lv_conf_template.h文件名中的template去掉,并改名为lv_conf.h。它是对整个LVGL库的宏管理,使用LVGL库必需用到它。
7. GUI其它子文件夹介绍
- GUI->board_drv文件夹用于放置每个界面.c文件和所有界面下的回调函数.c文件
- GUI->examples文件夹用于放置官方给出的参考例子.c文件
- GUI->images文件夹用于存放界面上用到的图片对应的.c文件
- GUI->my_fonts文件夹用于存放自己添加的字体库
- 这部分文件夹是我个人为方便分类创建的,不是lvgl本身自带的
二、KEIL工程里的配置
1. 工程里的文件夹添加
2. 路径添加
3. 头文件使能
- 官方给的文件里用#if 0 #endif的方式将源码注释掉了,这里我们需要在.c和.h文件里将其改成#if 1
- 主要为以下文件:lv_conf.h、lv_port_indev.h、lv_port_indev.c、lv_port_disp.h、lv_port_disp.c、
- 用了文件系统的也要修改对应的文件
4. 接口配置
- 这里主要配置两个文件lv_port_display.c与lv_port_indev.c(lv_port_fs.c与之类似)
- 先讲下lv_port_display.c
- 需要做到如下几件事:注册显示设备的BUF缓冲区、初始化你的硬件设备、对应你定义的显示设备缓冲区的刷新函数
1. 初始化你的硬件设备
- 在lv_port_display.c中disp_init()函数中添加你的屏初始化函数即可,如下
/* Initialize your display and the required peripherals. */
static void disp_init(void)
{
/*You code here*/
LCD_Init();
}
2. 对应你定义的显示设备缓冲区的刷新函数
- 在lv_port_display.c中disp_flush()函数中添加你的刷新函数
- disp_flush()会传入要刷新的设备,区域大小以及处理好后的屏上的数据
- 刷新函数:当要发送的数据处理完成后,通过这个函数将屏显示的内容更新
- lv_disp_flush_ready()函数很重要,这里我使用的是硬件SPI,在刷屏结束后直接调用这个即可。如果你使用的是硬件SPI+DMA的方式,那么要每次DMA传输完毕后再调用此函数
/* 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)
{
/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
LCD_Fill_Pointer(area->x1,area->y1,area->x2,area->y2,(uint16_t*)color_p,1);
/* IMPORTANT!!!
* Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
3. 注册显示设备的BUF缓冲区
- 在lv_port_display.c中lv_port_disp_init()函数中注册
- lv_disp_t * disp_src 为我们注册的显示设备对象
- lvgl为我们提供了3种缓存区的方式,这里我们直接选第一种最简单的即可
- 关于下面的LV_HOR_RES_MAX和LV_VER_RES_MAX这两个宏在lv_conf.h中有定义,这里可以先忽略
lv_disp_t * disp_src = NULL;
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 your display drivers `flush_cb` to copy its content to your display.
* The buffer has to be greater than 1 display row
*
* There are three buffering configurations:
* 1. Create ONE buffer with some rows:
* LVGL will draw the display's content here and writes it to your display
*
* 2. Create TWO buffer with some rows:
* 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. Create TWO screen-sized buffer:
* Similar to 2) but the buffer have to be screen sized. When LVGL 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.
* */
/* Example for 1) */
static lv_disp_buf_t draw_buf_dsc_1;
static lv_color_t draw_buf_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/
lv_disp_buf_init(&draw_buf_dsc_1, draw_buf_1, NULL, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*/
/* Example for 2) */
// static lv_disp_buf_t draw_buf_dsc_2;
// static lv_color_t draw_buf_2_1[LV_HOR_RES_MAX * 10]; /*A buffer for 10 rows*/
// static lv_color_t draw_buf_2_2[LV_HOR_RES_MAX * 10]; /*An other buffer for 10 rows*/
// lv_disp_buf_init(&draw_buf_dsc_2, draw_buf_2_1, draw_buf_2_2, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*/
// /* Example for 3) */
// static lv_disp_buf_t draw_buf_dsc_3;
// static lv_color_t draw_buf_3_1[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*A screen sized buffer*/
// static lv_color_t draw_buf_3_2[LV_HOR_RES_MAX * LV_VER_RES_MAX]; /*An other screen sized buffer*/
// lv_disp_buf_init(&draw_buf_dsc_3, draw_buf_3_1, draw_buf_3_2, LV_HOR_RES_MAX * LV_VER_RES_MAX); /*Initialize the display buffer*/
/*-----------------------------------
* Register the display in LVGL
*----------------------------------*/
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;//LV_HOR_RES_MAX
disp_drv.ver_res = LV_VER_RES_MAX;//LV_VER_RES_MAX
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = disp_flush;
/*Set a display buffer*/
disp_drv.buffer = &draw_buf_dsc_1;
#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*/
disp_src = lv_disp_drv_register(&disp_drv);
/* 设置默认的显示器对象为屏幕 */
lv_disp_set_default(disp_src);
}
4. 硬件上的输入设备
-
输入设备这部分需要做到两件事:输入设备类型的确定、输入设备的关联、输入设备的初始化
-
我使用的是物理按键,每个都是独立的按键,原理图如下:
5. 输入设备的确定 -
lvgl提供touchpad、mouse、keypad、encoder、button这5种输入类型
-
根据我们的硬件设计,我们既可以把它设置成keypad也可以设置成button类型
-
我们用keypad的方式注册
-
在keypad_init()函数中可以初始化对应的IO
void GPIO_Init(void)
{
/* 使用SWD下载,不使用JTAG下载,管脚用作其它功能 */
gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE);
/* demo board KEYn I/O */
gpio_init(KEY_UP_PORT,GPIO_MODE_IPU,GPIO_OSPEED_50MHZ,KEY_UP_PIN);
gpio_init(KEY_DOWN_PORT,GPIO_MODE_IPU,GPIO_OSPEED_50MHZ,KEY_DOWN_PIN);
gpio_init(KEY_LEFT_PORT,GPIO_MODE_IPU,GPIO_OSPEED_50MHZ,KEY_LEFT_PIN);
gpio_init(KEY_ENTER_PORT,GPIO_MODE_IPU,GPIO_OSPEED_50MHZ,KEY_ENTER_PIN);
gpio_init(KEY_RIGHT_PORT,GPIO_MODE_IPU,GPIO_OSPEED_50MHZ,KEY_RIGHT_PIN);
}
/* Initialize your keypad */
static void keypad_init(void)
{
/*Your code comes here*/
GPIO_Init();
}
- keypad_get_key()函数则是关联我们的硬件函数
- 读键值这块很方便,不需要我们对键值消抖,lvgl自带消抖处理,具体可见lv_conf.h中LV_INDEV_DEF_READ_PERIOD这个宏
uint32_t Key_Flag_Event(void)
{
if(!gpio_input_bit_get(KEY_UP_PORT,KEY_UP_PIN))
return 1;
if(!gpio_input_bit_get(KEY_DOWN_PORT,KEY_DOWN_PIN))
return 2;
if(!gpio_input_bit_get(KEY_LEFT_PORT,KEY_LEFT_PIN))
return 3;
if(!gpio_input_bit_get(KEY_RIGHT_PORT,KEY_RIGHT_PIN))
return 4;
if(!gpio_input_bit_get(KEY_ENTER_PORT,KEY_ENTER_PIN))
return 5;
return 0;
}
/*Get the currently being pressed key. 0 if no key is pressed*/
static uint32_t keypad_get_key(void)
{
/*Your code comes here*/
return Key_Flag_Event();
// return 0;
}
- 得到键值后,我们会把键值与lvgl的按键事件关联起来
/* Will be called by the library to read the mouse */
static bool keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static uint32_t last_key = 0;
/*Get the current x and y coordinates*/
mouse_get_xy(&data->point.x, &data->point.y);
/*Get whether the a key is pressed and save the pressed key*/
uint32_t act_key = keypad_get_key();
if(act_key != 0) {
data->state = LV_INDEV_STATE_PR;
/*Translate the keys to LVGL control characters according to your key definitions*/
switch(act_key) {
case 1:
act_key = LV_KEY_UP_PART;
break;
case 2:
act_key = LV_KEY_DOWN_PART;
break;
case 3:
act_key = LV_KEY_LEFT_PART;
break;
case 4:
act_key = LV_KEY_RIGHT_PART;
break;
case 5:
act_key = LV_KEY_ENTER_PART;
break;
default:
break;
}
last_key = act_key;
} else {
data->state = LV_INDEV_STATE_REL;
}
data->key = last_key;
/*Return `false` because we are not buffering and no more data to read*/
return false;
}
- LV_KEY_UP_PART、LV_KEY_DOWN_PART、LV_KEY_LEFT_PART这些是我自定义的,你可以使用lvgl自带的事件,事件的定义在lv_group.h中
/*Predefined keys to control the focused object via lv_group_send(group, c)*/
/*For compatibility in signal function define the keys regardless to `LV_USE_GROUP`*/
enum {
LV_KEY_UP = 17, /*0x11*/
LV_KEY_DOWN = 18, /*0x12*/
LV_KEY_RIGHT = 19, /*0x13*/
LV_KEY_LEFT = 20, /*0x14*/
LV_KEY_ESC = 27, /*0x1B*/
LV_KEY_DEL = 127, /*0x7F*/
LV_KEY_BACKSPACE = 8, /*0x08*/
LV_KEY_ENTER = 10, /*0x0A, '\n'*/
LV_KEY_NEXT = 9, /*0x09, '\t'*/
LV_KEY_PREV = 11, /*0x0B, '*/
LV_KEY_HOME = 2, /*0x02, STX*/
LV_KEY_END = 3, /*0x03, ETX*/
/* 自定义键值 */
LV_KEY_UP_PART = 145, /* 0x91 */
LV_KEY_DOWN_PART = 146, /* 0x92 */
LV_KEY_LEFT_PART = 148, /* 0x94 */
LV_KEY_RIGHT_PART = 147, /* 0x93 */
LV_KEY_ENTER_PART = 130, /* 0x82 */
};
typedef uint8_t lv_key_t;
6. 输入设备的注册
- 到这一步,只需将lv_port_indev_init()函数里其它输入类型注释掉即可
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
*/
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);
/*------------------
* Mouse
* -----------------*/
// /*Initialize your touchpad 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_disp_get_scr_act(NULL), NULL);
// 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);
}
5. LVGL的配置头文件
- lvgl的配置功能主要在lv_conf.h里,这里主要提几个重要的宏
1. LV_HOR_RES_MAX与LV_VER_RES_MAX
- 在注册显示设备时,我们有用到过这两个宏,它的主要目的是定义我们显示设备的大小,如我用的是320x240
/* Maximal horizontal and vertical resolution to support by the library.*/
#define LV_HOR_RES_MAX (320)
#define LV_VER_RES_MAX (240)
2. LV_COLOR_DEPTH
- 这个宏则是定义我们每个像素点的颜色深度,这取决与我们的显示设备支持哪几种颜色深度以及我们传输的数据大小
- 如我的屏支持65k彩色(RGB565)和262k彩色(RGB666),前者只需要2个字节就能传输完成1个像素点的颜色深度,而后者至少需要3个字节(多数是4字节,ARGB8888的方式)
- 那么320x240x2就是我总共发的字节数,相当于完全刷一张屏每次要发150kB的数据,若使用ARGB的方式直接就翻了个倍
- 当然像lvgl这样的图形库肯定会做优化,不可能界面上有一点改动就刷整个屏的内容
/* Color depth:
* - 1: 1 byte per pixel
* - 8: RGB332
* - 16: RGB565
* - 32: ARGB8888
*/
#define LV_COLOR_DEPTH 16
3. LV_MEM_SIZE
- 这个也是我们lvgl能否跑成功的关键,由于lvgl自带内存管理,我们要给其设置一个大的数组,我设置成20k
/* Size of the memory used by `lv_mem_alloc` in bytes (>= 2kB)*/
# define LV_MEM_SIZE (20U * 1024U)
4. LV_FONT_MONTSERRAT_nn
- 这是对字体的设置,lvgl本身提供一部分字体,可以根据实际需要添加要用的字体
- 不建议全部使能,因为字体本身很占用我们的硬件资源,这里只加入我们需要的即可
/* The built-in fonts contains the ASCII range and some Symbols with 4 bit-per-pixel.
* The symbols are available via `LV_SYMBOL_...` defines
* More info about fonts: https://docs.lvgl.io/v7/en/html/overview/font.html
* To create a new font go to: https://lvgl.com/ttf-font-to-c-array
*/
/* Montserrat fonts with bpp = 4
* https://fonts.google.com/specimen/Montserrat */
#define LV_FONT_MONTSERRAT_8 0
#define LV_FONT_MONTSERRAT_10 0
#define LV_FONT_MONTSERRAT_12 1
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 0
#define LV_FONT_MONTSERRAT_18 1
#define LV_FONT_MONTSERRAT_20 0
#define LV_FONT_MONTSERRAT_22 0
#define LV_FONT_MONTSERRAT_24 1
#define LV_FONT_MONTSERRAT_26 0
#define LV_FONT_MONTSERRAT_28 0
#define LV_FONT_MONTSERRAT_30 0
#define LV_FONT_MONTSERRAT_32 0
#define LV_FONT_MONTSERRAT_34 0
#define LV_FONT_MONTSERRAT_36 0
#define LV_FONT_MONTSERRAT_38 0
#define LV_FONT_MONTSERRAT_40 0
#define LV_FONT_MONTSERRAT_42 0
#define LV_FONT_MONTSERRAT_44 0
#define LV_FONT_MONTSERRAT_46 0
#define LV_FONT_MONTSERRAT_48 0
/* Demonstrate special features */
#define LV_FONT_MONTSERRAT_12_SUBPX 0
#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /*bpp = 3*/
#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, PErisan letters and all their forms*/
#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/
5. LV_THEME_DEFAULT_xx
- lvgl也预留了默认设置功能,相当于在我们没设置屏背景主题色、组件的颜色、字体等等时,默认是什么
/*================
* THEME USAGE
*================*/
/*Always enable at least on theme*/
/* No theme, you can apply your styles as you need
* No flags. Set LV_THEME_DEFAULT_FLAG 0 */
#define LV_USE_THEME_EMPTY 1
/*Simple to the create your theme based on it
* No flags. Set LV_THEME_DEFAULT_FLAG 0 */
#define LV_USE_THEME_TEMPLATE 1
/* A fast and impressive theme.
* Flags:
* LV_THEME_MATERIAL_FLAG_LIGHT: light theme
* LV_THEME_MATERIAL_FLAG_DARK: dark theme
* LV_THEME_MATERIAL_FLAG_NO_TRANSITION: disable transitions (state change animations)
* LV_THEME_MATERIAL_FLAG_NO_FOCUS: disable indication of focused state)
* */
#define LV_USE_THEME_MATERIAL 1
/* Mono-color theme for monochrome displays.
* If LV_THEME_DEFAULT_COLOR_PRIMARY is LV_COLOR_BLACK the
* texts and borders will be black and the background will be
* white. Else the colors are inverted.
* No flags. Set LV_THEME_DEFAULT_FLAG 0 */
#define LV_USE_THEME_MONO 1
#define LV_THEME_DEFAULT_INCLUDE <stdint.h> /*Include a header for the init. function*/
#define LV_THEME_DEFAULT_INIT lv_theme_material_init
#define LV_THEME_DEFAULT_COLOR_PRIMARY lv_color_hex(0xff7e05) // lv_color_hex(0x01a2b1)
#define LV_THEME_DEFAULT_COLOR_SECONDARY lv_color_hex(0xff7e05) // lv_color_hex(0x44d1b6)
#define LV_THEME_DEFAULT_FLAG LV_THEME_MATERIAL_FLAG_LIGHT //LV_THEME_MATERIAL_FLAG_DARK LV_THEME_MATERIAL_FLAG_LIGHT
#define LV_THEME_DEFAULT_FONT_SMALL &lv_font_montserrat_14
#define LV_THEME_DEFAULT_FONT_NORMAL &lv_font_montserrat_14
#define LV_THEME_DEFAULT_FONT_SUBTITLE &lv_font_montserrat_14
#define LV_THEME_DEFAULT_FONT_TITLE &lv_font_montserrat_14
6. 其它
- 其它部分也类似,包括是否使能动画功能、使能某些控件实现一定程度上的功能裁剪等等
五、FreeRTOS中的调试与应用
- 由于我们使用了FreeRTOS,我们可以把lvgl作为一个任务添加进去
- 使用lvgl主要为以下5个函数:lv_tick_inc()、lv_task_handler()、lv_init()、lv_port_disp_init()、lv_port_indev_init()
1. lv_tick_inc()
- 与FreeRTOS类似,lvgl也需要一个tick心跳来执行自己内部相应的任务,我们将这个函数放到FreeRTOS中的钩子函数中即可
#define configUSE_TICK_HOOK 1
void vApplicationTickHook(void)
{
lv_tick_inc(1);
}
2. lv_task_handler()
- lvgl所有更新内容的处理都在这个函数里,V7版本及以前叫lv_task_handler(),V8版本后变成了lv_timer_handler(),内部的逻辑也作了改动
- 在FreeRTOS中,我们把其作为一个单独的任务即可,强烈建议对这个任务的栈大小设置不低于512
- 由于lvgl线程不安全,即函数不可重入,我们给这个任务加个锁,lvgl的任务优先级不必调至最高
#define LCD_TASK_PRIO ( tskIDLE_PRIORITY + 1 )
void lcd_refresh_task(void * pvParameters)
{
const TickType_t xDelay10ms = pdMS_TO_TICKS( 10UL );
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
if(pdTRUE == xSemaphoreTake(xMutex,portMAX_DELAY))
{
lv_task_handler();
xSemaphoreGive(xMutex);
}
xTaskDelayUntil(&xLastWakeTime,xDelay10ms);
}
}
3. lv_init()、lv_port_disp_init()、lv_port_indev_init()
- lv_init()是根据我们在lv_conf.h中提到的各种宏配置后的初始化函数
- lv_port_disp_init()与lv_port_indev_init()是上面我们提到的显示、和按键输入接口
- 这些放在我们的main.c中即可
int main()
{
/* 变量初始化(一些设定值的初始化) */
Variables_Init();
SystemTick_Init();
SystemClock_Reconfig();
SetUpHareWare();
lv_init();
lv_port_disp_init();
lv_port_indev_init();
// lv_log_register_print_cb((lv_log_print_g_cb_t )put_string);
//下面的代码省略
}
4. lvgl_bootScreen()
- 做完上面的步骤,接下来就可以创建LVGL的控件了
- lvgl的对象都是lv_obj_t * 类型,无论是整个界面还是每个界面里的各个控件,我们也可以随意关联,在代码中通常以parent和child决定它们之间的关系
- 定义一个包含我们所有界面的结构体,包含以下3个界面
typedef struct
{
lv_obj_t * BootScreen; /* 启动界面 */
lv_obj_t * MainScreen; /* 主界面 */
lv_obj_t * SetScreen; /* 设置界面 */
}lv_ui;
/* 所有界面的对象结构体 */
lv_ui Gui={.BootScreen=NULL};
- 写一个函数专门用于绘制第一个界面
lv_obj_t * time1_label = NULL;
lv_obj_t * btn_label = NULL;
lv_obj_t * time2_label = NULL;
lv_group_t * group_boot = NULL;
void lv_obj_set_style_local_set(lv_obj_t * obj,uint8_t part,lv_state_t state,lv_color_t color,lv_style_int_t value)
{
lv_obj_set_style_local_radius(obj,part,state,8);
lv_obj_set_style_local_border_color(obj,part,state,color);
lv_obj_set_style_local_border_width(obj,part,state,value);
lv_obj_set_style_local_outline_color(obj,part,state,lv_color_make(0xd4, 0xd7, 0xd9));
lv_obj_set_style_local_outline_width(obj,part,state,0);
}
void lvgl_bootScreen(lv_ui * ui)
{
lv_obj_t * btn_left = NULL;
lv_obj_t * btn_mid = NULL;
lv_obj_t * btn_right = NULL;
lv_obj_t * boot_bar = NULL;
if( (lv_scr_act()==ui->BootScreen) && (ui->BootScreen != NULL) )
lv_obj_clean(ui->BootScreen);
else
ui->BootScreen = lv_obj_create(NULL, NULL);
lv_obj_set_size(ui->BootScreen, LV_HOR_RES, LV_VER_RES);
lv_scr_load(ui->BootScreen);
time1_label = lv_label_create(ui->BootScreen,NULL);
lv_label_set_text_fmt(time1_label,"timer1:%5d",ScheduleExe_Task_Count); //
lv_obj_set_style_local_text_font(time1_label, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_18);
lv_obj_set_style_local_text_color(time1_label, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT,LV_COLOR_PURPLE);
lv_obj_set_pos(time1_label, 2, 0);
time2_label = lv_label_create(ui->BootScreen,NULL);
lv_label_set_text_fmt(time2_label,"timer2:%5d",65535-ScheduleExe_Task_Count); //
lv_obj_set_style_local_text_font(time2_label, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_18);
lv_obj_set_style_local_text_color(time2_label, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT,LV_COLOR_PURPLE);
lv_obj_set_pos(time2_label, 200, 0);
btn_label = lv_label_create(ui->BootScreen,NULL);
lv_label_set_text(btn_label,"KeyValue:0");
lv_obj_set_pos(btn_label,120,60);
/*****************************************************************/
btn_left = lv_btn_create(ui->BootScreen,NULL);
lv_obj_set_style_local_set(btn_left,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,LV_COLOR_ORANGE,1);
lv_obj_set_style_local_set(btn_left,LV_OBJ_PART_MAIN,LV_STATE_FOCUSED,LV_COLOR_ORANGE,3);
//设置位置
lv_obj_set_pos(btn_left, 15, 135);
lv_obj_set_size(btn_left, 90, 40);
lv_obj_t * btn_left_label = lv_label_create(btn_left,NULL);
lv_label_set_text(btn_left_label,"btn_left");
/*****************************************************************/
/*****************************************************************/
btn_mid = lv_btn_create(ui->BootScreen,NULL);
lv_obj_set_style_local_set(btn_mid,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,LV_COLOR_RED,1);
lv_obj_set_style_local_set(btn_mid,LV_OBJ_PART_MAIN,LV_STATE_FOCUSED,LV_COLOR_RED,3);
//设置位置
lv_obj_set_pos(btn_mid, 115, 135);
lv_obj_set_size(btn_mid, 90, 40);
lv_obj_t * btn_mid_label = lv_label_create(btn_mid,NULL);
lv_label_set_text(btn_mid_label,"btn_mid");
/*****************************************************************/
/*****************************************************************/
btn_right = lv_btn_create(ui->BootScreen,NULL);
lv_obj_set_style_local_set(btn_right,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,LV_COLOR_LIME,1);
lv_obj_set_style_local_set(btn_right,LV_OBJ_PART_MAIN,LV_STATE_FOCUSED,LV_COLOR_LIME,3);
//设置位置
lv_obj_set_pos(btn_right, 215, 135);
lv_obj_set_size(btn_right, 90, 40);
lv_obj_t * btn_right_label = lv_label_create(btn_right,NULL);
lv_label_set_text(btn_right_label,"btn_right");
/*****************************************************************/
/* 子级 boot_scr下的boot_bar */
boot_bar = lv_bar_create(ui->BootScreen, NULL);
/* boot_bar的半径、不透明度与颜色等 */
lv_obj_set_style_local_radius(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,30);
lv_obj_set_style_local_bg_color(boot_bar,LV_BAR_PART_BG,LV_STATE_DEFAULT,LV_COLOR_GRAY);
lv_obj_set_style_local_bg_color(boot_bar,LV_BAR_PART_INDIC,LV_STATE_DEFAULT,lv_color_make(0x57,0x7D,0xF2));
// lv_obj_set_style_local_bg_grad_color(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,LV_COLOR_GRAY);
lv_obj_set_style_local_bg_opa(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,255);
lv_obj_set_style_local_bg_grad_dir(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,LV_GRAD_DIR_HOR);
lv_obj_set_style_local_border_color(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,LV_COLOR_GRAY);
lv_obj_set_style_local_border_width(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,1);
lv_obj_set_style_local_border_opa(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,255);
lv_obj_set_style_local_outline_width(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,1);
lv_obj_set_style_local_outline_color(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,LV_COLOR_GRAY);
lv_obj_set_style_local_outline_opa(boot_bar,LV_OBJ_PART_MAIN,LV_STATE_DEFAULT,0);
/* boot_bar的位置 */
lv_obj_set_pos(boot_bar, 38, 222);
lv_obj_set_size(boot_bar, 245, 7);
/* boot_bar的动画 */
lv_bar_set_anim_time(boot_bar,1200);
lv_bar_set_value(boot_bar,100,LV_ANIM_ON);
lv_bar_set_range(boot_bar,0,100);
/*****************************************************************/
/*****************************************************************/
/*****************************************************************/
lv_obj_set_event_cb(btn_left, lv_task_event_cb[MENU_BOOT]);
lv_obj_set_event_cb(btn_mid, lv_task_event_cb[MENU_BOOT]);
lv_obj_set_event_cb(btn_right,lv_task_event_cb[MENU_BOOT]);
group_boot = lv_group_create();
lv_group_add_obj(group_boot, btn_left);
lv_group_add_obj(group_boot, btn_mid);
lv_group_add_obj(group_boot, btn_right);
lv_indev_set_group(indev_keypad, group_boot);
lv_group_focus_obj(btn_left);
}
- 对第一个界面下的回调函数
static void Boot_screen_btn_enterevent_handler(lv_obj_t * obj,lv_event_t event);
/* 回调函数指针数组*/
void (*lv_task_event_cb[])(lv_obj_t * obj, lv_event_t e) = \
{ \
Boot_screen_btn_enterevent_handler,Menu_total_btnm_event_handler,\
};
/* 开机动画回调 */
static void Boot_screen_btn_enterevent_handler(lv_obj_t * obj,lv_event_t event)
{
const uint32_t * BootKeyValue = lv_event_get_data();
if( *BootKeyValue==LV_KEY_LEFT_PART )
{
lv_group_focus_prev(group_boot);
}
else if( *BootKeyValue==LV_KEY_RIGHT_PART )
{
lv_group_focus_next(group_boot);
}
lv_label_set_text_fmt(btn_label,"KeyValue:%d",(uint8_t)(*BootKeyValue));
// lv_label_set_text_fmt(btn_label,"state:%d",(uint8_t)(lv_obj_get_state(lv_obj_get_focused_obj(obj), LV_OBJ_PART_MAIN)));
}
5. FreeRTOS中对任务的创建
- FreeRTOS中我们创建两个任务,一个是用于处理lv_task_handler(),另一个用于处理不通过lvgl回调函数刷新的内容
- 用于处理lv_task_handler()的任务
void lcd_refresh_task(void * pvParameters)
{
const TickType_t xDelay10ms = pdMS_TO_TICKS( 10UL );
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
if(pdTRUE == xSemaphoreTake(xMutex,portMAX_DELAY))
{
lv_task_handler();
xSemaphoreGive(xMutex);
}
xTaskDelayUntil(&xLastWakeTime,xDelay10ms);
}
}
- 用于处理自己更新部分组件的任务
void lvgl_refresh_task(void * pvParameters)
{
const TickType_t xDelay1000ms = pdMS_TO_TICKS( 1000UL );
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
if(pdTRUE == xSemaphoreTake(xMutex_LVGL,portMAX_DELAY))
{
if(lv_scr_act()==Gui.BootScreen)
{
lv_label_set_text_fmt(time1_label,"timer1:%5d",ScheduleExe_Task_Count);
lv_label_set_text_fmt(time2_label,"timer2:%5d",65535-ScheduleExe_Task_Count);
}
else if(lv_scr_act()==Gui.MainScreen)
{
}
else if(lv_scr_act()==Gui.SetScreen)
{
}
xSemaphoreGive(xMutex_LVGL);
}
xTaskDelayUntil(&xLastWakeTime,xDelay1000ms);
}
}
- 其它FreeRTOS任务,主要包括一个背光任务和一个定时发送数据与喂狗的任务
- 背光任务
void LCD_BG_task(void * pvParameters)
{
const TickType_t xTicksToWait = pdMS_TO_TICKS( 1UL );
for(;;)
{
if(pdTRUE == lcd_backlight_breathe())
vTaskDelete(LCDBGTask_Handle);
vTaskDelay(xTicksToWait);
}
}
- 定时发送与喂狗任务
void Scheduled_Execute_task(void * pvParameters)
{
const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
TickType_t xLastWakeTime;
BaseType_t xStatus;
/* 获得当前的Tick Count */
xLastWakeTime = xTaskGetTickCount();
while(1)
{
if(SET == dma_flag_get(DMA0, DMA_CH6, DMA_FLAG_FTF))
{
dma_flag_clear(DMA0,DMA_CH6,DMA_FLAG_FTF);
Usartx_Transmit_DMA(USART1,(uint8_t *)"FreeRTOS_1S_Task!\n",18);
ScheduleExe_Task_Count++;
}
/* uncock fwdgt write protect*/
fwdgt_write_enable();
/* feed fwdgt */
fwdgt_counter_reload();
vTaskDelay(1000);
}
}
6. 主函数部分的完整代码
- 介绍完上面的4个任务,主函数部分整体代码逻辑如下
#include "gd32f30x.h"
#include "gd32f30x_libopt.h"
#include "main.h"
FLAG_BIT Module;
ADCx_enum ADCx;
uint16_t ScheduleExe_Task_Count=0;
extern uint32_t delay;
void Variables_Init(void);
void put_string(lv_log_level_t level, const char *file, uint32_t line, const char *dsc);
#define LCDBG_TASK_PRIO ( tskIDLE_PRIORITY + 3 )
#define ScheduleExe_TASK_PRIO ( tskIDLE_PRIORITY + 1 )
#define LCD_TASK_PRIO ( tskIDLE_PRIORITY + 1 )
#define LVGL_TASK_PRIO ( tskIDLE_PRIORITY + 1 )
TaskHandle_t LCDBGTask_Handle;
void LCD_BG_task(void * pvParameters);
void Scheduled_Execute_task(void * pvParameters);
void lcd_refresh_task(void * pvParameters);
void lvgl_refresh_task(void * pvParameters);
/* 互斥量句柄 */
SemaphoreHandle_t xMutex;
SemaphoreHandle_t xMutex_LVGL;
int main()
{
/* 变量初始化(一些设定值的初始化) */
Variables_Init();
SystemTick_Init();
SystemClock_Reconfig();
SetUpHareWare();
lv_init();
lv_port_disp_init();
lv_port_indev_init();
// lv_log_register_print_cb((lv_log_print_g_cb_t )put_string);
lvgl_bootScreen(&Gui);
Usartx_Transmit_DMA(USART1,(uint8_t *)"FreeRTOS_1S_Task!\n",18);
/* 创建互斥量 */
xMutex = xSemaphoreCreateMutex( );
/* 创建互斥量 */
xMutex_LVGL = xSemaphoreCreateMutex( );
xTaskCreate(LCD_BG_task, "LCD_BG_Task", configMINIMAL_STACK_SIZE, NULL,
LCDBG_TASK_PRIO, (TaskHandle_t* )&LCDBGTask_Handle);
xTaskCreate(Scheduled_Execute_task, "Scheduled_Execute_Task", configMINIMAL_STACK_SIZE, NULL,
ScheduleExe_TASK_PRIO, NULL);
xTaskCreate(lcd_refresh_task, "LCD_Task", configMINIMAL_STACK_SIZE*4, NULL,
LCD_TASK_PRIO, NULL);
xTaskCreate(lvgl_refresh_task, "Lvgl_Task", configMINIMAL_STACK_SIZE*2, NULL,
LVGL_TASK_PRIO, NULL);
vTaskStartScheduler();
while(1){}
}
void put_string(lv_log_level_t level, const char *file, uint32_t line, const char *dsc)
{
static char buf[150];
sprintf(buf,"level:%d,file:%s,line:%d,dsc:%s\r",level,file,line,dsc);
// Usartx_Transmit_DMA(USART1,buf,sizeof(buf));
}
六、最终效果演示
-
最终效果如下
视频 -
这里也提供本文的工程源码,不免费提供,有需要的可以下载