第二章 LVGL无操作系统移植
本章我们主要介绍LVGL的无操作系统移植(裸机),让大家了解如何将LVGL移植到DMG474电机开发板。在移植之前,先声明一点,本书仅适用于正点原子DMG474电机开发板。
本章节将分为以下几个小节:
2.1 移植准备工作
2.2 向工程添加文件
2.3 修改工程文件
2.4 移植官方例程
2.5下载验证
2.1 移植准备工作
1. 准备基础工程
在移植LVGL之前,用户必须在裸机实验中选择一个例程作为移植的基础工程,值得注意的是,最终的基础工程中必须包含LCD显示驱动以及基本定时器驱动。这里建议大家选择《TFTLCD(1.3寸)实验》作为基础工程,并准备好《基本定时器中断实验》例程,有了这些之后,我们接下来需要将基本定时器相关的驱动文件夹复制到《TFTLCD(1.3寸)实验》中,如图2.1.1所示:

图2.1.1 添加基本定时器驱动
2. 准备LVGL源码
LVGL官方的GitHub仓库(https://github.com/lvgl/lvgl/)中可以下载源码,除此之外,我们也可以在开发板的光盘资料中获取,路径: A盘 →5,软件资料 →9,LVGL学习资料 →lvgl-maser.zip。解压lvgl-maser.zip压缩包后,即可得到如下图所示的文件和文件夹:

图2.1.2 LVGL源码目录
3. 把上图中的lv_conf_template.h文件改名为lv_conf.h。
4. 打开lv_conf.h文件,修改条件编译指令。
修改前:
#if 0 /*Set it to "1" to enable content*/
修改后:
#if 1 /* 把#if 0 修改成#if 1 */
5. 精简LVGL源码
除了examples文件夹、src文件夹、lv_conf_template.h和lvgl.h文件,其他的文件和文件夹均与移植无关,我们可以将它们删除,这样即可得到LVGL的精简源码,如下图所示(保留demos文件夹):

图2.1.3 LVGL移植简洁源码
由上图可知, demos文件夹没有被删除,该文件夹中存放的是LVGL官方的演示例程,后续我们将移植其中的一些示例。
接下来我们打开上图中的examples文件夹,仅保留其中的porting文件夹,其他的文件和文件夹皆可删除,删减后如下图所示:

图2.1.4 仅保留porting文件夹
2.2 向工程添加文件
1. 新建LVGL相关文件夹
首先我们把《TFTLCD(1.3寸)实验》重命名为“LVGL例程1 无操作系统移植”,然后在该工程的Middlewares目录下新建LVGL文件夹,并在该文件夹下新建GUI文件夹和GUI_APP文件夹,最后在GUI文件夹下新建lvgl文件夹。具体的文件夹结构如下图所示:

图2.2.1 Middlewares目录下文件夹结构
这里说明一点,我们的工程之所以采用这样的文件夹结构,主要是为了兼容LVGL源码中包含头文件的格式。对于初学者,这里建议按照此结构新建文件夹,否则有可能会遇到很多关于头文件的报错。
2. 复制LVGL源码到工程中
把精简后的LVGL源码(图2.1.3中的文件和文件夹)复制到《LVGL例程1 无操作系统移植》的Middlewares/LVGL/GUI/lvgl路径下,如下图所示:

图2.2.2 移植LVGL源码
3. 添加工程分组、LVGL源文件
打开《LVGL例程1 无操作系统移植》工程,点击
图标,添加如下图所示的分组:

图2.2.3 添加工程分组
打开《LVGL例程1 无操作系统移植》工程下的src文件夹(所在路径:Middlewares/LVGL/GUI/lvgl/src),我们会发现它的子文件夹名称与上图的分组名称相似,如下图所示:

图2.2.4 src文件夹下的子文件夹
结合图2.2.3和图2.2.4,我们在对应的分组中添加文件,具体如下所示:
(1) 往Middlewares/lvgl/src/core分组中添加core文件夹下的全部.c文件,如下图所示:

图2.2.5 Middlewares/lvgl/src/core组添加的文件
(2) 往Middlewares/lvgl/src/draw组中添加draw文件夹下除nxp_pxp、nxp_vglite、sdl和stm32_dma2d文件夹之外的全部.c文件,如下图所示:

图2.2.6 Middlewares/lvgl/src/draw组添加的文件
(3)往Middlewares/lvgl/src/extra组中添加extra文件夹下除了lib文件夹之外的全部.c文件,如下图所示:

图2.2.7 Middlewares/lvgl/src/extra组添加的文件
注意:Middlewares/lvgl/src/extra/lib文件夹下的文件没有添加到该分组,这些文件是第三方解码库相关的,我们目前还不用。
(4)往 Middlewares/lvgl/src/font组中添加font文件夹下的全部.c文件,如下图所示:

图2.2.8 Middlewares/lvgl/src/font组添加的文件
(5) 往Middlewares/lvgl/src/gpu组中添加draw/stm32_dma2d和draw/sdl文件夹下的全部.c文件,如下图所示:

图2.2.9 Middlewares/lvgl/src/gpu组添加的文件
(6) 往Middlewares/lvgl/src/hal组中添加hal文件夹下的全部.c文件,如下图所示:

图2.2.10 Middlewares/lvgl/src/hal组添加的文件
(7) 往Middlewares/lvgl/src/misc组中添加misc文件夹下的全部.c文件,如下图所示:

图2.2.11 Middlewares/lvgl/src/misc组添加的文件
(8)往 Middlewares/lvgl/src/widgets组中添加widgets文件夹下的全部.c文件,如下图所示:

图2.2.12 Middlewares/lvgl/src/widgets组添加的文件
(9) Middlewares/lvgl/examples/porting组添加Middlewares/LVGL/GUI/lvgl/examples/porting目录下的lv_port_disp_template.c和lv_port_indev_template.c文件,如下图所示:

图2.2.13 Middlewares/lvgl/examples/porting组添加的文件
4. 添加头文件路径
移植LVGL只需要添加关键的头文件路径即可,因为lvgl.h文件已经为我们省去了很多包含头文件的操作,添加的头文件路径如下图所示:

图2.2.14 添加头文件路径
5. 添加定时器驱动
往Drivers/BSP分组中添加基本定时器相关驱动,如下图所示:

图2.2.15 添加基本定时器驱动
6. 修改工程目标名称
将工程的目标名修改为“LVGL”或根据读者的实际场景进行修改,修改后如下图所示:

图 2.2.16 修改工程目标名称
7. 屏蔽MDK的警告
移植至此,如果我们编译代码,则会出现很多警告,这些警告都是LVGL源码所带来的,大家如果想屏幕这些警告,可以采用以下方法(非必须,慎用):点击
图标,选中C/C++选项卡,在Misc Controls框中填入以下内容:--diag_suppress=68 --diag_suppress=111 --diag_suppress=188 --diag_suppress=223 --diag_suppress=546 --diag_suppress=1295。
2.3 修改工程文件
1. 为LVGL提供时基
打开Drivers/BSP分组中的btim.c文件,声明LVGL的头文件,如下源码所示:
#include "lvgl.h"
在该文件下的HAL_TIM_PeriodElapsedCallback函数中添加以下源码:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == (&g_timx_handle))
{
lv_tick_inc(1); /* lvgl的1ms心跳 */
}
}
上述源码中,定时器中断回调函数调用了LVGL的lv_tick_inc函数,该函数可以让LVGL内部的时基参数加1(入口参数为1的情况下)。
接下来我们打开main.c文件,在main函数中,对基本定时器进行初始化,并设置中断周期为1ms。初始化配置如下:btim_timx_int_init(10 - 1, 17000 - 1);
2. 配置显示屏、按键输入驱动
打开Middlewares/lvgl/examples/porting分组中的lv_port_disp_template.c/h(显示屏相关)和lv_port_indev_template.c/h(按键输入相关)文件,将这4个文件中的条件编译指令#if 0都修改成#if 1,如下源码所示:
修改前:
#if 0 /* lv_port_disp_template.c/h和lv_port_indev_template.c/h */
修改后:
#if 1 /* 把#if 0 修改成#if 1 */
① 修改lv_port_disp_template.c文件
该文件用于配置显示屏,它可以将用户的底层显示驱动与LVGL的显示驱动衔接起来。我们打开官方提供的lv_port_disp_template.c文件,包含LCD驱动头文件,然后初始化LCD以及配置打点函数,修改后的源码如下:
#include "lv_port_disp_template.h"
#include "../../lvgl.h"
#include "./BSP/LCD/lcd.h"
#define MY_DISP_HOR_RES (240) /* 屏幕宽度 */
#define MY_DISP_VER_RES (240) /* 屏幕高度 */
/* 显示设备初始化函数 */
static void disp_init(void);
/* 显示设备刷新函数 */
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area,
lv_color_t *color_p);
/**
* @brief 初始化并注册显示设备
* @param 无
* @retval 无
*/
void lv_port_disp_init(void)
{
/* 第一步:初始化显示设备 */
disp_init();
/* 创建一个绘图缓冲区 */
static lv_disp_draw_buf_t draw_buf_dsc_1;
/* 设置缓冲区的大小为20行屏幕的大小 */
static lv_color_t buf_1[MY_DISP_HOR_RES * 20];
/* 第二步:初始化显示缓冲区 */
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 20);
/* 第三步:在 LVGL 中注册显示设备 */
static lv_disp_drv_t disp_drv; /* 显示设备的描述符 */
lv_disp_drv_init(&disp_drv); /* 初始化为默认值 */
/* 第四步:设置显示设备的分辨率 */
disp_drv.hor_res = 240;
disp_drv.ver_res = 240;
/* 第五步:用来将缓冲区的内容复制到显示设备 */
disp_drv.flush_cb = disp_flush;
/* 第六步:设置显示缓冲区 */
disp_drv.draw_buf = &draw_buf_dsc_1;
/* 第七步:注册显示设备 */
lv_disp_drv_register(&disp_drv);
}
/**
* @brief 初始化显示设备和必要的外围设备
* @param 无
* @retval 无
*/
static void disp_init(void)
{
lcd_init(); /* 初始化LCD */
}
/**
* @brief 将内部缓冲区的内容刷新到显示屏上的特定区域
* @param disp_drv : 显示设备
* @arg area : 要刷新的区域,包含了填充矩形的对角坐标
* @arg color_p : 颜色数组
* @retval 无
*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area,
lv_color_t * color_p)
{
/* 在指定区域内填充指定颜色块 */
lcd_color_fill(area->x1, area->y1, area->x2, area->y2,(uint16_t*)color_p);
/* 通知图形库,已经刷新完毕了 */
lv_disp_flush_ready(disp_drv);
}
#else
typedef int keep_pedantic_happy;
#endif
由上述源码可知,配置LVGL显示屏驱动的步骤可分为以下7步:
(1) 调用函数disp_init初始化LCD驱动。
(2) 调用函数lv_disp_draw_buf_init初始化缓冲区(最低标准:显示屏的宽度分配率*20)。
(3) 调用函数lv_disp_drv_init初始化显示驱动。
(4) 设置显示的高度与宽度。
(5) 注册显示驱动回调(打点函数)。
(6) 设置显示驱动的绘画缓冲区。
(7) 调用lv_disp_drv_register函数注册显示驱动到LVGL列表中。
在整个配置的过程中,实际上我们只需要提供两个函数:LCD初始化函数和LCD填充函数。
② 修改lv_port_indev_template.c文件
该文件用于配置输入设备,例如:触摸屏、鼠标、键盘、编码器、按键等,它可以将用户的底层输入设备驱动与LVGL的输入驱动衔接起来。这里我们使用键盘(多个按键)作为输入设备,具体的配置源码如下所示:
#include "lv_port_indev_template.h"
#include "../../lvgl.h"
#include "./BSP/KEY/key.h"
/* 键盘 */
static void keypad_init(void);
static void keypad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data);
static uint32_t keypad_get_key(void);
lv_indev_t *indev_keypad;
/**
* @brief 初始化并注册输入设备
* @param 无
* @retval 无
*/
void lv_port_indev_init(void)
{
static lv_indev_drv_t indev_drv;
/* 第一步:初始化按键*/
keypad_init();
/* 第二步:初始化输入设备 */
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);
}
/**
* @brief 初始化键盘
* @param 无
* @retval 无
*/
static void keypad_init(void)
{
key_init();
}
uint32_t g_last_key = 0;
/**
* @brief 图形库的键盘读取回调函数
* @param indev_drv : 键盘设备
* @arg data : 输入设备数据结构体
* @retval 无
*/
static void keypad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data)
{
/* 获取按键是否被按下,并保存键值 */
uint32_t act_key = keypad_get_key();
if(act_key != 0)
{
data->state = LV_INDEV_STATE_PR;
/* 将键值转换成 LVGL 的控制字符 */
switch(act_key)
{
case KEY0_PRES:
act_key = LV_KEY_NEXT;
break;
case KEY1_PRES:
act_key = LV_KEY_PREV;
break;
case KEY2_PRES:
act_key = LV_KEY_ENTER;
break;
}
g_last_key = act_key;
}
else
{
data->state = LV_INDEV_STATE_REL;
g_last_key = 0;
}
data->key = g_last_key;
}
/**
* @brief 获取当前正在按下的按键
* @param 无
* @retval 0 : 按键没有被按下
*/
static uint32_t keypad_get_key(void)
{
return key_scan(0);
}
#else
typedef int keep_pedantic_happy;
#endif
由上述源码可知,配置LVGL的按键驱动分为以下5个步骤:
(1) 调用keypad_init函数初始化按键设备。
(2) 调用函数lv_indev_drv_init初始化输入设备。
(3) 设置设备的类型。
(4) 设置按键回调函数,该函数用于获取按键的键值(需要根据实际需求转换为LVGL内部键值,这样才能使部件成功识别键值)。
(5) 调用函数lv_indev_drv_register注册输入设备。
3. 编写测试代码
在编写测试代码之前,我们必须先开启C99模式,否则编译代码会出现很多报错,开启C99模式的方法如下图所示:

图2.3.3 开启C99模式
开启C99模式之后,接下来就可以在main.c中编写测试代码,检测LVGL是否移植成功,具体源码如下:
#include "./BSP/TIMER/btim.h"
#include "lvgl.h"
#include "lv_port_indev_template.h"
#include "lv_port_disp_template.h"
int main(void)
{
/* 系统其他硬件初始化信息,这里忽略 */
btim_timx_int_init(10 - 1, 17000 - 1); /* 定时器周期为1ms */
lv_init(); /* lvgl系统初始化 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
lv_port_indev_init(); /* lvgl输入接口初始化,放在lv_init()的后面 */
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label,"Hello Alientek!!!");
lv_obj_center(label);
while(1)
{
lv_timer_handler(); /* LVGL管理函数相当于RTOS触发任务调度函数 */
delay_ms(5);
}
}
值得注意的是,上述源码中,基本定时器的初始化函数需要正确配置,这一个细节非常的关键!如果移植成功,即可看到测试代码的运行效果,如下图所示:

图2.3.4 显示”Hello Alientek”字体
2.4 移植官方例程
在精简LVGL源码的时候,我们保留了demos文件夹,该文件夹中存放的就是LVGL的官方示例。由于demos文件夹中存在多个官方例程,不同的例程对硬件的要求有所不同,这里以官方的压力测试实验为例,为大家介绍官方示例的移植流程,具体步骤如下:
(1) 把demos文件夹复制到Middlewares/LVGL/GUI_APP路径下,如下图所示:

图2.4.1 复制官方例程
(2) 添加文件路径

图2.4.2 添加文件路径
(3) 打开lv_conf.h文件,找到LV_USE_DEMO_STRESS宏定义并设置为1,开启该实验。
(4) 创建Middlewares/LVGL/GUI_APP分组,往其中添加demos/stress路径下的全部.c文件,如下图所示:

图2.4.3 官方例程文件
(5) 编译工程,如果出现如图2.4.4所示的错误,这说明某些字体没有定义。

图2.4.4 字体没有定义
如果大家遇到此报错,可以打开lv_conf.h文件,找到相应字体的宏定义,并将其设置为1即可,如下图所示:

图2.4.5 定义字体
定义相应的字体之后,再一次编译工程,如果配置的内存足够,此时就不会出现报错了。接下来即可在main函数中调用lv_demo_stress函数,运行压力测试示例,具体源码如下:
#include " lv_demo_stress.h"
int main(void)
{
/* 各种初始化/时钟/驱动等等,省略 */
btim_timx_int_init(10 - 1, 17000 - 1); /* 初始化定时器 */
lv_init(); /* lvgl系统初始化 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
lv_port_indev_init(); /* lvgl输入接口初始化,放在lv_init()的后面 */
lv_demo_stress();
while(1)
{
lv_timer_handler(); /* LVGL管理 */
delay_ms(5);
}
}
2.5下载验证
编译工程并下载到开发板中。值得注意的是,这个官方例程仅支持800*480分辨率的屏幕,我们使用的1.3寸屏幕分辨率为240*240,,无法正常展现全部界面,但是不影响压力测试。压力测试例程在1.3寸屏幕中运行的界面如下图所示:

图2.5.1 压力测试界面
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3117729/ ,如需转载,请注明出处,否则将追究法律责任。