2、RTOS中断管理机制:中断服务例程(ISR)与任务的关系、临界区保护、中断延迟

好,咱们接着聊中断。上一章我们把中断的硬件基础讲清楚了,这一章要深入RTOS的核心——中断服务例程(ISR)和任务到底怎么配合?临界区怎么保护?中断延迟这个指标又该怎么看?

说实话,我见过不少工程师,裸机编程玩得很溜,一上RTOS就翻车。翻车点往往就在中断管理上。你想想看,裸机里中断就是中断,主循环就是主循环,关系简单。但RTOS里多了任务调度,中断和任务之间就有了“互动”,这个互动一旦处理不好,系统就崩了。

2.1 ISR与任务的关系:谁该做什么?

先问一个问题:中断来了,数据到了,你是在ISR里直接处理,还是扔给任务去处理?

我的答案是:ISR只做最紧急、最轻量的事。什么叫最紧急?把硬件数据搬到内存缓冲区,清中断标志位,通知任务“活来了”。这就够了。剩下的解析、运算、协议栈处理,统统交给任务。

为什么?因为ISR里不能阻塞,不能等信号量,不能调用可能引起任务切换的API(除非你明确知道自己在做什么)。ISR执行时间越长,系统响应越差。我在项目中遇到过有人把整个TCP/IP协议栈解析放在ISR里,结果系统中断延迟飙到毫秒级,CAN报文直接丢帧。嗯,那场面,相当惨烈。

所以,ISR和任务的分工,我建议这样划分:

职责 ISR 任务
数据搬运 ✅ 从硬件FIFO搬到内存缓冲区 ❌ 太慢,来不及
清中断标志 ✅ 必须做 ❌ 硬件层面的事
协议解析 ❌ 耗时太长 ✅ 慢慢来
复杂计算 ❌ 阻塞调度 ✅ 适合
通知任务 ✅ 发信号量或消息 ❌ 被动等待

说白了,ISR是“快递员”,只负责把包裹放到门口,任务才是“拆包裹的人”。

2.2 临界区保护:别让中断把你的数据撕碎

临界区,这是个老生常谈的话题。但我要说的是,在RTOS里,临界区保护比裸机复杂得多。

裸机里,你关中断就完事了。RTOS里呢?你不仅要考虑中断,还要考虑其他任务。比如一个全局变量,ISR在写,任务在读。如果不保护,任务读到一半,ISR把数据改了,那读出来的就是“半成品”,逻辑直接崩。

我个人的习惯是:能不用全局变量就不用。但有些场景躲不开,比如共享缓冲区、状态标志。这时候就得用临界区保护。

RTOS里常见的临界区保护方式有两种:

  • 关中断:最简单粗暴,但会影响中断延迟。适合保护极短的操作,比如修改一个变量。
  • 使用互斥量或信号量:更优雅,但ISR里不能等信号量(会死锁)。所以ISR和任务之间,我建议用任务通知无锁队列

来看一个典型例子。假设一个ADC中断,每次采样完把数据放到缓冲区:

// 错误示范:没有保护
int buffer[100];
int index = 0;

void ADC_ISR(void) {
    buffer[index++] = read_adc();  // 如果此时任务也在读buffer,数据就乱了
}

// 正确示范:关中断保护
void ADC_ISR(void) {
    taskENTER_CRITICAL();
    buffer[index++] = read_adc();
    taskEXIT_CRITICAL();
}

注意,taskENTER_CRITICAL() 在FreeRTOS里会关掉所有可屏蔽中断。所以临界区里的代码一定要短,否则中断延迟会爆炸。我曾经在一个项目里看到有人把日志打印放在临界区里,结果串口打印一次要几毫秒,中断延迟直接上天。嗯,后来我帮他改成了用DMA+环形缓冲区,问题才解决。

⚠️ 警告: 临界区里绝对不要调用阻塞函数!比如 vTaskDelay()printf()(如果用了重定向)、或者等待信号量。否则系统会死锁,或者中断延迟变得不可接受。

2.3 中断延迟:一个被低估的指标

中断延迟,说白了就是从中断触发到ISR第一条指令执行的时间。这个指标在车载领域极其重要。为什么?因为CAN报文、LIN总线、传感器数据都有严格的时序要求。如果中断延迟太大,数据就丢了。

中断延迟由哪些部分组成?我列一下:

  1. 硬件延迟:CPU响应中断的时间,一般几个时钟周期,固定不变。
  2. 中断嵌套:如果高优先级中断正在执行,低优先级中断就得等。
  3. 临界区:这是最大的变量。如果临界区太长,所有中断都得排队。
  4. 指令执行:当前指令执行完才能响应中断,一般忽略不计。

你看,真正可控的是第2点和第3点。尤其是临界区,很多工程师不注意,随手写个长临界区,结果系统响应变慢。

我建议你这样做:

  • 测量中断延迟:用GPIO翻转法,在ISR入口拉高一个引脚,用示波器看。这是最直观的方法。
  • 设置中断优先级:把时间敏感的中断(比如CAN、定时器)设成最高优先级,确保它们不会被其他中断阻塞。
  • 缩短临界区:临界区里只做原子操作,比如赋值、读寄存器。复杂逻辑放到外面。
💡 小技巧: 我习惯在临界区前后加一个断言,检查中断延迟是否超标。比如用硬件定时器记录进入临界区的时间,如果超过某个阈值(比如10微秒),就触发一个警告。这样在开发阶段就能发现问题,而不是等到路试时才暴露。

最后说一句,中断延迟不是越小越好,而是可预测才好。车载系统要求确定性,你哪怕延迟大一点,但只要每次都是固定的,系统就能接受。最怕的是忽大忽小,那才是噩梦。

好了,这一章就到这里。下一章我们讲中断嵌套和优先级配置,那是另一个容易踩坑的地方。到时候我会分享一个我踩过的坑——因为优先级设错了,导致CAN报文在高速工况下周期性丢帧。嗯,那故事挺有意思的。