基于NXP iMX8MP处理器M7核心LVGL移植
LVGL (Light and Versatile Graphics Library)是一个轻量级的开源图形库,采用 C 或者 MicroPython 语言开发。可以在资源有限的 MCU 上轻松地绘制图形界面。Verdin iMX8M Plus 模块的处理器除了 Cortex-A53 核心外,还具有一个 Cortex-M7 核心,其可以运行诸如 FreeRTOS 的实时操作系统。本文接下来就将介绍如何移植 LVGL 到 Verdin iMX8M Plus 的Cortex-M7 核上。
本次演示采用一块 SPI 接口的 LCD ,屏幕控制器为 ILI9341。除了 VCC 、GND 和背光外,和 Verdin iMX8M Plus 连接的引脚主要有下面四个。
ILI9341 | Verdin iMX8M Plus | ||
CS | SPI 片选 | ECSPI1_SS0 | SODIMM202 |
RESET | 复位 | GPIO1_IO001 | SODIMM208 |
DC | 命令/数据 | GPIO1_IO00 | SODIMM206 |
MOSI | SPI MOSI | ECSPI1_MOSI | SODIMM200 |
SCK | SPI 时钟 | ECSPI1_SCLK | SODIMM196 |
表一:Verdin iMX8M Plus 连接 ILI9341
注意 Verdin iMX8M Plus 的 SoC 使用 1.8V IO,在连接 SPI LCD 时需要使用 3.3V – 1.8V 电压转换电路。
LVGL 库分为两部分。第一部分是图形实现,包括绘制各类形状、色彩管理、动画事件、定时器等,第二部分是硬件驱动实现 lvgl_drivers。LVGL 将每一帧绘制好的图片数据保存在 RAM 中,lvgl_drivers 负责将数据传输到外部显示设备。 lvgl_drivers 支持多种显示器,如 TFT、电子墨水屏、OLED 等。这里是 lvgl_drivers 支持的常见显示控制器。LVGL 移植时通常只需要修改 lvgl_drivers。例如在本次演示使用了 SPI 接口的显示屏。Verdin iMX8M Plus 可以提供连接显示屏所需的 SPI Master 功能。移植任务主要是适配 lvgl_drivers 中 ILI9341 的 SPI 数据传输以及 LVGL 图形库的几个重要定时任务。
首先安装 iMX8M Plus M7 开发所需的 SDK,如 SDK_2_12_1_MIMX8ML8xxxKZ。将该工程下载到 SDK 安装目录的 SDK_2_12_1_MIMX8ML8xxxKZ/boards/evkmimx8mp/rtos_examples/freertos_ecspi/ 位置。这个工程已经包含了下面提到的修改内容。
在工程目录的 armgcc/CmakeLists.txt 添加 lvgl 和 lvgl_drivers。这里指定 v8.3.7,其他的版本可能发生 API 变更,需要做对应的修改。设置 lvgl 和 lvgl_drivers 的 github 下载源。
---------------------------------------
# Fetch LVGL from GitHub FetchContent_Declare(lvgl GIT_REPOSITORY https://github.com/lvgl/lvgl.git GIT_TAG v8.3.7) FetchContent_MakeAvailable(lvgl) FetchContent_Declare(lv_drivers GIT_REPOSITORY https://github.com/lvgl/lv_drivers GIT_TAG v8.3.0) FetchContent_MakeAvailable(lv_drivers)
---------------------------------------
将 lvgl::lvgl 和 lvgl::drivers 编译到工程中。
---------------------------------------
target_link_libraries(${MCUX_SDK_PROJECT_NAME} PRIVATE lvgl::lvgl lvgl::drivers)
---------------------------------------
在 add_executable(${MCUX_SDK_PROJECT_NAME} 添加下面两个头文件。
---------------------------------------
"${ProjDirPath}/../lv_drv_conf.h" "${ProjDirPath}/../lv_conf.h"
---------------------------------------
设置变量 LV_CONF_PATH,这是 lvgl 的配置文件 lv_conf.h,里面包含屏幕分辨率和 lvgl 图形库参数。
---------------------------------------
# Specify path to own LVGL config header set(LV_CONF_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../lv_conf.h CACHE STRING "" FORCE)
---------------------------------------
FETCHCONTENT_UPDATES_DISCONNECTED 允许每次编译的时候不必重新下载 lvgl 代码。
---------------------------------------
SET(FETCHCONTENT_UPDATES_DISCONNECTED ON)
---------------------------------------
在工程目录下的 lv_conf.h 设置SPI TFT 屏幕分辨率240*320。
---------------------------------------
#define LV_HOR_RES_MAX 240 #define LV_VER_RES_MAX 320
---------------------------------------
在工程目录下的 lv_drv_conf.h 设置 LVGL 硬件驱动相关参数。
LV_DRV_DELAY_US() 和 LV_DRV_DELAY_MS() 需要在自己的代码中实现(位于freertos_ecspi_loopback.c)。
---------------------------------------
/********************* * DELAY INTERFACE *********************/ #define LV_DRV_DELAY_INCLUDE
---------------------------------------
延时函数在每个平台上的实现方法都不同,有的可以使用 while() 或 for() 循环,在运行操作系统的平台上可以利用系统提供的 API,例如 Verdin iMX8M Plus M7 的 FreeRTOS 中使用 vTaskDelay()。
---------------------------------------
void LVGL_DELAY_MS(uint8_t ms) { vTaskDelay( ms / portTICK_PERIOD_MS ); }
---------------------------------------
SPI TFT LCD 采用了 ILI9341 控制器,因此设置 USE_ILI9341 宏定义,以及分辨率参数。
---------------------------------------
#ifndef USE_ILI9341 # define USE_ILI9341 1 #endif # define LV_HOR_RES 240 # define LV_VER_RES 320
---------------------------------------
除了上面的延时函数外,用于控制 ILI9341 数据/命令引脚 的 LV_DRV_DISP_CMD_DATA() 、复位ILI9341 的 LV_DRV_DISP_RST() 和 SPI 传输一个字节、多个字节的函数spi_transaction_one_byte(),spi_transaction_array ()也需要自己实现。在 lv_drv_conf.h 里定义 lv_diplay_cmd_data() 和 lv_diplay_reset()。
---------------------------------------
#define LV_DRV_DISP_INCLUDE
---------------------------------------
由于 iMX8M Plus 的 SPI 在收发时会自动控制 CS 引脚,因此 LV_DRV_DISP_SPI_CS(val) 可以设置为空函数。
---------------------------------------
#define LV_DRV_DISP_SPI_CS(val) /*spi_cs_set(val)*/ /*Set the SPI's Chip select to 'val'*/ #define LV_DRV_DISP_SPI_WR_BYTE(data) spi_transaction_one_byte((data))/*spi_wr(data)*/ /*Write a byte the SPI bus*/ #define LV_DRV_DISP_SPI_WR_ARRAY(adr, n) spi_transaction_array((adr), (n))/*spi_wr_mem(adr, n)*/ /*Write 'n' bytes to SPI bus from 'adr'*/
---------------------------------------
在 freertos_ecspi_loopback.c 中实现 lv_diplay_cmd_data() ,lv_diplay_reset(),spi_transaction_one_byte(),spi_transaction_array()。
---------------------------------------
void lv_diplay_cmd_data(uint8_t val) { GPIO_PinWrite(GPIO_PAD, LCD_CMD_DATA, val); } void lv_diplay_reset(uint8_t val) { GPIO_PinWrite(GPIO_PAD, LCD_RESET, val); }
---------------------------------------
LCD_CMD_DATA 和 LCD_RESET 分别定义如下,用于控制ILI9341 的 命令/数据和复位引脚。
---------------------------------------
#define GPIO_PAD GPIO1 #define LCD_CMD_DATA 0U #define LCD_RESET 1U
---------------------------------------
SPI 数据传输采用列队形式发送。spi_transaction_one_byte() 和spi_transaction_array() 均采用 xQueueSend() 将需要发送的数据加入到 spi_queue 列队中,该列队长度为 128 字节。然后运行一个高优先级的任务 ecspi_task() 将数据从列队中通过 ECSPI_RTOS_Transfer() 发送到 ILI9341 控制器。由于发送数据的任务优先级高于写入列队的,所以spi_queue 列队中保存的数据会被很快发送出去。
---------------------------------------
void spi_transaction_one_byte(uint8_t data) { BaseType_t xStatus; uint32_t data_to_queue; data_to_queue = (uint32_t)data; xStatus = xQueueSend(spi_queue, &data_to_queue, portMAX_DELAY); if( xStatus != pdPASS ) { PRINTF( "Could not send to the queue.\r\n" ); } }
---------------------------------------
本演示中,采用不同优先级的任务来实现相应的工作。优先级数字越大便是优先级越高。为了保证 SPI 及时发送到 ILI9341,将其设置为最高优先级。
任务函数 | 优先级 | 功能描述 |
draw_lvgl_ui | 2 | LVGL UI |
lv_task_hander_task | 1 | 调用 lv_task_handler |
init_task | 3 | SPI、lv_init, hal_init 初始化 |
ecspi_task | 4 | 发送 SPI 数据到ILI9341 |
vApplicationTickHook | executed every tick | 调用 lv_tick_inc |
表二:FreeRTOS 任务描述
draw_lvgl_ui() 中绘制需要显示的 LVGL UI 内容,本演示中将显示一个动态伸缩变化的彩色柱。
lv_task_hander_task() 将每隔 5ms 调用 lv_task_handler(),该函数会每 5ms 处理 lvgl 相关任务。
init_task() 中完成 SPI、ILI9341 的初始化,以及 LVGL 图形库的相关初始化。为了防止在初始化完成前调用 lv_task_handler 和 UI 绘制,该任务运行时使用 vTaskSuspend 暂时停止 draw_lvgl_ui 和 lv_task_hander_task 两个任务。但 ecspi_task 继续运行。
---------------------------------------
void init_task(void *pvParameters) { vTaskSuspend(xUITaskHandle); //suspend ui task untill init task finisded. vTaskSuspend(xLVTaskHandle); spi_init(); ili9341_init(); lv_init(); hal_init(); PRINTF("Init finised. resume xUI and XLV tasks\r\n"); vTaskResume(xUITaskHandle); vTaskResume(xLVTaskHandle);
---------------------------------------
vApplicationTickHook 并不是一个单独的 FreeRTOS 任务,而是在每个 tick 都会被执行。因此,lv_tick_inc 将在每 2ms 运行。该函数向 LVGL 动画和其他任务提供已经运行的时间信息,需要保证其运行的准确性和粒度。
---------------------------------------
void vApplicationTickHook(void) { static uint32_t ulCount = 0; ulCount++; if (ulCount >= 2UL) { lv_tick_inc(2); //calling every 2 milliseconds. ulCount = 0UL; } }
---------------------------------------
修改 FreeRTOSConfig.h 中的下面参数,实现每个 TICK 为 1ms,以及启用上面提到的 TICK_HOOK。
---------------------------------------
#define configTICK_RATE_HZ ((TickType_t)1000) #define configUSE_TICK_HOOK 1
---------------------------------------
由于 LVGL 运行需要较大的 RAM 空间,因此该演示的 M7 固件会被加载到 DDR RAM 上运行。在编译的时候使用 build_ddr_release.sh 脚本。
---------------------------------------
export ARMGCC_DIR=/opt/gcc-arm-none-eabi-10.3-2021.10 cd armgcc ./build_ddr_release.sh
---------------------------------------
在 U-Boot 里面设置 m7bootddr 参数,将上面编译好的 M7 固件加载到地址为 0x80000000 的 DDR RAM 中。
---------------------------------------
Verdin iMX8MP # print m7bootddr m7bootddr=tftp 0x80000000 m7.bin; dcache flush; bootaux 0x80000000
---------------------------------------
启动时在 U-Boot 中运行下面命令。
---------------------------------------
run m7bootddr
---------------------------------------
运行效果如下。
总结
本文介绍为 Verdin iMX8M Plus M7 移植 LVGL 的步骤和创建对应 FreeRTOS 任务。在项目中需要为实际使用的外设和业务设置合适的任务优先级,保证图形流畅显示以及数据及时处理。在 device tree 也需要把 M7 所使用的外设禁用,避免和 Linux 系统的冲突。
提交
Verdin AM62 LVGL 移植
基于 NXP iMX8MM 测试 Secure Boot 功能
隆重推出 Aquila - 新一代 Toradex 计算机模块
Verdin iMX8MP 调试串口更改
NXP iMX8MM Cortex-M4 核心 GPT Capture 测试