1. 中断基础概念:中断的定义、中断向量表、中断优先级与嵌套

大家好,欢迎来到咱们这门课的第一节。中断这个话题,说实在的,是MCU开发的基石。你想想看,一个单片机如果没有中断,那就只能轮询——CPU得不停地去问“你好了没?你好了没?”,效率低得吓人。我刚开始做嵌入式那会儿,第一个项目就是搞一个串口通信,没用中断,结果CPU啥都干不了,光顾着在那轮询了。后来一换成中断驱动,整个世界都清净了。

1.1 中断到底是什么?

中断,说白了就是CPU正在干一件事,突然被一个外部或内部的事件打断了。CPU得把手头的活儿先放一放,去处理这个紧急事件,处理完了再回来接着干。

举个例子:你在家看书,突然门铃响了。你放下书去开门,跟快递员说几句话,签收包裹,然后回来继续看书。这个过程,就是中断。

  • 门铃响 — 中断请求
  • 放下书 — 保存现场(把当前读到哪一页记下来)
  • 去开门 — 跳转到中断服务函数
  • 签收包裹 — 执行中断处理
  • 回来继续看书 — 恢复现场,继续执行

在MCU里,中断源有很多种:外部引脚电平变化、定时器溢出、串口收到数据、ADC转换完成……每个中断源都有一个唯一的中断号。

核心要点:中断机制让CPU从“被动轮询”变成了“主动响应”,实时性就是这么来的。

1.2 中断向量表 — CPU的“紧急联络簿”

中断向量表,我习惯叫它“中断跳转表”。它是一张存储在Flash或ROM里的表格,每个表项对应一个中断号,里面存放的是该中断服务函数(ISR)的入口地址。

当CPU收到一个中断请求时,硬件会自动做这几件事:

  1. 保存当前正在执行的指令地址(也就是PC指针)
  2. 根据中断号,去中断向量表里查对应的ISR地址
  3. 跳转到那个地址去执行

以STM32为例,它的中断向量表起始地址默认在0x00000000。第一个表项放的是栈顶指针,第二个表项放的是复位中断的入口地址,后面依次是各种外设中断。

; STM32F103 中断向量表示例(汇编)
__Vectors       DCD     __initial_sp           ; 栈顶指针
                DCD     Reset_Handler          ; 复位中断
                DCD     NMI_Handler            ; NMI
                DCD     HardFault_Handler      ; 硬错误
                DCD     MemManage_Handler      ; 内存管理
                DCD     BusFault_Handler       ; 总线错误
                DCD     UsageFault_Handler     ; 用法错误
                ; ... 后面还有几十个中断向量
                DCD     TIM2_IRQHandler        ; 定时器2中断
                DCD     TIM3_IRQHandler        ; 定时器3中断
                DCD     USART1_IRQHandler      ; 串口1中断

个人经验:我遇到过一个问题,程序跑飞了查了半天,最后发现是中断向量表被意外修改了。嗯,这里要注意——如果你的MCU支持中断向量表重定向(比如把向量表搬到SRAM里),一定要确保那片内存不会被其他代码踩到。

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

多个中断同时发生怎么办?CPU得有个决策机制。这就是中断优先级干的事。

不同的MCU对优先级的实现方式不太一样。拿ARM Cortex-M系列来说,它支持两种优先级模型:

优先级模型 特点 应用场景
固定优先级 中断号越小,优先级越高 简单系统,中断源少
可编程优先级 用户可以给每个中断设置优先级数值 复杂系统,需要精细控制

在Cortex-M3/M4里,优先级数值越小,优先级越高。比如你给定时器中断设优先级0,串口中断设优先级1,那定时器中断永远比串口中断优先响应。

// STM32 中断优先级配置示例
NVIC_InitTypeDef NVIC_InitStructure;

// 配置USART1中断,优先级为1
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;         // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// 配置TIM2中断,优先级为0(更高)
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

避坑指南:我曾经在一个项目里把两个关键中断设成了相同优先级,结果它们同时触发时,系统行为变得不可预测。后来查手册才发现,同优先级的中断,硬件会按中断号顺序处理——但这并不是所有MCU都一样的。所以我的建议是:尽量给每个中断分配不同的优先级,别偷懒。

1.4 中断嵌套 — 中断里还能再进中断

中断嵌套,就是正在处理一个中断的时候,来了一个更高优先级的中断,CPU会暂停当前的中断服务函数,先去处理那个更紧急的。

这就像你正在接一个电话,突然另一个更重要的电话打进来了——你会跟第一个说“稍等一下”,然后接第二个。

中断嵌套的规则其实很简单:

  • 高优先级中断可以打断低优先级中断
  • 同优先级中断不能互相打断
  • 低优先级中断不能打断高优先级中断

在Cortex-M系列里,中断嵌套是由硬件自动完成的。你不需要在ISR里写额外的代码来保存和恢复现场——硬件已经帮你做了大部分工作。但要注意一点:中断嵌套层数太多,会消耗大量的栈空间。

关键点:中断嵌套虽然能保证高优先级事件得到及时响应,但也会带来风险——栈溢出。我见过一个产品,平时跑得好好的,一旦所有中断同时爆发,系统就死机。最后查出来是中断嵌套把栈给撑爆了。所以,合理规划中断优先级,控制嵌套深度,这是基本功。

1.5 中断响应流程 — 硬件和软件的分工

一个完整的中断响应过程,硬件和软件各干各的活:

硬件自动完成:

  1. 检测到中断请求信号
  2. 判断优先级,决定是否响应
  3. 保存当前PC和xPSR寄存器(Cortex-M还会自动压栈)
  4. 从向量表取出ISR地址
  5. 跳转到ISR执行

软件需要完成:

  1. 在ISR里处理中断事件(读数据、清标志位等)
  2. 如果需要,通知主循环或RTOS任务
  3. 退出ISR(硬件自动恢复现场)
// 一个典型的中断服务函数
void USART1_IRQHandler(void)
{
    // 检查是不是接收中断
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t data = USART_ReceiveData(USART1);  // 读数据
        // 把数据放到缓冲区,或者设置一个标志位
        g_rx_buffer[g_rx_index++] = data;
        
        // 清中断标志位(重要!)
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

我的习惯:ISR里尽量只做最必要的事——读数据、清标志、设标志。复杂的处理逻辑放到主循环或任务里去。为什么?因为ISR执行时间越长,系统对其他事件的响应就越慢。我曾经把一个浮点运算放在ISR里,结果整个系统的实时性一塌糊涂。

1.6 中断延迟 — 从事件发生到CPU响应

中断延迟,指的是从中断事件发生到CPU开始执行ISR的第一条指令之间的时间。这个时间受几个因素影响:

  • CPU当前正在执行的指令(有些指令执行时间很长)
  • 中断优先级判断的时间
  • 是否有关中断的情况
  • 中断嵌套的深度

对于Cortex-M系列,中断延迟通常在12个时钟周期左右。但如果你在ISR里关了中断,或者执行了某些特殊指令,这个延迟会变长。

注意:中断延迟是衡量MCU实时性的关键指标。如果你在做电机控制、开关电源这类对时间敏感的应用,一定要把中断延迟算清楚。我见过一个做BLDC驱动的工程师,因为没算中断延迟,导致换相时机总是差那么几微秒,电机嗡嗡响。

好了,中断的基础概念就聊到这儿。下一节我们会深入讲讲中断的配置实战,包括怎么设置优先级分组、怎么处理中断标志位这些具体操作。记住一句话:中断用好了,你的MCU就像长了眼睛和耳朵;用不好,那就是给自己挖坑。