从零开始设计RISC-V处理器——单周期处理器的设计

系列文章目录

(一)从零开始设计RISC-V处理器——指令系统
(二)从零开始设计RISC-V处理器——单周期处理器的设计
(三)从零开始设计RISC-V处理器——单周期处理器的仿真
(四)从零开始设计RISC-V处理器——ALU的优化
(五)从零开始设计RISC-V处理器——五级流水线之数据通路的设计
(六)从零开始设计RISC-V处理器——五级流水线之控制器的设计
(七)从零开始设计RISC-V处理器——五级流水线之数据冒险
(八)从零开始设计RISC-V处理器——五级流水线之控制冒险
(九)从零开始设计RISC-V处理器——五级流水线之分支计算前移
(十)从零开始设计RISC-V处理器——五级流水线之静态预测



前言

上一篇文章 中已经详细介绍了计划实现的37条指令,今天开始进行单周期的处理器设计


一、CPU如何执行指令?

CPU包括控制器和数据通路,数据通路从字面意思理解,就是指处理器中数据流通的路径。不同类型的指令的数据通路不一样,具体由控制器产生的控制信号决定。
下面以一个简单的数据通路为例,介绍以下处理器执行指令的过程。
在这里插入图片描述

1.CPU执行指令的过程如下:
(1)pc做为地址,输入到指令存储器,读取一条指令出来。这条指令是一个32bit的二进制数,里面包含了将要执行的指令的所有信息,包括指令类型(opcode),指令执行的功能(func3,func7),立即数部分(imme),源寄存器(rs1,rs2),目的寄存器(rd)。

(2)将读出来的指令进行译码,输入到不同的部件。比如,rs1,rs2,rd输入到寄存器堆,将立即数部分进行立即数扩展,将opcode输入到控制部件,产生控制信号。将func3,func7输入到子控制模块,产生ALU的控制信号。
(3)译码出来的寄存器号,输入到寄存器堆,读出寄存器号对应的数据Read_data1和Read_data2。
对于R-type指令,进行的操作就是两个寄存器的数据进行运算。
对于I-type指令,进行的操作是一个寄存器的数据与立即数进行运算。
因此这里要产生立即数,并且需要一个二选一选择器进行数据的选择。
(4)ALU即是运算单元,对输入的两个数据进行运算操作并且将运算结果输出,具体进行什么运算,由ALU_control产生的控制信号决定。
(5)将计算结果写回寄存器,写入到目标寄存器号对应的寄存器中。
(6)对于访存指令,如lb,lh,lw,sb,sh,sw,指令,还需要与数据存储器进行数据交换。
对于load指令,执行的操作是将数据从数据存储器的某个地址读出数据并讲这个数据写回到寄存器。
对于store指令,执行的操作是将寄存器的数据写入到数据存储器的某个地址内。
这个数据存储器的地址怎么产生呢?
地址以基地址加偏移的形式给出,基地址从寄存器中取得,偏移量从立即数中得到,因此ALU进行加法运算的结果便是数据存储器的地址。
因此ALU的运算结果有两个去向,一是写回寄存器,二是作为数据存储器的地址。
(7)对于load指令,是将数据存储器的数据写回到寄存器。
因此寄存器的数据来源有两个,一是ALU的运算结果,二是从数据存储器读出的数据。
(8)对于不进行跳转的指令,每个周期pc+4,指令按顺序执行,因此需要一个加法器。为什么是加4呢?
一条指令有32bit,CPU的内存空间按字节编址,所以一条指令4个字节实际上占了4个地址空间。
(9)对于条件跳转指令,如BEQ,BNE等,满足跳转的条件时,跳转的新地址由pc加左移一位的立即数产生,又需要一个加法器。因此,pc的来源有两个,一是顺序执行,pc+4,二是条件跳转,pc+(imme2)。
(10)对于无条件跳转指令,如jal,pc+4写回寄存器,将pc+(imme
2)作为新地址。对于jalr,将pc+4写回寄存器,将源寄存器的数据与立即数相加并将最低位置0,作为下一个pc。
因此,写入寄存器的数据来源变成3个:ALU的运算结果,数据存储器读出的数据,pc+4。
pc的来源也变成3个:pc+4,pc+imme,Read_data1+imme。
(11)再考虑U-type,即lui,auipc。对于lui,将立即数进行处理,写入寄存器。对于auipc,将pc+imme写入寄存器。
因此,写入寄存器的数据来源变成5个,分别是:ALU的运算结果,数据存储器读出的数据,pc+4,lui的立即数 ,auipc的pc+imme。
上图所示的数据通路并不完整,如果想实现我们提到的37条指令,必须逐条分析指令,在上图的基础上添加相应的部件。
完整的数据通路如下所示(图中不包含控制器):
在这里插入图片描述

2.从以上的分析中,可以将CPU的关键部件总结如下:
(1)指令存储器
(2)译码单元
(3)寄存器堆
(4)ALU
(5)数据存储器
(6)控制器
其中指令存储器和数据存储器为CPU外部的存储器,CPU的核心为数据通路+控制器。
因此,我们设计完的顶层模块应该是这样的:
在这里插入图片描述
下面来进行具体设计。

二、存储器

1.指令存储器

指令存储器属于只读存储器(rom),可读不可写。
实现方式有两种:
(1)用硬件描述性语言设计
(2)用FPGA工具调用专用的rom IP核。
此处为了提高代码的通用性,采用第一种方式设计。
模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
addr 8bit input 存储器读地址
instr 32bit output 读出的指令

对rom进行初始化,只需要将二进制指令存到文件“rom_binary_file.txt”中。如下所示:
在这里插入图片描述
代码如下:

module instr_memory(
	addr,
	instr
    );
	input [7:0]addr;
	output [31:0]instr;
	
	reg[31:0] rom[255:0];
	
    //rom进行初始化
    initial begin
        $readmemb("./rom_binary_file.txt", rom);
        //$readmemh("rom_hex_file.txt", rom);
    end
	
    assign instr = rom[addr];

endmodule

2.数据存储器

数据存储器属于随机读写存储器(ram),可读可写,同样是两种实现方式。
这里用硬件描述语言设计。
模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
clk 1bit input 时钟信号
rst_n 1bit input 读出的指令
W_en 1bit input 写使能
addr 32bit input 读(写)地址
RW_tyoe 3bit input 读(写)类型,字节,半字,字,双字
din 32bit input 要写入存储器的数据
dout 32bit output 从数据存储器中读出的数据

由于指令集中包含多位宽的读写指令,所以这里数据存储器的设计也应该支持多位宽读写。
根据输入的RW_type信号来确定读写类型。这里的RW_type信号,其实就是指令中的func3信号。

RW_type 对应指令 读写类型
000 lb,sb 读取或写入一个字节,读取时将8bit符号扩展为32bit
001 lh,sh 读取或写入两个字节,读取时将16bit符号扩展为32bit
010 lw ,sw 读取或写入32bit
100 lbu 读取一个字节,将8bit无符号扩展为32bit
101 lhu 读取两个字节,将16bit无符号扩展为32bit

代码如下:

`include "define.v"
module data_memory(
	clk,
	rst_n,
	W_en,
	R_en,
	addr,
	RW_type,
	din,
	dout
    );
	
	
	input clk;
	input rst_n;
	
	input W_en;
	input R_en;
	
	input [31:0]addr;
	input [2:0]RW_type;

	input [31:0]din;
	output [31:0]dout;


	reg [31:0]ram[255:0];
	
	wire [31:0]Rd_data;reference
	
//
	
	
	reg [31:0]Wr_data_B;//字节拼接
	wire [31:0]Wr_data_H;//半字拼接
	
	wire [31:0]Wr_data;
	
	
	assign Rd_data=ram[addr[31:2]];/读基准

always@(*)
	begin
		case(addr[1:0])
			2'b00:Wr_data_B={Rd_data[31:8],din[7:0]};
			2'b01:Wr_data_B={Rd_data[31:16],din[7:0],Rd_data[7:0]};
			2'b10:Wr_data_B={Rd_data[31:24],din[7:0],Rd_data[15:0]};
			2'b11:Wr_data_B={din[7:0],Rd_data[23:0]};
		endcase
	end

assign Wr_data_H=(addr[1]) ? {din[15:0],Rd_data[15:0]} : {Rd_data[31:16],din[15:0]} ;
	
///根据写类型,选择写入的数捿

assign Wr_data=(RW_type[1:0]==2'b00) ? Wr_data_B :( (RW_type[1:0]==2'b01) ? Wr_data_H : din   );

///上升沿写入数捿

always@(posedge clk)
begin
	if(W_en)
		ram[addr[9:2]]<=Wr_data;
end

 
//读部刿


reg [7:0]Rd_data_B;
wire [15:0]Rd_data_H;

wire [31:0] Rd_data_B_ext;
wire [31:0] Rd_data_H_ext;

根据写地坿,实现截叿
always@(*)
begin
	case(addr[1:0])
		2'b00:Rd_data_B=Rd_data[7:0];
		2'b01:Rd_data_B=Rd_data[15:8];
		2'b10:Rd_data_B=Rd_data[23:16];
		2'b11:Rd_data_B=Rd_data[31:24];
	endcase
end
		
assign Rd_data_H=(addr[1])? Rd_data[31:16]:Rd_data[15:0];

///扩展丿32使
assign Rd_data_B_ext=(RW_type[2]) ? {24'd0,Rd_data_B} : {{24{Rd_data_B[7]}},Rd_data_B};

assign Rd_data_H_ext=(RW_type[2]) ? {16'd0,Rd_data_H} : {{16{Rd_data_H[15]}},Rd_data_H};


/
assign dout=(RW_type[1:0]==2'b00) ? Rd_data_B_ext : ((RW_type[1:0]==2'b01) ? Rd_data_H_ext : Rd_data );


endmodule


上面代码中出现的define.v文件里面定义了一些参数,该文件展示如下:

`define		zero_word		32'd0

`define		lui				7'b0110111
`define		auipc			7'b0010111
`define		jal				7'b1101111
`define		jalr			7'b1100111
`define		B_type			7'b1100011
`define		load			7'b0000011
`define		store			7'b0100011
`define		I_type			7'b0010011
`define		R_type			7'b0110011

`define 	ADD  			4'b0001
`define 	SUB  			4'b0011
`define 	SLL  			4'b1100
`define 	SLT  			4'b1001
`define 	SLTU 			4'b1000
`define 	XOR  			4'b0110
`define 	SRL  			4'b1101
`define 	SRA  			4'b1110
`define 	OR   			4'b0101
`define 	AND  			4'b0100

三.数据通路

1.寄存器堆

寄存器堆由快速的静态随机读写存储器(sram)实现,这种ram可以多路并发访问不同的寄存器。
寄存器堆由32个寄存器阵列组成,其中0号寄存器的值恒为0,只读不写。
模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
clk 1bit input 时钟信号
Rs1 5bit input 源寄存器1
Rs2 5bit input 源寄存器2
W_en 1bit input 读使能
Rd 5bit input 目标寄存器
Wr_data 32bit input 写入寄存器的数据
Rd_data1 32bit output 源寄存器1的数据
Rd_data2 32bit output 源寄存器2的数据

代码如下:

`include "define.v"
module registers(
	clk,
	W_en,
	Rs1,
	Rs2,
	Rd,
	Wr_data,
	Rd_data1,
	Rd_data2
    );
	input clk;
	input W_en;
	input [4:0]Rs1;
	input [4:0]Rs2;
	input [4:0]Rd;
	input [31:0]Wr_data;
	
	output [31:0]Rd_data1;
	output [31:0]Rd_data2;
	
	reg [31:0] regs [31:0];
	
	
///write
	always@(posedge clk )
		begin
			if(W_en & (Rd!=0))
			regs[Rd]<=Wr_data;	
		end
//read

	assign Rd_data1=(Rs1==5'd0)?`zero_word: regs[Rs1];
	assign Rd_data2=(Rs2==5'd0)?`zero_word: regs[Rs2];
	
endmodule

2.译码模块

该模块对指令存储器输出的32位指令进行译码,得到opcode,Rs1,Rs2,Rd,imme,func3,func7等信息。
按照以下格式进行译码:
在这里插入图片描述
这里补充以下上一篇文中中提到的问题:
对于jal指令(UJ-type)和条件跳转指令(SB-type),他们的立即数表示方法是[20:1]或[12:1],这里其实就是把最低位默认成0了。
如果按照最低位为0来扩展立即数,那么这个立即数直接与pc相加,即为跳转地址。如果最低位不进行补0,那么就如本篇刚开始提到的数据通路所示,必须将立即数左移一位之后与立即数相加。其最终效果是一样的。

模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
instr 32bit input 32位指令
opcde 7bit output 7位操作码
func3 3bit output func3
func7 1bit output func7的第6位
Rs1 5bit output 源寄存器1
Rs2 5bit output 源寄存器2
Rd 5bit output 目标寄存器
imme 32bit output 符号扩展后的32位立即数

代码如下:

`include "define.v"
module instr_decode(
	input [31:0]instr,
	output [6:0]opcode,
	output [2:0]func3,
	output func7,
	output [4:0]Rs1,
	output [4:0]Rs2,
	output [4:0]Rd,
	output [31:0]imme
	 );
	 
	wire I_type;
	wire U_type;
	wire J_type;
	wire B_type;
	wire S_type;
	
	wire [31:0]I_imme;
	wire [31:0]U_imme;
	wire [31:0]J_imme;
	wire [31:0]B_imme;
	wire [31:0]S_imme;
	
	
	assign opcode=instr[6:0];
	assign func3=instr[14:12];
	assign func7=instr[30];
	assign Rs1=instr[19:15];
	assign Rs2=instr[24:20];
	assign Rd =instr[11:7];
	
	assign I_type=(instr[6:0]==`jalr) | (instr[6:0]==`load) | (instr[6:0]==`I_type);
	assign U_type=(instr[6:0]==`lui) | (instr[6:0]==`auipc);
	assign J_type=(instr[6:0]==`jal);
	assign B_type=(instr[6:0]==`B_type);
	assign S_type=(instr[6:0]==`store);
	
	
	assign I_imme={{20{instr[31]}},instr[31:20]}; 
	assign U_imme={instr[31:12],{12{1'b0}}};
	assign J_imme={{12{instr[31]}},instr[19:12],instr[20],instr[30:21],1'b0};   
	assign B_imme={{20{instr[31]}},instr[7],instr[30:25],instr[11:8],1'b0};
	assign S_imme={{20{instr[31]}},instr[31:25],instr[11:7]}; 
	
	assign imme= I_type?I_imme :
				 U_type?U_imme :
				 J_type?J_imme :
				 B_type?B_imme :
				 S_type?S_imme : 32'd0;


endmodule

3.ALU模块

ALU模块主要进行数据的运算,根据ALU的控制模块产生的控制信ALU_CTL决定ALU进行的运算类型。
ALU_CTL对应的运算关系如下:

ALU_CTL 运算类型 具体的运算
0000 加法运算 +
0011 加法运算 -
0100 逻辑运算 &
0101 逻辑运算
0110 逻辑运算 ^
0111 逻辑运算 或非
1000 小于置一 无符号小于置一
1001 小于置一 有符号小于置一
1100 移位运算 逻辑左移
1101 移位运算 逻辑右移
1110 移位运算 算数右移

由以上表格可以看出,ALU的运算类型分为加法运算,逻辑运算,小于置一,移位运算,根据ALU_CTL的高两位便可以判断出要执行的运算类型。
(1)加法运算:由加法器实现,减法实质上也是加法。
对于A-B,可以看作A+(-B)。
对于补码表示的二进制数,天然地有如下关系:
A+(~A)=32’b1111_1111_1111_1111_1111_1111_1111_1111=-1;
因此,-A=(~A)+1,这便是我们常说的补码等于反码加1,其实更严谨的说法应该是,一个数的相反数,等于这个数按位取反再加一。
于是,对于减法运算,可以做如下转换:
A-B=A+(-B)=A+(B的补码)+1;

另外,加法运算还应检测是否溢出,检测依据就是:当数为负数时,最左侧的位为0,或者数为正数时,最左侧的位为1。
具体细分位以下四种情况:
a.正数+正数=负数,则溢出
b.负数+负数=正数,则溢出
c.正数-负数=负数,则溢出
d.负数-正数=正数,则溢出
(2)逻辑运算,用逻辑门阵列实现即可。
(3)小于置一,实质上是减法运算。根据加法器的运算结果,进一步判断是否小于。
(4)移位运算,这里直接使用移位运算符(>>,<<)。

以上就是ALU的主要运算部件的设计,可以看到,加法器和移位运算比较复杂,但这里直接用Verilog的运算符实现的,后续会尝试设计超前进位加法器代替“+”以及用位拼接运算代替“>>,<<”,以提升CPU的性能。

模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
ALU_DA 32bit input ALU的操作数1
ALU_DB 32bit input ALU的操作数2
ALU_CTL 4bit input ALU的控制信号
ALU_ZERO 1bit output ALU运算结果为0的标志
ALU_Overflow 1bit output 溢出标志
ALU_DC 32bit output ALU的运算结果

代码如下:

module alu(
	   ALU_DA,
       ALU_DB,
       ALU_CTL,
       ALU_ZERO,
       ALU_OverFlow,
       ALU_DC   
        );
	input [31:0]    ALU_DA;
    input [31:0]    ALU_DB;
    input [3:0]     ALU_CTL;
    output          ALU_ZERO;
    output          ALU_OverFlow;
    output reg [31:0]   ALU_DC;
		   
//********************generate ctr***********************
wire SUBctr;
wire SIGctr;
wire Ovctr;
wire [1:0] Opctr;
wire [1:0] Logicctr;
wire [1:0] Shiftctr;

assign SUBctr = (~ ALU_CTL[3]  & ~ALU_CTL[2]  & ALU_CTL[1]) | ( ALU_CTL[3]  & ~ALU_CTL[2]);
assign Opctr = ALU_CTL[3:2];
assign Ovctr = ALU_CTL[0] & ~ ALU_CTL[3]  & ~ALU_CTL[2] ;
assign SIGctr = ALU_CTL[0];
assign Logicctr = ALU_CTL[1:0]; 
assign Shiftctr = ALU_CTL[1:0]; 

//********************************************************

//*********************logic op***************************
reg [31:0] logic_result;

always@(*) begin
    case(Logicctr)
	2'b00:logic_result = ALU_DA & ALU_DB;
	2'b01:logic_result = ALU_DA | ALU_DB;
	2'b10:logic_result = ALU_DA ^ ALU_DB;
	2'b11:logic_result = ~(ALU_DA | ALU_DB);
	endcase
end 

//********************************************************
//************************shift op************************
wire [4:0]     ALU_SHIFT;
wire [31:0] shift_result;
assign ALU_SHIFT=ALU_DB[4:0];

Shifter Shifter(.ALU_DA(ALU_DA),
                .ALU_SHIFT(ALU_SHIFT),
				.Shiftctr(Shiftctr),
				.shift_result(shift_result));

//********************************************************
//************************add sub op**********************
wire [31:0] BIT_M,XOR_M;
wire ADD_carry,ADD_OverFlow;
wire [31:0] ADD_result;

assign BIT_M={32{SUBctr}};
assign XOR_M=BIT_M^ALU_DB;

Adder Adder(.A(ALU_DA),
            .B(XOR_M),
			.Cin(SUBctr),
			.ALU_CTL(ALU_CTL),
			.ADD_carry(ADD_carry),
			.ADD_OverFlow(ADD_OverFlow),
			.ADD_zero(ALU_ZERO),
			.ADD_result(ADD_result));

assign ALU_OverFlow = ADD_OverFlow & Ovctr;

//********************************************************
//**************************slt op************************
wire [31:0] SLT_result;
wire LESS_M1,LESS_M2,LESS_S,SLT_M;

assign LESS_M1 = ADD_carry ^ SUBctr;
assign LESS_M2 = ADD_OverFlow ^ ADD_result[31];
assign LESS_S = (SIGctr==1'b0)?LESS_M1:LESS_M2;
assign SLT_result = (LESS_S)?32'h00000001:32'h00000000;

//********************************************************
//**************************ALU result********************
always @(*) 
begin
  case(Opctr)
     2'b00:ALU_DC<=ADD_result;
     2'b01:ALU_DC<=logic_result;
     2'b10:ALU_DC<=SLT_result;
     2'b11:ALU_DC<=shift_result; 
  endcase
end

//********************************************************
endmodule


//********************************************************
//*************************shifter************************
module Shifter(input [31:0] ALU_DA,
               input [4:0] ALU_SHIFT,
			   input [1:0] Shiftctr,
			   output reg [31:0] shift_result);
			   

     wire [5:0] shift_n;
	 assign shift_n = 6'd32 - Shiftctr;
     always@(*) begin
	   case(Shiftctr)
	   2'b00:shift_result = ALU_DA << ALU_SHIFT;
	   2'b01:shift_result = ALU_DA >> ALU_SHIFT;
	   2'b10:shift_result = ({32{ALU_DA[31]}} << shift_n) | (ALU_DA >> ALU_SHIFT);
	   default:shift_result = ALU_DA;
	   endcase
	 end


endmodule

//*************************************************************
//***********************************adder*********************
module Adder(input [31:0] A,
             input [31:0] B,
			 input Cin,
			 input [3:0] ALU_CTL,
			 output ADD_carry,
			 output ADD_OverFlow,
			 output ADD_zero,
			 output [31:0] ADD_result);


    assign {ADD_carry,ADD_result}=A+B+Cin;
   assign ADD_zero = ~(|ADD_result);
   assign ADD_OverFlow=((ALU_CTL==4'b0001) & ~A[31] & ~B[31] & ADD_result[31]) 
                      | ((ALU_CTL==4'b0001) & A[31] & B[31] & ~ADD_result[31])
                      | ((ALU_CTL==4'b0011) & A[31] & ~B[31] & ~ADD_result[31]) 
					  | ((ALU_CTL==4'b0011) & ~A[31] & B[31] & ADD_result[31]);
endmodule



4.PC寄存器

PC寄存器用以更新pc的值。
顺序执行时,pc_new=pc+4,条件跳转时,pc_new=pc+imme。
jalr时,pc_new=data(Rs1)+imme。

模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
clk 1bit input 时钟信号
rst_n 1bit input 读出的指令
pc_new 32bit input 下一个时钟周期的pc值
pc_out 32bit output 更新后的pc值

代码如下:

`include "define.v"
module pc_reg(
	clk,
	rst_n,
	pc_new,
	pc_out
    );
	input clk;
	input rst_n;
	input [31:0]pc_new;
	
	output reg [31:0]pc_out;
	
	always@(posedge clk or negedge rst_n)
	begin
		if(!rst_n)
			pc_out<=`zero_word;
		else
			pc_out<=pc_new;
	end	

endmodule



5.数据通路

数据通路就是将以上的几个关键的部件进行连接,为了形成完整的数据通路,还必须添加一些多路选择器,作用是对不同信号的来源进行选择,并输出到相应的模块。
比如:写入寄存器的数据来源有5个,分别是:ALU的运算结果,数据存储器读出的数据,pc+4,lui的立即数 ,auipc的pc+imme。
pc的来源有3个:pc+4,pc+imme,Read_data1+imme。
另外,还需要加入两个加法器,分别计算pc+4和pc+imme。

模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
clk 1bit input 时钟信号
rst_n 1bit input 读出的指令
instr 32bit input 指令存储器读出的指令
MemtoReg 1bit input 写回寄存器的数据选择器控制信号
ALUSrc 1bit input ALU的数据来源的数据选择器控制信号
RegWrite 1bit input 寄存器的写使能控制信号
lui 1bit input lui指令标志,写回寄存器的数据选择器的控制信号
U_type 1bit input U-type指令标志,写回寄存器的数据选择器的控制信号
jal 1bit input jal指令标志,选择pc的数据选择器的控制信号,同时也是写回寄存器的数据选择器的控制信号
jalr 1bit input jal指令标志,选择pc的数据选择器的控制信号,同时也是写回寄存器的数据选择器的控制信号
beq 1bit input beq指令标志,判断是否跳转的控制信号
bne 1bit input bne指令标志,判断是否跳转的控制信号
blt 1bit input blt指令标志,判断是否跳转的控制信号
bge 1bit input bge指令标志,判断是否跳转的控制信号
bltu 1bit input bltu指令标志,判断是否跳转的控制信号
bgeu 1bit input bgeu指令标志,判断是否跳转的控制信号
ALUctl 4bit input ALU的控制信号,决定ALU进行什么运算
Rd_mem_data 32bit input 从数据存储器读出的数据,作为写回寄存器的数据来源之一
rom_addr 8bit output 指令存储器的地址
Wr_mem_data 32bit output 数据存储器的写数据
ALU_result 32bit output ALU的运算结果,作为数据存储器的读(写)地址
opcode 7bit output 7位操作码
func3 3bit output func3
func7 1bit output func7

代码如下:

module datapath(
	input 	clk,
	input   rst_n,
	input   [31:0]instr,


	input   MemtoReg,
	input   ALUSrc,
	input   RegWrite,
	input   lui,
	input   U_type,
	input   jal,
	input   jalr,
	input   beq,
	input   bne,
	input   blt,
	input   bge,
	input   bltu,
	input   bgeu,
	input   [3:0]ALUctl,
	
	input [31:0]Rd_mem_data,
	output  [7:0]rom_addr,
	output [31:0]Wr_mem_data,
	output [31:0]ALU_result,
	output [6:0]opcode,
	output [2:0]func3,
	output func7
	
	
    );
	
	
	
	
	
	
	wire [4:0]Rs1;
	wire [4:0]Rs2;
	wire [4:0]Rd;
	wire [31:0]imme;
	
	wire [31:0] Wr_reg_data;
	wire [31:0] Rd_data1;
	wire [31:0] Rd_data2;
	
	wire zero;
	
	wire [31:0]pc_order;
	wire [31:0]pc_jump;
	
	wire   [31:0]pc_new;
	wire [31:0]pc_out;
	
	wire jump_flag;
	
	wire [31:0]ALU_DB;
	wire [31:0]WB_data;
	
	wire reg_sel;
	wire [31:0]Wr_reg_data1;
	wire [31:0]Wr_reg_data2;
	wire [31:0]pc_jump_order;
	wire [31:0]pc_jalr;
	
	
	assign reg_sel=jal | jalr ;
	assign Wr_mem_data=Rd_data2;
	assign rom_addr=pc_out[9:2];
	assign pc_jalr={ALU_result[31:1],1'b0};
	
	pc_reg pc_reg_inst (
    .clk(clk), 
    .rst_n(rst_n), 
    .pc_new(pc_new), 
    .pc_out(pc_out)
    );

	
	instr_decode instr_decode_inst (
    .instr(instr), 
    .opcode(opcode), 
    .func3(func3), 
    .func7(func7), 
    .Rs1(Rs1), 
    .Rs2(Rs2), 
    .Rd(Rd), 
    .imme(imme)
    );
	
    registers registers_inst (
    .clk(clk), 
    .W_en(RegWrite), 
    .Rs1(Rs1), 
    .Rs2(Rs2), 
    .Rd(Rd), 
    .Wr_data(Wr_reg_data), 
    .Rd_data1(Rd_data1), 
    .Rd_data2(Rd_data2)
    );

	
	alu alu_inst (
    .ALU_DA(Rd_data1), 
    .ALU_DB(ALU_DB), 
    .ALU_CTL(ALUctl), 
    .ALU_ZERO(zero), 
    .ALU_OverFlow(), 
    .ALU_DC(ALU_result)
    );

	branch_judge branch_judge_inst (
    .beq(beq), 
    .bne(bne), 
    .blt(blt), 
    .bge(bge), 
    .bltu(bltu), 
    .bgeu(bgeu), 
    .jal(jal), 
    .jalr(jalr), 
    .zero(zero), 
    .ALU_result(ALU_result), 
    .jump_flag(jump_flag)
    );

	

	
//pc+4	
	cla_adder32 pc_adder_4 (
    .A(pc_out), 
    .B(32'd4), 
    .cin(1'd0), 
    .result(pc_order), 
    .cout()
    );
	
/pc+imme
	cla_adder32 pc_adder_imme (
    .A(pc_out), 
    .B(imme), 
    .cin(1'd0), 
    .result(pc_jump), 
    .cout()
    );
	

/pc_sel
	mux pc_mux (
    .data1(pc_jump), 
    .data2(pc_order), 
    .sel(jump_flag), 
    .dout(pc_jump_order)
    );
///pc_jalr
	mux pc_jalr_mux (
    .data1(pc_jalr), 
    .data2(pc_jump_order), 
    .sel(jalr), 
    .dout(pc_new)
    );

	
	
ALUdata_sel	
	mux ALU_data_mux (
    .data1(imme), 
    .data2(Rd_data2), 
    .sel(ALUSrc), 
    .dout(ALU_DB)
    );
	
	
/ALU_result or datamem	
	mux WB_data_mux (
    .data1(Rd_mem_data), 
    .data2(ALU_result), 
    .sel(MemtoReg), 
    .dout(WB_data)
    );
	
	
Wr_data_sel
	mux jalr_mux (
    .data1(pc_order), 
    .data2(WB_data), 
    .sel(reg_sel), 
    .dout(Wr_reg_data2)
    );
	
	mux lui_mux (
    .data1(imme), 
    .data2(pc_jump), 
    .sel(lui), 
    .dout(Wr_reg_data1)
    );
	
	mux Wr_reg_mux (
    .data1(Wr_reg_data1), 
    .data2(Wr_reg_data2), 
    .sel(U_type), 
    .dout(Wr_reg_data)
    );

endmodule

四.控制器

1.主控制器

由于不同类型的指令所经过的数据通路不同,所以需要有控制信号控制数据通路,使得数据经过正确的通路,得到正确的运算结果。
本设计将控制器分为两级控制,主控制器产生大部分的控制信号,子控制器是ALU控制器,产生控制ALU进行正确运算的信号。
模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
opcode 7bit input 7位操作码
func3 3bit input func3
Memread 1bit output 数据存储器读使能
ALUop 2bit output 子控制器的控制信号
MemtoReg 1bit output 写回寄存器的数据选择器控制信号
Memwrite 1bit output 数据存储器写使能
ALUSrc 1bit output ALU的数据来源的数据选择器控制信号
RegWrite 1bit output 寄存器的写使能控制信号
lui 1bit output lui指令标志,写回寄存器的数据选择器的控制信号
U_type 1bit output U-type指令标志,写回寄存器的数据选择器的控制信号
jal 1bit output jal指令标志,选择pc的数据选择器的控制信号,同时也是写回寄存器的数据选择器的控制信号
jalr 1bit output jal指令标志,选择pc的数据选择器的控制信号,同时也是写回寄存器的数据选择器的控制信号
beq 1bit output beq指令标志,判断是否跳转的控制信号
bne 1bit output bne指令标志,判断是否跳转的控制信号
blt 1bit output blt指令标志,判断是否跳转的控制信号
bge 1bit output bge指令标志,判断是否跳转的控制信号
bltu 1bit output bltu指令标志,判断是否跳转的控制信号
bgeu 1bit output bgeu指令标志,判断是否跳转的控制信号
RW_type 3bit output 数据存储器的读(写)类型

代码如下:

module main_control(
	opcode,
	func3,
	MemRead,
	MemtoReg,
	ALUop,
	MemWrite,
	ALUSrc,
	RegWrite,
	lui,
	U_type,
	jal,
	jalr,
	beq,
	bne,
	blt,
	bge,
	bltu,
	bgeu,
	RW_type
    );
	input [6:0]opcode;
	input [2:0]func3;
	
	output   MemRead;
	output   MemtoReg;
	output   [1:0]ALUop;
	output   MemWrite;
	output   ALUSrc;
	output   RegWrite;
	output   lui;
	output   U_type;
	output   jal;
	output   jalr;
	output   beq;
	output   bne;
	output   blt;
	output   bge;
	output   bltu;
	output   bgeu;
	output   [2:0]RW_type;
	
	wire branch;
	wire R_type;
	wire I_type;
	wire load;
	wire store;
	wire lui;
	wire auipc;

	
	assign branch=(opcode==`B_type)?1'b1:1'b0;
	assign R_type=(opcode==`R_type)?1'b1:1'b0;
	assign I_type=(opcode==`I_type)?1'b1:1'b0;
	assign U_type=(lui | auipc)? 1'b1:1'b0;
	assign load=(opcode==`load)?1'b1:1'b0;
	assign store=(opcode==`store)?1'b1:1'b0;
	
	assign jal=(opcode==`jal)?1'b1:1'b0;
	assign jalr=(opcode==`jalr)?1'b1:1'b0;
	assign lui=(opcode==`lui)?1'b1:1'b0;
	assign auipc=(opcode==`auipc)?1'b1:1'b0;
	assign beq= branch & (func3==3'b000);
	assign bne= branch & (func3==3'b001);
	assign blt= branch & (func3==3'b100);
	assign bge= branch & (func3==3'b101);
	assign bltu= branch & (func3==3'b110);
	assign bgeu= branch & (func3==3'b111);
	assign RW_type=func3;
	
	
	enable
	assign MemRead= load;
	assign MemWrite= store;
	assign RegWrite= jal| jalr | load | I_type |R_type | U_type;
	
	MUX
	assign ALUSrc=load | store |I_type | jalr;  //select imme
	assign MemtoReg= load;  //select datamemory data
	
	ALUop
	assign ALUop[1]= R_type|branch; //R 10 I 01 B 11 add 00
	assign ALUop[0]= I_type|branch;
	
	
endmodule

2.子控制器

子控制器根据主控制器产生的ALUop信号,结合func3和func7信号来产生ALUctl信号。
ALUop信号的设置如下:

ALUop 子控制器的操作
00 加法运算
10 R-type指令,根据func3和func7判断运算类型
01 R-type指令,根据func3和func7判断运算类型
11 条件跳转信号,根据func3判断运算类型

ALUctl信号设置如下:

ALUctl 运算类型 具体的运算
0000 加法运算 +
0011 加法运算 -
0100 逻辑运算 &
0101 逻辑运算
0110 逻辑运算 ^
0111 逻辑运算 或非
1000 小于置一 无符号小于置一
1001 小于置一 有符号小于置一
1100 移位运算 逻辑左移
1101 移位运算 逻辑右移
1110 移位运算 算数右移

模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
opcode 7bit input 7位操作码
func3 3bit input func3
Memread 1bit output 数据存储器读使能
ALUop 2bit output 子控制器的控制信号
MemtoReg 1bit output 写回寄存器的数据选择器控制信号

代码如下:

module alu_control(
	ALUop,
	func3,
	func7,
	ALUctl
    );
	input [1:0]ALUop;
	input [2:0]func3;
	input func7;
	output [3:0]ALUctl;
	
	wire [3:0]branchop;
	reg  [3:0]RIop;
	
	
	
	assign branchop=(func3[2] & func3[1])? `SLTU : (func3[2] ^ func3[1])? `SLT : `SUB;
	
	always@(*)
	begin
		case(func3)
			3'b000: if(ALUop[1] & func7) //R
					RIop=`SUB;
					else                 //I
					RIop=`ADD;
			3'b001: RIop=`SLL;
			3'b010: RIop=`SLT;
			3'b011: RIop=`SLTU;
			3'b100: RIop=`XOR;
			3'b101: if(func7)
					RIop=`SRA;
					else
					RIop=`SRL;
			3'b110: RIop=`OR;
			3'b111: RIop=`AND;
			default:RIop=`ADD;
		endcase
	end
	
	assign ALUctl=(ALUop[1]^ALUop[0])? RIop:(ALUop[1]&ALUop[0])?branchop:`ADD;

endmodule

3.控制器

将主控制模块和子控制模块进行实例化,得到控制模块。
代码如下:

`include "define.v"
module control(
	opcode,
	func3,
	func7,
	MemRead,
	MemtoReg,
	MemWrite,
	ALUSrc,
	RegWrite,
	lui,
	U_type,
	jal,
	jalr,
	beq,
	bne,
	blt,
	bge,
	bltu,
	bgeu,
	RW_type,
	ALUctl

    );
	input 	 [6:0]opcode;
	input 	 [2:0]func3;
	input 	 func7;
	output   MemRead;
	output   MemtoReg;
	output   MemWrite;
	output   ALUSrc;
	output   RegWrite;
	output   lui;
	output   U_type;
	output   jal;
	output   jalr;
	output   beq;
	output   bne;
	output   blt;
	output   bge;
	output   bltu;
	output   bgeu;
	output   [2:0]RW_type;
	output   [3:0]ALUctl;
	
	wire [1:0]ALUop;
	
	main_control main_control_inst(
	.opcode(opcode),
	.func3(func3),
	.MemRead(MemRead),
	.MemtoReg(MemtoReg),
	.ALUop(ALUop),
	.MemWrite(MemWrite),
	.ALUSrc(ALUSrc),
	.RegWrite(RegWrite),
	.lui(lui),
	.U_type(U_type),
	.jal(jal),
	.jalr(jalr),
	.beq(beq),
	.bne(bne),
	.blt(blt),
	.bge(bge),
	.bltu(bltu),
	.bgeu(bgeu),
	.RW_type(RW_type)
    );
	
	alu_control alu_control_inst(
	.ALUop(ALUop),
	.func3(func3),
	.func7(func7),
	.ALUctl(ALUctl)
    );
	
endmodule

五.CPU的实现

1. RISC-V核心

将数据通路和及控制信号连接。
模块的输入输出端口定义如下:

信号名 位宽 输入输出 说明
clk 1bit input 系统时钟
rst_n 1bit input 复位信号
instr 32bit input 来自指令存储器的指令
Rd_mem_data 32bit input 来自数据存储器的读数据
rom_addr 8bit output 指令存储器的读地址
Wr_mem_data 32bit output 写入数据存储器的数据
R_en 1bit output 数据存储器的读使能信号
W_en 1bit output 数据存储器的写使能信号
ram_addr 32bit output 数据存储器的读(写)地址
RW_type 3bit output 数据存储器的读写类型

代码如下:

module riscv(
	input clk,
	input rst_n,
	input [31:0]instr,
	input [31:0]Rd_mem_data,
	
	output [7:0]rom_addr,
	
	output [31:0]Wr_mem_data,
	output W_en,
	output R_en,
	output [31:0]ram_addr,
	output [2:0]RW_type
    );
	
	wire [6:0]opcode;
	wire [2:0]func3;
	wire func7;
	wire MemtoReg;
	wire ALUSrc;
	wire RegWrite;
	wire lui;
	wire U_type;
	wire jal;
	wire jalr;
	wire beq;
	wire bne;
	wire blt;
	wire bge;
	wire bltu;
	wire bgeu;
	wire [3:0]ALUctl;
	
	
	
	control control_inst (
    .opcode(opcode), 
    .func3(func3), 
    .func7(func7), 
    .MemRead(R_en), 
    .MemtoReg(MemtoReg), 
    .MemWrite(W_en), 
    .ALUSrc(ALUSrc), 
    .RegWrite(RegWrite), 
	.lui(lui),
	.U_type(U_type),
    .jal(jal), 
    .jalr(jalr), 
    .beq(beq), 
    .bne(bne), 
    .blt(blt), 
    .bge(bge), 
    .bltu(bltu), 
    .bgeu(bgeu), 
    .RW_type(RW_type), 
    .ALUctl(ALUctl)
    );
	
	datapath datapath_inst (
    .clk(clk), 
    .rst_n(rst_n), 
    .instr(instr), 
    .MemtoReg(MemtoReg), 
    .ALUSrc(ALUSrc), 
    .RegWrite(RegWrite), 
	.lui(lui),
	.U_type(U_type),
    .jal(jal), 
    .jalr(jalr), 
    .beq(beq), 
    .bne(bne), 
    .blt(blt), 
    .bge(bge), 
    .bltu(bltu), 
    .bgeu(bgeu), 
    .ALUctl(ALUctl), 
    .Rd_mem_data(Rd_mem_data), 
    .rom_addr(rom_addr), 
    .Wr_mem_data(Wr_mem_data),
	.ALU_result(ram_addr),
	.opcode(opcode),
	.func3(func3),
	.func7(func7)
    );

endmodule

2.完整CPU

接入指令存储器和数据存储器。
代码如下:

module riscv_top(
	input clk,
	input rst_n,
	output [7:0]rom_addr
    );

//	wire [7:0]rom_addr;
	wire [31:0]ram_addr;
	wire [31:0]instr;
	wire [31:0]Rd_mem_data;
	wire [31:0]Wr_mem_data;
	wire W_en;
	wire R_en;
	wire [2:0]RW_type;
	
	instr_memory instr_memory_inst (
    .addr(rom_addr), 
    .instr(instr)
    );

	riscv riscv_inst (
    .clk(clk), 
    .rst_n(rst_n), 
    .instr(instr), 
    .Rd_mem_data(Rd_mem_data), 
    .rom_addr(rom_addr), 
    .Wr_mem_data(Wr_mem_data), 
    .W_en(W_en), 
    .R_en(R_en), 
    .ram_addr(ram_addr), 
    .RW_type(RW_type)
    );

	
	
	
	data_memory data_memory_inst (
    .clk(clk), 
    .rst_n(rst_n), 
    .W_en(W_en), 
    .R_en(R_en), 
    .addr(ram_addr), 
    .RW_type(RW_type), 
    .din(Wr_mem_data), 
    .dout(Rd_mem_data)
    );	

endmodule

3.整体预览

最终的代码结构如下:
在这里插入图片描述
FPGA工具生成的RTL图如下:

在这里插入图片描述

总结

以上就是今天要讲的内容,本文仅仅简单介绍了单周期处理器的设计,后续还需要在此基础上不断改善。 下一篇文章介绍对这个处理器的整体仿真。


版权声明:本文为CSDN博主「不学无术的小胖子.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_45677520/article/details/122386632

生成海报
点赞 0

不学无术的小胖子.

我还没有学会写个人说明!

暂无评论

相关推荐

智能车浅谈——抗干扰技术软件篇

软件抗干扰技术 前面介绍了一些硬件抗干扰技术,不难发现,要做好一个完美的硬件系统是需要花大心思的,而且很多硬件抗扰方案都需要借助一些元器件来实现,这在一定程度上增加了费用,

基于STM32的0.96寸OLED显示屏显示数据

实验要求 理解OLED屏显和汉字点阵编码原理,使用STM32F103的SPI或IIC接口实现以下功能: 显示自己的学号和姓名; 显示AHT20的温度和湿度; 上下或左右的滑动显示长字