STM32 DMA 传输:原理、配置与串口 DMA 收发实战
👁 14 阅读 · 2026-08-14 · STM32 DMA
DMA(Direct Memory Access)让 STM32 无需 CPU 干预即可在内存与外设间搬运数据,是提高实时性能的关键技术。本文从 DMA 的核心工作流程讲起,深入解析 STM32 的 DMA 架构与传输模式,并给出基于 HAL 库的串口 DMA 收发完整配置与示例代码,帮助开发者避开常见陷阱,实现高效可靠的数据传输。
## DMA 是什么?为什么需要它?
在传统的数据传输中,CPU 需要反复读取外设数据寄存器,再写入内存,或者反过来。这种 PIO(Programmed I/O)方式占用大量 CPU 时间,尤其在高速通信(如串口、SPI、ADC)时,CPU 几乎被中断淹没。STM32 内置的 DMA 外设可以在没有 CPU 参与的情况下,直接在内存与外设之间搬运数据,传输完成后仅需一次中断通知 CPU。
DMA 的核心价值:
- 解放 CPU,提升系统实时响应能力;
- 支持批量数据传输,减少中断频率;
- 配合定时器、ADC 等可实现自动数据采集。
## STM32 DMA 内部原理
STM32 的 DMA 控制器(如 DMA1、DMA2)包含多个独立的通道(Stream),每个通道可管理一个外设的读写请求。DMA 传输的三大要素:
- **源地址**:外设数据寄存器或内存地址;
- **目标地址**:内存或外设寄存器;
- **传输数量**:要传输的数据单元个数。
传输时,DMA 控制器根据外设请求信号,自动完成以下操作:
1. 从源地址读取数据;
2. 暂存到内部缓冲区;
3. 写入目标地址;
4. 更新源/目标地址(若为递增模式),并递减传输计数;
5. 传输计数为 0 时,产生传输完成中断(可选)。
关键参数:
- **方向**:外设到内存(读)、内存到外设(写)、内存到内存;
- **地址递增**:外设地址通常固定,内存地址可自动递增;
- **数据宽度**:字节(8位)、半字(16位)、字(32位);
- **循环模式**:传输完成后自动重载配置,适合连续采样。
## 串口 DMA 收发配置步骤(HAL 库)
以 STM32F103 系列为例,使用 USART1 + DMA1 实现串口收发。
### 1. 使能 DMA 时钟并配置 DMA 通道
首先使能 DMA1 时钟,然后分别配置 TX 和 RX 的 DMA 通道。
```c
// 使能 DMA1 时钟
__HAL_RCC_DMA1_CLK_ENABLE();
// 发送 DMA 通道配置(内存到外设)
DMA_HandleTypeDef hdma_tx;
hdma_tx.Instance = DMA1_Channel4; // USART1_TX 使用 DMA1_Channel4
hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址固定
hdma_tx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_tx.Init.Mode = DMA_NORMAL; // 正常模式
HAL_DMA_Init(&hdma_tx);
// 将 DMA 句柄链接到 USART1 的 TX 句柄
__HAL_LINKDMA(&huart1, hdmatx, hdma_tx);
```
### 2. 配置接收 DMA 通道
```c
DMA_HandleTypeDef hdma_rx;
hdma_rx.Instance = DMA1_Channel5; // USART1_RX 使用 DMA1_Channel5
hdma_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_rx.Init.Mode = DMA_NORMAL;
HAL_DMA_Init(&hdma_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_rx);
```
### 3. 配置串口并启用 DMA 中断
在串口初始化后,启动 DMA 收发并开启 DMA 中断(用于处理传输完成)。
```c
// 启动 DMA 接收(接收缓冲区为 rx_buf,长度为 RX_BUF_SIZE)
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
// 开启 DMA 中断(TX 和 RX 通道)
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
```
## 完整示例代码
以下是一个使用 DMA 实现串口回环(接收什么发什么)的示例:
```c
#include "stm32f1xx_hal.h"
#define RX_BUF_SIZE 64
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_tx;
DMA_HandleTypeDef hdma_rx;
uint8_t rx_buf[RX_BUF_SIZE];
void DMA_Config(void);
void UART_Config(void);
int main(void)
{
HAL_Init();
UART_Config();
DMA_Config();
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
while (1)
{
// 主循环可以处理其他任务
}
}
void DMA_Config(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
// TX DMA
hdma_tx.Instance = DMA1_Channel4;
hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_tx.Init.Mode = DMA_NORMAL;
HAL_DMA_Init(&hdma_tx);
__HAL_LINKDMA(&huart1, hdmatx, hdma_tx);
// RX DMA
hdma_rx.Instance = DMA1_Channel5;
hdma_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_rx.Init.Mode = DMA_NORMAL;
HAL_DMA_Init(&hdma_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_rx);
// 使能 DMA 中断
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}
void UART_Config(void)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&huart1);
}
// 接收完成回调(在 DMA 中断中调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
// 将接收到的数据发回
HAL_UART_Transmit_DMA(&huart1, rx_buf, RX_BUF_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
}
void DMA1_Channel4_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_tx);
}
void DMA1_Channel5_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_rx);
}
```
## 注意事项与常见陷阱
- **DMA 通道映射**:不同 STM32 系列、不同外设使用的 DMA 通道/流不同,务必查阅参考手册(如 USART1_TX 在 F1 是 DMA1_Channel4,在 F4 是 DMA2_Stream7)。
- **缓冲区生命周期**:DMA 是异步传输,启动后必须保证源/目标缓冲区在传输期间不被释放或修改;尽量使用全局或静态数组。
- **循环模式与正常模式**:正常模式一次传输完毕即停止,需要重新启动;循环模式自动重载,适合持续采样,但要注意数据处理及时性。
- **对齐问题**:外设寄存器通常按字节对齐,内存地址递增时建议将缓冲区按自然边界对齐,避免性能损失或硬错误。
- **DMA 与缓存一致性**:在带 Cache 的 Cortex-M7 系列(如 STM32H7)中,需要处理 Cache 一致性问题,使用 __HAL_DMA_CLEAR_FLAG 或调用 SCB_CleanDCache/SCB_InvalidateDCache。
- **中断优先级**:DMA 中断优先级应合理设置,避免与高实时性中断冲突;接收超时检测(如空闲中断)常与 DMA 配合使用,实现不定长数据接收。
## 总结
DMA 是嵌入式系统中提升数据吞吐量的核心外设。通过合理配置 STM32 的 DMA 通道,可以轻松实现串口零 CPU 占用收发。掌握 DMA 原理和 HAL 库配置流程,再结合中断和空闲检测,能够构建出高效可靠的通信系统。开发时注意通道映射、缓冲区和 Cache 等细节,DMA 将成为你的得力助手。