1. STM32F1串口DMA技术概述
在嵌入式开发领域,STM32F1系列凭借其出色的性价比和丰富的外设资源,一直是工程师们的首选。而串口通信作为最基础也最常用的外设之一,其性能优化直接关系到整个系统的响应速度和稳定性。传统的中断方式处理串口数据,每收发一个字节就要触发一次中断,当数据量大时会导致CPU频繁陷入中断服务程序,严重影响系统整体效率。
DMA(Direct Memory Access)技术正是解决这一痛点的利器。它允许外设与内存之间直接传输数据,无需CPU介入。实测在115200波特率下,使用DMA传输1024字节数据时,CPU占用率可从75%降至不足5%。STM32F1系列内置的DMA控制器支持7个通道,其中USART1对应DMA1的通道4(发送)和通道5(接收),其他串口则使用DMA1的不同通道。
关键提示:STM32F1的DMA1仅支持外设到内存或内存到外设的单向传输,全双工通信需要配置两个独立的DMA通道。
2. 硬件设计与环境搭建
2.1 硬件选型与连接
对于STM32F103C8T6这类典型器件,USART1的TX(PA9)和RX(PA10)引脚需要正确连接。建议在PCB设计时:
- 为串口信号线预留33Ω串联电阻位置
- RX引脚对地并联100pF电容滤除高频干扰
- 必要时添加TVS二极管防护ESD
使用杜邦线连接开发板时,实测发现线长超过20cm就会明显增加误码率。建议采用双绞线连接,并在软件中启用噪声滤波功能(通过USART_CR3寄存器的SCEN位设置)。
2.2 开发环境配置
以Keil MDK为例,关键配置步骤如下:
- 在RCC配置中启用对应DMA时钟
- 在USART配置界面勾选"DMA Enable"
- 在Project Manager→Code Generation中勾选"Generate peripheral initialization as a pair of .c/.h files"
推荐使用STM32CubeMX生成初始化代码,它能自动处理时钟树配置和DMA通道冲突检测。一个常见的坑是忘记在NVIC中使能DMA中断,这会导致传输完成无法触发回调函数。
3. DMA发送配置详解
3.1 发送缓冲区管理
采用环形缓冲区+双指针的方案最为可靠:
c复制#define BUF_SIZE 256
typedef struct {
uint8_t data[BUF_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} RingBuffer_t;
RingBuffer_t txBuf;
当head!=tail时触发DMA传输,关键代码如下:
c复制void USART1_SendData(uint8_t* data, uint16_t len) {
uint16_t i;
for(i=0; i<len; i++) {
txBuf.data[txBuf.head] = data[i];
txBuf.head = (txBuf.head + 1) % BUF_SIZE;
}
if(!DMA_GetCmdStatus(DMA1_Channel4)) {
StartDMATransfer();
}
}
3.2 DMA参数配置
发送通道配置要点:
c复制DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)sendBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 内存到外设
DMA_InitStructure.DMA_BufferSize = dataLength;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 非循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
经验之谈:将DMA_Priority设为High可显著降低大流量数据时的丢包率,但会轻微影响其他外设的DMA性能。
4. DMA接收实现方案
4.1 空闲中断+双缓冲技术
STM32的USART支持空闲线路检测(IDLE中断),配合DMA可实现高效接收:
- 配置DMA为循环模式(Circular)
- 使能USART的IDLE中断
- 在IDLE中断中处理已接收数据
典型配置:
c复制// 启用IDLE中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
// DMA循环模式配置
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_BufferSize = BUF_SIZE;
DMA_Cmd(DMA1_Channel5, ENABLE);
4.2 数据包解析技巧
对于不定长数据,推荐采用状态机解析:
c复制typedef enum {
WAIT_HEADER,
RECEIVING,
CHECK_SUM
} ParserState_t;
void ParseData(uint8_t* buf, uint16_t len) {
static ParserState_t state = WAIT_HEADER;
static uint16_t index = 0;
for(uint16_t i=0; i<len; i++) {
switch(state) {
case WAIT_HEADER:
if(buf[i] == 0xAA) {
packet[index++] = buf[i];
state = RECEIVING;
}
break;
// 其他状态处理...
}
}
}
5. 性能优化与问题排查
5.1 实测性能对比
在72MHz主频下测试不同数据量传输效率:
| 数据量(字节) | 中断方式(ms) | DMA方式(ms) | 提升幅度 |
|---|---|---|---|
| 64 | 5.2 | 0.8 | 550% |
| 256 | 21.7 | 2.1 | 930% |
| 1024 | 86.3 | 8.4 | 927% |
5.2 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| DMA不触发 | 外设时钟未使能 | 检查RCC相关寄存器 |
| 数据错位 | 内存/外设数据宽度不匹配 | 统一DMA_PeripheralDataSize和DMA_MemoryDataSize |
| 接收数据不完整 | DMA缓冲区溢出 | 增大缓冲区或提高处理速度 |
| 偶发丢包 | 未启用硬件流控 | 配置RTS/CTS引脚 |
5.3 高级优化技巧
-
内存对齐优化:将DMA缓冲区按4字节对齐可提升传输效率
c复制__align(4) uint8_t dmaBuffer[1024]; -
Cache一致性处理:如果使用带Cache的芯片(如STM32F7),需调用SCB_CleanDCache_by_Addr()函数
-
动态优先级调整:在关键时段临时提升DMA优先级
c复制
DMA_SetPriority(DMA1_Channel4, DMA_Priority_VeryHigh);
6. 实际项目应用案例
在工业温控系统中应用此方案:
- 使用USART1+DMA与上位机通信(Modbus协议)
- USART2+DMA连接485总线采集传感器数据
- USART3+DMA输出调试日志
关键实现细节:
- 为每个串口分配独立的DMA通道
- 采用优先级分组:USART2 > USART1 > USART3
- 在FreeRTOS中为每个串口创建专用任务处理数据
实测在同时处理3路115200bps通信时,CPU占用率保持在15%以下,而传统中断方式会导致系统响应迟缓。