ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Logisim实现带中断的单总线CPU:从数据通路到微程序控制器全攻略

用Logisim实现带中断的单总线CPU:从数据通路到微程序控制器全攻略 如果你是在计算机组成原理课上被布置了“用Logisim实现单总线CPU”这类实验大概率会经历一段疯狂的debug日子明明每个模块单独测都正常连起来一跑却满屏红叉微程序改了一版又一版程序就是跳不到该去的地址好不容易跑通常见指令一加中断又立刻翻车。这篇文章把我折腾了完整两周的“带中断的单总线结构CPU”从零复盘一遍把Logisim里的器件选型、数据通路连接、微程序控制器设计、中断响应链路和踩坑点一次讲透。它能帮你同时啃下三块硬骨头单总线数据通路怎么搭、微程序控制器怎么设计、中断机制怎么从请求到返回串成完整闭环。适合正在做课程设计的学生、准备复试的考研党、以及单纯想搞明白“CPU到底是怎么一步步思考”的自学者。我会尽量按实验推进的顺序来写而不是按教科书章节这样你上手时可以直接对着操作。1. 为什么这项设计值得做方案选型与整体思路1.1 Logisim在教学CPU设计上的优势先说选型。现在能用来设计CPU的工具其实不少 Quartus/Vivado配合FPGA开发板、Verilog仿真、甚至用Python写个模拟器都行。但课程设计用Logisim有一个独到的优势它把门电路、触发器、寄存器、ALU、RAM、ROM这些部件都做成了可视化组件你拉一条线、点一个引脚就能直观看到数据在总线上怎么流动、控制信号怎么起作用。你在纸上画数据通路图画得再熟也不如在Logisim里真正拖出一个CPU来跑一遍程序印象深刻。另一个好处是Logisim的调试成本极低。FPGA烧录一次要几分钟Logisim改完电路立刻就能跑配合探针Probe和LED你能实时看到每一根线上当前的值。很多学校在计算机组成原理实验里选Logisim正是因为它能把学习重心放在“CPU怎么工作”这件事本身而不是先花半个月学VHDL语法。如果你用的是Logisim 2.7.x或3.x版本下面的操作基本通用。提示Logisim有两个常见版本分支一个叫Logisim classic2.7.1一个是更新维护的Logisim Evolution。做CPU实验建议用Evolution版本它对子电路引脚排序、ROM/RAM初始化、自动布线的支持更好能少掉很多头发。1.2 单总线结构的特点与取舍单总线结构字面意思就是CPU内部所有部件——PC、寄存器堆、ALU、内存地址寄存器MAR、内存数据寄存器MDR——都挂到同一条数据总线上。同一时刻只允许一个部件往总线上发数其他部件只能从总线取数。这和现代CPU内部动辄上百条总线、多级流水线相比听起来“土”得不行但它有一个无法替代的价值结构极其清晰控制逻辑好写。我从实验中体会最深的是单总线结构让你把每个指令周期都拆成了非常机械的“微操作序列”。比如一条加法指令ADD R0, R1在单总线上大致要经历PC送地址寄存器MAR读内存取指令、IR接收指令、译码、R0送总线、R1送总线给ALU、结果暂存Z、再把Z写回R0。每一步哪个部件“说话”输出、哪个部件“听话”输入完全由控制信号决定本质上就是一个状态机在按节拍分发控制信号。它的缺点也一眼能看出来所有数据共享一条路所以任何一步都只能传一个数指令周期必然被拉长而且多个部件输出信号如果同时有效总线就会冲突——在Logisim里就是红色错误线这也是调试时最大的噩梦。后面我会专门讲怎么通过控制信号的互斥编码来避免。为什么课程设计通常选单总线而不是多总线因为多总线虽然速度快但控制逻辑复杂很多连线也翻倍。对学生来说单总线能把“CPU执行一条指令需要几步、每一步干什么”这件事看得明明白白这就够了。做实验的初衷从来不是追求性能而是搞懂原理。如果你想挑战更高难度完成单总线后再改成双总线或三总线那也是一条很自然的进阶路径。1.3 中断需求的拆解“中断”这个词从使用者的角度看就是CPU正在干手头的活突然来了个更重要的事CPU得停下当前工作处理完重要的事再回来接着干。放在我们设计的单总线CPU里需要拆成四个环节去看中断请求外部设备我这里是按键和定时器计数溢出产生一个电平或脉冲信号告诉CPU“有事要处理”。中断响应CPU在执行完当前指令后检查中断请求和中断允许标志如果都有效就响应中断。中断处理保存现场跳转到中断服务程序执行。中断返回恢复现场回到被中断的指令继续执行。这些环节在真实CPU里是由硬件自动完成的但在教学CPU里除了“检查请求”和“跳转”这两步由微程序控制器完成其余都可以写成普通程序。所以我建议把中断实现拆成两部分一部分是在微程序控制器里增加“中断响应微周期”另一部分是在指令系统里增加一条IRET中断返回指令中断服务程序本身用普通指令写就行。这里还有个容易想不明白的点中断什么时候能被响应我们不可能让CPU在任何微小步骤中都停下来响应那会把指令执行过程彻底打乱。所以约定俗成的做法是每条指令的全部微周期执行完后进入下一条指令的取指之前检查中断请求。也就是说中断响应的最小时机是“指令边界”。这个边界定了微程序控制器里加中断就变成一个简单的分支判断执行完一条指令后看有没有中断来有就转去中断响应微程序。2. 搭建前的准备存储器、寄存器堆与ALU连接细节2.1 存储器与CPU连接的核心问题在Logisim里做存储器和CPU连接最关键的认知是单总线CPU访问内存时地址和数据不能同时占用总线。很多第一次做的人会想当然直接把RAM的地址引脚和数据引脚都接到总线上结果一片混乱。正确的连接思路是把存储器拆成两层地址寄存器MAR和数据寄存器MDR。CPU要读内存时先把地址从总线送到MAR再给存储器发出读信号存储器把数据吐到MDR最后MDR再把数据送上总线CPU其他部件才能取走。写内存的过程反过来把数据写入MDR把地址写入MAR然后给写信号。用Logisim实现时我推荐用自带RAM组件RAM而不是ROM因为RAM既可以读又可以写也方便加载初始程序。RAM组件的“数据”引脚要分输入和输出两个口合并到单总线上必须加三态门Buffer带控制引脚那种。MAR用寄存器组件Register即可MDR也是寄存器但它接入总线的输出端要经过三态门否则它会把已有内容一直顶在总线上导致总线冲突。时序上有个细节ROM/RAM的读操作有延迟Logisim里虽然走的是“理想时序”但如果你在读指令的同一个时钟周期就立刻从MDR取数可能拿到的是旧值。稳妥的做法是给访存操作多留半个或一个时钟周期或者用触发器的时钟边沿来采样结果。后面微程序设计时我会专门讲这个问题。注意Logisim的RAM组件默认是异步读、同步写。异步读的意思是只要地址有效输出马上跟着变同步写则是要等时钟边沿到来才写入。这意味着读操作不能在一个时钟节拍内“瞬间完成再立刻使用”写操作倒是可以依赖边沿。理解这一点你就明白为什么微程序里读内存时通常需要两个微周期。2.2 寄存器堆与ALU的挂载方法寄存器堆是CPU的通用存储区我用Logisim的Register组件做了一组寄存器R0~R7。它们的输入引脚统一接在总线上但谁从总线取数由各自的“使能写”也就是寄存器本身的时钟加持控制。这里有一个非常容易踩的坑如果R0~R7每个寄存器的时钟引脚都用同一个全局时钟那么每个时钟边沿它们都会无条件从总线取数这不是我们想要的。正确的做法是把“寄存器写使能”和时钟做一个与门。只有当前微指令要求“R0写”时R0的时钟引脚才得到上升沿。所以我建议每个寄存器的时钟输入接一个受控的时钟门CLK AND (寄存器写信号)。这样控制信号为1时寄存器在时钟边沿锁存总线数据为0时它的值保持不变。ALU的接入比寄存器堆麻烦一点。因为单总线上同一时刻只能有一个数据而ALU需要两个操作数所以必须在ALU前面加两个暂存器A和B。执行运算时先通过两条微指令把两个操作数分别送进A和B然后再执行运算结果存在暂存器Z里最后再把Z写回目标寄存器或内存。这样一来ALU虽然做的是组合逻辑但在使用上被隔开了不会因为同时要从总线上拿两个数而出错。我在实验里用的是Logisim自带的ALU组件它有加减法、与或非、比较等运算模式通过控制端选择。你也可以用门电路自己搭一个超简ALU但一般没必要课程设计重点不在ALU内部电路直接用封装组件能省大量时间把精力放在数据通路和控制逻辑上。2.3 指令集与指令周期的初步设计在开始画控制逻辑之前必须先明确自己实现哪些指令。我最终的指令集很小但覆盖了访存、运算、跳转和中断返回取值和操作LDA addr从内存地址addr取数到ACC我用了累加器模型简化了寄存器堆逻辑STA addr把ACC的值存入内存地址addrADD addr把内存addr的值和ACC相加结果存回ACCJMP addr无条件跳转到addrJZ addr如果ACC为零则跳转JNZ addrACC不为零则跳转NOP空操作IRET中断返回用累加器模型可以少写“双操作数寄存器指令”那些复杂寻址但寄存器堆还是要保留我让ACC实际上就是寄存器堆中的R0这样写起来简单又不失通用性。每一条指令的“指令周期”都要拆成取指、译码、执行三个阶段。取指阶段对所有指令都一样PC送MAR、读内存、IR接收、PC加1。译码阶段由微程序控制器根据IR的操作码字段决定走哪条执行微程序。执行阶段才因指令而异。有了这个共同框架后面设计微程序时就有了“公共取指微周期”“各指令分派微周期”的结构也是中断检查和响应最好的挂载点。3. 核心实现单总线数据通路与微程序控制器3.1 单总线数据通路信号规划数据通路的核心是把上一节提到的所有部件都挂到总线上每个“输出到总线”的口都要经过一个三态门每个“从总线接收”的口都要有写使能。我把自己设计的信号分成“输出类”和“输入类”两组方便后续编码信号类型作用PCout输出PC值送总线MDRout输出MDR值送总线Zout输出ALU结果暂存器Z送总线ACCout输出累加器R0值送总线存储器数据输出输出RAM读数据送总线实际经MDRPCin输入总线值装入PCMARin输入总线值装入MARMDRin输入总线值装入MDRIRin输入总线值装入IRACCin输入总线值装入累加器R0Ain / Bin输入总线值装入ALU暂存器A/BZin输入ALU结果写入Z存储写控制RAM写使能存储读控制RAM读使能可省RAM常开ALUop控制选择ALU运算模式这个表看着多但编微指令时它们会被压缩成一个控制字段。你需要特别留意那些“输出类”信号因为它们是互斥的任何时刻最多只能有一个为1。如果两个信号同时为1总线上的数据就会打架Logisim会直接显示红色错误线。我在实验里专门用了一个译码器来约束输出信号把输出类信号编码成3位二进制经过3-8译码器后信号线恰好只有一条有效从硬件上杜绝冲突。这就是所谓的“互斥编码”强烈建议你也这么做能少排查很多怪问题。输入类信号则不需要互斥因为同一时刻可以有多个人同时从总线取同一个数。比如一条指令既要修改PC又要写IR那PCin和IRin可以在同一微周期同时为1。3.2 微指令格式与微程序控制器设计控制器的方案有硬布线和微程序两种。硬布线用状态机和组合逻辑生成控制信号速度快但改逻辑很麻烦微程序把控制信号编码成一条条“微指令”存放在ROM中运行时按顺序读取微指令来驱动数据通路。我选择微程序理由很实际单总线模型机的控制逻辑本质就是一堆按顺序打出的电位组合用ROM表达最自然改一条微指令等于改一个ROM地址单元比改一大片门电路省事太多。我设计的微指令格式分成三段下地址字段7位指定下一条要执行的微指令地址。判别测试字段3位决定下地址是需要条件分支还是直接使用下地址字段。判别条件包括IR操作码、零标志Z、中断请求INT与IE。控制字段约24位每个位或每个编码组对应一个控制信号控制方法就是前面设计的那组信号。控制字段可以采用“每位一个信号”的直译法直观但ROM很宽也可以采用“组内编码”法把同类信号编码后经译码器展开。我最终采用的是混合方式输出类信号用译码器互斥压缩输入类信号用直译法这样ROM宽度合适又不会让电路太绕。微程序入口如何分配这是很多人的疑惑点。我给每个指令分配了独立的微程序起始地址并用一个“指令译码器”根据IR操作码产生对应的分支地址。整个微程序存储器地址规划大致是微地址存放内容0x00取指微周期公共部分0x01中断判别与响应入口0x02~0x09LDA、STA、ADD、JMP、JZ、JNZ、NOP、IRET指令微程序0x10其他扩展或常数表这样安排的好处是在取指微周期结束时微程序控制器先做一次判别测试如果此时有中断请求且中断允许就跳去0x01否则根据IR的op码跳转到该指令的微程序入口。3.3 微程序控制器的实际连接微程序控制器在Logisim里由三块构成微地址寄存器uPC用计数器或寄存器、控制存储器ROM、微指令寄存器uIR用寄存器。工作过程一句话就能概括uPC给出当前微地址ROM输出对应微指令微指令一方面驱动数据通路完成微操作另一方面又决定下一条微指令的地址在时钟边沿更新uPC。条件判别测试是微程序控制器里最容易画错的部分。我的做法是ROM输出的“判别测试”字段先进入一个“测试逻辑”模块它根据判别条件生成一个结果位。然后这个结果位控制一个多路选择器MUX从“当前微地址1”和“ROM下地址字段”这两个候选中选择下一个微地址。如果条件成立就跳转到指定微地址条件不成立就顺序执行。具体到中断的实现我在测试逻辑里加了一个条件判别测试字段如果等于“测试中断”那么当INT1且IE1时MUX选择“中断响应微程序入口地址”。这一个判断就是整个中断机制在控制器层面的核心落点。其余的中断处理逻辑全都放在中断响应微程序里。4. 中断机制设计从请求到返回的完整链路4.1 中断请求的产生与采样在Logisim里模拟中断请求我用两种方式一个按钮作为外部按键中断源一个计数器溢出作为定时器中断源。按钮简单按下去产生一个高电平计数器用一个Logisim自带的Counter组件让它不断计数溢出时产生一个高电平脉冲。但CPU只在指令边界检查中断而请求可能在任何时刻到来。如果请求是一个短脉冲CPU采样时可能已经消失了所以要加一个“中断请求锁存器”。我用一个带置位和复位端的SR触发器请求到来时把它置1CPU响应中断后通过控制信号把它清0。这就像实际系统中的中断控制器把外部脉冲锁存成稳定的电平信号等待CPU“认领”。我还加了一个中断允许标志IE用一个普通D触发器实现。程序可以通过指令比如设计一条EI/DI指令或者直接在IRET里恢复来控制IE的值。没有IE的话中断服务程序内部还没处理完就被同一中断反复打断就变成了无限递归程序完全失控。4.2 中断响应流程的实现中断响应是整个设计里最精彩也最容易出错的部分。我的CPU执行完每一条指令后在下一个取指微周期的判别测试点检查INT IE若成立就进入中断响应微程序。它的核心任务有三件第一把当前的PC值保存起来也就是“压栈”。我设计了一个栈指针寄存器SP初始指向栈底。压栈的微指令序列大致是SP送总线MARin栈顶地址作为内存地址PC送总线MDRin当前断点值准备写内存发出存储器写信号同时SP加1。注意压栈的地址是写“当前SP指向的位置”写完后再让SP加1。这个顺序要严格保持否则返回时就找不准地址。第二清中断请求锁存器防止同一中断返回后又被立刻触发。同时把IE清零实现“关中断”保证中断服务程序执行期间不会被新的中断打扰。第三把中断服务程序的入口地址送入PC。最简单的做法是约定一个固定入口地址比如0x80中断响应微程序直接执行“0x80送总线PCin”然后跳转到普通取指微周期开始执行中断服务程序。如果希望支持多个中断源可以做成“向量中断”根据请求编号查一个中断向量表把表项内容装入PC。Logisim里实现向量中断也不复杂就是把入口地址的选择改成由请求编号决定。4.3 中断服务程序与IRET指令设计中断服务程序本身是用我前面定义的指令写的一段普通程序比如把R1的值加1存入某个内存统计变量然后执行IRET返回。但从被中断的主程序角度看这个服务程序可能会修改R1所以服务程序开头要把用到的寄存器先保存到栈里返回前再恢复。这就是“现场保护”和“现场恢复”在真实系统里同样由软件完成或者由硬件自动保存到栈中。IRET指令的微程序是整个链路的下半场从栈中弹出PC值SP先减1然后读内存装入PC同时恢复IE。IRET执行完后PC已经回到主程序被中断的那条指令CPU继续运行仿佛什么都没发生过。我强烈建议你把整个中断过程先写在纸上主程序的PC值、SP的变化、栈中数据的变化一步一步推演一遍。很多人直接在Logisim里连接电路中断一触发就乱套根本原因是脑子里没有完整的“栈变化图”。纸上推演顺畅了再回来对照电路查问题效率会高得多。5. 联调、故障排查与优化实录5.1 Logisim里的调试利器Logisim调试电路我最常用的三个工具是探针Probe、LED和分频时钟。探针可以直接显示一条线上的当前数值比对着波形图脑补强太多了LED适合挂在控制信号上一眼就能看出当前微指令让哪个部件工作分频时钟则用来控制运行速度。调试微程序控制器时建议把时钟放到最慢档或者干脆用手动时钟在Logisim里最多只能步进到时钟单位我一般是把时钟调慢到1Hz。每按一下时钟就产生一个时钟边沿对应一个微周期然后观察每条控制信号的变化是否符合预期。配合探针观察总线上的值你几乎是“逐条微指令”地看着CPU在执行出错时能立刻定位到是哪一步的哪个信号不对。还有个小技巧微程序ROM可以导出/导入文件。每次修改微程序不用重新在Logisim界面里一个个填ROM单元格直接编辑文本文件再加载即可调试效率提升不是一点半点。提示如果某条线变成了红色99%是总线冲突。别急着把所有连接都拆了重来先用探针逐段确认哪几个输出同时为1。用互斥编码处理输出信号后这类问题会大幅减少但偶尔还是会有边缘情况比如RAM三态门控制信号接反。5.2 常见故障与排查方法我把实验里踩过的坑和给同学答疑时遇到的共性问题整理成了一张速查表照着排查能省很多时间现象可能原因解决办法总线出现红色X两个及以上输出信号同时有效检查输出类控制信号是否互斥用译码器约束PC每步都乱跳PC的时钟没有受控PC的时钟引脚也要加门控不是直接接全局时钟读内存拿到旧值读操作时序不够增加读内存微周期的等待或改用寄存器缓存采样程序跑起来全错微程序入口地址分配错确认指令译码器的输出和微程序ROM地址对应中断响应后主程序回不来PC压栈/弹栈顺序反了核对SP先加还是后加IRET里SP先减还是后减中断反复进入服务程序中断请求锁存器没清或IE没关响应后清请求并清零IEIRET里恢复IE栈顶数据被覆盖SP初始值和栈区重叠栈区设在程序/数据区之上的空余RAM区域其中最隐蔽的问题是“读内存拿旧值”。Logisim的RAM异步读从MAR地址稳定到RAM输出稳定虽然模拟环境中几乎零延迟但如果你紧接着就在下一个微周期里把MDR送总线MDR里的值可能还是上次的残留数据。我在MDR的输入侧加了一个“采样沿”逻辑让存储读信号有效后稍等一个微周期再锁存MDR就彻底解决了。5.3 轻量优化把指令周期缩短单总线结构本身慢但同一条指令在微程序层面仍然有优化空间。一个最简单的优化是把“PC加1”这个操作和“读内存”放到同一个微周期完成因为PC加1并不占用总线它可以在PC送MAR的同时进行。我在设计时用了一个带独立计数使能的PC计数器取指周期从三条微指令压缩到两条。另一个优化是合并相同的执行序列。不同指令在结束前往往都要“把结果写回某个地方”可以把这些公共微周期做成公共子程序一样的“微子程序”通过微程序子程序调用机制复用。不过这个对课程设计来说属于加分项逻辑复杂度会上升不少建议先把基础版本跑通再考虑。6. 从单总线到更远扩展方向与个人复盘6.1 中断能力的扩展空间我做的这个版本是单级中断即任何时刻只响应一个中断服务程序期间屏蔽所有新中断。如果想做多级中断需要给每个中断源分配优先级支持中断嵌套也就是高优先级中断可以打断低优先级中断服务程序。这需要在IE之外增加一个“当前中断级别”的寄存器并且IRET要能恢复上一级中断级别复杂度会上一个台阶但基本原理和单级中断完全一致。另一个可扩展方向是增加中断向量表。我目前用固定入口地址如果改成向量中断中断请求编号可以作为查表地址从一张存放各中断服务程序入口的表中读取真正的入口地址这更贴近真实的CPU实现。这个改动本质上就是加一个MUX或查找表逻辑并不复杂。6.2 从单总线到多周期与流水线如果你的实验进度允许完成单总线CPU后可以尝试两条进阶路线一是改成多周期CPU把一条指令的执行步骤用独立状态机控制每个周期只做一步这比单总线更接近真实CPU二是探索流水线把取指、译码、执行重叠起来这会遇到数据冒险和控制冒险那又是一层全新的境界。但无论走哪条路单总线实验给你建立的“指令周期多个微周期控制信号序列”的思维框架都是最重要的地基。6.3 我在实验中的几点亲测体会做完这个设计我心里最深的体会是中断不是高不可攀的玄学它不过是一段特殊的微程序加上一个聪明的分支判断。困难不在于理解原理而在于把原理转换成微指令序列再把微指令序列落到ROM数据里。这个过程没有捷径只能画表、推演、连接、测试循环往复。调试时序问题的时候我的做法是在纸上画出每个部件的控制信号脉冲波形和时钟边沿对齐关系再对照Logisim实际运行结果检查。有一次程序总是漏执行一条指令我把纸上的波形画出来后才发现IRin信号比数据稳定的时刻早了半个节拍IR锁存了一瞬间的无效数据。这种问题光看电路根本看不出必须借助时序图。最后分享一个实用的杂项小技巧主程序里放一个死循环等待中断比让程序反复执行加法更容易观察中断效果栈区地址不要和程序代码区挨得太近否则压栈数据可能覆盖尚未执行到的指令Logisim的RAM可以通过“加载映像”直接导入十六进制文件我把汇编器生成的机器码经常用脚本转成Hex再导入省去手动填数据的痛苦。做完这个带中断的单总线CPU再回过头看课本上关于中断响应、现场保护、中断返回的描述感觉完全不一样了。你不再是在背概念而是在看自己亲手搭出来的电路怎么把这些概念一步步落实成控制信号和微指令。这份“看穿底层”的感觉才是这门课真正值钱的地方。
返回列表