STM32F7xx移植LVGL记录
1 简介
项目中要用到屏幕,也是工作多年首次进行彩屏的开发。之前的项目大多是不需要用户界面的,或者是单色屏。GUI的开发,也就是使用过u8g2,这是个不熟悉的领域。但是也一直想要尝试彩屏的,这就有了机会。
STM32就不多说了。简单说一下LVGL,在这之前也是听过LVGL这个开源图形库的鼎鼎大名,项目开始的时候,对LVGL做了简单的了解,简单的label,button等自不必说,还有优美的动画效果,那是一定要用在项目中。文档可以看LVGL的官网或者百问网上的中文翻译。个人感觉内容不是很详细,好在LVGL上手也简单,从一个小项目开始也是足够了。
2 移植
2.1 环境说明
在Windows使用MDK进行STM32的开发,MDK版本5.30+,编译器使用AC6+,选择C99(很多地方看到LVGL需要支持C99以上)。LVGL版本使用的V9.2。
2.2 移植准备
- 下载LVGL源码,GitHub:https://github.com/lvgl/lvgl.git
- 创建STM32的Keil工程
- 在工程中添加LVGL源码,这是个比较繁琐的步骤,源码中的文件放在不同的文件夹中,而且新手会不清楚哪些需要,哪些可以不用(我就是这样的,不想把不需要的添加到工程中,但是我又不了解)。可以参考正点原子的视频或文档,但是正点的使用的是V8.3的版本,还是有不一样的地方。
- 或者使用Keil的pack,一键添加到工程中,操作简单。LVGL的pack在源码中就有,在
lvgl\env_support\cmsis-pack\LVGL.lvgl.9.2.0.pack
,先进行安装。在项目中点击Manage Run-Time Environment,选择LVGL,需要勾选Essential(核心部分,必选),在不用其他的情况下,勾选Porting中的Display,使用显示的移植模板。当然还可以选择File System和Input的移植模板,我是没有用到。
准备工作差不多就完成了,下面开始移植部分。
2.3 移植步骤
移植,也就是接口对接。使其代码库能在现有的硬件平台上运行。图形库,其工作就是在屏幕上显示图形,那就一定需要我们为其提供屏幕显示的接口;然后,LVGL需要我们提供类似“心跳”的功能;最后,在程序中怎么去调用,来实现屏幕显示和刷新呢?需要我们在主循环中调用lv_timer_handler()
。LVGL就可以简单运行起来了。说明一下,我使用的是裸机。
屏幕驱动
屏幕驱动用于屏幕的初始化和屏幕指定区域的数据填充。
主要实现三个函数供LVGL调用。
屏幕初始化
这一步,点亮屏幕。需要根据自己用的屏幕,和所用的屏幕驱动芯片,以及MCU用到的IO进行编写。
屏幕刷新区域设置
屏幕驱动IC根据其手册,在刷新数据前先设置刷新区域,我所使用的屏幕根据起始的xy坐标和结束的xy坐标进行区域选择,这恰好也是LVGL所给出的区域参数。驱动IC有专门的指令进行区域设置,编码实现这个功能也是很简单的。
屏幕数据刷新
设置好刷新区域,紧跟着输入要显示的数据,即可实现屏幕的区域刷新。这里的数据就涉及到颜色了。我使用的屏幕支持的颜色是RGB565的,即一个点的像素使用16bit表示,R:G:B=5:6:5。刷新N个像素点,就需要发送N*2字节的数据。实际根据自己的屏幕情况,来进行数据发送。
网上有些是吧屏幕刷新区域设置和数据刷新合为一个,都是可以的。最主要就是实现区域的刷新就OK
驱动对接LVGL
驱动的对接,有个lv_port_disp_template.c
文件,这个文件就是LVGL提供的移植模板,其中有个disp_init(void)
的函数,在里面调用自己的屏幕初始化函数。
/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
/*You code here*/
}
还有个区域刷新的函数,在里面调用自己的屏幕刷新函数。其中的put_px(x, y, *px_map)
可以替换为自己的数据发送函数。循环前需设定刷新区域,这里没有,要自己添加。以16bit的颜色数据为例,要注意的是,最后面的数据入参是uint8_t *
类型,所以在循环中发送数据时,可以每次发送1字节,一次循环发送两次,同样px_map++
也要有两个。也可以在循环中发送一次2字节的数据,在循环之前,定义16bit数据的指针uint16_t* px = (uint16_t*)px_map
,循环中也不再使用px_map++
,而是px++
了,同样,要发送的也是px
所指向的数据。
/*Flush the content of the internal buffer the specific area on the display.
*`px_map` contains the rendered image as raw pixel map and it should be copied to `area` on the display.
*You can use DMA or any hardware acceleration to do this operation in the background but
*'lv_display_flush_ready()' has to be called when it's finished.*/
static void disp_flush(lv_display_t * disp_drv, const lv_area_t * area, uint8_t * px_map)
{
if(disp_flush_enabled) {
/*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, *px_map)*/
px_map++;
}
}
}
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_display_flush_ready(disp_drv);
}
LVGL配置
通常,LVGL的配置使用lv_conf.h
文件,使用宏定义进行参数的配置和功能的开关。使用MDK的RTE环境配置的LVGL项目,其配置文件是lv_conf_cmsis.h
。配置的重要几个点,屏幕分辨率,堆大小,色彩格式,字体,使用到的组件等。
- 屏幕分辨率
分辨率在生成的文件中是没有定义的,可以自己添加进去。例如:
#define MY_DISP_HOR_RES 480
#define MY_DISP_VER_RES 320
- 堆大小
堆大小默认配置是128kB,默认使用的是LVGL内建的malloc。宏定义如下:
/* Possible values
* - LV_STDLIB_BUILTIN: LVGL's built in implementation
* - LV_STDLIB_CLIB: Standard C functions, like malloc, strlen, etc
* - LV_STDLIB_MICROPYTHON: MicroPython implementation
* - LV_STDLIB_RTTHREAD: RT-Thread implementation
* - LV_STDLIB_CUSTOM: Implement the functions externally
*/
#define LV_USE_STDLIB_MALLOC LV_STDLIB_BUILTIN
#define LV_USE_STDLIB_STRING LV_STDLIB_BUILTIN
#define LV_USE_STDLIB_SPRINTF LV_STDLIB_BUILTIN
/*Size of the memory available for `lv_malloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE (128 * 1024U) /*[bytes]*/
这里我做了修改,改为LV_STDLIB_CLIB
,使用标准C库的相关函数,这样LV_MEM_SIZE
的定义就不起作用,需要增大startup_stm32f407xx.s
中的堆空间大小,我配置的是0x20000
。同时,实测LV_USE_STDLIB_SPRINTF
需要使用LV_STDLIB_CLIB
,label组件的格式化输出才能正确显示。
- 色彩格式
之前提到我用的屏幕支持RGB565的色彩,所以我这里配置只使用RGB565。如下,其他的颜色都配置为0。
#define LV_DRAW_SW_SUPPORT_RGB565 1
- 字体
LVGL有内置的字体文件,定义的是const
数组,占用Flash空间,对于资源较少的硬件,不是很友好。字体启用在配置头文件中有如下的宏定义列表。同时需要配置默认的字体。字体这里,有一点需要注意的是,源文件需要utf-8编码,因为LVGL在对字体解码时是使用utf-8的算法,主要是针对非ASCII编码的符号。对于汉字的显示,LVGL也是可以很好支持,不过需要自己提供字体文件。我使用了字库IC,所以这里就不需要额外的字体c文件,不过要自己实现字体获取的函数,之后会单独出个文档进行LVGL加入字库IC的说明。
/*Montserrat fonts with ASCII range and some symbols using 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 1
#define LV_FONT_MONTSERRAT_18 0
#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_28_COMPRESSED 0 /*bpp = 3*/
#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, Persian letters and all their forms*/
#define LV_FONT_SIMSUN_14_CJK 0 /*1000 most common CJK radicals*/
#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/
/*Pixel perfect monospace fonts*/
#define LV_FONT_UNSCII_8 0
#define LV_FONT_UNSCII_16 0
/*Optionally declare custom fonts here.
*You can use these fonts as default font too and they will be available globally.
*E.g. #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2)*/
#define LV_FONT_CUSTOM_DECLARE
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_montserrat_14
- 组件启用
使用不到的组件,可以不启用,以减小编译输出的大小。例如不使用table,配置#define LV_USE_TABLE 0
就行了。根据自己项目需求自己配置。
LVGL初始化
经过以上的步骤,移植基本完成。再加入“心跳”,LVGL就可以运行了。我这边加到了systick的中断中:
lv_tick_inc(1); // 在中断中调用,表示增加的心跳为1ms
最后,在main()
函数中,初始化时进行如下调用:
lv_init(); // LVGL初始化
lv_port_disp_init(); // 显示初始化
在大循环中调用:
lv_timer_handler(); // LVGL的定时器处理
到此,程序完成,可以编译运行啦。