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)请求。

我画了一张架构图,帮你理清关系:

STM32 DMA控制器架构图 CPU 总线矩阵 DMA1 8个数据流 DMA2 8个数据流 外设(USART/SPI/ADC...) 存储器(SRAM/Flash) FIFO(4级深度) 关键点: • 每个数据流可配置不同优先级 • 每个数据流对应8个通道请求 • DMA2的外设总线连接AHB,速度更快 避坑提醒: • 外设和存储器不能同时挂在 同一个DMA控制器上?可以,但要注意总线仲裁

个人经验:我习惯把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直接“飞了”,数据乱套。记住:先停、再配、后启。

1.6 本章小结