中断处理机制: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结构:
- 读取中断标志:判断是RX、TX还是错误
- 搬运数据:RX中断把数据放入环形缓冲区,TX中断从缓冲区取数据
- 清除标志:务必在退出前清除中断标志
- 设置事件标志:通知主循环“有数据来了”
我曾经踩过的坑:在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中断里调用了malloc分配内存。malloc内部可能关中断,也可能触发缺页中断。结果就是中断延迟从几微秒飙升到几百微秒,串口数据丢得一塌糊涂。记住:ISR里绝对不要用动态内存分配、互斥锁、或者任何可能阻塞的操作。
总结一下UART中断处理的核心要点:分清中断类型、合理配置优先级、ISR快进快出、注意原子操作、控制中断延迟。把这些做到位了,多机通信的实时性和可靠性就有了基本保障。