起因

最近在调试GD32F4xx,想使用串口的闲时中断,发现与STM32有些区别:清标志的方法不一样,踩了“闲时中断反复触发/不触发”的坑。在此做个记录,备忘,防止重复踩坑。

需求

  • 串口收到数据逐字节进缓冲(RBNE 中断)
  • 一帧不定长数据接收结束时通知上层(IDLE 闲时中断断帧)

技术实现原理

闲时中断(IDLE)检测的是什么

串口的 IDLE 检测监视的是 RX 线的电平:收到一帧数据后,线路连续保持高电平超过一个字节时间(起始位+数据+校验+停止位的完整帧长),硬件就置 IDLE 标志。注意它检测的是“从收到数据到空闲”这个边沿,不是“空闲状态”本身——持续空闲不会再触发,必须先再来一帧数据。这个“一帧结束”信号正好用来给不定长报文断帧:协议没有长度字段时,靠它知道“这包收完了”。

根因:IDLE 标志的清除靠“读状态→读数据”的软件序列

本文最核心的坑在这:IDLE 标志不是写 1 清零的,它的清除约定是先读状态寄存器、再读数据寄存器——这套序列从老式 UART 一路继承下来。GD32 标准库里“读数据寄存器”被封装成 usart_data_receive(),于是:

  • 只调 usart_interrupt_flag_clear() 清标志,清除序列没走完,标志实际没清掉——表现就是闲时中断被触发多次或干脆不触发;
  • 必须再补一次 usart_data_receive()(哪怕丢弃返回值),把“读 SR→读 DR”走完整,标志才真正落下。

STM32F4 的 HAL 宏 __HAL_UART_CLEAR_IDLEFLAG 展开后同样是先读 SR 再读 DR(见下文贴出的宏定义),同一个硬件机制、两家封装不同——理解了清除序列,换厂牌也不会再踩。

中断处理流程

串口使能中断相关代码:

1
2
3
nvic_irq_enable(USART0_IRQn,0,1);
usart_interrupt_enable(USART0, USART_INT_RBNE);         /* 使能USART0读区非空中断 */  
usart_interrupt_enable(USART0, USART_INT_IDLE);         /* 使能USART0空闲中断 */

串口中断处理函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void USART0_IRQHandler(void)
{
    if ((RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) &&
        (RESET != usart_flag_get(USART0, USART_FLAG_RBNE))) {
        unsigned char value = usart_data_receive(USART0);
        RingBufWrite(g_debugRingBuf, value);		//将接收到的数据写入ringbuf中
        usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE); //清中断标志

    }
    else if (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) {
        usart_interrupt_flag_clear(USART0, USART_INT_FLAG_IDLE);
        usart_data_receive(USART0); /* 清除接收完成标志位 */
        (void)LOS_EventWrite(&g_shellInputEvent, 0x1); //事件通知接收完成
    }
}

代码说明

  • USART_INT_RBNE(读数据区非空)中断里逐字节 usart_data_receive 取数、写进 ringbuf;
  • USART_INT_IDLE 分支做三件事:清标志 → 再调一次 usart_data_receive() 补完清除序列 → 写事件通知上层“一帧结束”;
  • 特别要注意的是:与STM32F4不同的是,在进入闲时中断后,需要调用usart_data_receive函数,用于清除接收完成标志位,否则闲时中断会存在被触发多次或不触发的情况。

与 STM32 的对照

在STM32f4xx中,清除闲时中断标志宏定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** @brief  Clears the UART IDLE pending flag.
  * @param  __HANDLE__ specifies the UART Handle.
  *         UART Handle selects the USARTx or UARTy peripheral
  *         (USART,UART availability and x,y values depending on device).
  * @retval None
  */
#define __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__) __HAL_UART_CLEAR_PEFLAG(__HANDLE__)

/** @brief  Clears the UART PE pending flag.
  * @param  __HANDLE__ specifies the UART Handle.
  *         UART Handle selects the USARTx or UARTy peripheral
  *         (USART,UART availability and x,y values depending on device).
  * @retval None
  */
#define __HAL_UART_CLEAR_PEFLAG(__HANDLE__)     \
  do{                                           \
    __IO uint32_t tmpreg = 0x00U;               \
    tmpreg = (__HANDLE__)->Instance->SR;        \
    tmpreg = (__HANDLE__)->Instance->DR;        \
    UNUSED(tmpreg);                             \
  } while(0U)

可以看到有读取操作对DR寄存器进行读取操作,相当于GD32标准库中usart_data_receive函数的作用。

踩坑记录与注意事项

  • 只清标志不读 DATA = IDLE 异常:本文核心坑。清除序列是“读状态→读数据”两步,GD32 里必须显式补 usart_data_receive()
  • IDLE 只在“数据后变空闲”的边沿触发一次:持续空闲不再触发,想再触发得先收到新数据。调试时觉得“怎么只进来一次”多半是这个机制,不是 bug;
  • 断帧延迟与波特率挂钩:空闲判定时长约一个字节时间,9600 波特下约 1ms,115200 下几十微秒——对帧间隔敏感的协议要把这个延迟算进去;
  • 数据量大就换 DMA:RBNE 逐字节中断在高波特率下 CPU 开销大,标准组合是“DMA 循环收 + IDLE 中断断帧”,在 IDLE 中断里按 DMA 计数差取出一帧;
  • 中断里只做搬运和通知:本文在 ISR 里只发事件是对的做法,别在中断里做协议解析等长活。