STM32 工程移植 LVGL:一步一步完成
LVGL,作为一款强大且灵活的开源图形库,专为嵌入式系统GUI设计而生,极大地简化了开发者在创建美观用户界面时的工作。作为一名初学者,小编正逐步深入探索LVGL的奥秘,并决定记录下这段学习旅程,以便与同道中人共享心得。本文旨在于引导读者如何将LVGL图形库成功集成至STM32微控制器项目中,从零开始,步步为营,让嵌入式界面开发之旅变得更加平易近人。
小编选择了源自淘宝“魔女开发板”店铺的一款STM32F407开发板作为实践工具,其配套的丰富资源和客服技术支持为学习之路铺设了坚实的基础。特别值得关注的是,该店铺提供了一套详尽且实用的入门系列文章——【快速入门 LVGL】,首篇聚焦于如何在STM32工程中高效移植LVGL,该文详述了从环境搭建到实际应用的每一步,成为了我此次学习的重要指引。文章链接如下:
【快速入门 LVGL】-- 1、STM32 工程移植 LVGL
介绍
LVGL(Light and Variables Graphics Library)是一个免费的开源图形库,提供了创建具有易于使用的图形元素、优美的视觉效果和低内存占用的嵌入式GUI所需的一切。它用C语言编写,以实现最大的兼容性(与C ++兼容),模拟器可在没有嵌入式硬件的PC上启动嵌入式GUI设计。
1. 准备工作
首先,你需要以下几样东西:
- STM32CubeMX:用于配置 STM32 的硬件特性。
- STM32 HAL 库:STM32 的硬件抽象层库,用于简化硬件操作。
- LVGL 库:你可以在 LVGL 的 GitHub 页面下载最新版本。
STM32工程的要求
1️⃣设置堆栈大小:Heap、Stack,设置为:0x1000
2️⃣准备好你使用开发平台所用屏幕的驱动函数BSP(一般屏幕商家会提供)
- 画点函数,用于后面注册LVGL的显示功能
- 触摸检测函数 (返回:0-未按下、1-按下)、坐标获取函数,用于注册LVGL的触屏功能
2. 下载 LVGL
浏览器搜索 lvgl git
虽然LVGL已发布了v9.0、v9.1等,但v8.3版,是目前最广泛使用的版本。
它拥有众多的网上教程资源,使开发者能够轻松地学习和使用LVGL。而且多款主流的可视化设计工具(如SimVis Designer和Qt Design Studio)都支持LVGL的v8.3版本。v9.0、v9.1等版本由于其相对较新,相关资源和可视化设计工具的支持可能相对较少。
因此这里建议下载v8.3版
可以选择用git clone到本地或者直接下载zip
下载完成
3. LVGL源文件裁剪
我们所下载的LVGL源文件并不是所有文件都要加入到我们的工程中,我们首先要进行裁剪
我们只需要下面这五个文件
1️⃣将这五个文件复制一份作为模板,方便后面的移植
2️⃣打开模板中的examples文件夹,将porting文件夹以外的全部文件删除
删除后👇
3️⃣修改 porting 里面的文件名称
由于LVGL源代码中的头文件,使用了相对路径,如在 “lvgl.h” 中
#include "src/misc/lv_log.h"
#include "src/misc/lv_timer.h"
#include "src/misc/lv_math.h"
#include "src/misc/lv_mem.h"
#include "src/misc/lv_async.h"
#include "src/misc/lv_anim_timeline.h"
#include "src/misc/lv_printf.h"
所以我们在构造工程是也要使用相同目录结构
打开 “porting” 文件夹,修改里面的文件名,将 “_template” 删除
修改后如下
4️⃣ 修改 lv_conf.h 文件名
回到 “LVGL” 文件夹中,lv_conf_template.h
,是LVGL配置参数的重要文件,同样将它修改为: lv_conf.h
修改完成后如下
4. 将LVGL 添加到STM32工程
1️⃣将我们创建的 lvgl 文件夹,粘贴到STM32工程目录下
2️⃣打开Keil5,在工程里,添加4个文件夹
文件夹名称 | 文件类型 |
---|---|
LVGL_myGui | 用户自己的界面代码文件、官方demo等 |
LVGL_conf | LVGL 的两个h文件 |
LVGL_porting | LVGL 的接口文件, 如显示、触摸屏、键盘等 |
LVGL_src | LVGL 的所有底层c文件 |
操作如下👇
3️⃣为每一个文件夹组,添加需要的文件
文件夹 (Group) | 添加文件 |
---|---|
LVGL_myGUI | 不用添加。 |
LVGL_conf | lv_conf.h , lvgl.h |
LVGL_porting | lv_port_disp.c , lv_port_disp.h , lv_port_indev.c , lv_port_indev.h |
LVGL_src | 所有 .c 文件位于 lvhl/src 及其所有子文件夹下 |
🚨注意src文件夹下,会有多重的子文件夹,需要把每一个子文件夹的C文件全部添加进来
4️⃣打勾C99
5️⃣添加头文件路径
配置项 | 具体内容 |
---|---|
路径1 | LVGL 文件夹路径 |
路径2 | LVGL\src 文件夹路径 |
路径3 | LVGL\examples\porting 文件夹路径 |
添加方法如下
5. 注册输出设备
1️⃣打开 lv_conf.h,对第15行预编译#if 0
进行修改👇,将0改为1,以启用此文件
同样的方法启用LVGL_porting下的 lv_port_disp.h
,打开 lv_port_disp.h,进行如下修改
行号 | 原内容 | 修改后内容 |
---|---|---|
7 | #if 0 | #if 1 |
22 | lvgl/lvgl.h | lvgl.h |
同样启用 lv_port_disp.c
行号 | 原内容 | 修改后内容 |
---|---|---|
7 | #if 0 | #if 1 |
12 | "lv_port_disp_template.h" | "lv_port_disp.h" |
2️⃣添加 LCD 驱动的头文件
同样在 lv_port_disp.c
中
-
插入LCD驱动文件
- 位置: 第14行之后
- 操作: 插入您的LCD驱动头文件
-
设置显示屏宽度和高度
- 位置: 第20行 & 第25行(或根据实际代码结构调整)
- 操作: 根据您的显示屏参数,替换现有的宽度和高度定义
插入LCD驱动头文件 ,确保您的项目可以访问LCD的绘图功能,特别是画点函数。
🚨留意LVGL的横屏默认设置 LVGL库默认以横向模式布局,这意味着宽度对应水平像素,高度对应垂直像素。
3️⃣选择创建缓存的方式
在lv_port_disp.c
文件中,您需要配置LVGL的显示缓冲区。面对提供的三种创建缓冲区的选项,您需要选择其中一种方式。通常推荐选择第一种方案,特别是对于大多数基本应用场景。
定位到相关代码段:首先,在文件中找到第86行至101行附近的内容。这部分代码涉及到显示缓冲区的配置。
选择第一种方法:这通常是创建单个缓冲区的方法,适用于多数场景。确认第86行附近的代码(第一种缓冲区创建方式)是未被注释的,形如:
/* Example for 1) */
static lv_disp_draw_buf_t draw_buf_dsc_1;
static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
这段代码初始化了一个缓冲区,名为buf_1
,并分配了足够的内存空间来存储屏幕一行或多行的像素数据。
注释掉其他方法:为了明确选择第一种方式,需要将描述第二和第三种缓冲区创建方式的代码行(大约在第90行到101行之间)进行注释处理。注释后如下
4️⃣关联 画点函数
同样在lv_port_disp.c
文件里
假设您的LCD驱动中画点函数的原型为
void My_LCD_DrawPixel(int x, int y, uint16_t color);
我们需要将其与LVGL的显示刷新回调函数168行的disp_flush()
关联起来
替换画点函数 在disp_flush
函数内部,LVGL通过调用一个画点函数来实际在屏幕上绘制每个像素。你需要将此部分代码替换为我们自己的的LCD驱动中的画点函数调用。
修改后如下
6. 注册触摸屏
1️⃣启用 “lv_port_indev.h”
在lv_port_indev.h中修改下面两行
行号 | 原始代码 | 修改后代码 |
---|---|---|
8 | #if 0 | #if 1 |
20 | "lvgl / lvgl.h" | "lvgl.h" |
2️⃣启动 “lv_port_indev.c”
打开"lv_port_indev.c", 修改以下内容
行号 | 原内容 | 修改后内容 |
---|---|---|
7 | #if 0 | #if 1 |
12 | "lv_port_indev_template.h" | "lv_port_indev.h" |
13 | "../../lvgl.h" | "lvgl.h" |
3️⃣添加 触屏 的驱动头文件
同样在"lv_port_indev.c"文件下第14行,插入:#include “触摸屏的头文件”
4️⃣注释掉不需要的输入任务注册’
在文件 “lv_port_indev.c” 中,你还需要对输入设备初始化函数 lv_port_indev_init()
进行调整,以便仅启用触摸屏输入而禁用其他类型的输入设备注册。以下是具体的操作:
-
定位函数:首先,找到文件中的
lv_port_indev_init()
函数,它通常位于文件的中后部,大约在第70行左右。 -
识别并注释:在此函数内部,您会看到为不同输入设备(如触摸屏、鼠标、键盘、编码器和物理按键)注册处理任务的代码段。为了仅保留触摸屏输入,你需要:
-
保留:触摸屏输入相关的注册代码,这部分是我们希望保持活跃的部分。
-
注释掉:鼠标、键盘、编码器、物理按键等其它输入设备的注册代码。
-
示例修改:
5️⃣添加触摸检测函数
在"lv_port_indev.c"文件中,为了添加触屏检测功能,你需要对原有的触摸检测函数touchpad_is_pressed()
进行如下👇修改,以使用特定的触屏检测函数。:
// 假设原先的 touchpad_is_pressed 函数位于约209行
bool touchpad_is_pressed(void) {
// 第212行:插入魔女开发板提供的触屏检测函数
// 注意:这里使用XPT2046_IsPressed()是魔女开发板提供的外部函数
// 返回值已经是 0 代表未按下,1 代表已按下,符合LVGL要求
return XPT2046_IsPressed();
// 第213行:原有的 return false 应该被注释掉或直接删除
// 注释如下:
// // return false;
}
// 其余代码...
如果你使用的是其他开发板或库(比如原子哥的STM32库),并且该库提供了不同的触屏检测接口,您只需将XPT2046_IsPressed()
替换为对应的触屏检测函数名称,并确保该函数返回值逻辑与LVGL所需的逻辑相匹配(即0表示未按下,非0值表示按下)。
例如,如果使用原子哥的库且触屏检测函数名为TP_IsPress()
,并且它的返回值是1表示按下,0表示未按下,则代码应调整为:
bool touchpad_is_pressed(void) {
// 使用原子哥STM32库的触屏检测函数
return TP_IsPress();
}
🚨确保所做的修改符合您实际使用的硬件接口和函数逻辑。
6️⃣添加坐标获取函数
在"lv_port_indev.c"文件中,为了集成触屏坐标获取功能,你还需要修改坐标获取函数touchpad_get_xy()
,确保LVGL能够正确读取到触摸点的坐标。
void touchpad_get_xy(lv_coord_t *x, lv_coord_t *y) {
// 第221行:修改为调用魔女开发板提供的获取X坐标的函数
(*x) = XPT2046_GetX(); // XPT2046_GetX()为魔女开发板中实现了X坐标获取的方法
// 第222行:修改为调用获取Y坐标的函数
(*y) = XPT2046_GetY(); // 同样地,XPT2046_GetY()用于获取Y坐标
// 根据不同库或硬件,您可能需要调整这里的函数调用,确保它们返回的是当前触摸点的实际坐标值。
}
这段代码修改后,LVGL框架将能够通过调用touchpad_get_xy()
来获取触屏上每次触摸事件的坐标信息。请注意,XPT2046_GetX()
和XPT2046_GetY()
函数需要替换为是你硬件平台上正确的实现,用于读取触摸屏控制器坐标值。
7. 添加LVGL的文件引用
现在,我们已成功调整了LVGL显示和触摸功能相关的代码,接下来的步骤是将LVGL的功能实际整合到项目中,即在工程中“激活”LVGL,使其发挥作用。
1️⃣头文件包含:
在主程序或相应的配置文件中,确保包含了必要的LVGL头文件。这通常涉及lvgl.h
及其他之前配置的特定头文件,比如lv_port_disp.h
和lv_port_indev.h
。
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
2️⃣ 初始化LVGL:
在项目启动初期,调用LVGL的初始化函数,设置显示驱动和输入设备。这些在主循环开始前完成:
lv_init(); // 初始化LVGL库
lv_port_disp_init(); // 初始化显示驱动
lv_port_indev_init(); // 初始化输入设备
3️⃣初始化LCD、触摸屏
同样在main函数内、 while 循环之前,调用LCD初始化函数、触摸屏初始化函数
LCD_Init(); // 初始化 LCD
LCD_SetDir(1); // 设置LCD的显示方向:横屏
XPT2046_Init(xLCD.width, xLCD.height, xLCD.dir); // 初始化触摸屏
4️⃣ 创建及管理UI元素:
开始创建图形界面元素,如按钮、标签、滑块等。这通常涉及到使用LVGL的API来定义和控制界面组件:
static lv_obj_t *label = lv_label_create(lv_scr_act(), NULL); // 创建一个标签
lv_label_set_text(label, "Hello, LVGL!"); // 设置标签文本
5️⃣主循环集成LVGL任务处理:
在您的主循环或任务调度中,定期调用LVGL的任务处理函数,以确保UI的实时响应和更新:
while(1) {
// 处理其他任务...
// 更新LVGL任务
lv_task_handler(); // 或使用特定的调度函数,如lv_tick_inc()
// 确保系统休眠或延时,避免CPU过载
HAL_Delay(1 - 1);// 1ms延时
}
8. LVGL 心跳、任务刷新
LVGL图形库为了保证用户界面的流畅更新和响应,引入了心跳机制(Heartbeat)和任务刷新的概念。这些机制确保了LVGL能够及时处理图形渲染、输入事件和动画更新等任务。以下是关于如何在项目中正确实现LVGL心跳与任务刷新的基本指南:
心跳机制(Heartbeat)
LVGL的心跳机制主要通过周期性的调用来维持,它负责触发内部的任务调度,从而更新UI元素和处理后台任务。心跳频率影响着UI的流畅度和响应速度,一般推荐的频率为每秒大约60次(即大约每16毫秒一次)。
实现方法:
使用系统定时器:在嵌入式系统中,可以通过配置硬件定时器中断来定期触发心跳。定时器中断服务例程(ISR)中调用lv_tick_inc()
函数来模拟心跳。
🚨但要注意在很多实时操作系统(RTOS)环境中,SysTick定时器通常被操作系统本身用于基本的时间管理,比如任务调度和时间片分配。因此,直接用SysTick来驱动LVGL心跳可能会与RTOS的核心功能冲突,或者限制系统的灵活性和可扩展性。
为了实现高精度的定时控制,建议利用TIM机制生成1毫秒的周期性中断,并将此中断配置为高级别优先级。通过在中断服务程序中触发LVGL心跳时钟,确保用户界面的实时更新。实现这一功能时,可灵活选择TIM外设及编程手段,涵盖直接操作寄存器、采用STM32标准库或是手动集成HAL库等不同策略。本文照仿【快速入门 LVGL】-- 1、STM32 工程移植 LVGL 将以CubeMX工具为例,配置TIM6以实现每1毫秒的精确中断调度。
1️⃣打开CubeMX并选择您的STM32型号
- 启动STM32 CubeMX软件,选择或创建您的项目,指定目标STM32微控制器型号。
2️⃣配置TIM6
-
启用TIM6: 在“Pinout & Configuration”页面左侧的外设列表中找到
TIM6
,点击展开。 -
基本定时器配置:
- 设置
Prescaler(预分频器)
以得到1ms的周期。例如,如果您的STM32运行在84MHz(这是很多STM32的默认频率),您需要设置预分频器为PSC = 84-1
(因为 84000000 / 84- 1 = 100kHz,即每1us触发一次更新事件)。 - 设置
Counter Mode(计数模式)
为Up
,意味着计数器从0递增到自动重载值(Autoreload
)。 - 设置
Autoreload(自动重载)
值为1000-1
,配合上述预分频设置达到1ms中断周期。
- 设置
3️⃣设置中断优先级
转到Configuration
-> NVIC Settings
,在中断列表中找到TIM6
,点击它并设置优先级为高。您可以根据系统中的其他中断需求调整具体的优先级数值。
确保Interrupt(更新中断)
被勾选,这样每当计数器达到自动重载值时就会产生中断。
4️⃣生成代码
点击Project Manager
,然后Generate Code
,选择您偏好的IDE和工程类型,让CubeMX生成代码。
5️⃣编写中断服务例程
在生成的代码中,找到stm32f4xx_hal_tim.c
文件,里面会有一个空的HAL_TIM_PeriodElapsedCallback
函数,这是一个弱定义的函数,需要我们重写。
我们将这个函数实现,放到main.c的最后
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6) // 判断是哪个TIM产生的中断
{
// 在这里调用LVGL的心跳函数
lv_tick_inc(1); // 给LVGL提供1ms的心跳时期
}
}
6️⃣主函数中使能TIM6
确保在main.c
或相应的主函数中,有代码初始化并使能TIM6
HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT(&htim6); // 使用中断模式启动TIM6
🎊🎉🎉🎉🎉🎉🎉🎉🎉🎉🎊
恭喜你至此,关于时间精度的需求已成功解决,LVGL图形库的移植工作也圆满结束。激动人心的时刻来临,点击编译按键后,结果显示令人满意——0个错误!这意味着我们的移植不仅完成了,而且实现了无缝对接与优化。