1. 中断基础:中断的概念、中断向量表、中断优先级

大家好,我是你们的嵌入式系统讲师。今天咱们来聊聊RTOS里最核心、也最让人头疼的一个话题——中断。说实话,我做了十几年嵌入式开发,见过太多因为中断没处理好而翻车的项目了。我自己也踩过不少坑,所以这一章我会把最基础、最实用的东西掰开揉碎了讲给你听。

1.1 中断到底是什么?

先问大家一个问题:你正在写代码,突然有人拍你肩膀说「外卖到了」,你会怎么做?

嗯,正常人都会先停下手里的活,去拿外卖,回来再继续写。中断就是这个道理。

中断,说白了就是CPU正在执行主程序时,突然收到一个「紧急信号」,于是暂停当前任务,转而去处理这个信号对应的服务程序。处理完了,再回来接着干。

这个「紧急信号」可以来自很多地方:

  • 定时器溢出(比如每1ms产生一次中断)
  • 外部引脚电平变化(比如按键按下)
  • 串口收到数据
  • ADC转换完成

我个人习惯把中断比作「硬件给软件打的电话」。你想想看,如果没有中断,CPU就得不停地轮询检查各个外设有没有事情要处理——那效率得多低啊!

核心要点:中断机制让CPU从「主动轮询」变成了「被动响应」,大大提高了系统的实时性和效率。

1.2 中断向量表——中断的「通讯录」

好,现在CPU收到了中断信号。它怎么知道该跳到哪里去执行代码呢?

这就引出了中断向量表的概念。

中断向量表,其实就是一张表格。表格里每一行对应一个中断源,记录着该中断服务程序(ISR)的入口地址。CPU收到中断后,会根据中断号去查这张表,找到对应的地址,然后跳过去执行。

我举个例子你就明白了:

// 以STM32为例,中断向量表通常放在代码的最开头
// 这是简化后的示意代码

__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    &__StackTop,           // 0: 栈顶指针
    Reset_Handler,         // 1: 复位中断
    NMI_Handler,           // 2: 不可屏蔽中断
    HardFault_Handler,     // 3: 硬件错误中断
    MemManage_Handler,     // 4: 内存管理中断
    BusFault_Handler,      // 5: 总线错误中断
    UsageFault_Handler,    // 6: 使用错误中断
    0, 0, 0, 0,           // 保留
    SVC_Handler,           // 11: 系统服务调用
    DebugMon_Handler,      // 12: 调试监视器
    0,                     // 13: 保留
    PendSV_Handler,        // 14: 可挂起系统服务
    SysTick_Handler,       // 15: 系统滴答定时器
    // ... 后面是外设中断
    TIM2_IRQHandler,       // 28: 定时器2中断
    USART1_IRQHandler,     // 37: 串口1中断
    // ...
};

你看,每个中断号对应一个函数指针。CPU硬件就是靠这个表来「导航」的。

小技巧:我在项目中遇到过一个问题——程序跑飞了,怎么都查不到原因。后来发现是中断向量表被意外改写了。所以我现在都会把中断向量表放在只读区域,或者加上写保护。这个坑,你记住了。

1.3 中断优先级——谁更紧急?

现在问题来了:如果两个中断同时发生,CPU先处理谁?

这就涉及到中断优先级了。

大多数现代MCU都支持多级中断优先级。比如STM32的NVIC(嵌套向量中断控制器)支持最多16级抢占优先级和16级子优先级。

我画了一张图,帮你理解中断优先级的工作逻辑:

中断优先级处理流程 主程序(优先级最低) 中断1(优先级3) 正在执行 中断2 优先级1 更高优先级 抢占执行 时间→ 主程序执行 中断1执行 中断2 主程序 中断1(优先级3) 中断2(优先级1,更高) 说明:中断2的优先级(1)高于中断1的优先级(3) 所以中断2可以抢占正在执行的中断1,处理完后再恢复中断1的执行

优先级的设计有几个关键点:

  • 抢占优先级:高优先级中断可以打断低优先级中断的执行
  • 子优先级:当抢占优先级相同时,子优先级决定谁先执行,但不能互相打断
  • 优先级数值越小,优先级越高(这是ARM Cortex-M系列的规则)

注意:我曾经在一个项目中,把两个外设的中断优先级设成了相同的值。结果其中一个中断处理时间太长,导致另一个中断响应延迟,整个系统出现了间歇性的卡顿。排查了整整两天才找到原因。所以,不同实时性要求的中断,一定要分配不同的优先级

1.4 中断的响应过程

一个完整的中断响应过程,大致分这么几步:

  1. 中断请求:外设或内部模块产生中断信号
  2. 中断判优:CPU检查当前中断的优先级是否高于正在执行的任务
  3. 保护现场:CPU自动压栈(保存当前执行的地址、寄存器状态等)
  4. 查向量表:根据中断号找到对应的ISR入口地址
  5. 执行ISR:跳转到中断服务程序执行
  6. 恢复现场:执行完ISR后,出栈恢复之前的状态
  7. 返回主程序:继续执行被中断的任务

这里面,第3步「保护现场」和第6步「恢复现场」是硬件自动完成的。你不需要手动写代码去压栈出栈——嗯,除非你在做非常底层的汇编开发。

1.5 中断服务程序(ISR)的编写原则

写ISR和写普通函数不一样。我总结了几个原则,都是血泪教训换来的:

原则 说明 我的经验
短小精悍 ISR里只做最必要的事,比如置一个标志位、读一个数据 我见过有人在ISR里做浮点运算,结果系统直接崩了
不要阻塞 不要在ISR里调用延时函数、等待信号量等阻塞操作 有一次我在ISR里用了while循环等待,整个系统卡死了
避免重入 同一个ISR不要被自己再次打断(除非你明确知道自己在做什么) 我习惯在ISR开头关中断,结尾开中断
数据保护 ISR和主程序共享的变量,要用volatile声明 这个坑我踩过,不加volatile,编译器优化后数据全乱套了

推荐做法:ISR里只做「标记」,真正的「处理」交给任务去做。比如ISR里设置一个全局标志位,然后通知RTOS里的某个任务去处理。这样既保证了实时性,又避免了ISR过于复杂。

1.6 中断与RTOS的关系

在RTOS环境下,中断的管理变得更加重要。因为:

  • RTOS的时钟节拍(Tick)本身就是由定时器中断驱动的
  • 任务切换(Context Switch)通常是在中断中完成的(比如PendSV中断)
  • 中断的优先级必须高于RTOS的任务优先级,否则系统无法及时响应外部事件

我个人习惯的做法是:

  • 把实时性要求最高的中断设为最高优先级(比如电机控制中的电流采样中断)
  • 把RTOS的Tick中断设为中等优先级
  • 把一些非关键的外设中断设为较低优先级

这样分层设计,既能保证关键任务的实时性,又不会让低优先级中断饿死。


好了,这一章的内容就到这里。中断是RTOS的基石,理解透了后面学任务通信、同步互斥才会轻松。下一章我们会深入讲中断服务程序与任务之间的数据传递——这可是实际项目中天天要用的技能。

公众号:蓝海资料掘金营,微信deep3321