用 DMA 环形缓冲区实现 UART 不定长接收时,如何精确处理半满中断与空闲中断的竞争
1. 问题背景与竞争根源
在STM32等MCU上,利用DMA的循环模式(Circular Mode)配合UART的空闲中断(IDLE)是实现不定长接收的经典方案。DMA将接收数据直接写入内存环形缓冲区,当总线空闲时触发IDLE中断,主程序解析数据。然而,当DMA缓冲区大小设置为2的幂次(如256字节)时,DMA传输过半(即达到半满点)会触发半传输中断(HT)。此时,若同时发生IDLE中断,两个中断的优先级和嵌套顺序将直接影响数据完整性。
竞争根源:
- 半满中断(HT)表示DMA已传输一半数据,但数据可能尚未被处理;
- 空闲中断(IDLE)表示总线空闲,可能意味着一个完整帧结束;
- 若HT先触发,而IDLE随后触发,但HT处理中读取的DMA位置可能已越过半满点,导致数据覆盖或重复读取。
2. 硬件与软件基础
2.1 DMA环形缓冲区原理
DMA循环模式下,传输地址在到达缓冲区末尾后自动回绕到起始地址,形成环形。通常设置缓冲区大小为2的幂(如256),并利用DMA的HT(半传输)和TC(传输完成)中断来分割缓冲区为两个半区,便于双缓冲处理。
2.2 空闲中断(IDLE)
UART空闲中断在接收线上检测到连续空闲(即一个字节时间内无数据)时触发,表示一帧数据接收完毕。它不依赖DMA,但常与DMA配合使用。
2.3 竞争场景示例
假设缓冲区大小256字节,DMA写入位置为pos。当pos=128时触发HT中断,此时若一帧数据恰好结束,IDLE中断也触发。若HT中断优先级高于IDLE,则先执行HT处理,可能将pos重置为0,而IDLE处理时读取的pos可能已变化,导致数据错乱。
3. 精确处理策略
3.1 状态机设计
引入一个接收状态机,状态包括:
-
IDLE:等待数据开始; -
RECEIVING:正在接收数据; -
FRAME_READY:一帧数据完整,待处理。
中断处理中,根据当前状态和触发中断类型,决定是否更新缓冲区读写指针。
3.2 中断优先级配置
建议将IDLE中断优先级设为高于HT中断(例如NVIC优先级分组,IDLE为抢占优先级0,HT为1),确保IDLE能抢占HT,避免数据被覆盖。但需注意,若HT处理时间过长,可能影响实时性。
3.3 核心处理逻辑
在IDLE中断中,读取DMA当前传输位置(通过__HAL_DMA_GET_COUNTER),计算实际接收长度,并更新读指针。在HT中断中,仅更新读指针到半满点,不进行数据解析。关键点:在IDLE处理时,需检查是否有HT中断挂起,若有,则先处理HT,再处理IDLE。
// 伪代码示例
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 处理IDLE中断(实际在UART中断中调用)
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
uint16_t pos = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// 处理HT竞争:若HT标志置位,先更新读指针
if (ht_flag) {
read_index = BUFFER_SIZE/2;
ht_flag = 0;
}
// 计算本次接收数据长度
uint16_t len = (pos >= read_index) ? (pos - read_index) : (BUFFER_SIZE - read_index + pos);
// 将数据拷贝到应用缓冲区,并更新read_index = pos;
process_data(read_index, len);
read_index = pos;
}
}
void DMA_HT_IRQHandler(void) {
// 半满中断处理
ht_flag = 1;
read_index = BUFFER_SIZE/2; // 假设从0开始
// 可在此处预取数据,但需注意与IDLE的竞争
}
4. 完整代码示例(基于STM32 HAL库)
#define BUFFER_SIZE 256
uint8_t dma_buffer[BUFFER_SIZE];
volatile uint16_t read_index = 0;
volatile uint8_t ht_flag = 0;
void UART_DMA_Init(void) {
// 配置UART,使能IDLE中断,DMA循环模式
// 初始化DMA,设置半传输中断和传输完成中断
HAL_UART_Receive_DMA(&huart, dma_buffer, BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE);
HAL_NVIC_SetPriority(DMA_Stream_IRQn, 1, 0); // HT优先级较低
HAL_NVIC_EnableIRQ(DMA_Stream_IRQn);
HAL_NVIC_SetPriority(USART_IRQn, 0, 0); // IDLE优先级较高
HAL_NVIC_EnableIRQ(USART_IRQn);
}
void USART_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart);
uint16_t pos = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&huart.hdmarx);
// 处理HT竞争:若HT标志置位,先更新读指针
if (ht_flag) {
read_index = BUFFER_SIZE/2;
ht_flag = 0;
}
uint16_t len;
if (pos >= read_index) {
len = pos - read_index;
} else {
len = BUFFER_SIZE - read_index + pos;
}
// 拷贝数据到应用层(此处仅示意)
if (len > 0) {
// 处理数据,注意环形缓冲区可能回绕,需分段拷贝
process_rx_data(read_index, len);
}
read_index = pos;
}
// 其他UART中断处理
}
void DMA_Stream_IRQHandler(void) {
if (__HAL_DMA_GET_FLAG(&huart.hdmarx, DMA_FLAG_HT)) {
__HAL_DMA_CLEAR_FLAG(&huart.hdmarx, DMA_FLAG_HT);
ht_flag = 1;
// 可选:预取半区数据,但需确保不覆盖未读数据
}
// 处理TC中断等
}
5. 注意事项
- 缓冲区大小:建议为2的幂,且大于最大帧长度,避免回绕时数据覆盖。
- 数据拷贝:处理环形缓冲区时,注意回绕情况,需分段拷贝。
- 中断嵌套:确保IDLE中断优先级高于HT,但也要考虑其他中断的优先级,避免死锁。
- 性能优化:在HT中断中可预取数据到临时缓冲区,但需同步机制防止与IDLE冲突。
- 调试建议:使用逻辑分析仪或串口打印调试信息,观察中断触发顺序。
6. 总结
通过合理设置中断优先级,并在IDLE中断中检查HT标志,可以精确处理半满中断与空闲中断的竞争。状态机设计进一步增强了健壮性。此方案在高速通信(如1Mbps以上)中表现稳定,是嵌入式UART接收的优选方案。