万恶之源
俗话说的好:羊毛出在羊身上。
笔者在这里也想说一句:移植出在文档上。
既然库作者编写了开源库,并且希望该开源库被大家广泛地采用,就一定会:
- 将库接口写得简单
- 为移植创作一份完善的移植文档
因此,在一直开源库的时候最右参考意义的应该是该库的文档。
我们先去LVGL的仓库看看:
值得一提的是lvgl的仓库分支结构:
Master分支用于存放当前的测试内容(即Beta版本代码)
而所有的稳定版本代码都会以release/版本号的方式建立一个新的分支来发布
我们来到v8版本的release分支,查看README文档,可以发现作者给出了建议的学习路线:
我们按照官方推荐的方式来学习库的使用。
一些无关紧要的操作
其实也不是无关紧要
首先进入官方的在线Demo来把玩一下用lvgl写出来的UI,感受一下那份流畅与跃动的感觉在你的鼠标之间旋转跳跃,再幻想一下你的嵌入式设备的屏幕上出现了一模一样的UI,你的内心瞬间被成就感填满......
好吧,其实这一步没什么用。
把玩过之后点进Introduction中了解一下lvgl的关键特点、硬件需求、许可证以及仓库的结构等等。
好吧,其实这一步也没什么用。
一顿操作之后,直到完成第五步,你都没有让lvgl与你的开发平台产生任何的联系。唯一的作用就是:
你对lvgl更加了解了
没错,这就是库作者希望的结果,当然也是笔者希望的结果。
其实笔者真正希望的结果是读者自行看懂PortGuide,然后直接快进到文档结束
库文件的移植
一些说明:
笔者在之前的工程中使用的lvgl版本为v7,为了能让自己更实际的体会到库文件的移植过程,特选择v8.0版本进行移植。
移植并使用lvgl的前提:
读者已经拥有一个能够使用的彩屏/单色屏的屏幕驱动,并且至少可以实现在屏幕的固定座标上进行画点操作。
整个移植过程均参考官方的PortingGuide(下称移植手册),如有疑问请参照移植手册原文,仍有问题请与库作者沟通。
(笔者不背锅)
新建工程
如题,使用的开发环境为:
STM32CubeMX+Clion
MCU型号为:
STM32H750VBT6
工程创建与屏幕驱动移植过程略。
本体移植
首先将release/v8.0的仓库Clone到本地。
按照移植手册的说法:
The graphics library is the lvgl directory which should be copied into your project.
得知应该将lvgl文件夹拷贝到工程目录下,但是在克隆来的分支中并没有名为lvgl的文件夹,于是笔者移植失败,并骂一句什么**垃圾作者。
事实上名为src的文件夹很明显就是源代码目录,故在使用的工程中新建lvgl目录用于存放库文件,并将src目录直接复制到lvgl目录下。
继续耐着性子读下去,发现工程中应该有一个配置文件名为:lv_conf_template.h
并且有如下的说明:
Copy lvgl/lv_conf_template.h next to the lvgl directory and rename it to lv_conf.h.
故将lv_conf_template.h
复制到lvgl目录中并更名为lv_conf.h
。
顺便将#if 0
改为#if 1
来使能文件的内容。
lvgl的每个接口库文件和配置库文件都有
#if 0
开关,使用时需要注意。
接着将lvgl.h复制到lvgl目录下,该头文件包含了所有相关的lvgl所需要的头文件,因此写程序时只需要包含lvgl.h即可。
接口移植
接口的移植主要分为两部分:
屏幕显示接口和输入设备接口。
其中屏幕显示是不可忽略的部分,而输入设备则可以根据自己需求来更改不同的设备。
接口文件都在Clone下来的目录下的example中的porting目录下:
分别为屏幕显示接口、文件系统接口和输入设备接口。
屏幕显示接口
将lv_port_disp_template.c
和lv_port_disp_template.h
复制到lvgl目录下,与src文件夹同级,然后将两个文件名字中的template删除,并打开#if 0
开关。
顺便将接口源文件中的头文件包含改为正确的名字:
#include "lv_port_disp.h"
接着将该两个文件添加到工程当中(以Cmake为例):
仅需更改模板文件中的两个语句后重新生成Cmake即可。
include_directories(${includes}
ST7735/Inc#此处为ST7735屏幕驱动所需的包含路径
lvgl/src/core
lvgl/src/draw
lvgl/src/extra
lvgl/src/font
lvgl/src/gpu
lvgl/src/hal
lvgl/src/misc
lvgl/src/widgets
lvgl
)
file(GLOB_RECURSE SOURCES ${sources} "ST7735/*.*" "lvgl/*.*")
将文件纳入工程后,很明显接口头文件开头的包含路径与我们不一致,故将包含路径稍作修改:#include "lvgl.h"
修改后文件的移植工作就完成了,接下来我们对代码进行一些修改。
配置文件修改
首先打开lv_conf.h
:
按照自己的硬件条件对下面的内容进行必要的修改。
-
修改屏幕颜色深度宏:
LV_COLOR_DEPTH
-
(非必要)修改RGB565颜色高低位交换宏:
LV_COLOR_16_SWAP
-
~v7版本的库还需要更改一下屏幕分辨率~
该文件中设置项较多,因此在使用的过程中可以按照需求对宏进行修改,每条宏都有对应的注释进行解释。
显示接口文件修改
首先需要在接口头文件中定义两个与屏幕分辨率有关的宏:
/*********************
* DEFINES
*********************/
#define MY_DISP_HOR_RES 80 //屏幕高度
#define MY_DISP_VER_RES 160 //屏幕宽度
顺便将下面的代码中的分辨率设置改为我们定义的宏以方便以后的使用:
/*Set the resolution of the display*/
disp_drv.hor_res = MY_DISP_HOR_RES;
disp_drv.ver_res = MY_DISP_VER_RES;
接下来必须要修改的内容是对于缓冲区的修改:
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*/
// /* 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]; /*An other 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*/
更改完之后来修改屏幕刷新函数。
最简单的修改方式可以使用逐点刷新:
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++) {
ST7735_DrawPixel(x, y, color_p->full);
}
}
lv_disp_flush_ready(disp_drv);
}
也可采用逐帧刷新的方式:
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
int32_t width = area->x2 - area->x1;
int32_t height = area->y2 - area->y1;
ST7735_DrawImage(area->x1, area->y1, width, height, &color_p->full);
lv_disp_flush_ready(disp_drv);
}
关于屏幕刷新率值得一提的主要原因有两方面:
- 逐点刷新效率过低
- 单缓冲区刷新对MCU资源占用很严重
针对这两点有几种方式来提高刷新率:
- 采用逐帧刷新
- 增加缓冲区大小或者个数
- 采用片上GPU或者DMA2D对帧率进行优化
但是奈何工程中用片出于成本考虑往往没有片上GPU,SRAM大小也不足以负载更多的缓冲区,因此建议采用逐帧刷新的方式。
做完以上的修改之后记得将lv_port_disp_init
函数的声明添加到接口头文件中
(这个锅从v7一直延续到v8,不知是官方有意为之还是如何
让lvgl的心脏跳动起来
这部分的内容对应移植手册中的Initialization部分
要想让屏幕亮起来首先要让库里面的代码正常工作,OS中有一个属于自己的心跳,而lvgl也有属于自己的心跳:lv_tick_inc(x)
我们需要以一个大于该函数执行时间的固定时间间隔调用该函数来让lvgl的心脏跳起来,其中x是心跳的周期(单位为ms)。
最通常的做法应该是开启一个定时器中断,然后在中断回调函数里面调用该函数,不过ST的HAL库为了使用延时而默认开启了systick中断,中断周期刚好为1ms,这大好资源怎么能放着不用呢?
打开中断服务函数源文件stm32h7xx_it.c
;
添加lvgl包含:
/* USER CODE BEGIN Includes */
#include "lvgl.h"
/* USER CODE END Includes */
在systick终端服务函数中添加心跳:
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void) {
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
lv_tick_inc(1);
/* USER CODE END SysTick_IRQn 1 */
}
接下来要在循环之前调用lv_init()
进行库初始化,接着调用lv_port_disp_init()
进行显示接口初始化,并循环调用lv_task_handler()
来执行任务即可:
/* USER CODE BEGIN 2 */
ST7735_Init();
lv_init();
lv_port_disp_init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
lv_task_handler();
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_3);//循环闪灯以确定程序在正常运行
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
至此,lvgl的屏幕显示接口移植完成。
由于设备遇到了一些问题,这些问题刚好触及到了一个笔者很感兴趣的领域,故转而鸽掉教程去解决问题,当然结局问题的过程中会撰写一篇过程记录,lvgl待更内容且记录如下。
待更新内容
- lvgl输入设备接口移植
- 按键接口移植
- 编码器接口移植
- lvgl v7食用方法(Edgeline)