在ZYNQ上跑超炫酷GUI!手把手教你移植LVGL到ZYNQ平台!
基于ZYNQ平台实现炫酷的GUI界面一般通常有两种方式,一种是跑Linux系统运行QT程序;另外一种是跑裸机程序,通过调用图形界面库实现GUI界面,选择哪种方式需结合使用场景。本次介绍第二种方式,教大家如何将图形界面库移植到ZYNQ平台,在ZYNQ平台实现炫酷的GUI界面,而无需依赖复杂的操作系统。此外,在移植成功后,大家后续就可以基于ZYNQ平台开发更有趣,更贴近实战的项目,如示波器、信号发生器等。
目前主流的图形界面库有uc/GUI、emWin和LVGL等,其中LVGL是一个免费的轻量级开源图形库,有着诸多优点,在下文会展开进行介绍,所以本次移植的是LVGL图形界面库。如果大家在网上搜索移植LVGL的方法,不出意外的话,绝大多数都是基于微处理器如STM32移植LVGL,基于ZYNQ平台的很少,所以本文章将手把手教大家如何在ZYNQ平台移植LVGL。
领航者开发板(点击查看)
一、认识LVGL
LVGL(Light and Versatile Graphics Library)是一个免费的轻量级开源图形库,其主要特征有:
-
丰富的部件:开关、按钮、图表、列表、滑块、图片,等等
-
高级图形属性:具有动画、抗锯齿、不透明度、平滑滚动等高级图形属性
-
支持多种输入设备:如触摸屏、鼠标、键盘、编码器等
-
支持多语言:UTF-8编码
-
支持多显示器:它可以同时使用多个TFT或者单色显示器
-
支持多种样式属性:它具有类CSS的样式,支持自定义图形元素
-
独立于硬件之外:它可以与任何微控制器或显示器一起使用
-
可扩展性:它能够以小内存运行(最低64KB闪存,16KB RAM)
-
支持操作系统、外部存储器和GPU(不是必需的)
-
具有高级图形效果:可进行单帧缓冲区操作。
-
纯C编写:LVGL基于C语言编写,以获得最大的兼容性。
综上可知:LVGL是一款具有丰富部件,具备高级图形特性,支持多种输入设备和多国语言,独立于硬件之外的开源图形库。官方地址为:LVGL --- Light and Versatile Embedded Graphics Library,该网页主要包含用户文档、图片转换器和字体转换器,该网页打开后如图1.1.1所示:
图 1.1.1 LVGL官方页面
上图中,点击右上角图标即可进入LVGL源码的github仓库,在该仓库中,可以下载LVGL相关的源码;点击Documentation既可打开LVGL官方文档,该文档中英文翻译都包含,主要讲解LVGL的基础知识、移植、部件使用、示例等等。可以结合官方文档以及本章内容一起学习LVGL的移植操作。
LVGL移植要求
市面上LVGL主要是在微处理器(MCU)上进行移植,然而ZYNQ的性能远远超过大多数MCU以及LVGL官方要求的最低性能要求,所以我们接下来只说它对于显示屏的要求。
LVGL只需要一个简单的驱动程序函数即可将像素阵列复制到显示器的指定区域当中,其对显示器的兼容性很强,具体要求如下(满足其一即可):
1.具有8/16/24/32位色深的显示器。
2.HDMI端口的显示器。
3.小型单色显示器。
4.LED矩阵。
5.其他可以控制像素颜色/状态的显示器。
我们正点原子的 2.8/3.5/4.3/7/10.1 寸 TFTLCD 模块以及 RGBLCD 模块都是16位色深的显示屏,这些显示屏皆可满足 LVGL 的要求。
LVGL源码下载
LVGL 相关的源码和工程都是存放在 GitHub 远程仓库中,该 GitHub 远程仓库地址为GitHub - lvgl/lvgl at release/v8.2,用户可以在该仓库中下载LVGL图形库的源码。由于GitHub仓库的服务器在国外,如果用户在国内访问该服务器可能登录不成功。我们可以从正点原子网盘资料中获取LVGL的V8.2版本源码,如图 1.1.2所示:
图 1.1.2 LVGL源码
上图中,"LVGL使用工具.zip"压缩包里存放了 LVGL 相关的离线转换工具,这些离线工具是广大爱好者根据 LVGL 的字库定义规则和图片的处理特性而编写的软件;"lvgl-release-v8.2.zip"压缩包里存放了LVGL图形库的V8.2 版本源码,其解压后如图 1.1.3所示:
图 1.1.3 LVGL源码文件
由上图可知,LVGL 源码的目录下有很多文件和文件夹,但用户并不需要完全了解它们,我们只需要了解与移植相关的部分即可。各文件夹和文件的功能如表 1.1.1所示:
表 1.1.1 lvgl-release-v8.2文件说明
上表中,与LVGL移植相关的有examples文件夹、src文件夹、lv_con_template.h和lvgl.h文件,其它的部分均与移植无关,用户可以忽略。接下来我们分别看一下examples、src这两个文件夹的文件结构:
examples文件夹
该文件夹主要包含LVGL部件实例、动画实例、其它第三方库实例以及输入设备和显示器驱动文件等内容,具体如表 1.1.2所示:
表 1.1.2 examples文件夹内容
上表中,只有porting文件夹与移植相关,其它文件夹中存放的是各种实例。
src文件夹
该文件夹主要包含LVGL源文件(部件源码、多种解码库),具体如表 1.1.3所示:
表 1.1.3 src文件夹的内容
上表中的内容都是与移植相关的,具体的移植方法我们后面将详细介绍,目前大家只需要对LVGL源码的文件结构有一定的了解即可。
二、移植前的准备工作
1)一款ZYNQ开发板:本次以领航者ZYNQ开发板来演示;
2)一款RGB LCD屏:本次以正点原子7寸RGB LCD屏来演示;
三、Vivado BlockDesign的工程搭建
本次实验需要完成RGB LCD屏显示GUI界面,并且支持触摸的功能,显示的功能可以由VDMA IP核、Video Timing Controller IP核、AXI-Stream to Video Out IP核、rgb2lcd IP核实现;触摸功能由EMIO模拟IIC接口实现。所以本次Block Design工程实际上和《领航者嵌入式Vitis开发指南》中的"RGB LCD画板实验"完全一致,可以直接在此工程基础上,另存为本次实验的工程即可,本次工程命名为"zynq_lvgl"。
本次实验Block Design的系统架构图如下图所示:
图 1.3.1 系统框图
由上图可知,首先我们需要在PS端写彩条数据到DDR中,然后PS需要通过AXI Interconnect IP核与AXI_HP端口进行连接,用于VDMA IP和PS之间高速传输数据。VDMA IP核将DDR3中的视频或图像数据传输给AXI4-Stream to Video Out IP核,AXI4-Stream to Video Out IP核在VTC IP核的控制下,把AXI4-Stream格式的数据转换成视频输出的数据格式(如RGB888),并将输出的视频数据流连接至RGB2LCD IP核(rgb2lcd)的输入端。最后PS通过AXI GPIO IP核获取LCD屏ID,并根据获取到的ID配置VDMA IP核的帧缓存空间大小、读通道等以及配置VTC IP核的输出的时序参数。由于不同分辨率的LCD屏其驱动时钟不一样,因此本次实验需要将时钟配置成动态时钟,PS根据LCD屏的ID配置时钟IP输出不同的时钟。再通过加入EMIO来检测LCD屏触摸状态的功能,这一部分功能是由PS端来实现的。PS不断地扫描LCD的触摸状态,包括是否有触摸动作发生、当前触摸点的坐标等信息,根据扫描到的信息来决定是否向VDMA的Frame Buffer中写入数据以及向Frame Buffer的哪个地址写入数据等等。然后位于PL端的VDMA读取逻辑不断地将Frame Buffer中存储的数据送给LCD进行显示。
PS与LCD的触摸芯片之间的通信,是由PS端GPIO引出的4个EMIO来完成的,包括CT_RST、CT_INT、IIC2_SCL、IIC2_SDA。CT_RST是ZYNQ送给触摸芯片的复位信号。CT_INT是触摸芯片送给ZYNQ的中断信号,用于指示是否有触摸事件发生。IIC2_SCL、IIC2_SDA是触摸芯片内置的IIC总线,ZYNQ使用它来访问触摸芯片的内部寄存器,来完成对触摸芯片的初始化、读取触摸点的坐标等动作。
图 1.3.2 整体系统框图
若想了解触摸屏的具体驱动以及硬件详解可以在网盘中下载《领航者ZYNQ之嵌入式Vitis开发指南》查看第二十四章《PS通过VDMA驱动LCD显示实验》以及第三十六章《RGB LCD画板实验》。
四、软件设计
将硬件导出至Vitis,打开Vitis后,我们创建一个Application Project,应用工程名为"zynq_lvgl",本实验和"RGB LCD画板"实验相比,源文件下新增了timer和LVGL文件夹,分别是定时器驱动代码和LVGL库文件,timer文件夹的内容可以直接拷贝"定时器中断实验"的代码。文件代码结构如下图所示:
图 1.4.1 工程目录结构
移植准备工作
首先将"RGB LCD画板实验"的代码全部拷贝到本工程当中,其次将"定时器中断实验"的代码拷贝到本工程当中,最后再准备LVGL源码。
我们将上述图 1.1.3获得的LVGL源码进行裁剪并将lv_conf_template.h文件改名为lv_conf.h。除了examples文件夹、src文件夹、lv_conf.h和lvgl.h文件,其它文件和文件夹均与移植无关,我们可以将它们删除,这样可以得到精简的LVGL源码。修改后的LVGL源码(保留了demos文件夹)如图 1.4.2所示:
图 1.4.2 精简后的LVGL源码
打开 lv_conf.h 文件,修改条件编译指令。
修改前:
vbscript
#if 0 /*Set it to "1" to enable content*/
修改后:
vbscript
#if 1 /*Set it to "1" to enable content*/
由上图可知,demos文件夹没有被删除,该文件夹中存放的是LVGL官方的演示例程,后续我们将移植其中的示例进行演示。
接下来我们打开上图中的examples文件夹,仅保留其中的porting文件夹,其它的文件和文件夹均可删除,删减后的examples文件夹如下所示:
图 1.4.3 仅保留proting文件夹
向工程添加文件
我们在工程目录下新建一个LVGL文件夹,并在该文件夹下面新建GUI文件夹以及GUI_APP文件夹,最后在GUI文件夹下新建lvgl文件夹。具体的文件夹结构如图 1.4.4所示:
图 1.4.4 LVGL目录下文件夹结构
这里说明一点,我们的工程之所以采用这样的文件夹结构,主要是为了兼容LVGL源码中的包含头文件的格式。对于初学者,这里建议按照此结构新建文件夹,否则有可能会遇到很多关于头文件的报错。
把精简后的LVGL源码(图 1.4.2中的文件夹和文件)复制到上述创建的LVGL/GUI/lvgl目录下,具体如所示:
图 1.4.5 移植LVGL源码
添加LVGL源码的文件夹路径,包含源码文件,右键选择应用工程,选择Properties选项,如下图所示:
图 1.4.6 选择Properties选项
在打开的Properties选项卡中选择C/C++ General选项下的Paths and Symbols,Add添加头文件路径,如下图所示:
图 1.4.7 添加头文件路径
点击Workspace选择要添加的头文件路径:
图 1.4.8 选择头文件路径
将所有头文件添加完成之后,点击Apply应用之后点击Apply and Close,确认并关闭选项卡,移植LVGL只需要添加关键的头文件路径即可,因为lvgl.h文件已经为我们省去了很多包含头文件的操作,添加的头文件路径如下图所示:
图 1.4.9 头文件路径
为LVGL提供时基
修改定时器驱动代码为LVGL提供时基,打开timer.c文件,声明LVGL的头文件,修改后的timer.c如下所示:
scss
1 #include "timer.h"
2 #include "lvgl.h"
3 #include "xil_cache.h"
4
5 #define TIMER_DEVICE_ID XPAR_XSCUTIMER_0_DEVICE_ID //定时器ID
6 #define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID //中断ID
7 #define TIMER_IRPT_INTR XPAR_SCUTIMER_INTR //定时器中断ID
8
9 //私有定时器的时钟频率 = CPU时钟频率/2 = 333MHz
10 //0.001s(1ms) 0.001*1000_000_000/(1000/333) - 1 = 0x514c7
11 #define TIMER_LOAD_VALUE 0x514c7 //定时器装载值
12
13 XScuTimer Timer; //定时器驱动程序实例
14
15 //定时器初始化程序
16 int timer_init(XScuGic *intc_ptr) //,XScuTimer *timer_ptr
17 {
18 int status;
19 //私有定时器初始化
20 XScuTimer_Config *timer_cfg_ptr;
21 timer_cfg_ptr = XScuTimer_LookupConfig(TIMER_DEVICE_ID);
22 if (NULL == timer_cfg_ptr)
23 return XST_FAILURE;
24 status = XScuTimer_CfgInitialize(&Timer, timer_cfg_ptr,timer_cfg_ptr->BaseAddr);
25 if (status != XST_SUCCESS) {
26 xil_printf("Timer Initial Failed\r\n");
27 return XST_FAILURE;
28 }
29 XScuTimer_LoadTimer(&Timer, TIMER_LOAD_VALUE); // 加载计数周期
30 XScuTimer_EnableAutoReload(&Timer); // 设置自动装载模式
31
32
33 //初始化中断控制器
34 XScuGic_Config *intc_cfg_ptr;
35 intc_cfg_ptr = XScuGic_LookupConfig(INTC_DEVICE_ID);
36 XScuGic_CfgInitialize(intc_ptr, intc_cfg_ptr,intc_cfg_ptr->CpuBaseAddress);
37 //设置并打开中断异常处理功能
38 Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
39 (Xil_ExceptionHandler)XScuGic_InterruptHandler, intc_ptr);
40 Xil_ExceptionEnable();
41
42 //设置定时器中断
43 XScuGic_Connect(intc_ptr, TIMER_IRPT_INTR,
44 (Xil_ExceptionHandler)timer_intr_handler, (void *)(&Timer));
45
46 XScuGic_Enable(intc_ptr, TIMER_IRPT_INTR); //使能GIC中的定时器中断
47 XScuTimer_EnableInterrupt(&Timer); //使能定时器中断
48
49 XScuTimer_Start(&Timer); //启动定时器
50 return XST_SUCCESS;
51 }
52
53 //定时器中断处理程序
54 void timer_intr_handler(void *CallBackRef)
55 {
56 static int ms_cnt = 0;
57 XScuTimer *timer_ptr = (XScuTimer *) CallBackRef;
58
59 lv_tick_inc(1); /* lvgl 的 1ms 心跳 */
60 ms_cnt++;
61 if(ms_cnt == 5)
62 {
63 ms_cnt = 0;
64 Xil_DCacheFlush(); //刷新Cache,数据更新至内存
65 }
66 //清除定时器中断标志
67 XScuTimer_ClearInterruptStatus(timer_ptr);
68 }
上述源码对比"定时器中断实验",修改了重装载值,让cpu每1ms进一次中断,定时器中断回调函数调用了LVGL的lv_tick_inc函数,该函数可以让LVGL内部的时基参数加1(入口参数为1的情况下),然后在中断中,每5ms刷新一次Cache,使数据更新至内存,若没有此操作,显示画面会有撕裂情况。
配置显示屏、触摸输入驱动
打开LVGL/GUI/lvgl/examples/porting下的lv_port_disp_template.c/h(显示屏相关)和lv_port_indev_template.c/h(触摸输入相关)文件,将这4个文件中的条件编译指令#if 0都修改成#if 1,如下所示:
修改前:
shell
#if 0 /* lv_port_disp_template.c/h 和 lv_port_indev_template.c/h */
修改后:
arduino
#if 1 /* 把 #if 0 修改成 #if 1 */
修改lv_port_disp_template.c文件
该文件用于配置显示屏,它可以将用户的底层显示驱动与LVGL的显示驱动衔接起来。我们打开官方提供的lv_port_disp_template.c文件,包含LCD驱动头文件,然后配置LCD显示相关的驱动程序,修改后的源码如下:
arduino
1 #if 1
2
3 #include "lv_port_disp_template.h"
4 #include "../../lvgl.h"
5 #include "../../../../../src/display_ctrl/display_ctrl.h"
6
7
8 #define MY_DISP_HOR_RES (800) /* 屏幕宽度 */
9 #define MY_DISP_VER_RES (480) /* 屏幕高度 */
10
11
12 extern VideoMode vd_mode;
13 extern unsigned int const frame_buffer_addr;
14
15 static void disp_init(void);
16
17 static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
18
19 void lv_port_disp_init(void)
20 {
21 disp_init();
22
23 /* Example for 1) */
24 static lv_disp_draw_buf_t draw_buf_dsc_1;
25 static lv_color_t buf_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*A buffer for 10 rows*/
26 lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * MY_DISP_VER_RES); /*Initialize the display buffer*/
27
28 static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
29 lv_disp_drv_init(&disp_drv); /*Basic initialization*/
30
31 /*Set up the functions to access to your display*/
32
33 /*Set the resolution of the display*/
34 disp_drv.hor_res = vd_mode.width;
35 disp_drv.ver_res = vd_mode.height;
36
37 /*Used to copy the buffer's content to the display*/
38 disp_drv.flush_cb = disp_flush;
39
40 /*Set a display buffer*/
41 disp_drv.draw_buf = &draw_buf_dsc_1;
42
43 /*Finally register the driver*/
44 lv_disp_drv_register(&disp_drv);
45 }
46
47 /*Initialize your display and the required peripherals.*/
48 static void disp_init(void)
49 {
50 /*You code here*/
51 }
52
53 static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
54 {
55 /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
56
57
58 uint16_t x;
59 uint16_t y;
60 uint32_t color_index = 0;
61 uint8_t * lcd_base_addr = (uint8_t *)frame_buffer_addr;
62 uint8_t * color_p_t = (uint8_t *)color_p;
63
64 for(y = area->y1; y <= area->y2; y++)
65 {
66 for(x = area->x1; x <= area->x2; x++)
67 {
68 /*Put a pixel to the display. For example:*/
69 /*put_px(x, y, *color_p)*/
70 lcd_base_addr[y*vd_mode.width*3+x*3] = color_p_t[color_index+0];
71 lcd_base_addr[y*vd_mode.width*3+x*3+1] = color_p_t[color_index+1];
72 lcd_base_addr[y*vd_mode.width*3+x*3+2] = color_p_t[color_index+2];
73 color_index = color_index + 4;
74 }
75 }
76
77 /*IMPORTANT!!!
78 *Inform the graphics library that you are ready with the flushing*/
79 lv_disp_flush_ready(disp_drv);
80 }
81
82 #else /*Enable this file at the top*/
83
84 /*This dummy typedef exists purely to silence -Wpedantic.*/
85 typedef int keep_pedantic_happy;
86 #endif
87
由上述源码可知,配置LVGL显示屏驱动的步骤可分为以下7步:
(1)调用函数disp_init初始化LCD驱动。
(2)调用函数lv_disp_draw_buf_init初始化缓冲区(最低标准:显示屏的宽度分辨率/10)。
(3)调用函数lv_disp_drv_init初始化显示驱动。
(4)设置显示的高度和宽度。
(5)注册显示驱动回调。
(6)设置显示驱动的绘画缓冲区。
(7)调用lv_disp_drv_register函数注册显示驱动到LVGL列表中。
在整个配置的过程中,实际上我们只需要设置缓冲区的大小和传输图像数据。如果想设置屏幕的方向,需要在touch.c文件中的TP_Init函数开头添加"tp_dev.touchtype |= 0x01"设置为横屏显示(默认为竖屏)。在本章中,lv_port_disp_template.c文件我们做了精简处理,删去了很多没有用到的东西,例如:缓冲区的刷新方式有三种,我们只是使用了其中一种,若想要使用其他两种刷新方式可以查看官方源码以及结合官方文档尝试移植使用。
修改lv_port_indev_template.c文件
该文件用于配置输入设备,例如:触摸屏、鼠标、键盘、编码器、按键等,它可以将用户的底层输入设备驱动与LVGL的输入驱动衔接起来。这里我们使用触摸屏作为输入设备(将源码中其他输入设备的代码进行了裁剪处理,若想要使用其他的输入设备建议查看官方文档移植),具体的配置源码如下所示:
arduino
1 #if 1
2
3 #include "lv_port_indev_template.h"
4 #include "../../lvgl.h"
5 #include "../../../../../src/TOUCH/touch.h"
6
7 static void touchpad_init(void);
8 static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
9 static bool touchpad_is_pressed(void);
10 static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y);
11
12 lv_indev_t * indev_touchpad;
13
14 void lv_port_indev_init(void)
15 {
16
17 static lv_indev_drv_t indev_drv;
18
19 /*Initialize your touchpad if you have*/
20 touchpad_init();
21
22 /*Register a touchpad input device*/
23 lv_indev_drv_init(&indev_drv);
24 indev_drv.type = LV_INDEV_TYPE_POINTER;
25 indev_drv.read_cb = touchpad_read;
26 indev_touchpad = lv_indev_drv_register(&indev_drv);
27 }
28
29 /*Initialize your touchpad*/
30 static void touchpad_init(void)
31 {
32 /*Your code comes here*/
33 tp_dev.init();//þ
34 }
35
36 /*Will be called by the library to read the touchpad*/
37 static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
38 {
39 static lv_coord_t last_x = 0;
40 static lv_coord_t last_y = 0;
41
42 /*Save the pressed coordinates and the state*/
43 if(touchpad_is_pressed()) {
44 touchpad_get_xy(&last_x, &last_y);
45 data->state = LV_INDEV_STATE_PR;
46 } else {
47 data->state = LV_INDEV_STATE_REL;
48 }
49
50 /*Set the last pressed coordinates*/
51 data->point.x = last_x;
52 data->point.y = last_y;
53 }
54
55 /*Return true is the touchpad is pressed*/
56 static bool touchpad_is_pressed(void)
57 {
58 /*Your code comes here*/
59
60 tp_dev.scan();
61
62 if (tp_dev.sta & TP_PRES_DOWN)
63 {
64 return true;
65 }
66
67 return false;
68 }
69
70 /*Get the x and y coordinates if the touchpad is pressed*/
71 static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
72 {
73 /*Your code comes here*/
74
75 (*x) = tp_dev.x[0];
76 (*y) = tp_dev.y[0];
77 }
78
79 #else /*Enable this file at the top*/
80
81 /*This dummy typedef exists purely to silence -Wpedantic.*/
82 typedef int keep_pedantic_happy;
83 #endif
84
由上述源码可知,配置LVGL的触摸部分分为以下5个步骤:
(1)调用函数touchpad_init初始化触摸设备。
(2)调用函数lv_indev_drv_init初始化输入设备。
(3)设置设备的类型。
(4)设置触摸回调函数,该函数用于获取触摸屏的坐标。
(5)调用函数lv_indev_drv_register注册输入设备。
LVGL官方提供的获取触摸坐标函数是touchpad_get_xy,该函数的x和y的值需要用户提供。
移植官方例程
在精简LVGL源码的时候,我们保留了demos文件夹,该文件夹中存放的就是LVGL的官方示例。由于demos文件夹中存在多个官方例程,不同的例程对硬件的要求有所不同,这里以官方的音乐播放器为例(该示例对硬件要求较高),为大家介绍官方示例的移植流程,具体步骤如下:
把demos文件夹复制到LVGL/GUI_APP路径下,只保留music文件夹,如下图所示:
图 1.4.10 复制官方例程
添加文件路径:
图 1.4.11 添加文件路径
打开lv_conf.h文件,找到LV_USE_DEMO_MUSIC宏定义并设置为1,开启该实验,并且修改颜色深度配置LV_COLOR_DEPTH宏定义为32,编译工程,若出现下图所示报错,这说明14号以及16号字体没有定义。
图 1.4.12 字体没有定义
如果大家遇到此报错,可以打开lv_conf.h文件,找到相应字体的宏定义,并将其设置为1即可,如下图所示:
图 1.4.13 定义字体
定义相应的字体之后,再一次编译工程,如果配置的内存足够,此时就不会出现报错了。接下来即可包含"lv_demo_music.h"头文件在main函数中调用lv_demo_music函数,运行音乐播放器示例,具体源码如下:
scss
59 int main(void)
60 {
61 timer_init(&Intc);
62 //获取LCD的ID
63 XGpio_Initialize(&axi_gpio_inst,AXI_GPIO_0_ID);
64 XGpio_SetDataDirection(&axi_gpio_inst,AXI_GPIO_0_CHANEL,0x07); //设置AXI GPIO为输入
65 lcd_id = LTDC_PanelID_Read(&axi_gpio_inst,AXI_GPIO_0_CHANEL);
66 XGpio_SetDataDirection(&axi_gpio_inst,AXI_GPIO_0_CHANEL,0x00); //设置AXI GPIO为输出
67 xil_printf("LCD ID: %x\r\n",lcd_id);
68
69 //根据获取的LCD的ID号来进行video参数的选择
70 switch(lcd_id){
71 case 0x4342 : vd_mode = VMODE_480x272; break; //4.3寸屏,480*272分辨率
72 case 0x4384 : vd_mode = VMODE_800x480; break; //4.3寸屏,800*480分辨率
73 case 0x7084 : vd_mode = VMODE_800x480; break; //7寸屏,800*480分辨率
74 case 0x7016 : vd_mode = VMODE_1024x600; break; //7寸屏,1024*600分辨率
75 case 0x1018 : vd_mode = VMODE_1280x800; break; //10.1寸屏,1280*800分辨率
76 default : vd_mode = VMODE_800x480; break;
77 }
78
79 emio_init();
80
81 //配置VDMA
82 run_vdma_frame_buffer(&vdma, VDMA_ID, vd_mode.width, vd_mode.height,
83 frame_buffer_addr,0, 0,ONLY_READ);
84
85 //设置时钟IP核输出的时钟频率
86 clk_wiz_cfg(CLK_WIZ_ID,vd_mode.freq);
87 //初始化Display controller
88 DisplayInitialize(&dispCtrl, DISP_VTC_ID);
89 //设置VideoMode
90 DisplaySetMode(&dispCtrl, &vd_mode);
91 DisplayStart(&dispCtrl);
92
93 lv_init(); /* lvgl系统初始化 */
94 lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
95 lv_port_indev_init(); /* lvgl输入接口初始化,放在lv_init()的后面 */
96
97 lv_demo_music(); /* 官方例程测试 */
98 while(1)
99 {
100 lv_task_handler();
101 }
102
103 return 0;
104 }
105
五、下载验证
首先我们将下载器与领航者底板上的JTAG接口连接,下载器另外一端与电脑连接。然后使用USB连接线将USB UART接口与电脑连接,用于串口通信。接下来使用FPC排线一端与RGB LCD液晶屏上的接口连接,另一端连接领航者底板上的RGB LCD接口,如图 24.5.1和图 24.5.2所示。连接时,先掀开FPC连接器上的黑色翻盖,将FPC排线蓝色面朝上插入连接器,最后将黑色盖压下以固定FPC排线。
图 1.5.1 RGB LCD液晶屏
图 1.5.2 开发板连接图
编译工程并下载到开发板中。值得注意的是,这个官方例程仅支持 800 * 480 分辨率的屏幕,如果使用其他分辨率的屏幕,可能导致例程功能无法正常展现。音乐播放器例程正常运行的界面如下图所示:
图 1.5.3 音乐播放器界面
如果想要查看屏幕的FPS显示,需要在lv_conf.h当中将LV_USE_PERF_MONITOR的宏定义置1就可以查看当前的帧数显示以及CPU占用率,可以看到右下角显示的帧率非常的低,只有11FPS,我们可以通过更改编译器的优化等级来优化帧率。
右键应用工程选择C/C++ Build Settings选项,如下图所示:
图 1.5.4 C/C++ Build Settings选项
将优化等级更改为-O2级,如下图所示:
图 1.5.5 修改编译器优化等级
接下来,我们简单介绍一下不同优化等级的说明和优势:
表 1.5.1 编译器优化等级说明
由上表所示:-O2优化等级会更好的提高代码的性能,并且不会增大体积和占用太多的编译时间,所以大多数编译会选择-O2优化等级。同样我们也需要更改屏幕静态的最大帧数显示,搜索LV_DISP_DEF_REFR_PERIOD,此宏定义为定义屏幕的刷新周期,默认为30ms,此值设置过大可能会出现卡顿现象,设置过小会浪费性能,我们将他设置为10,使其静态的最大帧数为100FPS;这里可以将LV_INDEV_DEF_READ_PERIOD宏定义也修改为10,此宏定义为输入设备的轮询周期,10ms代表每隔10ms采样一次输入设备的状态。编译工程之后,下载代码,如下图所示:
图 1.5.6 优化后的帧率显示
从上图可以看到,帧率从11FPS优化到了29FPS。
以上内容实现了将LVGL的官方Demo显示到RGB LCD屏上,后续我们还会基于LVGL,实现一个简易示波器的功能,让大家更能深入体会到ZYNQ软硬件协同开发的优势,目前简易示波器的实现方案正在评估中,做出来后也会第一时间分享给大家,敬请期待。