1. 项目概述:从“中断”这个神来之笔说起
如果你写过单片机程序,或者接触过任何嵌入式开发,一定对“中断”这个概念不陌生。它就像一个随时待命的管家,当有紧急事件(比如按键按下、数据到达)发生时,会立刻打断CPU当前的工作,优先去处理这个紧急事务,处理完再回来继续原来的活。这个机制是现代计算机系统高效、实时响应的基石。但说实话,很多朋友对中断的理解,可能还停留在“概念”层面,知道它很重要,但具体到CPU内部,从一根引脚的电平变化,到最终执行你写的那个中断服务函数,这中间到底发生了什么?寄存器怎么变的?程序计数器(PC)跳到了哪里?中断嵌套又是怎么实现的?这些问题,光看课本和示意图,总觉得隔了一层纱。
这个“计算机组成原理 中断原理实验”,就是亲手把这层纱扯掉。它不是让你在现成的开发板上调用一个attachInterrupt()函数就完事了,而是让你从最底层开始,设计一个支持中断的简易CPU模型,或者在一个已有的CPU模型(比如MIPS单周期CPU)上,添加中断处理机制。你需要自己设计中断请求(IRQ)信号线、定义中断向量、编写中断服务程序(ISR)、管理现场保护与恢复的堆栈操作,甚至实现中断优先级和嵌套。这个过程,会让你对“中断”的理解,从“知道有这么回事”飞跃到“原来它是这么回事”的层面。无论是计算机专业的学生夯实基础,还是嵌入式工程师想深入理解内核调度原理,这个实验都是一次绝佳的“庖丁解牛”式的实践。
2. 实验核心设计思路与方案选型
做这个实验,首先得明确我们的“战场”在哪里。通常有两种主流路径,它们各有侧重,适合不同背景的实践者。
2.1 路径一:基于硬件描述语言(HDL)的CPU模型扩展
这是最硬核、也最贴近“组成原理”本质的做法。假设你已经用Verilog或VHDL完成了一个基本的单周期或多周期CPU设计,比如实现了MIPS或RISC-V的一个子集。现在,你要为这个“裸CPU”添加上中断能力。
核心设计考量如下:
- 中断源与中断请求(IRQ)信号:你需要定义中断的来源。最简单的,可以设计一个外部按键作为中断源,按键按下产生一个高电平脉冲作为IRQ信号。在CPU模块中,你需要增加一个输入端口来接收这个信号。
- 中断使能控制:CPU不能随时被中断,否则就乱套了。你需要设计一个中断使能寄存器(如Status寄存器中的IE位)。只有当IE=1时,CPU才响应中断。
- 中断检测与响应时机:CPU在每个时钟周期(通常是取指阶段结束后)检查两个条件:a) 当前是否有未屏蔽的中断请求(IRQ有效);b) 中断是否全局使能(IE=1)。同时,CPU自身不能正处于异常处理(如另一个中断)中。当条件满足,CPU进入中断响应流程。
- 中断向量与程序计数器(PC)的重定向:这是关键一步。中断发生时,CPU需要知道该跳转到哪里去执行中断服务程序。通常有两种模式:
- 固定向量中断:为不同类型的中断分配固定的内存地址(中断向量)。例如,将地址
0x00000020定义为外部中断入口。一旦响应中断,硬件自动将PC设置为这个固定地址。 - 向量化中断:更高级,中断控制器会提供一个向量号,CPU根据该向量号查询一个在内存中的“中断向量表”(IVT),从中取出对应的服务程序入口地址。我们的实验从简单的固定向量开始即可。
- 固定向量中断:为不同类型的中断分配固定的内存地址(中断向量)。例如,将地址
- 现场保护与恢复:中断服务程序要使用寄存器,但绝对不能破坏主程序原来的寄存器值。因此,在跳转到服务程序之前,必须把当前的“现场”(主要是PC和状态寄存器)保存起来。通常的做法是:
- 硬件自动保存:由CPU硬件自动将当前PC(即被中断指令的下一条指令地址)和状态寄存器压入一个特殊的寄存器(如EPC)或系统堆栈。
- 软件显式保存:在中断服务程序的开头,用软件指令将需要使用的通用寄存器压入堆栈;在服务程序末尾,再弹出恢复。 我们的简易模型,可以让硬件自动保存PC到EPC寄存器,通用寄存器的保存/恢复则由服务程序完成。
- 中断返回:服务程序执行完毕后,需要一条特殊的指令(如MIPS的
eret指令)从异常中返回。这条指令会让CPU硬件从EPC寄存器中恢复PC,并可能恢复之前的状态寄存器,从而跳回被中断的主程序继续执行。
注意:在HDL实现中,你需要修改控制单元(CU)的状态机,增加一个“中断响应”状态。在这个状态里,完成PC的重定向、现场保存等操作。同时,数据通路需要增加EPC等特殊寄存器,以及访问它们的路径。
2.2 路径二:基于模拟器或FPGA开发板的软硬件协同验证
如果你觉得从零设计CPU过于复杂,或者想更快看到效果,这条路径更友好。我们利用现有的、支持中断的CPU核(如基于FPGA的软核处理器Nios II,或模拟器如Mars for MIPS),重点编写和调试中断服务程序,并观察CPU内部状态的变化。
以在Mars模拟器中实现中断为例:
Mars内置了模拟外部中断的工具。我们可以这样设计实验:
- 编写主程序:一个简单的循环,比如在控制台输出数字。
- 编写中断服务程序(ISR):定义一个在固定地址(如
0x00000180,这是MIPS默认的中断向量地址)开始的程序。它的职责是:保护现场(将$at, $v0, $a0等寄存器压栈)、处理中断(如读取一个模拟的设备寄存器、改变某个全局变量)、恢复现场、最后执行eret返回。 - 配置Mars的中断模拟器:在Tools菜单下打开“Interrupt and Alarms”工具,可以设置定时器中断或手动触发中断。
- 观察与调试:单步执行程序,在中断触发瞬间,观察PC寄存器的跳转、EPC寄存器(保存的返回地址)的值、Cause寄存器(记录中断原因)的变化,以及堆栈指针($sp)和内存中堆栈内容的变化。这是理解中断机制最直观的方式。
方案选型建议:
- 初学者/侧重原理理解:强烈推荐路径二(Mars模拟器)。它屏蔽了复杂的硬件细节,让你能专注于中断流程、现场保护、返回机制等核心概念,通过单步调试获得直观感受。
- 进阶者/侧重硬件实现:选择路径一(HDL扩展)。这需要你先有一个能正确运行的基础CPU,然后将其当作一个项目来迭代升级。这对理解计算机硬件系统的协同工作方式有质的提升。
3. 核心细节解析与实操要点
无论选择哪条路径,以下几个核心细节是必须啃透的硬骨头,它们直接决定了你的中断系统是否能正确、稳定地工作。
3.1 中断响应与指令执行的原子性
这是一个极易出错的关键点。中断检测发生在哪个时刻?假设CPU正在执行一条多周期指令(比如乘法指令),执行到一半时来了中断,该怎么办?
正确处理逻辑:中断响应应该发生在一条指令执行完毕之后,下一条指令开始之前。也就是说,中断的插入要保证指令的原子性,不能打断一条指令的执行过程。在你的HDL状态机中,中断检测和判断逻辑应该放在一条指令的最后一个周期(通常是写回阶段之后),或者下一条指令的第一个周期(取指阶段之前)。如果检测到有效中断,则取消下一条指令的取指,转而进入中断响应流程。
实操心得:在Mars中单步调试时,你可以清晰地看到,中断总是在当前指令完全执行完后才生效。如果你自己设计CPU,在控制单元里一定要有一个明确的“指令边界”信号,用来安全地插入中断处理。
3.2 现场保护:到底要保存什么?
“现场”指的是被中断那一刻,程序能够正确恢复运行所需要的所有CPU状态。至少包括:
- 程序计数器(PC):必须保存,否则无法返回。通常由硬件自动保存到EPC。
- 状态寄存器(SR/Status):里面包含了中断使能位(IE)、之前的处理器模式等关键信息。这些也需要保存,以便返回时恢复中断前的系统状态。在简单实现中,可以和PC一起由硬件处理。
- 通用寄存器:中断服务程序极有可能会修改它们。如果主程序正在使用
$t0计算一个重要结果,被中断后ISR也用了$t0,结果就被覆盖了。因此,在ISR开头,你必须把你将要使用的所有通用寄存器压入堆栈。通常,编译器或编程规范会定义一组“调用者保存”和“被调用者保存”的寄存器,ISR需要遵守这个约定,保存那些被调用者需要保存的寄存器(如MIPS中的$s0-$s7)。
一个常见的错误是只保存了部分寄存器,导致返回后主程序出现灵异错误。建议:在ISR开始时,简单粗暴地将所有通用寄存器($0除外)都压栈,结束时再弹出。虽然效率不高,但对于学习和调试来说,是最安全、最不容易出错的方式。
3.3 中断嵌套与优先级管理
当一个中断服务程序正在执行时,另一个更高优先级的中断来了,该怎么办?这就涉及到中断嵌套。
实现嵌套需要几个条件:
- 现场保护的堆栈化:现场必须保存在堆栈(内存)中,而不是固定的寄存器里。因为每个中断都有自己的现场,它们需要被依次压栈、弹栈。
- 中断服务程序中重新开中断:在保存完现场后,ISR需要尽快执行一条“开中断”指令(如MIPS的
ei),允许更高优先级的中断打断自己。否则,在ISR执行期间,整个系统是无法响应新中断的。 - 中断优先级控制器(可选):在硬件上,需要有一个模块来管理多个中断源,比较它们的优先级,当多个中断同时发生时,只向CPU提交优先级最高的那个。在简单实验中,我们可以用软件模拟优先级。
嵌套流程示例:
- 主程序执行中...
- 低优先级中断A发生:硬件关中断,保存PC/Status到堆栈,跳转到ISR_A。
- ISR_A开始:软件保存通用寄存器到堆栈,然后执行“开中断”指令。
- 此时,更高优先级中断B发生:硬件再次关中断,将当前的PC(指向ISR_A中的某条指令)和Status压入堆栈,然后跳转到ISR_B。
- ISR_B执行:保存通用寄存器,处理事务,恢复通用寄存器,执行“中断返回”指令。
- 返回时:硬件从堆栈中弹出Status和PC,系统回到ISR_A中被打断的那条指令继续执行。
- ISR_A继续:执行完毕,恢复通用寄存器,执行“中断返回”。
- 最终返回主程序。
这个过程就像“套娃”,堆栈操作必须严格对称(压入和弹出顺序相反),否则系统必然崩溃。
4. 基于Mars模拟器的中断实验实操过程
我们以最直观的路径二为例,展示一个完整的中断实验流程。目标:在Mars中编写一个程序,主程序循环打印字符,通过键盘模拟触发中断,在中断服务程序中改变打印的字符。
4.1 实验环境与代码准备
首先,确保你已安装MARS MIPS Simulator。我们编写三个核心部分:
1. 主程序 (main.asm)
.data msg: .asciiz "Running in main loop...\n" char: .byte 'A' # 全局变量,存储要打印的字符 .text .globl main main: # 初始化堆栈指针(Mars初始$sp指向0x7fffeffc,但为了安全我们显式设置) la $sp, 0x7fffeffc # 主循环 loop: # 打印当前字符 li $v0, 11 # syscall 11: print character lb $a0, char # 加载字符 syscall # 打印换行(可选) li $v0, 11 li $a0, '\n' syscall # 延迟一小段时间,方便观察 li $v0, 32 # syscall 32: sleep li $a0, 500 # 睡眠500毫秒 syscall j loop # 无限循环这个主程序很简单,就是不断打印char变量里的字符(初始是‘A’),每次间隔500毫秒。
2. 中断服务程序 (isr.asm)中断服务程序需要链接到特定的中断向量地址。MIPS默认的中断/异常处理入口地址是0x80000180(在Mars中,用户态程序的中断入口是0x00000180,但为了兼容性,我们使用Mars工具约定的地址)。我们通过.ktext段来定义。
.ktext 0x80000180 # 内核文本段,中断向量地址 # 注意:在Mars中,使用.ktext和.kdata来区分内核与用户空间,中断处理在内核态。 # 实际使用中,Mars的模拟中断工具会跳转到0x80000180,但我们需要在工具中设置。 # 但更简单的做法是,利用Mars的“Interrupt and Alarms”工具,它允许我们指定一个标签作为处理程序。 # 所以我们通常用.text段,但定义一个全局标签。 .text .globl my_interrupt_handler my_interrupt_handler: # 1. 保存现场(保护所有可能用到的寄存器) # 由于是演示,我们简单保存几个关键寄存器。实际中应保存更多或全部。 addi $sp, $sp, -12 # 在堆栈上分配空间(3个寄存器*4字节) sw $t0, 0($sp) sw $t1, 4($sp) sw $v0, 8($sp) # 注意:$a0, $a1在syscall中也会被使用,但这里我们没用到,所以不保存。 # 2. 处理中断:改变全局变量`char`的值 la $t0, char # 加载char的地址 lb $t1, 0($t0) # 读取当前字符 addi $t1, $t1, 1 # 字符值加1(A->B, B->C...) # 如果超过'Z',回到'A' li $t2, 'Z' ble $t1, $t2, not_wrap li $t1, 'A' not_wrap: sb $t1, 0($t0) # 存回char变量 # 3. 清除中断原因(模拟硬件操作) # 在真实硬件中,需要向中断控制器的特定寄存器写值来清除中断请求。 # 在Mars模拟中,这个步骤可以省略,或者通过工具完成。 # 4. 恢复现场 lw $v0, 8($sp) lw $t1, 4($sp) lw $t0, 0($sp) addi $sp, $sp, 12 # 5. 中断返回 (使用eret指令) # eret会同时恢复PC和Status寄存器。在Mars中,直接使用即可。 eret .end my_interrupt_handler3. 完整的整合程序将主程序和ISR放在同一个.asm文件中,并设置好全局符号。
.data msg: .asciiz "Running in main loop...\n" char: .byte 'A' .text .globl main main: la $sp, 0x7fffeffc loop: li $v0, 11 lb $a0, char syscall li $v0, 11 li $a0, '\n' syscall li $v0, 32 li $a0, 500 syscall j loop # 中断服务程序 .ktext 0x80000180 # 或者使用下面的标签方式,在工具中指定 __interrupt_handler: # 保存现场 addi $sp, $sp, -12 sw $t0, 0($sp) sw $t1, 4($sp) sw $v0, 8($sp) # 处理中断:修改char la $t0, char lb $t1, 0($t0) addi $t1, $t1, 1 li $t2, 'Z' ble $t1, $t2, no_wrap li $t1, 'A' no_wrap: sb $t1, 0($t0) # 恢复现场 lw $v0, 8($sp) lw $t1, 4($sp) lw $t0, 0($sp) addi $sp, $sp, 12 eret4.2 Mars模拟器配置与调试运行
- 汇编与加载:在Mars中打开上述汇编文件,点击“Assemble”进行汇编。如果没有语法错误,代码会被加载到模拟内存中。
- 打开中断工具:点击菜单栏的“Tools” -> “Interrupt and Alarms...”。
- 配置中断:
- 在工具界面,你会看到一个列表。点击“Connect to Program”。
- 在弹出的对话框中,找到“Interrupt Handler”一栏。这里需要指定你的中断服务程序的起始地址。有两种方式:
- 指定地址:如果你像上面代码一样使用了
.ktext 0x80000180,那么这里就填0x80000180。 - 指定标签(更推荐):如果你在代码中定义了一个全局标签(如
__interrupt_handler),你可以在这里直接输入标签名。Mars会自动计算其地址。
- 指定地址:如果你像上面代码一样使用了
- 在“Interrupt the program”部分,你可以选择触发方式。我们选择“Keyboard and Display”,然后勾选“Activate keyboard interrupts”。这样,当你在Mars的“Run I/O”标签页下的键盘输入区域敲击任何键时,就会模拟触发一个外部中断。
- 运行与观察:
- 回到主界面,不要直接点“Run”(全速运行),那样太快看不清。点击“Tools” -> “Keyboard and Display MMIO Simulator”,打开键盘显示模拟器。
- 然后点击“Step”(单步)或“Slow”慢速运行主程序。你会看到输出窗口开始循环打印‘A’。
- 关键时刻:将鼠标焦点切换到“Keyboard and Display MMIO Simulator”窗口,在“Transmitter”区域点击“Clear”按钮旁边的输入框,然后按一下键盘上的任意键(比如空格)。立刻观察:
- 寄存器窗口:你会发现PC寄存器的值瞬间跳转到了你的中断向量地址(如
0x80000180)。 - 堆栈指针$sp:它的值减少了(因为我们执行了
addi $sp, $sp, -12)。 - 内存窗口:查看
$sp地址附近的内存,可以看到我们保存的$t0, $t1, $v0的旧值。 - 继续单步执行,你会看到ISR中修改
char变量的指令被执行。 - 执行到
eret时,PC又神奇地跳回了主循环中刚才被打断的那条指令的下一条指令(注意,不是跳回循环开头!),并且char已经变成了‘B’。之后主程序就会开始打印‘B’。
- 寄存器窗口:你会发现PC寄存器的值瞬间跳转到了你的中断向量地址(如
- 反复触发:你可以在主程序打印‘B’的时候再次敲击键盘,触发第二次中断,字符会变成‘C’,如此往复。
这个实操过程,让你清晰地看到了中断触发 -> PC跳转 -> 现场保护 -> 执行ISR -> 恢复现场 -> 返回原流程的完整链条。每一个步骤都可以通过寄存器和内存的变化来验证。
5. 基于Logisim的硬件中断模型实现要点
对于选择路径一(硬件实现)的朋友,使用数字电路仿真软件Logisim是一个很好的起点。你可以在一个已有的简单CPU电路上添加中断模块。
5.1 硬件模块设计
你需要新增或修改以下电路模块:
- 中断请求锁存器:一个D触发器,用于锁存外部的中断请求信号(IRQ)。因为中断请求可能是脉冲,需要锁存住直到被CPU响应和处理。
- 中断使能寄存器(IE):一个位寄存器,作为CPU状态寄存器的一部分。可以由特定指令(如
ei、di)来置位或清零。 - 中断判断逻辑:一个与门(或更复杂的逻辑),输入为
锁存的IRQ和IE,输出为有效中断信号。这个信号送到控制单元。 - 控制单元(CU)修改:
- 在状态机中增加一个
INT状态(或称为异常处理状态)。 - 在每条指令执行的最后一个周期,检查
有效中断信号。如果为真,则下一个状态转入INT,而不是下一条指令的FETCH状态。
- 在状态机中增加一个
- INT状态的行为:
- 保存PC:将当前PC(即被中断指令的下一条指令地址)写入一个专门的EPC寄存器。这里注意,在单周期CPU中,PC+4的计算已经在当前周期完成,这个值就是需要保存的返回地址。
- 跳转中断向量:将PC的输入设置为固定的中断向量地址(如
0x00000004或0x00000020)。这可以通过一个多路选择器实现:正常情况下PC来源是PC+4或分支跳转地址;中断时,选择器输出中断向量地址。 - 关闭中断使能:将IE位清零,防止中断嵌套(在简单模型中先不实现嵌套)。
- 状态转移:
INT状态结束后,回到FETCH状态,此时PC已经是中断向量地址,从而开始取指执行中断服务程序的第一条指令。
- 中断返回指令
eret的实现:这是一条特殊指令。当译码器识别出eret操作码时,控制单元需要:- 将EPC寄存器的值写回PC。
- 重新打开中断使能(将IE置1)。
- 这可能也需要一个独立的状态(
ERET)来完成。
5.2 数据通路的修改
- PC多路选择器:需要增加一个输入源,即“中断向量地址”。其选择信号来自控制单元的中断响应信号。
- 新增EPC寄存器:需要连接到数据通路,支持写入(来自PC)和读出(到PC)。
- 状态寄存器:需要扩展一位作为IE位,并有相应的写入控制线。
5.3 测试方案
在Logisim中测试中断功能,需要构建一个简单的测试程序:
- 主程序:一段循环指令,比如不断让一个寄存器加1,你可以用一个探针观察这个寄存器的值在变化。
- 中断服务程序:从中断向量地址开始存放几条指令,比如让另一个寄存器置为一个特殊值(如0xFFFF),或者改变一个输出引脚的电平。
- 手动触发中断:用一个按钮模拟外部中断源,连接到IRQ锁存器。
- 仿真观察:
- 运行主程序,看到计数器寄存器规律变化。
- 在某个时刻,按下按钮。
- 观察:PC是否瞬间跳转到中断向量地址?计数器寄存器是否停止变化?EPC寄存器是否保存了正确的返回地址?服务程序里的寄存器是否被修改?
- 单步执行完服务程序,遇到
eret指令后,观察PC是否准确跳回主程序断点,计数器是否从断点处继续递增。
这个过程能让你对硬件中断的每一个时钟周期行为都有透彻的理解。
6. 常见问题、调试技巧与深度思考
在实际动手做这个实验时,你几乎一定会遇到下面这些问题。这里记录了我的踩坑实录和解决思路。
6.1 问题一:中断触发后,程序跑飞或进入死循环
- 现象:按下中断触发键,Mars程序计数器(PC)跳转后,没有执行我写的ISR代码,或者执行几条指令后就跑到未知地址去了。
- 排查思路:
- 检查中断向量地址是否正确:这是最常见的问题。确认你的ISR起始地址与你在Mars中断工具中设置的“Interrupt Handler”地址完全一致。一个字节都不能差。技巧:在Mars的“Execute”标签页,查看“Text Segment”(代码段)列表,找到你的ISR标签对应的地址,直接复制粘贴到工具配置里。
- 检查ISR的结束指令:ISR必须以
eret结束,而不是jr $ra。eret是专门从中断/异常返回的指令,它会从EPC和Status寄存器恢复状态。用jr $ra返回,PC会跳转到$ra寄存器里的地址,而这个地址在中断发生时是未定义的。 - 现场保护破坏了返回地址:如果你在ISR中使用了
jal指令调用其他子程序,会修改$ra寄存器。而eret并不使用$ra,所以通常没问题。但如果你错误地压栈/弹栈了$ra,或者堆栈操作不对称(push和pop次数不匹配),导致最后$sp指针错乱,可能会影响eret执行后的内存访问。务必保证堆栈操作平衡。
6.2 问题二:中断处理结束后,主程序状态出错
- 现象:中断能正常触发和执行ISR,但返回主程序后,某个寄存器的值莫名其妙变了,或者程序逻辑出错。
- 排查思路:
- 现场保护不完整:这是根本原因。ISR修改了某个寄存器(比如
$t0),但这个寄存器在主程序中也正在被使用。你的ISR在修改前没有保存它,返回后主程序用的就是被污染的值。解决方案:在ISR开头,将所有你计划使用的寄存器(或者简单点,把所有非$zero的临时寄存器$t0-$t9、参数寄存器$a0-$a3、返回值寄存器$v0-$v1)都压栈。虽然开销大,但保平安。在恢复时,严格按照相反的顺序弹出。 - 使用了非可重入的代码或变量:如果你的ISR和主程序访问了同一个全局变量,且没有进行原子保护(比如主程序正在读取该变量的一半时被中断,ISR修改了它),就会导致数据错乱。在简单实验中尽量避免共享变量,或者确保访问是原子的(对于单字节变量,在MIPS上通常是原子的)。
- 现场保护不完整:这是根本原因。ISR修改了某个寄存器(比如
6.3 问题三:在Logisim硬件模型中,中断无法打断当前指令
- 现象:按下中断按钮,CPU没有反应,继续执行完当前指令周期,甚至要等很多个周期后才响应。
- 排查思路:
- 中断检测时机不对:你的控制单元可能是在指令周期的开始检测中断。如果当前指令是多周期指令(比如5个周期),中断信号在第2个周期到来,它需要等到第5个周期结束后才被检测,响应延迟很大。正确做法:应该在每个时钟周期结束时(或者下个周期开始时)检测中断,并且判断逻辑要包含“当前是否正在处理中断”(即是否有中断标志位被锁定)。一旦检测到且允许响应,就在当前指令执行完毕的下一个时钟周期立即进入中断响应状态。
- 中断请求信号未锁存:外部中断按钮是一个脉冲,如果你的CPU只在脉冲为高电平的那个周期检测,很容易错过。必须用一个D触发器锁存这个请求,直到CPU响应并清除它为止。
6.4 深度思考:中断与函数调用的本质区别
做完实验,一定要想清楚这个问题。两者都会跳转和返回,但本质截然不同:
- 触发方式:函数调用是同步的,由程序中的
jal指令主动发起;中断是异步的,由外部事件随机触发,程序无法预知。 - 返回地址:函数调用由
jal指令将PC+4自动存入$ra寄存器;中断由硬件将PC(断点地址)保存到EPC这个特殊寄存器。 - 状态保存:函数调用遵循软件约定(调用者/被调用者保存寄存器),由编译器生成的代码负责;中断需要保存整个“机器状态”(PC、Status等),这部分由硬件自动完成关键部分,软件补充保存通用寄存器。
- 执行上下文:函数调用发生在同一特权级(如用户态);中断通常会引起特权级切换(从用户态切换到内核态),从而能访问更多特权指令和资源。
理解这些区别,你就能明白为什么操作系统内核、设备驱动都严重依赖中断机制,因为它提供了硬件事件主动通知CPU的能力,是实现并发、实时响应的基础。
这个实验的终点,绝不是仅仅让一个LED灯因按键而闪烁。它的价值在于,当你日后在复杂的RTOS或Linux驱动中编写中断处理程序时,眼前能清晰地浮现出PC跳转、堆栈变化、寄存器保存恢复这一连串精细入微的硬件舞蹈。你知道了local_irq_save()和local_irq_restore()背后在保护什么,知道了中断上下文为什么不能睡眠,知道了底层的硬件机制如何支撑起上层的软件抽象。这才是“计算机组成原理”实验该有的味道——打通软硬件的任督二脉。