中断处理机制:UART中断类型与优先级配置

UART的中断处理,说白了就是让CPU不用一直盯着串口看。数据来了,或者发完了,硬件自动通知CPU。我刚开始做嵌入式时,总觉得轮询也挺好,直到有一次项目里要同时处理三路串口加一个传感器采样……嗯,从那以后我再也不敢轻视中断了。

UART中断类型:TX/RX/错误

大部分MCU的UART外设会提供三类中断源。我习惯把它们分成“必须开的”和“按需开的”:

中断类型 触发条件 典型用途 优先级建议
RX中断 接收寄存器非空(RXNE) 读取接收数据,触发协议解析 高(数据不能丢)
TX中断 发送寄存器空(TXE)或发送完成(TC) 发送下一字节,或关闭发送使能 中低(可等待)
错误中断 溢出(ORE)、帧错误(FE)、校验错误(PE) 错误计数、复位接收状态机 中(需及时处理)
我的习惯:RX中断我几乎必开,TX中断只在需要发送长数据帧时才开。错误中断在调试阶段全开,量产阶段只保留ORE(溢出错误),因为其他错误通常由硬件连接问题导致,软件处理不了。

中断优先级配置

中断优先级配置,很多人觉得设个数字就行。其实这里有个坑——抢占优先级和子优先级的分配。

我曾经在一个多串口项目中,把三个UART的抢占优先级设成了相同值。结果高波特率的串口一忙,低优先级的串口就丢数据。后来改成每个UART独占一个抢占优先级,问题才解决。

配置原则我总结了三句话:

  • RX中断 > TX中断:接收不及时会丢数据,发送可以等
  • 错误中断 ≥ RX中断:错误不处理,后续数据全是错的
  • UART中断 < 系统滴答中断:系统时钟是命根子,不能被打乱

实际配置示例(基于Cortex-M内核):

// 假设NVIC优先级分组为4(4位抢占,0位子优先级)
NVIC_SetPriority(UART1_IRQn, 1);   // RX中断:抢占优先级1
NVIC_SetPriority(UART2_IRQn, 2);   // RX中断:抢占优先级2
NVIC_SetPriority(UART1_TX_IRQn, 3); // TX中断:抢占优先级3
NVIC_SetPriority(SysTick_IRQn, 0);  // 系统滴答:最高优先级

中断服务函数编写规范

写ISR(中断服务函数)有个铁律:快进快出。我见过有人把整个协议解析放在ISR里,结果串口波特率一高,CPU就卡死在中断里。

我个人推荐的ISR结构:

  1. 读取中断标志:判断是RX、TX还是错误
  2. 搬运数据:RX中断把数据放入环形缓冲区,TX中断从缓冲区取数据
  3. 清除标志:务必在退出前清除中断标志
  4. 设置事件标志:通知主循环“有数据来了”
我曾经踩过的坑:在ISR里调用printf打印调试信息。printf本身可能触发UART发送中断,导致中断嵌套死循环。调试时用GPIO翻转代替打印,安全得多。
// 标准ISR模板
void UART1_IRQHandler(void)
{
    uint32_t status = UART1->SR;  // 读取状态寄存器
    
    if (status & UART_SR_RXNE)    // RX中断
    {
        uint8_t data = UART1->DR; // 读取数据(自动清除RXNE标志)
        ringbuf_push(&rx_buf, data);
        event_set(EVENT_UART1_RX);
    }
    
    if (status & UART_SR_TXE)     // TX中断
    {
        if (ringbuf_pop(&tx_buf, &data))
            UART1->DR = data;     // 发送下一字节
        else
            UART1->CR1 &= ~UART_CR1_TXEIE; // 关闭TX中断
    }
    
    if (status & UART_SR_ORE)     // 溢出错误
    {
        UART1->SR &= ~UART_SR_ORE; // 清除溢出标志
        error_count++;
    }
}

中断嵌套与原子操作

中断嵌套,说白了就是高优先级中断打断低优先级中断。这本身不是问题,问题在于共享资源的访问。

你想想看,主循环在修改一个全局变量,RX中断突然进来也修改同一个变量——结果就是数据错乱。这就是典型的临界区问题。

解决方式有三种:

  • 关中断保护:进入临界区前关中断,退出后开中断。简单粗暴,但关中断时间不能太长
  • 使用原子操作:Cortex-M内核支持LDREX/STREX指令,适合对单个变量做原子读写
  • 中断延迟处理:ISR只做数据搬运,复杂逻辑放到主循环或任务中处理

原子操作示例(使用CMSIS-Core):

// 原子加1操作
uint32_t atomic_increment(uint32_t *val)
{
    uint32_t result;
    do {
        result = __LDREXW(val);
        result++;
    } while (__STREXW(result, val));
    __DMB(); // 数据内存屏障
    return result;
}

中断延迟分析

中断延迟,就是从硬件触发中断到CPU执行ISR第一条指令的时间。这个指标直接影响系统的实时性。

我做过一个测试:在STM32F103上,UART波特率115200,一个字节的传输时间大约是87μs。如果中断延迟超过87μs,下一个字节来了,上一个还没处理完——丢数据。

影响中断延迟的因素:

因素 典型延迟 优化方法
CPU响应时间 12~16个时钟周期 使用零等待中断控制器
高优先级ISR执行时间 取决于ISR长度 ISR尽量短,复杂逻辑放任务
关中断时间 取决于临界区长度 减少关中断区域,使用原子操作
中断优先级反转 不可预测 使用优先级继承协议
实测方法:在ISR入口处翻转一个GPIO,用示波器测量GPIO翻转时刻与UART数据线上的起始位下降沿之间的时间差。这就是实际的中断延迟。
UART中断处理流程与延迟分析 硬件触发中断 CPU响应(12~16周期) 中断延迟 ISR执行(快进快出) 读取标志 → 搬运数据 → 清除标志 → 设置事件 中断嵌套(高优先级打断) 注意共享资源保护 原子操作 / 关中断保护 解决临界区问题 主循环 / 任务处理(协议解析等) 关键指标:中断延迟 < 一个字节传输时间(如115200bps时 < 87μs)
避坑指南:我曾经在一个项目中,UART中断里调用了malloc分配内存。malloc内部可能关中断,也可能触发缺页中断。结果就是中断延迟从几微秒飙升到几百微秒,串口数据丢得一塌糊涂。记住:ISR里绝对不要用动态内存分配、互斥锁、或者任何可能阻塞的操作。

总结一下UART中断处理的核心要点:分清中断类型、合理配置优先级、ISR快进快出、注意原子操作、控制中断延迟。把这些做到位了,多机通信的实时性和可靠性就有了基本保障。