STM32 工程移植 LVGL:一步一步完成

avatar
作者
筋斗云
阅读量:0

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_confLVGL 的两个h文件
LVGL_portingLVGL 的接口文件, 如显示、触摸屏、键盘等
LVGL_srcLVGL 的所有底层c文件

操作如下👇

在这里插入图片描述

3️⃣为每一个文件夹组,添加需要的文件

文件夹 (Group)添加文件
LVGL_myGUI不用添加。
LVGL_conflv_conf.h, lvgl.h
LVGL_portinglv_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️⃣添加头文件路径

配置项具体内容
路径1LVGL 文件夹路径
路径2LVGL\src 文件夹路径
路径3LVGL\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
22lvgl/lvgl.hlvgl.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

  1. 插入LCD驱动文件

    • 位置: 第14行之后
    • 操作: 插入您的LCD驱动头文件
  2. 设置显示屏宽度和高度

    • 位置: 第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() 进行调整,以便仅启用触摸屏输入而禁用其他类型的输入设备注册。以下是具体的操作:

  1. 定位函数:首先,找到文件中的 lv_port_indev_init() 函数,它通常位于文件的中后部,大约在第70行左右。

  2. 识别并注释:在此函数内部,您会看到为不同输入设备(如触摸屏、鼠标、键盘、编码器和物理按键)注册处理任务的代码段。为了仅保留触摸屏输入,你需要:

    • 保留:触摸屏输入相关的注册代码,这部分是我们希望保持活跃的部分。

    • 注释掉:鼠标、键盘、编码器、物理按键等其它输入设备的注册代码。

示例修改:
在这里插入图片描述
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.hlv_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

  1. 启用TIM6: 在“Pinout & Configuration”页面左侧的外设列表中找到TIM6,点击展开。

  2. 基本定时器配置:

    • 设置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个错误!这意味着我们的移植不仅完成了,而且实现了无缝对接与优化。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!