ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103外部中断实战:NVIC分组、EXTI配置与常见坑

STM32F103外部中断实战:NVIC分组、EXTI配置与常见坑

在STM32F103C8T6上搞中断,看起来是入门阶段的必修课,实际踩坑踩到怀疑人生的也不在少数。前阵子我拿一块C8T6核心板做按键控制LED的实验,从轮询改成中断驱动,结果遇到了中断根本进不去、按下一次却跳了三四次、把SysTick延时写进中断里直接卡死等一系列问题。折腾完一整天回头再看,发现中断这套东西,概念上就那么几个点,但每个点都值得掰开揉碎讲清楚。这篇笔记就针对STM32F103C8T6,把外部中断从NVIC分组、AFIO映射、EXTI配置到ISR命名和调试排查完整过一遍。

这篇内容适合已经能点亮LED、跑过几个GPIO例程,正准备把外设从轮询改成中断方式的读者。如果你只是在图形化配置工具里拖一拖,但对底层为什么这么配不太清楚,这篇文章也能帮你把中断骨架搭起来。F103系列的中断架构在C8T6上并没有因为Flash容量缩小而缩水,学会了,往F103家族其他型号上迁移基本无痛,甚至看F4、G0系列的HAL库中断配置也能一眼看懂在干什么。

1. 中断系统的整体认知:NVIC、EXTI和AFIO是怎么协同的

1.1 为什么中断比轮询更适合事件驱动

先聊一个最基础的问题:既然轮询也能读按键、读串口数据,为什么非要引入中断。举个我自己常打的比方,你在厨房煮汤,轮询就是每隔几秒跑去看一眼火,期间什么都干不了;中断就像是装了个烟雾报警器,只有锅烧干冒烟了才响,你听到响声再冲进厨房处理。

放在MCU上就是这个逻辑。如果主循环一边做显示刷新,一边扫描按键引脚,按键检测的及时性完全取决于主循环跑到扫描语句的间隔。主循环稍微复杂一点,串口收发、传感器读取、算法计算堆在一起,按键按下的瞬间可能被延迟几十毫秒甚至更久。而中断机制让外设事件独立向CPU发出请求,CPU在当前指令结束后自动保存现场,跳到对应的中断服务函数,处理完再回到原来的代码继续执行。Cortex-M3内核的中断响应大约12个时钟周期,在72MHz主频下就是不到170ns的时间,硬件自动完成大部分压栈和现场恢复工作,实时性远超轮询。

中断的另一个优点是没有事件时不占用CPU。按键不按下,CPU完全不需要理会这个外部信号,可以把算力全部留给主流程。对于电池供电的便携设备,用中断唤醒代替轮询扫描,是降低功耗的最直接手段。所以当时我那个小工程,虽然轮询也能跑,还是决定改成中断驱动,权当把基础打牢。

1.2 NVIC优先级分组:中断嵌套的基本法

NVIC全称是嵌套向量中断控制器,是Cortex-M3内核自带的中断管理单元。STM32F103C8T6的NVIC最多支持60个可屏蔽中断通道,覆盖定时器、串口、SPI、I2C、外部中断等外设,另外还有Reset、NMI、HardFault、SysTick这些内核异常。很多资料说C8T6是F103里的入门芯片,但中断通道架构和大容量型号是一致的,只是部分外设资源少一些,所以不用担心学的东西会白学。

NVIC最核心的功能是优先级管理。F103的优先级用的是4位二进制数,这4位被拆成两部分:抢占优先级和子优先级。两者的关系是关键中的关键。抢占优先级决定一个中断能否打断另一个正在执行的中断,数字越小优先级越高;子优先级只在抢占优先级相同的时候用来排序,数字越小越靠前,但子优先级不能产生打断效果。

原厂标准外设库提供了5种分组方式,简单列一下:

分组抢占优先级位数子优先级位数抢占优先级可取值子优先级可取值
Group 004只能00~15
Group 1130~10~7
Group 2220~30~3
Group 3310~70~1
Group 4400~15只能0

实际项目中用Group 2比较多,也就是2位抢占优先级加2位子优先级。这个分组方式能支持的优先级等级足够日常使用,而且配置起来不复杂。我用一个类比来记抢占优先级和子优先级:抢占优先级相当于部门级别,只有更高部门级别的会议主持人才能打断当前会议;子优先级相当于同级别部门里的工号,工号小的先发言,但同级别之间互相不能打断对方正在开的会。

还有一个非常容易翻车的点,NVIC分组只能全局设置一次,而且要放在所有中断初始化之前。因为分组设置改的是内核里的一个重要配置寄存器,一旦分组改变了,之前已经设置好的优先级数值含义就全部变了。同一个数字,在Group 2下代表抢占优先级2,切到Group 3下可能就代表抢占优先级4,整个中断嵌套关系瞬间乱套。所以最稳妥的做法是在main函数第一行就调用NVIC_PriorityGroupConfig,之后再配置任何外设中断。

1.3 EXTI、AFIO和GPIO到底怎么配合的

STM32F103的外部中断叫EXTI,它管理的不是某一个GPIO引脚的输入,而是一组编号为EXTI0到EXTI15的外部中断线。每条EXTI线可以对应多个引脚的同一个编号,比如PA0、PB0、PC0都能映射到EXTI0,但同一时刻EXTI0只能选择其中一个引脚。这个映射关系由AFIO控制器里的寄存器决定,标准外设库的GPIO_EXTILineConfig函数就是用来做这件事的。

这就引出AFIO的角色。AFIO全称是复用功能IO控制器,它负责把外设功能重映射到不同的引脚,也包括把GPIO引脚连接到EXTI线上。很多新手在这里栽跟头,只配置了GPIO和EXTI,忘了使能AFIO时钟,结果外部中断怎么都不触发。因为GPIO_EXTILineConfig要操作AFIO的寄存器,AFIO的时钟都没打开,寄存器写进去也是白写,相当于线压根没接上。

EXTI线到中断服务函数的对应关系也有讲究。EXTI0到EXTI4这5条线各自拥有独立的中断服务函数,EXTI5到EXTI9这5条线共用一个中断服务函数,EXTI10到EXTI15共用一个。设计外部中断时,如果按键和传感器信号比较多,尽量把引脚分散到不共享的线上,这样每个中断源都有独立的入口,排查和维护都更省心。如果确实只能共用,那在同一个中断服务函数里就要逐个检查是哪个中断线触发的,再分别处理。

2. 外部中断EXTI配置全流程拆解(附每步参数选择原因)

2.1 第一步先开启AFIO时钟,别漏

既然AFIO是EXTI映射的关键,配置外部中断的第一步,就要把相关时钟打开。代码看起来很简单:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);

注意这里同时使能了GPIOA、GPIOB和AFIO。如果你用的引脚在GPIOA上,别以为只开GPIOA就够了。GPIO_EXTILineConfig会操作AFIO的EXTICR寄存器,因此AFIO时钟必须使能。这个操作不会报错,但默默失效,排查起来相当隐蔽。我调试的时候吃过一次亏,先查GPIO配置、再查EXTI配置,折腾了半小时才想起来AFIO时钟没开。

还有一个细节,AFIO时钟通常只需使能一次,不需要每个外部中断配置函数都去开一次。可以把所有时钟使能集中放在一个初始化函数里,管理起来更清晰。

2.2 GPIO引脚模式怎么选、按键电路怎么接

外部中断对应的GPIO引脚要设置成输入模式。具体选上拉输入还是下拉输入,取决于按键另一头接的是GND还是VCC。

实际中最常见的按键接法是按下一端接地,这时候GPIO要配置成上拉输入。平时引脚通过内部上拉电阻拉到高电平,按键按下时引脚被拉低,于是产生一个下降沿。对应代码里EXTI触发方式选择EXTI_Trigger_Falling。如果按键接VDD,GPIO配置成下拉输入,平时引脚为低,按下变高,触发方式就选EXTI_Trigger_Rising。

GPIO配置代码:

GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = KEY1_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);

这里有一个小知识点,输入模式下GPIO_Speed其实不影响信号采集,它主要决定输出引脚的翻转速率。很多例程不管什么模式都写上Speed,实际上在输入模式里这个参数没有实际意义。不写也不会出错,但为了和输出配置统一,写上也没问题。

有一点要特别提醒,如果按键悬空且GPIO配置成浮空输入,按下去和释放的过程中电平会因为外界干扰乱跳,EXTI可能会被误触发。所以外部中断的输入引脚必须有明确的上拉或下拉,要么用内部上拉,要么外部电路已经把电平固定住了。

2.3 EXTI结构体与NVIC结构体逐项说明

GPIO配置好之后,做两件事:把GPIO引脚映射到对应的EXTI线,再配置EXTI触发参数。

GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure);

这里最大的坑是GPIO_PinSource0和GPIO_Pin_0的区别。GPIO_PinSource0是引脚编号0,值是0x00,专门给GPIO_EXTILineConfig这种需要"第几号引脚"的函数用的;而GPIO_Pin_0是引脚掩码,值是0x0001,是给GPIO_Init结构体用的。如果把GPIO_Pin_0传给GPIO_EXTILineConfig,函数会把整个映射搞乱,中断线完全对不上。这种问题光看代码很难发现,只能对照寄存器窗口排查,我见过不少同行栽在这一处。

EXTI_Mode有两个选项,Interrupt模式才是进中断服务函数,Event模式只是产生事件脉冲,不会触发ISR,通常配合DMA使用。普通按键、传感器信号都用Interrupt模式。EXTI_Trigger有三种,上升沿、下降沿、双边沿,按照2.2讲的电路接法选就行。

紧接着配置NVIC:

NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);

NVIC_IRQChannel选择中断通道,这个要和引脚编号严格对应。PA0对应EXTI0_IRQn,PA1对应EXTI1_IRQn,PA5这种就得对应EXTI9_5_IRQn。选错通道的话,中断信号进不了配置的通道,ISR根本不会执行。为了方便记忆,我做了一个对应表:

引脚编号中断线NVIC通道ISR函数名
Pin0EXTI0EXTI0_IRQnEXTI0_IRQHandler
Pin1EXTI1EXTI1_IRQnEXTI1_IRQHandler
Pin2EXTI2EXTI2_IRQnEXTI2_IRQHandler
Pin3EXTI3EXTI3_IRQnEXTI3_IRQHandler
Pin4EXTI4EXTI4_IRQnEXTI4_IRQHandler
Pin5~Pin9EXTI5~EXTI9EXTI9_5_IRQnEXTI9_5_IRQHandler
Pin10~Pin15EXTI10~EXTI15EXTI15_10_IRQnEXTI15_10_IRQHandler

2.4 中断服务函数的名字不能自己起

外部中断使能之后,中断来了,CPU会从向量表里取对应中断服务函数的地址。这个函数名必须在启动文件里已经定义过,不能自己随手起一个。STM32F103C8T6的启动文件通常叫startup_stm32f10x_md.s,里面已经把所有的中断入口都声明好了,像EXTI0_IRQHandler、USART1_IRQHandler这些名字都写死在向量表里。

如果你写了你自己命名的功能函数,比如void MyKey_IRQHandler(void),哪怕里面逻辑写得再对,编译器也不会把它放到向量表的位置。中断一旦发生,CPU跳转到向量表指向的默认处理函数,绝大多数情况就是进HardFault_Handler,表现为程序卡死或复位循环。所以判断"中断不进"的第一个动作,永远是对照启动文件检查ISR名字是否一字不差。

标准外设库工程的IT文件里通常预置了这些空函数,一般直接在stm32f10x_it.c里找对应名字填代码。如果你在别的源文件里定义ISR,可以把启动文件对应的weak函数覆盖掉,但名字依然不能变。新手期我拼错过一次EXTI0_IRQHandler,多打了一个下划线,整整一个下午都在查为什么中断不触发,后来还是用调试器看PC指针跳到哪里才发现的。

3. 标准外设库完整示例:两个按键中断控制LED

3.1 硬件连接和工程初始化顺序

先假设一套最简单的硬件连接:KEY1接PA0,另一端接GND;KEY2接PA1,另一端接GND;LED1接PB1,阳极通过限流电阻接3.3V,IO低电平点亮。这套接法在市面上绝大多数核心板和面包板搭建里都很常见。

初始化顺序上用我自己的固定套路:

  1. main函数第一行设置NVIC优先级分组
  2. 配置系统时钟,这个一般由启动代码和system_stm32f10x.c完成
  3. 使能GPIO和AFIO时钟
  4. 配置LED引脚为推挽输出
  5. 配置按键引脚为上拉输入
  6. 映射EXTI线并配置EXTI触发方式
  7. 配置NVIC中断通道
  8. 进入主循环

把NVIC_PriorityGroupConfig放在第一行是铁律,理由前面讲过,分组改变会导致所有已配置的优先级数值被重新解释。我见过一些例程把分组设置放在某个外设初始化之后,虽然实验能跑通,但一旦工程里外设多了就不稳定,还是统一放在最前面最稳妥。

3.2 完整代码与逐段讲解

整个示例可以用一个源文件写完,为了可读性我拆成几个函数。

LED初始化部分:

#include "stm32f10x.h" #define KEY1_PORT GPIOA #define KEY1_PIN GPIO_Pin_0 #define KEY2_PORT GPIOA #define KEY2_PIN GPIO_Pin_1 #define LED1_PORT GPIOB #define LED1_PIN GPIO_Pin_1 volatile uint8_t key1_pressed = 0; volatile uint8_t key2_pressed = 0; static void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = LED1_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LED1_PORT, &GPIO_InitStructure); GPIO_SetBits(LED1_PORT, LED1_PIN); // 初始熄灭 }

LED配置成推挽输出,Speed选50MHz是为了让IO翻转速率足够快,点灯其实10MHz都够,但统一写50MHz不影响。

按键和EXTI配置:

static void KEY_EXTI_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = KEY1_PIN | KEY2_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource1); EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); EXTI_InitStructure.EXTI_Line = EXTI_Line1; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }

这段代码用了同一个EXTI_InitTypeDef结构体,第一次把EXTI_Line0配置好之后,第二次只改EXTI_Line字段再调用一次EXTI_Init,因为其他字段保持上一个值不变。这是标准外设库的常见用法,省去重复写结构体的冗余。

两个外部中断的抢占优先级都设为2,子优先级设为0。它们之间没有打断需求,按键同时按下时谁先执行都无所谓。如果后面加了串口中断,希望串口能随时打断按键处理,就把串口的抢占优先级设成1,比按键高,这样串口数据的实时性就有保障。

主函数和软延时消抖:

static void Delay_Loop(volatile uint32_t n) { while (n) { n--; } } int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); LED_Init(); KEY_EXTI_Config(); while (1) { if (key1_pressed) { key1_pressed = 0; Delay_Loop(2000000); if (GPIO_ReadInputDataBit(KEY1_PORT, KEY1_PIN) == Bit_RESET) { GPIO_WriteBit(LED1_PORT, LED1_PIN, Bit_SET); // 低电平点亮 } } if (key2_pressed) { key2_pressed = 0; Delay_Loop(2000000); if (GPIO_ReadInputDataBit(KEY2_PORT, KEY2_PIN) == Bit_RESET) { GPIO_WriteBit(LED1_PORT, LED1_PIN, Bit_RESET); // 熄灭 } } } }

Delay_Loop里的参数用volatile修饰,是为了防止编译器在开优化时把空循环整个优化掉。真实项目建议换成SysTick或定时器实现的精确延时,这里纯粹是按键消抖,几毫秒到几十毫秒的误差无所谓。消抖逻辑就是发现标志位置位后,不立即处理,先延时一段时间,绕过机械触点回弹期,再读一次引脚电平确认按键确实保持按下状态。比如KEY1执行延时后读到低电平,说明不是抖动误触,这时才真正执行点亮LED的操作。

3.3 中断里的标志位、清挂起和主循环分工

中断服务函数的写法是整个示范里最体现功底的地方:

void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { key1_pressed = 1; EXTI_ClearITPendingBit(EXTI_Line0); } } void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) != RESET) { key2_pressed = 1; EXTI_ClearITPendingBit(EXTI_Line1); } }

注意两个细节。一是先判断EXTI_GetITStatus,确认确实是这条线触发的,再执行置标志和清挂起。二是清挂起位是必须的,EXTI挂起位不手动清除的话,中断会在返回后再次触发,最典型的现象就是程序卡在中断里出不来,看起来像死循环。EXTI_ClearITPendingBit的底层实现是向相应寄存器写1清零,标准外设库封装好了直接用。

标志位必须在中断和主循环共享,所以加了volatile。这是C语言里特别容易忽略的点。编译器在开启优化后,会把一些频繁读取的变量暂时缓存在寄存器里,如果主循环每次都从寄存器读副本,不重新从内存里加载,那中断服务函数里修改的新值就永远看不见。加了volatile等于告诉编译器,这个变量每次都要从内存里重新读。

中断里只做了两件最短的操作:置标志位和清挂起位。真正耗时的事情,比如延时消抖、翻转LED、打印日志,全部放主循环。这个分工原则要记住。ISR执行时间越长,其他中断被阻塞的时间就越长,串口缓冲区溢出、定时器丢失节拍这些问题都跟ISR过长有直接关系。ISR里也尽量不要调用库函数,一些库函数内部有自己的状态机,在中断上下文里调用容易产生不可预料的副作用。

4. 中断调试中最常踩的坑与排查方法实录

4.1 中断进不去、重复进、卡死的排查思路

先讲一个我调试时的通用排查顺序,按这个顺序走一遍,绝大多数外部中断问题都能定位。

第一查ISR名字。对照启动文件的向量表,看自己写的中断服务函数和向量表里的名字是不是完全一致。我在2.4说过,名字不对的中断处理函数就像没贴对标签的信,永远送不到指定邮箱。第二查GPIO_EXTILineConfig的引脚映射参数,确认用的是GPIO_PinSource0而不是GPIO_Pin_0。第三查EXTI_Line和NVIC_IRQChannel是否和引脚编号对应,PA0必须对应EXTI_Line0和EXTI0_IRQn。第四查NVIC_Init结构体里NVIC_IRQChannelCmd是不是ENABLE,不少代码抄来抄去忘了最后把通道使能打开。第五查清挂起位,中断服务函数里如果不清理挂起位,这个中断就会像闹钟按了暂停但没取消一样,一到点就响。

有了这个顺序,基本可以覆盖"中断不进"和"中断死循环"两大类问题。还有一个高级检查技巧,用调试器看EXTI的PR寄存器。按键按下的瞬间,如果PR寄存器对应位置1,说明信号确实到达了EXTI;如果PR始终为0,问题就出在GPIO配置、AFIO映射或硬件连接。如果PR置1但ISR没进,问题基本在NVIC配置或ISR名称。这个定位方法比盲猜快一个数量级。

4.2 按键抖动、SysTick抢占和volatile那些事

按键抖动是外部中断场景下最普遍的伴生问题,机械开关的簧片从接触到稳定,会在几毫秒内反复通断,产生一串杂乱的边沿。如果不做处理,一次按键可能触发多次中断,LED会出现按一下闪好几下的现象。我在示例里的做法是中断里只置标志,主循环里用延时加电平再确认。这个方案简单可靠,适合大多数按键场景。如果系统对功耗要求高,可以改成进入中断后启动定时器,定时器溢出后读一次引脚电平确认,但这套逻辑复杂不少,对于入门学习不是必须。

SysTick的坑是我这次调试里最深刻的教训之一。标准外设库或一些HAL工程里的Delay_ms函数,很多是基于SysTick中断实现的。如果外部中断的抢占优先级比SysTick高,SysTick中断会被外部中断一直阻塞,Delay_ms内部的时间计数就无法更新,导致延时无限拉长或者直接卡死。我这个示例特意用了空循环延时,就是为了避免把SysTick依赖写进演示代码里。你以后在自己的项目里,如果打算在ISR中调用延时函数,先想清楚这个延时的实现机制,以及当前中断和SysTick的优先级关系。最简单的原则:ISR里别放延时,别放串口打印,这两个都是新手的重灾区。

volatile前面提了不少,这里再强调一下背后的机制。Keil和IAR在-O2及以上优化级别时,编译器认为某个变量在顺序执行中不会被外部修改,就可能只读一次放进寄存器,后续逻辑全部用寄存器旧值。中断服务函数对共享变量的修改,发生在和主循环完全无关的执行上下文里,编译器无法静态分析到。结果就是主循环里即使写了if (flag),条件也永远不成立。这个坑在新手阶段很难凭直觉发现,但只要所有跨中断共享的标志位都加volatile,就永远不会踩中。

4.3 中断常见问题速查表

为了方便参考,我把这次调试过程中遇到的、以及早年项目中见过的问题整理成一张速查表,基本覆盖STM32F103C8T6外部中断的日常问题。

现象可能原因排查与解决
中断完全不响应AFIO时钟没有使能在RCC_APB2PeriphClockCmd中加上RCC_APB2Periph_AFIO
中断完全不响应ISR函数名和向量表不一致对照startup_stm32f10x_md.s逐字符检查
中断完全不响应NVIC通道没有使能检查NVIC_IRQChannelCmd是否为ENABLE
中断完全不响应引脚映射参数用错GPIO_PinSource0不要写成GPIO_Pin_0
中断反复进入、程序卡死未清EXTI挂起位ISR末尾调用EXTI_ClearITPendingBit
按键一次触发多次逻辑机械抖动主循环加延时后重新读引脚电平确认
中断里调用延时卡死SysTick被高优先级中断阻塞ISR内不使用基于SysTick的延时,改用主循环处理
主循环读不到中断标志共享变量缺少volatile给跨中断和主循环的共享变量加volatile
引脚悬空导致误触发输入浮空、没有确定电平配置内部上拉/下拉或外部电路固定电平
按键一直按住时中断不重复边沿触发的正常行为需要重复检测则改用电平模式或用定时器轮询

这张表不需要死记,但在遇到"看起来很正常就是不工作"的情况时,按表逐项排查,基本能节省一个下午的调试时间。

个人习惯上,我一般把中断服务函数集中放在单独的源文件里,比如stm32f10x_it.c或者自己建的irq.c,逻辑清晰,也方便别人接手。调试外部中断期间,我会在ISR里临时翻转一个空置引脚,用逻辑分析仪或者哪怕示波器看一下响应时间,比打断点可靠得多。毕竟在ISR里打断点,相当于人为把中断停住,看到的"卡死"很可能是调试器造成的假象。这些经验虽然不是多高深的知识,但在实际项目中确实是能救命的东西。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进