3. 硬件加速与DMA2D:STM32的DMA2D控制器、LTDC与DSI接口、帧缓冲(FrameBuffer)机制

做嵌入式UI,尤其是跑LVGL这种带复杂动画的框架,最怕什么?

最怕屏幕卡成PPT。明明CPU主频不低,但一刷动画CPU占用就飙到90%,其他任务全被拖死。

我早年做第一版产品时就吃过这个亏。那时候不懂硬件加速,全靠CPU逐像素刷屏,结果动画一开,触摸响应直接延迟半秒。后来老老实实把STM32的DMA2D和LTDC摸透了,才算是真正把LVGL跑顺了。

这一章,我就把这几块硬件的用法和坑,掰开了讲清楚。

3.1 为什么需要硬件加速?

先看一个简单场景:你要在屏幕上移动一个48×48的图标。如果纯靠CPU,每帧需要做:

  • 读取原图数据
  • 计算新位置
  • 逐像素拷贝到帧缓冲
  • 处理透明混合(Alpha Blending)

一个图标还好,但LVGL里动辄十几个控件同时刷新,CPU就扛不住了。

核心结论:硬件加速的本质,是把「像素搬运」和「像素运算」从CPU手里解放出来,交给专用的DMA2D或GPU去做。CPU只负责算逻辑,不负责画点。

STM32系列里,跟显示加速相关的硬件主要有三个:

硬件模块 主要职责 适用场景
DMA2D 图形数据搬运、格式转换、Alpha混合 LVGL的脏矩形刷新、图片解码后输出
LTDC 控制TFT屏幕时序,从帧缓冲读取数据并发送给屏幕 RGB接口屏幕的直接驱动
DSI 高速串行显示接口,传输带宽更高 高分辨率屏幕(480×854以上)

这三者配合起来,才能构成一条完整的显示链路。我画了一张图,帮你理清它们之间的关系:

STM32 显示硬件加速链路 CPU LVGL逻辑运算 SDRAM 帧缓冲存储区 DMA2D 像素搬运/混合 LTDC 时序控制/读取 DSI 高速串行传输 LCD屏幕 数据总线 加速后数据 像素时钟 💡 关键路径 CPU → DMA2D → LTDC → DSI → 屏幕 CPU只参与第一环,后续全由硬件完成

3.2 DMA2D:图形搬运工

DMA2D,全称是「2D图形DMA控制器」。它跟普通DMA最大的区别是:普通DMA只会搬数据,DMA2D会在搬运的同时做运算。

它能干什么?我列几个最常用的:

  • 内存拷贝(M2M):把一块显存数据搬到另一块,速度比CPU memcpy快3-5倍
  • 填充(R2M):用固定颜色填充一块区域,清屏或画纯色背景时用
  • 混合(M2M_BLEND):带Alpha通道的图层叠加,LVGL的透明度动画全靠它
  • 格式转换(M2M_PFC):ARGB8888转RGB565,或者反过来

我的习惯:在LVGL的`lv_conf.h`里开启`LV_USE_DMA2D`后,我会把`LV_COLOR_DEPTH`设为32,让DMA2D做内部运算时保持高精度,最后输出到屏幕时再转成16位。这样颜色过渡更平滑,而且DMA2D做格式转换几乎不花时间。

来看一段实际初始化代码:

// DMA2D配置示例:将ARGB8888数据混合到RGB565帧缓冲
void dma2d_blend_test(void)
{
    // 配置前景层(带Alpha的图标)
    DMA2D->FGMAR = (uint32_t)icon_argb8888;   // 前景数据地址
    DMA2D->FGOR = 0;                           // 前景行偏移
    DMA2D->FGPFCCR = 0x00000000;               // ARGB8888格式

    // 配置背景层(当前帧缓冲)
    DMA2D->BGMAR = (uint32_t)fb_rgb565;        // 背景数据地址
    DMA2D->BGOR = 0;
    DMA2D->BGPFCCR = 0x00020000;               // RGB565格式

    // 配置输出
    DMA2D->OMAR = (uint32_t)fb_rgb565;         // 输出到同一缓冲
    DMA2D->OOR = 0;
    DMA2D->OPFCCR = 0x00020000;                // 输出RGB565

    // 配置混合模式
    DMA2D->CR = DMA2D_CR_MODE_2D_BLEND;        // 混合模式

    // 启动传输
    DMA2D->CR |= DMA2D_CR_START;

    // 等待完成
    while(DMA2D->CR & DMA2D_CR_START);
}

这段代码看起来不复杂,但有几个细节要注意:

  • 前景和背景的格式可以不同,DMA2D会自动转换
  • 行偏移(FGOR/BGOR)在非全屏刷新时特别有用,可以只更新屏幕的一部分
  • 一定要等START位清零后再操作帧缓冲,否则数据会错乱

我曾经踩过的坑:有一次我忘了等DMA2D完成,就直接修改了前景数据地址。结果屏幕上出现了撕裂的画面——上半部分是旧图标,下半部分是新图标。后来老老实实在每次DMA2D传输前加了一个等待循环,问题解决。

3.3 LTDC:屏幕的时钟管家

LTDC(LCD-TFT Display Controller)负责生成屏幕需要的时序信号:VSYNC、HSYNC、DE、像素时钟。说白了,它就是个精确的节拍器,告诉屏幕「现在该显示哪一行、哪一列」。

LTDC最核心的配置是时序参数。我直接给出一份常用的480×272屏幕配置:

参数 值 说明
HBP(水平后肩) 43 每行开始前的空白像素
HFP(水平前肩) 8 每行结束后的空白像素
VBP(垂直后肩) 12 每帧开始前的空白行
VFP(垂直前肩) 4 每帧结束后的空白行
HSYNC 1 行同步脉冲宽度
VSYNC 1 帧同步脉冲宽度
Pixel Clock 9MHz 像素时钟频率

这些参数从哪里来?从屏幕的数据手册里找。每个屏幕都不一样,配错了要么画面偏移,要么直接黑屏。

LTDC还有一个重要特性:支持多层显示。STM32F7/H7系列的LTDC通常有2层(Layer0和Layer1),每层可以独立配置地址、格式和透明度。LVGL可以利用这个特性实现硬件级别的图层叠加,完全不消耗CPU。

实用技巧:把LVGL的显示缓冲放在LTDC的Layer0,把静态背景(比如开机logo)放在Layer1。这样背景完全不需要刷新,LVGL只更新动态内容,帧率能提升20%以上。

3.4 DSI:高分辨率屏幕的救星

当屏幕分辨率超过480×272时,传统的RGB接口就有点吃力了。因为RGB接口需要并行传输,线多、干扰大、频率上不去。

DSI(Display Serial Interface)用差分串行信号传输,只需要2-4对数据线,就能跑到500Mbps以上。STM32H7系列集成了DSI主机控制器,支持DSI-2协议。

DSI有两种工作模式:

  • 命令模式:通过DSI发送命令和数据给屏幕驱动IC,屏幕自带帧缓冲。适合低功耗场景。
  • 视频模式:DSI作为LTDC的物理层,把LTDC的并行数据转成串行发送给屏幕。适合高帧率动画。

做LVGL动画,我建议用视频模式。因为命令模式下,每次刷新都要通过DSI发送大量数据,延迟比视频模式高不少。

个人经验:DSI的时钟频率要根据屏幕分辨率和刷新率算一下。比如480×854@60fps,像素时钟大约需要30MHz。DSI的时钟一般是像素时钟的2-4倍,具体看数据通道数。算不准的话,屏幕会出现闪烁或者条纹。

3.5 帧缓冲机制:双缓冲与撕裂

帧缓冲(FrameBuffer)就是显存里的一块区域,LTDC不断从中读取数据发送给屏幕。如果LVGL在更新帧缓冲的同时,LTDC也在读取同一块区域,就会出现撕裂——上半部分是旧画面,下半部分是新画面。

解决撕裂的标准方案是双缓冲:

  1. 准备两个缓冲区:Buffer A 和 Buffer B
  2. LTDC当前显示 Buffer A
  3. LVGL在 Buffer B 里绘制下一帧
  4. 绘制完成后,通过VSYNC中断切换显示地址到 Buffer B
  5. LTDC开始读取 Buffer B,LVGL继续在 Buffer A 里画下一帧

在STM32上实现双缓冲,核心代码就这几行:

// 双缓冲切换(在VSYNC中断中调用)
void LTDC_IRQHandler(void)
{
    if(LTDC->ISR & LTDC_ISR_LIF) {  // 行中断
        // 清除中断标志
        LTDC->ICR = LTDC_ICR_CLIF;

        // 切换显示地址
        if(current_buffer == 0) {
            LTDC_Layer1->CFBAR = (uint32_t)buffer_b;
            current_buffer = 1;
        } else {
            LTDC_Layer1->CFBAR = (uint32_t)buffer_a;
            current_buffer = 0;
        }
    }
}

这里有个细节:切换地址的时机必须在VSYNC消隐区,否则还是会撕裂。LTDC的中断里,LIF(Line Interrupt Flag)可以配置在消隐区触发,用这个中断做切换最安全。

注意:双缓冲虽然解决了撕裂,但会多占用一倍的显存。如果你的SDRAM只有8MB,屏幕分辨率是800×480(16位色),一帧需要800×480×2 = 768KB,双缓冲就是1.5MB,完全够用。但如果分辨率再高,就要考虑压缩或者局部刷新了。

3.6 把硬件加速接入LVGL

LVGL从v8开始,对STM32的硬件加速支持已经很完善了。你只需要做两件事:

  1. 在`lv_conf.h`里开启对应的宏
  2. 实现几个底层接口函数

来看配置:

// lv_conf.h 中的关键配置
#define LV_USE_DMA2D           1       // 开启DMA2D加速
#define LV_COLOR_DEPTH         32      // 内部颜色深度
#define LV_COLOR_16_SWAP       0       // 字节序交换(视屏幕而定)
#define LV_DISP_DEF_REFR_PERIOD 10     // 刷新周期(ms),10ms=100fps

然后实现`lv_port_disp.c`里的`disp_flush`回调:

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    // 使用DMA2D将LVGL的绘制结果搬运到LTDC的显示层
    dma2d_copy((uint32_t)color_p,                    // 源:LVGL绘制缓冲
               (uint32_t)ltdc_layer0_fb,              // 目标:LTDC层0帧缓冲
               (area->x2 - area->x1 + 1),            // 宽度
               (area->y2 - area->y1 + 1));           // 高度

    // 通知LVGL刷新完成
    lv_disp_flush_ready(disp_drv);
}

这样配置之后,LVGL的每一次脏矩形刷新,都会由DMA2D完成像素搬运。CPU只需要处理触摸事件和动画逻辑,负载能降到30%以下。

最终效果:我做过一个测试,在STM32H750上跑LVGL的「音乐播放器」demo,不开DMA2D时帧率只有28fps,CPU占用85%。开启DMA2D后帧率跳到58fps,CPU占用降到32%。差距就是这么明显。

嗯,硬件加速这块的内容差不多就这些。DMA2D负责搬砖,LTDC负责节奏,DSI负责传得快,帧缓冲负责不撕裂。把这几个东西配合好,LVGL的动画流畅度就能上一个台阶。