中断服务程序(ISR)设计原则:短小精悍、避免阻塞、不可重入问题

各位同学,咱们今天聊聊ISR设计。说白了,中断服务程序就是CPU在响应外部事件时,临时跳过去执行的一段代码。这段代码写得好不好,直接决定了整个系统的实时性和稳定性。我在项目里见过太多因为ISR写得拖泥带水,导致系统崩溃的例子。嗯,咱们一个一个来说。

原则一:短小精悍——ISR不是让你写长篇大论的地方

我个人习惯,ISR里的代码量控制在几十行以内。为什么?因为中断是抢占式的。你这边在ISR里磨蹭,其他高优先级的中断就得等着。系统响应就慢了。

举个例子,我在一个电机控制项目里,同事把PID计算直接塞进了定时器ISR。结果呢?电机一启动,系统就卡死。后来我把计算挪到任务里,ISR只负责读传感器、发信号,问题就解决了。

核心原则:ISR只做最必要的事——读取硬件寄存器、清除中断标志、通过队列或信号量通知任务。其他所有处理,交给任务去做。

你想想看,ISR就像急诊室的医生,只做紧急处理,然后转给专科医生。你不能让急诊医生做开胸手术,对吧?

原则二:避免阻塞——ISR里不能等

这个原则,我强调多少遍都不为过。ISR里绝对不能调用阻塞型函数,比如等待信号量、等待消息队列、延时函数。为什么?因为ISR运行在中断上下文中,它不归属于任何任务。一旦阻塞,整个系统就挂在那了。

我曾经在一个通信项目中,看到有人在UART接收ISR里调用了vTaskDelay()。结果呢?串口数据一多,系统直接死机。排查了两天才找到原因。

避坑指南:ISR中禁止使用以下操作:

  • 调用阻塞型API(如vTaskDelayxSemaphoreTake
  • 等待外部事件或资源
  • 执行长循环或复杂计算
  • 调用不可重入的库函数

那ISR里怎么跟任务通信?用中断安全的API。比如FreeRTOS里,xQueueSendFromISRxSemaphoreGiveFromISR就是专门给ISR用的。它们不会阻塞,只是把数据丢进队列或释放信号量,然后让调度器在退出ISR后决定是否切换任务。

原则三:不可重入问题——ISR会嵌套,你得防着点

这个坑,我踩过不止一次。什么叫不可重入?就是一个函数在执行过程中被中断打断,然后中断里又调用了同一个函数。如果这个函数使用了全局变量或静态变量,数据就乱了。

举个例子,你写了一个函数update_counter(),里面用了一个全局变量counter。主循环里正在调用它,突然来了个中断,ISR里也调用了update_counter()。等ISR返回,主循环里的counter值已经不对了。这就是典型的不可重入问题。

我的经验:ISR和任务共享的变量,一定要用volatile关键字声明。如果变量是多字节的(比如32位int在8位MCU上),还要考虑原子访问。最简单的办法:ISR只写,任务只读,或者用临界区保护。

那怎么解决不可重入?三个办法:

  1. ISR里只调用可重入函数——函数内部不使用全局/静态变量,只使用局部变量或参数传递。
  2. 使用临界区——在访问共享资源时,关中断或使用互斥量。但注意,临界区要尽量短。
  3. ISR和任务分工明确——ISR只负责“通知”,任务负责“处理”。这样共享资源的访问都在任务上下文中,自然就避免了重入问题。

知识体系:ISR设计核心逻辑

下面这张图,是我自己总结的ISR设计流程。你照着这个思路走,基本不会出大问题。

ISR设计核心逻辑 外部中断触发 ISR入口(保存上下文) 是否可快速处理? 直接处理(短操作) 通知任务处理 ISR出口(恢复上下文)

总结一下

ISR设计,说白了就三句话:

  • 短小精悍——只做最必要的事,其他交给任务。
  • 避免阻塞——ISR里不能等,用中断安全API通信。
  • 注意不可重入——共享资源要保护,函数要可重入。

我在实际项目中,通常把ISR写成这样:先读硬件寄存器,清中断标志,然后通过队列或信号量把数据发给任务,最后退出。整个ISR不超过20行代码。这样既保证了实时性,又避免了各种坑。

记住:ISR是系统的“神经反射”,不是“大脑思考”。反射要快,思考要交给大脑(任务)。

好,这一节就到这里。下一节咱们聊聊具体怎么用队列和信号量在ISR和任务之间传数据。


专注资料整理