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的传输模式、缓冲策略和循环机制串起来了。你看一眼就能明白它们之间的关系。
实际配置示例
拿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,结果外设根本没发请求,数据当然不动。
模式选择决策指南
这么多模式,到底怎么选?我一般按这个思路来:
- 先看数据流向:M2M、M2P还是P2M,这个最明确
- 再看是否需要连续传输:需要就开循环模式
- 然后看外设速度:高速外设用FIFO+突发,低速用直接模式
- 最后看总线负载:总线紧张时用FIFO+大突发,减少请求次数
注意:不是所有DMA控制器都支持FIFO模式和突发传输。低端MCU的DMA可能只有直接模式。选型时一定要看数据手册,别想当然。
好了,DMA的传输模式就这些。说白了就是根据你的数据量、外设速度、总线负载来选合适的组合。没有绝对的最好,只有最适合你项目的方案。