4、DMA2D硬件加速原理:寄存器配置、颜色格式转换与内存操作
DMA2D 这个外设,说白了就是 ST 给 Cortex-M 系列配的一个「画图加速器」。我刚开始接触时也觉得它不过是个高级 DMA,后来在项目里被刷屏卡得头皮发麻,才真正体会到——没有 DMA2D,LVGL 在高分辨率屏上根本跑不动。
这一节我们就把 DMA2D 的底裤扒干净。寄存器怎么配、颜色格式怎么转、内存拷贝和填充怎么用硬件做,一条一条说清楚。
4.1 DMA2D 到底在加速什么?
先看一张整体结构图,帮你建立直观印象。
DMA2D 的核心价值就三个字:不占 CPU。你配好寄存器,写个启动位,它就自己吭哧吭哧把活干完了。CPU 可以趁这个时间去跑 LVGL 的任务调度、处理触摸事件,或者干脆睡一觉省电。
4.2 寄存器配置:别被手册吓到
DMA2D 的寄存器其实没几个,但手册写得又臭又长。我帮你提炼出最常用的几个,配一张速查表。
| 寄存器 | 作用 | 关键位域 | 我常用的值 |
|---|---|---|---|
| CR | 控制寄存器,启动传输、选择模式 | MODE[1:0]、START、TCIE | 0x00030001(M2M_PFC + 启动) |
| NDTR | 传输像素总数 | NDT[16:0] | 图像宽 × 高 |
| FGMAR | 前景层内存地址(输入) | 32位地址 | 解码后的图像缓冲区地址 |
| FGOR | 前景层行偏移(字节) | LO[13:0] | 图像宽度 × 像素字节数 |
| FGPFCCR | 前景层像素格式 | PF[2:0] | 0x2(ARGB8888)或 0x1(RGB565) |
| OMAR | 输出内存地址 | 32位地址 | 帧缓冲地址 |
| OPFCCR | 输出像素格式 | PF[2:0] | 0x1(RGB565,如果你的屏是 16 位色) |
| OOR | 输出行偏移(字节) | LO[13:0] | 屏幕宽度 × 2(RGB565 每像素 2 字节) |
4.3 颜色格式转换:ARGB8888 → RGB565
这是 LVGL 加载外部存储图片时最常用的场景。解码库(比如 libjpeg、PNGlib)吐出来的通常是 ARGB8888,但你的屏大概率是 RGB565。用 CPU 逐像素转换?太慢了。DMA2D 一行代码都不用写,配好寄存器就自动转。
配置要点:
- 模式选 M2M_PFC(CR 寄存器的 MODE 位 = 10)
- 前景格式设 ARGB8888(FGPFCCR.PF = 010)
- 输出格式设 RGB565(OPFCCR.PF = 001)
- NDTR 填像素总数,不是字节数
举个例子,一张 320×240 的图片:
// 假设:
// src_buf — 解码后的 ARGB8888 数据
// fb_buf — 帧缓冲(RGB565)
// width = 320
// height = 240
DMA2D->CR = 0x00000000; // 先复位
DMA2D->FGMAR = (uint32_t)src_buf;
DMA2D->FGOR = 0; // 连续存储,无行偏移
DMA2D->FGPFCCR = 0x2; // ARGB8888
DMA2D->OMAR = (uint32_t)fb_buf;
DMA2D->OOR = 0; // 连续存储
DMA2D->OPFCCR = 0x1; // RGB565
DMA2D->NDTR = width * height; // 像素总数
DMA2D->CR = 0x00030001; // M2M_PFC + 启动
// 等待完成
while (DMA2D->CR & DMA2D_CR_START);
4.4 内存拷贝:P2P 模式
有时候你不需要转换格式,只是想把一块内存搬到另一块。比如双缓冲切换、帧缓冲滚动。这时候用 P2P 模式。
配置更简单:
- MODE = 00(P2P)
- 前景和输出格式设成一样
- NDTR 填像素数
DMA2D->CR = 0x00000000;
DMA2D->FGMAR = (uint32_t)src;
DMA2D->OMAR = (uint32_t)dst;
DMA2D->NDTR = pixel_count;
DMA2D->CR = 0x00000001; // P2P + 启动
while (DMA2D->CR & DMA2D_CR_START);
嗯,这里要注意:P2P 模式下 FGOR 和 OOR 可以设行偏移,实现「非连续区域拷贝」。比如只拷贝屏幕中间一块矩形区域,而不是整帧。这在 LVGL 的局部刷新里很有用。
4.5 内存填充:R2M 模式
清屏、画纯色背景、填充矩形——这些操作在 LVGL 里非常频繁。用 CPU 写循环填充?太浪费了。R2M 模式直接从寄存器拿颜色值往内存里填,速度是 memcpy 的 3~5 倍。
配置要点:
- MODE = 11(R2M)
- 不需要设 FGMAR,颜色值写在 FGPFCCR 的高 24 位
- OPFCCR 决定输出格式
// 用红色填充整个帧缓冲(RGB565 格式)
uint16_t color = 0xF800; // 红色
DMA2D->CR = 0x00000000;
DMA2D->OMAR = (uint32_t)fb_buf;
DMA2D->OOR = 0;
DMA2D->OPFCCR = 0x1; // RGB565
DMA2D->NDTR = width * height;
DMA2D->FGPFCCR = (color << 16) | 0x1; // 颜色值放在高16位,低3位是格式
DMA2D->CR = 0x00030001; // R2M + 启动
while (DMA2D->CR & DMA2D_CR_START);
4.6 中断与同步:别让 CPU 空等
上面例子都用了 while 轮询等待。实际项目中,我更推荐用中断:
- 使能 CR 寄存器的 TCIE 位(传输完成中断)
- 在中断服务函数里清标志位,然后通知 LVGL 刷新完成
- CPU 在 DMA2D 干活期间可以去处理其他任务
// 启动时使能中断
DMA2D->CR = 0x00030003; // M2M_PFC + TCIE + START
// 中断服务函数
void DMA2D_IRQHandler(void) {
if (DMA2D->ISR & DMA2D_ISR_TCIF) {
DMA2D->IFCR = DMA2D_IFCR_CTCIF; // 清标志
// 通知 LVGL:传输完成,可以刷新了
lv_disp_flush_ready(&disp_drv);
}
}
我个人习惯在 LVGL 的 disp_flush_cb 回调里启动 DMA2D 传输,然后在中断里调用 lv_disp_flush_ready。这样整个刷屏流程完全不阻塞 CPU,帧率能再提一档。
4.7 避坑指南:DMA2D 的「脾气」
DMA2D 虽然快,但有几个地方容易翻车:
| 问题 | 现象 | 原因 | 解决办法 |
|---|---|---|---|
| 图像花屏 | 颜色错乱、条纹 | FGOR/OOR 行偏移算错 | 行偏移 = 图像宽度 × 每像素字节数,不是像素数 |
| 传输卡死 | START 位一直为 1 | NDTR 填了 0 或地址不对齐 | 检查 NDTR 是否 > 0,地址是否 4 字节对齐 |
| 颜色偏色 | 整体色调不对 | 前景/输出格式不匹配 | 确认 FGPFCCR 和 OPFCCR 的 PF 位 |
| 中断不触发 | ISR 标志位一直为 0 | NVIC 没使能或优先级配置错 | 检查 NVIC_EnableIRQ(DMA2D_IRQn) |