4. LVGL双缓冲实战(一):基于STM32F429的硬件环境搭建、LTDC与DMA2D配置、双buffer初始化代码
说实话,做GUI最怕什么?最怕画面撕裂。你想想看,屏幕正在刷新的时候,MCU突然把显存内容改了,上半截是旧画面,下半截是新画面——这种割裂感,用户一眼就能看出来。
双缓冲就是来解决这个问题的。这一章,咱们先把STM32F429的硬件环境搭起来,把LTDC和DMA2D配置好,最后把双缓冲的初始化代码跑通。我习惯先把硬件底子打扎实,再往上堆软件,这样出了问题也好定位。
4.1 硬件环境概览
STM32F429这款芯片,我用了好几年了。它内置TFT-LCD控制器(LTDC)和DMA2D图形加速器,做双缓冲简直是天生一对。咱们先看看这次实战用到的硬件资源:
| 硬件模块 | 型号/参数 | 用途 |
|---|---|---|
| 主控MCU | STM32F429IGT6 | 核心处理,运行LVGL |
| 外部SDRAM | W9825G6KH-6 (32MB) | 存放两个显存buffer + LVGL内存池 |
| LCD屏 | 7寸 1024×600 RGB接口 | 显示输出 |
| LTDC | 内置于F429 | 驱动LCD时序 |
| DMA2D | 内置于F429 | 快速拷贝/填充显存 |
4.2 LTDC配置要点
LTDC说白了就是给LCD提供时序信号的。它不需要你手动控制每个像素,只要配好时钟、分辨率、同步参数,它自己就能把显存里的数据刷到屏幕上。
我个人习惯先配时钟。LTDC的像素时钟来自PLLSAI,一般配到33.3MHz左右,这样1024×600@60Hz刚好够用。配高了屏幕可能花屏,配低了刷新率不够。
/* LTDC时钟配置:PLLSAI输出 33.3MHz */
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_LTDC;
PeriphClkInit.PLLSAI.PLLSAIN = 192;
PeriphClkInit.PLLSAI.PLLSAIR = 5;
PeriphClkInit.PLLSAI.PLLSAIQ = 7;
PeriphClkInit.PLLSAIDivR = RCC_PLLSAIDIVR_2;
HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
然后是LTDC的初始化结构体。这里要注意的是水平同步和垂直同步的脉冲宽度,不同屏幕的规格书不一样,一定要对着数据手册配。
/* LTDC初始化结构体 */
LTDC_HandleTypeDef hltdc;
hltdc.Instance = LTDC;
hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL;
hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL;
hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL;
hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC;
hltdc.Init.HorizontalSync = 40; /* 水平同步宽度 */
hltdc.Init.VerticalSync = 9; /* 垂直同步宽度 */
hltdc.Init.AccumulatedHBP = 53; /* 水平后肩 */
hltdc.Init.AccumulatedVBP = 11; /* 垂直后肩 */
hltdc.Init.AccumulatedActiveW = 1077; /* 有效宽度 + HBP + HSync */
hltdc.Init.AccumulatedActiveH = 620; /* 有效高度 + VBP + VSync */
hltdc.Init.TotalWidth = 1117; /* 总宽度 */
hltdc.Init.TotalHeigh = 635; /* 总高度 */
hltdc.Init.Backcolor.Blue = 0;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Red = 0;
HAL_LTDC_Init(&hltdc);
4.3 DMA2D配置
DMA2D是F429的图形加速器。它可以在后台快速搬运数据,不占用CPU。双缓冲切换的时候,用DMA2D来拷贝buffer,效率比CPU memcpy高好几倍。
我一般把DMA2D配置成两种模式:寄存器模式(直接操作)和中断模式(配合LVGL的刷新回调)。这里先展示寄存器模式的配置,简单直接:
/* DMA2D初始化 */
DMA2D_HandleTypeDef hdma2d;
hdma2d.Instance = DMA2D;
hdma2d.Init.Mode = DMA2D_M2M; /* 内存到内存模式 */
hdma2d.Init.ColorMode = DMA2D_ARGB8888; /* 32位色深 */
hdma2d.Init.OutputOffset = 0;
hdma2d.Init.BytesSwap = DMA2D_BYTESWAP_DISABLE;
hdma2d.Init.LineOffsetMode = DMA2D_LOM_PIXELS;
hdma2d.Init.LineNumber = 600; /* 屏幕高度 */
hdma2d.Init.PixelPerLine = 1024; /* 屏幕宽度 */
HAL_DMA2D_Init(&hdma2d);
配置好之后,拷贝一个buffer只需要一行代码:
/* 使用DMA2D拷贝buffer */
HAL_DMA2D_Start(&hdma2d,
(uint32_t)buffer_src,
(uint32_t)buffer_dst,
1024, /* 每行像素数 */
600); /* 行数 */
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
4.4 双Buffer初始化代码
好了,硬件配置完了,现在来写双缓冲的初始化。两个buffer都放在SDRAM里,地址要连续,方便DMA2D批量操作。
我习惯把buffer地址定义成宏,这样代码可读性更好:
/* SDRAM基地址(F429的FMC映射) */
#define SDRAM_BASE_ADDR 0xC0000000
/* 两个显存buffer的偏移地址 */
#define BUFFER1_OFFSET 0x00000000 /* 第一个buffer从SDRAM起始 */
#define BUFFER2_OFFSET 0x00258000 /* 第二个buffer紧挨着 */
/* 每个buffer的大小:1024 * 600 * 4 = 2,457,600 bytes */
#define BUFFER_SIZE (1024 * 600 * 4)
/* buffer地址宏 */
#define LCD_BUF1 (SDRAM_BASE_ADDR + BUFFER1_OFFSET)
#define LCD_BUF2 (SDRAM_BASE_ADDR + BUFFER2_OFFSET)
然后写初始化函数。这里要注意:LTDC默认指向第一个buffer,第二个buffer先不激活,等LVGL需要切换时再换过去。
/* 双缓冲初始化 */
void LCD_DoubleBuffer_Init(void)
{
/* 1. 清空两个buffer */
memset((void*)LCD_BUF1, 0x00, BUFFER_SIZE);
memset((void*)LCD_BUF2, 0x00, BUFFER_SIZE);
/* 2. 配置LTDC图层0,指向buffer1 */
LTDC_LayerCfgTypeDef layer_cfg = {0};
layer_cfg.WindowX0 = 0;
layer_cfg.WindowX1 = 1024;
layer_cfg.WindowY0 = 0;
layer_cfg.WindowY1 = 600;
layer_cfg.PixelFormat = LTDC_PIXEL_FORMAT_ARGB8888;
layer_cfg.FBStartAdress = LCD_BUF1; /* 当前显示buffer1 */
layer_cfg.Alpha = 255;
layer_cfg.Alpha0 = 0;
layer_cfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_PAxCA;
layer_cfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_PAxCA;
layer_cfg.ImageWidth = 1024;
layer_cfg.ImageHeight = 600;
layer_cfg.Backcolor.Blue = 0;
layer_cfg.Backcolor.Green = 0;
layer_cfg.Backcolor.Red = 0;
HAL_LTDC_ConfigLayer(&hltdc, &layer_cfg, 0);
/* 3. 记录当前显示的buffer编号 */
current_buffer = 0; /* 0表示buffer1,1表示buffer2 */
/* 4. 启动LTDC */
HAL_LTDC_Start(&hltdc);
}
HAL_DMA2D_Fill(&hdma2d, LCD_BUF1, 0, 1024, 600);,这样CPU可以去做别的事。
4.5 双缓冲切换逻辑
初始化完了,还得有个切换函数。LVGL在刷新时会调用这个函数,把后台buffer和前台buffer互换。
/* 双缓冲切换函数 */
void LCD_SwapBuffers(void)
{
if (current_buffer == 0)
{
/* 当前显示buffer1,切换到buffer2 */
HAL_LTDC_SetAddress(&hltdc, LCD_BUF2, 0);
current_buffer = 1;
}
else
{
/* 当前显示buffer2,切换到buffer1 */
HAL_LTDC_SetAddress(&hltdc, LCD_BUF1, 0);
current_buffer = 0;
}
}
这里有个细节:HAL_LTDC_SetAddress函数会等待当前帧结束才切换地址,所以不会出现撕裂。嗯,这就是硬件双缓冲的好处——你不用操心同步问题,LTDC自己会处理好。
4.6 整体流程图
为了让你更直观地理解整个流程,我画了一张图:
从图上可以看得很清楚:整个流程就是先配硬件,再分配buffer,最后让LTDC和LVGL配合起来做切换。每一步都有明确的输入输出,出了问题也好排查。
4.7 验证与调试
代码写完了,怎么验证对不对?我一般用两个方法:
- 填充颜色测试:把buffer1填充成红色,buffer2填充成蓝色。如果屏幕显示红色,说明LTDC正确指向了buffer1。然后调用切换函数,屏幕应该变成蓝色。
- DMA2D拷贝测试:在buffer1里画一个图案,用DMA2D拷贝到buffer2,然后切换显示。如果图案一致,说明DMA2D配置正确。