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_IMR(Interrupt Mask Register)和EXTI_EMR(Event 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_RTSR(Rising Trigger Selection Register)和EXTI_FTSR(Falling Trigger Selection Register)决定触发边沿。写1使能对应边沿,写0关闭。
- 按键按下检测:通常用下降沿(
FTSR),因为按键按下是电平从高到低。 - 按键释放检测:用上升沿(
RTSR)。 - 编码器信号:两个边沿都要,
RTSR和FTSR同时置位。
有个细节要注意:边沿检测是“边沿”不是“电平”。如果你按住按键不放,下降沿只触发一次,不会一直触发。想要连续触发得用定时器轮询或者电平触发模式(但EXTI不支持电平触发,只有边沿)。
实际配置代码大概长这样:
// 假设用PA0做下降沿中断 AFIO->EXTICR[0] &= ~0x0F; // 清除低4位,对应EXTI0 AFIO->EXTICR[0] |= 0x00; // 选择PA0(0000) 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~3,EXTI1对应bit4~7。这个计算如果搞错,引脚映射就全乱了。
2.3 EXTI_PR:挂起寄存器的“写1清除”机制
EXTI_PR(Pending 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并且退出ISR,NVIC才会释放。如果你在ISR里没清EXTI_PR,退出ISR后NVIC会立刻再次进入同一个ISR,形成死循环。
2.4 EXTI_SWIER:软件触发中断的用途
EXTI_SWIER(Software 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。 - 退出ISR,NVIC释放,等待下一次触发。
这八步里,第4步是NVIC的事,下一节详细讲。第7步的清除操作是最容易忘的,忘了就是死循环。
3. NVIC优先级分组:为什么你的中断嵌套不按预期工作
3.1 NVIC_ISER和NVIC_ICER:使能与失能
NVIC的寄存器在系统地址空间里,不是外设地址。STM32F103的NVIC支持60个可屏蔽中断通道(具体数量看型号),每个通道对应一个IRQ编号。
NVIC_ISER(Interrupt 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_IPR(Interrupt Priority Register)有15个寄存器(IPR[0]~IPR[14]),每个寄存器管4个中断通道,每个通道占8个bit。STM32F103只用了高4位,低4位保留。
8个bit里,高4位又分成“抢占优先级”和“子优先级”两部分。具体怎么分,由SCB->AIRCR的PRIGROUP字段决定。这就是优先级分组。
分组方式有5种:
| 分组 | 抢占优先级位数 | 子优先级位数 | 抢占级数 | 子级数 |
|---|---|---|---|---|
| 0 | 0 | 4 | 1 | 16 |
| 1 | 1 | 3 | 2 | 8 |
| 2 | 2 | 2 | 4 | 4 |
| 3 | 3 | 1 | 8 | 2 |
| 4 | 4 | 0 | 16 | 1 |
分组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]对应IRQ6(EXTI0)。高4位里,bit7~6是抢占优先级,bit5~4是子优先级。所以1 << 6设置抢占级为1,1 << 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子0,TIM2子1),那么:
- 同时挂起时,EXTI0先执行(子级0比1高)。
- 如果TIM2正在执行,EXTI0触发,EXTI0不会打断TIM2,因为抢占级相同。
这个规则在实际项目里很关键。比如串口接收中断和定时器中断,如果串口接收的实时性要求高,就把串口的抢占级设得比定时器高,这样串口数据来了能立刻处理,不会因为定时器ISR执行太久而丢数据。
提示:中断嵌套层数不要超过3层。层数太多会导致栈溢出,而且逻辑很难调试。如果发现需要4层以上的嵌套,说明你的中断设计有问题,应该把部分逻辑移到主循环或者用DMA。
4. 从寄存器到可运行代码:一个完整的按键中断实例
4.1 硬件连接与寄存器映射
假设用PA0接按键,按下时接地(下降沿触发)。PA0对应EXTI0,IRQ编号6。需要配置的寄存器清单:
RCC->APB2ENR:使能GPIOA和AFIO时钟GPIOA->CRL:PA0配置为输入(浮空或上拉)AFIO->EXTICR[0]:选择PA0映射到EXTI0EXTI->IMR:使能EXTI0中断EXTI->FTSR:下降沿触发NVIC->ISER[0]:使能IRQ6NVIC->IPR[6]:设置优先级SCB->AIRCR:设置优先级分组
时钟使能是最容易忘的一步。RCC->APB2ENR的bit2是GPIOA,bit0是AFIO。不使能AFIO时钟,AFIO_EXTICR写不进去,引脚映射就失败。
RCC->APB2ENR |= (1 << 2) | (1 << 0); // GPIOA + AFIOPA0配置为输入,CRL的bit0~3控制PA0。浮空输入是0100(0x4),上拉输入是1000(0x8)。按键接地的话,用上拉输入更稳,避免悬空时误触发。
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_IMR | 0x00 | 中断使能 | 写1使能对应线 |
| EXTI_EMR | 0x04 | 事件使能 | 写1使能对应线 |
| EXTI_RTSR | 0x08 | 上升沿触发 | 写1使能 |
| EXTI_FTSR | 0x0C | 下降沿触发 | 写1使能 |
| EXTI_SWIER | 0x10 | 软件触发 | 写1触发,自动清除 |
| EXTI_PR | 0x14 | 挂起标志 | 写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_AIRCR | 0xE000ED0C | 优先级分组 | 密钥0x5FA+分组值 |
5.3 优先级分组选择建议
| 应用场景 | 推荐分组 | 理由 |
|---|---|---|
| 简单项目,中断少 | 分组4 | 全部抢占级,嵌套逻辑简单 |
| 有实时性要求的中断 | 分组2 | 兼顾嵌套和同级顺序 |
| 中断多且需要精细调度 | 分组3 | 8级抢占,2级子优先级 |
| 不需要嵌套 | 分组0 | 全部子优先级,不会互相打断 |
分组2是我个人最常用的,4级抢占够用,4级子优先级也能处理同级竞争。分组4虽然简单,但所有中断都能互相打断,容易出竞态问题。
5.4 中断配置的检查清单
写完中断代码后,按这个清单过一遍:
- 外设时钟使能了吗?(GPIO、AFIO、外设本身)
- GPIO模式配置对吗?(输入/输出/复用)
- AFIO映射写对了吗?(EXTICR的索引和bit位置)
- EXTI触发边沿设了吗?(RTSR/FTSR)
- EXTI中断使能了吗?(IMR,不是EMR)
- NVIC通道使能了吗?(ISER,IRQ编号查表)
- 优先级分组设了吗?(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里打断点高效得多,而且不会干扰中断的实时性。