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 控制器根据外设请求信号,自动完成以下操作:
- 从源地址读取数据;
- 暂存到内部缓冲区;
- 写入目标地址;
- 更新源/目标地址(若为递增模式),并递减传输计数;
- 传输计数为 0 时,产生传输完成中断(可选)。
关键参数:
- 方向:外设到内存(读)、内存到外设(写)、内存到内存;
- 地址递增:外设地址通常固定,内存地址可自动递增;
- 数据宽度:字节(8位)、半字(16位)、字(32位);
- 循环模式:传输完成后自动重载配置,适合连续采样。
串口 DMA 收发配置步骤(HAL 库)
以 STM32F103 系列为例,使用 USART1 + DMA1 实现串口收发。
1. 使能 DMA 时钟并配置 DMA 通道
首先使能 DMA1 时钟,然后分别配置 TX 和 RX 的 DMA 通道。
// 使能 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 通道
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 中断(用于处理传输完成)。
// 启动 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 实现串口回环(接收什么发什么)的示例:
#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 将成为你的得力助手。