准备工作
1. 开发环境
开发环境介绍主要如下:
■ 硬件开发板:GD32F470ZGT6 立创·梁山派开发板
■ 屏幕:1.69寸 240x280 SPI接口TFT彩屏幕
■ Cortex-M4:GD32F470ZGT6
■ 操作系统:Win10-64 位
■ 开发环境:KEIL 5.37
■ 固件库:GD32F4xx_Firmware_Library V2.1.3
■ GUI:LVGL 8.1.0
1. 准备一个LCD的工程
我的工程是以梁山派GD32F470ZGT6作为主控,以1.69寸TFT彩色的240x280屏幕作为显示。主控与屏幕的连接如下:
为了提高LVGL的刷新率,建议使用硬件SPI+DMA,最好是双BUFF方式。
关于如何配置使用硬件SPI+DMA以及代码,可以看我上一章的内容:
立创梁山派GD32F470ZGT6–硬件SPI+DMA的快速刷屏
文件移植
材料准备
移植过程中需要用到 LVGL 图形库,LVGL 图形库的下载地址为:https://github.com/lvgl/lvgl
本案例使用的是 LVGL8.2.0 版本
删减修改LVGL文件
直接在工程下,新建一个文件【LVGL】。
将下载的 lvgl 源码解压。
提取【 example】文件夹 和【 src】 文件夹。提取文件 “lv_conf_template.h” 和 “lvgl.h”
将提取的文件放到我们工程目录的LVGL 文件夹目录下。并将“lv_conf_template.h”名称修改为“lv_conf.h”;
在我们之前提取的【examples】文件夹里,将文件夹【porting】提取到【LVGL】文件夹下。
将【examples】文件夹删除。让我们工程LVGL目录里只剩下文件夹【porting】文件夹【src】文件"lv_conf.h" 文件"lvgl.h"。
在“LVGL\porting”文件夹目录下,将“lv_port_disp_template.c”和“lv_port_disp_template.h”文件名称修改为“lv_port_disp.c”和“lv_port_disp.h”,将“ lv_port_indev_template.c ” 和 “ lv_port_indev_template.h ” 文 件 名 称 修 改 为“lv_port_indev.c”和“lv_port_indev.h”。
KEIL中添加LVGL相关文件与路径
添加.h路径
新建工程目录管理。具体如下:
往新建的目录文件夹添加文件。文件过多,我这里不在写明,只截图。
-
lvgl_porting
-
lvgl_core
将【LVGL】->【src】->【core】下的全部文件添加。
-
lvgl_draw
添加.\src\draw下所有源码(不要添加文件夹!),以及sw文件夹下所有源码。
-
lvgl_extra
添加\src\extra下所有源码和layouts、libs、others、themes、widgets下所有源文件。这里的文件很多很杂,只要是说到的文件夹,那么里面的所有文件夹里的.c文件都要全部添加! -
lvgl_font
添加\src\font下所有源码。
-
lvgl_gpu
添加\src\draw\sdl下所有源码,还有\src\draw\stm32_dma2d下的所有源码。
-
lvgl_hal
添加\src\hal下所有源码.
-
lvgl_misc
添加\src\misc下所有源码.
-
lvgl_widgets
添加\src\widgets下所有源码.
现在文件的移植已经完成,我们进行相关代码修改。
修改报错
尝试编译,出现很多个错误,大多都是找不到lv_conf.h的路径,导致打开文件失败。
双击上图中画红色方框的地方跳转到对应报错的地方。将
../../lv_conf.h
修改为
../lv_conf.h
继续编译,还是很多个错误。因为LVGL的源码需要C99的支持,否则编译无法通过。
将编译模式,修改为C99模式。
再编译,就没有错误,只有警告了。
如果大家编译还是有错误。参考如下:
如果编译还是有错,.\Objects\Application.axf: Error: L6218E: Undefined symbol __aeabi_assert (referred from qrcodegen.o).
qrcodegen 中使用了 assert这个断言
断言里面用到了 __aeabi_assert,这个NDEBUG没有定义所以报错了
我们可以在外面定义__aeabi_assert这个函数如下,或者通过添加定义 NDEBUG宏来失效断言
void __aeabi_assert(const char *err, const char *file, int line)
{
/* 输出内容自己定 */
}
如果工程之中没有内存管理,则需要修改启动文件中的堆栈。根据官方推荐我们可以把堆栈修改为4K,假如使用的功能比较多,还需要再适当增大。
打开lv_conf.h文件中的条件编译。
现在移植成功了,但是还显示不了,我们还需要去实现显示的相关接口。
修改显示接口文件
默认【 lv_port_disp.c】 和 【lv_port_disp.h】 的条件编译是关闭的,我们需要把他打开并修改包含目录层级。
将【 lv_port_disp.c】里的头文件 #include “lv_port_disp_template.h” 修改为 #include “lv_port_disp.h”。并将 #include “. ./. ./lvgl.h” 修改为 #include “. ./lvgl.h”
更改【lv_port_disp.c 】文件中 lv_port_disp_init 驱动函数。此函数提供了三种写缓存方式,保留其中一种即可,本案例采用方式一。
方式一:单缓存显示(10行),主控内存较小时选用此方式。
方式二:双缓存显示(两个10行),此方式支持DMA交替传输,缓存区越大,显示效果越好(有条件两个满屏缓存)。
方式三:双满屏缓存显示,相当于有条件的方式二
注释方式二和方式三,更改第一种方式如下:
将报错的宏【MY_DISP_HOR_RES】定义为我们屏幕的宽,这里我的屏幕宽是280,所以我重新宏定义为 280。这里建议大家直接将屏幕的宽定义拿过来定义。拿过来的话,需要像我一样导入LCD的头文件。LCD_W = 280 LCD_H = 240
更改屏幕大小
在static void disp_init(void)中,添加你的LCD初始化函数。
修改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*/
int32_t x;
int32_t y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
/*Put a pixel to the display. For example:*/
/*put_px(x, y, *color_p)*/
LCD_DrawPointFlush(x,y,color_p->full);
color_p++;
}
}
// LCD_DrawLump(area->x1,area->y1,area->x2,area->y2,(uint32_t)color_p);
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
其中,LCD_DrawPointFlush 函数 在lcdgui.中,根据上一片文章得来:
立创梁山派GD32F470ZGT6–硬件SPI+DMA的快速刷屏
/************************************************************
* 函数名称:LCD_DrawPointFlush
* 函数说明:在指定地方画点
* 型 参:x,y显示坐标
color 显示的颜色
* 返 回 值:无
* 备 注:无
*************************************************************/
void LCD_DrawPointFlush( uint16_t x, uint16_t y, uint16_t color)
{
Show_Gram[((LCD_W * y) + x)] = color;
}
去到【lv_port_disp.h】文件,将头文件路径 :#include “lvgl/lvgl.h” 修改为 #include “. ./lvgl.h”
根据需求修改lv_conf.h中的宏定义
- LV_COLOR_DEPTH:屏幕的色彩深度,支持1bit、8bit、16bit、32bit。
- LV_COLOR_16_SWAP:字节交换,使用SPI或者DMA刷屏的时候需要置1。
- LV_MEM_SIZE:GUI可支配的内存空间,根据使用的功能调节。
- LV_USE_PERF_MONITOR:显示CPU使用率和FPS计数。
- LV_USE_MEM_MONITOR:显示已使用的内存和内存碎片。
-
- LV_FONT_MONTSERRAT_*:字体大小,太小会很模糊。
修改上面字体时,下面这个地方也要修改:例如改为16大小,则将14改为16即可。
这里需要注意,在设置里不能使用 Use MicroLIB 不然会卡死。将【Use MicroLIB】选项的打勾取消。
如果需要通过 printf 打印一些信息。则将以下代码,写入串口文件中。
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
/************************************************
函数名称 : fputc
功 能 : 串口重定向函数
参 数 :
返 回 值 :
作 者 : LC
*************************************************/
int fputc(int ch, FILE *f)
{
usart_send_data(ch);
// 等待发送数据缓冲区标志置位
return ch;
}
这样就可以使用 printf 了。
要让LVGL运行还需要给LVGL周期的运行两个函数。【lv_tick_inc】【lv_task_handler】。其中【lv_tick_inc】的运行周期单位为ms。这里我将其放在滴答定时器中断中,每隔1ms调用一次。
LVGL显示验证
在main.c中
#include "gd32f4xx.h"
#include "systick.h"
#include "bsp_led.h"
#include "lcdinit.h"
#include "lcdgui.h"
#include "bsp_usart.h"
#include "stdio.h"
#include "string.h"
#include "lv_conf.h"
#include "lv_port_disp.h"
#include "lvgl.h"
void Lvgl_Lable_Demo(void)
{
lv_obj_t *scr = lv_scr_act();
lv_obj_t * label1 = lv_label_create(scr);
lv_label_set_long_mode(label1, LV_LABEL_LONG_WRAP);
lv_label_set_recolor(label1, true);
lv_label_set_text(label1, "#0000ff Re-color# #ff00ff words# #ff0000 of a# label, align the lines to the center "
"and wrap long text automatically.");
lv_obj_set_width(label1, 150);
lv_obj_set_style_text_align(label1, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(label1, LV_ALIGN_CENTER, 0, -40);
lv_obj_t * label2 = lv_label_create(scr);
lv_label_set_long_mode(label2, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_width(label2, 150);
lv_label_set_text(label2, "It is a circularly scrolling text. ");
lv_obj_align(label2, LV_ALIGN_CENTER, 0, 40);
}
/******************************************************************
* 函 数 名 称:main
* 函 数 说 明:主函数
* 函 数 形 参:无
* 函 数 返 回:无
* 作 者: LC
* 备 注:无
******************************************************************/
int main(void)
{
int i=0;
Subscribe_message_struct data;
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
//滴答定时器初始化 1us
systick_config();
//sram初始化
exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);
//串口0初始化(调试)
usart_gpio_config( 115200U );
printf("start\r\n");
//lcd初始化
LCD_Init();
// Lcd_Gram_Fill(BLACK);
// LCD_Show_Gram();
// //等待一帧数据搬运完成
// while(get_show_over_flag());
lv_init(); // 初始化lvgl
lv_port_disp_init(); // 显示初始化
Lvgl_Lable_Demo(); //LVGL显示测试
//开启定时器固定刷屏
Lcd_Show_Time_config();
while(1)
{
//定时器循环固定数据刷屏
while(get_show_over_flag());
set_show_update_flag(1);
while(get_show_update_flag());
}
}
实物展示
关于其他:
LVGL帧率限制
LVGL是有一个帧率刷新周期的宏定义,在lv_conf.h里。LVGL会通过LVGL内部的tick,定时去刷屏幕,也就是说该宏定义限定了LVGL刷屏帧率的上限,默认满帧33帧。这里的30即1000ms/30ms=33FPS,这里我们直接改成10ms刷新一次,满帧100帧。满帧是指达到显示器刷新率的上限。
在lv_conf.h里配置DPI:
/*
* LV_DPI_DEF 注意这里,虽然LVGL的作者说这个没这么重要,但他会严重影响到LVGL的动画效果
* 你应该进行DPI的手动计算,例如280*240分辨率1.69英寸的屏幕,那么 DPI = (280*280+240*240)^0.5 / 1.69 = 153
*/
#define LV_DPI_DEF 218 /*[px/inch]*/
目前只移植了显示部分,后面完善触摸或者编码器部分。
代码: