一、为什么需要DMA+空闲中断?
传统串口接收采用单字节中断,每收到一个字节就进一次中断,在高波特率下CPU开销巨大。DMA+空闲中断的组合让DMA自动搬运数据,仅在总线空闲时触发一次中断,极大降低CPU负载。
- DMA:自动将串口数据寄存器(DR)的内容搬运到内存缓冲区,无需CPU干预。
- 空闲中断:当串口总线连续一个字节时间无新数据时触发,标志一帧数据结束。
但问题在于:空闲中断只表示“当前没有新数据”,并不保证一帧数据完整。如果数据发送方分多次发送,或总线受到干扰,就会出现半包(一帧被拆成多段)或粘包(多帧连在一起)的情况。
二、半包与粘包的成因分析
2.1 半包
发送方调用两次HAL_UART_Transmit发送“Hello”和“World”,中间有微小间隔。若间隔超过一个字节时间,接收方会触发两次空闲中断,第一次收到“Hello”,第二次收到“World”,但应用层期望的是“HelloWorld”。
2.2 粘包
发送方连续发送“AAA”和“BBB”,中间无间隔。DMA将“AAABBB”一次性搬入缓冲区,空闲中断只触发一次,应用层收到“AAABBB”,无法区分两帧。
三、边界处理核心策略
3.1 环形缓冲区+双指针
使用环形缓冲区存储DMA搬运的数据,维护write_index(DMA写入位置)和read_index(应用读取位置)。空闲中断时计算本次新增数据长度:
uint16_t new_len = (write_index - read_index + BUF_SIZE) % BUF_SIZE;
但仅靠长度无法区分半包与粘包,需结合时间戳或协议帧头。
3.2 时间戳判定半包
记录每次空闲中断的时间戳,若两次中断间隔小于预设阈值(如5ms),则认为是同一帧的延续,合并处理。
if (HAL_GetTick() - last_tick < FRAME_GAP_MS) {
// 合并到上一帧
} else {
// 新帧
}
3.3 协议帧头判定粘包
若应用层协议有固定帧头(如0xAA 0x55),可在接收数据中搜索帧头,按帧头切分粘包数据。
for (int i = 0; i < len - 1; i++) {
if (buf[i] == 0xAA && buf[i+1] == 0x55) {
// 找到新帧起始
}
}
四、完整代码实现(STM32 HAL库)
4.1 初始化配置
#define BUF_SIZE 256
uint8_t rx_buf[BUF_SIZE];
uint16_t write_index = 0;
uint32_t last_tick = 0;
void UART_DMA_Init(void) {
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
4.2 空闲中断回调
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
uint16_t dma_remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
uint16_t new_write = BUF_SIZE - dma_remain;
uint16_t len = (new_write - write_index + BUF_SIZE) % BUF_SIZE;
if (len > 0) {
uint32_t now = HAL_GetTick();
if (now - last_tick < FRAME_GAP_MS) {
// 半包:合并到上一帧(此处简化,实际需维护帧缓冲区)
process_frame_append(&rx_buf[write_index], len);
} else {
// 新帧
process_frame_new(&rx_buf[write_index], len);
}
last_tick = now;
write_index = new_write;
}
}
HAL_UART_IRQHandler(&huart1);
}
4.3 粘包切分函数
void process_frame_new(uint8_t *data, uint16_t len) {
uint16_t start = 0;
for (uint16_t i = 0; i < len - 1; i++) {
if (data[i] == 0xAA && data[i+1] == 0x55) {
if (i > start) {
handle_single_frame(&data[start], i - start);
}
start = i;
}
}
if (start < len) {
handle_single_frame(&data[start], len - start);
}
}
五、注意事项
- DMA缓冲区大小:需大于最大可能帧长,否则会覆盖未处理数据。建议256~1024字节。
- 空闲中断清除顺序:先清空闲标志再读DMA计数器,避免数据竞争。
- 半包合并超时:若半包后长时间无后续数据,应强制超时处理,避免内存泄漏。
- 粘包切分效率:逐字节搜索帧头在高速数据下可能成为瓶颈,可考虑状态机或硬件CRC辅助。
- 多串口场景:每个串口独立维护环形缓冲区和时间戳,避免交叉干扰。
通过上述策略,可有效解决DMA+空闲中断接收中的半包与粘包问题,提升嵌入式串口通信的可靠性。
暂无评论,来抢沙发~