2. DMA与CPU交互机制:传输请求、应答、仲裁

咱们搞嵌入式开发的,多少都遇到过这种场景:外设数据狂飙,CPU忙着搬砖,两边抢总线抢到死机。说白了,DMA和CPU之间得有一套默契的握手协议,否则系统分分钟崩给你看。这一章我就带你把“请求—应答—仲裁”这三个环节拆开揉碎了讲透。

2.1 传输请求:DMA什么时候“举手”

DMA不是自己在那瞎干活的。它必须等外设说“我有数据了”或者“我需要数据了”才动起来。这个“举手”的动作,就是传输请求。

最常用的请求信号叫 DREQ(DMA Request)。外设把DREQ拉高,DMA控制器看到后,就会开始一次传输。我记得早年调试一个SPI DMA驱动,SDRAM带宽不足,DREQ被反复触发,结果DMA陷入“请求—重试—再请求”的死循环。后来查手册才发现,每个DMA通道都有一个请求触发模式,得配对才行。

软件触发 vs 硬件触发

类型描述典型场景
软件触发CPU写寄存器置位,立即启动DMA内存拷贝、数据搬移
硬件触发外设硬件引脚(DREQ)有效时触发ADC采样、UART接收

我个人习惯,凡是跟外设实时打交道的,一律用硬件触发。软件触发虽然方便,但CPU一旦被高优先级中断打断,延迟会飘得很厉害。

请求边沿 vs 电平

这里有个细节很多人栽过:DREQ可以是边沿触发(上升沿/下降沿),也可以是电平触发。电平触发要求外设一直保持请求有效,直到DMA应答;边沿触发则只认一次跳变。

小技巧:如果外设的FIFO深度大,用边沿触发更省线;如果FIFO很小,建议用电平触发,不易丢数据。

2.2 应答机制:CPU或仲裁器说“收到”

DMA发出请求后,谁来决定让它干?答案是总线仲裁器或DMA控制器内部的优先级逻辑。

应答信号通常叫 DACK(DMA Acknowledge)。DMA控制器在抢到总线后,会拉低DACK告诉外设:“你现在可以读/写数据了。”外设在DACK有效期间,把数据放到总线上,或者从总线上取走数据。

我记得有一次项目里,UART的DMA接收总丢第一个字节。查了三天,发现问题出在DACK时序——外设在DACK拉低前就把数据撤走了。后来在DMA配置里加了1个时钟周期的应答延迟,问题解决。嗯,这种坑特别隐蔽。

应答模式:单次还是突发

模式每次应答传输次数总线占用适合场景
单次模式1个数据单元短时占用,释放总线外设数据间断、低优先级
突发模式连续传输2/4/8/16次长时间占用,效率高外设FIFO较深、高速传输

你想想看,如果ADC每10μs就产生一次数据,用单次模式的话,DMA每次都要重新仲裁,总线切换开销很大。这时候用突发模式,一次抢到总线就搬一堆数据回去,效率能翻一倍。

2.3 仲裁逻辑:谁说了算?

多个DMA通道同时请求,或者CPU和DMA同时要用总线,就得有个裁判——总线仲裁器。

常见的仲裁策略有两种:

  • 固定优先级:比如通道0最高,通道1次之……通道N最低。简单粗暴,但低优先级通道容易被饿死。
  • 轮询/循环优先级:每个通道轮流获得访问权,相对公平,但延迟不确定。

我曾经在一个四轴飞控项目里用过固定优先级——高优先级的DMA通道负责读取陀螺仪数据,低优先级的负责日志记录。结果日志通道经常超时,因为陀螺仪数据量太大。后来改为轮询+加一个“低优先级超时抢占”机制,才稳住。

⚠ 注意:仲裁时CPU会被“暂停”吗?这要看总线架构。在多数ARM Cortex-M系列中,DMA和CPU使用多层AHB总线,可以并行访问不同外设,不需要停止CPU。但在早期的51单片机或一些低端MCU里,DMA会直接HOLD CPU,那段时间CPU啥也干不了。

2.4 握手协议:从请求到释放的完整流程

下面这张图展示了一个典型的DMA单次传输的握手序列。你一看就明白。

DMA传输握手流程(单次模式) 外设(Peripheral) DMA控制器 总线仲裁器 CPU ① DREQ 请求 ② 仲裁请求 仲裁 HOLD(可选) ③ 仲裁授权 ④ DACK ⑤ 数据传送(DMA→Memory 或 Periph→Memory) ⑥ 传输完成,释放总线 释放请求 说明: ① 外设拉高DREQ → DMA收到后向仲裁器申请总线。 ② 仲裁器根据优先级决定是否授权。若CPU当前不用总线,直接授权。 ③ DMA拉低DACK,外设将数据放到总线上;数据搬移完后释放DREQ和DACK。

看了这张图,你应该能理清:DREQ是外设到DMA的请求;DACK是DMA到外设的答复;仲裁器在中间决定总线所有权。整个过程中,DMA其实是替CPU去搬数据,搬完就释放总线,CPU继续正常工作。

2.5 仲裁中的优先级反转与死锁

说实话,仲裁不是万能的。我遇到过最头疼的问题就是优先级反转:一个低优先级DMA通道占着总线不放,高优先级请求被活活堵死。

造成这个的原因通常是突发模式太长。比如低优先级通道配置了突发16次,搬一半数据时被更高优先级打断——但有些老旧的DMA控制器不支持传输暂停,低优先级必须搬完16个数据才肯松手。高优先级只能干等。

我的建议:如果系统里多个DMA通道并跑,通道0和通道1用单次模式,其他优先级低的才用突发。这样关键数据不会被卡太久。

还有一个死锁场景:外设的DREQ永远不释放。有一次串口接收FIFO满了,外设持续拉高DREQ,DMA不断尝试应答,但FIFO已满无法写入,形成死循环。解决办法是加一个DREQ超时检测,超过合理时间就软件复位DMA通道。

2.6 实战中如何配置请求与应答

下面我以STM32的DMA控制器为例,给你一段初始化代码,标注关键点:

/* 配置DMA通道2:串口接收 */
DMA_InitTypeDef DMA_InitStruct;

DMA_InitStruct.DMA_Channel = DMA_Channel_4;          // 通道4映射到UART1_RX
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;  // 外设地址
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;        // 内存地址
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;            // 外设→内存

/* 请求源选择:硬件触发(外设DREQ) */
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;         // 循环模式
DMA_InitStruct.DMA_Priority = DMA_Priority_Low;      // 低优先级
DMA_InitStruct.DMA_BurstSize = DMA_Burst_Single;     // 突发单次,降低仲裁冲突
DMA_InitStruct.DMA_RequestNumber = DMA_Request_1;    // 触发一次传输1个数据

/* 千万别忘了配置外设侧,使能DMA请求 */
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

你看这里,我特意把突发模式配成了单次(Burst_Single),就是因为这个UART同时还要和ADC的DMA通道抢总线——低优先级,单次模式最稳。如果你把优先级设成DMA_Priority_VeryHigh,再配成突发16次,别的通道基本别想用了。

个人习惯:每个DMA通道配置完成后,先跑一个回环测试——让外设自发自收,用逻辑分析仪看DREQ和DACK的时序。如果DACK拉低后超过10个时钟周期外设还没开始读数据,那就要调请求极性或者插入等待周期。

小结

DMA和CPU的交互,说白了就是三个词:举手(请求)、同意(应答)、排队(仲裁)。搞懂了这几个节点上信号怎么走、仲裁策略怎么选、优先级如何避免反转,你就能把DMA的中断延迟压到可控范围内。嗯,记住这些坑,别像我一样在DACK时序上耗三天。