ARTICLE DETAIL

资讯详情

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

基于Logisim的MIPS五级流水线CPU设计:从数据冒险到HUST通关全攻略

基于Logisim的MIPS五级流水线CPU设计:从数据冒险到HUST通关全攻略 刚把MIPS流水线CPU完整跑通的第二天趁记忆还热乎我把整个设计过程和踩坑记录整理出来。如果你也在做计算机组成原理的课程设计或者正在跟HUST那套Logisim实验死磕这篇文章应该能帮你省下好几个通宵。这套课程设计的核心任务是基于Logisim可视化硬件仿真工具从零搭建一个支持MIPS指令集的五级流水线CPU。它和普通的单周期CPU最大的区别就在于“流水线”三个字——指令像工厂流水线一样分段加工理想情况下每个时钟周期都能完成一条指令吞吐量直接拉满。但代价是数据冒险、控制冒险、结构冒险这些问题会一并冒出来需要你动手设计转发、暂停、冲刷等机制去解决。我想说的是这套实验真正锻炼人的地方不是“把线路连出来”而是“把CPU里面每个信号为什么这样设计搞清楚”。你全程敲的不是代码而是一根根导线、一个个选择器、一组组控制信号拼出来的却是一台能跑指令的完整处理器。下面我把整个项目从设计思路、前置知识、流水线实现到HUST各关卡的通关要点按我自己实操的顺序完整过一遍。1. 项目整体设计与思路拆解1.1 为什么选择Logisim MIPS这套组合先说Logisim。这是很多高校计算机组成原理课程指定的仿真工具纯图形化操作拖拽组件就能搭电路而且自带子电路、隧道、时钟信号、RAM等实用组件。相比写Verilog或者搭面包板Logisim最大的好处是能让你把注意力放在“数据通路怎么走”上而不是“语法怎么编译过”上。连接线看得见信号传得到哪个环节出错顺着线一查就能定位这对理解CPU内部工作机制太有用了。MIPS则是典型的RISC架构指令定长32位寻址方式简单寄存器堆和ALU的操作逻辑非常规整。它不像x86那样指令长度可变、寻址模式繁多天生适合做教学CPU。更重要的是MIPS指令集的五种经典指令格式——R型、I型、J型覆盖了运算、访存、分支跳转三大类操作刚好能撑起一个完整的单周期和流水线CPU设计。我在动手之前画了一张整体架构思维导图指令从取指到写回共分IF、ID、EX、MEM、WB五级每两级之间插一个流水线寄存器保存该级产生的中间结果控制信号分为“贯穿型”和“本级使用型”两类冒险处理分为数据转发、指令暂停、分支冲刷三块。把这张图画清楚后面所有布线工作都变得有条理了。1.2 MIPS流水线的整体框架五级流水线是怎么划分的五级流水线的划分本质上是把一条指令的执行过程拆成五个独立的加工工序每道工序只做一件事且每道工序都有一个专门的硬件模块来完成IF取指根据PC程序计数器从指令存储器中取出当前指令同时完成PC4的更新。如果有跳转或分支这里要接收目标地址。ID译码解析指令字段读寄存器堆取两个源操作数并生成该指令所需的全部控制信号。EX执行由ALU完成算术逻辑运算同时进行分支判断根据ID级读出的两个寄存器值比较结果。MEM访存仅对lw、sw指令有效读写数据存储器。WB写回将运算结果或访存数据写回寄存器堆。如果把单周期CPU比作一条产线上只有一个人在从头干到尾那流水线CPU就是按工序分成五个工位各干各的活同时有五条指令在不同工位上加工。理想情况下单周期CPU执行5条指令需要5个时钟周期而流水线CPU同样5个周期就能让5条指令全部“进入”流水线第5个周期之后每周期都能完成一条指令这个吞吐量的差距是质变的。1.3 课程设计的关卡全貌HUST这套实验到底考什么HUST的CPU系列实验整体是从易到难递进的大致流程是先用Logisim做几个基础组件实验熟悉工具再完成单周期CPU进阶到微程序控制的多周期CPU最后才是流水线CPU。我把这个流程理解为修房子基础实验是“学会用砖刀”单周期是“砌一堵墙”多周期是“盖平房”流水线是“盖楼房”。基础入门实验熟悉Logisim组件做加法器、七段数码管显示、运动码表这类小电路。这关如果卡住多半是分线器、常量、输入输出引脚这些基础元件还不熟。单周期CPU硬布线控制这是第一次完整地搭建数据通路要求能够执行一组指定的MIPS指令每个周期执行一条。多周期CPU微程序控制引入状态机和微指令支持现代时序一条指令分多个时钟周期完成。这个阶段会进一步加深对控制信号、时序状态的理解。流水线CPU在单周期数据通路基础上加入流水线寄存器并处理三类冒险。这是整个课程设计的集大成者。知道了全貌你就能预判整个工作的核心难点前期的组件和单周期打好基础流水线才有得玩。后面每一关的代码或者电路图都是在前面基础上“改扩建”的。2. MIPS指令系统与Logisim核心组件准备2.1 MIPS指令格式与必要指令集设计CPU之前指令集是必须先定下来的。我这里采用了最常见的教学子集共10条左右覆盖三类格式指令类型指令示例功能格式R型add、sub、and、or、slt寄存器运算opcode0用funct区分具体操作I型addi、lw、sw、beq立即数运算、访存、分支opcode区分操作16位立即数J型j无条件跳转opcode226位目标地址R型指令的32位字段分布是op(6位) rs(5位) rt(5位) rd(5位) shamt(5位) funct(6位)。I型是op(6位) rs(5位) rt(5位) imm(16位)。J型是op(6位) target(26位)。这里有个新手特别容易绕晕的点寄存器编号rs、rt、rd的用途随指令类型不同会变化。比如add指令中rs和rt是源操作数寄存器rd是目的寄存器而lw指令中rs是基址寄存器rt是目标寄存器rd字段完全不用。所以在设计ID级的寄存器堆读写端口时要仔细对照每条指令的字段含义不能一概而论。2.2 Logisim中必须熟练的组件速查Logisim的组件库里真正高频使用的其实就那几个我列一份速查清单输入输出引脚Pin和LED用于向电路输入信号和显示输出结果时钟信号Clock用来驱动时序逻辑。寄存器Register边沿触发的D触发器组可以设置位宽时钟上升沿写入。寄存器堆Register FileLogisim自带Register File组件包含两个读端口和一个写端口要设置寄存器个数通常32个和位宽32位。ALU与算术组件Logisim的Arithmetic库中有加法器、减法器、比较器、乘法器等也可以自己封装一个支持多操作类型的ALU子电路。存储器RAMLogisim的RAM组件支持读写数据需要设置位宽和地址位宽指令存储器可以复用RAM或使用ROM组件。多路选择器MUX根据选择信号从多路输入选一路输出的关键组件数据通路的“岔路口”全靠它。分线器Splitter把一个多位总线按需拆成多个子段比如把32位指令拆成opcode、rs、rt、rd、funct等字段。隧道Tunnel让同名信号在不同位置“虚拟连通”避免导线交叉过多布大电路时的神器。比较器判断两个值是否相等、大小关系分支指令beq会用到。我个人的建议是正式搭CPU之前用一下午时间把“寄存器堆读写 ALU运算 指令字段拆分”这三个小模块分别单独点亮一次每个小模块都能跑通后期组装时心态会稳很多。2.3 数据通路草图先画图再连线我在课程设计群里见过很多同学上来就直接打开Logisim开始拖组件拉线结果连到一半发现数据通路设计有缺陷满屏导线推倒重来。我的经验是在Logisim里连第一根线之前先把纸上的数据通路图画到能直接照抄的程度。画图时重点标清楚以下信号流向PC到指令存储器、指令存储器拆分出各字段、寄存器堆读写地址与写数据来源、ALU的两个输入来源寄存器值和立即数扩展值、ALU结果去向写回寄存器或作为访存地址、访存数据回写路径、分支跳转地址的计算路径、控制信号对MUX的选择值。每一条线的数据宽度是多少位旁边标好。这个前期工作大概花2小时但能帮你把后期调试时间压缩一半。真到连线的阶段你就是照着图纸施工而不是边想边连。3. 五级流水线CPU核心里程碑拆解3.1 流水线寄存器的设计与位宽计算流水线寄存器是流水线CPU和单周期CPU在物理结构上最大的区别。它是插在相邻两级之间的暂存模块保存上一级在这一拍产生的、下一级还需要用到的所有数据和控制信号。四个流水线寄存器分别为IF/ID、ID/EX、EX/MEM、MEM/WB。每个寄存器要保存的内容取决于它后面几级还需要什么数据。以ID/EX为例它需要保存从寄存器堆读出的两个源操作数各32位、立即数扩展结果32位、rt字段和rd字段各5位用于确定写回目标、以及这一级要用的控制信号RegDst、ALUSrc、ALUOp、Branch等。寄存器位宽的计算方法很简单把所有需要保存的信号位宽相加。比如ID/EX总位宽 32(rs值) 32(rt值) 32(ext_imm) 5(rt) 5(rd) 若干控制位。设计时建议用隧道或者总线分组把这些信号聚合起来避免位宽差错。我踩过的一个坑是ADD指令的写入目标是rd而ADDI和LW的写入目标是rt这两者在单周期里只需要一个MUX根据RegDst信号选择但到了流水线里必须在ID/EX阶段就把“写哪个寄存器”这个信息一路带到WB级否则到了最后根本不知道要写回哪。这就是流水线寄存器“保存控制信号与数据”的核心意义——信息必须跟着指令一起往后传而不是在某一级用掉就完。3.2 数据冒险转发、暂停和load-use问题数据冒险是流水线CPU设计中最容易让人头疼的部分。它的本质是后续指令要用到的寄存器值在当前指令还没有写回要等到WB级之前就已经需要被读出了。这就像流水线上第二道工序需要第一道工序的成品但第一道工序还没加工完。解决数据冒险有两种经典机制。第一种是转发Forwarding也叫旁路Bypass。思路是当前指令的结果一旦在EX或MEM阶段产生出来就直接把它送到需要该结果的EX级ALU输入端而不是傻等它走完WB再写回寄存器。具体逻辑是EX/MEM寄存器的结果如果恰好是后面某条指令需要的rs或rt就把EX/MEM输出端直接引回ALU输入端的MUX上。第二种是暂停Stall。当遇到load-use冒险时——也就是上一条指令是lw下一条指令紧跟就要用lw加载回来的数据——即使转发也来不及因为数据要到MEM级结束才有而下游指令在ID级就要读源操作数了。这种情况下只能让流水线“冻住”一拍下一条指令在ID级原地等待同时往流水线里插入一条空指令气泡等lw的数据从存储器取回来后再继续运行。我在设计转发逻辑时把判断条件整理成了一个表格方便查信号条件转发动作EX/MEM.RegWrite1 且 EX/MEM.rd≠0 且 EX/MEM.rdID/EX.rs将EX/MEM的ALU结果转发到ALU输入端AEX/MEM.RegWrite1 且 EX/MEM.rd≠0 且 EX/MEM.rdID/EX.rt将EX/MEM的ALU结果转发到ALU输入端BMEM/WB.RegWrite1 且 MEM/WB.rd≠0 且 MEM/WB.rdID/EX.rs将MEM/WB的回写数据转发到ALU输入端AMEM/WB.RegWrite1 且 MEM/WB.rd≠0 且 MEM/WB.rdID/EX.rt将MEM/WB的回写数据转发到ALU输入端B注意优先级当两条转发路径同时满足条件时应该优先选最近的那一个EX/MEM优先于MEM/WB因为前者的数据更新。3.3 控制冒险分支预测与流水线冲刷控制冒险的根源是分支指令。beq的判断结果在EX级末尾才能得出而分支目标地址要到MEM级才真正确定如果判断信号在那里做的话这意味着分支指令之后的几条指令已经被错误地取进流水线了。不处理的话程序就会沿着错误的路径执行下去。最简单的处理策略是“预测不跳转”默认beq不跳继续顺序取指。当EX级发现分支条件成立时就需要把已经进入流水线的后续指令全部清空Flush然后从目标地址重新取指。清空操作在硬件上就是往ID/EX寄存器写入一个“空指令”的控制信号让指令在EX级什么也不做相当于插入气泡。这里有一个更高效的做法是把分支判断提前到ID级来做。也就是说分支的判断不等ALU结果而是在ID级用比较器直接比较从寄存器堆读出的两个值是否相等同时把目标地址的计算也放到ID级。这样一旦发现分支成立只需要冲刷IF/ID寄存器里那条本不该进入的指令代价只有原来的一半性能明显改善。我在课程设计里就是这么做的实测下来比传统策略快了约20%。不过要注意提前分支判断要求ID级就有rs和rt的寄存器值这又和前面的数据冒险交织在一起。如果分支指令的输入寄存器恰好是前一条写寄存器指令的目标就需要照常做转发甚至暂停逻辑相当考验耐心。3.4 结构冒险的处理方案结构冒险简单说就是两条指令同时想用同一个硬件资源。MIPS五级流水线在理想设计中每级使用的硬件模块是错开的IF级用指令存储器、MEM级用数据存储器。但如果课程设计里指令存储器和数据存储器共用同一个RAMIF级和MEM级就会在同一时钟周期争用这个RAM发生结构冒险。处理结构冒险最干净的办法是把指令存储器和数据存储器在硬件上分开。在Logisim里可以使用两个独立的RAM/ROM组件来模拟。另一种做法是让访存级和取指级错开时钟沿取指上升沿读访存下降沿读虽然也能绕过冲突但会给后续时序分析带来麻烦不推荐新手尝试。我自己的设计是直接用两个存储模块这是最省心也最贴近真实CPU的做法。真实处理器里指令Cache和数据Cache也都是物理分离的。4. HUST课程设计各关卡实战通关4.1 关卡一Logisim基础实验与常用电路搭建第一关卡的内容通常包括用Logisim搭建全加器、七段数码管显示、运动码表等基础电路。表面上是“熟悉工具”实际上是在训练你三件事位宽意识、时序意识、模块化意识。做全加器时我第一次没意识到进位是可以级联的按照真值表单独搭了三个输入的逻辑门结果功能是对的但完全没有可扩展性。后来改成“半加器 半加器 或门”的结构才真正理解了加法器的本质。七段数码管实验里核心是搞清共阳共阴接法和译码逻辑可以用Logisim自带的七段显示组件也可以自己用逻辑门搭译码器后者对理解数电基础帮助更大。运动码表这个实验就是连续计时和显示的联动看起来花哨实际用到的主要是时钟计数和数码管刷新。这一关的通关技巧很简单把组件属性面板里的位宽、标签、触发方式这些设置挨个试一遍不要怕试错。我见过太多同学第一关就把时间浪费在找不存在的Bug上结果只是某个引脚宽度设错了。4.2 关卡二单周期MIPS CPU设计与硬布线控制单周期CPU是整条课程设计的第一个真正的大工程。它要求在一个时钟周期内完成一条指令从取指到写回的全过程因此所有组合逻辑路径的延迟之和必须小于时钟周期长度。在Logisim里这通常意味着你要设计这么几条完整的数据通路取指路径PC→指令存储器→指令拆分寄存器读写路径寄存器堆双读口、写入端口与写数据来源选择ALU执行路径两个源操作数选择寄存器值或立即数、ALU操作码控制访存路径ALU结果作为地址、RAM的数据写入与读出PC更新路径PC4、beq分支目标、j跳转目标的MUX选择硬布线控制的本质是把每条指令对应的控制信号用组合逻辑生成出来。我习惯的做法是先列一张控制信号真值表每一列是一种控制信号每一行是一条指令值由指令的opcode和funct共同决定。比如RegDst写寄存器目标选择、ALUSrcALU源操作数选择、MemRead、MemWrite、RegWrite、Branch、Jump、ALUOp等。个人体会最深的坑是lw指令的寄存器写地址来自rt字段而运算指令来自rd字段这个选择RegDst一旦接反所有的运算指令写回地址都会错位程序跑起来全是乱数。调试的时候看寄存器的写地址和期望值对不上代码又看不出来最后用探针一个一个信号对照才发现是这里的问题。4.3 关卡三多周期CPU与微程序控制多周期CPU的设计思路和单周期不同它不再试图让一条指令在一个周期内完成而是把每条指令拆成若干微步骤每个时钟周期只执行一个微步骤。这样每条指令需要的周期数从1到5不等硬件资源尤其是ALU可以被多条指令分时复用。HUST的多周期CPU通常要求微程序控制实现也就是用一块控制存储器ROM保存每条指令对应的微指令序列每条微指令里包含该步骤需要的所有控制信号以及下一条微指令的地址。设计微程序的过程很像写汇编程序只是寄存器名换成了控制信号名指令变成了微指令。这一关的难点在于状态转移逻辑的设计。每条MIPS指令执行时的状态图要自己画取指、译码、执行、访存、写回各状态之间的转换条件由指令类型决定。我的建议是先把每条指令的状态转移图在纸上画清楚再对照图排微指令地址最后向Logisim的ROM组件里填充微指令。如果上来就填ROM改一个地址就要重新理一遍逻辑太折磨人。现代时序的单总线CPU设计网上也有对应头歌题目比普通多周期多了一个“单总线”的约束所有数据传送都必须经过一条共享总线同一时刻只能有一个部件向总线发送数据。这要求你对每个时钟周期的数据流向有非常清晰的认识什么时候谁占用总线、什么时候释放一分一毫都不能乱。做这个实验时我习惯把每个微步骤的“总线占用表”也画出来谁发送、谁接收、哪个使能信号有效一目了然。4.4 关卡四MIPS流水线CPU完整设计与冒险处理到了流水线CPU这一关基础的单周期数据通路你已经有了核心工作从“搭路径”转移到了“加寄存器、处理冒险”。我设计的流水线CPU关键操作拆成下面几步在单周期数据通路的IF/ID、ID/EX、EX/MEM、MEM/WB之间插入四个流水线寄存器。重新设计控制信号把原来在一个周期内同时有效的信号按照“在哪一级使用”拆开分配到对应流水线寄存器中。加入转发单元Forwarding Unit根据前面表格里的判断逻辑用比较器判断流水线寄存器中的rd与当前ID/EX源寄存器编号是否相等输出转发选择信号。加入暂停检测单元Hazard Detection Unit专门检测load-use冒险输出PCWrite和IF/IDWrite的“冻结”信号同时插入气泡。把分支判断提前到ID级并设计对应的冲刷逻辑Flush信号清零IF/ID寄存器。这里面我反复调试最久的是暂停和转发的配合。转发单元正常工作后load-use情况处理不对后来才想明白当检测到load-use冒险时下一拍ID/EX寄存器里那条正在等待的指令需要清零而IF/ID寄存器保持不动同时PC停止更新。三路信号要配合好漏一路都会出问题。调试时我习惯把暂停检测单元的输出用LED引出来肉眼观察冻结发生的时候其他信号是否同步变化比单看波形直观得多。测试程序方面我准备了三条经典用例无冒险指令序列连续R型运算验证基本流水线吞吐。相邻指令存在数据依赖add后面紧跟sub使用add的结果验证转发。lw后面紧跟使用该数据的指令验证暂停和气泡插入。能在这三组程序上全部得到正确结果你的流水线CPU基本就过关了。4.5 HUST关卡常见变体与“头歌”平台注意点网上的HUST版实验在头歌平台上也有对应版本题目名称包括“单总线CPU设计(现代时序)”、“MIPS指令译码器设计”等。在平台上评测时通常会对输入输出接口有固定要求比如端口命名、引脚位宽必须完全一致否则评测脚本读不到信号。我的建议是拿到题目后先仔细读接口定义文档在Logisim里建子电路时把输入输出引脚的名字和位宽按文档严格设置。命名规则这种细节平台评测可不会跟你讲道理。另外头歌平台的编译环境和本地未必完全一致尽量使用Logisim自带的组件完成设计不要用过于冷门的扩展库否则上传评测时可能出现组件缺失问题。还有一点多周期CPU的题目里如果要求“现代时序”本质上是说CPU采用同步时钟控制每个状态在时钟上升沿更新状态间通过状态寄存器实现。这意味着状态转移逻辑和微指令取用要放在同一个时序体系里考虑组合逻辑延迟必须在时钟周期内稳定调试时尤其注意时钟频率不要设太慢导致肉眼难以分辨状态变化。5. 常见问题与排查技巧实录5.1 指令运行结果全乱先从寄存器堆写路径查起CPU跑起来后寄存器值完全不对这个问题出现频率最高。我的排查顺序是固定的先看写使能信号RegWrite是否正确再看写地址来自rt还是rd有没有搞混然后看写数据来源是ALU结果还是访存数据。用Logisim的探针工具把这几个点全部点上单步跑一条最简单的add指令看信号值是否一路正确流到寄存器堆的写端口。如果写路径全对再看读路径。寄存器堆的两个读端口分别读rs和rt要和指令字段拆分的Splitter一一对应。我犯过一个低级错误Splitter的分配顺序做反了rs和rt的编号对调导致所有指令都从错误的寄存器取数调试了很久才用探针发现。5.2 时钟频率调到多少才能稳定仿真Logisim的仿真时钟是可以调节频率的。CPU电路刚搭完的时候由于存在大量组合逻辑传播延迟时钟频率设太高会导致寄存器采到的数据不稳定。我建议刚开始调试时用单步时钟或者极低频率2Hz左右运行观察每个周期信号变化。全部功能验证通过后再逐步提高频率测试极限。如果时钟频率提高后结果开始出错通常不是时序问题就是竞争问题。检查一下是否有某个寄存器在上升沿和下降沿都被触发写入或者某个组合逻辑的延迟链路过长导致一个周期内信号没稳定。Logisim默认的仿真模型是理想化的但多个组件的传播延迟叠加依然会造成“慢信号”所以信号路径能精简就精简。5.3 数据转发逻辑明明按照标准画的为什么还是错转发逻辑常见的“隐性错误”有几个一是没考虑rd字段为0的情况寄存器堆的0号寄存器永远是0转发会覆盖它本来该有的0值二是优先级处理不对EX/MEM和MEM/WB两个来源同时命中时选了老数据而不是新数据三是转发判断用的是整条指令的rd/rt信号而不是流水线寄存器里当前级对应的字段。我的建议是把转发单元的每一路比较器的输出都引到LED指示器上然后运行一条简单的依赖序列看每一拍哪个LED亮了、哪个MUX被选中。硬件仿真和调试的魅力就在这儿信号流动是肉眼可见的。5.4 Logisim布线的三个实用技巧最后分享三个我天天用的Logisim操作技巧隧道Tunnel同名相连可以把跨区域的长导线变成命名标签整个电路图立即清爽很多。比如所有32位数据总线都通过隧道传输只有控制信号才真正拉线。子电路Subcircuit是模块化的关键。把ALU、寄存器堆、控制单元、指令拆分模块分别封装成子电路主电路只保留模块之间的连接关系查错时会非常直观。引脚标签旁边加上文字说明哪个信号是PCWrite、哪个是RegDst不仅方便自己看课程设计验收时老师一眼也能看出你的设计是思路清晰的。还有一个很多人不知道的功能Logisim的“合并/拆分总线”可以通过Splitter实现任意位宽的分组组合。32位指令拆出6位opcode和5位rs这必须用Splitter。设置Splitter时要特别注意位序是从低位开始排还是从高位开始排搞反了整个指令解析就错了。写在最后整套MIPS流水线CPU做下来我最大的感觉是单周期CPU让我理解了指令是怎么执行的多周期CPU让我理解了控制信号是怎么组织的而流水线CPU让我真正理解了性能优化是要付出代价的——每一份吞吐量的提升背后都是控制逻辑复杂度的暴增。如果你打算自己动手做我建议先不要急着追求跑通所有指令而是先把一条add指令在五级流水线上完整跑通再用lw和beq把三条冒险类型挨个点亮。每一步都把信号图看清楚比最后调通所有程序的一瞬间更重要。这套实验做完你对计算机组成原理很多抽象概念的理解会实实在在提升一个层次。
返回列表