3. DMA传输模式详解:普通模式、循环模式、外设流控模式、直接模式与FIFO模式
DMA传输模式,说白了就是告诉DMA控制器「怎么搬数据、搬完一次怎么办」。选错了模式,轻则浪费带宽,重则数据错乱。我刚开始用F407时,就因为在循环模式里忘了清标志位,导致ADC采集的数据全是重复的旧值……嗯,从那以后我对每个模式都格外小心。
STM32F407的DMA支持5种传输模式,但别被数字吓到。它们其实可以分成两组:传输调度模式(普通、循环、外设流控)和数据通路模式(直接、FIFO)。咱们一个一个拆开讲。
3.1 传输调度模式
这组模式解决的是「什么时候启动传输、传输完成后怎么办」的问题。
3.1.1 普通模式(Normal Mode)
这是最基础的DMA模式。配置好源地址、目标地址、传输数量后,DMA收到一次请求就搬一次数据。搬完设定的数量后,DMA通道自动关闭,产生传输完成中断。
特点:
- 一次性的——搬完就停,需要软件重新使能才能再次启动
- 适合单次数据块传输,比如从Flash搬一段数据到SRAM
- 地址增量由硬件自动完成,但传输结束后地址寄存器恢复为初始值
3.1.2 循环模式(Circular Mode)
循环模式和普通模式最大的区别是:搬完一轮自动从头开始。传输计数器从设定值减到0后,自动重载为初始值,地址也回到配置时的起始地址。
这个模式在哪些场景下特别香?
- ADC连续采集:ADC不断产生数据,DMA循环写入环形缓冲区
- DAC波形输出:从内存循环读取波形数据送给DAC
- 串口接收缓冲:用循环缓冲区接收不定长数据
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接收一个字节就搬一个字节
- 对总线带宽占用比较「碎」,每次只搬少量数据
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满了才产生中断
3.3 模式选择决策树
你可能会问:这么多模式,到底怎么选?我画了一张决策图,帮你快速定位。
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
3.5 避坑指南
最后,我把这些年遇到的坑整理一下,你写代码时多留个心眼:
- 循环模式 + 中断:循环模式下,传输完成中断和半传输中断会反复触发。记得在中断服务函数里不要做耗时操作,否则会丢数据。
- FIFO阈值与数据宽度:FIFO阈值必须大于等于源数据宽度。比如源是32位,阈值不能设为1/4(4字节),因为一次突发至少要搬4字节,但32位数据一次就是4字节,阈值设为1/4刚好触发一次传输,等于没用FIFO。
- 外设流控模式 + 普通模式:这两个模式可以组合,但要注意:外设流控模式下,传输完成由外设决定,普通模式的「传输完成自动关闭」可能不会按预期触发。
- 直接模式下的地址对齐:直接模式要求源地址和目标地址的数据宽度对齐。如果源是8位、目标是32位,必须用FIFO模式来做宽度转换。
嗯,模式就讲到这里。记住一句话:没有最好的模式,只有最合适的模式。根据你的外设特性、数据量和实时性要求来选,别盲目追求「高级」模式。