ARTICLE DETAIL

资讯详情

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

从零搭建32位MIPS运算器:Logisim实战指南与Bug修复

从零搭建32位MIPS运算器:Logisim实战指南与Bug修复 如果你是计算机组成原理课的学生或者正在自己折腾MIPS CPU那么 Logisim 这个名字你应该不陌生。这次要做的项目就是在这个开源仿真软件里从零搭一个32位MIPS运算器。运算器是CPU数据通路的核心部件所有加减乘除、与或非、移位、比较都在这里完成。网上能搜到不少教程但大多是贴两张成品图、讲两句原理真正能一步步带着连线、处理报错的内容不多。这篇文章把我带学生做实验时踩过的坑、改过的电路、总结的方案一次性写清楚适合正在做“Logisim新手上路实验”、之后打算做单周期或多周期MIPS CPU设计的同学参考。1. 项目回顾MIPS运算器到底要干什么1.1 运算器在CPU数据通路中的位置先不说电路我们把视角拉高一点。MIPS CPU的执行过程大概是取指令、读寄存器堆、ALU计算、写回寄存器堆。这里ALU干的就是“计算”这步——两个32位操作数进来根据控制信号做一次指定的运算把结果送出去。分支跳转要比较地址访存指令要计算内存地址R型指令要完成真正的算术逻辑操作这些都离不开运算器。所以运算器虽然看起来只是一块组合逻辑电路但它直接决定了CPU能干什么、干得快不快。如果把整个CPU比作一个厨房寄存器堆是冰箱控制器是菜谱那运算器就是操作台——所有原料最后都要拿到这里处理。1.2 功能需求拆解标题里写的是“32位MIPS运算器”32位指的是操作数宽度MIPS则规定了这些操作习惯。一个标准的教学版ALU需要支持这些功能算术运算ADD加、SUB减逻辑运算AND、OR、XOR、NOR移位运算SLL逻辑左移、SRL逻辑右移、SRA算术右移比较运算SLTset less than小于则置1输入两个32位操作数我习惯命名为 A[31:0] 和 B[31:0]控制信号用 ALUOp[3:0]输出为 Result[31:0]另外还要引出几个标志位Zero、Overflow、CarryOut。这里有个关键点MIPS里的数据默认是32位有符号整数用补码表示。很多人做减法、比较时出错多半就是没把“有符号”和“无符号”分开处理。1.3 设计思路并行计算加多路选择ALU 本质上是一块组合逻辑电路没有时钟、没有寄存器输入一变输出立即跟着变。既然如此最朴素的做法就是把每一种运算都实现一遍让所有模块同时在工作最后用一个多路选择器 MUX 按 ALUOp 选出一个结果。打个比方你请了六个厨师同时做六道菜顾客点菜时服务员只需要把对应那道菜端上来。这样做的好处是速度快、结构规整缺点就是费电路。对于实验来说这个“空间换时间”的思路反而是最容易理解和排错的。所以在正式开始连线前心里要有个大的框架每个功能模块是并行的最后由 MUX 汇总。这个思想贯穿整个设计也是后面排查 Bug 时的主线。2. Logisim 元件选型与基础设置2.1 常用元件清单Logisim 自带的元件库足够完成这个项目不需要装第三方扩展。我用到的元件主要来自 Wiring、Gates、Arithmetic 这三个库元件所在库作用PinWiring输入输出引脚可设为32位TunnelWiring隧道标签相当于隐形的导线SplitterWiring总线拆分/合并处理位宽AND/OR/NOT/XORGates32位逻辑运算AdderArithmetic加法器带进位输入和进位输出ComparatorArithmetic比较器可做无符号比较ShifterArithmetic移位器支持逻辑/算术移位MultiplexerPlexers多路选择器按控制信号选输出LEDWiring可视化输出方便测试这里重点说一下位宽。Logisim 元件默认位宽大多是1位双击元件或者在左下角属性面板里把 Bit Width 改成32才能接32位总线。很多新手第一次连线看到红线和报错就是位宽没改。2.2 引脚与输入输出设置输入引脚 A 和 B 要在属性里把 Bit Width 设为32。显示方式可以选“十进制有符号”或“十六进制”做实验时我习惯用十六进制因为对应二进制的位关系一目了然。ALUOp 控制信号是4位但实际可能只用到几种组合。为了方便可以单独用四个引脚分别代表 ALUOp[3]、[2]、[1]、[0]也可以用高位宽的引脚直接输入。后者接线少但输入值时容易误操作前者直观适合验收讲解。两种我都在不同批次实验里用过初学者建议用后者——一个4位宽的 Pin 足够配合“位长”显示就能看到具体二进制值。2.3 子电路封装策略运算器本身可以做成一个子电路这样后面做单周期MIPS硬布线 CPU 时直接把它拖进去当元件用。Logisim 里新建子电路很简单Project → Add Circuit改名 ALU进入子电路后放上输入引脚和输出引脚再把功能模块搭出来。在子电路里最需要注意的是引脚顺序。Logisim 子电路引脚次序会直接影响外部连接时端子排列同名引脚的下标比如 A_0、A_1也会参与排序。我踩过的坑是明明写好了“A低位、B高位、ALUOp、Result、Zero、Overflow”但拖到顶层后发现引脚顺序完全乱掉最后只能逐个核对属性。规范做法是在子电路里就给引脚起清晰的名字并且按功能把引脚放在电路方块的不同侧。提示子电路里的 Pin 元件Direction 属性可以选“从子电路外部输入East/West”或“向外输出”也就是在母电路中看到的“输入侧”和“输出侧”。方向设置错了子电路外部会出现奇怪的接口总线宽度也可能对不上。3. 核心模块设计与接线细节3.1 算术运算加法、减法与溢出判断加法和减法是最核心的运算。Logisim 的 Arithmetic 库里有 Adder32位加法直接拖出来用。Adder 有 A、B、Cin 三个输入S 和 Cout 两个输出。减法不能直接用一个“减法器”因为减法在硬件里就是“加上减数的补码”。具体做法用 ALUOp 里的一位控制信号来选是否对 B 取反。如果这一位是0B 原样进入加法器如果是1B 经过非门取反同时加法器的进位输入 Cin 置1这样就完成了“A (~B) 1”。在 Logisim 里实现的时候需要两个 MUX一个选 B 还是 ~B一个选 Cin 是0还是1。这里的控制信号只需要1位通常用 ALUOp 的最低位或者单独一个 Sub 信号均可。很多资料把 ALUOp[0] 用作减法标志把 ALUOp[3] 用作区分SLT这样编码紧凑但也增加了理解和排错的难度。我自己的方案是另设一个1位控制虽然多了根线但逻辑直观得多尤其适合课程验收。溢出的判断是另一个重点。补码运算的溢出发生在“两个正数相加得到负数”或“两个负数相加得到正数”的时候。硬件上最可靠的做法是看最高位进位和次高位进位是否相同如果两者不同说明符号被进位冲掉了就是溢出。设最高位进位为 Cout31次高位进位为 Cout30则 Overflow Cout31 XOR Cout30。在 Adder 里最低位的进位 Cin 就是0或1次高位进位需要从内部引出。一个技巧是不要死磕 Logisim 的 Adder 内部进位而是自己用32个一位全加器串起来做加法。做一个“全加器子电路”然后复制32份进位链依次传递这样每个位置的进位都在外部可见接溢出判断时非常方便。虽然比内置 Adder 慢但教学演示的价值要高很多。3.2 逻辑运算与或非、异或逻辑运算反而是最简单的部分。把 A 和 B 两根32位总线分别接到与门、或门、异或门的两个输入门电路属性里位宽设成32输出自然就是32位结果。注意 Logisim 的门电路如果位宽是32它的“多输入”特性会自动变成32路输入但我们要的是两输入32位宽所以输入数量保持2即可。NOR 稍微绕一点MIPS 指令集里要求有 NOR但 Logisim 的 Gates 库没有直接的 NOR 门。办法就是“或门后面接非门”或者用“或门输出取反”的属性选项——有的版本中 Gate 可以勾选 Negate Output。在电路上就是多连一个非门的事不复杂。3.3 移位运算逻辑移位与算术移位移位运算在 Logisim 里可以直接用 Arithmetic 库的 Shifter。这个元件有两个输入数据端 Data 和移位量 Shift Amount输出就是移位后的结果。属性里有 Shift Type可选逻辑左移、逻辑右移、算术右移。设计时要注意移位量是几位的输入32位操作数最多移31位所以移位量只需要5位。如果你的移位量直接来自 B 的低5位那就从 B 总线接一个 Splitter取最低5位给 Shifter 的移位量输入端。Splitter 的用法是把32位总线拆成多个小段在属性里设置 Fan Out 和 Bit 分配即可。算术右移是容易遗漏的它和逻辑右移的区别在于最高位不是补0而是补原符号位。对负数来说算术右移一位相当于除以2。这个功能在做 MIPS 的 SRA 指令时必须正确否则负数运算就是错的。Logisim Shifter 选了 Arithmetic Right Shift 后会自动处理符号位。注意如果老师要求“不能用内置 Shifter自己用 MUX 搭桶形移位器”也不用慌。思路是把输入按1位、2位、4位、8位、16位5级串联每级由移位量对应位控制是否移位逐级流过。每级就是一组MUX32位数据配32个MUX即可原理很简单。3.4 比较运算SLT的实现与无符号比较SLT 是“若 A B 则输出1否则输出0”。MIPS 的 SLT 指令针对有符号数。很多同学直接用 Logisim 的 Comparator 比较器这其实埋了雷——Comparator 默认可以做无符号比较也可以做有符号比较但很多人没有设置 Signed 比较属性导致负数比较结果不对。我推荐用减法来判断大小计算 A - B结果的最高位符号位和溢出标志决定最终结果。经典公式是若无溢出SLT 结果符号位若溢出SLT 结果符号位取反等价于 SLT Sign XOR Overflow。验证一下当 A7, B-3 时A-B10符号位为0无溢出SLT0正确当 A-7, B3 时A-B-10符号位为1无溢出SLT1正确当 A0x80000000, B1 时A-B 会正溢出符号位0Overflow1SLT0 XOR 1 1说明 AB正确。把这个逻辑接到硬件上用自己搭的32位加法器做减法把最高位进位和第31位差值的异或结果作为 Overflow再把最高位结果 Sign 与 Overflow 异或得到 SLT。SLT 本质上只有1位结果但为了接入32位 Result 总线需要对它做 32 位扩展全0或者低1位是SLT值。用 Logisim 的扩展器或者一个简单的 MUX 就能做到。3.5 顶层汇总MUX 统一输出与标志位各个功能模块的输出都是32位最后用一个多路选择器 Multiplexer 把这些结果按 ALUOp 选出。MUX 的属性里有两个关键参数Data Bits 设为32Select Bits 为控制信号位数。如果 ALUOp 是4位而只需要选8个以内的结果那么 MUX 的 Select Bits 可以是3位ALUOp 的低3位接线即可。但如果你的 ALUOp[3] 还要用于 SLT 标志位编码就需要仔细设计映射。我最常用的编码是ALUOp操作0000AND0001OR0010ADD0110SUB0111SLT1100NOR1101XOR1010SLL1011SRL1000SRA这个编码不必和标准MIPS一致实验的最终目标是逻辑正确。实际接线时MUX 的每个数据输入端口要依次连接到对应模块的输出一定记清楚每个端口的顺序否则控制信号写对了、输出却对应错路排查起来相当痛苦。Zero 标志位的生成方式把最终 Result 的32位全部接入一个 NOR 门因为 NOR 门位宽设32后输出的每一位都是所有输入位的或非。32位做 NOR当且仅当所有位都为0时输出1。用一个32位输入、1位输出的 NOR 门即可。4. 从零搭一个运算器完整实操流程4.1 建工程与放引脚打开 Logisim新建工程。先放两个32位输入引脚 A、B再放一个4位输入引脚 ALUOp。为了方便观察在输出侧放一个32位 LED 阵列或输出引脚 Result再放几个1位输出引脚用于 Zero、Overflow、CarryOut。建议从一开始就用 Tunnel 标签而不是直接拉长导线。比如从 A 引脚拉出一根线后放一个 Tunnel 命名为 A那么任何地方只要再放一个名为 A 的 Tunnel 就相当于连到了这根线。这样整个电路看起来清爽得多排查问题时也不用在线堆里找线。顺序上我建议先搭逻辑运算模块和算术模块再搭移位和比较模块最后统一接 MUX。4.2 搭逻辑运算模块拖入两个32位的 AND 门、OR 门、XOR 门、NOR 门OR门后接NOT门把 A、B 复制过去。这里需要注意的是当你在同一电路里多次使用 A 总线时用 Tunnel 比反复从原引脚拉线更整洁。每个门的输出都接到一个 Tunnel 上命名如 R_AND、R_OR、R_XOR、R_NOR。这样到了汇总阶段把 Tunnel 拉到 MUX 对应端口即可。4.3 搭算术模块与溢出逻辑如果使用32个一位全加器串联的方案先做一个全加器子电路输入 A_i、B_i、Cin_i输出 S_i、Cout_i。然后复制32份前一个的 Cout 接到下一个的 Cin。第一个的 Cin 接到 Sub 信号第32个的 Cout 就是最终进位 CarryOut。减法的实现把 B 的每一位都在进入全加器之前过一遍 XOR 门另一端接 Sub 信号。Sub1 时 B 各位取反加上 Cin1就完成了补码减法。溢出判断从全加器的进位链上取第30位进位和第31位进位做 XOR。这里要小心数字编号Logisim 的 Splitter 可以把总线拆成单个位但位序容易看反。建议对照数据手册或自己写一个测试用例A0x7FFFFFFFB1加法后应为溢出。4.4 搭移位模块与比较模块拖入 Shifter 元件Data 接 AShift Amount 接 B 的低5位。如果你还希望支持按立即数移位可以把移位量控制信号单独引进来实验阶段直接用 B 的低5位最省事。比较模块按前面讲的方案把减法的结果和溢出标志做 XOR输出 SLT。注意这里要复用减法模块的结果所以好的做法是在设计时把减法输出和溢出信号引到 Tunnel供多个模块使用。SLT 结果是1位需要扩展成32位再接 MUX扩展方式是把 SLT 接到一个32位选择器的一组输入上选择器的输入端可以是“SLT 位”加上31个0。我常用 Splitter 来拼接或者直接用两个 Tunnel 接到 MUX 的两个不同数据位组。4.5 MUX 汇总与标志位输出把所有功能模块输出端依次接到 MUX 的数据输入。MUX 数据输入0对应 AND1对应 OR2对应 ADD3对应 SUB4对应 SLT5对应 NOR6对应 XOR7对应 SLL8对应 SRL9对应 SRA。这样 ALUOp 的低4位或低3位加独立标志直接控制选择。MUX 的 Select 端接 ALUOp 的对应位。例如用 ALUOp[2:0] 选低8路ALUOp[3] 用于区分移位操作时就要在 ALUOp[3]1 时强制让选择端选中移位的结果。最简单的做法是把移位模块用一个单独的 MUX 先选出来再和前面8路结果做第二级选择。两级 MUX 办法虽然元件多一点但可读性高。4.6 测试用例设计测试不能只测加法。我每次验收前必测的用例是AB操作期望0x7FFFFFFF1ADDOverflow10x800000000x80000000ADD结果0x00000000CarryOut153SUB235SUB-2 (0xFFFFFFFE)-5-3SLT0-53SLT10x800000001SLT10xF0F0F0F00x0F0F0F0FAND0x000000000xF0F0F0F00x0F0F0F0FOR0xFFFFFFFF0x800000001SLL00x800000001SRA0xC0000000逐个用手改引脚值、观察输出的方式虽然朴素但最能发现接线错误。如果有条件可以把输入引脚和输出元件复制成多个方便同时观察也可以加 Text 标注写明每一组输入对应的期望值验收时直接对照演示老师印象分会好很多。5. 常见 Bug 与修复指南5.1 Bug 速查表先放一个速查表后面逐个展开现象可能原因解决方法出现红色或蓝色线位宽不匹配检查门电路、MUX、引脚的 Bit Width负数比较总不对使用了无符号比较器改用减法符号溢出判断减法结果不对B取反逻辑失效或Cin没置1检查取反MUX和Cin连接溢出标志不亮/乱亮进位取错或判断逻辑接反核对第30、31位进位XOR关系MUX输出和ALUOp对不上数据输入顺序接错逐路用固定值测试电路仿真卡死/振荡组合逻辑回路断掉反馈路径重新接线子电路外部引脚乱子电路引脚顺序混乱规范命名并调整方向5.2 红色、蓝色、灰色的线位宽相关Logisim 中线的颜色能帮助你快速定位问题。绿色表示正常有值红色表示位宽冲突蓝色是高阻态灰色是未连接或没有值。最常见的就是“红色”和“蓝色”同时出现。比如一个32位输出直接接到了一个1位输入或者一个1位选择信号接到了4位控制端。再比如说你给门电路设了32位宽但输入引脚忘了设32位门输入端就会甩出红色线。遇到红色线时双击元件看属性面板中的 Bit Width确认输入端和输出端的位宽是否两边对齐。MUX 的 Data Bits 和 Select Bits 是分开设置的容易忽略。整个电路搭完如果一片红不要慌从输入引脚开始逐个检查位宽通常20分钟内能修完。5.3 负数相关Bug有符号和无符号的混淆这是 MIPS 运算器里最隐蔽的坑。很多人在比较模块上直接放一个 Comparator默认情况下它按无符号数比较结果就是“-1 大于 1”因为无符号视角 0xFFFFFFFF 0x00000001但 MIPS 的 SLT 需要的是“-1 小于 1”。解决方式有两种一是把 Comparator 属性里的 Signed 改成是但这样容易忘二是干脆不用 Comparator用减法加 Overflow 判断。我推荐第二种因为它是从原理出发做好了以后换到任何环境都能直接迁移。另外SLT 结果本身只有1位接入32位 Result 时需要扩展。扩展时千万不要用“位扩展”直接把1位拉成32位的高位都接0这不是符号扩展的问题而是你只想要最低位是有意义的值。用 32位 MUX 的一路输入固定为“低位SLT高31位0”最稳妥。5.4 溢出标志和进位混乱溢出标志只对 ADD 和 SUB 有意义逻辑运算和移位运算的溢出应当视为0。如果你的 Overflow 输出常年显示1多半是溢出判断逻辑的输入取错了。在自搭串行加法器方案中要特别小心进位编号。0号进位是最低位31号进位是最高位。溢出 第31位进位 XOR 第30位进位注意是进位不是结果位的值。如果你用的数列顺序搞反溢出标志会在正常加法时乱跳。另一个坑是加法器最高位的输出结果 S31 不是进位。很多同学以为最高位是进位直接拿 S31 去和 Cout31 比较结果完全不对。要理解结果是结果进位是进位一码归一码。5.5 MUX 选择信号与控制编码不匹配MUX 的选择端长度由 Select Bits 决定。如果你定义了4位 ALUOp但 MUX 的 Select Bits 只有2那么选择的只能是4路之一而不是16路。要么把 ALUOp 的低2位接 MUX其余位作其他用途要么把 Select Bits 设为4把 ALUOp 全部接入。还有一个很容易犯的错误MUX 数据端口的顺序在 Logisim 中是固定的从上到下依次为0、1、2、3……如果你在接线时把 ADD 结果接到端口3却把控制信号3当作减法就会“按下减法却输出加法”。这种问题光看电路很难发现最快的方法是逐路测试设置 ALUOp 为 0000看输出是否等于 AND 结果然后 0001 看 OR一路测下去30分钟能定位。如果控制编码和 MUX 端口顺序没有一一对应可以加一个“译码器”电路把 ALUOp 的4位输入用比较器或者逻辑门转换成各个功能模块的独立使能信号再用一组与门控制哪些结果参与 MUX 选择。这种方法多用了元件但编码自由度高和 MIPS 控制信号设计也天然吻合后续做指令译码器时能直接复用。5.6 仿真振荡组合逻辑互锁Logisim 在仿真时偶尔会弹 “circuit oscillation” 的警告或者时钟跑到天荒地老。这种问题通常是电路的某两个门之间形成了反馈回路。ALU 是组合逻辑理论上不应该有反馈。如果你的电路里出现了“A门的输出接到B门B门的输出又接回A门”的路径仿真器就无法稳定。最常见的情况是为了省事把 MUX 的输出直接又接到了输入端的 Tunnel 上或者复制模块时误把输出端口接到了同名 Tunnel。排查方法是点 Simulation 菜单里的 Turbulence/Ticks观察警告框提示的循环路径或者从头到尾沿着 Tunnel 名称查一遍看有没有重名误连。Tunnel 功能好用但也有风险两个不同模块如果都用了同一个名字Logisim 会自动认为它们需要相连。命名时加上前缀比如 A_OR、B_SUB能避免这类尴尬。5.7 子电路引脚顺序与方向当你把 ALU 封装成子电路并拖到顶层做单周期MIPS CPU时子电路的引脚顺序会直接影响外部设备的使用体验。Logisim 中子电路的外观是一个方块左边是输入引脚右边是输出引脚取决于你设置的 Direction 属性。常见 Bug 是在子电路里明明把 A 和 B 放成上下两个输入但回到顶层发现顺序是反的导致 B 接到了原来 A 的位置运算结果全乱。这个问题的根源是 Logisim 按引脚名称的字母和数字顺序排列而不是按摆放顺序。想要控制顺序可以在引脚名称里加入序号比如 A_0、B_1、ALUOp_2这样方块的引脚显示会严格按名称排序。另一种做法是把引脚命名成有意义的单词然后用 Appearance 菜单调整子电路外观手动拖到理想位置。提示在子电路里所有引脚默认的点位置遵循“输入靠左输出靠右”的布局。如果需要输出的引脚在电路方块左侧显示可以把 Pin 的 Direction 设为 West然后在子电路外部再用 Tunnel 转一圈也能变相调整排布但不推荐维护起来太痛苦。5.8 扩展从 ALU 到完整 MIPS CPU运算器搭完并测试通过后可以顺手把它封装成“ALU32”子电路。后面的单周期MIPS硬布线 CPU、多周期MIPS CPU甚至流水线的理想排布实验都可以直接使用它。这个时候你会发现ALU 的 ALUOp 输入其实应该来自控制器而不是手动引脚。控制器会根据指令 opcode 和 funct 字段生成对应 ALUOp而我们的运算器只负责按控制信号“干活”。我在做完整单周期 CPU 时就是把 ALUOp 引脚替换成从顶层控制器引来的线整块电路直接拖进去零改造。再往后做多周期MIPS CPU时ALU 需要支持更多操作比如移位指令、无符号加减。只要在现有的 MUX 输入端口新增几路控制编码里补上对应值整个运算器就能平滑升级。这也是当初选择“并行模块 MUX 汇总”最大的好处——扩展就是加个模块和加根线的事。我在实际指导学生时发现一个规律凡是能独立、干净地完成这个运算器实验的人后面做单周期CPU时普遍很少踩雷反之运算器里靠“试出来”蒙对的人到了 CPU 集成阶段基本都会返工。原因其实很简单运算器是整个数据通路的纯度测试它逼着你把“控制信号如何决定运算行为”这件事想清楚。与其后面积累一堆债务不如在这个阶段把每一个 Bug 背后的原理都弄明白。这也是我写这篇修复指南的初衷——希望你能在动手连线的过程中真正理解每根线、每个信号为什么在那里而不只是让仿真窗口里的灯亮起来。
返回列表