一、问题现象与背景
在STM32项目中,我们常让DMA搬运外设数据,同时用中断处理事件。例如UART接收:DMA负责把数据搬进缓冲区,而HAL_UART_Receive_IT或空闲中断负责帧边界判断。当两者同时操作同一外设时,典型现象包括:
- 接收缓冲区数据错位、丢字节
- 中断服务函数中读到的状态寄存器与DMA实际进度不一致
- 偶发HardFault(常见于DMA与CPU同时写同一内存)
- 串口发送时DMA未完成就触发TC中断,导致下一帧覆盖
根因通常不是HAL库本身,而是NVIC优先级配置不当与外设事件与DMA请求的时序竞争。
二、原理:DMA与中断如何“抢”同一外设
2.1 两条独立通路
以UART为例:
- 中断通路:RXNE/TC/IDLE等事件置位状态寄存器 → NVIC → CPU执行ISR
- DMA通路:外设发出DMA请求 → DMA控制器搬运数据 → 传输完成产生DMA中断
两条通路共享外设的状态与数据寄存器。若ISR中读DR清标志,而DMA同时也在读DR,就会发生“读竞争”:一方读走数据,另一方读到空或旧值。
2.2 优先级冲突的本质
Cortex-M的NVIC只对中断排优先级,DMA请求本身不经过NVIC。因此冲突表现为:
- DMA中断与USART中断优先级相同或更低,导致DMA完成事件被外设中断长时间阻塞;
- ISR执行时间过长,DMA请求得不到及时响应,外设溢出(ORE);
- 在ISR中调用HAL的阻塞式函数,破坏HAL状态机。
三、配置步骤
3.1 明确分工
- 数据搬运交给DMA,CPU不参与逐字节读写;
- 中断只做“事件通知”,如IDLE检测帧结束、DMA传输完成回调;
- 禁止在ISR中调用
HAL_UART_Receive等阻塞函数。
3.2 设置NVIC优先级
原则:DMA中断优先级 ≥ 外设中断优先级(数值更小),且都高于普通任务中断。
/* DMA1_Stream5 对应 USART2_RX,优先级设为1 */
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
/* USART2 中断优先级设为2,低于DMA */
HAL_NVIC_SetPriority(USART2_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);
3.3 使用IDLE中断配合DMA
这是最稳妥的“DMA+中断”组合:DMA持续搬运,IDLE中断只负责标记一帧结束。
#define RX_BUF_SIZE 256
uint8_t rx_buf[RX_BUF_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t frame_ready = 0;
void UART_DMA_Start(void)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart2, rx_buf, RX_BUF_SIZE);
}
/* USART2_IRQHandler 中由HAL调用 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2) {
frame_ready = 1;
}
}
IDLE处理放在USART中断里,只清标志、算长度,不做搬运:
void USART2_IRQHandler(void)
{
if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
/* 停止DMA以获取已传输长度 */
HAL_UART_DMAStop(&huart2);
rx_len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart2.hdmarx);
frame_ready = 1;
/* 重新启动,注意在任务中处理数据后再启动更安全 */
}
HAL_UART_IRQHandler(&huart2);
}
四、完整排查与解决示例
4.1 发送方向:DMA发送与TC中断冲突
常见错误:DMA发送未完成就调用下一次HAL_UART_Transmit_DMA,HAL返回BUSY。解决方式是等待DMA完成回调再发下一帧。
volatile uint8_t tx_done = 1;
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2) {
tx_done = 1; /* 仅置标志,不在此启动下一帧 */
}
}
void Send_Frame(uint8_t *data, uint16_t len)
{
while (!tx_done) { /* 可换成信号量 */ }
tx_done = 0;
HAL_UART_Transmit_DMA(&huart2, data, len);
}
4.2 关键检查清单
- 确认DMA中断优先级数值小于外设中断;
- 确认
HAL_UART_IRQHandler在自定义ISR末尾被调用,否则HAL状态机不更新; - 确认DMA与CPU不会同时写同一缓冲区(双缓冲或停止DMA后再处理);
- 确认
__HAL_DMA_GET_COUNTER在DMA停止后读取,运行中读取值可能跳变; - 使用
__DSB()/__DMB()保证内存序(尤其开启Cache的M7/M33)。
五、注意事项
- 不要在DMA完成回调里做耗时处理,只置标志,交给主循环或RTOS任务;
- 同一外设的TX/RX若共用DMA控制器,注意Stream/Channel不能冲突;
- 开启D-Cache的芯片,DMA缓冲区需放在非Cache区或手动
SCB_CleanInvalidateDCache; - 调试时用逻辑分析仪抓UART波形,比单步调试更能暴露时序问题;
- 若使用FreeRTOS,DMA回调中用
xSemaphoreGiveFromISR而非阻塞API。
六、小结
DMA与中断操作同一外设的冲突,本质是事件响应顺序与共享资源访问问题。核心策略是:DMA负责搬运、中断只做通知、NVIC优先级让DMA高于外设、缓冲区访问串行化。按本文配置与清单逐项核对,可消除绝大多数丢包、错位与HardFault问题。
暂无评论,来抢沙发~