1. STM32与DMA基础:工作原理、控制器架构、传输方向与模式、FIFO与突发传输
DMA,全称Direct Memory Access,直接存储器访问。说白了,就是让数据自己“走路”,不用CPU一趟一趟地搬。
我刚开始做嵌入式那会儿,总觉得DMA是个可有可无的“高级功能”。直到有一次做一个高速数据采集的项目,CPU一边要处理ADC数据,一边要刷新LCD,还要响应按键——结果系统直接卡死。嗯,从那以后,我再也不敢小看DMA了。
1.1 DMA工作原理:为什么需要它?
传统的数据传输方式是这样的:CPU从外设读取数据→存入寄存器→再写入内存。每一步都要CPU参与。如果数据量一大,CPU就被“绑死”在搬运工作上,没法干正事。
DMA的出现,相当于给系统配了一个“专职搬运工”。CPU只需要告诉DMA:从哪里搬、搬到哪里、搬多少,然后就可以去处理其他任务了。搬运工干完活,会发个中断通知CPU:“搞定了!”
核心要点:DMA传输的本质是——总线控制权的临时移交。DMA控制器在传输期间成为总线主机,CPU则释放总线或进入等待状态。
1.2 STM32 DMA控制器架构
STM32的DMA控制器,不同系列略有差异。以最常用的STM32F4系列为例,它有两个DMA控制器:DMA1和DMA2。每个控制器有8个数据流(Stream),每个数据流对应8个通道(Channel)请求。
我画了一张架构图,帮你理清关系:
个人经验:我习惯把DMA2留给高速外设(比如ADC、DAC、定时器),因为DMA2挂在AHB总线上,带宽更高。DMA1则处理USART、SPI这类中低速通信。这样分配,能有效避免总线冲突。
1.3 传输方向与模式
DMA传输方向有三种,很好理解:
| 传输方向 | 描述 | 典型应用场景 |
|---|---|---|
| 外设→内存 | 从外设数据寄存器读取,写入内存 | ADC采集、USART接收 |
| 内存→外设 | 从内存读取,写入外设数据寄存器 | DAC输出、USART发送 |
| 内存→内存 | 从一个内存地址拷贝到另一个内存地址 | 数据搬移、图像处理缓冲区交换 |
传输模式上,STM32支持两种:
- 普通模式(Normal):传输完设定的数据量后,DMA自动停止。需要重新使能才能再次启动。
- 循环模式(Circular):传输完一轮后,自动从头开始。适合连续数据流,比如麦克风采集、连续ADC采样。
我曾经踩过的坑:做音频播放时,用了普通模式,结果DMA只播了一次就停了,声音“噗”一下没了。后来改成循环模式,配合双缓冲,才实现流畅播放。记住:连续数据流,一定要用循环模式。
1.4 FIFO与突发传输
FIFO是DMA控制器内部的一个小缓存。STM32的DMA FIFO深度是4级(即4个字,每个字32位)。
为什么要加FIFO?你想想看,如果外设每次只产生1个字节的数据,DMA就要频繁访问总线。有了FIFO,可以等数据攒到一定量再一次性搬运,效率高得多。
FIFO的阈值可以配置:
| FIFO阈值 | 触发传输的数据量 | 适用场景 |
|---|---|---|
| 1/4 | 1个字 | 低延迟要求,数据量小 |
| 1/2 | 2个字 | 平衡延迟与效率 |
| 3/4 | 3个字 | 较高效率,可接受一定延迟 |
| 满 | 4个字 | 最高效率,延迟最大 |
突发传输(Burst Transfer)是和FIFO配合使用的。当FIFO达到阈值时,DMA会以“突发”方式连续传输多个数据,而不是一个一个地传。突发传输的规模可以是4、8、16个节拍(beat)。
效率对比:
- 无FIFO + 单次传输:每次传输都要仲裁总线,效率低
- FIFO + 突发传输:一次仲裁,连续传输多个数据,总线利用率大幅提升
举个例子:配置FIFO阈值为1/2(2个字),突发传输规模为4个节拍。那么当FIFO里攒够2个字时,DMA会一口气从源地址读取4个数据,写入目标地址。这比单次传输快了4倍。
我的配置习惯:对于SPI、USART这类外设,我通常把FIFO阈值设为1/2,突发规模设为4。既保证响应速度,又不浪费总线带宽。如果是内存到内存的批量拷贝,我会把FIFO阈值设为满,突发规模设为16,追求极致吞吐量。
1.5 一个简单的DMA配置示例
下面是一个配置DMA1数据流0,将USART2接收数据搬运到内存缓冲区的代码片段:
// 使能DMA1时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
// 等待DMA1数据流0复位完成
while (DMA1->LISR & DMA_LISR_TCIF0);
// 配置DMA1数据流0
DMA1_Stream0->CR = 0; // 先清零控制寄存器
// 外设地址(USART2数据寄存器)
DMA1_Stream0->PAR = (uint32_t)&USART2->DR;
// 内存地址(缓冲区)
DMA1_Stream0->M0AR = (uint32_t)uart_rx_buffer;
// 传输数据量
DMA1_Stream0->NDTR = BUFFER_SIZE;
// 配置:外设到内存、外设地址递增关闭、内存地址递增开启
// 数据宽度8位、普通模式、优先级中等
DMA1_Stream0->CR = DMA_SxCR_CHSEL_2 // 通道4(USART2_RX)
| DMA_SxCR_DIR_0 // 外设到内存
| DMA_SxCR_PSIZE_0 // 外设数据宽度8位
| DMA_SxCR_MSIZE_0 // 内存数据宽度8位
| DMA_SxCR_MINC // 内存地址递增
| DMA_SxCR_TCIE; // 传输完成中断使能
// 使能数据流
DMA1_Stream0->CR |= DMA_SxCR_EN;
注意:配置DMA时,一定要先关闭数据流(CR寄存器的EN位清零),再修改配置参数。我曾经在数据流还在运行时直接改了NDTR,结果DMA直接“飞了”,数据乱套。记住:先停、再配、后启。