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 通道。

// 使能 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 将成为你的得力助手。