3. 中断系统入门:中断的概念与优先级、中断向量表、中断服务函数(ISR)编写、外部中断实战

各位同学,欢迎来到第三章。这一章我们聊点真正让嵌入式系统“活”起来的东西——中断。

说实话,我刚入行那会儿,对中断的理解就是“一个东西突然跳出来打断你”。后来被项目毒打了几次,才真正明白中断是嵌入式系统的灵魂。没有中断,你的CPU就得像个傻子一样轮询,浪费大量时间在“等”上。

3.1 中断到底是什么?

想象一下,你正在写代码,突然有人拍你肩膀。你停下手中的活,转头看是谁,处理完他的事,再回头继续写。这就是中断。

在嵌入式世界里,中断是硬件或软件发出的一种信号,告诉CPU:“嘿,有急事,先处理我!”CPU会暂停当前任务,保存现场,跳去执行一个专门的函数,处理完再回来继续。

核心概念:中断的本质是“异步事件处理”。它让CPU从轮询的苦海中解脱出来,只在需要时才响应。

我做过一个项目,用轮询方式检测按键。CPU每毫秒都要去读一次GPIO,结果功耗高得离谱。后来改成中断触发,功耗直接降了80%。你想想看,这差距有多大。

3.2 中断优先级:谁更紧急?

多个中断同时来了怎么办?谁先处理?这就涉及到优先级。

中断优先级,说白了就是给每个中断源排个队。优先级高的先执行,低的靠后。如果高优先级中断正在处理,低优先级的来了也得等着。

大部分MCU支持可编程优先级。比如STM32的NVIC(嵌套向量中断控制器),可以设置抢占优先级和子优先级。

优先级类型 说明 典型应用
抢占优先级 高优先级可以打断低优先级 紧急故障处理
子优先级 同抢占优先级下,子优先级高的先执行 普通外设中断

注意:优先级设置不当会导致“中断风暴”或“死锁”。我曾经在一个项目中把定时器中断优先级设得比看门狗还高,结果看门狗永远无法复位系统……嗯,那是个难忘的加班夜。

3.3 中断向量表:中断的“电话簿”

CPU怎么知道中断来了该跳到哪里去执行?靠的就是中断向量表。

中断向量表是一张表格,每个中断源对应一个表项,里面存放着该中断服务函数(ISR)的入口地址。CPU收到中断信号后,根据中断号查表,直接跳转。

这张表通常放在Flash或RAM的起始地址。以ARM Cortex-M为例,向量表从0x00000000开始,前几个表项是栈指针和复位向量,后面依次是各种异常和中断向量。

// 典型的Cortex-M向量表布局(简化)
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    (void *)&__StackTop,          // 0: 栈顶指针
    Reset_Handler,                // 1: 复位向量
    NMI_Handler,                  // 2: NMI
    HardFault_Handler,            // 3: 硬错误
    // ... 其他异常向量 ...
    TIM2_IRQHandler,              // 28: 定时器2中断
    USART1_IRQHandler,            // 37: 串口1中断
    EXTI0_IRQHandler,             // 6: 外部中断0
    // ...
};

我个人习惯把向量表放在独立的段里,这样链接时好控制位置。有些MCU支持向量表重定位,运行时可以搬到RAM里,方便动态修改。

3.4 中断服务函数(ISR)编写

ISR就是中断发生时执行的函数。写ISR有几个铁律,违反了系统就会出问题。

  • 短小精悍:ISR里不要做复杂运算,不要调用printf,不要用延时。越快退出越好。
  • 保存和恢复现场:编译器会自动处理,但如果你写汇编,就得手动来。
  • 避免死锁:ISR里不要等待某个资源,尤其是主循环也在用的资源。
  • 清除中断标志:处理完必须清除中断标志,否则会一直触发。
// 一个典型的外部中断ISR示例
void EXTI0_IRQHandler(void) {
    // 1. 检查中断标志
    if (EXTI->PR & EXTI_PR_PR0) {
        // 2. 处理中断(这里只做标记,实际处理放主循环)
        g_bButtonPressed = 1;
        
        // 3. 清除中断标志
        EXTI->PR = EXTI_PR_PR0;
    }
}

经验之谈:ISR里尽量只做“标记”,把实际处理放到主循环或任务里。这样能减少ISR占用时间,避免影响其他中断。我见过有人直接在ISR里做FFT运算,结果系统直接卡死。

3.5 外部中断实战:按键触发

理论说完了,咱们来点实际的。以STM32F103为例,实现一个按键外部中断。

步骤很简单:配置GPIO为输入,使能外部中断线,设置触发边沿,编写ISR。

// 外部中断初始化
void EXTI_Button_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    EXTI_InitTypeDef EXTI_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;
    
    // 1. 使能GPIO时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    
    // 2. 配置PA0为输入
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;  // 上拉输入
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 3. 连接EXTI线到PA0
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
    
    // 4. 配置EXTI线0
    EXTI_InitStruct.EXTI_Line = EXTI_Line0;
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;  // 下降沿触发
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStruct);
    
    // 5. 配置NVIC
    NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
}

这段代码配置了PA0作为外部中断输入,下降沿触发。按键按下时,电平从高变低,触发中断。

ISR里我们只设置一个全局标志,主循环检测到这个标志后再做具体处理。这样既保证了ISR的快速退出,又避免了在中断里做复杂操作。

避坑指南:我曾经在ISR里直接调用HAL_Delay(),结果程序卡死。原因是HAL_Delay依赖SysTick中断,而SysTick中断优先级比外部中断低,导致死锁。记住:ISR里不要调用任何可能依赖其他中断的函数。

3.6 中断系统的知识体系

为了让你更直观地理解中断系统的整体结构,我画了一张图。

中断系统核心知识体系 中断源 外部/定时器/串口等 中断控制器 NVIC / 优先级管理 中断向量表 ISR入口地址映射 CPU 保存现场 / 跳转执行 ISR 中断服务函数 返回主程序 关键原则 ISR短小精悍 清除中断标志

这张图展示了中断的完整流程:中断源产生信号 -> 中断控制器判断优先级 -> CPU查向量表 -> 跳转执行ISR -> 返回主程序。每一步都环环相扣,缺一不可。

3.7 小结

中断是嵌入式系统的核心机制,掌握了它,你的程序才能真正做到“实时响应”。

这一章我们讲了中断的概念、优先级、向量表、ISR编写,还做了一个外部中断的实战。这些东西看起来简单,但用好了能让系统性能提升一个档次。

记住我踩过的坑:ISR里别做复杂操作,优先级别乱设,中断标志别忘了清。做到这三点,你的中断系统基本就稳了。