eefocus_3891719 发表于 3 天前

【Avnet | NXP FRDM-MCXN947试用活动】评测5--移植 LVGL跑benchmark

本帖最后由 eefocus_3891719 于 2024-11-19 10:53 编辑

# 背景

上一个帖子以 FLEXIO_SPI 驱动了 TFT LCD,此贴移植 LVGL,测试刷屏速度。

# LVGL 移植

## 1. 引入 SDK 组件

在 MCUXpresso For VS Code 开发环境中,引入 NXP SDK 组件的方法很简单,如下图所示:

!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104526vp3w6dgk4g3wuy6z.png)

1. 在 VS Code 侧边栏打开 MCUXpresso 插件图标,点击打开;
2. 鼠标右键单击项目,展开选项;
3. 选择 **Configure** 展开子菜单栏;
4. 选择 **Manage Components** 弹开组件选择窗口。

在弹出的组件选择框中查找或者输入 **lvgl** 并勾选,导入 LVGL 组件,如下图所示。

!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104539iegnamkehwekedal.png)

需要注意此方法导入的 SDK 组件并不会把源码拷贝到当前工程目录中,只是修改了文件 **armgcc/config.cmake** 文件,如下:

```shell
set(CONFIG_USE_xxxx    true) # 其他组件

# 引入 LVGL 时添加的两行
set(CONFIG_USE_middleware_lvgl true)
set(CONFIG_USE_middleware_lvgl_template true)
```

## 2. 添加 LVGL 显示适配层

参考 LVGL 官方的移植文档, LVGL 移植主要干以下两件事:

1. LVGL 初始化,LCD 硬件初始化;
2. LVGL 刷新缓冲区接口的实现;

### 1. 新增文件

新增3个文件,如下所示:

```shell
bsp/lvgl_port/
      lv_conf.h
      lvgl_support.c
      lvgl_support.h
```

- lv_conf.h 是 LVGL 配置头文件,配置选项非常多,这里仅介绍几个重要的配置选项:`LV_COLOR_DEPTH` 设置颜色深度;`LV_MEM_SIZE` 设置 LVGL 内部使用的内存分配池的大小;`LV_USE_XXX` 使能widgets组件;`LV_BUILD_EXAMPLES` 允许编译内置的示例到 LVGL 库文件中;
- lv_support.c 是适配接口实现文件;
- lv_support.h 是适配接口对外的接口文件,包括宏定义、函数声明等;

### 2. lv_port_disp_init()

此函数做了以下几件事:

1. 初始化 LVGL 显示缓冲区内存,调用 `lv_disp_draw_buf_init()`;
2. 初始化 LCD 硬件,但是已经在外部调用 `LCD_init()`,此处无需调用;
3. 注册 `disp_drv.flush_cb = DEMO_FlushDisplay`,需要实现自己的 `DEMO_FlushDisplay()` 函数;
4. 最后注册显示驱动 `lv_disp_drv_register(&disp_drv)`

```c
void lv_port_disp_init(void)
{
    static lv_disp_draw_buf_t disp_buf;

    memset(s_frameBuffer, 0, sizeof(s_frameBuffer));
    lv_disp_draw_buf_init(&disp_buf, (void *)s_frameBuffer, (void *)s_frameBuffer, LCD_VIRTUAL_BUF_SIZE);

    /*-------------------------
   * Initialize your display
   * -----------------------*/
        // NOTE: 已在其他位置调用 LCD_Init() ,此处无需调用

    /*-----------------------------------
   * Register the display in LittlevGL
   *----------------------------------*/

    static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);   /*Basic initialization*/

    /*Set up the functions to access to your display*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = LCD_WIDTH;
    disp_drv.ver_res = LCD_HEIGHT;

    /*Used to copy the buffer's content to the display*/
    disp_drv.flush_cb = DEMO_FlushDisplay;

    /*Set a display buffer*/
    disp_drv.draw_buf = &disp_buf;

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}
```

### 3. lvgl 刷新缓冲区接口的实现

在 `bsp/lvgl_port/lvgl_support.c` 中实现如下函数,最终调用 LCD 中的函数 `LCD_DrawBitmap()` 实现把LVGL缓冲区内容刷新到屏幕上。

```c
/* Flush the content of the internal buffer the specific area on the display
* You can use DMA or any hardware acceleration to do this operation in the background but
* 'lv_flush_ready()' has to be called when finished
* This function is required only when LV_VDB_SIZE != 0 in lv_conf.h*/
static void DEMO_FlushDisplay(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
    lv_coord_t x1 = area->x1;
    lv_coord_t y1 = area->y1;
    lv_coord_t x2 = area->x2;
    lv_coord_t y2 = area->y2;

    int32_t length = (x2 - x1 + 1) * (y2 - y1 + 1) * LCD_FB_BYTE_PER_PIXEL;

        LCD_DrawBitmap(x1, y1, x2, y2, (uint16_t *)color_p);

        /* IMPORTANT!!!
   * Inform the graphics library that you are ready with the flushing*/
    lv_disp_flush_ready(disp_drv);
}
```

`LCD_DrawBitmap()` 函数实现非常重要,如果速度慢导致卡顿,如果速度快则可以流畅地刷屏。在调试过程中实现了两个版本:

1. 逐个打点,调用 `LCD_WriteData_16Bit()`,刷屏卡成PPT;
2. 利用 EDMA 直接发送整个缓冲区,速度很快,达到 31FPS;

```c
void LCD_DrawBitmap(uint16_t xstart, uint16_t ystart, uint16_t xend, uint16_t yend, uint16_t *color)
{
        LCD_SetWindows(xstart, ystart, xend, yend);

        uint16_t width = xend - xstart + 1;
        uint16_t height = yend - ystart + 1;
        uint16_t size = width * height;

#if 0
        // NOTE: 逐个打点,非常慢--卡成 PPT
        for (uint16_t i = ystart; i <= yend; i++) {
                for (uint16_t j = xstart; j <= xend; j++) {
                        Lcd_WriteData_16Bit(*color);
                        color++;
                }
        }
#else
        // NOTE: EDMA 发送一个缓冲帧,速度很快 -- 31FPS
        Lcd_WriteData_16BitArray(color, size);
#endif
}
```

#### EDMA 发送整个缓冲帧的实现

逐个打点的速度太慢,这里就不展示了。在移植LCD驱动时就发现刷屏非常慢,迫切地需要实现一个利用EDMA发送缓冲区地函数,在LVGL刷屏时正好可以用到。

函数 `LCD_WriteData_16BitArray()` 如下,利用 EDMA 的特性,一下子传输整个缓冲区比逐个打点快很多。

这里遇到了一个坑,LVGL 配置颜色深度为16bit,一个像素点占据两个字节,写代码时脑子短路了,知道这个细节,但是在配置 EDMA 传输参数时没有反映过来,一开始屏幕没有显示,后来 debug 才发现 EDMA 传输启动就卡主了,原来下面的 `xfer.dataSize` 配置需要注意 `size * 2`。

```c
/**
* @brief SPI 发送一个数组
*
* @param Data
* @param size
*/
void Lcd_WriteData_16BitArray(uint16_t *Data, uint32_t size)
{
        flexio_spi_transfer_t xfer = { 0 };

        LCD_CS_CLR;
        LCD_RS_SET;

        xfer.txData = (uint8_t *)Data;
        xfer.rxData = NULL;
        xfer.dataSize = size * 2; // NOTE: 16bit颜色,一个像素点占据2个字节 (这里差点坑哭了!!!)
        xfer.flags = kFLEXIO_SPI_16bitMsb;

        FLEXIO_SPI_MasterTransferCreateHandleEDMA(&spiDev, &g_spiHandle,
                spi_master_completionCallback, NULL, &txHandle, &rxHandle);
        FLEXIO_SPI_MasterTransferEDMA(&spiDev, &g_spiHandle, &xfer);
        while(!completeFlag);
        completeFlag = false;

        LCD_CS_SET;
}
```

## 3. 创建 LVGL 任务

改造上一版的程序,把 LCD 和 LVGL 初始化拎出来放到一个单独的 GUI Task 中,如下所示:

```c
/**
* @brief LVGL Core 线程
*
* @param pvParameters
*/
static void gui_task_entry(void *pvParameters)
{
        lv_port_pre_init();
        lv_init();
        lv_port_disp_init();
        lv_port_indev_init();

        s_lvgl_initialized = true;

        lv_demo_benchmark();

        while (1) {
                lv_task_handler();
                vTaskDelay(pdMS_TO_TICKS(5));
        }
}


void gui_task_create(void)
{
        // LCD init
        LCD_Init();

        // LVGL init

        if (xTaskCreate(gui_task_entry, "gui_task",
                ZYGOTE_TASK_STACK_SIZE, NULL, ZYGOTE_TASK_PRIORITY, NULL) != pdPASS)
        {
                PRINTF("Task creation failed!.\r\n");
                while (1);
        }
}
```

## 4. 添加 LVGL 心跳

上面的 `gui_task_entry()` 在死循环中每隔5毫秒进入一次 lvgl 任务,但是还没有更新 LVGL 心跳,是不会刷新屏幕的。

当前使用了 FreeRTOS ,一个简单的做法是把 LVGL 心跳放到 `vApplicationTickHook()` 中,非常简单快捷。但是需要注意,需要在 `FreeRTOSConfig.h` 中使能宏定义 `configUSE_TICK_HOOK = 1` 才能使这个 `vApplicationTickHook()` 函数生效。

```c
/*!
* @brief FreeRTOS tick hook.
*/
void vApplicationTickHook(void)
{
    if (s_lvgl_initialized)
    {
      lv_tick_inc(1);
    }
}
```

## 5. 编译

#### 编译出错 undefined reference to `lv_demo_benchmark'

##### 第一反映是查看 `lv_conf.h` 文件

1. `LV_BUILD_EXAMPLES` 宏定义是否启用了;
2. `LV_USE_DEMO_BENCHMARK` 宏定义是否启用了

!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104608g4caa9wlez347e74.png)

检查过了,确认过来,启用了,但是还是**链接失败,最后不到 lv_demo_benchmark 符号**。

##### 找到 SDK 的 cmake 模块文件

找到 SDK 的 cmake 文件,如下 lvgl 拆分成了多个模块文件:

- middleware_lvgl.cmake 是 lvgl 核心组件的cmake模块文件
- middleware_lvgl_unused_files.cmake 其实是一个没有什么实际意义的cmake模块文件
- middleware_lvgl_demo_widgets.cmake 是把 lvgl demo widgets 示例的源码加入编译;
- middleware_lvgl_demo_stress.cmake 是把 lvgl demo stress 示例的源码加入编译;
- middleware_lvgl_demo_benchmark.cmake 是把 lvgl demo benchmark 示例的源码加入编译;
- middleware_lvgl_template.cmake 是把 lvgl_sdk 目录下的 **lvgl_support.c** 等三个文件添加到编译中;

!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104619n3s53t3ffmqma3fc.png)

这里就发现了 MCUXPresso for VS Code 的两个漏洞:

1. SDK 组件管理器中添加了 LVGL 组件,但是没有自动把其中的 3 个 demo 相关的模块加入到源码中,
2. 虽然把 `middleware_lvgl_template.cmake` 所在的组件加了进来,但是并没有把源码拷贝过来,难道需要用户手动去改 SDK 目录下的 lvgl_sdk 目录?可以是这样一改会对所有的依赖此SDK的工程都产生影响?
3. middleware_lvgl.cmake 模块,居然依赖于 middleware_lvgl_template.cmake 模块,这个是我不能理解的。就是第2点,LVGL 适配层可以字节写,但是依赖 middleware_lvgl_template 组件,那么所有用户工程都依赖同一份 SDK 中的 **lvgl_sdk/template** 模块,不合理。

`middleware_lvgl.cmake` 文件大致如下:

```c
# 从这里看出 lvgl core 居然依赖 middleware_lvgl
if(CONFIG_USE_middleware_lvgl_template)
# 添加 lvgl core 代码到编译系统,添加头文件路径为公共头文件搜索路径
else()
# 报错
message(SEND_ERROR "middleware_lvgl dependency does not meet, please check ${CMAKE_CURRENT_LIST_FILE}.")
endif()
```

#### 我的解决办法

1. 修改 `armgcc/config.cmake` 文件,手动增加 `middleware_lvgl_demo_benchmark` 组件;
2. 把 `middleware_lvgl_template` 禁用;
3. (临时解决方案)同时修改 `sdk/middleware/lvg/middleware_lvgl.cmake` 文件,去掉 `CONFIG_USE_middleware_lvgl_template` ,强行设置为TRUE;

`config.cmake` 文件中最终关于 LVGL 的配置如下:

!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104636xz6bib35bynnodst.png)

`middleware_lvgl.cmake` 文件修改如下:

!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104649t0jbyytjygltm90h.png)

#### 编译成功

最后编译成功,运行成功,lvgl_demo_benchmark 运行成功。

# 运行

运行速度对比:

- debug,刷屏打点,卡成PPT,没有拍视频;
- debug,EDMA 发送缓冲区,19FPS;
- release, EDMA 发送缓冲区,31FPS;
!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104659b0efm1mm00i0drkd.png)

演示视频见B站:

(https://www.bilibili.com/video/BV13pUbYhEPx/?vd_source=8f2bbf56b70c541bec2ea0b9f102ebee)

# 总结

## 1. MCUXpresso IDE 的不足与建议

其实我最早使用的就是 MCUXpresso IDE,发现添加组件不是那么顺利,例如在一个 no-os 的工程中添加 FreeRTOS 组件,发现 FreeRTOS 源码的确拷贝到了当前工程中,但是 FreeROTS 的 porting 层的源码却没有拷贝过来,当时没有搞清楚机制,编译失败就放弃了。

后来几经尝试之后才发现 SDK 组件管理器中关于 FreeROTS 有很多零碎的组件,如下图:

1. 标号(1)是 FreeRTOS 内核源码,但是缺少porting层源码;
2. 标号(2)是 FreeRTOS 的适配层,内存管理驱动;
3. 标号(3)是多核通信的 FreeRTOS 实现方法;

总之关于 FreeRTOS 有很多零碎的组件分散在各个单元,每个组件的 **Description** 太简短了,不能让人一眼就明白用途,且各个组件的依赖关系也没有说明,不易轻松上手。

建议:**Descriptiong** 文字描述的详细些;各个组件的依赖关系也明确的写出来。

!(https://www.eefocus.com/forum/data/attachment/forum/202411/19/104716mza0ac6axaaefge6.png)

## 2. MCUXpresso For VS Code 的不足与建议

不足之处:

1. 在 VS Code 开发环境中添加组件是很方便 ,但是对于新手不友好,特别是不熟悉 CMake 的人来说,编译找不到头文件、函数未定义,让新手望而却步。
2. 某些组件有 **core** 和 **porting** 层,其中 **core** 层可以所有项目通用,但是 **porting** 对于每个项目来说可能不一样;当前工程添加了这个组件只是在 CMake 标记了组件的 **core** 和 **porting** 层都纳入编译,但是它们依然存放在 SDK 目录下,改动一处影响所有其他工程,这很不好。

建议:

1. 添加组件把组件的**porting**层拷贝到当前工程,这样每个工程都有自己的适配层,互不影响。

页: [1]
查看完整版本: 【Avnet | NXP FRDM-MCXN947试用活动】评测5--移植 LVGL跑benchmark