1. STM32串口高效通信方案解析
在嵌入式开发中,串口通信是最基础也最常用的外设之一。传统轮询方式效率低下,而中断方式又容易丢失数据。今天分享一个在STM32F103C8T6上实现的稳定高效的串口通信方案,实测在115200波特率下,连续收发一万多个12字节数据包零丢包。
这个方案的核心是:
- 接收端:串口空闲中断+DMA循环接收+环形缓冲区
- 发送端:DMA发送+环形缓冲区驱动
2. 硬件设计与初始化配置
2.1 硬件连接与时钟配置
本方案基于STM32F103C8T6(Blue Pill开发板),具体硬件连接:
- USART3_TX → PB10(复用推挽输出)
- USART3_RX → PB11(浮空输入)
- DMA1_Channel3 → USART3_RX
- DMA1_Channel2 → USART3_TX
时钟配置:
- APB1总线时钟36MHz
- USART3挂载在APB1总线上
2.2 初始化流程详解
初始化代码位于USART3_Init()函数中,主要完成以下工作:
- 环形缓冲区初始化:
c复制RingBuf_Init(&s_rxRing, s_rxRingData, USART3_RX_RING_SIZE);
RingBuf_Init(&s_txRing, s_txRingData, USART3_TX_RING_SIZE);
- USART参数配置:
c复制huart3.Instance = USART3;
huart3.Init.BaudRate = baudrate;
huart3.Init.WordLength = UART_WORDLENGTH_8B;
huart3.Init.StopBits = UART_STOPBITS_1;
huart3.Init.Parity = UART_PARITY_NONE;
huart3.Init.Mode = UART_MODE_TX_RX;
huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart3.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart3);
- 启动DMA循环接收:
c复制HAL_UART_Receive_DMA(&huart3, s_rxDmaBuf, USART3_RX_DMA_BUF_SIZE);
- 使能空闲中断:
c复制__HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
注意:HAL库默认不会开启空闲中断,必须手动使能
3. 接收端实现细节
3.1 DMA循环接收机制
接收端采用DMA循环模式,配置要点:
- DMA模式:循环模式(Circular)
- 数据宽度:字节
- 内存地址自增,外设地址固定
- 不使能DMA中断
DMA缓冲区大小定义为128字节(USART3_RX_DMA_BUF_SIZE),这是一个关键参数:
- 太小:容易溢出
- 太大:浪费内存
- 建议值:大于最大预期数据包的2倍
3.2 空闲中断处理流程
当串口检测到空闲(IDLE)状态时,触发中断,处理流程如下:
- 清除IDLE标志(必须先读SR再读DR):
c复制__HAL_UART_CLEAR_IDLEFLAG(&huart3);
- 计算DMA当前写入位置:
c复制uint32_t dmaRemain = (uint32_t)(__HAL_DMA_GET_COUNTER(&hdma_usart3_rx));
uint32_t dmaCurPos = USART3_RX_DMA_BUF_SIZE - dmaRemain;
- 将新数据从DMA缓冲区搬移到环形缓冲区:
c复制if (dmaCurPos > s_rxDmaLastPos) {
/* 无回绕情况 */
RingBuf_Put(&s_rxRing, &s_rxDmaBuf[s_rxDmaLastPos], dmaCurPos - s_rxDmaLastPos);
} else {
/* 缓冲区回绕情况 */
RingBuf_Put(&s_rxRing, &s_rxDmaBuf[s_rxDmaLastPos], USART3_RX_DMA_BUF_SIZE - s_rxDmaLastPos);
RingBuf_Put(&s_rxRing, &s_rxDmaBuf[0], dmaCurPos);
}
s_rxDmaLastPos = dmaCurPos;
实测技巧:在115200波特率下,即使连续接收大量数据,这套机制也能稳定工作,不会丢失任何字节。
4. 发送端实现细节
4.1 环形缓冲区驱动发送
发送端采用环形缓冲区+DMA的方案,主要特点:
- 用户数据先存入发送环形缓冲区
- DMA从环形缓冲区取出数据发送
- 支持发送过程中随时追加新数据
发送函数USART3_Send()实现:
c复制uint32_t USART3_Send(const uint8_t *data, uint32_t len) {
uint32_t written = RingBuf_Put(&s_txRing, data, len);
if (!s_txBusy) {
_TxStart(); // 启动DMA发送
}
return written;
}
4.2 DMA发送流程
实际发送由内部函数_TxStart()完成:
c复制static void _TxStart(void) {
uint32_t avail = RingBuf_Used(&s_txRing);
if (avail == 0) {
s_txBusy = 0;
return;
}
uint32_t txLen = (avail > USART3_TX_DMA_BUF_SIZE) ?
USART3_TX_DMA_BUF_SIZE : avail;
RingBuf_Get(&s_txRing, s_txDmaBuf, txLen);
s_txBusy = 1;
HAL_UART_Transmit_DMA(&huart3, s_txDmaBuf, (uint16_t)txLen);
}
发送完成回调:
c复制void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART3) {
_TxStart(); // 继续发送剩余数据
}
}
5. 环形缓冲区实现
5.1 数据结构设计
环形缓冲区控制结构:
c复制typedef struct {
uint8_t *buf; // 数据缓冲区指针
uint32_t size; // 缓冲区总容量
volatile uint32_t head; // 写指针(生产者推进)
volatile uint32_t tail; // 读指针(消费者推进)
} RingBuf_t;
关键特性:
- 不使用模运算,支持任意大小的缓冲区
- 单生产者单消费者场景下线程安全
- 无动态内存分配
5.2 核心操作实现
缓冲区索引处理:
c复制#define RB_MASK(rb, idx) ((idx) % (rb)->size)
写入数据:
c复制uint32_t RingBuf_Put(RingBuf_t *rb, const uint8_t *data, uint32_t len) {
uint32_t i;
for (i = 0; i < len; i++) {
if (!RingBuf_PutByte(rb, data[i])) break;
}
return i;
}
读取数据:
c复制uint32_t RingBuf_Get(RingBuf_t *rb, uint8_t *buf, uint32_t maxLen) {
uint32_t i;
for (i = 0; i < maxLen; i++) {
if (!RingBuf_GetByte(rb, &buf[i])) break;
}
return i;
}
6. 性能优化与实测数据
6.1 缓冲区大小选择
经过实测验证的推荐配置:
- DMA接收缓冲区:128字节
- 接收环形缓冲区:256字节
- 发送环形缓冲区:256字节
- DMA发送缓冲区:128字节
这个配置在115200波特率下表现优异,即使每包12字节连续收发一万多包也不丢包。
6.2 中断处理优化
关键的中断处理技巧:
- 空闲中断处理要尽可能快
- DMA发送完成中断中不要做复杂操作
- 避免在中断中调用可能阻塞的函数
中断优先级配置建议:
- USART3中断:较高优先级
- DMA发送中断:较低优先级
7. 常见问题与解决方案
7.1 数据接收不完整
可能原因:
- 空闲中断未正确使能
- DMA缓冲区太小
- 环形缓冲区溢出
解决方案:
- 确认调用了
__HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE) - 增大DMA缓冲区大小
- 检查
RingBuf_Put()返回值,确认是否有数据被丢弃
7.2 发送速度慢
优化建议:
- 适当增大DMA发送缓冲区
- 提高发送环形缓冲区大小
- 检查是否有其他高优先级中断阻塞了DMA发送
7.3 稳定性问题
确保:
- 所有volatile变量正确声明
- 中断处理函数中没有耗时操作
- 缓冲区大小足够应对最坏情况的数据流量
8. 扩展应用与改进思路
这套方案可以轻松扩展到:
- 多串口同时工作
- 更高波特率(实测在2Mbps下也能稳定工作)
- 自定义协议解析
改进方向:
- 添加流控支持
- 实现动态缓冲区大小调整
- 增加超时检测机制
在实际项目中,我已经将这套方案应用于多个工业设备,包括:
- 传感器数据采集
- 设备间通信
- 调试信息输出
它的最大优势是稳定可靠,即使在复杂的多任务环境下也能保证数据完整性。
