从零开始写流水线——模块设计+指令定义

Posted aagnosticengineer

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始写流水线——模块设计+指令定义相关的知识,希望对你有一定的参考价值。

此前在UCSB交换旁听了那边的ECE154b,课设是写MIPS+cache+branch prediction,个人感觉很有意思。现在就按照教授给的project guideline自己动手走一遍流程(见https://www.ece.ucsb.edu/~strukov/ece154bSpring2017/project1.pdf)。参考的书籍是Harris编写的 "Digital Design and Computer Architecture" 。

我写的MIPS处理器为5级流水线,分为取指->译码->执行->存储->回写,有冲突监测。

1.指令集编码

首先我们需要确定我们希望MIPS所能实现的指令,并确定指令的编码。对于该32bit微处理器,我们希望实现至少以下27种指令,我们分为三种:

R型指令:add, addu, sub, subu, and, or, xor, xnor, slt, sltu, lw, sw, mult, multu. 指令格式:OP, rd, rs, rt. 对于R型指令,将32位的指令各部分分配为opcode(6), rs(5), rt(5), rd(5), shamt(5), funct(6),其中opcode=6‘b0,对于不同指令的区分取决于funct部分。

I型指令:addi,addiu,andi,ori,xori,slti,sltiu,lw,sw,lui,bne,beq.指令格式:OP, rt, rs, imm. 对于I型指令,将32位的指令各部分分配为opcode(6), rs(5), rt (5), immediate(16)。

J型指令:j. 指令格式:OP, LABEL. 对于J型指令,将32位的指令各部分分配为opcode(6), address(26)。

我们将上述15种指令编码得到表1.

表1 MIPS指令编码

R-type

instr

operation OP rs rt rd shamt funct
add 000000

sssss

(reg addr)

ttttt

(reg addr)

ddddd

(reg addr)

00000

(no use)

100000
addu 100001
sub 100010
subu 100011
and 100100
or 100101
xor 100110
xnor 100111
slt 101010
sltu 101011
mult 000111
multu 000101

I-type

instr

operation op rs rt imm
addi 001000

sssss

(reg addr)

ttttt

(reg addr)

imm

addiu 001001
andi 001100
ori 001101
xori 001110
slti 001011
sltiu 001100
lw 100011
sw 101001
lui 001111
bne 000101
beq 000100

J-type

instr

operation OP LABEL
J 000010 address

 

2.ALU设计

对于ALU,我们将模块设定为如图1所示(实际上我在此基础上加入了一个输出信号判断此时输出是否为0),是一个有3个输入、2个输出的组合逻辑电路。对于该ALU,我们希望实现除乘法外所有的运算环节(乘法耗时较长,我们另加一个独立单元计算,避免让流水线频率过低)。

技术图片

图1 ALU模块图(来自project guideline)

图1中Func信号来自control模块读取opcode与funct等作出的判断,输入ALU中以判断执行何种操作。Func[2:0]用于实现+,-,&,|,^等运算,Func[3]用于区分符号数运算与非符号数运算,代码如下:

module ALU(
input [31:0] In1E,
input [31:0] In2E,
input [3:0] FuncE,
output reg[31:0] AluoutE,
output ZeroE);//if ZeroE == 1, then AluoutE == 0

wire [31:0] In1_sign, In2_sign;
wire [31:0] ScrA, ScrB;
wire [31:0] minus;
//calculate 2‘s complemental #
assign In1_sign = (In1E[31])? ({In1E[31],(~(In1E[30:0]))}+32b00000000000000000000000000000001) : In1E;
assign In2_sign = (In2E[31])? ({In2E[31],(~(In2E[30:0]))}+32b00000000000000000000000000000001) : In2E;
//assign In1_sign = 

assign ScrA = (FuncE[3])? In1E : In1_sign;
assign ScrB = (FuncE[3])? In2E : In2_sign;
assign minus = ScrA - ScrB;
assign ZeroE = AluoutE? 0 : 1;

always @(*) begin
    case (FuncE[2:0]) 
        3b000 : AluoutE = ScrA+ScrB;
        3b001 : AluoutE = minus;//ScrA-ScrB
        3b010 : AluoutE = ScrA|ScrB;
        3b011 : AluoutE = ScrA&ScrB;
        3b100 : AluoutE = ScrA^ScrB;
        3b101 : AluoutE = ~(ScrA^ScrB);
        3b110 : AluoutE = {31b0,minus[31]};//slt
        3b111 : AluoutE = {ScrB[15:0],16b0};//lui
        default : AluoutE = 32b0;
    endcase
end
endmodule

给出testbench,主要用来判断符号数与非符号数运算(我在这纠结了很久,被绕进去了,感觉ALU中的算法可能很蠢lol),其他就无所谓了因为很简单。

module AlU_tb();

reg [31:0] In1E;
reg [31:0] In2E;
reg [3:0] FuncE;
wire [31:0] AluoutE;
wire ZeroE;

ALU ALU(.In1E(In1E),
    .In2E(In2E),
    .FuncE(FuncE),
    .AluoutE(AluoutE),
    .ZeroE(ZeroE));

initial
begin
In1E = 32b00000000000000000000000000000101;
In2E = 32b10000000000000000000000000000101;
FuncE = 4b1000;
#200
FuncE = 4b0000;
#200
FuncE = 4b1001;
#200
FuncE = 4b0001;

end
endmodule

 

3.数据内存设计

 数据内存为模拟cache功能,因为MIPS为RISC机,即寄存器与外部存储只能通过load/store指令交换数据,我们在开始一个新的程序时,必须要通过lw将操作数导入寄存器文件(Register File)。

module data_memory(
input [31:0] address,
input [31:0] write_data,
input clk,
input write,//enable signal
output [31:0] read_data);

reg [31:0] data_ram [63:0];

assign read_data = data_ram[address[31:2]];//word aligned

always@(posedge clk) begin
    if (write) data_ram[address[31:2]] <= write_data;
end
endmodule

这一模块为时序模块,处于五级流水线的第四级(存储部分)。

4.指令存储

该模块为流水线第一级,取指部分,只有一个输入一个输出,在初始的时候我们将机器码写入指令存储代码内部的寄存器组中,每过一个时钟上升沿,将地址+4得到下一条指令,但遇到j,bne,beq时需要通过特殊的运算跳转到特定的地址取指,这一部分我将在后面的文章中给出(重度拖延症患者)。

module inst_memory(
input [31:0] address,
output [31:0] read_data);

reg [31:0] inst_mem [63:0];

assign read_data = inst_mem[address[31:2]];

initial 
begin
    $readmemh("filename.dat",inst_mem);
//initialize instruction memory
end
endmodule

5.寄存器组

这一模块为译码部分,我们通过译码选中不同的寄存器导出相应数据输送到ALU与乘法器中进行计算,此外也可以通过lw与sw指令与cache中的数据进行交换。其中两个读输出为rs与rt,为了避免在同一时刻进行读和写,我规定了在时钟上升沿读取寄存器数据,下降沿写出寄存器数据。

module reg_file(
input [31:0] wd,//data written in
input [4:0] wr,//write addr
input clk,reg_wr,rst_n,
input [4:0] pr1,pr2,//read addr
output[31:0]rd1,rd2);//output data

reg [31:0] rf [63:0];
integer i;

always@(posedge clk or negedge rst_n) begin
    if (!rst_n) begin 
        for (i=0;i<64;i=i+1) begin rf[i] <= 0; end
    end
    
    if (reg_wr) begin rf[wr] <= wd; end
end 

assign rd1 = pr1?rf[pr1]:0; 
assign rd2 = pr2?rf[pr2]:0; 

endmodule

6.乘法器设计

因为乘法器耗时较长,所以我们将乘法功能从ALU中抽出来,可以直接用*号硬刚。

 

写好各个模块后我们就可以连接各个模块得到数据通路了。

以上是关于从零开始写流水线——模块设计+指令定义的主要内容,如果未能解决你的问题,请参考以下文章

RISC处理器设计-------处理器整体架构

tinyriscv---一个从零开始写的极简易懂的开源RISC-V处理器核

从零开始学架构

自己动手写CPU——第一篇

从零开始配置vim(27)——代码片段

深入浅出计算机组成原理:面向流水线的指令设计(上)-一心多用的现代CPU(第20讲)