1. 项目概述:STM32串口DMA通信实战
在嵌入式开发中,串口通信是最基础也最常用的外设之一。当我们需要处理大量数据收发时,传统的轮询或中断方式会占用大量CPU资源。我在最近的一个工业传感器采集项目中,就遇到了需要实时处理多路串口数据的挑战。通过采用STM32F103C8T6的DMA功能配合HAL库,成功实现了高效率的串口通信方案。
这个方案的核心价值在于:使用DMA(直接内存访问)控制器接管数据搬运工作,让CPU从繁重的字节搬运任务中解放出来。实测表明,在115200波特率下传输1024字节数据包时,DMA方式比中断方式节省约87%的CPU占用率。对于使用STM32F103C8T6这类资源有限的Cortex-M3内核芯片来说,这种优化尤为重要。
2. 硬件设计与环境搭建
2.1 STM32F103C8T6最小系统配置
我使用的是市面上常见的"蓝 pill"最小系统板,核心配置如下:
- 主控:STM32F103C8T6(72MHz Cortex-M3)
- 串口:USART1(PA9-TX, PA10-RX)
- DMA1通道4(USART1_TX)
- DMA1通道5(USART1_RX)
注意:不同厂商的最小系统板可能使用不同的晶振频率(8M或12M),需要根据实际情况调整时钟树配置。
2.2 开发环境准备
-
工具链:
- IDE:STM32CubeIDE 1.11.0
- 固件库:STM32CubeF1 V1.8.4
- 调试器:ST-Link V2
-
工程创建步骤:
bash复制# 在CubeIDE中:
1. 新建STM32 Project → 选择MCU型号STM32F103C8Tx
2. 配置SYS→Debug: Serial Wire
3. 配置RCC→HSE: Crystal/Ceramic Resonator
4. 配置USART1为Asynchronous模式
5. 启用DMA选项卡中的USART1_RX/USART1_TX
3. DMA原理与HAL库实现
3.1 DMA工作机制详解
DMA的本质是硬件级的数据搬运工,其工作流程可分为三个关键阶段:
-
初始化阶段:
- 配置源地址(外设或内存)
- 配置目标地址(内存或外设)
- 设置传输数据量(NDTR寄存器)
-
触发阶段:
- 外设发出DMA请求(如USART的TXE/RXNE标志)
- DMA控制器接管总线控制权
-
传输阶段:
- 数据直接在外设和内存间传输
- 每完成一个数据单元传输,NDTR自动递减
c复制// 典型DMA传输时序(以串口发送为例)
USART->DR = data; // 传统方式(CPU参与)
DMA->CPAR = &USART->DR; // DMA方式(硬件自动搬运)
3.2 HAL库关键API解析
HAL库为DMA提供了高度封装的接口,主要涉及以下函数:
- 初始化函数:
c复制HAL_UART_Init(UART_HandleTypeDef *huart);
HAL_DMA_Init(DMA_HandleTypeDef *hdma);
- 传输控制函数:
c复制HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
- 回调函数(需用户实现):
c复制__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
4. 完整代码实现与解析
4.1 工程文件结构
code复制├── Core
│ ├── Inc
│ │ ├── uart_dma.h // 串口DMA配置头文件
│ ├── Src
│ │ ├── uart_dma.c // 串口DMA实现文件
├── STM32F103C8Tx_FLASH.ld // 链接脚本
4.2 关键代码实现
4.2.1 DMA初始化配置
c复制// uart_dma.c
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
void MX_DMA_Init(void) {
__HAL_RCC_DMA1_CLK_ENABLE();
// TX DMA配置
hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
HAL_DMA_Init(&hdma_usart1_tx);
// RX DMA配置(关键区别点)
hdma_usart1_rx.Instance = DMA1_Channel5;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
// 其余配置类似...
}
4.2.2 串口DMA收发实现
c复制#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];
void UART_StartReceive(void) {
// 启动DMA接收(循环模式)
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
void UART_SendData(uint8_t *data, uint16_t size) {
// 非阻塞式发送
HAL_UART_Transmit_DMA(&huart1, data, size);
// 等待发送完成(可选)
while(HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);
}
// 接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
// 处理接收到的数据
ProcessData(rx_buffer, RX_BUFFER_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
}
5. 调试技巧与性能优化
5.1 常见问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| DMA不触发 | 时钟未使能 | 检查__HAL_RCC_DMA1_CLK_ENABLE() |
| 数据错位 | 对齐方式错误 | 确认Mem/Periph DataAlignment匹配 |
| 接收不完整 | 缓冲区太小 | 增大RX buffer并检查NDTR |
| 发送卡死 | 未等待上次完成 | 检查HAL_UART_GetState() |
5.2 高级优化技巧
- 双缓冲技术:
c复制uint8_t rx_buf1[256], rx_buf2[256];
HAL_UART_Receive_DMA(&huart1, rx_buf1, 256);
// 在回调函数中切换缓冲区
void HAL_UART_RxCpltCallback(...) {
static uint8_t *active_buf = rx_buf1;
active_buf = (active_buf == rx_buf1) ? rx_buf2 : rx_buf1;
HAL_UART_Receive_DMA(huart, active_buf, 256);
}
- 内存布局优化:
c复制// 在链接脚本中指定DMA缓冲区位置
MEMORY {
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
DMARAM (xrw) : ORIGIN = 0x20005000, LENGTH = 4K
}
// 使用__attribute__指定段
uint8_t dma_buffer[1024] __attribute__((section(".dmaram")));
- 中断优先级配置:
c复制HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 5, 0);
HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
6. 实际项目中的应用案例
在工业环境监测系统中,我采用这套方案实现了以下功能:
-
多传感器数据采集:
- 通过DMA同时接收4个Modbus传感器的数据
- 每个传感器地址映射到不同内存区域
c复制typedef struct { uint8_t addr; uint8_t data[32]; } SensorData; SensorData sensors[4]; -
大数据包传输优化:
- 将1KB的采集数据分包传输
- 使用DMA完成标志触发下一包发送
c复制void HAL_UART_TxCpltCallback(...) { if(++packet_count < TOTAL_PACKETS) { UART_SendData(&data[packet_count*64], 64); } } -
错误恢复机制:
- 检测DMA传输错误
- 自动重新初始化DMA通道
c复制void HAL_UART_ErrorCallback(...) { if(huart->ErrorCode & HAL_UART_ERROR_DMA) { HAL_UART_DMAStop(huart); MX_DMA_Init(); UART_StartReceive(); } }
经过三个月的现场运行测试,这套方案在115200波特率下实现了99.98%的数据完整率,CPU占用率始终低于15%,完全满足工业级应用的要求。
