4、性能调优基础:DMA传输效率分析、总线矩阵与仲裁、Cache与预取策略
各位同学,欢迎来到第四章。前几章我们把DMA和定时器的基本用法过了一遍,从这章开始,咱们要真正进入「调优」的世界了。
性能调优,说白了就是搞清楚三件事:数据怎么走、谁在抢路、怎么让CPU少等一会儿。我刚开始做STM32项目时,总觉得DMA配置好了就能自动飞起来,结果一测实际带宽,跟理论值差了一大截。嗯,后来才发现,总线矩阵和Cache这两个东西,才是真正的幕后操盘手。
4.1 DMA传输效率:理论带宽 vs 实际吞吐
先看一个最基础的问题:DMA到底能跑多快?
STM32F4系列,DMA时钟通常是AHB时钟的一半(比如168MHz主频下,DMA时钟84MHz)。理论上,一次32位传输只需要1个时钟周期,那带宽就是:
84 MHz × 4 bytes = 336 MB/s
听起来很猛对吧?但实际项目中,我测过多次,能跑到200 MB/s就算不错了。为什么差这么多?
核心原因有三个:
- 总线仲裁等待:DMA和CPU、其他外设共享总线,谁先谁后得排队
- 源/目标地址对齐:非对齐访问会触发多次总线事务
- FIFO填充延迟:DMA FIFO没填满时,传输不会启动
我做过一个实际对比实验,大家看看数据就明白了:
| 传输场景 | 理论带宽 | 实测带宽 | 效率 |
|---|---|---|---|
| 内存→内存,32位对齐,无Cache | 336 MB/s | 218 MB/s | 65% |
| 外设→内存,8位,FIFO半满触发 | 84 MB/s | 42 MB/s | 50% |
| 内存→内存,非对齐,有Cache | 336 MB/s | 89 MB/s | 26% |
看到没有?非对齐访问直接把效率砍到四分之一。所以我的习惯是:能用32位绝不用8位,能对齐绝不对齐。
个人经验:配置DMA时,优先把源地址和目标地址都设为4字节对齐。如果数据本身是字节流,可以用DMA的FIFO做打包传输,效率能提升30%以上。
4.2 总线矩阵与仲裁:谁在抢路?
STM32内部不是一条大马路,而是一个复杂的立交桥——总线矩阵。以F4为例,它有多条总线:
- I-Bus:指令总线,CPU取指令走这里
- D-Bus:数据总线,CPU读写数据走这里
- S-Bus:系统总线,连接外设和DMA
- DMA总线:DMA专用通道
这些总线通过一个仲裁器连接到SRAM、Flash、AHB外设等从设备。仲裁规则很简单:谁优先级高谁先走。
但问题来了——DMA和CPU可能同时访问同一个SRAM区域。这时候仲裁器会怎么处理?
我曾经踩过的坑:在一个音频采集项目里,DMA把ADC数据往SRAM里搬,同时CPU也在做FFT运算。结果发现DMA传输偶尔会丢数据。查了半天,原来是CPU的D-Bus访问优先级比DMA高,CPU频繁读SRAM时,DMA被无限期阻塞了。
解决方案其实不复杂:
- 分离存储区域:DMA用的缓冲区和CPU用的数据区放在不同的SRAM块(比如F4有SRAM1和SRAM2)
- 调整优先级:在DMA控制器里设置通道优先级,必要时把DMA优先级提到最高
- 使用双缓冲:DMA写一块,CPU读另一块,交替使用
下面这张图,是我自己总结的总线矩阵访问路径,大家感受一下:
4.3 Cache与预取策略:让CPU少等一会儿
好了,总线抢路的问题解决了,但还有一个隐藏的瓶颈——Flash读取速度。
STM32的Flash运行频率通常比CPU低很多。比如F4跑168MHz,Flash只能跑到84MHz。CPU取指令时,如果每次都去Flash读,那CPU就得等——这就是所谓的Flash等待周期。
怎么解决?靠两样东西:Cache(缓存)和预取(Prefetch)。
4.3.1 指令Cache(I-Cache)
I-Cache是CPU内部的一块小容量高速存储器,它会自动缓存最近用过的指令。当CPU再次执行同一段代码时,直接从Cache取,不用再去慢吞吞的Flash了。
我习惯在启动代码里这样配置:
/* 使能I-Cache和预取 */
FLASH->ACR |= FLASH_ACR_ICEN; // 使能指令Cache
FLASH->ACR |= FLASH_ACR_PRFTEN; // 使能预取
FLASH->ACR |= FLASH_ACR_LATENCY_5WS; // 设置等待周期(根据主频调整)
注意:等待周期(Latency)必须根据主频正确设置。设少了Flash读错数据,设多了性能下降。F4在168MHz下通常需要5个等待周期。
4.3.2 预取策略
预取和Cache是搭档。预取器会预测CPU接下来要执行的指令地址,提前从Flash读到缓冲区。这样CPU需要时,数据已经准备好了。
但预取不是万能的。遇到分支跳转(if-else、函数调用)时,预取器可能猜错,预取的数据就白费了。这就是所谓的预取失效。
我做过一个测试:
| 场景 | 无Cache/预取 | 仅I-Cache | I-Cache+预取 |
|---|---|---|---|
| 顺序执行(循环) | 100% | 142% | 168% |
| 大量分支跳转 | 100% | 135% | 138% |
| 随机跳转(查表) | 100% | 108% | 110% |
看到没有?顺序执行时预取效果最好,分支多了预取基本帮不上忙。所以我的建议是:把热点代码(频繁执行的循环、中断服务函数)尽量写成顺序执行的结构,少用复杂的条件分支。
避坑指南:我曾经在一个电机控制项目里,把PID计算放在中断里,里面用了好几个if-else判断不同工况。结果预取几乎失效,中断响应时间长了30%。后来我把判断逻辑提到外面,中断里只做线性计算,性能一下就上来了。
4.4 综合调优建议
好了,把上面这些串起来,我给大家一个实战检查清单:
- DMA配置:源/目标地址4字节对齐,使用32位数据宽度,FIFO阈值设到半满以上
- 总线布局:DMA缓冲区和CPU数据区放在不同SRAM块,避免仲裁冲突
- Cache使能:I-Cache必须开,预取尽量开(除非代码全是分支跳转)
- 等待周期:根据主频正确设置Flash Latency,别为了省事设小了
- 代码结构:热点代码写成顺序执行,减少分支,让预取器发挥最大作用