DMA2D 配置流程:从时钟到启动的完整链路
DMA2D 的配置,说白了就是告诉硬件:「你要从哪里搬数据、搬到哪去、数据长什么样、什么时候开始搬」。这六个步骤缺一不可,顺序也不能乱。我刚开始接触时,就因为在复位后忘了等时钟稳定,结果 DMA2D 死活不干活——嗯,后来查了两天才发现是时序问题。
4.1 时钟使能与复位
任何外设要工作,第一步都是给时钟。DMA2D 挂在 AHB 总线上,对应的时钟使能位在 RCC 的 AHB1 外设时钟使能寄存器里。
// 使能 DMA2D 时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2DEN;
// 等待时钟稳定(我个人习惯加个小延迟)
__NOP(); __NOP(); __NOP();
时钟使能后,我建议做一次软件复位。虽然上电复位后寄存器已经是默认值,但如果你之前跑过别的程序,DMA2D 内部状态机可能还卡在某个中间态。
// DMA2D 软件复位
DMA2D->CR |= DMA2D_CR_SWRST;
while (DMA2D->CR & DMA2D_CR_SWRST); // 等待复位完成
4.2 模式选择
DMA2D 支持三种传输模式,对应不同的应用场景。我在 LVGL 图片加载中最常用的是「寄存器到存储器」模式——说白了就是纯内存搬运,不涉及任何颜色格式转换。
| 模式 | MODE[1:0] | 典型用途 |
|---|---|---|
| 寄存器到存储器 | 00 | 纯填充、纯搬运(无格式转换) |
| 存储器到存储器 | 01 | 带 PFC 的格式转换(如 RGB565→ARGB8888) |
| 存储器到存储器(带 PFC) | 10 | 带颜色查找表或 Alpha 混合的复杂转换 |
// 选择寄存器到存储器模式(纯搬运)
DMA2D->CR = (DMA2D->CR & ~DMA2D_CR_MODE) | (0x00 << DMA2D_CR_MODE_Pos);
你想想看,如果只是把解码后的位图数据从 SRAM 搬到帧缓冲,为什么要做格式转换?那不是多此一举嘛。所以纯搬运模式就够了。
4.3 源地址与目标地址设置
地址设置看起来简单,但有个坑:DMA2D 要求地址必须按像素格式对齐。比如 RGB565 是 2 字节对齐,ARGB8888 是 4 字节对齐。我曾经因为地址没对齐,导致传输到一半卡死——DMA2D 的 FIFO 会报错。
// 源地址:图片解码后的缓冲区
DMA2D->FGMAR = (uint32_t)src_buffer;
// 目标地址:帧缓冲区的起始位置
DMA2D->OMAR = (uint32_t)fb_addr;
4.4 像素格式设置
这一步决定了 DMA2D 怎么解释源数据和目标数据。对于纯搬运模式,源和目标格式必须一致,否则数据会错位。
// 源像素格式:RGB565
DMA2D->FGPFCCR = (0x05 << DMA2D_FGPFCCR_PF_Pos); // 0x05 = RGB565
// 目标像素格式:RGB565(与源一致)
DMA2D->OPFCCR = (0x05 << DMA2D_OPFCCR_PF_Pos);
像素格式编码表(部分):
| 编码值 | 格式 | 每像素字节数 |
|---|---|---|
| 0x00 | ARGB8888 | 4 |
| 0x01 | RGB888 | 3 |
| 0x05 | RGB565 | 2 |
| 0x06 | ARGB1555 | 2 |
| 0x07 | ARGB4444 | 2 |
4.5 传输尺寸与行偏移
DMA2D 的传输是按「矩形块」来定义的。你需要告诉它:每行多少个像素、总共多少行、以及行与行之间的偏移量。
// 设置传输尺寸:320 像素宽,240 像素高
DMA2D->NLR = (240 << DMA2D_NLR_NL_Pos) | (320 << DMA2D_NLR_PL_Pos);
// 行偏移:如果源和目标都是连续内存,偏移设为 0
DMA2D->FGOR = 0; // 源行偏移
DMA2D->OOR = 0; // 目标行偏移
行偏移这个参数,说白了就是「每搬完一行,地址要跳多少字节」。如果你要往 LCD 的帧缓冲里写数据,而 LCD 的帧缓冲每行可能有 padding(比如 320 像素对齐到 512 字节),那偏移量就要算好。
4.6 传输启动与完成等待
所有参数配好后,最后一步就是启动传输。启动很简单,但要注意:启动后要轮询完成标志,或者用中断。
// 启动 DMA2D 传输
DMA2D->CR |= DMA2D_CR_START;
// 等待传输完成(轮询方式)
while (!(DMA2D->ISR & DMA2D_ISR_TCIF));
// 清除完成标志
DMA2D->IFCR = DMA2D_IFCR_CTCIF;
我个人更推荐用中断方式,尤其是传输大图片时。轮询会阻塞 CPU,而 DMA2D 传输期间 CPU 完全可以去做别的事——比如准备下一帧数据。
4.7 完整配置流程图
下面这张图把整个配置流程串起来了,你可以对照着写代码,不容易漏步骤。
4.8 完整配置代码示例
把上面所有步骤串起来,就是一个完整的 DMA2D 配置函数。我习惯把它封装成 dma2d_copy_buffer,这样上层调用时只需要传源地址、目标地址和尺寸。
void dma2d_copy_buffer(void *src, void *dst, uint16_t width, uint16_t height)
{
// 1. 时钟使能
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2DEN;
__NOP();
// 2. 软件复位
DMA2D->CR |= DMA2D_CR_SWRST;
while (DMA2D->CR & DMA2D_CR_SWRST);
// 3. 模式选择:寄存器到存储器(纯搬运)
DMA2D->CR = (DMA2D->CR & ~DMA2D_CR_MODE) | (0x00 << DMA2D_CR_MODE_Pos);
// 4. 地址设置
DMA2D->FGMAR = (uint32_t)src;
DMA2D->OMAR = (uint32_t)dst;
// 5. 像素格式:RGB565
DMA2D->FGPFCCR = (0x05 << DMA2D_FGPFCCR_PF_Pos);
DMA2D->OPFCCR = (0x05 << DMA2D_OPFCCR_PF_Pos);
// 6. 传输尺寸(宽320,高240)
DMA2D->NLR = (height << DMA2D_NLR_NL_Pos) | (width << DMA2D_NLR_PL_Pos);
DMA2D->FGOR = 0;
DMA2D->OOR = 0;
// 7. 启动传输
DMA2D->CR |= DMA2D_CR_START;
// 8. 等待完成
while (!(DMA2D->ISR & DMA2D_ISR_TCIF));
DMA2D->IFCR = DMA2D_IFCR_CTCIF;
}
flush_cb 回调里调用最合适。每次 LVGL 刷新局部区域时,你只需要把脏矩形对应的缓冲区数据通过 DMA2D 搬到 LCD 的帧缓冲里,效率比 CPU 逐像素拷贝高 5-10 倍。