LVGL(LittlevGL) 是一个功能强大的开源图形用户界面(GUI)库,主要应用于嵌入式设备。为了在不同的硬件平台上运行 LVGL,我们需要进行移植(porting)。移植的目标是让 LVGL 库能够在特定的硬件和操作环境下正常运行,从而显示图形界面并响应用户输入。
测试屏幕
在移植 LVGL 之前我们最好先测试一个我们的 LED 显示屏是否可以正常工作,如果可以正常工作再将 LVGL 生成的图像渲染到屏幕上;
我们这里使用的是 P169H002-CTP 型号的 1.69 寸显示屏;他的显示芯片为 ST7789V,触控芯片为 CST816T,我们可以先通过官方提供的驱动函数先试着点亮屏幕,看屏幕是否可以支持工作
显示驱动
拷贝官方 ST7789 的驱动程序等到我们工程中;
我们需要修改官方驱动程序的引脚与我们板子上所使用的是一致的:
1 | // st7789.h |
在修改时候一定要注意,如果官方提供的代码示例(SPI 实现)是通过硬件实现的,我们在修改引脚后一定不要忘记修改引脚的复用编号,和具体使用的复用功能(SPI几)的编号;
触控驱动
拷贝官方 CST816T 的驱动程序等到我们工程中,因为触控驱动使用的是 I2C 进行通信所以这里还需要拷贝 I2C 的实现代码;
修改驱动引脚;
这里要知道触控的实现是我们点击屏幕,然后它对应的引脚就会产生变化,我们需要去监听这个引脚的变化,也就是需要一个外部中断;所以官方提供的驱动中有引脚是需要外部中断的;
1 | // cst816t.h |
修改的时候细心些,修改完引脚后,中断配置中的一些也需要修改,不要漏了,否则会导致中断无法触发;
如果上面正确,会在屏幕上显示一条线,并且点击屏幕会在中断中有打印输出;
移植流程
获取 LVGL
获取 LVGL(为了保证与 LVGL 图形化工具生成的代码兼容,二者最好选择一样的版本 v8.3)
lvgl 图形库下包含多个子目录,要移植 LVGL,只需要 src 目录下的 .c 和 .h 文件,以及根目录下的 lvgl.h 这个文件即可。
lvgl 官方库中还包含了 examples 和 demos 目录。如果你的项目需要示例或演示,你可以将这些目录添加到你的项目中。如果使用 make 或 CMake 来处理 examples 和 demos 目录,那么就不需要额外的操作。
LVGL 本身只是一个 GUI 的图形库,他将图像生成后,需要通过驱动程序将数据发送给屏幕;
为了检测移植是否成功所以我们需要保留 examples 和 demos 目录
创建 lv_conf.h
当你第一次设置项目时,首先将 lvgl 目录下的 lv_conf_template.h 重命名为 lv_conf.h。
接着,将文件中第一个 #if 0 改为 #if 1,以启用该文件的内容。
然后,根据你使用的显示屏的颜色深度,设置 LV_COLOR_DEPTH 这个宏。有关详细信息,请查看 lv_conf.h 文件中的注释。
这里我们需要去看下我们屏幕所使用的颜色格式或者说颜色深度,我们使用的屏幕显示部分驱动用的是 ST7789 芯片,颜色深度为可选择为 6/16/18 色,这里我们让二者保持一致都选择呢 16 色即可;
#define LV_COLOR_DEPTH 16
此时我们需要的目录结构如下:
1 | lvgl/ |
代码移植
此时我们需要将裁剪后的代码添加到我们的项目中去,将 lvgl 目录下裁剪后的代码复制到我们工程下的 Middleware/LVGL 目录中;
在 Keil 中添加 lvgl 相关文件时候,要注意我们并不是要拷贝 src 下的所有代码,他有些代码是为了兼容其他架构的我们就不需要添加进来;
兼容性驱动代码:
1 | Middlewares/ |
我们再添加代码时候,上述目录中与我们使用的硬件不相关管的代码可以不用添加到我们的 Keil 工程中去;
- src 目录下的代码负责生成 2D 图像的像素数据,但要将这些数据传输到屏幕并正确显示出来,还需要具体硬件的驱动程序来完成屏幕的初始化和数据传输。因此,我们需要添加并移植显示设备驱动和输入设备驱动的模板文件,以实现屏幕显示功能和输入交互功能。
1 | Middleware/ |
我们需要将图像显示在屏幕中,并通过触摸屏去操控屏幕,所以这里我们还需要添加 lv_port_disp_template.c 和 lv_port_indev_template.c 两个文件;
添加完成后记得将头文件目录加到项目中去;
lv_conf.h ;
Middleware/LVGL/examples/porting 下驱动程序相关的头文件
如果编译不报错,则代表初步移植完成,但它此时他还会爆一些警告,因为我们还没有添加一些必须要的参数信息;
配置驱动
显示驱动
启用输出模板代码
我们虽然添加了显示的驱动程序模板,但是它默认是不启用的,所以我们这里需要配置以启用它;
我们需要修改 lv_port_disp_template.c 和 lv_port_disp_template.h 中的条件编译选项以启用他的输出模板代码;
1 |
在输出模板中初始化我们的屏幕驱动
- 我们要先设置显示屏的分辨率,这样可以让 LVGL 生成的图像适配我们的屏幕
1 | // lv_port_disp_template.h |
- 初始化我们屏幕驱动
1 | // lv_port_disp_template.c |
相当于官方给我们提供了一个统一的显示接口,所有的显示都放在这个模板文件中,在 LVGL 层面起到了一个屏蔽不同硬件差异的一个效果
屏幕显示驱动的代码一般是由硬件的供应商提供的,我们只需要引入到我们的工程目录下并在显示驱动文件中调用即可
- 配置我们的输出缓冲区
LVGL 的渲染流程是先将生成的图像存放在一个缓冲区中,然后通过硬件的显示驱动,将缓冲区中的数据显示到屏幕上。
LVGL 提供了三种不同的缓冲区类型,这里我们需要配置其中选择一个
1 | // lv_port_disp_template.c |
lv_port_disp_init 函数中包含了显示驱动和显示器绘图缓冲区的配置
在 disp_flush 函数中配置打点输出
在配置完后,它会调用 disp_flush 这个函数将图形缓冲区中的内容正式的推送或者说刷新到屏幕上进行显示;
1 | // lv_port_disp_template.c |
我们需要在 LVGL 官方提供的输出模板中初始化我们屏幕的显示芯片,然后调用显示函数将 LVGL 生成的图像显示在我们的屏幕上;
触摸驱动
配置触摸驱动与显示流程是一样的,区别在于文件换成了 lv_port_indev_template.h 和 lv_port_indev_template.c
注释不必要的代码
这里要注意的是,LVGL 支持多种输入方式,而我们硬件只支持触摸的方式,所以我们需要先将除了触摸意外的输如配置给删掉
1 | // lv_port_indev_template.c |
初始化驱动配置
1 | /*Initialize your touchpad*/ |
判断屏幕是否被按下
1 | /*Return true is the touchpad is pressed*/ |
获取被按下位置的坐标
1 | /*Get the x and y coordinates if the touchpad is pressed*/ |
我们可以通过触控芯片判断是否被按下和获取坐标,这里为什么还需要多此一举在封装一次呢?
1.规范化,LVGL 提供了一个统一的接口这样可以避免硬件之间的差异化,
2.LVGL 本身他也有一些事件的处理机制,如果我们直接通过触控芯片去判断是否有操作,那么就相当于绕过 LVGL 的事件处理机制,他不知道屏幕上有一些操作,也就会导致UI 元件的事件回调将无法被触发,我们需要手动触发,这样就太麻烦了;
LVGL 的事件处理机制是当他检测到有操作时候,会去执行对应的回调函数,然后等到 lv_timer_handler() 执行时就会将图像缓冲区的内容显示到屏幕上,如果绕过去,他就无法判断是否有操作,那么也就无法执行回调函数,那么他就会认为图像缓冲区中的内存没有改动,他也就不会将内容显示在屏幕上;
初始化 LVGL
按照官方示例,初始化 LVGL 步骤如下:
在系统执行早期通过调用初始化 LVGL 一次 lv_init()。这需要在进行任何其他 LVGL 调用之前完成。
lv_init();
初始化您的驱动程序。
lv_port_disp_init();、lv_port_indev_init();
连接 Tick 接口。
lv_tick_inc(1)
连接显示接口。
连接输入设备接口。
lv_timer_handler() 通过每隔几毫秒调用一次来管理 LVGL 计时器,从而驱动 LVGL 时间相关任务。请参阅计时器处理程序以了解执行此操作的不同方法。
lv_timer_handler()
可选择使用 来设置主题 lv_display_set_theme()。
此后,在需要使用 LVGL 函数的源文件中 #include “lvgl/lvgl.h”。
1 | // main.c |
1.我们要使用 LVGL 图像库,必须要在使用前需要先初始化 LVGL,他会初始化其内部的数据结构和功能模块(例如,任务管理器、显示缓冲区管理等)。不初始化会导致后续的 LVGL 代码无法执行。
2.我们要将图像显示在屏幕上,并通过触屏去操控屏幕,所以需要初始化屏幕和触控的驱动,他会初始化显示缓冲区等。
3.连接 Tick 接口,因为 LVGL 有些任务是和时间挂钩的,他需要感知时间的流逝,他不像我们板子可以依靠晶振去计数时间,所以需要我们给他提供一个计数的方式(时基或者心跳), lv_tick_inc(1) 中 1 表示 1ms 也就是说这个函数调用一次就表示过去了 1 ms,LVGL 通过这种函数调用的方式来进行计数;
4.连接显示接口,LVGL 只是一个图形库,它本身并不会直接操控屏幕;为了让 LVGL 能将绘制的内容显示到屏幕上,我们需要将使用的显示驱动添加或者说注册 LVGL 中,它会将一些配置,比如缓冲区的内容、屏幕像素的大小添加进 LVGL 中;这一步在初始化屏幕时候它会自动调用,我们通常不需要显示的去手动执行;lv_disp_drv_register(&disp_drv);
5.连接输入设备接口同上,但要注意的是我们需要给输出设备提供一个回调函数用于处理 LVGL 返回的数据;
6.定期调用 lv_timer_handler() 函数,这个函数它会检查并执行所有定时器任务、动画任务,以及更新显示缓冲区并触发渲染;
移植到 FreeRTOS
移植到 FreeRTOS 就是在项目中同时导入 FreeRTOS 和 LVGL,然后将 LVGL 的时基和中断回调函数放在 FreeRTOS 的任务中去执行;
修改 stm43fxxx.h 中 __NVIC_PRIO_BITS 的值
将 __NVIC_PRIO_BITS 的值从 4U 改为 4;
__NVIC_PRIO_BITS 是一个宏,它定义了中断优先级字段的位数;FreeRTOS 会通过这个宏去适配 STM32 的优先级,让二者能够保持一致,而在 FreeRTOS 中类型(5 << (8 - 4U))这种写法他是不识别的,在以前的 ARM 编译器中他是不支持这种写法的,他会认为这是两种不同的类(常量类型和一种未定义的类型)他会认为这二者是不可以进行逻辑运算的所以我们需要修改这个宏将 4U 改为4
在当前的版本中如果不报错一般可以不用进行修改,1 是在 STM32fxxx.h 中 __NVIC_PRIO_BITS 它被定义为了 4,二是在高版本的 ARM 编译器中,他已经可以识别 4U 这种写法并会去做一些优化;
在 lv_conf.h 文件中配置自定义时钟源
在不用 FreeRTOS 之前我们是将 LVGL 的时基或者说心跳直接写在我们的main函数或者定时器中的,既然使用了 FreeRTOS 我们就需要将时基交给 FreeRTOS 去处理;
LVGL 提供了自定义时钟源的配置,可以让我们指定时基时从哪产生的
1 |
|
#define LV_TICK_CUSTOM 1,启用自定义时钟源
#if LV_TICK_CUSTOM,相当于一个开关,非 0 时执行下面的代码
#define LV_TICK_CUSTOM_INCLUDE “FreeRTOS.h”,定义一个宏指向 “FreeRTOS.h” 这个头文件,这个文件提供了 FreeRTOS 中的各种任务管理、调度器、时间管理等接口函数。
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (xTaskGetCount()),指向了 xTaskGetCount()) 这个函数,该函数用于获取系统启动以来的 tick 数(时钟节拍次数),它相当于系统的 tick 计数器;每个 tick 通常是 1 毫秒(或更小,具体由 configTICK_RATE_HZ 配置决定)。
编写 FreeRTOS 任务函数进行 LVGL 任务调度
1 | void lv_Timer_task(void* pvParameters) { |
创建一个任务专门用于 LVGL 的任务调度,这样就成功将 FreeRTOS 与 LVGL 结合在一起了;
外部 SRAM
外部 SRAM 是个硬件,当内部 SRAM 不足以存放所需的数据时,我们就需要外接一个 SRAM 用来提供额外的存储空间;
由于 LVGL 是一个图形库,涉及大量的界面渲染和控件更新,这些操作会消耗较多的内存。如果系统内部的 SRAM 不够用,尤其是在处理较大的 GUI 元素或图形时,外部 SRAM 就变得非常重要。
我们就正常把 SRAM 当成一个外设芯片,将他的驱动添加到我们的工程中去使用就可以了;
使用
将 LVGL 管理的内存空间放到外部 SRAM 中(非常不推荐,因为速度会变慢)
- 确定外部 SDRAM 首地址,根据需求确定地址偏移
- 在 lv_conf.h 中将 LV_MEN_ADR 定义到外部的 SRAM 中去(不一定是首地址)
将 绘图缓冲区放到外部 SARM 中去(如果内存不够用时候可以采取)
- 确定外部 SDRAM 首地址,根据需求确定地址偏移
- 在 lv_port_disp_template.c 中创建全屏分辨率大小的数组,并将其定位到外部 SRAM 的地址
内存管理
LVGL 内存报错
LVGL 本身渲染会占用一定的内存如果报错我们可以从以下几个角度去分析:
1.修改 lv_conf.h,适当减小分配给 LVGL 管理的内存
1 | /*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/ |
#define LV_MEM_CUSTOM 0 表示是使用自动内存管理的方式,我们不去动他;我们只需要修改 #define LV_MEM_SIZE (48U * 1024U) 定义的大小即可,要保证可以显示 LVGL 的所有小组件,但是又不能太大;可以试着一点一点去进行调试;
2.lv_port_diso_timplate.c,适当减小图像缓冲区的大小,同时需要兼顾运行效果
1 | /* Example for 1) */ |
这个空间越大,理论上运行的效果越好,我们尽量不是去给他调的太小;
3.FreeRTOSConfig.h,适当减小分配给 FreeRTOS 的内存,简单的工程一般 10~20k 就够了
1 |
这里设置减小但一般不要小于 10~20k;
错误
一、**../Middleware/LVGL/src/core../lv_conf_internal.h(41): error: ‘../../lv_conf.h’ file not found**
说明没有添加 lv_conf.h 头文件目录,添加即可;