来到 P4,我们正式开启了 Verilog 的长期使用。本次实验课下内容相对简单,我们只需要将 P3 完成的 CPU 翻译成 Verilog 即可。本次博客依旧分成课下实验课上测试两部分。

一、课下实验

利用 Verilog 编写 CPU 相对 Logisim 来讲可能并没有那么直观,但不用再手动拉线,而是回归传统的代码编写方式,反而会更易于上手和修改。

整个代码架构正如指导书一样:mips.v 作为整个顶层模块,将所有模块通过信号的传递连接起来,其就像是我们在 Logisim 中用到的 main 电路图;底层依旧是有各个功能模块和一个控制模块
在这里插入图片描述

编写时一个可行的思路是首先构造各个底层模块,每构造一个底层模块就顺便在 mips.v 中编写对应的模块声明,逐渐形成清晰直接的思路。接下来要做的工作就是对每个模块进行翻译和构造了。

1. IM

IM 模块其实就是之前的 IFU ,通过传入的 NPC 读取到对应的指令 Instr ,并将 NPC 作为当前 PC 输出即可。

需要注意的是从 txt 文件读取指令的方式:

initial begin
	$readmemh("code.txt",im_reg);
	PC = 32'h0000_3000;
end

这样把指令全部读入初始化为 reg [31:0] im_reg [0:8191];的寄存器中,每次读取对应的指令需要进行如下操作:

wire [31:0] midPC;
wire [31:0] address;
assign midPC = (PC - 32'h0000_3000) >> 2;
assign address = midPC[12:0];
assign Instr = im_reg[address];

同时在赋值 PC 时注意 reset 信号的处理:

always @(posedge clk) beign
	if (reset) begin
		PC <= 32'h0000_3000;
	end
	else begin
		PC <= NPC;
	end
end

IM 的模块声明如下:

module IM(
input clk,
input reset,
input [31:0]NPC,
output [31:0]Instr,
output reg [31:0]PC
);

2. CMP

考虑到 P4 课上一般固定有一道跳转类题目,因此相较于 P3 专门新增加了 CMP 模块,用于这些跳转信号的条件判断

其根据输入的 JUMPop 的不同来对 a b 两项进行不同的条件判断,最后均输出 jump 这一信号判断是否跳转:

assign jump = (JUMPop == 3'b000)? (a == b):
			  (JUMPop == 3'b001)? (a >= 0 && a[31] != 1'b1):
			  //....

3. NPC

NPC 模块实现 NPC 的获取输入进 IM 模块。稍微总结一下就知道 NPC 需要哪些输入信号:

module NPC(
input [31:0] PC,
input [15:0] imm16, // 16 位立即数,用于 b 类型跳转
input [25:0] imm26, // 26 位立即数,用于 j 类型跳转
input [31:0] rd1, //寄存器值,用于 jr 类型跳转
input [2:0] NPCop, //从 CMP 得到的 NPCop
input jump, // 从 CMP 得到的 jump
output [31:0] NPC
);

随后依旧是一个 assign 语句,根据不同的 NPCop 跳转到不同的位置。

需要注意的是 b 类型指令只有在条件成立时才跳转,因此其对应的判断条件为 NPCop == 3'b001 && jump

4. EXT

EXT 依旧是我们熟悉的立即数扩展模块,根据 EXTop 实现不同的扩展方式:

assign imm32 = (EXTop == 1'b0)? {{16{1'b0}}, imm16} : {{16{imm[15]}}, imm16};

5. GRF

相较于 Logisim ,Verilog 的 GRF 模块就不需要那么多重复性工作,只需要一个 reg [31:0] regs[0:31]; 即可完成 32 个寄存器的构造。初始化时全部设置为 0,进行写入时判断是否要写入第 0 寄存器即可:

always @(posedge clk) begin
	if (reset) begin
		for (i = 0;i <= 31; i=i+1) begin
			regs[i] <= 32'h0000_0000;
		end
	end
	else begin
        if (RegWrite && A3 != 5'b0) begin
			$display("@%h: $%d <= %h", PC, A3, WD);
			regs[A3] <= WD;
		end
	end
end

6. ALU

写了这几个模块后, ALU 模块其实依旧和它们类似,通过 ALUop 的不同对两个输入数 a b 进行不同计算,得到最后的 ALUresult:

always @(*) begin
	case(ALUop) 
	3'b000 : ALUresult = a + b;
	3'b001 : ALUresult = a - b;
	//......
	endcase
end

7. DM

DM 的构造依旧不难,首先是内部结构 : reg [31:0] dm_reg [0:3071];

DM 涉及到读写两部分,首先是 DM 的读入功能,也就是对应的 l 类型指令,我们以 lb 为例进行分析:

assign Memaddr = ALUresult[13:2]; // 截取ALUresult,获得读取/写入地址
assign byte01 = ALUresult[1:0]; //截取 ALUresult 低两位,用于判断读取/写入的是 32 位的哪 8 位
assign lb_result = (byte01 == 2'b00)? {{24{dm_reg[Memaddr][7]}},dm_reg[Memaddr][7:0]}:
				   (byte01 == 2'b01)? {{24{dm_reg[Memaddr][15]}},dm_reg[Memaddr][15:8]}:
				   //...
				   :32'b0;//根据 byte01 获取指令最终要读取的数据
//最后根据不同指令得到不同结果
assign DMresult = (DMop == 3'b000)? dm_reg[Memaddr]:
				  (Dmop == 3'b001)? lb_result : 32'b0;

写入的功能类似,在时钟上升沿判断 MemWrite 是否为 1,进行对应写入和输出即可。

8.control

完成了功能部分,随后就是控制信号的 control 模块。 control 部分的代码较多,因此需要梳理好逻辑,尽量更为直观易懂,方便进行后续的复习和课上测试

这里以部分指令为例,分享出自己的 control 构造:

//control.v
//part1 : module 声明部分
module control(
    //输入部分
    input [31:0] Instr,
    input jump,
    
    //输出部分 1 : 立即数和地址
    output [15:0] imm16,
    output [4:0] grf_rs_addr,
    output [4:0] grf_rt_addr,
    
    //输出部分 2 : 判断信号
    output ALUsrc,
    output MemtoReg,
    output RegWrite,
    output [2:0] NPCop,
    output [2:0] ALUop,
    //....
);

// part2 : 拆分 Instr
wire [5:0] op;
wire [5:0] funct;
assign op = Instr[31:26];
assign funct = Instr[5:0];
//rs rt 等不再列出,后续同理,只列出部分内容

//part3 : 获取指令信号
wire add = (op == 6'b0 && funct == 6'b010000);
wire beq = (op == 6'b000100);

//part4 : 获取判断信号
assign RegDst = add || sub || jarl;
assign DMop = (lw)? 3'b000:
			  (lb)? 3'b001:3'b000;

//part5 : 赋值rs rt rd 地址
assign grf_rs_addr = rs;
assign grf_wd_addr = (RegDst)? rd:
					 (jal)? 5'd31:rt;
endmodule

9. mips顶层模块

最后就来到了 mips 顶层模块的构建。考虑到需要构造很多变量,因此我选择在每个模块上方构造该模块需要的变量,从而方便修改,防止遗忘。例如:

wire [31:0] NPC,Instr,PC;	 
IM im(
  .clk(clk),
  .reset(reset),
  .NPC(NPC),
  .Instr(Instr),
  .PC(PC)
  );

除了连接起各个模块, mips 顶层还需要进行一些数据的选择,例如 ALU 部分第二个计算数是寄存器值还是立即数: .b((ALUsrc)? imm32:grf_rd2_data),

综上我们就完成了整个单周期的 CPU 构造。

二、课上测试

转战到 Verilog,其实更方便我们在课上测试的时候编写代码和 debug。依旧秉持和 P3 一样的思路,我们能总结出大致有哪几种题型。不同于 logisim 画图,在 Verilog 上我们能够做出更为充足的准备。另外,以下的内容都是基于自己个人的代码展开,你也可以结合你的代码为课上测试做好准备。

1.计算型

首先依旧是计算型,新增一种新的计算方式。第一步需要在 ALU.v 中增加新的计算。在课上测试前,我通过注释的方式已经设置好新的信号选择,因此正式测试时只需要填空即可:

// ALU.v
always @(*) begin
	case(ALUop)
	3'b000: ALUresult = a + b;
	//...
    //3'b101: ALUresult = [ ]  注释部分,课上测试只需直接填写[]中内容即可

随后在 control.v 中新增指令声明、信号选择:

// control.v
/*指令声明*/
//wire [] = (op == 6'b[]);
/*信号选择*/
assign ALUop =  (sub || beq)? 3'b001:
                (ori)? 3'b010:
				(lui)? 3'b011:
				(jal || jarl || bgezal||bltzal)? 3'b100:3'b000;
				//([])? 3'b101:3'b000;

在亲身进行完课上测试后,自己对这种方法有了更多的想法:其实即使是提前用注释的方式写,最多也就为你节省了一两分钟,少敲了几个字母而已。因此,最重要的应该是熟悉每一种指令在自己的架构上怎么加?需要改动哪些地方?可能会产生哪些 bug? 而所谓的注释只是你熟悉思路的一种方式!切勿本末倒置!

2.跳转型

跳转类指令一般分为 b 类(有判断条件,如 beq) 和 j 类(无判断条件,如 jal)。一般情况都是 b 类指令。

b 类跳转指令一般涉及到判断条件,因此第一步我们需要进行 jump 信号的运算:

在 CMP.v 中新增 JUMPop 及对应的判断条件:

assign jump = (JUMPop == 3'b000)? (a == b):
              (JUMPop == 3'b001)? (a >= 0 && a[31] != 1'b1):
			  (JUMPop == 3'b010)? (a < 0 || a[31] == 1'b1):1'b0;
			//(JUMPop == 3'b011)? []:1'b0;

在这里还需要强调的一点是,如果你也选择添加一些注释帮助自己完成题目的话。一定不能因为它们影响到自己程序原本的正确性。同时就像上述代码,当你把第 4 行代码的注释删掉后,还需要把第三行的 :1'b0删掉,否则程序就无法正确编译了!

第二步在 NPC.v 中新增新的 NPCop 及对应的跳转地址,有时可能会涉及到一些其他的寄存器 (例如 rs )。此时我们还需要增加新的输入信号。切记:每次改动模块的输入输出信号都需要在顶层 mips.v 做出对应修改!

最后还是在 control.v 中设置指令、增加信号 JUMPop 、NPCop 等等。

另外:在为新的指令设置信号时很容易漏掉或者多添某些信号,在设置时参考已有的指令能有效避免这个问题。例如在增加 b 类指令时参考 beq 都设置有哪些信号。

而 j 类跳转指令一般不涉及判断条件,因此第一步可以进行省略,剩余的步骤类似。

3.访存型

访存型一般是三道题目中的最后一题,也可能是最难的一题。可以分为 l 类和 s 类,即读取和写入。

这类题的大致思路其实不难,主要涉及到对 DM.v 和 control.v 的一些操作。难就难在这种题可能会涉及到对的一些操作,例如算数右移、位的拆分合并、判断1/0的数量、按位取反等等(不只是访存类题,每道题都会对这种知识或多或少的涉及)。由于时隔久远,自己很难想起来具体常见的操作,重要的是对 verilog 的基础语法没有理解错误的地方、读清楚题干的要求、理清思路、一步一步按要求开始进行编写!

4.tips

事实上,即使我们准备得再全面,但我们终究不知道最终的题目会是什么,因此总会出现各种各样的突发情况。在昏暗的新北地下考场,完成任何事情似乎都比平常更加艰难~ 因此总结出一些小的提醒和建议,帮助你更好的进行课上测试:

  1. 一定、一定、一定要构造样例!!!不要干瞪着代码!很多问题只有实践才能出真知,只看是看不出来的。
  2. 考前可以做好一些必要的准备,避免在不必要的地方浪费时间。例如:构建好 verilog 文件,考试提供的就是你提交的包含 .v 文件的压缩包。
  3. 考试时会发布魔改版 MARS,用于测试新的指令,因此阅读好相关提示进行操作。
  4. 有了我们前面总结的各个类型的思路,基本可以确定如果提交之后有 bug ,就是题干条件的编写有误。例如题干要求 “a 整除 b”,而自己编写成了 a / b (笔者本人亲身经历,寒心!!!)。构造好各种类型的样例进行测试!

以上就是 P4 实验的全部内容,祝你计组顺利!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐