3、帧缓冲管理:显存分配、双缓冲机制、DMA传输优化
帧缓冲管理,说白了就是给LCD屏幕准备一个「画板」。你画什么,屏幕就显示什么。但这个画板怎么分配、怎么切换、怎么搬运数据,里面门道不少。我这些年调过的屏,至少有一半的闪烁、撕裂、卡顿问题,根源都在帧缓冲管理上。
3.1 显存分配:别让内存成为瓶颈
显存,就是帧缓冲占用的内存区域。分配多少?怎么对齐?用哪块内存?这些看似基础的问题,踩坑的人可不少。
显存大小计算
公式很简单:显存大小 = 分辨率宽度 × 分辨率高度 × 每像素字节数。
举个例子:一块480×272的RGB565屏,每像素2字节。那么一帧需要:
480 × 272 × 2 = 261,120 字节 ≈ 255 KB
嗯,这里要注意。很多MCU的DMA控制器要求源地址和目的地址按4字节对齐。如果你分配的内存没对齐,DMA传输会出问题。我个人习惯在分配时多申请几个字节,手动对齐到32字节甚至64字节边界。
避坑指南
我曾经在一个项目里,用malloc直接分配显存,结果DMA传输时断时续。查了两天才发现,malloc返回的地址只按8字节对齐,而我的DMA控制器要求32字节对齐。从那以后,我都是自己实现一个对齐分配函数。
内存区域选择
不同的内存区域,访问速度天差地别:
| 内存类型 | 访问速度 | 适用场景 | 注意事项 |
|---|---|---|---|
| 内部SRAM | 最快 | 小尺寸屏、低分辨率 | 容量有限,通常几十到几百KB |
| 外部SDRAM | 较快 | 大尺寸屏、高分辨率 | 需要初始化,延迟稍高 |
| PSRAM | 中等 | 中等分辨率、低功耗场景 | 读写速度不如SDRAM |
| Flash模拟显存 | 最慢 | 静态画面、电子墨水屏 | 不能频繁写入,寿命有限 |
你想想看,如果屏是800×480的RGB565,一帧需要750KB。内部SRAM通常只有256KB或512KB,根本放不下。这时候就必须用外部SDRAM。但SDRAM的初始化时序、刷新周期,都是坑。
3.2 双缓冲机制:告别撕裂和闪烁
单缓冲的问题很明显:你在往显存里写数据的同时,LCD控制器也在读同一块显存去刷新屏幕。结果就是——上半屏是新的,下半屏还是旧的。这就是撕裂(tearing)。
双缓冲的工作原理
说白了就是准备两块显存:
- 前端缓冲(Front Buffer):LCD控制器正在读取并显示的内容
- 后端缓冲(Back Buffer):CPU/GPU正在绘制的新内容
绘制完成后,交换指针。LCD控制器读取新的前端缓冲,整个过程无缝衔接。
// 双缓冲交换示例
volatile uint16_t *front_buffer = (uint16_t *)0x20000000;
volatile uint16_t *back_buffer = (uint16_t *)0x20040000;
void swap_buffers(void) {
// 等待垂直消隐期(VBlank)
while (!(LCD->SR & LCD_SR_VBLANK));
// 交换缓冲区指针
LCD->FBAR = (uint32_t)back_buffer;
// 交换逻辑指针
volatile uint16_t *tmp = front_buffer;
front_buffer = back_buffer;
back_buffer = tmp;
}
三缓冲又是怎么回事?
双缓冲有个小问题:如果绘制速度跟不上刷新速度,交换后前端缓冲的内容可能还没绘制完。三缓冲多加了一个缓冲区,让绘制和显示完全解耦。不过代价是内存占用增加50%。我个人觉得,对于手持终端这种资源受限的设备,双缓冲已经够用了。三缓冲更多是用在高端平板或游戏机上。
我的经验
双缓冲交换时,一定要等VBlank信号。我曾经偷懒没等,结果撕裂率从0%直接飙到15%。用户反馈说「屏幕老闪」,其实就是撕裂。加了个等待VBlank的循环,问题立刻解决。
3.3 DMA传输优化:让CPU喘口气
如果每次刷新屏幕都让CPU逐像素搬运数据,那CPU基本干不了别的了。DMA(直接存储器访问)就是干这个的——它自己搬数据,CPU该干嘛干嘛。
DMA传输的关键参数
配置DMA时,这几个参数必须搞清楚:
- 源地址:显存地址,通常是递增模式
- 目的地址:LCD控制器的GRAM地址,通常是固定模式
- 传输宽度:8位、16位还是32位?取决于屏的接口
- 传输长度:一帧的总字节数
- 触发方式:软件触发还是硬件自动触发
// DMA传输配置示例(基于STM32 HAL库)
void lcd_dma_start(uint16_t *src_addr, uint32_t size) {
// 配置DMA
hdma.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不变
hdma.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 16位
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma.Init.Mode = DMA_NORMAL; // 单次传输
HAL_DMA_Start(&hdma, (uint32_t)src_addr, (uint32_t)&LCD->DR, size);
}
DMA传输的优化技巧
为什么同样是DMA,有人用起来流畅,有人却卡顿?区别在于细节:
- 使用双缓冲DMA:配置两个DMA通道,一个传输当前帧,另一个准备下一帧。交替使用,消除等待时间。
- 合理设置优先级:DMA优先级不要太高,否则会阻塞CPU访问内存。我一般设置为中等优先级。
- 利用FIFO或突发传输:很多DMA控制器支持FIFO和突发模式,一次传输多个数据,减少总线占用。
- 避免DMA中断频繁触发:每帧只触发一次传输完成中断,不要在每行传输完成后都进中断。
注意:DMA与Cache的一致性
如果你的MCU带Cache(比如Cortex-M7),DMA传输的数据可能被Cache缓存,导致LCD读到的是旧数据。解决办法是:在DMA传输前,调用SCB_CleanInvalidateDCache()刷新Cache。我见过有人忘了这步,调试了整整一周。
行中断 vs 帧中断
有些场景下,你需要在特定行位置更新显存内容(比如滚动字幕、分屏显示)。这时候可以用行中断:
// 行中断服务函数
void LCD_LINE_IRQHandler(void) {
uint16_t current_line = LCD->LCR & 0xFFFF;
if (current_line == 100) {
// 在第100行更新显存内容
dma_transfer_partial(back_buffer + 100 * 480, 480 * 50);
}
}
但行中断频率很高(每秒上万次),处理不当会拖垮系统。我建议能用帧中断就别用行中断,除非你真的需要逐行控制。
3.4 综合实战:一个完整的帧缓冲管理方案
说了这么多,咱们来个实际的。假设你手头有个项目:480×272的RGB565屏,MCU是STM32F429,带LTDC控制器和DMA2D。
方案设计
- 显存分配:在外部SDRAM中分配两块256KB的缓冲区,地址按32字节对齐
- 双缓冲管理:使用LTDC的硬件双缓冲功能,通过修改LTDC_Layer_CFBAR寄存器切换
- DMA传输:使用DMA2D的存储器到存储器模式,配合行中断实现局部刷新
// 初始化显存
#define LCD_WIDTH 480
#define LCD_HEIGHT 272
#define LCD_BPP 2 // RGB565
// 按32字节对齐分配
uint16_t *buffer1 = (uint16_t *)0xD0000000; // SDRAM基址
uint16_t *buffer2 = (uint16_t *)0xD0040000; // 偏移256KB
void lcd_fb_init(void) {
// 配置LTDC层
LTDC_Layer_InitTypeDef layer_cfg;
layer_cfg.FBStartAdress = (uint32_t)buffer1;
layer_cfg.ImageWidth = LCD_WIDTH;
layer_cfg.ImageHeight = LCD_HEIGHT;
layer_cfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;
HAL_LTDC_Init(&hltdc, &layer_cfg, 0);
}
// 交换缓冲区
void lcd_swap(void) {
static uint8_t using_buffer1 = 1;
// 等待VBlank
while (!(LTDC->CDSR & LTDC_CDSR_VSYNCS));
if (using_buffer1) {
LTDC->Layer1->CFBAR = (uint32_t)buffer2;
} else {
LTDC->Layer1->CFBAR = (uint32_t)buffer1;
}
// 重新加载配置
LTDC->Layer1->CFBLR = (LCD_WIDTH * LCD_BPP) << 16;
LTDC->SRCR |= LTDC_SRCR_IMR;
using_buffer1 = !using_buffer1;
}
这个方案我实际用过,在480×272的屏上跑到了60fps,CPU占用率不到10%。剩下的90% CPU时间,可以用来跑GUI、处理触摸、做业务逻辑。
嗯,帧缓冲管理这块,说白了就是三个字:快、稳、省。快——DMA传输要快;稳——双缓冲防止撕裂;省——内存分配要精打细算。把这三点做到位,你的LCD驱动就成功了一大半。