4. GPIO寄存器与驱动模型

中断,是嵌入式系统里最核心的异步事件处理机制。说白了,就是 CPU 正在干着活,突然被一个外部或内部事件打断,转去处理更紧急的事,处理完再回来接着干。GPIO 中断,就是让一个普通的 IO 引脚,具备“主动报告”的能力。

我个人习惯把中断系统比作一个公司的前台。CPU 是老板,外设是来访的客人。没有中断的时候,老板得不停抬头看门口有没有人(轮询)。有了中断,客人按门铃(触发中断),前台(中断控制器)筛选一下,再告诉老板谁来了、优先级多高。老板放下手头的事,去接待。嗯,这个比喻虽然简单,但核心逻辑全在里面了。

4.1 中断控制器基础:谁在管理这些“门铃”?

在 Cortex-M 内核的 MCU 里,中断控制器叫 NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)。它干三件事:

  • 使能/禁止中断:决定哪个外设能“按门铃”。
  • 管理优先级:多个门铃同时响,先处理谁。
  • 向量表寻址:门铃响了,CPU 该跑去哪个门口(中断服务函数地址)。

我在项目中遇到过一个问题:GPIO 中断没反应,查了半天发现是 NVIC 里对应的中断通道没使能。外设的中断标志位都置上了,但 CPU 根本收不到信号。所以记住,外设中断使能 + NVIC 中断使能,缺一不可。

核心流程: GPIO 引脚检测到边沿 → 挂起中断标志位 → 中断控制器裁决优先级 → CPU 响应 → 执行中断服务函数 → 清除标志位。

4.2 GPIO 中断触发方式:上升沿、下降沿还是电平?

GPIO 中断的触发方式,通常有四种。选错了,要么漏事件,要么频繁误触发。我曾经在一个按键项目中,用了上升沿触发,结果按键按下时因为机械抖动,一次按压产生了七八次中断。后来改成下降沿触发加硬件消抖,才稳定下来。

触发方式 触发条件 典型应用 注意事项
上升沿触发 信号从低到高跳变 按键释放检测 易受噪声干扰,需消抖
下降沿触发 信号从高到低跳变 按键按下检测 同上,建议配合硬件滤波
双边沿触发 任意电平跳变 编码器脉冲计数 中断频率可能翻倍,注意负载
电平触发 引脚保持高/低电平 低功耗唤醒 电平不消失则持续触发,需在 ISR 中处理

我的建议: 对于机械开关,优先用下降沿触发 + 硬件 RC 滤波。对于高速信号(如编码器),用双边沿触发,但 ISR 里要尽量精简,避免丢脉冲。

4.3 中断服务函数编写:ISR 里的“三要三不要”

中断服务函数(ISR)是中断系统的灵魂。写得好,系统稳如泰山;写得烂,系统卡成幻灯片。我见过有人把 printf 和延时函数直接塞进 ISR,结果系统直接死机。你想想看,ISR 里跑打印,串口还没发完,下一个中断又来了,不崩才怪。

这里我总结了一个“三要三不要”:

  • 要快:ISR 执行时间越短越好。只做标志位设置、数据拷贝、简单计算。
  • 要清标志:退出前必须清除中断标志位,否则会反复进入 ISR。
  • 要保护共享数据:ISR 和主循环共享的变量,记得加 volatile 关键字。
  • 不要调用阻塞函数:printf、delay、malloc 这些,千万别在 ISR 里用。
  • 不要做复杂运算:浮点运算、大循环,统统放到主循环里处理。
  • 不要嵌套太深:除非你很清楚优先级和栈空间,否则尽量保持 ISR 简单。

下面是一个标准的 GPIO 中断服务函数模板,以 STM32 为例:

void EXTI0_IRQHandler(void)
{
    // 1. 检查中断标志位
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 2. 做最必要的事:设置一个标志位
        g_button_pressed = 1;

        // 3. 清除中断标志位(必须!)
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

我曾经踩过的坑: 在 ISR 里直接调用了 HAL_Delay(),结果系统卡死在 SysTick 中断里。因为 HAL_Delay 依赖 SysTick 中断,而 SysTick 中断优先级可能比当前 GPIO 中断低,导致死锁。所以,ISR 里绝对不要用任何依赖其他中断的函数。

4.4 中断优先级与嵌套:谁先谁后,谁打断谁?

中断优先级,决定了多个中断同时发生时,CPU 先响应谁。而中断嵌套,则是一个高优先级中断可以打断正在执行的低优先级中断。

Cortex-M 内核支持抢占优先级和子优先级(也叫响应优先级)。抢占优先级决定能否嵌套,子优先级决定同抢占优先级下的响应顺序。说白了,抢占优先级是“级别”,子优先级是“同级别里的座次”。

优先级分组 抢占优先级位数 子优先级位数 典型场景
NVIC_PriorityGroup_0 0 位 4 位 所有中断同级,无嵌套
NVIC_PriorityGroup_2 2 位 2 位 4 级抢占,每级 4 个子优先级
NVIC_PriorityGroup_4 4 位 0 位 16 级抢占,无子优先级

我个人习惯用 NVIC_PriorityGroup_4,也就是 16 级抢占优先级。这样逻辑最清晰,每个中断的优先级就是一个数字,数字越小优先级越高。0 最高,15 最低。

配置优先级时,要注意一个原则:紧急且短暂的中断,给高优先级;不紧急或耗时长的中断,给低优先级。比如,定时器中断通常给高优先级,因为要保证时间精度;串口接收中断可以给中等优先级;按键中断给低优先级就行,因为人的操作速度远慢于 CPU。

优先级配置示例(STM32 HAL 库):

HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);  // 抢占优先级 1,子优先级 0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);          // 使能中断

4.5 知识体系总览:一张图看懂 GPIO 中断

下面这张 SVG 图,把 GPIO 中断从触发到响应的完整链路画了出来。你可以把它当作本章的思维导图。

GPIO 中断完整链路 GPIO 引脚 边沿/电平检测 挂起中断标志位 NVIC 优先级裁决 CPU 响应中断 执行 ISR + 清标志 触发方式选择 • 上升沿 / 下降沿 • 双边沿 / 电平触发 • 硬件消抖 vs 软件消抖 优先级配置 • 抢占优先级(嵌套) • 子优先级(同级别顺序) • 数字越小,优先级越高 ISR 编写要点 • 快进快出,只设标志 • 必须清除中断标志 • 禁止阻塞/延时函数

从这张图可以清楚看到,GPIO 中断不是“引脚一变就进 ISR”那么简单。中间经过了边沿检测、标志位挂起、NVIC 裁决三道关卡。每一关都可能出问题,这也是为什么我强调调试时要逐级排查。

好了,GPIO 中断编程的核心内容就这些。从控制器基础,到触发方式选择,再到 ISR 编写规范,最后是优先级与嵌套的配置,每一步都有讲究。你写代码的时候,多想想那个“前台接待”的比喻,思路会清晰很多。