ARTICLE DETAIL

资讯详情

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

MIPS寄存器文件设计与Logisim搭建实验详解:从结构到验证

MIPS寄存器文件设计与Logisim搭建实验详解:从结构到验证 我没有找到你提到的计组头哥实验 实验三 MIPS寄存器实验的原始内容。不过从标题和相关搜索词来看这应该是计算机组成原理课程中一个用Logisim完成MIPS寄存器文件设计的经典实验。下面我基于MIPS寄存器文件的典型实验要求结合我在做单周期CPU实验时的实际经验写一篇完整、可直接参考的实验博文。注意文中给出的引脚命名和电路搭建方式以常见的实验平台如教育版Logisim为准如果你用的版本或实验指导书有不同的引脚定义以你们实验课的官方文档为准。1. 实验背景为什么头哥平台把寄存器文件单拎出来做一次实验很多同学拿到实验三 MIPS寄存器实验这个题目时会觉得有点小题大做——寄存器文件不就是一堆寄存器堆在一起吗但等你真正开始搭单周期CPU的时候就会发现寄存器文件是整个数据通路的心脏。所有指令的运算结果都要写回这里所有后续指令的操作数都要从这里读出来。如果寄存器文件的设计有毛病后面ALU、控制单元、数据存储器做得再对也白搭。头哥平台之所以把这个实验单独列出来是因为它在整个MIPS单周期CPU设计流程中是一个承上启下的关键节点。承上它要求你理解MIPS架构中32个32位寄存器的组织方式理解rs、rt、rd这三个寄存器编号段在指令中的位置和作用启下它要求你掌握如何在Logisim中用多路选择器、译码器、寄存器阵列搭出可读可写的存储部件为后面实现lw、sw、R型运算指令打下基础。这个实验表面上是在搭电路实际上是在考察三件事一是你能不能读懂MIPS寄存器号与名称的对应关系$zero到$ra那套约定二是你能不能设计出正确的读写逻辑读端口是组合逻辑、写端口是时序逻辑三是你能不能理解寄存器文件是边沿触发写入这一核心时序约束。把这个实验做扎实了后面单周期CPU的数据通路对你来说就是一条清晰的流水线而不是一团乱麻。我当年做这个实验的时候头一次体会到了看着简单、做起来全是细节的感觉。下面我把整个实验拆解开从结构、接口、搭建步骤到验证方法一点一点说清楚。2. MIPS寄存器文件的结构拆解32个寄存器、两个读口、一个写口2.1 寄存器文件在MIPS架构中的定位MIPS体系结构规定CPU内部有32个通用寄存器每个寄存器32位宽统称为寄存器文件Register File。它们被赋予了固定的编号和约定的名称。这32个寄存器不是单纯的一排存储单元它们有明确的分工$zero寄存器0永远读出0写入无效。这是硬件上恒零的寄存器MIPS指令集大量使用它来实现清零、比较后做选择等操作。$at寄存器1留给汇编器使用的临时寄存器普通程序不直接使用。$v0-$v1寄存器2-3存放函数返回值。$a0-$a3寄存器4-7传递函数参数。$t0-$t7寄存器8-15临时寄存器调用者保存。$s0-$s7寄存器16-23保存寄存器被调用者保存。$t8-$t9寄存器24-25额外的临时寄存器。$k0-$k1寄存器26-27留给操作系统内核使用。$gp寄存器28全局指针。$sp寄存器29栈指针。$fp寄存器30帧指针。$ra寄存器31返回地址寄存器存放函数调用返回的地址。在实际的CPU硬件实现中我们不需要给每个寄存器设计不同的行为除了0号寄存器只需要提供统一的读写接口由指令中的寄存器编号段来选中具体操作哪个寄存器。2.2 为什么需要两个读端口和一个写端口MIPS指令格式中R型指令如add $rd, $rs, $rt需要同时从两个源寄存器读取数据然后执行运算最后把结果写入目标寄存器。这就要求寄存器文件在同一时刻能输出两个不同寄存器的内容因此需要两个独立的读端口Read Data 1和Read Data 2每个读端口各有5位地址输入。写端口只需要一个因为一条指令最多只会写一个寄存器。但写端口在硬件设计上比读端口复杂读操作是组合逻辑只要地址稳定数据就稳定输出写操作是时序逻辑必须由时钟边沿触发且在写使能信号有效时才会把数据锁存进指定的寄存器。这里有一个关键的设计思想读端口不消耗时钟边沿随时可以读取写端口必须依赖时钟边沿。这也是为什么在搭建电路时寄存器组件必须配置为边沿触发写入而不是电平触发写入。如果用高电平触发写入在时钟高电平期间写使能一直有效数据会持续写入这在单周期CPU的流水线设计中会造成严重的数据竞争问题。2.3 0号寄存器的特殊性MIPS架构规定$zero寄存器永远输出0任何写入操作都会被丢弃。在Logisim中实现这个逻辑有几种方式方式一把0号寄存器的写使能信号强制接地让它永远不被写入。使用时把1号到31号寄存器作为普通寄存器阵列0号单独用常量0替代。方式二把0号寄存器装入寄存器阵列但译码器输出的写使能信号经过一个与门当写地址为0时把写使能信号屏蔽掉。方式三在电路层面不做特殊处理但在验证时不去写0号寄存器。我推荐方式一。理由很简单如果你把0号寄存器也接入写逻辑万一测试用例中出现了写入0号寄存器的指令虽然汇编器不会生成这种指令但手动构造的机器码可能包含你的电路行为就不符合MIPS规范仿真验证时会出错。把0号寄存器写使能直接接地从硬件上保证它永远只读不改最省事也最不容易出问题。2.4 寄存器编号编码的本质读地址和写地址都是5位宽度刚好能编码0到31共32个编号。当你看到指令机器码中bits 25:21是rs字段、bits 20:16是rt字段、bits 15:11是rd字段时要明白这些字段本身就是5位寄存器地址直接接到寄存器文件的读地址和写地址端口上即可。不需要再做任何转换或译码译码发生在寄存器文件内部。这一点对后面的单周期CPU实验特别重要。很多同学在做CPU数据通路时会纠结怎么把指令中的寄存器字段转换成寄存器文件的地址其实完全不需要转换直接从指令相应位段引线接到寄存器文件的地址输入引脚上就行。寄存器文件的内部译码电路会自动把5位编号解析成32路选择信号。3. 接口设计每一个引脚都要知道它为什么存在在Logisim中新建一个子电路命名为RegisterFile。在设计引脚之前先想清楚这个部件需要对外提供什么功能。一个完整的MIPS寄存器文件最少需要以下引脚引脚名位宽方向功能说明Read_Register_15输入第一个读端口的寄存器编号通常接指令的rs字段Read_Register_25输入第二个读端口的寄存器编号通常接指令的rt字段Write_Register5输入写端口的寄存器编号通常接指令的rd字段R型或rt字段I型Write_Data32输入要写入寄存器的32位数据RegWrite1输入写使能信号为1时允许在时钟上升沿写入Clock1输入时钟信号控制写入时序Read_Data_132输出第一个读端口输出的32位数据Read_Data_232输出第二个读端口输出的32位数据需要特别强调的是Read_Register_1和Read_Register_2是输入引脚Read_Data_1和Read_Data_2是输出引脚。别小看这个方向问题在Logisim中引脚方向一旦搞反整个电路就完全不通而且报错信息不一定直观经常需要排查半天。每个引脚的命名最好和实验指导书保持一致。头哥平台的测试代码是通过引脚名来匹配信号的如果引脚名和测试脚本不一致即使你的电路逻辑完全正确也无法通过测试。这一条在实验中最容易踩坑。4. Logisim搭建步骤从放置组件到内部连线4.1 准备好基础组件在Logisim右侧组件库中我们需要用到的组件有寄存器Register位于存储器Memory库中默认位宽为8位需要在属性面板中把数据位宽改成32把触发方式设为上升沿触发。多路选择器Multiplexer位于选择器Plexers库中用于根据读地址选择对应寄存器的数据。默认数据位宽为1位需要改成32位默认选择位宽为1位需要改成5位。译码器Decoder位于选择器Plexers库中用于把5位写地址解析成32路写选择信号。默认选择位宽需要改成5位。常量Constant位于导线Wiring库中数值设为0数据位宽设为32用于实现0号寄存器恒零。分线器Splitter位于导线库中用于在需要时拆分或合并位宽。引脚Pin位于导线库中设置输入/输出方向和位宽。注意这里的寄存器组件是一个带时钟输入的32位存储单元它本身就是边沿触发的。我们不需要自己去搭D触发器阵列直接用现成的寄存器组件即可。如果你使用的是Logisim Evolution版本组件名称和位置可能略有不同但基本功能一致。4.2 搭建写数据通路写数据通路的作用是把Write_Data上的数据写入到Write_Register指定的寄存器中。步骤如下放置32个32位寄存器组件按R0到R31垂直排列在画布上可以排成几列以节省空间。把这32个寄存器的时钟输入引脚CK全部连到一个公共输入引脚Clock上。在Logisim中你可以用导线把所有时钟引脚串在一起或者使用隧道Tunnel标签来连接。用隧道标签会让电路图更清晰强烈推荐。把这32个寄存器的数据输入引脚D全部连到公共输入引脚Write_Data上。同样用隧道标签统一标记。把这32个寄存器的使能输入引脚EN分别接到一个5位译码器的32个输出端上。译码器的输入接Write_Register。关键一步把译码器的输出和RegWrite信号做与运算再接到寄存器的使能端。也就是说只有当RegWrite1且译码器选中了某个寄存器时该寄存器的使能才有效。如果有32个寄存器就需要32个与门。0号寄存器的使能端直接接地常量0确保它永远不会被写入。这里有一个简化的思路有些实验指导书说寄存器组件自带使能端译码器输出直接接使能端即可甚至有些版本的Logisim寄存器组件没有使能端。如果碰上没有使能端的寄存器组件可以把译码器的输出和RegWrite做与运算后接到寄存器的时钟输入端。也就是说只有RegWrite1且该寄存器被选中时时钟边沿才有效数据才会写入。两种方式效果是一样的。不过使用使能端的方式更接近真实CPU设计推荐优先使用。4.3 搭建读数据通路读数据通路的作用是根据Read_Register_1和Read_Register_2分别从32个寄存器中选出对应数据并输出。把所有寄存器包括0号寄存器的数据输出端Q分别接到两个32位多路选择器的数据输入端。第一个多路选择器的选择输入端接Read_Register_1输出端接输出引脚Read_Data_1。第二个多路选择器的选择输入端接Read_Register_2输出端接输出引脚Read_Data_2。这里需要注意多路选择器的数据输入端顺序。Logisim中多路选择器的数据输入是自下而上排列的最下端的输入对应选择值0往上依次对应选择值1、2……31。也就是说R0寄存器的输出要接到最下面那个输入R31要接到最上面那个输入。方向接反了读出来的数据就是错的。如果觉得32个寄存器输出接两个多路选择器太乱可以先用隧道标签把每个寄存器的输出标记为R0_Q、R1_Q……再用同样的标签在多路选择器输入端拉出引线。隧道标签在Logisim中是全局匹配的同一名称的隧道即使不直接连线在逻辑上也是连通的。这能极大简化画布上的导线密度。4.4 处理0号寄存器的恒零输出上文提到0号寄存器我们不让它被写入因此它的实际输出始终是0。但你仍然需要把它接到多路选择器的第0个输入端。这样当读地址为0时读出的数据就是0符合MIPS规范。如果你是超净室里精雕细琢型的选手也可以把0号寄存器组件删掉直接用一个32位常量0接到两个多路选择器的第0个输入端效果完全一样。两种方式我都试过后者更快更干净但可读性稍微差一点因为其他同学看你的电路时可能疑惑为什么少了一个寄存器。在实验报告中解释清楚即可。4.5 设置组件的关键属性这一步最容易被忽略但恰恰是错误高发区。在Logisim中组件的默认属性往往不符合我们这个实验的需求必须手动修改寄存器组件数据位宽改为32触发方式选上升沿触发如果选下降沿测试结果也可能不对因为头哥平台的时钟信号约定通常是上升沿有效。多路选择器组件数据位宽改为32选择位宽改为5。如果不改成5多路选择器只能处理最多4路输入后面接32路数据根本接不上。译码器组件选择位宽改为5这样才会有32个输出端。常量组件数值0数据位宽改为32。输入引脚位宽设置为5或32根据引脚定义标签名和实验指导书一致。输出引脚位宽设置为32标签名和实验指导书一致。所有这些属性都要在放置组件之后、连线之前或者连线之后点击组件在左侧属性面板中修改。改完记得检查一遍特别是多路选择器的数据位宽因为Logisim默认数据位宽是1位如果只改了选择位宽忘了改数据位宽出来的结果会非常诡异——数据只有最低位能通过。5. 验证与仿真不只在头哥平台测试也要自己搭测试电路5.1 手写激励信号做基本功能验证就算头哥平台上最终会跑自动化测试我强烈建议在提交前自己先做一轮手动验证。方法很简单在Logisim中临时添加一些输入引脚和输出引脚模拟几条典型操作观察输出结果是否符合预期。测试用例至少要覆盖以下几种情况写入并读回设置Write_Register5、Write_Data0x12345678、RegWrite1给一个时钟上升沿然后把Read_Register_1设为5观察Read_Data_1是否等于0x12345678。读0号寄存器无论往0号寄存器写入什么数据实际上写不进去把Read_Register_1设为0Read_Data_1应该永远是0。两个读端口同时工作先写入两个寄存器然后分别设置两个读地址观察两个读数据输出是否互不干扰。RegWrite0时写入无效把写使能设为0给时钟边沿寄存器内容应该保持不变。写入一个寄存器不影响其他寄存器写入R3然后读R2R2应该是之前写入的值而不是刚写入的R3的值。这些测试在Logisim中的操作方式是先用时钟组件或手动切换引脚值产生上升沿再用探针Probe组件观察数据输出。如果你觉得手动切换太麻烦Logisim还提供了仿真菜单下的自动仿真功能可以手动给时钟引脚发送边沿信号。5.2 用Logisim的测试向量功能做自动化验证Logisim Evolution版本自带测试向量Test Vector功能可以以文本形式写一系列输入输出期望值一键运行并显示对错。这个功能在做这种寄存器文件实验时极其好用。你可以在仿真菜单中找到测试向量选项里面可以编辑测试用例。测试向量的格式大致是Clock Write_Register Write_Data RegWrite Read_Register_1 Read_Data_1 Read_Register_2 Read_Data_2 0 0 00000000000000000000000000000000 0 0 00000000000000000000000000000000 0 00000000000000000000000000000000上面只是示意实际格式需要根据Logisim版本调整。测试向量功能的关键在于它能同时检测多个输出引脚的期望值并且能批量跑很多组用例。如果你想把验证做扎实把上面的5类测试用例都写成测试向量跑一遍全绿再提交心里就踏实了。需要注意的是测试向量功能对时钟边沿的处理有特定规则建议先在仿真面板里手动跑一个简单用例确认逻辑正确再上测试向量。5.3 一个很容易被忽略的时序问题在Logisim中如果你把Write_Data、Write_Register、RegWrite和Clock同时改变仿真器会按照组件内部的传播延迟逐步更新信号最终结果通常是正确的。但如果你用的是真实验证平台比如基于FPGA的测试环境就必须保证数据信号先于时钟边沿稳定下来这就是建立时间setup time的概念。在纯Logisim仿真阶段需要注意在同一个时钟周期内你先改变写地址和写数据再产生时钟上升沿这样写入的才是新数据。如果你把所有信号在一个瞬间同时改变可能会看到写入的是旧数据。这不是你电路错了而是仿真时序的问题。实际操作中只要记住先给数据再给时钟边沿这个顺序即可。6. 常见错误与排查链路我踩过的坑你大概率也会踩6.1 多路选择器数据输入端顺序接反这个错误最隐蔽也最容易犯。Logisim中多路选择器的数据输入端口是从下往上依次对应选择值0、1、2……的。很多同学习惯性地以为是按从上到下排列结果读地址0读到的是R31的数据读地址31读到的是R0的数据。如果你发现读出的数据整体是反转的十有八九是这个问题。排查方法把读地址设为0观察读出的数据是不是应该是R0里的值如果不匹配检查多路选择器输入端的连线顺序。6.2 数据位宽不一致导致的数据截断Logisim中不同组件的默认数据位宽可能不同。寄存器默认8位、多路选择器默认1位、常量默认16位有些版本是8位如果忘记改成32位数据在传递过程中就会被截断。比如Write_Data是32位但寄存器数据输入端是8位就只能写入低8位。排查方法点击每个组件在属性面板中查看数据位宽字段确认所有涉及数据通路的组件都改成了32位。一个快速检查技巧是看导线的颜色和粗细——32位总线在Logisim中显示为深绿色粗线位宽不同颜色不同。6.3 引脚方向设置错误导致信号冲突把输入引脚的方向属性改成输出、或者把输出引脚改成输入都会导致电路无法正常仿真。更麻烦的是Logisim有时候对这种冲突的报错信息很模糊只会显示红色导线或者无输出。排查方法比较稳妥的是把所有引脚一次性删除按照接口表重新放置并且顺手在标签里加上输入输出标识或者用不同颜色区分方便后期排错。6.4 译码器输出没和RegWrite做与运算如果译码器的输出直接接到寄存器使能端而没有和RegWrite做与运算会出现什么后果只要写地址选中了某个寄存器该寄存器的使能就始终有效无论RegWrite是0还是1。这样一来即使不需要写入的时钟周期寄存器也会被写入当前Write_Data上的数据整个CPU的数据通路就乱套了。有些实验指导书的电路示例中译码器输出确实直接接了寄存器使能端但那是建立在写使能信号通过门控时钟实现的前提下。在Logisim中使用带使能端的寄存器组件时一定要把译码器输出和RegWrite做与运算。6.5 头哥平台引脚名不匹配导致测试失败这是头哥平台特有的坑。实验指导书和测试代码中使用的引脚名可能与你在Logisim中起的名字不完全一致比如指导书写ReadData1你起的名字是Read_Data_1大小写、下划线任何一个不一致都会导致测试报错。解决办法是拿到实验指导书后先把其中接口表里的引脚名原封不动地复制到Logisim的引脚标签里一个字符都不要改。头哥平台的自动测试是通过匹配引脚标签名来注入信号和获取输出的标签名不一致就会读不到结果。6.6 忘记给电路添加时钟信号如果没有给寄存器组件提供时钟输入仿真时寄存器的数据永远不会更新。Logisim中寄存器组件有一个默认的Clock引脚你需要把它连接到某个输入信号上。如果你看到写操作后寄存器数据不变先检查时钟引脚是否连上了再检查触发边沿方向。排查思路总结当仿真结果不符合预期时我推荐的排查顺序是先看引脚标签是否和平台一致再看组件位宽是否全部正确然后检查多路选择器输入顺序最后检查使能逻辑。按照这个顺序走一遍绝大多数问题都能解决。7. 进阶思考为单周期CPU实验做好铺垫7.1 寄存器的写数据来源可能是多路的在完整的单周期MIPS CPU中寄存器文件的Write_Data并不总是来自ALU运算结果。lw指令需要把数据存储器读出的数据写回寄存器jal指令需要把返回地址PC4写入$rajalr指令也需要类似的处理。因此在设计寄存器文件时你应该认真考虑是否把回写数据多路选择Writeback MUX放在寄存器文件内部还是放在外部数据通路中。头哥平台的实验三可能只要求实现寄存器文件本身不涉及回写选择逻辑。但我建议你在心里留个概念寄存器文件只负责存储和读写数据从哪来由CPU数据通路中的其他部分决定。实验四或者实验五做CPU数据通路时你会用到3选1或4选1的多路选择器来选择写回数据。7.2 读端口和写端口的分离设计是RISC架构的精髓CISC架构如x86的寄存器读写通常和运算逻辑紧密耦合而RISC架构如MIPS刻意把寄存器文件设计成独立的、带有对称读写端口的部件这样指令流水线中每一级都能清晰地定义在哪一级读寄存器在哪一级写寄存器。MIPS五级流水线取指、译码、执行、访存、写回中寄存器文件的读取发生在译码阶段写入发生在写回阶段。这也是为什么寄存器文件必须支持两个读端口——因为流水线译码阶段经常需要同时读取两个源操作数。理解这一点之后你就明白为什么实验三的寄存器文件设计如此重要。它不是一次孤立的测试而是为后面流水线CPU中寄存器文件两级操作的时序理解埋下伏笔。7.3 关于Logisim Evolution和普通版Logisim的差异如果你用的是Logisim Evolution目前大多数高校推荐版本界面上会有一些差异组件库名称更细化寄存器组件自带位宽可调多路选择器的属性写法略有不同。还有一点Evolution版支持在子电路上直接添加引脚时设置位宽这比普通版方便。如果你用的是老版Logisim 2.7.x需要在放置引脚后再手动修改引脚位宽。在做实验前确认一下你用的到底是哪个版本。别看这只是个软件工具差异因为引脚属性界面和默认值差别挺大的很多人照着网上的教程做结果因为版本不同导致属性找不到白白浪费时间。7.4 为实验报告准备的关键内容如果头哥平台要求提交实验报告以下内容建议在动手做实验时同步记录寄存器文件接口定义表、各组件参数设置截图、电路连接说明、功能验证的测试用例列表、测试结果截图。尤其要写清楚为什么读端口不需要时钟、为什么写端口需要时钟、为什么0号寄存器不能写这三个核心问题。把这些过程记录清楚实验报告就成功了一大半。8. 写在最后的几个实测心得我在做这个实验时遇到过一个问题提出来供大家参考当时我把32个寄存器按两列放置左边16个右边16个连线时用了大量隧道标签。结果仿真时发现有些寄存器的写入不生效检查了很久最终发现是隧道标签名字里多了个空格。Logisim的隧道标签是全字符精确匹配的多一个空格就是两个不同的网络看起来连上了实际是断开的。所以隧道标签的命名一定要简单、统一、无多余空格命名规则从开始就定好比如WE_R0、WE_R1这种。规格化命名是一个非常实用的技巧。我在搭建时给每个寄存器的使能信号命名WE_R0到WE_R31数据输入统一命名WDATA时钟统一命名CLK。这样画布上几乎没有交叉导线排查问题的时候也可以直接在标签上判断信号归属。强烈推荐用这个方式搭电路省下的时间远超你起名字的时间。另外关于Logisim的时钟组件在做寄存器文件实验时我建议直接用一个输入引脚模拟时钟信号手动切换0/1来产生上升沿而不是用自动时钟Clock组件。原因在于自动时钟的频率太快手动验证时很难看清是哪个边沿触发写入。等你把寄存器文件验证通过、开始做CPU数据通路时再换成自动时钟也不迟。最后说一下我对头哥实验的整体感觉它的自动测试用例通常覆盖了边界情况比如同时读两个相邻寄存器、连续写两个寄存器再读回、验证0号寄存器不被修改。如果你的电路能通过这些测试说明你的寄存器文件功能是没有问题的。但为了后面几个实验少走弯路建议你在完成基本功能后再多想一步如果我加一条lw指令写回数据应该从哪里来这样在后续做单周期CPU时你已经有了清晰的思路不会在寄存器文件这个环节返工。
返回列表