4. 性能分析基础

DMA这东西,说白了就是给CPU找了个搬运工。CPU本来要亲自把数据从外设搬到内存,或者从内存搬到外设,现在好了,DMA控制器替它干这活。我刚开始做嵌入式那会儿,总觉得DMA可有可无,直到有一次项目里ADC采样率提不上去,CPU被中断淹没了,才意识到DMA有多重要。

DMA传输模式:M2M、M2P、P2M

这三种模式,你想想看,其实就是数据从哪来到哪去的问题。

模式 源地址 目的地址 典型场景
M2M(内存到内存) 内存 内存 数据拷贝、缓冲区搬移
M2P(内存到外设) 内存 外设寄存器 DAC输出、SPI发送
P2M(外设到内存) 外设寄存器 内存 ADC采集、UART接收

M2M模式,我习惯叫它“内存搬运工”。比如你要把一个大数组从A区拷到B区,用CPU的memcpy当然也行,但DMA做这事不占CPU时间。我记得有个项目需要实时处理图像数据,每帧都要搬移几十KB,用DMA M2M模式后,CPU几乎零开销。

M2P模式,说白了就是CPU把数据准备好,DMA负责往外送。比如你要通过SPI发送一批数据,传统做法是CPU一个字节一个字节往SPI数据寄存器里写,写一个等一个。用DMA M2P,你只需要告诉DMA“数据在内存这里,往SPI_DR寄存器送,送完通知我”,然后CPU就可以去干别的了。

P2M模式正好反过来。外设产生数据,DMA自动收进内存。ADC采集、UART接收、I2S音频输入,这些场景我几乎都用P2M。你想想看,如果每次ADC转换完都进中断,CPU得频繁进出中断服务函数,效率能高吗?

关键点:M2M模式需要源和目的都是内存地址,而M2P/P2M中有一端是外设寄存器地址。外设地址通常是固定的,比如USART1_DR地址0x40013804,你配置DMA时直接填这个地址就行。

循环模式(Circular Mode)

这个模式太实用了。普通模式下,DMA传输完设定的数据量就停了,你得重新配置才能再启动。循环模式呢?传完一轮自动从头开始,周而复始。

我举个例子你就明白了。ADC连续采样,采样率1kHz,每次采样结果16位。你开一个256字节的缓冲区,配置DMA为P2M循环模式。ADC每转换完一次,DMA就把结果搬到缓冲区,搬完256字节后自动回到缓冲区开头继续搬。CPU只需要在缓冲区满一半时处理数据就行,不用管DMA怎么搬的。

个人经验:循环模式配合双缓冲区(ping-pong buffer)效果更好。一个缓冲区被DMA写,另一个被CPU读,互不干扰。我做过一个音频采集项目,就是用循环模式+双缓冲,CPU几乎零等待。

但要注意一点:循环模式下,DMA的传输完成中断只在第一轮结束时触发,后续轮次不会重复触发。你需要用半传输中断或自定义计数器来同步。嗯,这个坑我踩过。

突发传输(Burst Transfer)

突发传输,说白了就是一次请求搬多个数据。普通模式下,DMA每次请求只搬一个数据单元(字节、半字或字)。突发模式下,一次请求可以搬4个、8个、16个数据单元。

为什么会这样设计?因为总线仲裁是有开销的。每次DMA请求都要经过总线仲裁、地址建立、数据传送、释放总线这一套流程。如果一次请求能多搬几个数据,总线的利用率就上去了。

传输方式 一次请求传输量 总线占用时间 适用场景
单次传输 1个数据单元 短 低速外设、总线冲突多
4拍突发 4个数据单元 中 中等速度外设
8拍突发 8个数据单元 长 高速外设、内存拷贝
16拍突发 16个数据单元 最长 批量数据搬移

避坑指南:我曾经在一个项目里把DMA突发长度设成16拍,结果外设跟不上节奏,数据丢得一塌糊涂。后来改成4拍才稳定。突发长度不是越大越好,得看外设的FIFO深度和响应速度。

FIFO模式与直接模式

这两个模式,是DMA内部数据缓冲的两种策略。

直接模式(Direct Mode):数据来了就传,不缓存。外设产生一个数据,DMA立即发起总线请求,把数据搬到目的地。这种方式延迟最小,但总线请求频繁,适合数据量小、实时性要求高的场景。

FIFO模式:DMA内部有个FIFO缓冲区,数据先存到FIFO里,攒到一定量再一次性搬走。FIFO深度通常是4级或8级,可以配置触发阈值(比如FIFO半满或全满时启动传输)。

特性 直接模式 FIFO模式
数据缓存 无 有(4/8级)
总线请求频率 高 低
传输延迟 低 稍高(取决于阈值)
适合场景 实时性高、数据量小 批量传输、总线负载高
突发传输支持 通常不支持 支持

我个人习惯,做音频或高速数据采集时用FIFO模式,配合突发传输,总线效率能提升不少。做GPIO模拟时序或低速UART时用直接模式,延迟小,响应快。

配置建议:FIFO阈值一般设为半满。比如8级FIFO,阈值设4,这样FIFO里攒了4个数据就触发一次传输。既不会等太久,也不会频繁请求总线。

DMA传输模式关系图

下面这张图,把DMA的传输模式、缓冲策略和循环机制串起来了。你看一眼就能明白它们之间的关系。

DMA传输模式与缓冲机制关系图 DMA传输模式与缓冲机制 DMA控制器 M2M 内存→内存 M2P 内存→外设 P2M 外设→内存 循环模式(Circular) 直接模式(无缓冲) FIFO模式(有缓冲) 突发传输(Burst) FIFO模式通常配合突发传输使用,直接模式一般用单次传输 循环模式可应用于M2P和P2M,M2M较少使用循环模式

实际配置示例

拿STM32的DMA来举例,配置一个P2M循环模式,带FIFO和4拍突发传输。代码不长,但关键参数都在里面了。

// DMA配置:ADC采集,P2M模式,循环,FIFO半满,4拍突发
DMA_HandleTypeDef hdma_adc;

hdma_adc.Instance = DMA1_Channel1;
hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;   // P2M模式
hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;        // 外设地址不变
hdma_adc.Init.MemInc = DMA_MINC_ENABLE;            // 内存地址递增
hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  // 16位
hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc.Init.Mode = DMA_CIRCULAR;                 // 循环模式
hdma_adc.Init.Priority = DMA_PRIORITY_HIGH;

// FIFO和突发配置(STM32F4及以上)
hdma_adc.Init.FIFOMode = DMA_FIFOMODE_ENABLE;      // 使能FIFO
hdma_adc.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_HALFFULL;  // 半满触发
hdma_adc.Init.MemBurst = DMA_MBURST_4;             // 内存侧4拍突发
hdma_adc.Init.PeriphBurst = DMA_PBURST_4;          // 外设侧4拍突发

HAL_DMA_Init(&hdma_adc);

小技巧:配置完DMA后,记得检查外设的DMA请求是否使能。比如ADC的DMA请求使能位在ADC_CR2寄存器里。我见过有人配了半天DMA,结果外设根本没发请求,数据当然不动。

模式选择决策指南

这么多模式,到底怎么选?我一般按这个思路来:

  1. 先看数据流向:M2M、M2P还是P2M,这个最明确
  2. 再看是否需要连续传输:需要就开循环模式
  3. 然后看外设速度:高速外设用FIFO+突发,低速用直接模式
  4. 最后看总线负载:总线紧张时用FIFO+大突发,减少请求次数

注意:不是所有DMA控制器都支持FIFO模式和突发传输。低端MCU的DMA可能只有直接模式。选型时一定要看数据手册,别想当然。

好了,DMA的传输模式就这些。说白了就是根据你的数据量、外设速度、总线负载来选合适的组合。没有绝对的最好,只有最适合你项目的方案。