4、DMA传输模式:一维传输、二维传输、FIFO模式、AB同步传输、数据传输对齐要求

DMA,说白了就是让数据自己跑,别老麻烦CPU。我做了这么多年嵌入式,发现很多性能瓶颈其实都卡在数据传输上。CPU本来该干正事,结果老被中断去搬数据,效率能高才怪。

今天咱们聊聊DMA的几种传输模式。嗯,这些模式选对了,系统性能能翻倍;选错了,调试起来能让你怀疑人生。

4.1 一维传输:最基础,也最常用

一维传输,就是数据在内存里连续搬。比如从ADC采样的缓冲区搬到内存,或者从内存搬到SPI发送寄存器。

我个人习惯把一维传输叫做「线性搬砖」。源地址和目的地址都是线性递增的,每次触发搬一个数据块。

关键参数:

  • 源地址起始值
  • 目的地址起始值
  • 传输长度(字节/半字/字)
  • 传输次数(循环次数)

举个例子,从外设接收256个字节:

// 伪代码示意
DMA_Config.SRC_ADDR = (uint32_t)&UART_RX_REG;
DMA_Config.DST_ADDR = (uint32_t)rx_buffer;
DMA_Config.TRANSFER_SIZE = 1;    // 每次传1字节
DMA_Config.BLOCK_COUNT = 256;    // 传256次
DMA_Config.SRC_INC = 0;          // 外设地址不递增
DMA_Config.DST_INC = 1;          // 内存地址递增

我在项目中遇到过一个问题:一维传输时,如果源地址和目的地址的位宽不匹配,比如源是8位,目的是32位,那就要小心了。有些DMA控制器会自动帮你对齐,有些不会。不会对齐的,数据就乱了。

4.2 二维传输:矩阵数据的神器

二维传输,说白了就是搬一个二维数组。比如图像处理中的一帧数据,或者矩阵运算中的子矩阵。

你想想看,如果图像是640×480的,用一维传输你得手动计算每一行的偏移地址,多麻烦。二维传输直接搞定:

  • 第一维:每行有多少个数据
  • 第二维:有多少行
  • 行步长:每行之间的地址间隔

我的经验:二维传输最常用的场景是图像裁剪。比如摄像头输出的是1920×1080,你只需要中间640×480的区域。用二维传输,设置好起始地址和行步长,DMA自动跳过不需要的部分。

为什么会这样?因为二维传输内部有两个计数器:一个计当前行还剩多少数据,一个计还剩多少行。行结束时,地址自动加上步长跳到下一行起始位置。

// 二维传输配置示例
DMA_Config.XFER_MODE = DMA_2D;
DMA_Config.SRC_ADDR = (uint32_t)image_src;
DMA_Config.DST_ADDR = (uint32_t)image_dst;
DMA_Config.X_COUNT = 640;        // 每行640像素
DMA_Config.Y_COUNT = 480;        // 共480行
DMA_Config.X_STEP = 4;           // 每个像素4字节(RGBA)
DMA_Config.Y_STEP = 1920 * 4;    // 源图像每行宽度

4.3 FIFO模式:缓冲的艺术

FIFO模式,就是DMA内部有个小缓存。数据先到FIFO里排队,攒够了再发出去。

我记得刚开始做DSP时,总觉得FIFO是个累赘,多此一举。后来发现,没有FIFO,高速外设和低速内存之间根本没法配合。

FIFO模式有几个好处:

  • 减少总线占用:攒够一次突发传输的长度再发起请求
  • 匹配速率:高速外设的数据先存FIFO,内存慢慢读
  • 数据对齐:FIFO可以自动处理位宽转换

注意:FIFO深度是有限的。我曾经在一个项目中,外设数据速率太高,FIFO深度不够,导致数据溢出。后来不得不降低外设时钟,或者增大FIFO阈值。

FIFO的阈值设置很关键。阈值设得太低,频繁触发传输,总线效率低;设得太高,容易溢出。我个人习惯设到FIFO深度的一半左右。

4.4 AB同步传输:乒乓操作的灵魂

AB同步传输,也叫乒乓传输。两个缓冲区A和B,DMA往A里写数据时,CPU处理B里的数据;下一轮反过来。

说白了就是「你干你的,我干我的」,谁也不等谁。

我做过一个音频处理项目,采样率48kHz,每帧1024个样本。如果用单缓冲区,CPU必须等DMA传完才能处理,这段时间音频就断了。用AB同步传输,DMA往A里写的时候,CPU处理B里的数据,无缝衔接。

AB同步传输的关键:

  • 两个缓冲区大小相同
  • DMA完成中断里切换缓冲区指针
  • CPU和DMA不能同时访问同一个缓冲区
// AB同步传输中断处理
void DMA_IRQHandler(void) {
    if (DMA_GetFlag(DMA_FLAG_TC)) {
        // 当前使用的缓冲区处理完成
        current_buf = (current_buf == BUF_A) ? BUF_B : BUF_A;
        DMA_SetBuffer(current_buf);
        // 通知CPU处理另一个缓冲区
        ProcessBuffer((current_buf == BUF_A) ? BUF_B : BUF_A);
    }
}

4.5 数据传输对齐要求:踩坑最多的地方

对齐要求,说白了就是数据地址必须是某个值的整数倍。比如32位数据要求4字节对齐,16位数据要求2字节对齐。

为什么要有对齐要求?因为DMA控制器内部的总线宽度是固定的。如果地址不对齐,DMA需要拆分成多次访问,效率低不说,有些DMA干脆不支持。

数据类型 对齐要求 常见问题
8位(字节) 1字节对齐 基本没问题
16位(半字) 2字节对齐 地址为奇数时出错
32位(字) 4字节对齐 地址不是4的倍数时出错
64位(双字) 8字节对齐 ARM Cortex-M7常见

我曾经踩过的坑:在一个项目中,用DMA从以太网控制器接收数据包。以太网数据包长度是随机的,有时候是奇数长度。我把数据放到一个32位对齐的缓冲区里,结果最后一个字节的位置不对,整个数据包解析失败。

后来我加了一个字节对齐的中间缓冲区,或者用memcpy把不对齐的数据拷贝到对齐的缓冲区里。虽然多了一次拷贝,但至少不会出错。

对齐问题的排查方法:

  • 检查源地址和目的地址是否满足对齐要求
  • 检查传输长度是否是对齐单位的整数倍
  • 检查DMA控制器的数据宽度设置是否正确

4.6 知识体系总览

下面这张图,我把这几种传输模式的关系画出来了。你看一眼就能明白它们各自的位置:

DMA传输模式知识体系 DMA传输模式 一维传输 线性搬砖,连续地址 二维传输 矩阵数据,行步长 FIFO模式 缓冲排队,速率匹配 AB同步传输 乒乓操作,无缝切换 数据传输对齐要求(1/2/4/8字节对齐) 所有传输模式都受对齐要求约束,选错模式或忽略对齐都会导致数据错误

嗯,这几种模式在实际项目中经常组合使用。比如图像采集:用二维传输搬一帧数据,用AB同步传输实现双缓冲,用FIFO匹配摄像头和内存的速率,最后还要保证缓冲区地址对齐。

我的建议:刚开始学DMA时,先把一维传输玩熟。一维传输搞明白了,二维传输就是加个维度,FIFO就是加个缓存,AB同步就是加个缓冲区切换。别一上来就想搞复杂的,容易把自己绕进去。

最后说一句,DMA的调试其实挺痛苦的。数据不对,你分不清是DMA配置错了,还是对齐问题,还是外设没准备好。我的习惯是:先关掉DMA,用CPU手动搬一次数据,确认外设和内存都没问题,再开DMA。这样能排除很多干扰因素。


公众号:蓝海资料掘金营,微信deep3321