一、问题现象与背景

在STM32项目中,常使用DMA搬运外设数据(如UART接收、ADC采样),同时开启外设中断处理事件(如UART空闲中断、ADC转换完成中断)。当两者同时访问同一外设时,可能出现:

  • 数据丢失或重复处理
  • 中断服务函数中调用HAL_UART_Receive_DMA等函数返回HAL_BUSY
  • 系统死锁或HardFault

根本原因在于NVIC优先级配置不当导致DMA中断与外设中断的抢占关系混乱,以及HAL库内部状态机未正确同步。

二、原理讲解:优先级与抢占机制

2.1 NVIC优先级分组

Cortex-M内核使用4位优先级(STM32中通常配置为4位全部用于抢占优先级或分组)。HAL库通过HAL_NVIC_SetPriorityGrouping()设置分组。抢占优先级高的中断可以打断抢占优先级低的中断,子优先级仅在同时挂起时决定顺序。

2.2 DMA中断与外设中断的关系

以UART为例:

  • USART全局中断:处理RXNE、IDLE、TC等事件。
  • DMA通道中断:处理传输完成(TC)、半传输(HT)、错误(TE)。

若USART中断抢占优先级高于DMA中断,则USART中断可以打断DMA中断服务函数;反之亦然。若两者优先级相同,则无法互相抢占,但可能因HAL库状态机竞争导致异常。

2.3 典型冲突场景

  • 在USART中断中调用HAL_UART_Receive_DMA()重启接收,但此时DMA中断正在处理上一次传输完成,状态未更新,返回HAL_BUSY。
  • DMA传输完成中断中调用HAL_UART_Transmit(),而USART中断正在处理发送完成,导致状态冲突。

三、配置步骤与完整代码示例

3.1 步骤概览

  1. 设置NVIC优先级分组(推荐NVIC_PRIORITYGROUP_4,4位抢占优先级)。
  2. 为DMA通道中断和外设中断分配不同的抢占优先级,确保关键中断可抢占。
  3. 在中断服务函数中仅做标志置位或简短处理,避免调用阻塞型HAL函数。
  4. 使用__HAL_LOCK()/__HAL_UNLOCK()保护共享资源(HAL库内部已实现,但需注意调用时机)。

3.2 代码示例:UART空闲中断+DMA接收

/* 优先级分组:4位抢占优先级 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

/* USART1中断:抢占优先级1 */
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

/* DMA1_Stream5中断(UART1_RX):抢占优先级2,低于USART,允许USART抢占 */
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);

/* 启动DMA接收 */
uint8_t rx_buf[128];
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能空闲中断

/* USART1中断服务函数 */
void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart1);
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        // 仅置位标志,主循环处理
        uart_idle_flag = 1;
    }
}

/* DMA中断服务函数 */
void DMA1_Stream5_IRQHandler(void)
{
    HAL_DMA_IRQHandler(huart1.hdmarx);
}

/* 主循环中处理空闲事件 */
if (uart_idle_flag) {
    uart_idle_flag = 0;
    HAL_UART_DMAStop(&huart1); // 先停止DMA
    uint16_t len = sizeof(rx_buf) - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
    // 处理数据...
    HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf)); // 重新启动
}

3.3 关键点说明

  • USART抢占优先级高于DMA,确保空闲中断能及时响应,但DMA中断不会被长时间阻塞。
  • 在USART中断中不调用HAL_UART_Receive_DMA,避免与DMA中断竞争。
  • 主循环中先HAL_UART_DMAStop再重启,确保状态机复位。

四、注意事项与排查清单

  • 优先级分组必须统一:整个工程只调用一次HAL_NVIC_SetPriorityGrouping,否则优先级解析错乱。
  • 避免在中断中调用HAL阻塞函数:如HAL_UART_Transmit(带超时)会忙等,破坏实时性。
  • 检查HAL库状态机:调用HAL_UART_Receive_DMA前确认huart->RxState == HAL_UART_STATE_READY。
  • 使用调试工具:通过__HAL_DMA_GET_FLAG和__HAL_UART_GET_FLAG检查中断标志,或使用逻辑分析仪观察时序。
  • DMA与中断同时操作同一外设时,建议关闭外设中断中的DMA相关操作,将重启DMA的动作放到主循环或低优先级任务中。
  • 若必须在外设中断中重启DMA,可临时提升DMA中断优先级至高于外设中断,但需评估实时性影响。

五、总结

DMA与中断同时操作同一外设的优先级冲突,本质是抢占关系与状态机同步问题。合理配置NVIC优先级分组、分离中断处理与DMA重启逻辑、利用HAL库状态检查,可有效避免数据丢失和死锁。建议在项目初期就规划好中断优先级表,并在调试中验证抢占行为。