ARTICLE DETAIL

资讯详情

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

32位MIPS运算器Logisim实战:从加法器到ALU搭建与Bug修复指南

32位MIPS运算器Logisim实战:从加法器到ALU搭建与Bug修复指南 说实话我大学第一次拿到“32位MIPS运算器”这个实验时第一反应是去网上找一个现成的Logisim文件改个名直接交上去。结果找了一圈要么是付费资源要么是别人作业截图根本看不清好不容易找到能打开的工程里面全是看不懂的英文引脚改也不知道从哪改起。后来我彻底放弃抄作业的念头从1位全加器开始花了两天时间把整个32位运算器在Logisim里一点点搭出来。等搭完回头看发现课上讲的ALU控制信号、补码减法、进位链、Zero标志这些东西突然全串起来了那种感觉比抄十份作业都值。这篇不是什么理论课是一份实战记录。我会按我自己做实验的顺序来写先讲清楚这个运算器到底要实现哪些功能、怎么规划Logisim工程再讲怎么从加法器开始搭、怎么把逻辑运算和比较器加进去最后组装出顶层ALU。标题里既然说了附Bug修复指南那后半部分就把我在实际调试中遇到过的所有奇怪问题、以及排查思路一个不落写出来。适合正在做计算机组成原理课设、或者单纯想自己搞明白MIPS数据通路怎么走的同学参考。1. 拿到实验题先别急着连线需求拆解与ALU功能定义很多同学打开Logisim的第一件事就是拉一堆门电路开始画画到一半发现不知道自己在干嘛。我见过太多人交上来的工程里堆了二十多个模块连线密密麻麻问他“这个输出是干什么的”也答不上来。原因很简单没做需求拆解就直接动手。设计运算器跟写代码一样第一步永远是把功能列表列清楚而不是先开编辑器。1.1 运算器在MIPS数据通路里的真实位置先搞清楚一个容易被忽略的前提MIPS运算器ALU不是一个独立设备它是CPU数据通路里的一个组合逻辑部件。CPU执行add $t0, $t1, $t2这条指令时寄存器堆把$t1和$t2的值送到ALU的两个输入端口ALU根据控制信号算出结果再写回$t0。整个过程没有时钟参与输入一变输出跟着变这是组合逻辑电路。理解了这一点你就能明白为什么ALU的接口设计是固定的两个32位数据输入端一般叫A和B、一个控制信号输入端ALUOp、一个32位结果输出端再加上一个Zero标志位。部分实验还会要求Overflow溢出标志和CarryOut进位输出这些都属于可选项得看具体实验要求。我自己做的时候把Overflow和CarryOut都做了出来因为后面做单周期CPU时加法溢出判断和数据存储器读写都会用到提前做出来不亏。1.2 一份可直接对着做的ALU功能表典型MIPS运算器实验要求支持以下运算AND与、OR或、XOR异或、NOR或非、ADD加、SUB减、SLT小于则置位。我用的控制信号是4位ALUSel编码表如下ALUSel 值运算类型功能说明0000ANDResult A B0001ORResult A | B0010XORResult A ^ B0011NORResult ~(A | B)0100ADDResult A B0110SUBResult A - B0111SLT若 A BResult 1否则 Result 0注意编码里我特意跳过了0101用了0110表示SUB、0111表示SLT。这么做不是拍脑袋而是为了跟MIPS经典ALU控制信号编码兼容——如果你后面要把ALU接到单周期CPU控制器上这种编码可以直接复用真实MIPS的ALUOpFunSel生成逻辑省掉重新对码的麻烦。这一点在实验报告里写清楚老师会觉得你是真的理解而不是瞎选。1.3 现在不该做什么先给设计划边界这个划边界太重要了。运算器实验只要完成“算术逻辑运算”跟寄存器堆、指令存储器、控制器、PC计数器统统没关系。有同学喜欢把整个CPU都画进去结果局部仿真时各种信号互相干扰查错查到崩溃。我的建议是把工程拆成“顶层ALU芯片”“测试平台”两层。顶层ALU芯片就是纯运算逻辑不包含任何存储和状态测试平台只是用来提供输入、观察输出的引脚组合不参与芯片设计。这样后面做单周期CPU时直接把ALU作为一个子电路拖进去复用而不是推翻重来。另一个容易踩的坑是把“移位操作”也做进ALU。MIPS里sll、srl指令虽然由ALU执行但在很多CPU设计中移位器是独立模块不放进ALU。要做的话也行但会给控制编码表和MUX选择器增加不少复杂度。我的建议是先完成基础七种运算保证课设能过如果时间充裕再单独做一个移位器子电路作为扩展功能。贪多嚼不烂这是做课设的第一条铁律。2. Logisim工程搭建与子电路规划确定完功能表下一步是把Logisim的工程结构搭清楚。我一直认为工程架构决定调试效率。那些交上来一坨连线的作业大概率是没做子电路规划所有东西堆在Main里。Logisim的子电路功能非常强大用好它你的电路会清晰得多也更容易定位故障。2.1 版本选择与基础工程设置Logisim有两个常见版本官方原版2.7.1和Logisim Evolution。我的建议很简单直接用Evolution。原版2.7.1在复制粘贴大块电路时经常出现位宽错乱而且对高DPI屏幕的支持很差画一小会儿就眼睛疼。Evolution修复了大量这类问题还增加了代码模块、多选布线等实用功能对32位这种宽数据总线友好得多。打开软件后第一件事是设置“仿真位宽”。默认电路位宽是1位做32位运算器必须把引脚、隧道、Splitter的位宽属性都设为32或按需设置。另外把“显示门延迟”选项打开方便观察组合逻辑的传播延迟——虽然运算器不需要时钟但观察延迟能帮你判断是否存在意料之外的长路径。2.2 子电路拆分先建骨架再填内容我建议按“自底向上”的思路拆分FullAdder1位全加器Adder4由4个FullAdder组成的4位加法器Adder32由8个Adder4级联而成的32位加法器ALU32顶层运算器内部包含Adder32、逻辑运算门、SLT比较器、MUX选择器在Logisim左侧的电路面板上右键选择“Add Circuit”逐个创建。子电路名称不要用中文避免某些版本保存/加载时出现编码问题命名上用Adder4、Adder32这种驼峰命名一眼就能看出功能。创建好骨架后先不要急着连线。我习惯在每个子电路内部先摆放输入输出引脚并给引脚写上标签名比如A[4]、B[4]、Cin、Sum[4]、Cout。这样在后续连线时Logisim会自动匹配引脚标签减少很多低级错误。2.3 引脚命名与隧道使用规范Logisim里有一个非常容易踩的坑Tunnel隧道同名即同网。也就是说两个画在不同位置、名字完全相同的隧道在电气上是连通的。这是个功能特性但也是非常经典的Bug来源。我见过一个同学把加法器输出隧道命名为outMUX输出隧道也命名为out结果两个信号在视觉上完全没连接仿真时却互相干扰找了一晚上没发现。所以命名规范我从一开始就定死所有信号线有明确前缀比如add_sum、add_cout、and_res、or_res、mux_out、aluop_ctrl。绝对不用out、in、temp这种通用名。这个习惯看着不起眼在Bug排查时能帮你省下大量时间。3. 加法器是运算器的心脏从1位全加器到32位逐级搭建ALU的核心是加法器。原因很简单减法本质是加法补码加法SLT本质也是减法后判符号。所以加法器搭得稳不稳直接决定整个运算器靠不靠谱。我搭加法器花了整个项目一半的调试时间不是因为难而是因为低级错误实在太多后面Bug章节会详细讲。这里先给出正确做法。3.1 1位全加器最小单元全加器的逻辑表达式是Sum A XOR B XOR Cin Cout (A AND B) OR (Cin AND (A XOR B))在Logisim里新建FullAdder子电路放置三个输入引脚A、B、Cin两个输出引脚Sum、Cout然后按表达式连逻辑门。这里有个小技巧先连A XOR B这个中间信号把它的输出分别引向Sum的XOR门和Cout的AND门这样能省一次XOR计算电路也更简洁。连完后别急着做下一步先用常量测试给A1、B1、Cin0输出应该是Sum0, Cout1。通过后说明这个最小单元没问题。3.2 4位加法器验证进位链把4个FullAdder串联成4位加法器时关键是把前一位的Cout接后一位的Cin。这里需要用到Logisim的Splitter组件4位输入总线要先拆成4根单线分别接到4个全加器的A、B端输出也通过一个Splitter把4个Sum位合并成4位总线。刚开始用Splitter的人经常会困惑它的引脚顺序。记住一点在Logisim中Splitter的第0位输出通常在边缘端最靠近顶部或左侧的位置具体可以通过鼠标悬停查看高亮提示。连接好后用16进制常量分别设置A和B的值观察Sum输出是否符合预期。比如A50101、B30011进位链完整输出应该是Sum81000且Cout0。3.3 8个4位加法器级联成32位加法器把Adder4实例化8次级联成32位加法器。这一步逻辑简单但画布布局很容易乱。我的做法是8个Adder4按从低到高的顺序垂直排列最低位在下方进位线用一条长直线从下往上依次串起来每个Adder4的Cout接上一个的Cin。输入输出总线分别在左右两侧排开然后用Splitter将32位数据总线拆成8组4位。这样布局的好处是进位链走向一目了然万一某一段进位线断了顺着那条竖线就能找到断点。画完以后务必先测试边界值0xFFFFFFFF 1应该是0x00000000且Cout10x80000000 0x80000000输出0且溢出标志置位如果你做了溢出检测。3.4 给加法器加上溢出与进位输出加减法共用一个加法器的时候溢出判断逻辑不太一样需要单独处理。加法溢出条件是两个加数符号相同且结果符号不同。减法可以转换成加法处理A - B A (~B 1)这时“两个加数”实际上是A和~B所以溢出判断条件要相应调整。Logisim里做溢出判断其实有更直接的方法比较加法器的最高位进位Cout和次高位进位。两者不同就说明有溢出。为了拿到这两个进位需要在32位加法器内部预留两个观察点第30位到第31位的进位以及第31位输出的进位。把这两个信号引到ALU32顶层用一个XOR门判断就是Overflow标志。这个做法比看“符号位变化”更可靠也更容易接线。4. 逻辑运算、SLT比较与额外功能的实现细节加法器搞定后其他模块就容易多了。逻辑运算和SLT虽然简单但有个共同的关键点数据位宽的处理。很多同学在这里出错是因为没搞清楚Logisim里“门电路会自动按位扩展”的规则。4.1 按位逻辑门的位宽处理Logisim中的AND、OR、XOR、NOR门当输入引脚连的是32位总线时输出也自动是32位相当于对每一位分别做运算。所以实现AND运算只需要从顶层引出A和B两条32位总线直接接到一个32位AND门的两个输入上输出命名为and_res。OR、XOR、NOR同理。注意这里我会把四个逻辑门的结果全部独立引出来最后统一接到MUX选择器上。不要图省事直接把逻辑门输出接到最终Result端口上——否则控制信号就没办法切换运算类型了。逻辑门只负责“把结果算出来”MUX才负责“选哪个结果输出”分工必须清晰。4.2 左移/右移日志移位器的搭法与直接使用库组件如果实验要求实现移位操作最简单高效的方法是用Logisim自带的Shifter组件在Arithmetic库。把数据位宽设为32移位量位宽设为5类型选逻辑左移或逻辑右移即可。但如果实验要求“用门电路实现”那就得自己搭对数移位器。对数移位器的思路很经典5位移位量shamt[4:0]每一位对应一级可控制的移位。第0级控制移1位第1级控制移2位第2级控制移4位第3级控制移8位第4级控制移16位。每一级内部是一个MUX选择信号为0时直通为1时把输入整体移动对应位数。数据从32位输入经过五级最后得到移位结果。这个结构最考验人的是“移位后的低位补0”怎么处理。左移时右侧空出的位直接补0右移时左侧空出的位逻辑右移补0算术右移补符号位。Logisim里的常量0可以解决大部分问题但要注意位宽得和总线匹配。4.3 SLT比较器符号位、借位与溢出SLT的本意是“有符号数小于则置1”。最直接的实现方式是做减法A - B看结果符号位。如果符号位为1说明结果是负数即 A B输出1。在Logisim里把Adder32的减法结果A ~B 1的最高位用Splitter取出来作为SLT的输出即可。但我要提醒一个细节直接用符号位判断在溢出时会出错。比如A0x7FFFFFFF(2147483647)B0xFFFFFFFE(-2)A - B 0x80000001符号位是1会误判成 A B但实际是正数大于负数。正确处理方式是SLT 结果符号位 XOR 溢出标志。我在3.4节已经做了Overflow输出这里直接把它和符号位异或一下就能得到正确的SLT结果。如果不想处理溢出很多课设也允许直接用符号位。但实验报告里我会建议你把溢出问题讨论写进去哪怕你电路里只用了符号位也说明你“知道”这里有个坑只是根据课程要求简化了。这比闭口不提要加分不少。5. 顶层组装控制编码、多路选择器与Zero标志输出所有功能子模块都做完并单独测试通过后就可以组装ALU32顶层电路了。这个阶段的核心工作就三样MUX选通、Zero标志生成、子电路封装。每一步都有不小的翻车概率。5.1 ALUOp控制编码与真值表在顶层新建ALU32子电路放入输入引脚A[32]、B[32]、ALUOp[4]输出引脚Result[32]、Zero可选Overflow、CarryOut。四个逻辑门的结果、加法结果、减法结果、SLT结果总共七路信号全部接入一个8选1MUX。MUX的选择端口正好3位但ALUOp是4位。我的处理方式是只接入ALUOp的低3位最高位用来扩展其他功能比如以后做移位。这样控制编码表里我预留的空间就能派上用场。MUX别忘了把位宽设为32否则输出会被截断成1位这是新手特别容易忽略的选项。5.2 MUX选通与结果汇总连线时建议把MUX的几个输入端口按顺序布置0号接AND1号接OR2号接XOR3号接NOR4号接ADD5号空置6号接SUB7号接SLT。选通信号正好跟编码表的低3位对应。这样组合逻辑输出到MUX的走线非常整齐也方便核对。接完后做一次快速验证把ALUOp依次设为0000、0001、0010、0011、0100、0110、0111分别观察Result是否输出对应的AND、OR、XOR、NOR、ADD、SUB、SLT结果。这一步一定要做因为它是后续CPU联调的“基础健康检查”。5.3 Zero标志与后续CPU分支指令的对接Zero标志是MIPS数据通路里很有用的信号beq分支指令就是靠它判断两个数是否相等Zero为1说明相等跳转成立。逻辑上只要Result的所有32位都是0Zero就为1。在Logisim中实现位宽压缩有现成办法使用一个Comparator组件把Result和一个常量0比较输出1信号即可。不过很多作业要求用门电路实现那就要用Splitter把Result拆成32根单线接多输入OR门再接一个NOT门。OR门输出1说明存在非0位取反后就是Zero标志。注意OR门的输入引脚数可以在属性面板里直接增加不需要手动拖32个OR门否则画布会非常拥挤。5.4 子电路封装时的引脚顺序坑这个坑我必须单独拿出来说因为它几乎坑过每一个在Logisim里做实验的人。Logisim子电路对外呈现的引脚顺序不是按名称字母排的而是按你在子电路内部“添加引脚的先后顺序”排的。比如你从上到下添加了输出引脚Zero、Overflow、Result封装后在外部看到的引脚顺序可能完全以添加顺序为主甚至看起来是乱序的。解决办法有两个一是从设计一开始就给每个子电路内的引脚打上清楚的标签名外部连线时不靠位置猜只看标签二是利用Logisim的“重排序引脚”功能在子电路上右键选择“Edit Circuit Appearance”之类的选项调整引脚布局。我自己的习惯是以标签为主封装后视线永远跟着标签走才不会被引脚顺序带偏。6. Bug修复实录六个高频问题与完整排查链路这一节是重头戏。我把自己和周围同学在调试32位MIPS运算器时遇到的典型Bug全列出来每条都给出排查链路。你如果正在调试可以直接拿这些当对照表用。6.1 输出全红X或蓝线位宽冲突与逻辑环现象仿真时一堆线显示红色有错误或蓝色未确定态结果输出全是X。排查链路鼠标悬停在出错的导线上Logisim左下角状态栏会提示具体错误类型常见是“位宽不匹配”或“逻辑环”。检查是不是把1位进位输出接到了32位数据总线上。最常见的是FULLADDER的Cin引脚只有1位却接到了32位总线上。检查Splitter的位宽配置需要32位的拆成了1位或者反过来。如果提示逻辑环说明你把某个子电路的输出又接回到了它的某个输入。也就是说形成了组合逻辑回路。这个在现代Logisim里通常一眼就能看出来——红色线沿着环蔓延。修复把错误段落的连线删掉按位宽分别连Splitter的配置需要反复确认尤其是位序。这个问题没别的捷径只能一段一段查但养成“每个子电路单独测试”的习惯后错误范围会迅速缩小。6.2 切换控制信号没反应MUX选择线与编码错位现象无论ALUOp怎么改Result始终是某一种运算的结果比如永远是加法结果。排查链路先看ALUOp是否真的接到了MUX的选择端。有同学把ALUOp接到了数据输入端选择端空着结果MUX默认选择0号通道。检查布线是否错位例如ALUOp的4根线本来应该接MUX选择端低3位结果接了最高位和中间两位。如果MUX输入端的顺序跟编码表不对应调整接线或编码表二选一但要同步。经验用Logisim的探针Probe去观察MUX选择端每个位的值能最快确认编码问题。别急着改线先定位是“编码发错”还是“接线接错”。6.3 加法结果错一位进位链断裂现象A5、B3理想输出8实际却是某个奇怪值比如3或11。排查链路断开顶层把Adder32单独设为Main电路进行测试。输入几组简单数据从低到高逐位验证。先测11再测12再看0x00000001 0xFFFFFFFF这类涉及高位的用例。如果低4位相加正确但第5位起出错基本可以确定是第4位到第5位之间的进位线断了或接错。尤其注意Adder4级联时低位的Cout引脚是否接到了高位的Cin引脚。拖线时光标偏移一位整个进位链就废了。经验进位线尽量用不同颜色或更粗的线宽与数据线区分。Logisim里可以单独选中某条线修改颜色强烈建议把进位链统一设成红色视觉上一眼就能追踪整条链路。6.4 减法/负数场景的补码陷阱现象单纯加法全部正确但一用SUB或负数参与运算就出错。排查链路确认SUB是“A ~B 1”实现的还是直接用Logisim的减法器组件。如果用减法器组件它内部有自己的一套逻辑与自定义加法器完全是两码事。如果自己实现补码减法检查B取反后是否经过了一次NOT门。很多人在这一步把B总线复制出来后直接接到加法器忘了取反。检查最低位进位Cin在减法模式下是否为1。这是补码加法的关键A - B A (~B) 1那个1就是把Cin设为1。如果Cin是0结果会恒小1。检查符号扩展参与运算的负数是否用了32位补码形式。比如-1在32位下是0xFFFFFFFF有人习惯性输入0x80000001那就不是-1。经验测试减法和SLT时一定要包含“正数-负数”和“负数-正数”这两类跨越符号边界的用例否则补码的错误很难暴露。6.5 同名隧道导致的诡异短路现象两个看起来完全不相干的模块输出却互相影响。比如加法器改个输入逻辑门输出也跟着跳。排查链路全选电路打开导航窗口列出所有隧道名逐项检查有没有重名。特别警惕out、in、tmp、result这几个高频通用名。MUX输出、加法器输出、逻辑门输出如果都叫out就等于全部短接在一起。如果发现重名全部改成带模块前缀的独立名字。经验这个小问题在视觉上极有迷惑性因为你肉眼看到的明明是两个隔很远的隧道怎么都想不到它们其实是同一根线。我甚至见过一个工程里有三处都叫Sum结果整个电路行为完全不可预测。所以从搭建第一天开始就要控制住命名这比任何调试技巧都管用。6.6 建立自己的逐段排查顺序如果你遇到了上面六类问题中都找不到的Bug别慌顺着下面这条链路从头到尾查一遍确认每个子电路单独测试都能通过。这是第一道防线。确认顶层所有输入引脚都有实际输入值。悬浮输入在仿真中会显示蓝色/红色别急着往下查。从输入到输出逐段加探针在A、B入口转成内部线后加探针看信号是否完好到达ALU内部。在四路逻辑门和加法器输出处加探针确认各模块都在正常工作。在MUX输入端口加探针确认七路信号是否都到齐。在MUX选择端加探针确认ALUOp编码到达。最后看Result输出。这条顺序本质上是“由因到果”的分层排查法。每次通过一组探针缩小嫌疑范围最多查三轮就能定位到具体断点或错误元件比满画布乱点要高效得多。7. 验证运算器正确性的测试清单与实验报告要点电路能跑通不代表它就是对的。我在验收前吃过一次亏自己随手测了几组数据都正确结果验收现场老师输入一个极端值直接崩了。所以在交作业前一定要系统性地做测试。7.1 分模块测试子电路单独验证在组装完成后Adder32、逻辑运算模块、SLT模块都已经单独测过顶层组装完成后还需要把ALU32整体作为一个子电路单独添加输入输出引脚进行一次完整测试。具体做法是在Main中放置ALU32子电路接上常量输入和输出显示然后依次给A、B、ALUOp赋值观察Result、Zero、Overflow是否正确。这一步的关键是“每次只改一个变量”比如固定A5、B3遍历所有ALUOp再固定ALUOpADD遍历A、B的不同边界值。不要把多个变量同时改来改去否则出错了你根本不知道是哪组输入触发的。7.2 顶层测试向量表与手动拨测下面是我自己用的最小测试向量集覆盖了常规、边界和进位/溢出三类情况ABALUOp预期Result说明0x0000000F0x000000F00000 AND0x00000000常规AND0x0000000F0x000000F00001 OR0x000000FF常规OR0x0000000F0x000000F00010 XOR0x000000FF常规XOR0x000000000x000000000011 NOR0xFFFFFFFFNOR边界0x000000010x000000020100 ADD0x00000003常规加法0x800000000x800000000100 ADD0x00000000 Overflow1有符号溢出0x000000050x000000030110 SUB0x00000002常规减法0x800000000x7FFFFFFF0110 SUB0x00000001大数减小数0x000000010x000000020111 SLT0x00000001小于成立0x000000020x000000010111 SLT0x00000000小于不成立0x7FFFFFFF0xFFFFFFFE0111 SLT0x00000000SLT溢出边界把这张表里每一行都测一遍并记录结果你的运算器才算真正经得起考验。7.3 测试中容易忽略的边界数据有几个边界数据几乎每次都能抓到Bug0xFFFFFFFF加1会回绕到0并产生进位很多没做好进位链的电路在这里露馅。0x80000000和0x7FFFFFFF正负数最大跨度测SLT和减法溢出必用。0x00000000测Zero标志是否正常为1同时也测NOR、OR等运算在零输入下的表现。两个完全相等的数做减法结果是0Zero标志必须为1。这直接关系到最后CPU实现beq指令是否正确。7.4 实验报告里应该写清楚哪些内容报告不用长篇大论但有几块不能缺ALU功能表和编码表、子电路结构图或层次关系、每个关键模块的实现说明尤其加法器进位链和SLT溢出处理、完整的测试向量表及结果截图、以及你在调试中遇到的问题和解决过程。最后一条特别重要它最能体现你的工作量。如果你遇到了Bug并成功解决哪怕是个很小的问题也值得写进报告。老师看报告时最想看到的不是“一次成功”的假象而是你理解电路运行规律的过程。写在最后的一点经验做完整个运算器之后我最大的体会是Logisim的调试成本跟连线整洁度直接挂钩。你越图省事后面花在找Bug上的时间就越多。我后来做单周期CPU时直接把这次的ALU32子电路拖进去复用省了非常多事那一刻才觉得前面没白折腾。如果你现在正卡在某个Bug上我建议你先不要继续乱试。停下来把电路缩小范围用探针一节一节看信号。实在找不到就把这部分删掉重画。你可能会觉得删了可惜但一次干净的重画通常比带着隐患到处修要来得快。这就像代码重构一样看着是重写实际上是在还技术债。最后再分享一个让我少踩很多坑的习惯每完成一个小模块都先单独验证一遍别等到最后一次性组装完再测。虽然看起来多花了一点时间但真正到了联调阶段你就知道这有多香了。
返回列表