文章目录
计组P3&P4碎碎念
前言
其实这篇总结在上周过了p3的时候就应该写出来的,但本人苦于近一周各类事务过于繁忙,因此把上一次的给鸽了。这周过了p4,感觉事实上这两p的道理都差不多,坑点大致类似,p3踩过了几个小坑后p4也就进行得比较顺利,因此在这里将这两p的总结合在一起写一写,顺便告别单周期CPU的时代,迈步进入流水线阶段。
在本文中,您可以收获:便宜的单周期CPU搭建经验(包括胎教级别的logisim搭建操作、Verilog搭建操作以及加指令操作)、本人两次上机的真题概述、便宜的心得。但是请注意:严禁抄袭代码。
p.s. 总是在总结文里使用刃牙表情包的我……要是被不认识の同学线下开盒了,大学生涯就要结束了罢……
正文
初见单周期CPU
初见单周期CPU时,想必大家都在课件上见过这样一句话:CPU的功能是控制指令执行,过程是取指、取数、执行(运算或从内存中取数存数)。请不要将这句话当作课件套话一笑而过,事实上,这句话就是整个单周期CPU设计实验的真正诀窍。只要搭建时严格按照这一过程建立CPU框架,新加指令时以RTL描述为基准、严格按照这一过程添加或修改信号,就能够轻松地应付p3、p4的实验。
或许同学们对R型指令、I型指令、J型指令这样的字眼也很熟悉,它们也是不可忽视的一点。根据不同指令,我们能够总结出不同的[固定搭建方法],这在添加新指令时是非常有效的。为了方便记忆,我们可以把它们记成 [算术逻辑运算类]、[立即数运算类](事实上取数存数和有条件跳转的过程也就是对立即数的运算过程,只不过把结果当成了内存地址 / PC值而已)和 [无条件跳转类]。
在实验的实际搭建过程中,指令的执行过程可以大致被总结为:取指 -> (查找指令集中对应指令的RTL语言) -> 取数 -> 运算 -> (内存存取操作) -> (向寄存器写值操作) -> 计算Next PC值
下面分别给出三类型指令的固定执行方法。
- 算术逻辑运算类
- 读指令 -> 控制器译码 -> 从寄存器堆GRF取所需数 -> 送从GRF读到的数入ALU运算 -> 写结果入GRF -> PC + 4
- 立即数运算类
- 读指令 -> 控制器译码 -> 从寄存器堆GRF取所需数 + 对立即数 / 地址进行对应的扩展操作 -> 写结果入数据存储器DM / 从数据存储器DM读数据 / 直接运算Next PC值实现跳转(针对Branch类指令) -> (写结果入GRF)-> PC + 4(针对非Branch指令)
- 无条件跳转类
- 读指令 -> 控制器译码 -> 从寄存器堆GRF取所需数 -> Next PC = 地址运算所得结果(一般通过拼接实现,当然上机时新加指令可能会需要你通过ALU计算实现)
下面分析CPU控制指令执行的三个过程对应的单元(你需要搭建的部分)。
-
取指
- 从指令存储器中取出指令并分析指令 -> 对应【程序计数器NPC部件、指令存储器IM部件、控制信号生成单元CU部件】
-
取数
- 从通用寄存器堆里取出需要的数 -> 对应【通用寄存器堆GRF部件】
-
执行
-
运算或者存取数据 -> 对应【算术逻辑运算单元ALU部件、数据存储器DM部件、数据扩展Ext部件】
// Ext的实现可有可无,但有了一定更方便,实现对立即数/地址的有符号、无符号扩展以及加载到高位,如果新加指令对立即数有任何的骚操作,都可以放在这里执行
-
大致框架已经分析结束,然后就可以直接进行单周期CPU的搭建了~(下面的内容将是Logisim和Verilog结合的)
具体搭建步骤
首先请你先决定:我需要实现多少个指令?这将与各个指令控制信号的位数强相关。我的建议是:ALU控制码采用4位,数据存储器和指令存储器采用32bits * 1K的规模,扩展部件Ext采用2位。现在的你可能会对如何搭建手足无措,我认为平地起高楼,应该先从与具体信号低相关甚至无关的部件入手,比如ALU部件。那么我们先实现ALU。
算术逻辑运算单元ALU
ALU部件事实上是一个运算器黑盒子。输入运算数A、运算数B和ALU控制码ALUOP(选择进行何种运算),并在内部进行运算处理后,我们将希望得到的结果从Result输出。同时,为了满足Branch信号的判断要求*,我们添加一个零判断输出。有闲情雅致的话可以多做一个溢出检测考验一下自己的熟练度,但根据本人的上机经历它并没有什么用。)
*判断要求:举例说,如果我们希望实现beq指令,即从寄存器堆取出的两个数相等时跳转,那么我们只需要选择运算为[减法],如果结果为0,那么零判断输出为1,与Beq控制信号进行与运算以后决定PC是否进行有条件跳转。在课上遇到新加指令类型为[有条件跳转]时,我们都可以好好利用这个零标志位,为自己的跳转进行条件判断。
- Logisim
- Logisim实现是显然的,把ALUOP当作MUX的选择信号,用MUX对不同运算的结果进行选择就可以了。内置的各种运算单元已经足够满足我们的需求。
-
Verilog
- 在Verilog中善用宏定义`define,代码的书写也是显然的。
`define AND 4'b0000 `define OR 4'b0001 `define ADD 4'b0010 `define SUB 4'b0011 `define LTU 4'b0100 `define GTU 4'b0101 `define SLL 4'b0110 `define SRL 4'b0111 module ALU( input [31:0] A, input [31:0] B, input [3:0] OpCode, output [31:0] Result, output Zero_Sig, output Overflow_Sig ); reg [31 : 0] Re; reg Overflow; assign Zero_Sig = (Result == 32'd0) ? 1'b1 : 1'b0; assign Result = Re; assign Overflow_Sig = Overflow; always@(*)begin case(OpCode) `AND: {Of, Re} = A & B; `OR : {Of, Re} = A | B; `ADD: {Of, Re} = A + B; `SUB: {Of, Re} = A - B; `LTU: begin Re = (A < B) ? 32'd1 : 32'd0; Of = 1'b0; end `GTU: begin Re = (A > B) ? 32'd1 : 32'd0; Of = 1'b0; end `SLL: {Overflow, Re} = (A << B[4 : 0]); `SRL: {Overflow, Re} = (A >> B[4 : 0]); default: begin Re = 0; Overflow = 0; end endcase end endmodule
指令存储器IM
下一个指令低相关的部件是指令存储器。在Logisim中,我们只要用一个只读存储器ROM指令就可以轻松完成了。如果你的指令存储器容量是32,那么取PC[6:2]为地址,如果是1K就取PC[11:2]为地址,以此类推。
在Verilog中,存储器通过这样的语句实现:reg [BitWidth - 1 : 0] Memory [Num - 1 : 0]
,比如,想要实现一个32bits * 1K的存储器,就可以通过这样的语句实现:reg [31 : 0] instrumemory [1023 : 0]
。
module InsMemory(
input [31:0] PC,
output [31:0] RD
);
reg [31 : 0] instrumemory [1023 : 0];
assign RD = instrumemory[PC[11 : 2]];
endmodule
同时,强烈建议将得到的指令在IM中按字段分好(本人在自己的实验中已实现,只是在截图中没有体现)。为免去大家查找指令集的麻烦,具体的字段分割如下图所示:
-
你可能会遇到的问题:
-
怎么往ROM里读指令?
-
Logisim:右键单击ROM部件,点击
Load Image
选项导入机器码txt文件,切记文件头需要有v2.0 raw
。 -
Verilog:使用
$readmemh("code.txt", instrumemory);
语句向内存中导入code.txt里存储的机器码,注意要将这个文件通过Add source的方式加入到ISE左侧的Project列表里;同时,这个操作应当通过initial块实现。
-
-
为什么地址从PC值的第2位开始呢?(同时是p4的一道思考题)
- 通过参考MIPS32指令集中的AddressTranslation一节,利用数据存储器存取数据时,对操作码(也即从ALU传入的32位运算结果)的处理行为是这样的:
-
可以看到,为了解析内存调用行为:
传入的32位Address数据会被翻译成:给定的虚拟地址vAddr,决定调用指令还是数据的信号IorD -> 找到对应的物理地址pAddr,以及用于解析内存调用的高速缓存一致性算法。因此低2位在从内存中取数时会被用作访问索引IorD,决定是否访问指令或数据,并查找[AccessLength]这一字段内容中的数据;
通用寄存器堆GRF
对于Logisim,通过了p1(?我不记得了)的同学应该已经清楚其实现了,不再赘述,疯狂ctrl c + ctrl v即可。
对于Verilog,其实现要简单得多,无非是利用上面提到的语句,建立reg [31:0] Registers [31:0]
即可。但是需要注意:请额外判断0号寄存器的输出,已经观察到有同学在这里出错导致WA了。同时也要注意用Initial块帮所有寄存器初始化为0。
题外话:请各位一定要养成对[存储记忆部件]进行Initial操作的好习惯,p4上机完后竟然看到有人因为这个出错而没过,还是很可惜的。
Ext扩展部件
没什么好说的,定下0扩展(无符号扩展)、有符号扩展、加载到高16位的操作码进行黑盒子操作,跟ALU的思路一模一样。可以留下一位给课上或许会出现的新加指令骚操作。
Verilog中的实现就更简单了。
module Ext(
input [15:0] Imm,
input [1:0] Ext,
output [31:0] Result
);
assign Result = (Ext == 2'b00) ? {{16{1'b0}}, Imm} :
(Ext == 2'b01) ? {{16{Imm[15]}}, Imm} :
(Ext == 2'b10) ? {Imm, {16{1'b0}}} : {{16{1'b0}}, Imm};
endmodule
数据存储器DM
至此,与指令低相关的部件都已经搭建完了。现在只剩下控制信号生成单元和数据存储器两个大部件,以及Next PC的计算和一些稀稀拉拉的MUX了。我们先来实现指令相关程度更弱的数据存储器。
前面我们可以看到,针对【立即数运算类】指令的固定操作,[送数入ALU运算]后接着的是[写结果入DM],这个”写结果“的操作表现为将ALU运算得到的结果或者GRF中取出的数作为数据存储器输入的地址A或者存入数据WD。通过查阅指令集,我们总结出规律:存入存储器的数据一般是GRF[rt],也就是GRF的RD2输出结果;而输入的地址A一般是ALU运算得到的结果。(当然,课上或许会有出现例外的指令,但这是正常指令的规律)。
对于DM内部的行为,如果只求实现基本指令,那么只用一个RAM就可以解决,思路同前面的ROM。(记得在Logisim中要将Data Interface改成Separate load)。
但如果你想要锻炼自己的搭建操作,就可以试着实现lh、lb、sh、sb等位宽不同的操作。事实上通过在指令集中查阅相应指令的RTL就可以轻松地搭建出这四个指令。在这里给出或许不太容易做出的Logisim的参考,Verilog的实现很简单,不再给出了。
控制信号生成单元CU
这可以说是单周期CPU中搭建最为关键的一步,但在我看来,又是搭建过程中最简单的一步,只需要无脑查表、填表、搭电路即可。首先确定我们需要怎样的控制信号:
-
寄存器写使能RegWrite - 我想往寄存器里写东西时这个值为1
-
数据存储器写使能MemWrite - 我想往数据存储器里写东西时这个值为1
-
算术码ALUOP - 选择这个指令需要怎样的运算操作
-
扩展指令Ext - 选择进行哪种类型的扩展(这里务必通过查指令集的RTL保证正确性,已经见过有人在此出锅)
-
寄存器写入地址来源选择信号RegDst - 观察不同指令的RTL,我们发现有的结果需要写到rt寄存器,有的需要写到rd寄存器,因此需要用一个MUX来选择
-
寄存器写入地址来源选择信号Jal - 如果你做了Jal指令,就会发现其中有一个(GRF[31] <- PC + 4)的操作,这个时候需要选择写入地址为31号寄存器还是上面得到的rt / rd,用一个MUX来实现(强烈建议实现Jal,课上两次都出现了GRF[31] <- PC + 4的组合操作!届时只需要让该指令跟Jal共用一个选择信号即可!!)
-
选择写入寄存器堆的值为运算器运算结果还是存储器中取出的数的选择信号MemToReg - 想把内存中的值写入寄存器(如load指令)时为1
-
选择ALU的操作数B为立即数还是寄存器堆中取出的数的选择信号ALUSrc - 想跟立即数运算时取1
-
位宽信号BW - 如果你做了上面的lh等不同位宽指令,那么就需要一个这样的信号选择当前位宽
-
j、jr、beq、blez……:选择Next PC值为PC + 4还是该指令对应的跳转地址
-
【?】:根据课上新加指令添加的新选择信号,通常用来选择运算数 / 写入数据 / 写入地址。涉及PC运算的控制信号一般可以与上面的合并。
因此在课上得到新指令时,我们应当先分析指令的RTL,看看需要更改这些控制信号的值为什么,以及需不需要新加控制信号。
-
Logisim搭建小技巧
-
用小手将OP、Func点成对应指令的值(如果Func不关心,就只与OP的6个信号;如果Func关心,就把12个信号一起与起来),然后把变绿的线全部用一个与门连起来就可以了,这个方法还可以有效排除课上指令是否与课下另做的指令重合,个人认为比用Tunnel实现好得多。
-
善用文字工具(图标A)添加注释
-
-
Verilog搭建说明
- 比在Logisim中简单得多,无脑或起来 / 无脑三目运算符就可以了。
- 善用`define
Next PC计算
根据跳转指令的RTL具体描述进行选择就可以了。不过在Verilog中要千万注意初始化的问题!(Logisim中的实现很简单,用几个MUX串在一起就可以了)
module NPC(
input Clk,
input Reset,
input Branch,
input J,
input Jr,
input [31:0] imm,
input [25:0] Address,
input [31:0] PC_JR,
output [31:0] NextPC,
output [31:0] PC_4
);
reg [31 : 0] NPC;
reg [31 : 0] NPC_4;
assign NextPC = NPC;
assign PC_4 = (NPC + 4);
initial begin
NPC = 32'h0000_3000;
end
always@(posedge Clk)begin
if (Reset)begin
NPC <= 32'h00003000;
end
else if (Branch == 1'b1)begin
NPC <= ((imm << 2) + (NPC + 4));
end
else if (J == 1'b1)begin
NPC <= {{NPC_4[31 : 28]}, Address, {2{1'b0}}};
end
else if (Jr == 1'b1)begin
NPC <= PC_JR;
end
else begin
NPC <= (NPC + 4);
end
end
endmodule
至此,所有部件搭建结束。
接线
-
Logisim
-
参照黑书,把你前面建成的部件全部连起来。
-
-
Verilog
-
现在,你该认识到课本中”wire实际上就是导线“这句话的意义所在了。在顶端模块中不断引用前面搭建好的部件(Design Utilities -> View HDL Instantiation Template -> 复制),用定义的wire将它们连接起来!
-
如果在上机时,你的测评反馈出现了zzzz的情况或者output fewer than expected的情况,请优先考虑自己是不是线接少了/浮空了,对于这一点的避免,我建议将Logisim中搭建好的直观电路图截图并贴到实验报告pdf中去,对着直观电路图一一对照自己的接线是否有错漏,这样是非常方便且不容易出错的。
-
测试
将机器码文件导入IM部件中,执行测试。具体的对拍等方法讨论区已经珠玉在前,这里不赘述了。
上机真题与经验
要来力,要来力
p3
- BEZAL:Branch if equal to zero and link
- 注意如果不符合跳转条件的时候不要link!!!也就是说,RegWrite的取值不能简单粗暴地根据该指令的OP码定,而是OP && ZeroJudge定,本人在这里de了一个半小时才发现,深有体会!其实这一点也已经在RTL中明明白白地写出来了,只是当时太紧张……
- 忘了名字的I型指令,但是需要实现左移并补1,这一点可以直接通过数学计算实现(请自己思考,简单的二进制运算)而不是复杂的移位,当时看到旁边的老哥花里胡哨的splitter我都惊了……
- 名字忘了,操作是:GRF[rt] <- (GPR[rs] + sign_extend(offset) + rt * 4),很经典的一个I型指令,只要吃透了前面的[固定执行方法]直接就秒了。
p4
- RLB:将GRF[rs]的低Imm位按位取反以后存入GRF[rt]
- 经典的R型指令,没什么好说的
- BNEZALC:如果GRF[rs] ≠ 0,跳转到label所指的地址(就是Branch的地址,所以我直接让它们共用了一个控制信号)并且GRF[31] <- PC + 4(康康,这不就是Jal的行为吗,直接合并控制信号,秒了)
- 看到开头的B我立马警惕了,这里跟p3一样,一定要注意RegWrite的条件取值。踩过p3的坑以后,这一点就不值一提了。
- LWRR:将从Memory[GRF[rs] + sign_extend(offset)]取出的值循环右移vAddr[1:0]个字节后插入到GRF[rt]中(vAddr = GRF[rs] + sign_extend(offset))
- 又是一个非常经典的I型指令,跟p3的T3一模一样,还是那句话,吃透了前面三种不同类型指令的[固定执行方法]后直接秒掉。
总结
- 吃透三种不同类型指令的[固定执行方法],按步骤搭建、执行
- 读清楚RTL描述!! p3第一题就差点栽在这一点上,一定要读清楚要求,分析属于哪一类指令,然后选择执行方法。
- 我主要栽在之前总结[固定执行方法]的时候就忽视了条件跳转中的[条件]一词,盲目地根据指令决定了RegWrite的取值,虽然临交卷时发现了,但终究是浪费了不少时间。请大家一定要引以为戒。
- 做到以上两点,可以说单周期CPU的两次上机基本上就没有什么难度。除了p3第一题的疏忽,后面5道题按照此步骤执行的本人均做到了不用测试就能一交AC。
回望初见单周期CPU时的迷茫,还是生出了不少感悟,非常希望这篇文章可以切实地帮到不知道从何建起的同学、正在经历p3、p4实验的同学以及在p3 / p4考试中不慎失误的同学!(没人看也没关系hhh)如有错误,请大家不吝赐教;如果有什么别的想知道的内容(助教问答?思考题?)也欢迎在讨论区告知。(土下座)