LTDC 与 DMA2D 配置:让屏幕真正跑起来

好,咱们进入第四章。前面几章我们把 TouchGFX 的工程框架搭好了,也把 FreeRTOS 和驱动层理清了。但说实话,屏幕能不能流畅显示,关键就看这一章——LTDC 和 DMA2D 的配置。

我见过不少项目,UI 逻辑写得挺漂亮,结果一上屏就卡成幻灯片。查到最后,往往是 LTDC 时序算错了,或者 DMA2D 没使能。嗯,咱们今天就把这两个硬骨头啃下来。

核心要点:LTDC 负责把显存里的数据源源不断地推给屏幕,DMA2D 负责加速像素搬运和格式转换。两者配合好了,60fps 是基本盘。

LTDC 时序参数计算

说白了,LTDC 就是个严格的“时钟工”。它按照你设定的时序,一行一行、一帧一帧地把数据推出去。时序错了,屏幕要么不亮,要么闪烁,要么图像偏移。

每个屏幕都有自己的 datasheet,里面会给出时序参数。我拿最常见的 480x272 屏举个例子:

参数 含义 典型值(480x272)
HBP 水平后肩 43
HFP 水平前肩 8
HSYNC 水平同步宽度 4
VBP 垂直后肩 12
VFP 垂直前肩 4
VSYNC 垂直同步宽度 4

计算像素时钟的公式很简单:

PixelClock = (H_Width + HBP + HFP + HSYNC) × (V_Height + VBP + VFP + VSYNC) × FrameRate

代入数值:

PixelClock = (480 + 43 + 8 + 4) × (272 + 12 + 4 + 4) × 60
           = 535 × 292 × 60
           ≈ 9.37 MHz

实际配置时,我会选一个接近的整数分频。比如用 10MHz,只要在屏幕允许范围内就行。我曾经在一个项目里把 PixelClock 算高了 0.5MHz,结果屏幕边缘出现了一条细细的噪点线,查了两天才发现是时序裕量不够。

注意:LTDC 的同步极性也要配对。大部分屏幕是低电平有效(Active Low),但有些屏是反的。配反了屏幕直接不亮,别问我怎么知道的。

图层配置

TouchGFX 通常只用 LTDC 的 Layer1。Layer0 可以留给背景或者硬件光标,但大多数场景下我们用单层就够了。

配置图层时,有几个关键寄存器:

  • PFCR:像素格式。TouchGFX 常用 RGB565 或 ARGB8888。RGB565 省一半带宽,但颜色精度差一点。
  • CFBAR:帧缓冲起始地址。这就是显存的起点。
  • CFBLR:帧缓冲行长度。等于 水平分辨率 × 每像素字节数。
  • CFBNR:帧缓冲行数。就是垂直分辨率。

举个例子,配置 RGB565 的 480x272 图层:

LTDC_Layer1->PFCR  = 0x02;        // RGB565
LTDC_Layer1->CFBAR = (uint32_t)frame_buffer;
LTDC_Layer1->CFBLR = (480 * 2) << 16 | (480 * 2 + 3);
LTDC_Layer1->CFBNR = 272;

这里有个坑:CFBLR 的低 16 位是行长度,高 16 位是行偏移。行偏移一般等于行长度,但如果用了双缓冲且两个缓冲连续存放,行偏移可以设成 行长度 × 2。我刚开始搞的时候没注意这个,结果图像显示出来是斜的。

DMA2D 加速原理

DMA2D 是 STM32 里专门干图像活儿的 DMA。它不只能搬数据,还能在搬的过程中做格式转换、颜色填充、甚至 Alpha 混合。

说白了,CPU 干这些事太慢了。一个 480x272 的 ARGB8888 图层做 Alpha 混合,CPU 要算 480×272×4 ≈ 52 万次乘加。DMA2D 一个时钟周期能干完一个像素,CPU 得几十个周期。

DMA2D 有四种模式:

  • 寄存器到内存:填充一块颜色,比如清屏。
  • 内存到内存:纯搬运,不做转换。
  • 内存到内存(PFC):搬运同时做像素格式转换,比如 ARGB8888 转 RGB565。
  • 内存到内存(混合):搬运同时做 Alpha 混合,这是 TouchGFX 最常用的模式。

经验之谈:TouchGFX 的 Framebuffer 通常用 RGB565,但 UI 素材可能是 ARGB8888。DMA2D 可以在搬运时自动完成转换,省掉 CPU 的转换时间。我在一个项目里靠这个把帧率从 35fps 提到了 58fps。

双缓冲与帧缓冲配置

双缓冲是解决屏幕撕裂的经典方案。TouchGFX 在后台画下一帧,同时 LTDC 在往前台送当前帧。画完了,交换指针,LTDC 自动从新缓冲读数据。

配置双缓冲需要两块连续的内存区域:

// 以 RGB565 480x272 为例
// 每帧大小 = 480 * 272 * 2 = 261120 字节
#define FRAME_BUFFER_SIZE  (480 * 272 * 2)

// 在 SDRAM 中分配两块连续区域
uint8_t frame_buffer0[FRAME_BUFFER_SIZE] __attribute__((section(".sdram")));
uint8_t frame_buffer1[FRAME_BUFFER_SIZE] __attribute__((section(".sdram")));

TouchGFX 的 HAL 层会管理这两个缓冲的切换。你只需要在初始化时把两个地址告诉它:

hal.setFrameBufferStartAddresses(
    (uint16_t*)frame_buffer0,
    (uint16_t*)frame_buffer1
);

这里有个关键点:LTDC 的当前帧缓冲地址和 TouchGFX 的后台缓冲地址不能冲突。TouchGFX 会在 VSYNC 中断里做交换,所以中断响应要快。我习惯把中断优先级设成最高,不然一旦被其他任务抢占,就会出现半帧撕裂。

避坑:SDRAM 的访问速度直接影响帧率。如果 SDRAM 时钟太低,或者总线仲裁配置不当,DMA2D 读显存时会等很久。我曾经在 F429 上遇到过,SDRAM 配成了 90MHz,结果 LTDC 读数据跟不上,屏幕出现水平条纹。后来提到 120MHz 才解决。

整体架构图

下面这张图把 LTDC、DMA2D、双缓冲和 TouchGFX 的关系画清楚了:

LTDC + DMA2D + 双缓冲架构 TouchGFX 引擎 生成绘制命令 DMA2D 格式转换 + 混合 双缓冲 (SDRAM) 前台缓冲 (显示中) 后台缓冲 (绘制中) LTDC 控制器 按时序推送到屏幕 LCD 屏幕 480x272 / 800x480 绘制命令 写入后台缓冲 读取前台缓冲 像素时钟 + 数据 VSYNC 中断 (缓冲交换) 关键流程: 1. TouchGFX 生成绘制命令 → DMA2D 执行格式转换和混合 2. DMA2D 将结果写入后台缓冲 → LTDC 从后台缓冲读取并推送到屏幕 3. VSYNC 中断触发缓冲交换,前台变后台,后台变前台

从图上你能看到,整个流程是流水线式的。TouchGFX 只管发命令,DMA2D 负责干活,LTDC 负责输出。只要每个环节不堵车,60fps 稳稳的。

嗯,这一章的内容就到这儿。LTDC 时序算对了,图层配好了,DMA2D 用上了,双缓冲切起来了,你的屏幕基本就稳了。剩下的就是调调参数,看看实际效果。