STM32寄存器学习分水岭:EXTI与NVIC中断配置详解 1. 为什么第三章才是寄存器学习的分水岭很多人学STM32寄存器第一章看GPIO觉得“就这”第二章看时钟树开始皱眉到了第三章——中断和NVIC——直接卡住不动了。我见过太多人卡在这一章前两章还能靠“抄配置”混过去第三章开始光靠抄已经不好使了因为你根本不知道那个NVIC_ISER寄存器里写的那个bit到底对应哪个中断线。第三章的核心就一件事让CPU知道“出事了”并且知道“该去找谁”。听起来简单但STM32的中断体系有三层结构——外设级、NVIC级、CPU级——每一层都有自己的寄存器要配。很多人只配了外设级比如EXTI的IMR忘了NVIC的使能结果中断死活不进或者NVIC配了但优先级分组没设对两个中断互相打断逻辑全乱。这一章我会把EXTI和NVIC这两块硬骨头拆开从寄存器的每一个bit讲到实际能跑的代码。不扯HAL库不调NVIC_Init()就是直接对着参考手册写寄存器。你把这章吃透了后面DMA、定时器中断、串口接收中断全都是同一套逻辑换个外设而已。提示第三章的内容量比前两章加起来都大建议分两次看。第一次先把EXTI的五个寄存器搞清楚第二次再啃NVIC的优先级分组。2. EXTI的五个寄存器从引脚电平到中断挂起2.1 EXTI_IMR和EXTI_EMR谁有资格触发中断EXTI外部中断/事件控制器的寄存器不多但每一个都容易搞混。先看最基础的两个EXTI_IMRInterrupt Mask Register和EXTI_EMREvent Mask Register。这两个寄存器的结构一模一样都是32位每一位对应一条EXTI线。STM32F103系列有19条EXTI线0~18其中0~15对应GPIO的16个引脚16~18对应PVD输出、RTC闹钟、USB唤醒这些内部事件。IMR管的是中断EMR管的是事件。区别在哪中断会触发NVIC让CPU跳转到中断服务函数事件不会触发NVIC只是产生一个脉冲信号可以触发其他外设比如ADC启动转换、TIM启动计数。你可以把中断理解成“打电话给CPU”事件理解成“按了一下门铃但没人接电话只是通知其他设备”。配置的时候如果你要的是中断就写EXTI-IMR | (1 line)如果要的是事件就写EXTI-EMR | (1 line)。两个都写也行但一般不会这么干因为中断和事件同时使能没有额外好处反而增加功耗。这里有个新手常踩的坑IMR的bit位置和GPIO的引脚号是对应的但EXTI线是共享的。比如PA0和PB0都连到EXTI0你没法同时用PA0和PB0做中断。选引脚的时候要用AFIO的AFIO_EXTICR寄存器来指定哪一组GPIO映射到EXTI线。这个寄存器在第三章容易被忽略但它是EXTI能工作的前提。2.2 EXTI_RTSR和EXTI_FTSR上升沿还是下降沿EXTI_RTSRRising Trigger Selection Register和EXTI_FTSRFalling Trigger Selection Register决定触发边沿。写1使能对应边沿写0关闭。按键按下检测通常用下降沿FTSR因为按键按下是电平从高到低。按键释放检测用上升沿RTSR。编码器信号两个边沿都要RTSR和FTSR同时置位。有个细节要注意边沿检测是“边沿”不是“电平”。如果你按住按键不放下降沿只触发一次不会一直触发。想要连续触发得用定时器轮询或者电平触发模式但EXTI不支持电平触发只有边沿。实际配置代码大概长这样// 假设用PA0做下降沿中断 AFIO-EXTICR[0] ~0x0F; // 清除低4位对应EXTI0 AFIO-EXTICR[0] | 0x00; // 选择PA00000 EXTI-IMR | (1 0); // 使能EXTI0中断 EXTI-FTSR | (1 0); // 下降沿触发 EXTI-RTSR ~(1 0); // 关闭上升沿注意AFIO_EXTICR的索引计算EXTI0~3在EXTICR[0]EXTI4~7在EXTICR[1]以此类推。每个EXTI线占4个bit所以EXTI0对应EXTICR[0]的bit0~3EXTI1对应bit4~7。这个计算如果搞错引脚映射就全乱了。2.3 EXTI_PR挂起寄存器的“写1清除”机制EXTI_PRPending Register是EXTI里最特殊的一个寄存器。当触发条件满足时硬件自动把对应的bit置1表示“这条线有中断挂起了”。清除这个bit的方式是写1不是写0。为什么这么设计因为写0清除会有竞态问题你读寄存器、改bit、写回去的过程中硬件可能又置1了结果你写回去的值把新来的中断给清掉了。写1清除是原子操作硬件只清除你写1的那些bit其他bit不受影响。在中断服务函数里必须手动清除EXTI_PR对应的bit否则中断会反复触发CPU一直卡在ISR里出不来。标准写法void EXTI0_IRQHandler(void) { if (EXTI-PR (1 0)) { EXTI-PR (1 0); // 写1清除 // 你的处理逻辑 } }注意这里先判断再清除。虽然直接写EXTI-PR (1 0)也能清除但加上判断更稳妥避免误清其他线的挂起标志。注意EXTI_PR的清除操作必须在ISR里完成不能在主循环里清。因为中断触发后NVIC的挂起位会一直保持直到你清了EXTI_PR并且退出ISRNVIC才会释放。如果你在ISR里没清EXTI_PR退出ISR后NVIC会立刻再次进入同一个ISR形成死循环。2.4 EXTI_SWIER软件触发中断的用途EXTI_SWIERSoftware Interrupt Event Register允许你用软件模拟一次中断触发。写1到对应bit硬件会像检测到边沿一样把EXTI_PR置1然后走正常的中断流程。这个寄存器有什么用两个场景一是调试你想测试ISR逻辑但手头没有信号源直接写SWIER就能触发二是多外设联动比如某个定时器事件需要触发一个软件中断来同步任务。用完之后SWIER的bit会自动清除不需要手动清。但EXTI_PR还是得在ISR里清这个不能忘。2.5 五个寄存器的协作流程把上面五个寄存器串起来一次完整的外部中断流程是这样的配置AFIO_EXTICR把GPIO引脚映射到EXTI线。配置EXTI_RTSR/FTSR选择触发边沿。配置EXTI_IMR使能中断不是事件。配置NVIC使能对应的IRQ通道设置优先级。信号到来边沿检测电路触发EXTI_PR对应bit置1。NVIC收到中断请求如果优先级允许CPU跳转到ISR。ISR里判断EXTI_PR执行业务逻辑写1清除EXTI_PR。退出ISRNVIC释放等待下一次触发。这八步里第4步是NVIC的事下一节详细讲。第7步的清除操作是最容易忘的忘了就是死循环。3. NVIC优先级分组为什么你的中断嵌套不按预期工作3.1 NVIC_ISER和NVIC_ICER使能与失能NVIC的寄存器在系统地址空间里不是外设地址。STM32F103的NVIC支持60个可屏蔽中断通道具体数量看型号每个通道对应一个IRQ编号。NVIC_ISERInterrupt Set-Enable Register有3个寄存器ISER[0]~ISER[2]每个32位总共96个bit覆盖所有中断通道。写1使能对应通道写0无效不是失能。失能要用NVIC_ICER同样是写1失能。为什么使能和失能要分开两个寄存器因为这样设计可以避免读-改-写操作。你直接写ISER的某个bit为1就使能不需要先读出来再改。ICER同理。这种设计在中断上下文里特别有用因为中断里不能有太复杂的操作。使能EXTI0中断的代码NVIC-ISER[0] | (1 6); // EXTI0_IRQn 6这里1 6是因为EXTI0的IRQ编号是6。不同中断的编号在参考手册的中断向量表里查别搞错。3.2 NVIC_IPR优先级寄存器的8位结构NVIC_IPRInterrupt Priority Register有15个寄存器IPR[0]~IPR[14]每个寄存器管4个中断通道每个通道占8个bit。STM32F103只用了高4位低4位保留。8个bit里高4位又分成“抢占优先级”和“子优先级”两部分。具体怎么分由SCB-AIRCR的PRIGROUP字段决定。这就是优先级分组。分组方式有5种分组抢占优先级位数子优先级位数抢占级数子级数004116113282224433182440161分组0表示没有抢占优先级所有中断都是子优先级不能互相打断。分组4表示没有子优先级只有抢占优先级高抢占级可以打断低抢占级。抢占优先级决定能不能嵌套高抢占级的中断可以打断低抢占级的ISR。子优先级决定同抢占级下的执行顺序两个中断同时挂起时子优先级高的先执行但已经在执行的ISR不会被同抢占级的中断打断。设置分组的代码SCB-AIRCR (0x5FA 16) | (2 8); // 分组20x5FA是写入AIRCR的密钥不写这个值写入无效。分组2表示2位抢占、2位子优先级抢占级有4档0~3子优先级有4档0~3。3.3 优先级数值越小优先级越高这是个反直觉的点优先级数值越小优先级越高。抢占优先级0比抢占优先级1高子优先级0比子优先级1高。为什么这么设计因为复位后所有优先级都是0如果0是最低优先级那复位后所有中断都没法互相打断系统响应会很差。0作为最高优先级复位后所有中断平等但至少能正常工作。配置EXTI0优先级为抢占1、子1分组2下NVIC-IPR[6] (1 6) | (1 4); // 抢占1子1IPR[6]对应IRQ6EXTI0。高4位里bit7~6是抢占优先级bit5~4是子优先级。所以1 6设置抢占级为11 4设置子级为1。3.4 中断嵌套的实际案例假设你有两个中断EXTI0抢占1子0和TIM2抢占2子0。分组2下抢占级1比2高。如果TIM2的ISR正在执行EXTI0触发EXTI0会打断TIM2先执行EXTI0的ISR执行完再回到TIM2。如果EXTI0正在执行TIM2触发TIM2不会打断EXTI0得等EXTI0执行完才轮到TIM2。如果两个中断抢占级相同都是1子级不同EXTI0子0TIM2子1那么同时挂起时EXTI0先执行子级0比1高。如果TIM2正在执行EXTI0触发EXTI0不会打断TIM2因为抢占级相同。这个规则在实际项目里很关键。比如串口接收中断和定时器中断如果串口接收的实时性要求高就把串口的抢占级设得比定时器高这样串口数据来了能立刻处理不会因为定时器ISR执行太久而丢数据。提示中断嵌套层数不要超过3层。层数太多会导致栈溢出而且逻辑很难调试。如果发现需要4层以上的嵌套说明你的中断设计有问题应该把部分逻辑移到主循环或者用DMA。4. 从寄存器到可运行代码一个完整的按键中断实例4.1 硬件连接与寄存器映射假设用PA0接按键按下时接地下降沿触发。PA0对应EXTI0IRQ编号6。需要配置的寄存器清单RCC-APB2ENR使能GPIOA和AFIO时钟GPIOA-CRLPA0配置为输入浮空或上拉AFIO-EXTICR[0]选择PA0映射到EXTI0EXTI-IMR使能EXTI0中断EXTI-FTSR下降沿触发NVIC-ISER[0]使能IRQ6NVIC-IPR[6]设置优先级SCB-AIRCR设置优先级分组时钟使能是最容易忘的一步。RCC-APB2ENR的bit2是GPIOAbit0是AFIO。不使能AFIO时钟AFIO_EXTICR写不进去引脚映射就失败。RCC-APB2ENR | (1 2) | (1 0); // GPIOA AFIOPA0配置为输入CRL的bit0~3控制PA0。浮空输入是01000x4上拉输入是10000x8。按键接地的话用上拉输入更稳避免悬空时误触发。GPIOA-CRL ~0x0F; GPIOA-CRL | 0x08; // 上拉输入4.2 中断服务函数的写法与常见错误ISR的名字必须和启动文件里的向量表一致。STM32F103的EXTI0中断服务函数名是EXTI0_IRQHandler写错了就不会被调用。void EXTI0_IRQHandler(void) { if (EXTI-PR (1 0)) { EXTI-PR (1 0); // 清除挂起 // 业务逻辑比如翻转LED GPIOB-ODR ^ (1 0); } }常见错误一忘了清EXTI_PR。结果就是ISR反复执行主循环根本跑不到。常见错误二在ISR里做耗时操作。比如delay_ms(100)这会阻塞其他中断系统响应变慢。ISR里只做标志位设置耗时逻辑放主循环。常见错误三优先级分组没设。复位后AIRCR的PRIGROUP是0所有优先级都是子优先级没有抢占级。如果你按分组2的格式写了IPR实际生效的却是分组0的解析方式优先级完全乱套。4.3 用SWIER做软件触发的调试技巧调试中断逻辑时手头不一定有信号源。用EXTI_SWIER可以直接触发一次中断验证ISR逻辑是否正确。EXTI-SWIER | (1 0); // 软件触发EXTI0中断写完之后硬件会把EXTI_PR的bit0置1然后走正常的中断流程。你可以在ISR里打断点看是否进入。这个技巧在调试NVIC优先级时特别有用因为你可以精确控制触发时机观察不同优先级中断的嵌套行为。4.4 实测中的意外情况与排查思路情况一中断进了一次就不再进。大概率是EXTI_PR没清或者清了但清错了bit。检查ISR里的清除语句确认写的是EXTI-PR (1 0)而不是EXTI-PR ~(1 0)。情况二中断完全进不去。按顺序检查GPIO时钟使能了吗AFIO时钟使能了吗EXTICR写对了吗IMR使能了吗NVIC的ISER使能了吗优先级分组设了吗这六步任何一步漏了中断都不会进。情况三中断进了但嵌套不对。检查AIRCR的分组设置和IPR的写入值是否匹配。分组2下IPR的高4位是2位抢占2位子优先级如果你按分组4的格式写4位抢占实际生效的只有高2位低2位被当成子优先级结果和预期不符。情况四按键抖动导致多次触发。硬件上加RC滤波或者软件上在ISR里做消抖比如记录时间戳50ms内的触发忽略。但消抖逻辑不要放在ISR里做延时用计数器或者标志位。5. 第三章的寄存器配置清单与速查表5.1 EXTI寄存器速查寄存器地址偏移功能关键操作EXTI_IMR0x00中断使能写1使能对应线EXTI_EMR0x04事件使能写1使能对应线EXTI_RTSR0x08上升沿触发写1使能EXTI_FTSR0x0C下降沿触发写1使能EXTI_SWIER0x10软件触发写1触发自动清除EXTI_PR0x14挂起标志写1清除5.2 NVIC寄存器速查寄存器地址功能关键操作NVIC_ISER[0~2]0xE000E100中断使能写1使能NVIC_ICER[0~2]0xE000E180中断失能写1失能NVIC_ISPR[0~2]0xE000E200挂起设置写1挂起NVIC_ICPR[0~2]0xE000E280挂起清除写1清除NVIC_IPR[0~14]0xE000E400优先级每通道8位高4位有效SCB_AIRCR0xE000ED0C优先级分组密钥0x5FA分组值5.3 优先级分组选择建议应用场景推荐分组理由简单项目中断少分组4全部抢占级嵌套逻辑简单有实时性要求的中断分组2兼顾嵌套和同级顺序中断多且需要精细调度分组38级抢占2级子优先级不需要嵌套分组0全部子优先级不会互相打断分组2是我个人最常用的4级抢占够用4级子优先级也能处理同级竞争。分组4虽然简单但所有中断都能互相打断容易出竞态问题。5.4 中断配置的检查清单写完中断代码后按这个清单过一遍外设时钟使能了吗GPIO、AFIO、外设本身GPIO模式配置对吗输入/输出/复用AFIO映射写对了吗EXTICR的索引和bit位置EXTI触发边沿设了吗RTSR/FTSREXTI中断使能了吗IMR不是EMRNVIC通道使能了吗ISERIRQ编号查表优先级分组设了吗AIRCR密钥别忘了优先级值写对了吗IPR高4位有效ISR名字和向量表一致吗ISR里清EXTI_PR了吗写1清除这十条里第3、7、10条是最容易出错的。AFIO映射错了中断触发源就不对分组没设优先级解析全乱PR没清死循环。6. 从第三章延伸出去中断体系的全貌第三章讲的是EXTI和NVIC但这只是STM32中断体系的一部分。完整的体系还包括外设级中断每个外设TIM、USART、ADC、SPI等都有自己的中断使能位和状态标志。比如USART的CR1寄存器里有RXNEIE接收非空中断使能SR寄存器里有RXNE接收非空标志。配置逻辑和EXTI类似使能外设中断、使能NVIC通道、写ISR、清标志。SysTick中断Cortex-M3内核自带的系统定时器常用于RTOS的时基或者简单的延时。它的优先级配置和NVIC一样但寄存器在SCB里SysTick的CTRL、LOAD、VAL。HardFault和NMI不可屏蔽中断和硬件错误中断优先级固定不能配置。HardFault是调试时的最后一道防线出现HardFault说明有严重的硬件或软件错误比如访问非法地址、除零、栈溢出。把第三章的EXTI和NVIC搞明白之后其他外设的中断配置就是换个寄存器地址的事。逻辑完全一样使能外设中断、映射到NVIC、设优先级、写ISR、清标志。我个人在实际项目里的体会是中断配置的难点不在寄存器本身而在优先级规划。项目初期就要把所有中断列出来按实时性要求排个序统一分配抢占级和子级。不要等到出问题了再调那时候中断逻辑已经交织在一起改一个影响一片。最后分享一个小技巧调试中断时可以在ISR入口翻转一个GPIO用示波器或者逻辑分析仪看波形。这样能直观地看到中断的触发频率、执行时间、嵌套关系。比在IDE里打断点高效得多而且不会干扰中断的实时性。