3. DMA传输模式详解:普通模式、循环模式、外设流控模式、直接模式与FIFO模式

DMA传输模式,说白了就是告诉DMA控制器「怎么搬数据、搬完一次怎么办」。选错了模式,轻则浪费带宽,重则数据错乱。我刚开始用F407时,就因为在循环模式里忘了清标志位,导致ADC采集的数据全是重复的旧值……嗯,从那以后我对每个模式都格外小心。

STM32F407的DMA支持5种传输模式,但别被数字吓到。它们其实可以分成两组:传输调度模式(普通、循环、外设流控)和数据通路模式(直接、FIFO)。咱们一个一个拆开讲。

3.1 传输调度模式

这组模式解决的是「什么时候启动传输、传输完成后怎么办」的问题。

3.1.1 普通模式(Normal Mode)

这是最基础的DMA模式。配置好源地址、目标地址、传输数量后,DMA收到一次请求就搬一次数据。搬完设定的数量后,DMA通道自动关闭,产生传输完成中断。

特点:

  • 一次性的——搬完就停,需要软件重新使能才能再次启动
  • 适合单次数据块传输,比如从Flash搬一段数据到SRAM
  • 地址增量由硬件自动完成,但传输结束后地址寄存器恢复为初始值
我的习惯:普通模式常用于「一次性加载」场景。比如系统启动时,用DMA把字库从外部Flash搬到内部SRAM。搬完一次就够了,不需要循环。

3.1.2 循环模式(Circular Mode)

循环模式和普通模式最大的区别是:搬完一轮自动从头开始。传输计数器从设定值减到0后,自动重载为初始值,地址也回到配置时的起始地址。

这个模式在哪些场景下特别香?

  • ADC连续采集:ADC不断产生数据,DMA循环写入环形缓冲区
  • DAC波形输出:从内存循环读取波形数据送给DAC
  • 串口接收缓冲:用循环缓冲区接收不定长数据
我曾经踩过的坑:循环模式下,如果CPU读取缓冲区的速度跟不上DMA写入速度,就会读到「半新半旧」的数据。解决方案是用「半传输中断」和「传输完成中断」来划分缓冲区,保证CPU只读「安全区」的数据。

3.1.3 外设流控模式(Peripheral Flow Control Mode)

这个模式比较特殊。普通模式和循环模式都是由DMA控制器决定传输节奏,但外设流控模式把控制权交给了外设。

什么意思呢?就是外设通过硬件握手信号告诉DMA:「我现在准备好了,你可以搬数据了」。DMA不再主动发起传输,而是被动等待外设的请求。

适用场景:

  • 外设的数据产生速率不可预测,比如SDIO、以太网MAC
  • 需要外设精确控制每次传输的时机
  • 多通道DMA传输时,由外设仲裁优先级

关键区别:

特性 普通模式 循环模式 外设流控模式
传输完成后行为 关闭通道 自动重载 由外设决定
传输节奏控制者 DMA DMA 外设
典型应用 单次数据搬移 连续数据流 SDIO、以太网
是否需要软件干预 每次传输完需要 不需要 由外设事件触发

3.2 数据通路模式

这组模式解决的是「数据从源到目标的路径怎么走」的问题。F407的DMA内部有一个4字(16字节)的FIFO,用不用这个FIFO,就是直接模式和FIFO模式的区别。

3.2.1 直接模式(Direct Mode)

直接模式下,DMA不使用内部FIFO。数据从源外设/内存直接送到目标外设/内存。每次传输请求,DMA只搬一次数据(数据宽度由配置决定)。

特点:

  • 延迟最低——数据不经过FIFO缓冲
  • 适合外设到内存的简单传输,比如UART接收一个字节就搬一个字节
  • 对总线带宽占用比较「碎」,每次只搬少量数据
我建议:如果外设数据速率不高(比如UART、I2C),直接模式完全够用。没必要为了用FIFO而用FIFO,反而增加延迟。

3.2.2 FIFO模式(FIFO Mode)

FIFO模式下,DMA先把数据攒到内部FIFO里,攒到一定阈值(1/4、1/2、3/4或满)后,再一次性写入目标地址。

阈值配置:

FIFO阈值 触发写入条件 典型场景
1/4(4字节) FIFO中有4字节时触发 低延迟要求
1/2(8字节) FIFO中有8字节时触发 平衡模式
3/4(12字节) FIFO中有12字节时触发 高总线利用率
满(16字节) FIFO满时触发 批量传输

FIFO模式的好处:

  • 减少总线占用次数——一次突发传输代替多次单次传输
  • 支持数据宽度转换——比如源是8位,目标是32位,FIFO可以自动打包
  • 降低CPU干预频率——FIFO满了才产生中断
我曾经犯过的错:有一次做音频采集,ADC输出16位数据,我配置了FIFO模式且阈值设为满。结果发现音频有「卡顿感」。后来分析才发现,FIFO满了才触发传输,导致数据到达目标缓冲区的时间不均匀。解决方案是把阈值降到1/4,牺牲一点总线效率,换来了平滑的数据流。

3.3 模式选择决策树

你可能会问:这么多模式,到底怎么选?我画了一张决策图,帮你快速定位。

DMA传输模式选择决策树 选择DMA传输模式 传输调度模式 数据通路模式 普通模式 循环模式 外设流控模式 直接模式 FIFO模式 传输调度模式选择建议 • 单次传输 → 普通模式 • 连续数据流 → 循环模式 • 外设控制节奏 → 外设流控模式 数据通路模式选择建议 • 低延迟、简单传输 → 直接模式 • 需要数据宽度转换 → FIFO模式 • 减少总线占用 → FIFO模式 核心原则:传输调度模式决定「何时搬」,数据通路模式决定「怎么搬」 两种模式独立配置,可以任意组合使用

3.4 模式组合实战

实际项目中,传输调度模式和数据通路模式是独立配置的。你可以组合出各种搭配。我列几个常用组合:

组合 配置 典型场景 注意事项
普通 + 直接 DMA_SxCR.CIRC=0, DMDIS=0 单次UART发送 传输完需重新使能
循环 + 直接 DMA_SxCR.CIRC=1, DMDIS=0 ADC连续采集 注意缓冲区半满中断
循环 + FIFO DMA_SxCR.CIRC=1, DMDIS=1 音频数据流 FIFO阈值影响实时性
外设流控 + FIFO DMA_SxCR.PFCTRL=1, DMDIS=1 SDIO多块读写 外设必须支持流控信号

配置代码示例(循环模式 + FIFO模式,用于ADC采集):

// 使能DMA2时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;

// 等待DMA就绪
while(DMA2->LISR & DMA_LISR_TCIF0);

// 配置DMA2数据流0
DMA2_Stream0->CR = 0;  // 先清零

// 设置外设地址(ADC数据寄存器)
DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;

// 设置内存地址(缓冲区)
DMA2_Stream0->M0AR = (uint32_t)adc_buffer;

// 设置传输数量
DMA2_Stream0->NDTR = BUFFER_SIZE;

// 配置控制寄存器
DMA2_Stream0->CR = (0 << DMA_SxCR_CHSEL_Pos)   // 通道0
    | (1 << DMA_SxCR_MBURST_Pos)               // 内存突发传输(4拍)
    | (1 << DMA_SxCR_PBURST_Pos)               // 外设突发传输(4拍)
    | (2 << DMA_SxCR_PL_Pos)                   // 优先级高
    | (1 << DMA_SxCR_MSIZE_Pos)                // 内存数据宽度16位
    | (0 << DMA_SxCR_PSIZE_Pos)                // 外设数据宽度8位
    | (1 << DMA_SxCR_MINC_Pos)                 // 内存地址递增
    | (0 << DMA_SxCR_PINC_Pos)                 // 外设地址固定
    | (1 << DMA_SxCR_CIRC_Pos)                 // 循环模式
    | (0 << DMA_SxCR_DIR_Pos)                  // 外设到内存
    | (1 << DMA_SxCR_PFCTRL_Pos)               // 外设流控模式
    | (3 << DMA_SxCR_FTH_Pos)                  // FIFO阈值:满
    | (1 << DMA_SxCR_DMDIS_Pos)                // 使能FIFO模式
    | (1 << DMA_SxCR_EN_Pos);                  // 使能DMA
我的调试经验:配置完DMA后,先别急着跑业务代码。先读一下DMA_SxCR的EN位确认使能成功,再读NDTR确认传输数量正确。这两个寄存器是DMA是否正常工作的「晴雨表」。

3.5 避坑指南

最后,我把这些年遇到的坑整理一下,你写代码时多留个心眼:

  • 循环模式 + 中断:循环模式下,传输完成中断和半传输中断会反复触发。记得在中断服务函数里不要做耗时操作,否则会丢数据。
  • FIFO阈值与数据宽度:FIFO阈值必须大于等于源数据宽度。比如源是32位,阈值不能设为1/4(4字节),因为一次突发至少要搬4字节,但32位数据一次就是4字节,阈值设为1/4刚好触发一次传输,等于没用FIFO。
  • 外设流控模式 + 普通模式:这两个模式可以组合,但要注意:外设流控模式下,传输完成由外设决定,普通模式的「传输完成自动关闭」可能不会按预期触发。
  • 直接模式下的地址对齐:直接模式要求源地址和目标地址的数据宽度对齐。如果源是8位、目标是32位,必须用FIFO模式来做宽度转换。

嗯,模式就讲到这里。记住一句话:没有最好的模式,只有最合适的模式。根据你的外设特性、数据量和实时性要求来选,别盲目追求「高级」模式。