1. GPIO中断基础:从概念到实战

各位同学,咱们今天聊聊GPIO中断。说实话,中断这个概念,我刚开始学单片机时也觉得挺玄乎的。但说白了,它就是CPU处理突发事件的一种机制——就像你正在写代码,突然有人拍你肩膀,你得先停下来看看什么事,处理完了再继续写。

1.1 中断到底是什么?

中断,就是CPU暂停当前任务,去处理一个更紧急的事件。处理完再回来接着干。这个“紧急事件”可以是按键按下、定时器溢出、串口收到数据等等。

我举个例子你就明白了。你在厨房炒菜(主程序),突然水开了(中断事件),你得关火去倒水(中断服务函数),倒完水回来继续炒菜(返回主程序)。嗯,就是这么个道理。

核心要点:中断机制让CPU能及时响应外部事件,不用一直轮询等待。这在实时系统中至关重要——你想想看,如果按键按下后要等几百毫秒才能响应,那用户体验得多糟糕。

1.2 中断优先级:谁更紧急谁先处理

多个中断同时发生时,谁先处理?这就涉及到中断优先级了。高优先级的中断可以打断低优先级的中断,这叫“中断嵌套”。

我在一个工业控制项目中就遇到过这个问题。当时用了两个外部中断,一个负责急停按钮,一个负责普通按键。急停按钮的优先级必须设得最高,否则一旦系统卡在普通按键处理中,急停就失效了——这可是安全隐患。

优先级级别 典型应用场景 说明
最高(0级) 系统时钟、紧急停止 不可被其他中断打断
高(1级) 高速通信(SPI、UART) 数据不能丢
中(2级) 定时器、ADC转换 周期性任务
低(3级) GPIO按键、普通外设 可以稍等

注意:优先级不是设得越高越好。我曾经见过一个项目,把所有中断都设成最高优先级,结果高优先级中断互相嵌套,堆栈溢出直接死机。优先级要按实际需求合理分配。

1.3 中断向量表:中断的“通讯录”

中断向量表,说白了就是一张地址表。每个中断源对应一个表项,里面存着该中断服务函数(ISR)的入口地址。CPU收到中断信号后,就去查这张表,找到对应的ISR地址,跳过去执行。

以STM32为例,中断向量表默认放在Flash的起始地址0x00000000处。你可以通过修改VTOR寄存器来重定向向量表的位置——这在做Bootloader时特别有用。

// STM32中断向量表示例(部分)
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    &__initial_sp,          // 0: 栈顶指针
    Reset_Handler,          // 1: 复位
    NMI_Handler,            // 2: NMI
    HardFault_Handler,      // 3: 硬错误
    MemManage_Handler,      // 4: 内存管理
    BusFault_Handler,       // 5: 总线错误
    UsageFault_Handler,     // 6: 用法错误
    0, 0, 0, 0,            // 保留
    SVC_Handler,            // 11: SVC
    DebugMon_Handler,       // 12: 调试监视
    0,                      // 13: 保留
    PendSV_Handler,         // 14: PendSV
    SysTick_Handler,        // 15: SysTick
    // ... 外部中断从这里开始
    EXTI0_IRQHandler,       // 16: EXTI0
    EXTI1_IRQHandler,       // 17: EXTI1
    // ...
};

小技巧:调试时可以用IDE查看向量表内容,确认中断是否正确映射。我习惯在启动文件里把未使用的中断都指向一个空函数,防止意外触发导致跑飞。

1.4 中断服务函数(ISR)编写规范

ISR的编写,是嵌入式开发的基本功。写不好,系统就崩。我总结了几条铁律:

  1. 短小精悍——ISR里只做最必要的事,比如置个标志位、读个数据。千万别在里面做延时、打印、复杂计算。
  2. 可重入——如果同一个中断可能嵌套触发,ISR必须是可重入的。局部变量没问题,全局变量要小心。
  3. 保护共享资源——ISR和主程序共享的变量,要用volatile修饰,必要时加临界区保护。
  4. 清除中断标志——进ISR第一件事,清除中断标志位。否则会反复触发,死循环。
  5. 不要调用不可重入函数——像printf、malloc这些,千万别在ISR里用。
// 规范的GPIO中断ISR示例
volatile uint8_t g_button_pressed = 0;  // 共享标志,必须volatile

void EXTI0_IRQHandler(void)
{
    // 1. 清除中断标志(必须)
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR = EXTI_PR_PR0;
    }
    
    // 2. 只做最必要的事
    g_button_pressed = 1;  // 置标志,主循环去处理
    
    // 3. 不要在这里做延时、打印、复杂运算
    // HAL_Delay(10);       // ❌ 错误!
    // printf("button!\n"); // ❌ 错误!
}

我曾经踩过的坑:有一次在ISR里调用了HAL_Delay()做去抖,结果系统直接卡死。为什么?因为HAL_Delay依赖SysTick中断,而SysTick中断优先级比GPIO中断低,GPIO中断一直占着CPU,SysTick进不去——死锁了。所以记住:ISR里别用延时!

1.5 知识体系总览

下面这张图,把GPIO中断的核心知识点串起来了。你可以把它当作本章的“地图”。

GPIO中断知识体系 GPIO中断知识体系 GPIO中断 中断概念 中断优先级 中断向量表 ISR编写规范 暂停→处理→返回 硬件触发机制 抢占优先级 子优先级 地址映射表 VTOR重定向 短小精悍 清除标志位 核心原则:快进快出,不做复杂操作

1.6 本章小结

这一章我们聊了GPIO中断的四个核心知识点:

  • 中断概念——CPU处理紧急事件的机制,核心是“暂停→处理→返回”
  • 中断优先级——决定多个中断同时发生时的处理顺序,要合理分配
  • 中断向量表——中断的“通讯录”,CPU通过它找到对应的ISR
  • ISR编写规范——短小、可重入、保护共享资源、清除标志、不调不可重入函数

这些是基础中的基础。后面的章节,我们会在这个基础上,一步步深入GPIO中断的去抖动处理、响应时间优化,以及各种实战技巧。嗯,路还长,咱们一步一步来。