计算机组成原理课设指南-基于FPGA的具有MIPS风格指令集的CPU设计

前言

笔者刚做完这个课程设计,希望把经验整理成这篇文章供学弟学妹们参考。

(2022.4.24更新:笔者学完了计算机体系结构,再回来看这篇文章发现真是漏洞白出,因此添加和修改了很多内容)

刚开始做这个课设的时候,笔者也是一头雾水的状态,计组这个学期学完考完之后就忘了不少,而EDA课设的题目实在太简单,目的只是让我们学会verilog语言和熟悉vivado的使用。所以计组课设一上来就要求我们设计与实现CPU而且几乎没给教程,时间也很紧张,只有一周多点的时间,所以笔者认为这是很不合理的。如果全靠自己摸索就很难有思路,也容易走弯路。

这篇文章是写给和我之前一样完全没接触过硬件这方面的小白的,所以很多地方的语言表述与硬件设计并不标准,如有错误欢迎指出。

这个课设到底要做什么

首先就是这个课设的目标,我们到底要做什么?

我们首先要明确,这个课设不同于EDA课设的完全使用硬件实现一个小功能,而是要用指令(软件)组成一段程序来实现功能,硬件部分只是用于组成一个以CPU为核心的SOC(片上系统)。我们需要设计实现一个能执行一些指令的CPU,然后以这个CPU为核心组建一个SOC,然后让它执行一系列指令。而指令其实就是一串只有0和1的机器码,由编译器把汇编语句编译得到(在这个课设中我们可以使用MARS这个软件快速地将汇编程序转换成一行行机器语言)。我们最终需要实现的程序就需要用汇编语句实现。

开始设计

准备

不管题目是怎么样的,这个课设都需要我们做一个简单的CPU出来,这是整个课设的重点。如果这个CPU已经能够正常执行一些指令,那么其实这个课设已经接近完成了,剩下的就是用这些指令设计汇编程序,然后把指令交给CPU去执行就行了。当然对于不同的题目还需要考虑不同的外设模块,但CPU部分都是大同小异的,因为这个阶段还不用考虑《计算机体系结构》里面的那些复杂设计。

我们需要的知识有:计算机组成原理、verilog、汇编语言(这里是MIPS指令集)以及vivado与FPGA的基本使用方法。

分析实例

实验指导书中给了我们一个现成的MIPS系统,我们如果能够彻底研究明白它的运行机制就可以很轻松地在它上面加以修改,以实现我们的需求。(这本指导书值得仔细阅读,对于课设相当重要)

首先,这个CPU最主要的特点就是:使用了多周期的流水线设计

这里简单解释一下,首先我们的指令并不是在一个时钟周期里就能完成它的操作的,经典的五段流水线中,所有指令的执行都需要5个周期,分别是:取指(把指令从指令存储器中取出,放到指令寄存器等待执行)、译码(分析OP字段的含义,从而发出不同的控制信号进行不同的操作)、执行访存写回(将结果放回寄存器)。

那么我们可以选择这样一种方案:一条一条地执行指令,在前一条指令的5个周期都结束后再开始下一条指令的取指、译码、执行、访存、写回。这就是无流水线设计。这种设计显然效率比较低,因为例如当前一条指令执行到译码阶段时,CPU的取指模块就处于空闲状态了,那此时完全可以流入下一条指令进行取指,并且也不会影响上一条指令的正常运行。这种设计就是流水线了。

如果仔细思考这种流水线设计,我们就会发现,这种设计很容易出现一个问题,那就是后一条指令在译码段读取寄存器数据时,有可能前一条指令还未将新数据写回,导致读的还是旧数据。这个问题在计算机体系结构中是需要重点考虑的问题,但是奇怪的是我在整个课设的过程中从来没有发现这个问题,但是它居然也不影响执行结果。当我时隔接近一年之后回过头再来看这个课设,具体细节早就忘记了,也许是某些节拍上的小设计解决了这个问题,但是我已经记不清了,这个问题就只好交给大家去思考了(才不是因为懒)。

然后是其他的一些很重要的特点:样例MIPS系统特点
对照书上源码(文章最后也附上了源码)可以看到,这个CPU的数据线只有8位宽,而指令有32位,所以取指需要取4次,而如果数据线能拓宽至32位则只需取1次。此外,这个课设的要求也是需要32位的,所以我们晚点需要对其进行修改。

样例中已经实现了9条指令,使用这些指令已经可以实现相当多的程序了。注意,其中的add,sub,and,or,slt属于R型指令。

源码中几乎没有注释,在这里说明一下各部件的功能:

  • top
    最顶层,连接CPU与存储器
  • mips.v
    CPU顶层,与存储器memory相连,从memory里读取指令/数据交给CPU处理
  • controller
    控制单元,输入op字段,输出各控制信号
  • alucontrol
    处理R型指令的funct字段,通过其确定alucont信号
  • datapath
    数据通路,用于实例化并连接各器件以传递数据
  • zerodetect
    判断输入的数据是否为0
  • flop
    D触发器
  • flopen
    带使能的D触发器
  • flopenr
    带复位与使能的D触发器
  • mux4
    四选一选择器
  • mux2
    二选一选择器
  • regfile
    通用寄存器

下面它的结构图,我们可以对照着代码,尝试将一条指令代入其中观察执行过程,初步理解CPU是如何处理指令的。
在这里插入图片描述
从PC开始,先从存储器Memory里读取PC对应的指令,送至指令寄存器instr[31:0],然后取其高6位(op字段)送入控制单元Controller进行译码,也就是将op转化位各个器件的控制信号,比如alusrca信号就是一个二路选择器mux2的控制信号,若alusrca=0则将PC放入ALU的输入端a,若alusrca=1则是把通用寄存器读出的数据1放入ALU的a端。

值得一提的是,这里通用寄存器可以同时读2个数或者写入1个数,ra1 ra2表示读的寄存器地址,输出数据为rd1 rd2,wa表示写的寄存器地址,wd是写入的数据。

指令的执行节拍

通过初步地阅读代码,我们能大概了解一条指令中数据是如何传递与操作的,那么为了更精确的确定执行动作的时间与指令的执行时长,并且方便更多指令的设计与实现,我们需要一个控制信号状态转化图。
在这里插入图片描述
这个图可能乍一看很复杂,但其实相当简单。它的每个状态都对应一个节拍下各个控制信号的状态,0123表示4个取值动作,也就是说取值需要4个节拍,在3号状态结束后才算取指完成.4号状态就是开始译码,值得注意的是,0123号状态的alusrca=0,alusrcb=01,aluop=00,也就是说,每个节拍下都会让PC+1,因为PC是按字节寻址,而一条指令是4个字节,所以4个节拍下来刚好PC+4(这个数还在aluresult里面),对应下一条指令。

接下来是4号DECODE节拍,这个节拍中把aluresult的值传给PC(就是PC+4)而ALU中a端传入的还是PC,b端传入的是BEQ指令对应的offset字段(跳转指令条数),aluop=00,也就是把PC与offset左移2位(即乘4,因为按字节寻址)相加,这个数就是跳转目标指令。这个节拍中开始把指令的各个字段送到不同的单元。

接下来的一个节拍就要根据不同的指令发出不同的控制信号。比如,BEQ就是把两个寄存器的数相减得到一个结果是否为0的zero信号,然后决定是否需要跳转,若需要跳转,则pcen=1,将PC=aluresult,此刻的PC就是跳转的目标,而若不跳转,则pcen=0,不另外赋值,PC仍是PC+4。

接下来的一个节拍,对于BEQ或者J指令,就直接开始下一条指令的取指,对于R型指令,就还需要一个回写阶段,把计算的结果存回寄存器。

作出修改

首先我们需要把8位数据线改为32位,除了把所有的WIDTH从8改为32以外,还要考虑到,此时的取指只需要1个节拍,所以需要对取指节拍进行相应修改,较为简单,此处不再赘述。同时,这里我们的PC可以不再采用按字节寻址,而是改成按字寻址,这样PC只需要+1,存储器里读指令也不需要再除以4(右移2位)了。但是,由于修改了PC寻址方式,所以跳转指令也需要作小小的修改,比如J的后26位不再需要左移2位(乘4)了,这里就不详细说明了。

添加外设接口

笔者一开始设计时由于不了解外设应当如何连接,所以直接将其连接在通用寄存器上了,也就导致与其相连的寄存器被限制只能存放这个外设端口的相关数据,这种设计是不对的。我们可以在存储器中将一些地址专门分配给外设使用,将需要显示的信息先保存到存储器中,再通过外设接口输出。

(其实这个部分在计组的IO那章应该有讲到,但是笔者没认真学 ==)

结束

至此,所有的硬件部分都已基本完成。接下来的任务就是编写汇编程序,转成机器码之后放入存储器让CPU执行。推荐使用Mars4.5编写汇编,这是一个很小的.jar但是功能齐全。

至于源码,我并不建议参考我的项目,我认为把样例代码进行修改要比直接照搬现成源码好得多,一是你会更熟悉CPU的结构,对能力的提升很有帮助,二是现成的源码可能有很多细节处理不标准不到位,比如我写SRA右移指令时,原本应当是由末16位确定右移位数,但是我考虑到我们只需要用到右移一位,所以我没有把末16位也参与运算而是直接把目标数>>1,属于是偷懒了;而且我们的外设部分并不好,上文也已经提到了,如果要改进还不如重构来得方便。

附指导书样例源码

笔者建议使用vscode或者其他文本编辑器阅读与编写代码,因为方便找变量。vivado中用vscode打开.v文件教程:
https://copyfuture.com/blogs-details/20200821104053326mo45esfedgvkgr6
同时,笔者也建议每个module各自放在一个.v文件里。

top.v:

module top #(parameter WIDTH = 8, REGBITS = 3)();
	reg clk;
	reg reset;
	wire memread, memwrite;
	wire [WIDTH-1:0] adr, writedata;
	wire [WIDTH-1:0] memdata;
	// instantiate devices to be tested
	mips #(WIDTH, REGBITS) dut(clk, reset, memdata, memread, 
	memwrite, 
	adr, writedata);
	// external memory for code and data
	exmemory #(WIDTH) exmem(clk, memwrite, adr, writedata, memdata);
	// initialize test
	initial begin
		reset <= 1; #22; reset <= 0;
	end
	 // generate clock to sequence tests
	always begin
		clk <= 1; #5; clk <= 0; #5;
	end
	always @(negedge clk) begin
		if(memwrite)
			if (adr == 5 & writedata == 7)
				$display("Simulation completely successful");
			else $display("Simulation failed");
	end
endmodule

memory.v

// external memory accessed by MIPS
module exmemory #(parameter WIDTH = 8)(clk, memwrite, adr, writedata, 
memdata);
	input clk;
	input memwrite;
	input [WIDTH-1:0] adr, writedata;
	output reg [WIDTH-1:0] memdata;
	reg [31:0] RAM[(1<<WIDTH-2)1 : 0];
	wire [31:0] word;
	
	initial begin
	//这里使用了.dat文件初始化RAM,也可以直接RAM[]=进行初始化
	$readmemh("memfile.dat", RAM);
 	end
 	// read and write bytes from 32-bit word
 	always @(posedge clk)
 	if (memwrite) 
 		case (adr[1:0])
			 2'b00: RAM[adr>>2][7:0] <= writedata;
			 2'b01: RAM[adr>>2][15:8] <= writedata;
			 2'b10: RAM[adr>>2][23:16] <= writedata;
			 2'b11: RAM[adr>>2][31:24] <= writedata;
	 	endcase
	 assign word = RAM[adr>>2];
	 always @(*)
		 case (adr[1:0])
			 2'b00: memdata <= word[31:24];
			 2'b01: memdata <= word[23:16];
			 2'b10: memdata <= word[15:8];
			 2'b11: memdata <= word[7:0];
		 endcase
endmodule

mips.v

// simplified MIPS processor
module mips #(parameter WIDTH = 8, REGBITS = 3)(
	input clk, reset, 
	input [WIDTH-1:0] memdata, 
	output memread, memwrite, 
	output [WIDTH-1:0] adr, writedata);
	wire [31:0] instr;
	wire zero, alusrca, memtoreg, iord, pcen, regwrite, regdst;
	wire [1:0] aluop,pcsource,alusrcb;
	wire [3:0] irwrite;
	wire [2:0] alucont;
	
	controller cont(clk, reset, instr[31:26], zero, memread, memwrite, 
	alusrca, memtoreg, iord, pcen, regwrite, regdst,pcsource, alusrcb, aluop, irwrite);
	
	alucontrol ac(aluop, instr[5:0], alucont);
	
	datapath #(WIDTH, REGBITS) dp(clk, reset, memdata, alusrca, 
	memtoreg, iord, pcen, regwrite, regdst, pcsource, alusrcb, irwrite, alucont, zero, instr, adr, writedata);
endmodule

controller.v

module controller(
	input clk, reset, 
	input [5:0] op, 
	input zero, 
	output reg memread, memwrite, alusrca, memtoreg, iord,
	output pcen, 
	output reg regwrite, regdst, 
	output reg [1:0] pcsource, alusrcb, aluop, 
	output reg [3:0] irwrite);


	reg [3:0] state, nextstate;
	reg pcwrite, pcwritecond;
	parameter FETCH1 = 4'b0001;
	parameter FETCH2 = 4'b0010;
	parameter FETCH3 = 4'b0011;
	parameter FETCH4 = 4'b0100;
	parameter DECODE = 4'b0101;
	parameter MEMADR = 4'b0110;
	parameter LBRD = 4'b0111;
	parameter LBWR = 4'b1000;
	parameter SBWR = 4'b1001;
	parameter RTYPEEX = 4'b1010;
	parameter RTYPEWR = 4'b1011;
	parameter BEQEX = 4'b1100;
	parameter JEX = 4'b1101;
	parameter LB = 6'b100000;
	parameter SB = 6'b101000;
	parameter RTYPE = 6'b0;
	parameter BEQ = 6'b000100;
	parameter J = 6'b000010;
	// state register
	always @(posedge clk)
	if(reset) state <= FETCH1;
	 else state <= nextstate;
	 // next state logic
	 always @(*) begin
	 case (state)
		 FETCH1: nextstate <= FETCH2;
		 FETCH2: nextstate <= FETCH3;
		 FETCH3: nextstate <= FETCH4;
		 FETCH4: nextstate <= DECODE;
		 DECODE: case (op)
					 LB: nextstate <= MEMADR;
					 SB: nextstate <= MEMADR;
					 RTYPE: nextstate <= RTYPEEX;
					 BEQ: nextstate <= BEQEX;
					 J: nextstate <= JEX;
	 				default: nextstate <= FETCH1;
				//default should never happen
				 endcase
	 
		 MEMADR: case (op)
			 LB: nextstate <= LBRD;
			 SB: nextstate <= SBWR;
			 default: nextstate <= FETCH1;
				//default should never happen
		 		endcase
		 
		 LBRD: nextstate <= LBWR;
		 LBWR: nextstate <= FETCH1;
		 SBWR: nextstate <= FETCH1;
		 RTYPEEX: nextstate <= RTYPEWR;
		 RTYPEWR: nextstate <= FETCH1;
		 BEQEX: nextstate <= FETCH1;
		 JEX: nextstate <= FETCH1;
		 default: nextstate <= FETCH1; 
		 // default should never happen
	endcase
	 end
	 always @(*) begin
	// set all outputs to zero, then conditionally assert just the 
	// appropriate ones
		 irwrite <= 4'b0000;
		 pcwrite <= 0; pcwritecond <= 0;
		 regwrite <= 0; regdst <= 0;
		 memread <= 0; memwrite <= 0;
		 alusrca <= 0; alusrcb <= 2'b00;
		aluop <= 2'b00; pcsource <= 2'b00;
		 iord <= 0; memtoreg <= 0;
	 case (state)
	 FETCH1: begin
		 memread <= 1; 
		 irwrite <= 4'b1000; 
		 alusrcb <= 2'b01; 
		 pcwrite <= 1;
	 end
	 FETCH2: begin
		 memread <= 1;
		 irwrite <= 4'b0100;
		 alusrcb <= 2'b01;
		 pcwrite <= 1;
	 end
	 FETCH3: begin
		 memread <= 1;
		 irwrite <= 4'b0010;
		 alusrcb <= 2'b01;
		 pcwrite <= 1;
	 end
	 FETCH4: begin
		 memread <= 1;
		 irwrite <= 4'b0001;
		 alusrcb <= 2'b01;
		 pcwrite <= 1;
	 end
	 DECODE: alusrcb <= 2'b11;
	 MEMADR: begin
		 alusrca <= 1;
		 alusrcb <= 2'b10;
	 end
	 LBRD: begin
		 memread <= 1;
		 iord <= 1;
	 end
	 LBWR: begin
		 regwrite <= 1;
		 memtoreg <= 1;
	 end
	 SBWR: begin
		 memwrite <= 1;
		 iord <= 1;
	 end
	 RTYPEEX: begin
		 alusrca <= 1;
		 aluop <= 2'b10;
	 end
	 RTYPEWR: begin
		 regdst <= 1;
		 regwrite <= 1;
	 end
	 BEQEX: begin
		 alusrca <= 1;
		 aluop <= 2'b01;
		 pcwritecond <= 1;
		 pcsource <= 2'b01;
	 end
	 JEX: begin
		 pcwrite <= 1;
		 pcsource <= 2'b10;
	 end
	 endcase
	 end
	assign pcen = pcwrite | (pcwritecond & zero); // PC enable
endmodule

alucontrol.v

module alucontrol(
	input [1:0] aluop, 
	input [5:0] funct, 
	output reg [2:0] alucont);
	always @(*)
	 case (aluop)
		 2'b00: alucont <= 3'b010; // add for lb/sb/addi
		 2'b01: alucont <= 3'b110; // sub (for beq)
		 default: 
			case (funct) // R-Type instructions
			 6'b100000: alucont <= 3'b010; // add
			 6'b100010: alucont <= 3'b110; // subtract
			 6'b100100: alucont <= 3'b000; // AND
			 6'b100101: alucont <= 3'b001; // OR
			 6'b101010: alucont <= 3'b111; // slt
			 default: alucont <= 3'b101;
			// default should never happen
		 	endcase
	 endcase
endmodule

datapath.v

module datapath #(parameter WIDTH = 8, REGBITS = 3)(
	input clk, reset,
	input [WIDTH1:0] memdata,
	input alusrca, memtoreg, iord, pcen, 
	input regwrite, regdst,
	input [1:0] pcsource, alusrcb,
	input [3:0] irwrite,
	input [2:0] alucont,
	output zero,
	output [31:0] instr,
	output [WIDTH1:0] adr, writedata);
	// the size of the parameters must be changed to match the WIDTH 
	// parameter
	 parameter CONST_ZERO = 8'b0;
	 parameter CONST_ONE = 8'b1;
	 wire [REGBITS1:0] ra1, ra2, wa;
	 wire [WIDTH1:0] pc, nextpc, md, rd1, rd2, wd, a, src1, 
	 src2, aluresult, aluout, constx4;
	 // shift left constant field by 2
	 assign constx4 = {instr[WIDTH3:0], 2'b00};
	 // register file address fields
	 assign ra1 = instr[REGBITS+20 : 21];
	 assign ra2 = instr[REGBITS+15 : 16];
	 mux2 #(REGBITS) regmux(instr[REGBITS+15 : 16],
	 instr[REGBITS+10 : 11], regdst, wa);
	// independent of bit width, load instruction into four 8-bit registers // over four cycles
	 flopen #(8) ir0(clk, irwrite[0], memdata[7:0], instr[7:0]);
	 flopen #(8) ir1(clk, irwrite[1], memdata[7:0], instr[15:8]);
	 flopen #(8) ir2(clk, irwrite[2], memdata[7:0], instr[23:16]);
	 flopen #(8) ir3(clk, irwrite[3], memdata[7:0], instr[31:24]);
	 // datapath
	 flopenr #(WIDTH) pcreg(clk, reset, pcen, nextpc, pc);
	 flop #(WIDTH) mdr(clk, memdata, md);
	 flop #(WIDTH) areg(clk, rd1, a);
	 flop #(WIDTH) wrd(clk, rd2, writedata);
	 flop #(WIDTH) res(clk, aluresult, aluout);
	 mux2 #(WIDTH) adrmux(pc, aluout, iord, adr);
	 mux2 #(WIDTH) src1mux(pc, a, alusrca, src1);
	 mux4 #(WIDTH) src2mux(writedata, CONST_ONE, instr[WIDTH-1:0], constx4, alusrcb, src2);
	 mux4 #(WIDTH) pcmux(aluresult, aluout, constx4, CONST_ZERO, pcsource, nextpc);
	 mux2 #(WIDTH) wdmux(aluout, md, memtoreg, wd);
	 regfile #(WIDTH, REGBITS) rf(clk, regwrite, ra1, ra2, wa, wd, rd1, rd2);
	 alu #(WIDTH) alunit(src1, src2, alucont, aluresult);
	 zerodetect #(WIDTH) zd(aluresult, zero);
endmodule

alu.v

module alu #(parameter WIDTH = 8) (
	input [WIDTH1:0] a, b, 
	input [2:0] alucont, 
	output reg [WIDTH1:0] result);
	
	 wire [WIDTH1:0] b2, sum, slt;
	 assign b2 = alucont[2] ? ~b : b;
	 assign sum = a + b2 + alucont[2];
	 // slt should be 1 if most significant bit of sum is 1
	 assign slt = sum[WIDTH - 1];
	 
	 always@(*)
	 case (alucont[1:0])
		 2'b00: result <= a & b;
		 2'b01: result <= a | b;
		 2'b10: result <= sum;
		 2'b11: result <= slt;
	 endcase
endmodule

regfile.v

module regfile #(parameter WIDTH = 8, REGBITS = 3)(
	input clk, 
	input regwrite, 
	input [REGBITS1:0] ra1, ra2, wa, 
	input [WIDTH1:0] wd, 
	output [WIDTH1:0] rd1, rd2);
	
	reg [WIDTH1:0] RAM[(1<<REGBITS)1 : 0];
	 // 3-ported register file
	 // read two ports combinationally
	 // write third port on rising edge of clock
	 // register 0 hardwired to 0
	 always @(posedge clk)
		 if (regwrite) 
		 	RAM[wa] <= wd;
	 assign rd1 = ra1 ? RAM[ra1] : 0;
	 assign rd2 = ra2 ? RAM[ra2] : 0;
endmodule

zerodetect.v

module zerodetect #(parameter WIDTH = 8)(
	input [WIDTH1:0] a, 
	output y);
	
	assign y = (a == 0);
endmodule

flop.v

module flop #(parameter WIDTH = 8)(
	input clk, 
	input [WIDTH1:0] d, 
	output reg [WIDTH1:0] q);
	
	 always @(posedge clk)
	 	q <= d;
endmodule

flopen.v

module flopen #(parameter WIDTH = 8)(
	input clk, en,
	input [WIDTH1:0] d, 
	output reg [WIDTH1:0] q);
	
	always @(posedge clk)
		if (en) q <= d;
endmodule

flopenr.v

module flopenr #(parameter WIDTH = 8)(
	input clk, reset, en,
	input [WIDTH1:0] d, 
	output reg [WIDTH1:0] q);
	
	always @(posedge clk)
		if (reset) q <= 0;
		else if (en) q <= d;
endmodule

mux2.v

module mux2 #(parameter WIDTH = 8) (
	input [WIDTH1:0] d0, d1, 
	input s, 
	output [WIDTH1:0] y);
	
	assign y = s ? d1 : d0; 
endmodule

mux4.v

module mux4 #(parameter WIDTH = 8)(
	input [WIDTH1:0] d0, d1, d2, d3,
	input [1:0] s, 
	output reg [WIDTH1:0] y);
	
	always @(*)
	 case(s)
		 2'b00: y <= d0;
		 2'b01: y <= d1;
		 2'b10: y <= d2;
		 2'b11: y <= d3;
	 endcase
endmodule

版权声明:本文为TheBug114514原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。