使用恩智浦FRDM-MCXN947开发板移植LVGL跑benchmark

前言

恩智浦“FRDM-MCXN947”评测活动由安富利和与非网协同举办。本篇内容由与非网用户发布,已获转载许可。原文可在与非网(eefocus)工程师社区查看。

背景

上一期【用户测评(六):NXP FRDM-MCXN947 FLEXIO_SPI驱动TFT LCD】以FLEXIO_SPI驱动了TFT LCD,此贴移植LVGL,测试刷屏速度。

LVGL移植

01引入SDK组件

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

a8850918-4b5a-11f0-b715-92fbcf53809c.png

1. 在VS Code侧边栏打开MCUXpresso插件图标,点击打开;

2. 鼠标右键单击项目,展开选项;

3. 选择Configure展开子菜单栏;

4. 选择Manage Components弹开组件选择窗口;

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

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

set(CONFIG_USE_xxxx  true)# 其他组件


# 引入 LVGL 时添加的两行
set(CONFIG_USE_middleware_lvgltrue)
set(CONFIG_USE_middleware_lvgl_templatetrue)

02添加LVGL显示适配层

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

1.LVGL初始化,LCD硬件初始化;

2.LVGL刷新缓冲区接口的实现;

1. 新增文件

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

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);

voidlv_port_disp_init(void)
{
staticlv_disp_draw_buf_tdisp_buf;


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


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


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


staticlv_disp_drv_tdisp_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缓冲区内容刷新到屏幕上。

/* 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*/
staticvoidDEMO_FlushDisplay(lv_disp_drv_t*disp_drv,constlv_area_t*area,lv_color_t*color_p)
{
 lv_coord_tx1 = area->x1;
 lv_coord_ty1 = area->y1;
 lv_coord_tx2 = area->x2;
 lv_coord_ty2 = area->y2;


 int32_tlength = (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;

voidLCD_DrawBitmap(uint16_txstart,uint16_tystart,uint16_txend,uint16_tyend,uint16_t*color)
{
LCD_SetWindows(xstart, ystart, xend, yend);


uint16_twidth = xend - xstart +1;
uint16_theight = yend - ystart +1;
uint16_tsize = width * height;


#if0
//NOTE:逐个打点,非常慢--卡成 PPT
for(uint16_ti = 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。

/**
* @brief SPI 发送一个数组
*
* @param Data
* @param size
*/
voidLcd_WriteData_16BitArray(uint16_t*Data,uint32_tsize)
{
flexio_spi_transfer_txfer = {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;
}

03创建LVGL任务

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

/**
*@briefLVGL Core 线程
*
*@parampvParameters
*/
staticvoidgui_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));
 }
}




voidgui_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!.
");
while(1);
 }
}

04添加LVGL心跳

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

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

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

05编译

编译出错undefined reference to `lv_demo_benchmark'

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

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

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

a8b39bfc-4b5a-11f0-b715-92fbcf53809c.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等三个文件添加到编译中;

这里就发现了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文件大致如下:

# 从这里看出 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的配置如下:

a8e9337a-4b5a-11f0-b715-92fbcf53809c.png

middleware_lvgl.cmake文件修改如下:

a8fba302-4b5a-11f0-b715-92fbcf53809c.png

编译成功

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

运行

运行速度对比:

debug,刷屏打点,卡成PPT,没有拍视频

debug,EDMA发送缓冲区,19FPS;

release,EDMA发送缓冲区,31FPS;

a91893b8-4b5a-11f0-b715-92fbcf53809c.png

演示视频见B站:

https://www.bilibili.com/video/BV13pUbYhEPx/?spm_id_from=888.80997.embed_other.whitelist&t=1.46976&bvid=BV13pUbYhEPx

总结

01MCUXpresso IDE的不足与建议

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

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

1. 标号(1)是FreeRTOS内核源码,但是缺少porting层源码;

2. 标号(2)是FreeRTOS的适配层,内存管理驱动;

3. 标号(3)是多核通信的FreeRTOS实现方法;

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

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

a944ce6a-4b5a-11f0-b715-92fbcf53809c.png

02MCUXpresso For VS Code的不足与建议

不足之处:

1. 在VS Code开发环境中添加组件是很方便,但是对于新手不友好,特别是不熟悉CMake的人来说,编译找不到头文件、函数未定义,让新手望而却步。

2. 某些组件有core和porting层,其中core层可以所有项目通用,但是porting对于每个项目来说可能不一样;当前工程添加了这个组件只是在CMake标记了组件的core和porting层都纳入编译,但是它们依然存放在SDK目录下,改动一处影响所有其他工程,这很不好。

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

为您推荐

逆变焊机新时代:碳化硅(SiC)技术开启高效节能新篇章

逆变焊机新时代:碳化硅(SiC)技术开启高效节能新篇章

逆变焊机新时代:碳化硅(SiC)技术开启高效节能新篇章 引言:焊机行业的挑战与革新 在工业制造领域,焊接设备是...

详解不同等级浪涌保护器差异及选型要点

详解不同等级浪涌保护器差异及选型要点

当人们选购浪涌保护器时,常常会面临两个误区:一是混淆其一级浪涌保护器和二级浪涌保护器的功能定位,二是忽视参数的适配性。...

Intel至强6:AI江湖的幕后大佬、NVIDIA B300的唯一伙伴

Intel至强6:AI江湖的幕后大佬、NVIDIA B300的唯一伙伴

随着生成式AI、预测式AI的浪潮一波高过一波,工作负载的类型越来越丰富、复杂度越来越高,对于AI服务器性能、能效的需求...

基本股份B3M013C120Z(碳化硅SiC MOSFET)的产品力分析

基本股份B3M013C120Z(碳化硅SiC MOSFET)的产品力分析

从基本股份推出的B3M013C120Z(1200V/176A SiC MOSFET)的产品力分析,中国SiC碳化硅MO...

2025-06-26 标签:新能源功率机械能源
零碳园区建设的突破之路:智慧能源管理解决方案引领未来

零碳园区建设的突破之路:智慧能源管理解决方案引领未来

中国超过80%的工业企业已集中在园区,园区工业总产值平均占到全国的50%,碳排放量占全国的31%。2024年中央经济工...

2025-06-25 标签:能源管理能源碳排放阿里

当前非电脑浏览器正常宽度,请使用移动设备访问本站!