绘制缓冲区优化:单缓冲 vs 双缓冲 vs 三缓冲

缓冲区怎么配,是 LVGL 性能调优里最基础也最容易被忽视的一环。我见过不少项目,CPU 跑得飞快,但界面就是卡顿,一查发现缓冲区配置压根没动过,用的还是默认的单缓冲。说白了,缓冲区就是显存和 CPU 之间的一个「中转站」,配得好,画面丝滑;配不好,CPU 空转,帧率上不去。

核心观点:缓冲区配置没有银弹,需要根据硬件资源、屏幕分辨率、刷新率综合权衡。单缓冲省内存但容易撕裂,双缓冲平衡性好,三缓冲适合高刷场景。

1. 三种缓冲机制对比

缓冲模式 内存占用 画面撕裂 CPU 利用率 适用场景
单缓冲 1× 帧缓冲 严重 低(但空转多) 低分辨率、静态界面
双缓冲 2× 帧缓冲 无 中等 大多数嵌入式场景
三缓冲 3× 帧缓冲 无 高(但更平滑) 高刷屏、动画密集

单缓冲为什么会有撕裂?因为 CPU 在往缓冲区写数据的时候,显示控制器同时也在读这块区域。你想想看,一边写一边读,读到一半的数据就显示出来了,画面上下对不齐,这就是撕裂。我早期做一个温控器项目,用的单缓冲,用户一滑动菜单就出现横纹,后来改成双缓冲才解决。

2. 缓冲区大小计算

缓冲区大小不是拍脑袋定的。公式其实很简单:

缓冲区大小 = 水平分辨率 × 垂直分辨率 × 每像素字节数

每像素字节数取决于颜色深度:

  • LV_COLOR_DEPTH 16(RGB565):2 字节/像素
  • LV_COLOR_DEPTH 32(ARGB8888):4 字节/像素
  • LV_COLOR_DEPTH 8(L8):1 字节/像素

举个例子,一块 480×272 的屏幕,用 RGB565:

480 × 272 × 2 = 261,120 字节 ≈ 255 KB

这是单缓冲的大小。双缓冲就是 510 KB,三缓冲就是 765 KB。嗯,这里要注意,很多 MCU 的 SRAM 就那么几百 KB,三缓冲可能直接撑爆。我建议先看芯片手册,算清楚剩余 RAM 再决定。

小技巧:如果 RAM 紧张,可以只分配部分行缓冲(如 1/10 屏幕高度),LVGL 支持这种「部分刷新」模式,代价是刷新次数增加,CPU 负载上升。

3. DMA 加速传输

缓冲区配好了,数据怎么从内存搬到屏幕?靠 CPU 逐像素搬运?那太慢了。DMA(直接存储器访问)就是干这个的。

我习惯把 DMA 配置成「内存到内存」或「内存到外设」模式。LVGL 的 lv_disp_drv_t 结构体里有一个 flush_cb 回调,你可以在里面触发 DMA 传输:

static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    // 配置 DMA 源地址:color_p
    // 配置 DMA 目标地址:LCD 的 GRAM 地址
    // 配置传输长度:area 的宽 × 高 × 每像素字节数
    // 启动 DMA 传输
    // 传输完成后调用 lv_disp_flush_ready(disp_drv)
}

这里有个坑:DMA 传输期间,CPU 可以继续做其他事,比如计算下一帧的动画。但如果你用的是单缓冲,CPU 和 DMA 同时操作同一块内存,数据就乱了。所以 DMA 加速通常和双缓冲搭配使用——CPU 往后台缓冲写,DMA 把前台缓冲刷到屏幕。

我曾经踩过的坑:DMA 传输完成后没有及时调用 lv_disp_flush_ready(),导致 LVGL 认为缓冲区还在使用中,整个渲染管线卡死。记得在 DMA 中断里调用这个函数。

4. 帧缓冲(FrameBuffer)配置策略

帧缓冲是显存里的一块连续区域。不同的硬件平台,配置方式不一样:

  • MCU + 内置 TFT 控制器:帧缓冲通常在 SRAM 里,通过 LTDC 或 FMC 接口直接映射到 LCD。
  • MCU + 外部 SPI 屏:帧缓冲在 MCU 内部,每次刷新通过 SPI 发送整屏数据,速度较慢。
  • Linux + DRM/KMS:帧缓冲由内核管理,通过 mmap 映射到用户空间,LVGL 直接操作。

我个人建议的策略是:

  1. 优先用双缓冲:大多数场景下,双缓冲是性价比最高的选择。
  2. 帧缓冲对齐到 32 字节:很多 DMA 控制器要求源地址和目标地址 32 字节对齐,否则传输会出错。
  3. 利用硬件双层叠加:如果 LCD 控制器支持多层(如 STM32 的 LTDC 有 2 层),可以把静态背景放一层,动态内容放另一层,减少刷新量。

实战经验:我在一个智能家居面板项目里,屏幕是 800×480,RGB565,双缓冲需要 1.5 MB RAM。芯片只有 2 MB,还要跑 FreeRTOS 和协议栈。最后我用了部分行缓冲(每次刷新 50 行),配合 DMA 双缓冲,帧率稳定在 30 FPS。

5. 缓冲机制选择流程图

下面这张图是我自己总结的决策流程,帮你快速判断该用哪种缓冲:

缓冲机制选择决策流程 开始配置缓冲区 RAM 是否 ≥ 2× 帧缓冲大小? (含系统其他开销) 是 使用双缓冲 + DMA 需要 60 FPS 以上高刷? (动画密集或滑动列表) 是 使用三缓冲 + DMA 否 保持双缓冲 否 使用部分行缓冲 (1/4 ~ 1/10 屏高) 说明: 部分行缓冲会降低单次刷新面积,但增加刷新次数。配合 DMA 和双缓冲,可以在 RAM 受限时获得可接受的帧率。

6. 实际配置建议

最后给几个具体的配置参数,你可以直接参考:

屏幕规格 推荐缓冲模式 缓冲区大小 DMA 配置
320×240, RGB565 双缓冲 2 × 150 KB = 300 KB 内存到内存,32 位宽
480×272, RGB565 双缓冲 2 × 255 KB = 510 KB 内存到外设,16 位宽
800×480, RGB565 部分行缓冲 + 双缓冲 2 × 80 行 = 2 × 75 KB 内存到外设,32 位宽
1024×600, ARGB8888 三缓冲 3 × 2.4 MB = 7.2 MB 需要外部 SDRAM + DMA2D

嗯,配置完缓冲区后,记得用 LVGL 自带的 lv_demo_benchmark() 跑一下帧率。如果发现刷新率上不去,先别急着改代码,用逻辑分析仪抓一下 DMA 传输时间,很多时候瓶颈在总线带宽上。

个人习惯:我会在项目初期就预留好双缓冲的内存空间,哪怕先用单缓冲跑。这样后期切双缓冲时不用重构内存布局,省很多事。