1. 为什么第三章值得单独拎出来讲
如果你是从第一章、第二章一路看过来的,那说明你已经迈过了"点亮一颗LED"和"看懂时钟树"这两道坎。到了第三章,事情开始变得有意思了——我们不再满足于让某个外设"能动",而是要搞清楚它"为什么能动"、"动得多快"、"动的时候CPU在干嘛"。这一章的核心,就是中断与事件系统,以及它背后那套让无数新手抓狂的NVIC(嵌套向量中断控制器)。
我见过太多人卡在这一章。前两章配寄存器配得挺顺,一到中断就懵:明明使能了中断,为什么进不去?明明写了中断服务函数,为什么跳到了HardFault?明明优先级设了,为什么嵌套行为跟想象的不一样?这些问题不是因为你笨,而是因为寄存器手册在这一块写得极其"反人类"——它默认你已经理解了Cortex-M内核的中断模型,而绝大多数教程又只告诉你"这样配就行",不告诉你"为什么必须这样配"。
所以这一章我打算换个讲法。不按手册的寄存器顺序念,而是按一个中断从触发到返回的完整生命周期来拆。你跟着这条时间线走一遍,每个寄存器在哪个环节起作用、为什么非它不可,自然就串起来了。这一章的内容量比较大,我建议你手边开着参考手册的NVIC章节和向量表章节,对照着看。
先给个全局印象:STM32的中断系统分两层——内核层的NVIC管优先级、使能、挂起和嵌套,外设层的中断标志位管"谁触发了中断"。很多人搞混这两层,以为在NVIC里使能了就能进中断,结果外设那边的标志位根本没清或者根本没使能,自然进不去。这个"两层模型"是理解整章的地基,后面所有细节都挂在这上面。
2. 中断触发的完整链路:从外设到CPU取向量
2.1 外设侧:标志位才是真正的"触发源"
先明确一件事:NVIC本身不产生中断,它只是"中转站"。真正产生中断请求的是外设。以EXTI(外部中断/事件控制器)为例,一个按键按下,EXTI的挂起寄存器(EXTI_PR)里对应的位被硬件置1,这个"1"就是中断请求的源头。
这里有个新手最容易踩的坑:EXTI_PR是写1清零的,不是写0清零,也不是读清零。你写0进去它纹丝不动,写1才清。我第一次调EXTI的时候,中断服务函数里写了EXTI->PR = 0;,结果中断疯狂重入,CPU直接卡死。后来才反应过来,应该是EXTI->PR = EXTI_PR_PR0;(对应位写1)。这个设计是为了避免"读-改-写"竞争——你只想清某一位,不想影响其他位,写1清零天然支持这种原子操作。
除了EXTI_PR,还有两个寄存器必须配对使用:IMR(中断屏蔽寄存器)和EMR(事件屏蔽寄存器)。IMR控制"是否向NVIC发中断请求",EMR控制"是否向其他外设发事件脉冲"。这两个是独立的,你可以只开中断不开事件,也可以只开事件不开中断。事件机制在后面讲DMA和定时器联动时会大放异彩,这里先记住它存在。
配置一个EXTI通道的标准流程是这样的:
// 1. 使能SYSCFG时钟(EXTI配置依赖它) RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; // 2. 把GPIO引脚映射到EXTI线(以PA0为例) SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0; // 清空 SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA; // 选PA // 3. 配置触发边沿 EXTI->RTSR |= EXTI_RTSR_TR0; // 上升沿触发 EXTI->FTSR &= ~EXTI_FTSR_TR0; // 关闭下降沿 // 4. 解除屏蔽 EXTI->IMR |= EXTI_IMR_IM0; // 5. 清一次挂起位(防止配置过程中产生的假触发) EXTI->PR = EXTI_PR_PR0;注意第5步,很多人省了这一步,结果一使能中断立刻进一次,还以为是"上电抖动"。其实是你配置过程中某个瞬间满足了触发条件,硬件把PR置1了。养成"配置完先清一次"的习惯,能省掉大量莫名其妙的调试时间。
2.2 内核侧:NVIC的四个关键寄存器
外设把请求送到NVIC之后,NVIC要做四件事:判断是否使能、比较优先级、决定是否抢占当前中断、把向量地址送给CPU。对应四个寄存器组:
- ISER(中断使能寄存器):写1使能,写0无效(不是写0禁止)。要禁止得写ICER。这个"写1使能、写0无效"的设计跟EXTI_PR类似,都是为了原子操作。
- ISPR/ICPR(挂起/解挂寄存器):可以软件触发一个中断(写ISPR),也可以清除一个挂起的中断(写ICPR)。调试时特别有用——你可以不接任何硬件,直接写ISPR软件触发中断,验证你的中断服务函数逻辑对不对。
- IPR(优先级寄存器):每个中断占8位,但STM32只实现了高4位,低4位读回是0。这意味着优先级只有16级(0-15),数值越小优先级越高。
- IABR(活跃位寄存器):只读,告诉你当前哪些中断正在执行。调试嵌套问题时,读这个寄存器能一眼看出谁在跑。
这里要重点说优先级。STM32的优先级分抢占优先级和子优先级,通过SCB->AIRCR的PRIGROUP字段划分。比如PRIGROUP设为5,那么高5位是抢占优先级,低3位是子优先级(在4位有效位里切)。抢占优先级决定能不能嵌套,子优先级只在同时挂起时决定谁先执行,不能嵌套。
我个人的建议是:除非你有非常明确的嵌套需求,否则全部设成抢占优先级,子优先级全给0。这样行为最直观——高抢占优先级的随时能打断低的,同抢占优先级的排队执行。混用子优先级只会让调试复杂度指数上升,收益却很小。
2.3 向量表:CPU怎么找到你的函数
中断触发后,CPU从向量表里取地址。向量表默认在Flash起始地址(0x08000000),但可以通过SCB->VTOR重定位到RAM或其他地址。重定位在Bootloader场景下是必须的——App的向量表不在0x08000000,你不重定位,中断一来CPU就跳到Bootloader的向量表里去了,直接跑飞。
向量表的排列顺序是固定的:前16个是内核异常(Reset、NMI、HardFault等),从第16个开始是外设中断,顺序由芯片型号决定,不能改。你写中断服务函数时,函数名必须和启动文件里的弱定义符号一致,否则链接器找不到,你的函数永远不会被调用。
// 启动文件里的弱定义(以EXTI0为例) void EXTI0_IRQHandler(void) __attribute__((weak, alias("Default_Handler"))); // 你的实现(名字必须一模一样) void EXTI0_IRQHandler(void) { if (EXTI->PR & EXTI_PR_PR0) { // 处理逻辑 EXTI->PR = EXTI_PR_PR0; // 清标志 } }注意:中断服务函数里一定要先判断标志位再处理,最后清标志。判断标志位是因为多个中断源可能共用一个向量(比如EXTI0到EXTI4各自独立,但EXTI5到EXTI9共用一个向量),你不判断就处理,可能处理了不该处理的中断。
3. 优先级分组与嵌套:那些手册不会告诉你的行为细节
3.1 PRIGROUP的划分逻辑与常见误配
Cortex-M的优先级寄存器有8位,STM32只用了高4位。PRIGROUP决定这4位里哪几位是抢占、哪几位是子优先级。手册给了一张表,但那张表容易看晕。我用一句话总结:PRIGROUP的值等于"子优先级占的位数"。比如PRIGROUP=3,子优先级占3位,抢占优先级占1位(4-3=1),那么抢占优先级只有0和1两级,子优先级有0-7八级。
这里有个反直觉的点:PRIGROUP越大,抢占优先级能分的级数越少。很多人第一次配的时候想"我要精细控制抢占",结果把PRIGROUP设得很大,反而只剩一两级抢占,嵌套行为完全不是想要的。
我的经验是:大多数项目用PRIGROUP=4,即2位抢占、2位子优先级,或者干脆PRIGROUP=7(全子优先级,无嵌套)。前者适合有少量嵌套需求的场景,后者适合"我不需要嵌套,只要排队"的场景。真正需要4位全抢占(16级嵌套)的项目极少,因为嵌套太深会导致栈溢出,而且中断延迟不可控。
配置代码很简单,但要注意AIRCR有写保护:
// 设置PRIGROUP=4(2位抢占,2位子优先级) SCB->AIRCR = (0x5FA << SCB_AIRCR_VECTKEY_Pos) | (4 << SCB_AIRCR_PRIGROUP_Pos);那个0x5FA是密钥,不写这个值写不进去。我第一次配的时候忘了密钥,读回AIRCR发现没变,还以为是芯片坏了。
3.2 嵌套发生的精确条件
嵌套不是"高优先级一定能打断低优先级",它有三个条件同时满足才行:
- 新中断的抢占优先级严格高于当前正在执行的中断的抢占优先级。
- 新中断的NVIC使能位为1。
- 当前中断没有把PRIMASK置1(即没有关中断)。
子优先级不参与嵌套判断。两个中断抢占优先级相同、子优先级不同,后到的不会打断先到的,只会等先到的执行完再执行。这个行为在手册里写得很隐晦,但实际项目中经常遇到——你以为设了更小的子优先级就能抢占,结果并没有。
还有一个细节:中断返回时的"尾链"(tail-chaining)。当一个中断执行完,如果此时有另一个挂起的中断,CPU不会先出栈再入栈,而是直接取新向量继续执行,省掉了出栈入栈的12个周期。这个优化是硬件自动做的,你不需要配置,但理解它能帮你解释为什么某些场景下中断响应比预期快。
3.3 优先级反转与临界区保护
中断嵌套带来一个经典问题:优先级反转。低优先级任务持有锁,高优先级中断来了想拿同一把锁,结果高优先级被低优先级阻塞。在裸机环境里,这个问题表现为:主循环正在操作一个链表,中断来了也要操作同一个链表,链表操作到一半被中断打断,中断里看到的是不完整的状态。
解决方案是临界区。Cortex-M提供了PRIMASK、FAULTMASK、BASEPRI三个屏蔽寄存器:
- PRIMASK:置1屏蔽所有可配置优先级的中断(NMI和HardFault除外)。用
__disable_irq()和__enable_irq()操作。 - BASEPRI:屏蔽优先级数值大于等于某个值的中断。比如设BASEPRI=5,那么优先级5到15的中断都被屏蔽,0到4的还能进来。这个比PRIMASK精细,适合"我只想屏蔽低优先级中断,高优先级的实时任务不能停"的场景。
- FAULTMASK:置1连HardFault都屏蔽,只有NMI能进。一般只在极端错误处理时用,平时别碰。
// 用BASEPRI保护临界区(屏蔽优先级>=5的中断) __set_BASEPRI(5 << (8 - __NVIC_PRIO_BITS)); // ... 临界区代码 ... __set_BASEPRI(0); // 解除屏蔽注意:BASEPRI的值要左移(8 - __NVIC_PRIO_BITS)位,因为STM32只用了高4位。直接写5进去是错的,得写5<<4=80。这个移位我第一次写的时候漏了,结果屏蔽范围完全不对,调了半天。
4. 中断服务函数的编写规范与性能陷阱
4.1 函数签名与编译器行为
中断服务函数的签名必须是void func(void),不能有参数、不能有返回值。这不是约定,是硬件要求——CPU取向量后直接跳转,没有地方给你传参。如果你写成void func(int x),编译器可能不报错(因为弱符号匹配只看名字),但运行时栈会乱,行为不可预测。
另外,中断服务函数不需要手动保存现场,硬件自动压栈R0-R3、R12、LR、PC、xPSR这8个寄存器。但如果你在函数里用了R4-R11,编译器会自动插入压栈指令。这就是为什么中断服务函数里用的局部变量越多,中断延迟越大——压栈的寄存器多了。
我见过有人在中断里写了一个复杂的浮点运算,结果中断延迟飙到几百个周期,主循环的实时性直接崩了。中断服务函数的原则是:能短则短,能不在中断里做的就不在中断里做。把数据存到一个缓冲区,置个标志,主循环去处理。这就是"上半部/下半部"的思想,虽然STM32没有像Linux那样正式的下半部机制,但思路是一样的。
4.2 共享变量的volatile与原子性
中断和主循环共享的变量,必须加volatile。不加的话,编译器优化时可能把这个变量缓存到寄存器里,主循环永远看不到中断的修改。这个坑太经典了,几乎每个学STM32的人都踩过。
volatile uint8_t g_flag = 0; // 中断里置1,主循环轮询 void EXTI0_IRQHandler(void) { g_flag = 1; EXTI->PR = EXTI_PR_PR0; } int main(void) { while (1) { if (g_flag) { g_flag = 0; // 处理逻辑 } } }但volatile只保证"每次都从内存读",不保证原子性。如果共享的是一个32位变量,在32位MCU上读写是原子的,没问题。但如果是一个64位变量或者一个结构体,中断里改一半、主循环读一半,就会读到撕裂的数据。这时候要么用临界区保护,要么用双缓冲——中断写A缓冲,主循环读B缓冲,通过一个指针原子切换。
4.3 中断延迟的组成与优化
中断延迟 = 硬件响应时间 + 压栈时间 + 取向量时间 + 你的代码执行时间。前三个是固定的(Cortex-M3/M4大约12个周期压栈+取向量),你能优化的是最后一个。
优化手段有几个:一是减少中断里的局部变量,降低压栈量;二是把不紧急的处理挪到主循环;三是如果多个中断源共用一个向量,用查表代替if-else链;四是把中断服务函数放到RAM里执行(Flash等待周期在高速下会拖慢取指)。
最后一条在STM32F4以上(带ART加速器)效果不明显,但在F1系列上,把中断服务函数搬到RAM能省不少周期。方法是用__attribute__((section(".RamFunc")))把函数放到RAM段,启动时从Flash拷贝过去。
5. 调试中断问题的实战排查链路
5.1 进不去中断的五种可能
中断进不去,按这个顺序排查,基本能覆盖95%的情况:
- 外设时钟没开:EXTI依赖SYSCFG,SYSCFG在APB2上。你只开了GPIO时钟没开SYSCFG,EXTI配置写不进去。
- NVIC使能位没置:
NVIC_EnableIRQ(EXTI0_IRQn)漏了。这个最隐蔽,因为外设侧看起来都配好了。 - 优先级分组没设:AIRCR的PRIGROUP默认值可能不是你想要的,导致优先级判断异常。
- 全局中断没开:
__enable_irq()漏了,或者某处调了__disable_irq()没恢复。 - 向量表没重定位:用了Bootloader但忘了设VTOR,中断跳到Bootloader的向量表去了。
排查方法:先在NVIC的IABR寄存器看中断有没有变成活跃,如果没有,说明请求根本没到NVIC,问题在外设侧;如果活跃了但没进函数,问题在向量表或函数名。
5.2 中断反复进入的根因分析
中断反复进入,99%是标志位没清。但"没清"分几种情况:
- 清错了寄存器:清了NVIC的挂起位(ICPR),没清外设的标志位(EXTI_PR)。NVIC的挂起位是"请求排队"用的,外设标志位才是"请求源"。你清了ICPR,外设标志还在,NVIC立刻又收到请求,又进中断。
- 清标志的时机不对:在中断服务函数开头清,然后处理逻辑里又触发了同一个中断源,导致丢失一次。正确做法是先清标志,再处理,或者先处理,最后清,取决于你的逻辑是否会产生新触发。
- 多个中断源共用向量:EXTI5到EXTI9共用一个向量,你只清了EXTI5的标志,EXTI9的标志还在,出去立刻又进来。必须在函数里遍历所有可能的源,逐个判断清除。
5.3 HardFault的定位技巧
中断里跑飞进HardFault,最常见的原因是栈溢出或访问了非法地址。定位方法:
首先看LR寄存器的值。如果LR是0xFFFFFFF9,说明是从线程模式进的中断;如果是0xFFFFFFFD,说明是从中断模式进的。然后看压栈的PC值,那个地址就是跑飞的位置。
在调试器里,HardFault发生时,查看栈帧:
void HardFault_Handler(void) { __asm volatile ( "tst lr, #4\n" "ite eq\n" "mrseq r0, msp\n" "mrsne r0, psp\n" "b hardfault_report\n" ); }hardfault_report里把r0指向的栈帧打印出来,PC、LR、xPSR一目了然。PC指向的指令就是罪魁祸首。我靠这招定位过好几次"中断里调用了未初始化函数指针"的问题。
提示:栈溢出在中断场景下特别常见,因为中断嵌套会叠加栈使用。如果你的栈设得比较小(比如1KB),又开了多层嵌套,很容易溢出。建议至少留2KB给中断栈,嵌套深的场景留4KB。
6. 从寄存器操作到实际项目的映射
6.1 一个完整的按键中断案例
把前面的知识串起来,做一个按键控制LED的完整例子。需求:PA0按键按下,PC13的LED翻转。用寄存器操作,不用库函数。
#include "stm32f10x.h" volatile uint8_t g_key_pressed = 0; void EXTI0_IRQHandler(void) { if (EXTI->PR & EXTI_PR_PR0) { g_key_pressed = 1; EXTI->PR = EXTI_PR_PR0; } } int main(void) { // 1. 时钟:GPIOA、GPIOC、AFIO、SYSCFG RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN; // 2. PA0输入浮空 GPIOA->CRL &= ~(0xF << 0); GPIOA->CRL |= (0x4 << 0); // 浮空输入 // 3. PC13推挽输出 GPIOC->CRH &= ~(0xF << 20); GPIOC->CRH |= (0x2 << 20); // 推挽输出2MHz // 4. AFIO映射EXTI0到PA0 AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0; AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI0_PA; // 5. EXTI0下降沿触发 EXTI->FTSR |= EXTI_FTSR_TR0; EXTI->RTSR &= ~EXTI_RTSR_TR0; EXTI->IMR |= EXTI_IMR_IM0; EXTI->PR = EXTI_PR_PR0; // 清一次 // 6. NVIC配置 NVIC_SetPriorityGrouping(4); NVIC_SetPriority(EXTI0_IRQn, 5); NVIC_EnableIRQ(EXTI0_IRQn); // 7. 开全局中断 __enable_irq(); while (1) { if (g_key_pressed) { g_key_pressed = 0; GPIOC->ODR ^= (1 << 13); // 翻转LED } } }这个例子里有几个细节值得说。第一,AFIO时钟必须开,否则EXTICR写不进去。第二,NVIC_SetPriorityGrouping(4)要在设置优先级之前调用,否则优先级值会被错误解释。第三,EXTI->PR的清零放在配置最后,防止配置过程中的假触发。
6.2 中断与主循环的分工原则
什么时候该用中断,什么时候该用轮询?我的判断标准是:事件发生的时刻不可预测,且响应延迟有要求,用中断;事件发生频率极高,且处理逻辑简单,用轮询。
比如按键,按下时刻不可预测,用中断。比如串口接收,数据到达时刻不可预测,用中断。比如ADC连续采样,频率固定且高,用DMA+轮询,不用中断。比如LED闪烁,定时器中断里翻转,或者主循环里延时翻转都行,看你对时间精度的要求。
中断服务函数里只做"必须立刻做"的事。比如串口接收中断,你只需要把数据从DR寄存器读出来存到缓冲区,解析协议的事放到主循环。这样中断执行时间短,不会阻塞其他中断。
6.3 中断优先级的实际分配策略
一个典型项目的优先级分配,我一般这么排(数值越小优先级越高):
| 优先级 | 用途 | 理由 |
|---|---|---|
| 0 | 系统滴答定时器 | 提供时基,必须最准时 |
| 1 | 紧急故障处理 | 过流、过压等保护 |
| 2-3 | 高速通信 | 串口、SPI,防止数据丢失 |
| 4-5 | 普通外设 | 按键、普通定时器 |
| 6-7 | 低速外设 | 温湿度传感器等 |
| 8-15 | 非关键任务 | 状态指示、日志输出 |
这个分配不是死的,但原则是:越不能容忍延迟的,优先级越高。同时要注意,高优先级中断的执行时间要尽可能短,否则会阻塞所有低优先级中断。
还有一个经验:不要把所有中断都设成最高优先级。我见过一个项目,所有中断都是优先级0,结果中断之间不能嵌套,一个慢中断执行时其他中断全在排队,实时性反而更差。优先级的意义在于区分轻重缓急,全设成一样就失去了意义。
7. 一些手册上找不到的实操心得
7.1 软件触发中断的调试妙用
调试中断逻辑时,不一定要接硬件。直接写NVIC的ISPR寄存器,就能软件触发一个中断:
NVIC->ISPR[0] = (1 << 6); // 触发EXTI0中断(假设EXTI0是第6号中断)这样你可以验证中断服务函数的逻辑对不对,不用去按按键或者等外部信号。等逻辑调通了,再接硬件验证触发路径。这个技巧在调试复杂中断嵌套时特别有用,因为你可以精确控制触发时机。
7.2 中断向量表的RAM重定位
有些场景下需要动态修改中断服务函数,比如Bootloader跳转到App后,App想用自己的中断处理。这时候把向量表重定位到RAM,然后修改RAM里的向量表:
// 把向量表从Flash拷到RAM extern uint32_t _svector_table; extern uint32_t _evector_table; uint32_t *ram_vectors = (uint32_t *)0x20000000; memcpy(ram_vectors, &_svector_table, (uint32_t)&_evector_table - (uint32_t)&_svector_table); // 重定位 SCB->VTOR = (uint32_t)ram_vectors; // 动态修改某个中断的处理函数 ram_vectors[16 + EXTI0_IRQn] = (uint32_t)my_new_handler;这个技巧在实现"可插拔"的中断处理时很有用,但要注意RAM向量表要按128字节对齐(VTOR的低7位保留)。
7.3 中断延迟的实测方法
想知道你的中断延迟到底是多少?用GPIO翻转法测:在中断服务函数第一行翻转一个GPIO,用示波器看触发信号和GPIO翻转之间的时间差。这个时间差就是总中断延迟。
我实测过STM32F103在72MHz下的EXTI中断延迟,从引脚信号跳变到GPIO翻转,大约1.2微秒。其中硬件响应和压栈占了大头,我的中断服务函数本身只花了不到0.2微秒。这个数据帮你建立直觉:中断延迟不是零,微秒级的延迟在高速控制场景下是要考虑的。
如果要进一步降低延迟,可以把中断服务函数放到RAM执行,F1系列上能省大概0.3微秒。F4系列有ART加速器,放RAM的收益很小,不值得折腾。
7.4 嵌套中断的栈深度估算
中断嵌套的栈深度 = 主程序栈深度 + 每层中断的栈帧。每层中断的栈帧至少是8个寄存器(32字节),加上你函数里的局部变量。如果嵌套3层,每层用50字节,那就是150字节。加上主程序的栈使用,你就能估算出最小栈大小。
我的习惯是:栈大小 = 主程序最大栈深度 + 中断层数 × 每层最大栈帧 × 1.5(安全系数)。比如主程序用200字节,最多3层中断,每层80字节,那就是200 + 3×80×1.5 = 560字节。取个整,给1KB,留足余量。
栈溢出是嵌入式系统最隐蔽的bug之一,因为它不一定立刻崩溃,可能只是覆盖了其他变量,导致行为诡异。在栈的末尾放一个魔数(比如0xDEADBEEF),定期检查这个魔数有没有被改写,能提前发现栈溢出。
7.5 中断与低功耗的配合
STM32的低功耗模式和中断是深度绑定的。WFI(Wait For Interrupt)指令让CPU进入睡眠,任何中断都能唤醒。WFE(Wait For Event)则是等事件,事件可以是中断,也可以是其他外设发的事件脉冲。
在低功耗场景下,中断服务函数执行完后,如果没有任何挂起的中断,CPU会再次进入睡眠。这个行为是自动的,不需要你手动再执行WFI。但要注意,如果中断服务函数里清了某个标志,导致没有新的中断挂起,CPU就会一直睡下去,直到下一个中断到来。
调试低功耗时,我经常用GPIO翻转来观察CPU的睡眠时间。在进入WFI前翻转一个GPIO,在中断服务函数第一行翻转另一个GPIO,两个GPIO之间的时间就是睡眠时间。这个方法能直观地看到低功耗策略有没有生效。
8. 从这一章往后,你该往哪走
第三章的内容到这里就差不多了。中断和事件系统是STM32里最"内核"的部分,把它吃透,后面学DMA、学RTOS、学通信协议,都会轻松很多。因为DMA的传输完成中断、RTOS的任务切换(本质是PendSV异常)、通信协议的超时处理,全都建立在这一章的基础上。
我个人的建议是,不要急着往后翻。把这一章的每个寄存器都亲手写一遍,把按键中断的例子跑通,然后故意制造几个错误——比如不清标志位、不设优先级分组、不重定位向量表——看看现象是什么,用调试器定位问题。这种"故意踩坑"的练习,比顺顺利利跑通一个例子有价值得多。
下一章我们会讲DMA与总线矩阵,那是另一个让人又爱又恨的话题。但有了这一章的中断基础,DMA的中断配置对你来说就是小菜一碟了。