ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Logisim实战:手把手设计带中断的单总线CPU

Logisim实战:手把手设计带中断的单总线CPU 用Logisim玩转CPU设计手把手教你实现带中断的单总线结构如果你在搜索引擎里敲过CPU是如何思考问题的大概率会得到一大堆取指、译码、执行之类的标准答案但真到自己动手用Logisim搭一个能跑指令的CPU时才会发现最大的门槛根本不是懂不懂原理而是这些信号到底该怎么连、什么时刻该让哪个器件开口说话。这篇博文要聊的就是这件事在单总线CPU的基础上把中断机制完整地加进去。先说清楚这篇内容的适用范围。它适合正在做计算机组成原理课程设计、正在刷Logisim实验平台题目、或者纯粹想搞懂处理器内核的同学。你会看到一条完整的设计路径先从单总线数据通路和微程序控制器入手再往上叠加中断请求锁存、中断允许控制、断点压栈、向量跳转、RETI返回这些机制最后跑通一个按键触发中断、数码管显示结果的完整Demo。整个过程全部基于Logisim的图形化电路实现每一个模块都能在工程里找到对应物。真正做过这个实验的人都知道带中断的单总线CPU是最后一公里级别的难点。单总线结构本身只有一条数据通路所有的数据搬运都要排队轮流使用这条总线而中断的特性却是随时可能插队。怎么在不破坏原有指令流程的前提下让外部请求在合适的时机打断当前程序保存完现场之后再完美地切回来这中间涉及的时序配合、微指令调度、总线占用仲裁才是这个实验真正的核心考点。1. 为什么是单总线一条闷声干大活的窄路1.1 单总线结构的基本形态单总线CPUSingle Bus CPU也有教材叫单总线数据通路在数字逻辑实验里几乎是标配。它的核心思路很朴素所有需要交换数据的部件统统挂到同一组线路上这组线路就是单总线。PC、MAR、寄存器堆、ALU、内存控制器它们的数据输入输出端口全部通过三态缓冲器Tristate Buffer接到这条总线上。为什么必须用三态缓冲器因为在数字电路里多个输出端直接接到同一根导线上如果两个器件同时输出不同的电平轻则逻辑混乱重则相当于把两个反方向的驱动源短接在一起。三态缓冲器提供了第三种状态——高阻态也就是断开状态。只有需要某个部件把数据放到总线上的时候才让它的三态控制端使能其他时刻全部保持高阻这样总线上的数据就只由当前唯一被使能的部件决定。这个设计思路在Logisim里的实现非常直观。总线上引出来的每个连接点用一个三个输入一个输出的三态门做隔离一端接器件的数据输出一端接总线第三端是控制端。控制端为1器件数据被推上总线控制端为0输出端呈现高阻态在Logisim里你会看到浮空的高阻值用蓝色/灰色表示。1.2 单总线结构的天然约束单总线的窄体现在两个地方。第一同一时刻总线上只能有一个数据源。如果控制信号设计失误同时使能了PC_OUT和ALU_OUT总线上的数据就是两个信号的叠加完全不可预测。第二任何一次数据传送都必须拆成输出到总线和从总线写入目标部件两步而这两步往往不能在同一时钟周期内完成因为总线上的数据需要稳定一段时间才能被写入端正确采样。所以你在Logisim里实现单总线CPU时一定会在波形上看到一种典型的三步走节奏第一步某个寄存器的输出使能信号拉高数据被放到总线上第二步等待一小段时间让总线电平稳定第三步目标部件的写使能信号拉高在时钟边沿把总线上的数据锁存进去。微程序控制器的作用就是把这三步编成一条条微指令按节拍依次执行。1.3 带中断为什么难单总线结构的CPU本身已经是一个完整的有限状态机每一拍做什么都由微程序控制器决定。现在要加入中断等于在正常流水线之外突然多出一个外部事件请求的输入。这个输入的特点是异步、随机、随时可能发生。但CPU的内部逻辑是同步的、节拍化的。要处理好这个矛盾必须有三个机制请求锁存机制把异步的脉冲信号锁存成稳定的电平信号否则CPU在时钟边沿采样时会采到不确定的中间态。响应时序机制CPU只有在当前指令的最后一个微周期结束、下一个取指周期尚未开始的边界时刻才去检查有没有中断请求。断点保存与恢复机制响应中断前必须把当前的PC值也就是断点保存到堆栈里中断服务程序结束后再从堆栈恢复。这三条听起来都不难但把它们落到单总线的架构里你就会发现处处受制于只有一条总线这个约束。比如说压栈操作就必须依次完成SP减一、把SP值送到MAR、把PC输出到总线、内存写使能这一串微操作在单总线上至少要占好几个微周期。所以很多教材在讲中断时都喜欢把中断响应周期画成单独的状态让它插在取指周期前面就是这个原因。2. 数据通路先行让PC、寄存器堆和ALU在一个平面上工作2.1 核心模块与总线连接清单在我实际搭建的工程里单总线CPU的核心模块包括这些模块作用输出到总线的方式写入方式PC程序计数器给出下一条指令地址PC_OUT三态门PC_IN写信号从总线装载MAR内存地址寄存器提供访存地址MAR_OUT三态门MAR_IN写信号RAM指令/数据存储器RAM_OUT三态门RAM_IN写信号IR指令寄存器保存当前指令一般只送到指令译码器IR_IN写信号R0~R3通用寄存器堆每个寄存器独立三态门每个寄存器独立写信号TEMP_A / TEMP_BALU输入暂存器一般不直接上总线写信号ALU算术逻辑单元ALU_OUT三态门计算结果存储到目标寄存器SP堆栈指针中断后需要SP_OUT三态门SP_IN写信号中断向量寄存器保存中断服务程序入口INTV_OUT三态门由硬件或微指令写入在Logisim里用一根粗线把所有模块的数据端口连起来就形成了单总线。连线时我建议把总线画成红色或橙色标签统一命名为BUS[7:0]如果数据宽度8位。然后每个模块的数据输出端都通过一个三态门挂到BUS上控制端就是对应微指令位。所有模块的数据输入端直接连到BUS写入动作靠各自独立的写信号在触发沿完成。2.2 输出到总线与写入寄存器是两个动作很多第一次做实验的同学会在这卡住为什么一个简单的寄存器传值MOV R1, R2要拆成两步因为R2先要把数据放到总线上这需要R2_OUT使能总线数据稳定后R1_IN信号有效在时钟上升沿把总线数据锁存到R1。两步之间至少要隔一个节拍你不能让R2_OUT和R1_IN同时有效就期望R1立刻得到数据——如果你用的是电平触发写入那么在总线还没稳定的头几十纳秒里写入的就是垃圾值。这一块我特别推荐一个做法给所有写信号都用时钟边沿触发。也就是说寄存器的时钟输入端接统一的时钟信号写使能信号只是允许写入的标志真正把数据锁存进去的边沿是时钟的上升沿。这样只要保证总线数据早于时钟上升沿建立setup time就能可靠写入。Logisim的寄存器Register默认就是边沿触发这点很省心。2.3 ALU的暂存器是整个数据通路的节奏器单总线CPU的ALU设计有一个常规陷阱如果ALU的两个输入直接来自总线那么你必须在同一个时刻从总线上同时读取两个操作数这显然不可能因为总线一次只能有一个数据。于是需要在ALU前面加两个暂存器TEMP_A和TEMP_B先分两次把操作数从总线写入暂存器再让ALU完成计算最后把结果通过ALU_OUT送回总线。以一条加法指令ADD R1, R2为例微程序大致是这样使能R2_OUT写TEMP_A_IN把R2的值存入TEMP_A使能R1_OUT写TEMP_B_IN把R1的值存入TEMP_B等ALU内部完成加法组合逻辑输出稳定使能ALU_OUT写R1_IN把结果写回R1。你看单总线在一次运算中至少要经历四拍。这就是单总线CPU慢但清晰的特点。理解了这个节奏后面设计中断相关微指令会顺很多。2.4 作好每一个控制信号的名字我在自己工程里踩过最恶心的坑就是控制信号命名混乱。刚开始叫ALU_ADD、ALUSUM这类名字后来微指令一多完全分不清谁是谁。建议一开始就用器件_动作的格式统一命名PC_OUT、PC_IN、MAR_IN、RAM_OUT、RAM_IN、IR_IN、R0_IN、R1_OUT、TEMP_A_IN、TEMP_B_IN、ALU_OUT、SP_OUT、SP_IN等等。每个信号对应微指令字的一位这样微指令里的位序和控制信号表直接对应调试时一眼就能看出来微指令对不对。3. 微程序控制器给CPU装上会背剧本的大脑3.1 为什么选微程序而不是硬布线微程序控制器的本质是把每条指令需要哪些控制信号、按什么顺序发出预先存放在一个只读存储器ROM里。每个微周期从ROM中取出一个微指令字其中每一位控制一个内部信号。比如微指令字第0位是R0_OUT那么这条微指令输出的ROM内容这一位为1时R0的输出三态门就被打开。顺序执行时微程序计数器自动加一遇到条件分支时通过测试标志位决定下一条微指令地址。和硬布线控制器相比微程序控制器的最大好处是修改指令集不需要重新设计逻辑电路只要改ROM的内容。在Logisim里用ROM组件就可以实现导入一个内存镜像文件就完成了微代码更新。做中断扩展时我们只需要在ROM里多写几条微指令就能把整个响应流程搭建出来。3.2 微指令字的字段划分我用的是一个很经典的三段式结构。以我的一次实验为例微指令总宽度可以做到31位左右大致分为三块控制信号字段每一位直接对应前面的器件_动作信号。下址字段用于给出下一条微指令的地址在顺序不连续时使用。条件分支字段B_IF和分支类型决定是否根据某个测试条件跳转。举一个简化的微指令格式表实际实验里可以根据自己的控制信号数量增减位数字段位宽说明控制信号字段16~20位每位一个控制信号地址字段(NA)8位下一条微指令的下址条件字段3位000表示顺序执行001测试进位CF010测试零标志ZF011测试中断请求INTR100测试操作码译码结果中的某一位等分支模式2位决定当前地址是顺序加一、无条件跳转到NA、条件跳转到NA、还是调用子程序微程序计数器μPC可以是一个计数器组件。默认情况下每拍加一顺序读取微指令当条件字段和分支模式组合指示要跳转的时候通过多路选择器把μPC的输入切换为ROM输出的NA字段值。3.3 条件分支让中断请求能插话中断机制能在微程序层面成立靠的就是条件分支微指令。我们需要设计一种微指令它不做具体数据操作只做测试并跳转。典型写法是这样如果在取指周期的最后一个微指令中加入条件判断当INTR中断请求有效且中断允许EI为真时将微程序计数器的地址改为中断响应微程序的起始地址如果条件不成立则继续顺序执行下一地址也就是回到正常的取指周期起点。这样一来从微程序的角度看中断请求就像一个调用子程序的跳转条件。它跳转过去的目标微程序做一系列压栈、送向量、修改PC等操作最后通过一种类似RET的机制回到主程序取指周期。整个过程和调用子程序的微程序序列有惊人的相似性区别在于子程序调用是由指令发起的而中断响应是由外部信号发起的。3.4 ROM的内容怎么填Logisim的ROM组件可以加载十六进制机器码文件或如果我们的微指令字是32位每个地址存放一个微指令字。在实验阶段我强烈建议先用表格手工计算的方式把微程序的每个地址、内容、注释写全再转成十六进制文件导入。一个常用的流程是列出所有指令的微程序序列MOV、ADD、JMP、EI、DI、RETI等。给每条微指令分配地址并标注其下址字段。先安排固定的公共子程序如取指周期、中断响应周期再把各指令的微程序段依次排列。用Excel排布控制信号位生成二进制串再按高位到低位转成十六进制。在Logisim里导入ROM连接好微指令字各位到对应控制信号。这里的下址字段设计是整个微程序控制器最容易出bug的地方。很多同学会把顺序执行的也装满下址结果顺序逻辑和跳转逻辑混在一起最后微程序走得乱七八糟。我的建议是顺序执行时把分支模式设为加一并且不要管NA字段的值只有当确实需要跳转时才用条件分支模式并填写NA字段。这样调试时心智负担会小很多。4. 中断机制设计从打断程序这个本质出发4.1 中断的本质是现场切换抛开具体电路不谈中断的本质是CPU正在执行程序A外部事件请求CPU去处理程序B处理完再回到程序A刚才的下一条指令继续执行。用日常混类比就像你正写代码电话响了你先把正在敲的字符保个草稿去接电话挂断后再回来继续敲。这个保草稿在CPU里叫保存断点一般是把当前PC压栈。为什么要把PC压栈因为中断处理结束后CPU必须知道原程序刚才执行到哪儿。PC指向的地址就是断点。如果不保存中断服务程序哪怕什么都不做只要跳走再跳回来原来的程序也丢了。除了PC理论上通用寄存器、状态寄存器等现场信息也应该保存。真实CPU会保存一大堆东西。但在教学实验里为了方便很多实验只保存PC并且规定中断服务程序不允许修改它不使用的寄存器或者约定中断服务程序自己负责保存这些寄存器。这样设计能极大简化硬件同时不影响核心机制的理解。4.2 需要的硬件信号线要完整实现中断机制单总线CPU的微程序控制器至少要认识这几个信号IRQ中断请求输入外部设备发来的中断申请信号。在Logisim里可以用按键模拟输入0→1跳变表示一次中断申请。INTR中断请求锁存后的有效电平IRQ经过锁存器后变成稳定电平供微程序在每个取指边界采样。EI中断允许标志一个触发器EI1表示CPU允许响应中断。开中断指令EI置位关中断指令DI清零。它的存在非常关键因为程序中有时需要一段临界区比如正在修改堆栈指针时不允许被中断打扰。INTV中断向量号或中断服务程序入口地址CPU响应中断后需要知道跳到哪个地址去执行中断服务程序。最简单的方法是硬件提供一个8位向量比如手工设置的拨码开关直接装配到PC。4.3 响应时机的确定指令边界是唯一的合法打断点设计中断最重要的纪律就是不能在一条指令执行到一半的时候响应中断。为什么因为一条指令的微程序序列中每拍可能都在改写内部寄存器比如加法运算正要写到R1结果这时候中断来了微程序跳走了R1要么没写成、要么写了一半等回来时ALU的状态可能已经面目全非整个程序上下文就彻底撕裂了。所以在微程序层面中断检查点必须放在当前指令微程序的最后一条微指令和下一条指令取指周期的第一条微指令之间。实际做法有两种在每一条指令的末尾微指令中都包含一个测试INTR的条件分支字段符合条件就跳到中断响应微程序。或者更省事一点在取指周期的第一步做条件判断如果INTR有效则当前地址不经过正常的取指流程而直接跳转到中断响应微程序。两种方式在Logisim里都能实现。我推荐第二种因为取指周期的微程序是全局共用的改一处就能影响所有指令不用每条指令都单独加分支。缺点是取指周期第一步原本是先取指令现在被抢占为中断响应相当于中断响应优先于取指。这个优先级没有问题反而省掉了在每条指令末尾埋分支的麻烦。4.4 中断响应微程序的目标序列先不考虑细节单总线CPU上的中断响应微程序至少要做这几件脏活累活SP减一准备压栈堆栈向下增长把SP的值送入MAR让内存知道要写哪个地址把PC的值输出到总线同时内存写使能有效把断点存入栈顶把中断向量入口地址INTV送入PC清除中断请求锁存等待新的中断跳转到取指周期开始执行中断服务程序的第一条指令。注意第3步PC的值是在总线上的而此时的MAR已经是SP-1后的值。由于单总线只有一个数据源我们必须先让SP输出到MAR然后再让PC输出并写内存这两个操作不能在同一个微周期内同时让两个器件抢占总线。所以压栈在微程序里至少需要两到三个微周期。别嫌慢别嫌麻烦这就是单总线结构的代价。4.5 关中断与开中断EI/DI机制的设计真实CPU在响应中断后通常会自动关闭中断防止在保存断点的过程中又被新中断打断。在我们的设计里这个自动关闭可以用微程序的一条微指令直接对EI触发器清零来实现如果不做这一步就可能会出现两个中断嵌套的情况处理起来非常复杂。开中断和关中断也要对上指令比如设计两条指令EI指令执行后设置EI1CPU之后可以响应外部中断。DI指令执行后清零EI0CPU之后不响应外部中断但IRQ仍然会被锁存这很重要——如果IRQ没有被锁存的话关中断期间来的信号就白白丢了等再次开中断时CPU并不知道曾经有请求来过。这个设计的意义在于让中断请求等得起。即使当前CPU处于关中断状态外部按键按下的边沿也能被锁存器记住等到EI指令执行后再被CPU响应。可以说锁存器是中断请求的留言信箱。5. 中断与单总线的实际结合六个关键模块逐个落地5.1 中断请求锁存器IRQ_LATCH别让异步输入毁掉时序在Logisim里模拟外部中断最常用的办法是按键Button或者拨码开关。但按键输出的是人为电平抖动直接接到微程序控制器的INTR端大概率会出毛刺。原因是按键按下和时钟有效沿之间的关系完全随机有时候正好在时钟沿附近抖动导致微程序采样到一个不稳定的中间值。所以我强烈建议在IRQ输入端加一个D触发器锁存器。每次外部输入一个正脉冲按键按下产生的高电平D触发器锁存1微程序响应完中断后向锁存器发送一个清除信号CLR_IRQ把锁存器清零表示我已经处理过这个请求了。在Logisim里实现非常简单一个D触发器D端接高电平CLK端接按键信号异步清零端CLR接微指令的CLR_IRQ信号Q端输出就是INTR。按键按一下INTR变为1除非微程序显式清除否则它会一直维持高电平。这个设计既处理了抖动因为使用边沿触发又保证了请求不会丢失。5.2 中断允许触发器EI可关可开才能保护临界区EI触发器在Logisim里也是一个D触发器。它的时钟输入需要连接到特定的写信号EI_SET和EI_CLR。EI指令执行时微程序会让EI_SET信号有效把1写入EI触发器DI指令执行会让EI_CLR有效把0写入EI触发器。中断响应时也不会自动清零也可以设计成自动清零看你的课程要求但是有一点要注意响应中断后到RETI结束前响应微程序期间不能再响应新的中断否则刚压栈的断点可能被破坏。所以最稳妥的设计还是让中断响应微程序的第一拍就执行EI_CLEAR把EI清零等RETI时再恢复为1。5.3 中断向量入口INTV通过逻辑开关DIP Switch或ROM查找表给出。最简单可靠的方法是一组8位拨码开关把所有开关的输出接到总线的开关组上通过一个三态门受控输出。微程序在响应中断时先禁止其他器件占用总线然后使能INTV_OUT把开关值送到总线上再执行PC_IN写信号把向量值装入PC。这样CPU就能跳到中断服务程序起始地址执行。多个中断源呢如果希望区分多个中断源可以在锁存之后做优先级编码用一个优先级编码器把中断号转换成向量号。比如IRQ0优先级最高对应向量号0x10IRQ1对应向量号0x20。这个扩展不算复杂但会多出不少连线教学实验里一般做一个中断源就够了。5.4 SP与堆栈断点保存的核心在添加中断之前SP组件是可选的。添加中断后它就成了必需品。SP的设计也很朴素一个带加减使能的计数器寄存器。SP的值可以送到总线SP_OUT也可以从总线装载SP_IN。微程序需要执行SP-1操作时可以在微指令里单独留一个SP_DEC信号让SP在时钟边沿自行减一。堆栈方向我习惯用下增栈栈向下增长SP先减一后写数据。压栈序列如下SP_DEC有效此时SP自减一SP_OUT有效把SP送到MAR_INMAR得到栈顶地址接着PC_OUT有效、RAM_IN有效把PC的值写入以MAR为地址的栈单元。出栈序列是RETI的一部分大致反过来SP_OUT有效SP的值送到MAR_INRAM_OUT有效把栈顶数据送到总线PC_IN有效把栈顶数据装入PCSP_INC有效SP自增一。注意第2步RAM出栈RAM的数据输入输出如果都用同一组总线的话需要Tri-state控制好不能一边让RAM输出一边让其他器件输出。5.5 中断响应微程序的具体微指令顺序在我的工程里中断响应微程序的指令序列是这样的以8位数据宽度、PC和内存均为8位为例微地址有效控制信号说明0x80SP_DEC栈指针下移准备压栈0x81SP_OUT, MAR_IN栈指针送内存地址寄存器0x82PC_OUT, RAM_INPC写入栈顶断点保存0x83INTV_OUT, PC_IN, CLR_IRQ, EI_CLEAR向量入口装入PC清请求关中断0x84跳转到取指周期起始地址开始执行中断服务程序看起来只有5步但它同时完成了SP调整、断点保存、PC替换、请求清除和关中断5件事。每条微指令都能在微程序ROM里找到对应的行这就是微程序可编程控制逻辑的精髓。5.6 RETI指令从栈里找回原来的世界中断服务程序的最后一条指令应该是RETI中断返回。它的微程序也很短SP_OUT有效MAR_IN有效把当前栈顶地址交给内存RAM_OUT有效把栈顶数据也就是之前保存的断点PC值送到总线PC_IN有效把断点装入PCSP_INC有效SP回到压栈前的值EI_SET有效重新打开中断跳回取指周期主程序继续执行。这里有一个容易被忽略的点返回前后PC的值必须严格等于主程序中被打断的那条指令的下一条指令地址。如果你在非取指周期的某个边沿把IRQ打进去了而不是在指令边界响应那么压栈的PC可能是中间值返回时就可能从半条指令处开始执行后面一切就全乱了。所以再次强调响应时机比响应本身更重要。6. 指令集扩展与验证程序让中断被肉眼看见6.1 新增指令的机器码设计在原有指令集基础上我增加了几条和中断直接相关的指令助记符操作码示例功能EI0xE0置EI1允许中断DI0xE1清EI0禁止中断RETI0xE2从堆栈恢复PC开中断返回主程序HLT0xFF停机可选注意这里的EI和DI指令在指令译码后并不需要像普通运算指令那样走运算微程序而是各自对应一段极短的微程序EI对应SET_EI然后回到取指周期DI对应CLEAR_EI然后回到取指周期。这就是微程序扩展指令集的便利——在ROM里加两行就能实现。6.2 硬件Demo按键模拟外部中断LED显示结果为了让中断效果可视化我建议中断服务程序做的事情非常简单把一个全局变量比如R3加一然后立刻把这个值输出到LED显示。接法是这样的LED灯的输入接一个输出寄存器或者直接接R3的值主程序循环执行一个空转程序段数码管显示一个稳定值当外部按键按下IRQ锁存器捕获请求CPU在下一个指令边界中断响应进入服务程序把R3加一LED立刻变化执行RETI回到主程序。这样每次按键LED的数值都会加一中断机制是否生效一目了然。6.3 完整测试流程与预期结果初始化程序置R00R10执行EI指令进入主循环。主循环R0R1循环计算LED显示某个正常状态值可以用R0的值表示。按下一次按键IRQ请求产生CPU在取指边界检测到INTR1且EI1执行中断响应微程序压栈PC跳转至中断服务程序入口比如0x40。中断服务程序R3加一写LED执行RETI恢复PC重新开中断。再按一次R3再加一LED值更新。按5次LED应该从0变为5。如果LED没变或者主程序跑飞就说明中断某一环节出了问题后面我会讲怎么排查。6.4 一个容易被忽略的边界中断服务程序里的EI很多第一次做这个实验的同学会想在中断服务程序开始的地方先执行一次EI让CPU实现中断嵌套。但我建议在你还没完全吃透单总线时序之前千万别着急做嵌套。嵌套中断需要对每层现场都压栈保存而在只有SP没有通用栈帧寄存器组的演示系统里任何一个寄存器被外层中断修改内层程序再使用它时都会拿到错误的值。教学实验做到单级中断就够完美了真正理解多级中断中的优先级调度和栈帧恢复建议放到后面用更完整的CPU设计去实现。7. 从能跑到稳跑调试路上的经验和坑7.1 按键抖动能不能靠代码扛Logisim里的按钮在按下瞬间逻辑值可能变化多次。如果直接把按钮接到IRQ锁存器的CLK端一按可能锁存多次表现为中断服务程序被执行两次或多次。处理抖动的方法是加一个边沿检测消抖电路或者干脆用拨码开关替代按钮。拨码开关切换时虽然没有物理弹性抖动那么明显但有时候在总线上的毛刺也足够烦人。我试过一种比较稳的调试方案用拨码开关模拟中断请求并把开关值经过一个锁存器采样。这样CLK信号来自开关的稳定输出而不是按钮脉冲抖动影响大幅减小。最终版本可以用按钮加RC滤波加施密特触发器的方式来消抖原理都是一样的。7.2 压栈操作里先MAR后PC的顺序不能乱压栈时单总线必须先给MAR地址再让PC的数据上总线写RAM。如果顺序反了比如先PC上总线再把SP送到MAR那么因为MAR和PC都挂在同一条总线上MAR读到的可能是PC的值而不是SP的值栈地址就错了。这类bug在Logisim里极难肉眼发现因为微指令的速度太快波形图看着眼花缭乱。我的排查经验是把微程序ROM的地址也接到一个探针上逐拍记录ROM地址控制信号总线值三个变量的变化对照期望的微程序表逐行核对。只要有一行的信号组合不对目标地址立刻就能定位到。7.3 三态门悬空为什么有时候总线上飘出奇怪的值单总线CPU里每一拍都应该只有恰好一个三态门使能。但在微程序切换的瞬间可能出现上一拍的三态门关闭了下一拍的三态门还没打开的空窗期。这时总线悬空Logisim会显示一个不确定的浮空值紫色或灰色。如果紧接着的写信号在这时采样就会把垃圾值写进寄存器。解决方法有两个。第一在微指令设计中尽量让前一拍的数据源和后一拍的数据源保持部分重叠或者紧邻的时序关系比如先使能输出、下一个时钟再写目标。第二在总线上加一个总线保持器Bus Keeper或者上下拉电阻让总线在无人驱动时保持上一个值。Logisim里面可以用一个两端接地的电阻来模拟上拉或者直接用Constant组件拉一个默认值保证总线任何时候都有确定的逻辑值。这个方法虽然看起来有点土但能把很多莫名奇妙的bug直接消灭在源头上。7.4 用分步时钟和探针逐拍验证Logisim的时钟可以手动点击产生单脉冲这是最宝贵的调试武器。我习惯把所有时序都先跑在手动模式下先点一下时钟观察CPU执行到哪条微指令ROM地址探针再看总线值和所有写信号的状态。每一条微指令通过手动单步确认无误之后再切换到自动时钟跑完整程序。如果直接上自动时钟看起来好像偶尔能出结果但一旦出错就很难定位因为几百个周期过去了你根本不知道是哪一个周期出了问题。配合ROM地址探针还可以在怀疑的中断响应微程序起始地址附近直接用手动模式把地址设到那里再逐步推进观察SP值、MAR、PC、栈内存的变化。这个方法在追踪中断压栈、返回的过程中格外有效。7.5 一个容易被忽略的细节启动时SP初始化中断机制用上堆栈之后SP的初始值决定栈区开在哪里。如果SP一开始就是0压栈时SP先减一会变成255栈顶就是内存最高地址这在8位系统中是合法的但如果你把SP初始化到中间某个数据区压栈就可能覆盖程序存储区导致程序跑飞。所以在主程序最开始要有一条MOV SP, #XX的指令把SP设到内存最高地址附近比如0xFF。这也是为什么我之前说即便只是做中断实验也得先保证MOV指令甚至MVI指令的微程序是正确的。7.6 调试中断响应微程序一定要先验证能跳进去中断链条里最容易出问题的是从取指周期跳转到中断响应微程序的地址这一步。这种跳转依赖条件分支微指令的地址生成逻辑。我的建议是先把IRQ锁存器输出直接接成高电平看CPU是否会在取指边界跳转到中断响应微程序地址。如果跳不到问题在条件分支逻辑或者ROM地址没有正确放到多路选择器上如果能跳到再把IRQ恢复成按键控制继续排查后续压栈和跳转问题。把一个长链路拆成两段来调比一口气调试整个链路要高效得多。7.7 Logisim的黄色警告线看到就立刻停下Logisim里输出端颜色不是绿色时就代表逻辑值不确定、浮空或错误。当我把所有总线连接完、开始跑的时候黄色或紫色的线意味着某处有两个输出在打架或者某个输出悬空了。这种问题不能拖拖到后面整个电路都会逻辑错乱。常见原因有两个三态门控制信号写反了0为输出、1为高阻或者某根线两端同时接了普通输出。遇到黄线第一时间顺着线的两端检查三态门控制端和器件配置。7.8 关于能不能再大角度扩展的看法带中断的单总线CPU做完之后很多同学会想继续往前冲比如加上多级中断、加上通用寄存器自动保存、甚至改成流水线。我的看法是单总线结构本身已经非常接近教学极限它最适合的定位就是理解控制器的核心逻辑。继续在单总线上堆功能电路复杂度会飞速上升收益却不高。如果你是课程设计需要往深了写可以考虑的方向是把EI/DI扩展到可屏蔽中断与不可屏蔽中断的双请求模型或者增加一个简单的优先级编码器支持2~4个中断源。这些扩展能让报告有深度而且不会破坏你已经调通的单总线框架。最后说一点我个人的体会。做完这个实验之后最大的收获不是我会用Logisim了而是彻底看懂了为什么真实CPU要把中断处理分成响应和处理两段。响应是硬件/微程序自动做的干净利落就做压栈、跳转几件事处理是软件做的灵活多变。这个分工思想在我之后读RTOS源码时帮了我很大忙——每次看到上下文切换、中断退出后恢复现场我脑子里浮现的都是Logisim里那个SP递减、PC压栈的微周期序列。如果你现在正卡在某个三态门电压不对或者微指令跳转地址算错的痛苦里别着急这正是你真正理解CPU控制逻辑的开始。
返回列表