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);  // 等待复位完成
注意:复位位是自清零的,所以 while 循环会等到硬件自动清除该位。千万不要在复位期间去配置其他寄存器,否则行为未定义。

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;
小技巧:如果你用 malloc 分配缓冲区,记得用 aligned_alloc 或自己手动对齐。我一般用 32 字节对齐,这样还能顺便利用 Cache Line 的优势。

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 完全可以去做别的事——比如准备下一帧数据。

避坑:启动传输后,不要再修改任何配置寄存器,直到传输完成。我曾经在调试时手贱改了源地址,结果 DMA2D 直接挂死,连错误标志都没置位。

4.7 完整配置流程图

下面这张图把整个配置流程串起来了,你可以对照着写代码,不容易漏步骤。

DMA2D 配置流程 DMA2D 配置六步流程 ① 时钟使能 ② 软件复位 ③ 模式选择 ④ 源/目标地址设置 ⑤ 像素格式 + 尺寸 ⑥ 启动传输 ⚠ 启动后不要修改配置寄存器,等待 TCIF 标志置位后再进行下一步操作 建议使用中断方式,避免轮询阻塞 CPU

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;
}
经验之谈:这个函数在 LVGL 的 flush_cb 回调里调用最合适。每次 LVGL 刷新局部区域时,你只需要把脏矩形对应的缓冲区数据通过 DMA2D 搬到 LCD 的帧缓冲里,效率比 CPU 逐像素拷贝高 5-10 倍。