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