LC-3键盘中断实验:从轮询到事件驱动的CPU效率革命

发布时间:2026/8/1 16:21:21
LC-3键盘中断实验:从轮询到事件驱动的CPU效率革命 1. 项目概述从“轮询”到“中断”的思维跃迁搞过单片机或者写过底层代码的朋友对“中断”这个词一定不陌生。但很多初学者包括当年刚接触计算机体系结构的我都容易陷入一个误区把中断理解成一个孤立的、神秘的“功能”。实际上中断机制是现代计算机系统设计的基石之一它彻底改变了CPU与外部设备交互的方式。这次我们以经典的LC-3教学计算机为平台动手实现一个键盘中断实验目的不是简单地让一个灯闪烁或者打印一行字而是要亲手搭建起从硬件信号触发到CPU响应再到软件处理这一整套流程深刻理解“事件驱动”是如何取代低效的“忙等待”从而释放CPU算力的。想象一下在没有中断的系统中CPU如果想知道键盘有没有被按下它只能不停地去问“你按了吗你按了吗你按了吗”——这就是轮询Polling。CPU宝贵的计算周期大量浪费在这种无意义的询问上。而中断机制相当于给键盘装了一个门铃。CPU可以安心处理自己的任务主程序当键盘被按下事件发生门铃响起中断请求CPU暂时停下手中的活去门口中断服务程序处理一下快递读取按键值然后回来继续刚才的工作。这个“按下暂停键-处理紧急事务-恢复现场”的过程就是中断处理的核心。LC-3架构清晰指令集精简是理解这一过程的绝佳模型。通过这个实验你将能回答中断向量是什么PSR程序状态寄存器中的优先级字段如何工作中断服务程序为何要首先保存R7这些知识是通往操作系统、实时系统等更高级领域的必经之路。2. 实验核心原理与LC-3中断机制拆解2.1 LC-3中断硬件基础从信号到向量LC-3的中断机制依赖于一套清晰的硬件协作。首先外部设备如键盘有一个独立的“中断使能”位例如键盘状态寄存器的最高位。当这个位被软件设置为1且设备就绪如键盘缓冲区有数据时设备会向CPU的“中断控制逻辑”发出一个高电平的中断请求IRQ信号。CPU在每个指令周期的末尾都会检查是否有中断请求到来。但CPU不会立即响应它要先进行一场“优先级仲裁”。LC-3的CPU有一个当前的优先级Privilege Mode存储在PSR寄存器的第10-8位。同时发出请求的设备自身也有一个硬件优先级例如键盘可能优先级较高定时器优先级中等。只有当外部设备的优先级高于CPU当前优先级时这个中断请求才会被CPU受理否则会被暂时挂起。一旦中断被受理硬件会自动执行一系列“幕后工作”保存状态将当前PC程序计数器的值存入一个特殊的寄存器——中断返回地址寄存器在LC-3仿真器中这通常是R7在中断时刻的一个隐式副本但概念上更准确的是硬件自动保存PC。提升权限将PSR中的优先级字段设置为中断设备的优先级。获取入口CPU根据一个预设的“中断向量表”来找到处理程序。LC-3的中断向量表起始地址是固定的例如0x0180。每个设备的中断类型对应一个“向量号”向量号乘以2因为每个入口是16位地址加上基地址就得到了该设备中断服务程序ISR的起始地址。例如键盘中断的向量号可能是0x80那么其ISR入口地址就是0x0180 (0x80 * 2) 0x0280。跳转执行CPU将计算得到的ISR入口地址加载到PC下一条指令就开始执行ISR的代码。注意这里有一个关键点常被混淆。硬件自动保存的是返回地址即被中断指令的下一条指令的地址。而通用寄存器R0-R7的内容需要由ISR软件来保存和恢复这是编写ISR时的首要任务。2.2 软件视角中断服务程序的设计契约硬件完成了“敲门”和“引导”剩下的“处理事务”就交给软件——中断服务程序。编写一个正确的ISR必须遵守与硬件之间的“契约”现场保存PrologueISR的第一要务是保存它将要使用的所有寄存器的原始值到内存栈中。最重要的是R7因为LC-3的RET从子程序返回指令隐式使用R7作为返回地址。如果ISR中调用了其他子程序JSR或JSRRR7会被修改导致无法正确返回。因此标准的开头是ISR_START ST R0, SAVE_R0 ; 保存R0 ST R1, SAVE_R1 ; 保存R1 ... ; 保存其他需要使用的寄存器 ST R7, SAVE_R7 ; 必须保存R7你需要为每个寄存器在ISR数据区预留存储空间.BLKW 1。核心处理执行实际的中断处理工作。对于键盘中断就是读取键盘数据寄存器如内存映射地址0xFE02的值存储到程序定义的缓冲区或进行即时处理。现场恢复Epilogue在处理完成后必须按相反的顺序将寄存器的值从内存中恢复回来。LD R0, SAVE_R0 ; 恢复R0 LD R1, SAVE_R1 ; 恢复R1 ... LD R7, SAVE_R7 ; 恢复R7中断返回使用RTIReturn from Interrupt指令结束ISR。RTI指令会做两件事首先从栈中恢复之前的PSR从而恢复CPU优先级然后恢复PCCPU由此跳转回被中断的主程序继续执行。在LC-3的简单模型中有时也用RET但这要求R7已被正确恢复。2.3 键盘设备的内存映射I/O与中断使能LC-3通常采用内存映射I/O。键盘状态寄存器KBSR例如地址0xFE00和键盘数据寄存器KBDR例如地址0xFE02被映射到特定的内存地址。KBSR最高位第15位是“就绪位”Ready Bit。当键盘有按键输入时硬件会自动将此位置1。第14位是“中断使能位”Interrupt Enable Bit由软件控制。只有当就绪位和中断使能位同时为1时键盘才会向CPU发出中断请求。KBDR低8位第7-0位存储按键的ASCII码。因此初始化键盘中断的软件步骤是将KBSR的中断使能位置1。这通常通过向0xFE00写入一个最高两位为1的值来实现例如0x8000用于同时置位就绪位和使能位但更安全的做法是只操作使能位不过LC-3的简单模型常直接写0x8000。主程序进入一个循环或处理其他任务。当按键按下硬件置位KBSR就绪位因使能位已开故发出中断。CPU响应跳转到ISR。在ISR中读取KBDR0xFE02以获取按键值读取KBDR这个动作本身会由硬件自动将KBSR的就绪位清零为下一次按键中断做好准备。3. 实验详细步骤与代码实现剖析3.1 实验环境与初始化代码假设我们使用LC-3仿真器如LC-3 Edit或基于Web的仿真器。实验目标是编写一个主程序使能键盘中断后在一个循环中持续递增一个计数器并显示模拟CPU正在工作。当任何按键被按下时中断发生ISR读取按键字符并将其存储到一个缓冲区中同时在屏幕某个固定位置显示该字符然后返回主程序。主程序的计数器递增不应被中断破坏。步骤一主程序框架与中断初始化.ORIG x3000 ; 主程序起始地址 MAIN LEA R6, STACK_TOP ; 初始化栈指针R6为ISR保存现场做准备 ; 初始化键盘中断将KBSR的中断使能位置1 LD R0, KBSR_ADDR LD R1, KBSR_MASK ; R1 0x8000 (使能位就绪位掩码) STR R1, R0, #0 ; 写入KBSR开启中断 ; 初始化计数器和其他主程序变量 AND R2, R2, #0 ; R2作为计数器清零 MAIN_LOOP ADD R2, R2, #1 ; 计数器递增模拟主程序工作 ; 这里可以添加将R2显示到屏幕某处的代码例如转换为ASCII输出到显示器 BRnzp MAIN_LOOP ; 无限循环等待中断 ; 数据区 STACK_BASE .BLKW #100 ; 分配栈空间 STACK_TOP .FILL x0000 ; 栈顶初始位置实际会指向STACK_BASE100 KBSR_ADDR .FILL xFE00 ; 键盘状态寄存器地址 KBSR_MASK .FILL x8000 ; 中断使能掩码 BUFFER_PTR .FILL BUFFER ; 缓冲区指针 BUFFER .BLKW #10 ; 预留10个字的键盘缓冲区 .END实操心得栈的初始化至关重要。虽然LC-3的中断机制不一定强制使用栈但使用栈来保存/恢复寄存器是最规范、最安全的方式尤其适用于嵌套中断或ISR中调用子程序的情况。将栈指针R6初始化为一个安全的内存区域是编写健壮程序的第一步。3.2 中断服务程序ISR完整实现接下来在另一个代码段通常起始于中断向量指向的地址如x0280编写ISR。.ORIG x0280 ; 假设键盘中断向量指向此地址 KEYBOARD_ISR ; 1. 保存现场 ADD R6, R6, #-1 ; 栈指针下移预留空间 STR R0, R6, #0 ; 将R0压栈 ADD R6, R6, #-1 STR R1, R6, #0 ; 将R1压栈 ; ... 保存其他可能使用的寄存器如R2, R3, R4 ADD R6, R6, #-1 STR R7, R6, #0 ; !!! 必须保存R7 !!! ; 2. 中断处理核心 LDI R0, KBDR_ADDR ; 读取键盘数据寄存器自动清除KBSR就绪位 ; R0现在存放按键的ASCII码 ; 将按键存入缓冲区 LD R1, BUFFER_PTR_ISR ; 获取主程序定义的缓冲区指针 STR R0, R1, #0 ; 存储字符 ADD R1, R1, #1 ; 指针后移 ST R1, BUFFER_PTR_ISR ; 更新指针此处未处理缓冲区溢出 ; 在屏幕特定位置显示字符例如通过TRAP x21 OUT陷阱程序 ; 为了不破坏R0可以先保存到其他寄存器或直接使用OUT ; TRAP x21 (OUT) 会使用R0但我们已经保存了R0所以可以安全使用 ; OUT指令需要将字符放在R0且会修改R0但此时R0的值已使用完毕 TRAP x21 ; 调用OUT显示刚按下的字符 ; 3. 恢复现场 LDR R7, R6, #0 ; 恢复R7 ADD R6, R6, #1 ; ... 恢复其他寄存器 LDR R1, R6, #0 ADD R6, R6, #1 LDR R0, R6, #0 ADD R6, R6, #1 ; 4. 中断返回 RTI ; 关键恢复PSR和PC返回主程序 ; ISR私有数据区或使用主程序共享数据 KBDR_ADDR .FILL xFE02 BUFFER_PTR_ISR .FILL BUFFER ; 指向主程序缓冲区的指针 .END关键点解析LDI R0, KBDR_ADDR这条指令是中断处理的核心。LDI是“间接加载”它从KBDR_ADDR存放的地址即xFE02中读取数据。这个读取操作会触发硬件逻辑自动将KBSR的就绪位清零。如果使用LD指令直接加载xFE02的值在某些模型下可能不会自动清零就绪位导致中断持续触发陷入死循环。栈操作对称性保存和恢复寄存器的顺序必须严格相反后进先出LIFO。这是使用栈的基本原则任何错位都会导致程序状态混乱。RTIvsRET在完整的LC-3中断模型中必须使用RTI。RTI从系统栈或特定寄存器恢复中断前的处理器状态包括优先级。而RET仅从R7跳转不处理PSR。在简单的教学实验中如果中断不涉及优先级切换有时用RET也能工作但这是一种不规范的取巧行为应养成使用RTI的习惯。3.3 主程序与ISR的协作与数据共享主程序中的计数器R2在中断发生时其值必须被ISR保护。在我们的ISR中我们保存了R0和R1但没有保存R2。这意味着如果ISR使用了R2就会破坏主程序的状态。因此在ISR中我们只使用了R0和R1并妥善保存/恢复了它们。这是一种设计选择ISR应尽可能使用最少的寄存器并对它们全部进行保存/恢复。对于共享数据BUFFER和BUFFER_PTR主程序和ISR都能访问。这里存在潜在的“竞态条件”Race Condition虽然在这个单次按键的简单例子中不明显。例如如果主程序正在读取缓冲区指针同时发生中断ISR修改了指针主程序可能读到错误的值。在复杂的系统中这需要通过关中断、信号量等机制保护但在基础LC-3实验中我们通常假设中断处理非常快且主程序在访问共享数据时是安全的。4. 调试技巧与常见问题实录4.1 典型问题与排查清单在LC-3仿真器中调试中断程序经常会遇到一些令人困惑的现象。下面是一个快速排查表现象可能原因排查步骤与解决方案按下键盘程序毫无反应1. 键盘中断未使能。2. ISR入口地址设置错误。3. 仿真器键盘输入未连接或设置问题。1. 检查主程序是否成功向KBSR写入了0x8000。单步执行查看xFE00内存值是否变为0x8000。2. 确认键盘中断向量号如0x80和ISR的.ORIG地址如x0280计算是否正确。3. 在仿真器中确认键盘输入设备已启用并尝试在非中断模式下用TRAP x20GETC测试键盘是否正常。程序进入中断后死循环无法返回1. ISR中未正确保存/恢复R7。2. 使用了RET而不是RTI但PSR状态未恢复。3. ISR中修改了栈指针R6后未正确恢复。1.这是最常见错误仔细检查ISR开头是否将R7入栈结尾是否从栈中恢复R7。在LC-3中JSR/JSRR会修改R7如果ISR中调用了任何子程序或陷阱必须事先保存R7。2. 确保使用RTI指令结束ISR。检查仿真器是否支持完整的RTI语义。3. 单步调试ISR观察每条指令执行后R6和栈内存的变化确保压栈和出栈操作成对且平衡。第一次按键正常后续按键无反应1. ISR中读取KBDR的方式不正确未能自动清除KBSR就绪位。2. 中断处理完成后设备就绪位未被清除导致无法产生新的中断边沿。1.必须使用LDI指令读取KBDR。LDI通过内存映射机制会触发硬件清除就绪位。直接使用LD加载xFE02可能不会。2. 检查KBSR的值。在ISR执行后其就绪位第15位应被清零。如果仍是1说明清除机制未生效。主程序的寄存器值在中断返回后被改变ISR未保存/恢复所有它使用过的寄存器。遵循“最小破坏”原则在ISR开头将所有你计划使用的寄存器除了可能用作临时、可破坏的都压栈。在结尾按相反顺序弹出。如果主程序使用了R3而ISR也用了但没保存主程序的R3就会被覆盖。中断似乎随机发生或主程序显示乱码栈溢出或栈指针初始化错误破坏了其他数据或代码。确保为栈分配了足够大且独立的内存区域如.BLKW #50。初始化R6指向栈顶最高地址。在ISR中通过单步观察R6的值确保其始终在栈空间内移动。4.2 高级调试利用仿真器工具现代LC-3仿真器通常提供强大的调试功能中断状态查看查看PSR寄存器的优先级位确认CPU当前优先级。查看KBSR、KBDR等内存映射寄存器的实时值。单步执行Step Into/Over在中断发生时仿真器应能自动跳转到ISR并允许你单步调试。注意观察PC和R7的跳变。内存与寄存器监视设置对关键地址如栈区、缓冲区和寄存器R7 R6的监视观察其变化。断点在ISR入口x0280和RTI指令处设置断点可以清晰捕捉中断的进入和返回过程。4.3 从LC-3到现代体系结构的思考完成这个实验后你不应只停留在LC-3。可以思考其在现代系统中的映射中断向量表IVT- 操作系统中的中断描述符表IDT结构更复杂包含门描述符。简单的现场保存-上下文切换需要保存所有通用寄存器、段寄存器、控制寄存器等开销更大。单一的设备中断-中断嵌套、中断优先级分组、中断屏蔽管理更精细。轮询 vs 中断- 在极高实时性要求或极简系统中轮询忙等待可能比中断上下文切换的开销更小这是工程上的权衡。这个实验就像一把钥匙打开了理解计算机如何高效管理异步事件的大门。当你未来在STM32上用CubeMX配置一个按键中断或者在x86平台上编写一个设备驱动时底层逻辑是相通的配置使能、等待请求、保存现场、处理事务、恢复返回。理解了这个流程那些复杂的配置寄存器和驱动框架就不再是黑盒而是一个个可以推演和理解的逻辑模块。