打印
[应用相关]

STM32 上使用 LVGL

[复制链接]
3114|3
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
我将使用2.8英寸TFT ILI9341 SPI显示器和意法半导体的STM32L496核板。本系列的教程将更倾向于 LVGL,因此无论您使用什么显示器,您都应该拥有它的库。

第 1 步:- 创建一个新项目
我们将首先创建一个新项目并设置 SPI。由于我们在本教程中没有实现触摸,因此只需 1 个 SPI 设置就足以用于显示。

下面是LCD和控制器板之间的连接。



如上图所示,除了 SPI 引脚外,还有 3 个主要引脚用于 LCD、CS、RESET 和 DC。这 3 个引脚必须设置为 MCU 的输出。

CubeMX 设置如下所示。


我们将SPI设置为8位模式,波特率为40Mbps。SPI 发送 DMA 在正常模式下启用,数据宽度设置为 Byte。

此外,请确保启用 DMA 的中断。

除了 SPI 之外,我们还必须设置另外 3 个引脚作为输出。它们如下所示。




使用特权

评论回复
沙发
mintspring|  楼主 | 2024-3-18 22:40 | 只看该作者
第 2 步:- 添加和配置 LVGL 目录
我们将从他们的 Github 下载 LVGL 8.3 版。使用旧版本的原因是我们将使用 Squareline Studio 进行 UI 开发,并且它仍然使用 8.3 版本作为最新版本。
https://github.com/lvgl/lvgl/archive/refs/heads/release/v8.3.zip
从 zip 中解压缩文件夹后,将其重命名为 lvgl 并复制到 STM32 项目文件夹>驱动程序中。如下所示


现在从 lvgl 文件夹中复制 lv_conf_template.h,将其粘贴到 lvgl 文件夹旁边,并将其重命名为 lv_conf.h。如下图所示。

现在,在多维数据集 IDE 中刷新项目,您将能够在 Drivers 目录中找到 lv_conf.h。打开此文件并将 #if 0 更改为 #if 1,以便将该文件包含在项目中。

右键单击项目并打开“属性”。
打开 c/c++ build -> settings -> MCU GCC Compiler -> include paths。在这里,单击添加按钮以包含项目的 lvgl 路径。


在弹出的窗口中选择工作区,找到我们刚刚添加的 lvgl 文件夹,点击确定添加路径。

使用特权

评论回复
板凳
mintspring|  楼主 | 2024-3-18 22:43 | 只看该作者
第 3 步:- 将显示驱动程序连接到 LVGL
首先将 LCD 库文件复制到项目文件夹中,然后创建新文件 LCDController.c 和 LCDController.h。这些是我们将LCD库连接到LVGL的文件。包含新文件的项目结构如下所示。

显示端口模板可在 https://github.com/lvgl/lvgl/blob/release/v8.3/examples/porting/lv_port_disp_template.c 找到。我们可以根据MCU和显示器的需要和可用性简单地修改此模板。
lv_port_disp_init 函数用于初始化 LVGL 的显示和显示驱动程序。
void lv_port_disp_init(void)
{
    disp_init();
它里面的第一个函数是 disp_init(),用于初始化显示。在这里,您需要调用显示器的初始化函数。
static void disp_init(void)
{
    ILI9341_Init();
}
在我正在使用的ILI9341库中,函数 ILI9341_Init() 用于初始化它。这就是为什么我在 disp_init() 函数中调用它的原因。

接下来,我们将定义并初始化显示器的绘制缓冲区。可以使用单个绘制缓冲区或两个绘制缓冲区。请记住,您定义的缓冲区越大,它占用的内存就越多。因此,您必须在性能与空间之间做出明智的选择。

一个缓冲区
如果仅使用一个缓冲区,则 LVGL 会将屏幕内容绘制到该绘制缓冲区中,并将其发送到显示器。然后,LVGL 需要等到缓冲区的内容发送到显示器,然后才能在其中绘制新内容。我们可以这样定义它:
 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*/
在上面的代码中,我们定义了一个缓冲区,它可以保存 10 行的数据。这意味着显示屏上一次将刷新 10 行。我们可以通过一次绘制一个像素或绘制位图来简单地将此数据发送到显示器。

两个缓冲器
如果使用两个缓冲区,LVGL 可以绘制到一个缓冲区中,而另一个缓冲区的内容将发送到后台的显示器。应使用 DMA 或其他硬件将数据传输到显示器,以便 MCU 可以继续绘图。这样,显示器的渲染和刷新就变成了并行操作。我们可以定义 2 个缓冲区,如下所示:
static lv_disp_draw_buf_t draw_buf_dsc_2;
    static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10];                        /*A buffer for 10 rows*/
    static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10];                        /*An other buffer for 10 rows*/
    lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);   /*Initialize the display buffer*/
在上面的代码中,我们定义了两个缓冲区,每个缓冲区可以保存 10 行的数据。当使用 DMA 将一个缓冲器刷新到显示器时,LVGL 可以使用新数据更新另一个缓冲区。

定义并初始化缓冲区后,我们将设置显示驱动程序的其余参数。
lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/

    disp_drv.hor_res = MY_DISP_HOR_RES;
    disp_drv.ver_res = MY_DISP_VER_RES;

    disp_drv.flush_cb = disp_flush;

    disp_drv.draw_buf = &draw_buf_dsc_2;

    lv_disp_drv_register(&disp_drv);
}
这里首先,我们将初始化显示驱动程序。然后设置显示分辨率,该分辨率在文件开头定义。
设置刷新回调函数,当 LVGL 准备好将内容刷新到显示器时,LVGL 将调用该函数。我们稍后会编写这个函数。
然后设置我们上面定义的绘制缓冲区(一个缓冲区或 2 个缓冲区)。
最后,注册显示驱动程序。

使用特权

评论回复
地板
mintspring|  楼主 | 2024-3-18 22:46 | 只看该作者
现在我们将编写显示刷新函数。当 LVGL 准备好将内容刷新到显示器时,LVGL 会调用此函数。这是此文件最重要的功能,因为它包含将数据发送到显示器的方法。
在本教程中,我们将仅介绍绘制位图和使用 DMA 绘制位图的简单方法。

不带 DMA
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
  ILI9341_SetWindow(area->x1, area->y1, area->x2, area->y2);

  int height = area->y2 - area->y1 + 1;
  int width = area->x2 - area->x1 + 1;

  ILI9341_DrawBitmap(width, height, (uint8_t *)color_p);
  lv_disp_flush_ready(disp_drv);
}
我们首先调用函数 setwindow 来设置需要修改的区域。
然后计算需要修改的区域的高度和宽度
然后调用 DrawBitmap 函数将数据发送到上述函数中设置的区域。
将数据传输到显示器后,调用函数lv_disp_flush_ready以通知 LVGL 我们已准备好进行 ew 传输。
在这里,DrawBitmap 函数在阻塞模式下通过 SPI 将数据发送到显示器。下面是此函数在 ILI9341 库中的实现。
void ILI9341_DrawBitmap(uint16_t w, uint16_t h, uint8_t *s)
{
        LCD_WR_REG(0x2c);
        DC_H();
        ConvHL(s, (int32_t)w*h*2);
        HAL_SPI_Transmit(&hspi1, (uint8_t*)s, w*h*2, HAL_MAX_DELAY);
}
由于数据是在阻塞模式下发送的,因此该函数仅在传输完所有数据后才会退出。因此,在 DrawBitmap 函数之后立即调用函数lv_disp_flush_ready是有意义的。


使用 DMA

如果我们使用 2 个缓冲区来提高性能,使用 DMA 发送数据将帮助我们获得良好的性能。以下是使用 DMA 发送数据的实现。
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
          ILI9341_SetWindow(area->x1, area->y1, area->x2, area->y2);

          int height = area->y2 - area->y1 + 1;
          int width = area->x2 - area->x1 + 1;

          ILI9341_DrawBitmapDMA(width, height, (uint8_t *)color_p);
}

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
        lv_disp_flush_ready(&disp_drv);

}

设置窗口和计算高度和宽度的初始过程在这里是相同的。唯一的变化是,我们不再调用 DrawBitmap,而是调用函数 DrawBitmapDMA。此函数使用 DMA 发送数据,而不是阻塞模式 SPI 传输。
DMA 在后台将数据发送到 SPI,而无需使用 anu CPU。同时,LVGL 将使用新数据更新另一个缓冲区。DMA 完成传输后,将触发中断并调用 TxCpltCallback。
在此回调中,我们将调用函数 lv_disp_flush_ready 来通知 LVGL 我们已准备好刷新另一个缓冲区。整个系统的工作方式是刷新和呈现变得并行。
下面是 ILI9341 源文件中 DrawBitmapDMA 函数的实现。
void ILI9341_DrawBitmapDMA(uint16_t w, uint16_t h, uint8_t *s)
{
        LCD_WR_REG(0x2c);
        DC_H();
        ConvHL(s, (int32_t)w*h*2);
        HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)s, w * h *2);
正如你所看到的,唯一的变化是数据是通过SPI DMA发送的,而不是使用阻塞模式发送的。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

296

主题

4894

帖子

24

粉丝