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 的关系画清楚了:
从图上你能看到,整个流程是流水线式的。TouchGFX 只管发命令,DMA2D 负责干活,LTDC 负责输出。只要每个环节不堵车,60fps 稳稳的。
嗯,这一章的内容就到这儿。LTDC 时序算对了,图层配好了,DMA2D 用上了,双缓冲切起来了,你的屏幕基本就稳了。剩下的就是调调参数,看看实际效果。