简介:
我们在之前的基础上已经适配了LTDC 的驱动,可以将framebuff 的数据刷新到屏幕上显示,在此基础上我们继续适配LVGL,LVGL 的适配网上的教程也很多在此就不重复造轮子了,LVGL 的代码相互之间的耦合没那么高,一致的重点主要是将代码加入到工程编译,并将底层的显示刷线接口对接到LCD显示就完成了移植适配过程,本地使用LVGL 8.3.10 的版本,最新的LVGL已经更新到了V9版本了,本地使用旧版本主要是为了后续使用Gui Guider(1.7.2) 生成LVGL的代码所以版本保持了一致。
将下载的代码按照lvgl 的目录结构添加到IAR 工程:
修改lv_port_disp.c 对接lcd 显示,对应函数为disp_flash
/** * @file lv_port_disp_templ.c * */ /*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/ #if 1 /********************* * INCLUDES *********************/ #include "lv_port_disp.h" #include <stdbool.h> /********************* * DEFINES *********************/ #define LCD_WIDTH 800 #define LCD_HEIGHT 480 uint16_t lcd_buff[LCD_HEIGHT][LCD_WIDTH] @ ".fb"; /********************** * TYPEDEFS **********************/ /********************** * STATIC PROTOTYPES **********************/ static void disp_init(void); static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p); //static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width, // const lv_area_t * fill_area, lv_color_t color); /********************** * STATIC VARIABLES **********************/ /********************** * MACROS **********************/ /********************** * GLOBAL FUNCTIONS **********************/ void lv_port_disp_init(void) { /*------------------------- * Initialize your display * -----------------------*/ disp_init(); /*----------------------------- * Create a buffer for drawing *----------------------------*/ /** * LVGL requires a buffer where it internally draws the widgets. * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display. * The buffer has to be greater than 1 display row * * There are 3 buffering configurations: * 1. Create ONE buffer: * LVGL will draw the display's content here and writes it to your display * * 2. Create TWO buffer: * LVGL will draw the display's content to a buffer and writes it your display. * You should use DMA to write the buffer's content to the display. * It will enable LVGL to draw the next part of the screen to the other buffer while * the data is being sent form the first buffer. It makes rendering and flushing parallel. * * 3. Double buffering * Set 2 screens sized buffers and set disp_drv.full_refresh = 1. * This way LVGL will always provide the whole rendered screen in `flush_cb` * and you only need to change the frame buffer's address. */ /* Example for 1) */ static lv_disp_draw_buf_t draw_buf_dsc_1; static lv_color_t buf_1[LCD_WIDTH * 10]; /*A buffer for 10 rows*/ lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, LCD_WIDTH * 10); /*Initialize the display buffer*/ /*----------------------------------- * Register the display in LVGL *----------------------------------*/ 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 = disp_flush; /*Set a display buffer*/ disp_drv.draw_buf = &draw_buf_dsc_1; /*Required for Example 3)*/ //disp_drv.full_refresh = 1; /* Fill a memory array with a color if you have GPU. * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL. * But if you have a different GPU you can use with this callback.*/ //disp_drv.gpu_fill_cb = gpu_fill; /*Finally register the driver*/ lv_disp_drv_register(&disp_drv); } /********************** * STATIC FUNCTIONS **********************/ /*Initialize your display and the required peripherals.*/ static void disp_init(void) { /*You code here*/ } volatile bool disp_flush_enabled = true; /* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL */ void disp_enable_update(void) { disp_flush_enabled = true; } /* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL */ void disp_disable_update(void) { disp_flush_enabled = false; } /*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_disp_flush_ready()' has to be called when finished.*/ static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { if(disp_flush_enabled) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/ int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /*Put a pixel to the display. For example:*/ /*put_px(x, y, *color_p)*/ lcd_buff[y][x] = color_p->full; color_p++; } } } /*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv); } /*OPTIONAL: GPU INTERFACE*/ /*If your MCU has hardware accelerator (GPU) then you can use it to fill a memory with a color*/ //static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width, // const lv_area_t * fill_area, lv_color_t color) //{ // /*It's an example code which should be done by your GPU*/ // int32_t x, y; // dest_buf += dest_width * fill_area->y1; /*Go to the first line*/ // // for(y = fill_area->y1; y <= fill_area->y2; y++) { // for(x = fill_area->x1; x <= fill_area->x2; x++) { // dest_buf[x] = color; // } // dest_buf+=dest_width; /*Go to the next line*/ // } //} #else /*Enable this file at the top*/ /*This dummy typedef exists purely to silence -Wpedantic.*/ typedef int keep_pedantic_happy; #endif
对接好该函数后,更新lvgl_conf 本地使用RGB565 显示配置,并开启benchmark demo
/*==================== Graphical settings *====================*/ /* Color depth: * - 1: 1 byte per pixel * - 8: RGB332 * - 16: RGB565 * - 32: ARGB8888 */ #define LV_COLOR_DEPTH 16 /* Can be 8 or 1. */ /* Swap the 2 bytes of RGB565 color. * Useful if the display has a 8 bit interface (e.g. SPI)*/ #define LV_COLOR_16_SWAP 0 /*Enable features to draw on transparent background. *It's required if opa, and transform_* style properties are used. *Can be also used if the UI is above another layer, e.g. an OSD menu or video player.*//* 1: Enable screen transparency. * Useful for OSD or other overlapping GUIs. * Requires `LV_COLOR_DEPTH = 32` colors and the screen's style should be modified: `style.body.opa = ...`*/ #define LV_COLOR_SCREEN_TRANSP 0
/*Demonstrate the usage of encoder and keyboard*/ #define LV_USE_DEMO_KEYPAD_AND_ENCODER 0 /*Benchmark your system*/ #define LV_USE_DEMO_BENCHMARK 1 #if LV_USE_DEMO_BENCHMARK /*Use RGB565A8 images with 16 bit color depth instead of ARGB8565*/ #define LV_DEMO_BENCHMARK_RGB565A8 0 #endif /*Stress test for LVGL*/ #define LV_USE_DEMO_STRESS 0
在freertos 任务中初始化lvgl,并调用benchmark demo 程序
void start_task1(void *pvParameters) { printf("lvgl benchmark demo started\r\n"); //lv_port_pre_init(); lv_init(); lv_port_disp_init(); //lv_port_indev_init(); //s_lvgl_initialized = true; lv_demo_benchmark(); for (;;) { lv_task_handler(); vTaskDelay(5); } }
运行验证:
上述的显示会存在花屏幕的现象,需要进一步排查原因,本地配置未使用DMA2D加速搬运像素数据,只使用了单buff 后续继续研究提升性能。
花屏问题调查:
我们继续解决上述试验花屏的问题,初步认为是MCU 写frame buff 的时候LTDC 也在读取frame buff 数据造成的并发访问的异常问题,针对该思路本地使用DMA2D来搬运大块数据的效率会高于MCU搬运的效率,理论上虽然不能解决并发访问冲突的问题,但是会降低花屏的现象。
LTDC 使用DMA2D 搬运数据:
STM32CubeMX 配置开启DMA2D,本地LTDC配置数据格式为RGB565:
生成DMA2D 初始化代码
/** * @brief DMA2D Initialization Function * @param None * @retval None */ static void MX_DMA2D_Init(void) { /* USER CODE BEGIN DMA2D_Init 0 */ /* USER CODE END DMA2D_Init 0 */ /* USER CODE BEGIN DMA2D_Init 1 */ /* USER CODE END DMA2D_Init 1 */ hdma2d.Instance = DMA2D; hdma2d.Init.Mode = DMA2D_M2M; hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565; hdma2d.Init.OutputOffset = 0; hdma2d.LayerCfg[1].InputOffset = 0; hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0; hdma2d.LayerCfg[1].AlphaInverted = DMA2D_REGULAR_ALPHA; hdma2d.LayerCfg[1].RedBlueSwap = DMA2D_RB_REGULAR; hdma2d.LayerCfg[1].ChromaSubSampling = DMA2D_NO_CSS; if (HAL_DMA2D_Init(&hdma2d) != HAL_OK) { Error_Handler(); } if (HAL_DMA2D_ConfigLayer(&hdma2d, 1) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN DMA2D_Init 2 */ /* USER CODE END DMA2D_Init 2 */ }
在上述初始化代码的基础上,添加如下测试代码验证DMA2D的功能。
#define USED_DMA2D_TEST_CODE 1 #if (1 == USED_DMA2D_TEST_CODE) static void LL_ConvertLineToRGB(uint32_t *pSrc, uint32_t *pDst, uint32_t w,uint32_t h) { /* Configure the DMA2D Mode, Color Mode and output offset */ hdma2d.Init.Mode = DMA2D_M2M_PFC; hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565; hdma2d.Init.OutputOffset = 800 - w; /* Foreground Configuration */ hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0xFF; hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_RGB565; hdma2d.LayerCfg[1].InputOffset = 0; hdma2d.LayerCfg[1].AlphaInverted = DMA2D_REGULAR_ALPHA; hdma2d.Instance = DMA2D; /* DMA2D Initialization */ if (HAL_DMA2D_Init(&hdma2d) == HAL_OK) { if (HAL_DMA2D_ConfigLayer(&hdma2d, 1) == HAL_OK) { if (HAL_DMA2D_Start(&hdma2d, (uint32_t)pSrc, (uint32_t)pDst, w, h) == HAL_OK) { /* Polling For DMA transfer */ (void)HAL_DMA2D_PollForTransfer(&hdma2d, 50); } } } } int32_t BSP_LCD_FillBuff( uint32_t Xpos, uint32_t Ypos, uint32_t Width, uint32_t Height, uint32_t Color) { uint32_t Xaddress; /* Get the rectangle start address */ Xaddress = (hltdc.LayerCfg[0].FBStartAdress) + 2*(800*Ypos + Xpos); /* Fill the rectangle */ LL_ConvertLineToRGB((uint32_t *)_acrgb565data,(uint32_t *)Xaddress, Width, Height); return BSP_ERROR_NONE; } static void LL_FillBuffer(uint32_t *pDst, uint32_t xSize, uint32_t ySize, uint32_t OffLine, uint32_t Color) { /* Register to memory mode with ARGB8888 as color Mode */ hdma2d.Init.Mode = DMA2D_R2M; hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565; hdma2d.Init.OutputOffset = OffLine; hdma2d.Instance = DMA2D; /* DMA2D Initialization */ if (HAL_DMA2D_Init(&hdma2d) == HAL_OK) { if (HAL_DMA2D_Start(&hdma2d, Color, (uint32_t)pDst, xSize, ySize) == HAL_OK) { /* Polling For DMA transfer */ (void)HAL_DMA2D_PollForTransfer(&hdma2d, 50); } } }
调用上述接口,使用DMA2D搬运数据
BSP_LCD_FillBuff(0,0,200,200,0); vTaskDelay(5000); for (;;) { //lv_task_handler(); BSP_LCD_FillRect(0,0,100,300,0x55aa); vTaskDelay(5); BSP_LCD_FillRect(200,0,100,200,0xaa55); vTaskDelay(5); BSP_LCD_FillRect(300,0,100,300,0x55aa); vTaskDelay(5); BSP_LCD_FillRect(500,0,100,200,0xaa55); }
运行后结果跟预期的一致说明DMA2D 已经工作并搬运数据至LTDC framebuff
将上述DMA2D 功能验证通过代码对接到LVGL 刷新LCD显示接口函数中。
extern DMA2D_HandleTypeDef hdma2d; static void dma2d_transfer(uint32_t data, uint16_t x, uint16_t y, uint16_t width, uint16_t height) { /* Configure the DMA2D Mode, Color Mode and output offset */ hdma2d.Init.Mode = DMA2D_M2M_PFC; hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565; hdma2d.Init.OutputOffset = LCD_WIDTH - width; /* Foreground Configuration */ hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; hdma2d.LayerCfg[1].InputAlpha = 0xFF; hdma2d.LayerCfg[1].InputColorMode = DMA2D_OUTPUT_RGB565; hdma2d.LayerCfg[1].InputOffset = 0; hdma2d.LayerCfg[1].AlphaInverted = DMA2D_REGULAR_ALPHA; hdma2d.Instance = DMA2D; uint32_t fb_offset = (uint32_t)lcd_buff + (x + y * LCD_WIDTH) * 2; /* DMA2D Initialization */ if (HAL_DMA2D_Init(&hdma2d) == HAL_OK) { if (HAL_DMA2D_ConfigLayer(&hdma2d, 1) == HAL_OK) { if (HAL_DMA2D_Start(&hdma2d, (uint32_t)data, (uint32_t)fb_offset, width, height) == HAL_OK) { /* Polling For DMA transfer */ (void)HAL_DMA2D_PollForTransfer(&hdma2d, 50); } } } } int32_t BSP_LCD_FillBuff( uint32_t Xpos, uint32_t Ypos, uint32_t Width, uint32_t Height, uint32_t Color); /*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_disp_flush_ready()' has to be called when finished.*/ static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { #if (LV_USE_GPU_STM32_DMA2D == 0) if(disp_flush_enabled) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/ int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /*Put a pixel to the display. For example:*/ /*put_px(x, y, *color_p)*/ lcd_buff[y][x] = color_p->full; color_p++; } } } #else if(disp_flush_enabled) { uint16_t act_x1, act_x2, act_y1, act_y2; if(area->x2 < 0 || area->y2 < 0) return; if(area->x1 > (LCD_WIDTH - 1) || area->y1 > (LCD_HEIGHT - 1)) return; act_x1 = area->x1 < 0 ? 0 : area->x1; act_y1 = area->y1 < 0 ? 0 : area->y1; act_x2 = area->x2 > LCD_WIDTH - 1 ? LCD_WIDTH - 1 : area->x2; act_y2 = area->y2 > LCD_HEIGHT - 1 ? LCD_HEIGHT - 1 : area->y2; dma2d_transfer((uint32_t)color_p, act_x1, act_y1, act_x2 - act_x1 + 1, act_y2 - act_y1 + 1); } #endif /*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv); }
运行验证,不出意外的果然出意外了,跑起来比未使用DMA2D 之前花屏的更厉害了
经过几天的排查发现花屏的原因为本地framebuff 存储在外部PSRAM上,这段RAM 在MPU 初始化时配置的属性为MPU_ACCESS_CACHEABLE & MPU_ACCESS_BUFFERABLE 造成花屏的原因为LTDC读取的物理内存,我们写framebuff 时数据并没有及时被刷新到物理内存而是先保存在cache中,知道原因后我们可以有多种方式进行修改
1 修改外部PSRAM 为MPU_ACCESS_NOT_CACHEABLE & MPU_ACCESS_NOT_BUFFERABLE 这样就不会造成上述的数据不一致问题。
2 LVGL 底层更新frame buff前disable cache 更新后在 enable cache ,这种修改方式不好的一点会造成已经在cache 中的其他数据强制丢弃,在此访问造成cahce miss
3 划分出一段内存将frame buff 存放其中,配置为no cahce 属性,此方法跟方法1 基本时一致的。
我们按照方法一的修改修改这段内存的属性为不可cache 和 buffed
修改上述MPU属性后验证,花屏幕问题终于得到解决了。