一、为什么DMA+空闲中断会遭遇半包与粘包?
DMA+空闲中断的基本原理是:DMA自动将串口数据搬运到缓冲区,当总线空闲(超过1个字符时间无新数据)时触发空闲中断,此时读取DMA剩余传输量即可知道本帧长度。
但实际场景中:
- 半包:发送方数据被拆成多段,或线路干扰导致空闲中断提前触发,一帧数据被分成多次接收。
- 粘包:发送方连续发送多帧,帧间间隔小于空闲检测时间,多帧数据被合并成一次接收。
这两种情况都会破坏“一次空闲中断对应一帧完整数据”的假设,直接解析必然出错。
二、边界处理的核心思路
解决半包与粘包的关键是在应用层增加帧边界识别机制,而不是依赖空闲中断的天然分割。常用策略:
- 固定帧头+长度字段:协议设计时包含帧头和长度信息,接收端据此判断帧是否完整。
- 超时拼接:对半包设置拼接超时,超时内到达的数据视为同一帧。
- 长度校验与滑动窗口:对粘包数据按协议长度字段逐帧拆分。
下面以“帧头(2字节)+长度(1字节)+数据+校验(1字节)”的协议为例,给出完整实现。
三、硬件与CubeMX配置
以STM32F103 + USART1为例:
- 使能USART1,波特率115200,8N1。
- 开启USART1全局中断(NVIC)。
- 配置DMA1通道5(USART1_RX),模式为循环模式(Circular),数据宽度Byte。
- 在NVIC中使能DMA通道中断(可选,用于错误处理)。
关键代码初始化:
#define RX_BUF_SIZE 256
uint8_t rx_dma_buf[RX_BUF_SIZE];
volatile uint16_t last_pos = 0;
void USART1_Init(void) {
// HAL_UART_Init 已由CubeMX生成
HAL_UART_Receive_DMA(&huart1, rx_dma_buf, RX_BUF_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
四、空闲中断处理与数据提取
空闲中断中计算本次接收长度,并拷贝到应用缓冲区:
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
uint16_t cur_pos = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
if (cur_pos != last_pos) {
uint16_t len;
if (cur_pos > last_pos) {
len = cur_pos - last_pos;
feed_data(&rx_dma_buf[last_pos], len);
} else {
// 环形缓冲区回绕
len = RX_BUF_SIZE - last_pos;
feed_data(&rx_dma_buf[last_pos], len);
len = cur_pos;
feed_data(&rx_dma_buf[0], len);
}
last_pos = cur_pos;
}
}
HAL_UART_IRQHandler(&huart1);
}
五、半包与粘包的边界处理实现
feed_data函数负责将数据送入解析状态机,核心是维护一个拼接缓冲区:
#define MAX_FRAME_LEN 128
uint8_t frame_buf[MAX_FRAME_LEN];
uint16_t frame_len = 0;
uint32_t last_rx_tick = 0;
void feed_data(uint8_t *data, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
// 超时重置(半包处理)
if (frame_len > 0 && HAL_GetTick() - last_rx_tick > 50) {
frame_len = 0; // 超时丢弃不完整帧
}
last_rx_tick = HAL_GetTick();
if (frame_len < MAX_FRAME_LEN) {
frame_buf[frame_len++] = data[i];
} else {
frame_len = 0; // 溢出保护
}
// 尝试解析完整帧(粘包处理)
parse_frame();
}
}
parse_frame函数根据协议逐帧提取:
void parse_frame(void) {
while (frame_len >= 4) { // 最小帧长:帧头2+长度1+校验1
// 查找帧头 0xAA 0x55
if (frame_buf[0] != 0xAA || frame_buf[1] != 0x55) {
// 滑动窗口:丢弃第一个字节
memmove(frame_buf, frame_buf + 1, --frame_len);
continue;
}
uint8_t data_len = frame_buf[2];
uint16_t total_len = 2 + 1 + data_len + 1;
if (frame_len < total_len) {
break; // 半包,等待更多数据
}
// 校验(简单异或)
uint8_t checksum = 0;
for (uint16_t i = 0; i < total_len - 1; i++) {
checksum ^= frame_buf[i];
}
if (checksum == frame_buf[total_len - 1]) {
// 完整帧,交给应用处理
handle_frame(&frame_buf[3], data_len);
// 移除已处理帧(粘包拆分)
memmove(frame_buf, frame_buf + total_len, frame_len - total_len);
frame_len -= total_len;
} else {
// 校验失败,丢弃帧头
memmove(frame_buf, frame_buf + 1, --frame_len);
}
}
}
六、注意事项
-
DMA循环模式下,
last_pos需与DMA计数器同步,避免回绕时数据覆盖。 - 空闲中断时间由波特率决定,115200下约87us,若发送方帧间隔小于此值,粘包不可避免,必须依赖协议解析。
- 拼接超时需根据实际通信节奏调整,过短会误判半包,过长会增加延迟。
- 内存操作(memmove)在中断上下文中应尽量轻量,建议将解析移至主循环,中断仅做数据搬运。
- 若使用RTOS,可将
feed_data放入消息队列,由任务处理,避免中断阻塞。
七、总结
DMA+空闲中断是高效收包的基础,但半包与粘包必须通过应用层协议解析来兜底。本文给出的“帧头+长度+校验”状态机配合超时拼接,能有效处理绝大多数边界情况。实际项目中,建议将解析逻辑与硬件解耦,便于测试与复用。
暂无评论,来抢沙发~