CPU设计之一——VerilogHDL 开发单周期处理器(支持10条指令)

CPU设计之二——VerilogHDL 开发流水线处理器(支持42条指令)
CPU设计之三——VerilogHDL 开发流水线处理器(支持50条指令)

所有代码和参考文件已经上传至github:https://github.com/lcy19981225/Single-Cycle-CPU-10

VerilogHDL 开发单周期处理器

  • 一、目标
  • 二、说明
  • 三、模块设计
    • PC
    • PCAdd4
    • ALU
    • CONUNIT
    • dm_4k
    • im_4k
    • MUX
    • RegisterFile
    • SignExtend
    • shifter
    • SingleCycleCPU
    • mips
  • 四、控制器的设计
  • 五、结果验证
    • 验证的汇编程序
    • 模拟结果
    • 我的验证方法
  • 六、实验总结

一、目标

  • 使用Verilog开发基于单周期的处理器。
  • 使用Vivado对开发的处理器进行模拟仿真。
  • 编写测试用汇编,使用Mars工具汇编成为机器代码,并使用它调试测试你开发的处理器。

二、说明

  • 处理器应支持MIPS-Lite2指令集。
    • MIPS-Lite1 = { addu, subu, ori, lw, sw, beq, lui }
    • MIPS-Lite2 = { MIPS-Lite1, jal, jr}

MIPS32指令集的pdf我已经上传到github中,建议大家先好好看看自己要完成的命令的具体含义再开始写!

  • 处理器为单周期设计。
  • 单周期处理器由 datapath(数据通路)和 controller(控制器)组成。 数据通路可以由以下模块组成:
    • PC(程序计数器)
    • NPC(NextPC计算单元)
    • GPR(通用寄存器组,包含32个寄存器)
    • ALU(算术逻辑单元)
    • EXT(扩展单元)
    • IM(指令存储器-只读)
    • DM(数据存储器-可读可写)
    • IM规格:容量为4KB(32bit*1024字)
    • DM规格:容量为4KB(32bit*1024字)
  • 参考的数据通路架构图:
    CPU设计之一——VerilogHDL 开发单周期处理器(支持10条指令)_第1张图片
  • 指令处理流程
    • 取指令(IF):根据程序计数器PC中的指令地址,从指令存储器中取出一条指令,同时PC根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令时,则控制器把“转移地址”送入PC。

    • 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,具体方式根据自己的设定而定

    • 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。

    • 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。

    • 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。

  • code.txt中存储的是指令码。
    • 测试时,code.txt的格式为每条指令占用1行,指令以十六进制文本码形式存储。
    • 请使用verilog建模指令存储器IM时,以读取code.txt的方式载入指令,可以自行学习 $readmemh指令。
    • 调试时你可以使用Mars工具将汇编程序编译成机器码导入code.txt中。请注意Mars工具的内存设置请依据如下

Mars可以将你自己写的测试汇编代码转换成机器码,这样就可以生成你自己想要的而测试样例,如果没有使用过可以搜索一下相关博客啥的,Mars.jar我也同样上传到github中了~

CPU设计之一——VerilogHDL 开发单周期处理器(支持10条指令)_第2张图片

三、模块设计

PC

PC需要对reset进行判断,如果不需要重置,那么只用把PC置为下一条指令即可

module PC(Clk,Reset,Result,Address);  
    input Clk;//时钟
    input Reset;//是否重置地址。0-初始化PC,否则接受新地址       
    input[31:0] Result;
    output reg[31:0] Address;

    initial begin
        Address  <= 0;
    end
    
    always @(posedge Clk or negedge Reset)  
    begin  
    if (!Reset) //如果为0则初始化PC,否则接受新地址
        begin  
            Address <= 0;  
        end  
    else   
        begin
            Address =  Result;  
        end  
    end  
endmodule

PCAdd4

PCAdd4只需要对PC+4就行,按照之前讲的超前进位加法器写了一个cal32用来进行32位加法,代码放在GitHub上了~

module PCAdd4(PC_o,PCadd4);
    input [31:0] PC_o;//偏移量
    output [31:0] PCadd4;//新指令地址
    CLA_32 cla32(PC_o,4,0, PCadd4, Cout);
endmodule

ALU

ALU用来进行各种运算,需要设置一个多路选择器判断ALU B端的输入是来自寄存器堆还是来自扩展单元,同时当运算结果为0是,将控制信号ZERO置为1

module ALU( ReadData1, ReadData2,ALUOp,result,zero);
        input [31:0] ReadData1;
        input [31:0] ReadData2;
        input [2:0] ALUOp;
        output reg[31:0] result;
        output reg zero;
  
    always@(ReadData1 or ReadData2 or ALUOp) 
    begin

        case(ALUOp)
            3'b000: result = ReadData1 + ReadData2;
            3'b001: result = ReadData1 - ReadData2;
            3'b010: result = ReadData2 & ReadData1;
            3'b011: result = ReadData1 | ReadData2;
            3'b100: result = ReadData2<<16;
        endcase
        zero = (result == 0) ? 1 : 0;
    end 

endmodule

CONUNIT

控制单元控制各种信号的值,控制信号有

  • Regrt:判断寄存器堆的写回地址
  • Se:控制扩展单元是零扩展还是符号扩展
  • Wreg:判断是否需要写回寄存器堆
  • Aluqb:控制ALU的B端输入是来自寄存器堆还是扩展单元
  • Aluc[2:0]:控制ALU进行运算的类型
  • Wmem:判断是否需要对dm进行写操作,只有sw会用到
  • Pcsrc[2:0]:判断下一条指令的形式
  • Reg2reg:判断写回寄存器堆的数据来源,是从ALU来还是从dm来,只有lw会用到

对于不同的命令取值如下:
CPU设计之一——VerilogHDL 开发单周期处理器(支持10条指令)_第3张图片

以上控制信号需要一个5位二选一多路选择器,两个个32位二选一多路选择器,一个32位五选一多路选择器,具体实现在MUX.v中介绍

module CONUNIT(Op,Func,Z,Regrt,Se,Wreg,Aluqb,Aluc,Wmem,Pcsrc,Reg2reg);
    input[5:0]Op,Func;
    input Z;
    output Regrt,Se,Wreg,Aluqb,Wmem,Reg2reg;
    output[2:0]Pcsrc;
    output [2:0]Aluc;

    wire R_type=~|Op;
    wire I_add=R_type&Func[5]&~Func[4]&~Func[3]&~Func[2]&~Func[1]&~Func[0];
    wire I_sub=R_type&Func[5]&~Func[4]&~Func[3]&~Func[2]&Func[1]&~Func[0];
    wire I_and=R_type&Func[5]&~Func[4]&~Func[3]&Func[2]&~Func[1]&~Func[0];
    wire I_or=R_type&Func[5]&~Func[4]&~Func[3]&Func[2]&~Func[1]&Func[0];
    wire I_jr=R_type&~Func[5]&~Func[4]&Func[3]&~Func[2]&~Func[1]&~Func[0];

    wire I_addi=~Op[5]&~Op[4]&Op[3]&~Op[2]&~Op[1]&~Op[0];
    wire I_andi=~Op[5]&~Op[4]&Op[3]&Op[2]&~Op[1]&~Op[0];
    wire I_ori=~Op[5]&~Op[4]&Op[3]&Op[2]&~Op[1]&Op[0];
    wire I_lw=Op[5]&~Op[4]&~Op[3]&~Op[2]&Op[1]&Op[0];
    wire I_sw=Op[5]&~Op[4]&Op[3]&~Op[2]&Op[1]&Op[0];
    wire I_beq=~Op[5]&~Op[4]&~Op[3]&Op[2]&~Op[1]&~Op[0];
    wire I_bne=~Op[5]&~Op[4]&~Op[3]&Op[2]&~Op[1]&Op[0];
    wire I_J=~Op[5]&~Op[4]&~Op[3]&~Op[2]&Op[1]&~Op[0];
    wire I_lui=~Op[5]&~Op[4]&Op[3]&Op[2]&Op[1]&Op[0];
    wire I_jal=~Op[5]&~Op[4]&~Op[3]&~Op[2]&Op[1]&Op[0];

    assign Regrt=I_addi|I_andi|I_ori|I_lw|I_sw|I_beq|I_bne|I_J|I_lui|I_jal;
    assign Se=I_addi|I_lw|I_sw|I_beq|I_bne|I_lui;
    assign Wreg=I_add|I_sub|I_and|I_or|I_addi|I_andi|I_ori|I_lw|I_lui|I_jal;
    assign Aluqb=I_add|I_sub|I_and|I_or|I_beq|I_bne|I_J|I_jal|I_jr;
    assign Aluc[2]=I_lui;
    assign Aluc[1]=I_and|I_or|I_andi|I_ori;
    assign Aluc[0]=I_sub|I_or|I_ori|I_beq|I_bne;
    assign Wmem=I_sw;  
    assign Pcsrc[2]=I_jal|I_jr;
    assign Pcsrc[1]=I_beq&Z|I_bne&~Z|I_J;
    assign Pcsrc[0]=I_J|I_jr;
    assign Reg2reg=I_add|I_sub|I_and|I_or|I_addi|I_andi|I_ori|I_sw|I_beq|I_bne|I_J|I_lui|I_jal|I_jr;
endmodule    

dm_4k

只有lw,sw会用到,分别用来存数据和读数据

module dm_4k(Addr,Din,We,Clk,Dout);
    input[31:0]Addr,Din;
    input Clk,We;
    output[31:0]Dout;
    reg[31:0]Ram[1024:0];

    assign Dout=Ram[Addr[6:2]];
    
    always@(posedge Clk)begin
        if(We)
            Ram[Addr[6:2]]<=Din;
    end

    integer i;
    initial begin
        for(i=0;i<32;i=i+1)
            Ram[i]=0;
    end
endmodule

im_4k

从code.txt读取指令码

module im_4k(Addr,Inst);//指令存储器
input[31:0]Addr;
reg [31:0]Rom[1024:0];

output[31:0]Inst;

    initial 
    begin
        $readmemh("C:\\Users\\Y\\Desktop\\code.txt", Rom);
    end
    integer i;
    initial begin
        $display("start simulation");
        for (i=0;i<20;i=i+1)
            $display("%h %h", i,Rom[i]);
    end
    
    assign Inst=Rom[Addr[6:2]];

endmodule

MUX

分别实现了5位二选一多路选择器,32位二选一多路选择器,32位五选一多路选择器,每一个选择器都有信号S,根据信号S可以决定输出

module MUX2X5(A0,A1,S,Y);
    input [4:0] A0,A1;
    input S;
    output [4:0] Y;

    function [4:0] select;
        input [4:0] A0,A1;
        input S;
        case(S)
            0:select=A0;
            1:select=A1;
        endcase
    endfunction

    assign Y=select(A0,A1,S);
endmodule

module MUX2X32(A0,A1,S,Y);
    input [31:0] A0,A1;
    input S;
    output [31:0] Y;

    function [31:0] select;
        input [31:0] A0,A1;
        input S;
        case(S)
            0:select=A0;
            1:select=A1;
        endcase
    endfunction

    assign Y=select(A0,A1,S);
endmodule

module MUX5X32 (A0, A1, A2, A3, A4, S, Y);
    input [31:0] A0, A1, A2, A3, A4;
    input [2:0] S;
    output [31:0] Y;

    function [31:0] select;
        input [31:0] A0, A1, A2, A3, A4;
        input [2:0] S;
        case(S)
            3'b000: select = A0;
            3'b001: select = A1;
            3'b010: select = A2;
            3'b011: select = A3;
            3'b100: select = A3;
            3'b101: select = A4;
        endcase
    endfunction

    assign Y = select (A0, A1, A2, A3, A4, S);
endmodule

RegisterFile

寄存器堆需要找到存在寄存器中的值,还需要往寄存器堆中写入值

module RegisterFile(ReadReg1,ReadReg2,WriteData,WriteReg,RegWrite,CLK,Reset,ReadData1,ReadData2,Pcsrc,PcAdd4);
    input [4:0] ReadReg1;//rs
    input [4:0] ReadReg2;//rt或者立即数
    input [31:0] WriteData;//写入的数据
    input [4:0] WriteReg;//写入地址
    input RegWrite; //写信号
    input CLK;
    input Reset;
    output reg[31:0] ReadData1;
    output reg[31:0] ReadData2;
    input [2:0] Pcsrc;
    input [31:0] PcAdd4;
    

    initial begin
       ReadData1 <=0;
       ReadData2 <=0; 
    end

    reg [31:0] regFile[0:31];
    integer i;
    initial begin
        for (i=0;i<32;i=i+1)
            regFile[i]<=0;
    end
        
    always@(ReadReg1 or ReadReg2)
    begin
        ReadData1 = regFile[ReadReg1];
        ReadData2 = regFile[ReadReg2];
        //$display("regfile %d %d\n", ReadReg1, ReadReg2);
        
    end
     
    always@(negedge CLK )
    begin
     //$display("lala");
        if(RegWrite && WriteReg)
        begin
            regFile[WriteReg] = WriteData;
            //$display("%d %d",WriteReg, WriteData);
        end
    end

    always@(negedge CLK)
    begin
        //$display("lala");
        if(Pcsrc == 3'b100 && RegWrite )
        begin
        //$display("%h",PcAdd4);
            regFile[31] = PcAdd4;
        end
    end
endmodule


SignExtend

进行扩展操作,零扩展或者符号扩展

module SignExtend (X, Se, Y);
    input [15:0] X;
    input Se;
    output [31:0] Y;
    wire [31:0] E0, E1;
    wire [15:0] e = {
     16{
     X[15]}};
    parameter z = 16'b0;
    assign E0 = {
     z, X};
    assign E1 = {
     e, X};
    MUX2X32 i(E0, E1, Se, Y);
endmodule

shifter

进行移位操作,包括将立即数左移两位,还有取PC+4前4位+address26位+两个0

module SHIFTER_COMBINATION(X,PCADD4,Sh);
    input [26:0] X;
    input [31:0] PCADD4;
    output [31:0] Sh;
    parameter z=2'b00;
    assign Sh={
     PCADD4[31:28],X[26:0],z};
endmodule

module SHIFTER32_L2(X,Sh);
    input [31:0] X;
    output [31:0] Sh;
    parameter z=2'b00;
    assign Sh={
     X[29:0],z};
endmodule

SingleCycleCPU

将所有的模块合起来

module MAIN(Clk,Reset,Addr,Inst,Qa,Qb,ALU_R,NEXTADDR,D);
  input Clk,Reset;
  output [31:0] Inst,NEXTADDR,ALU_R,Qb,Qa,Addr,D;



  wire [31:0]Result,PCadd4,EXTIMM,InstL2,EXTIMML2,D,Y,Dout,mux4x32_2,R;
  wire Z,Regrt,Se,Wreg,Aluqb,Reg2reg,Cout,Wmem;
  wire [2:0]Pcsrc;
  wire [2:0]Aluc;
  wire [4:0]Wr;

  PC pc(Clk,Reset,Result,Addr);
  PCAdd4 pcadd4(Addr,PCadd4);
  im_4k instmem(Addr,Inst);

  CONUNIT conunit(Inst[31:26],Inst[5:0],Z,Regrt,Se,Wreg,Aluqb,Aluc,Wmem,Pcsrc,Reg2reg);
  MUX2X5 mux2x5(Inst[15:11],Inst[20:16],Regrt,Wr);

  SHIFTER_COMBINATION shifter1(Inst[26:0],PCadd4,InstL2);//J指令的跳转地址

  RegisterFile regfile(Inst[25:21],Inst[20:16],D,Wr,Wreg,Clk,Reset,Qa,Qb,Pcsrc,PCadd4);

  ALU alu(Qa,Y,Aluc,R,Z);
  dm_4k datamem(R,Qb,Wmem,Clk,Dout);
  MUX2X32 mux2x322(Dout,R,Reg2reg,D);//写寄存器堆的来源,是ALU 还是 DM
  SignExtend ext16t32(Inst[15:0],Se,EXTIMM);//扩展 0还是符号
  MUX2X32 mux2x321(EXTIMM,Qb,Aluqb,Y);//选择ALU B端的来源
  SHIFTER32_L2 shifter2(EXTIMM,EXTIMML2);
  CLA_32 cla_32(PCadd4,EXTIMML2,0,mux4x32_2, Cout);//beq bne的跳转至零


  MUX5X32 mux5x32(PCadd4,0,mux4x32_2,InstL2,Qa,Pcsrc,Result);//选择跳转的地址

  assign NEXTADDR=Result;
  assign ALU_R=R;

  integer i;
  initial begin
          $display("start simulation");
          //for (i=0;i<3;i=i+1)
            //  $display("%h %h", i,instmem.Rom[i]);
          //$display("%h %h",regfile.ReadReg1,regfile.ReadReg2);
          //$display("%h",pc.Address);
          //$display("%h",instmem.Addr);
      end

endmodule

mips

最后的模拟仿真文件,包括clk(不断进行翻转)和reset(最开始进行初始化之后就一直不需要初始化了)

module mips;
wire [31:0] Addr,Inst,Qa,Qb,ALU_R,NEXTADDR;

    reg CLK;
    reg Reset;
    MAIN uut(
    .Clk(CLK),
    .Reset(Reset),
    .Addr(Addr),
    .Inst(Inst),
    .Qa(Qa),
    .Qb(Qb),
    .ALU_R(ALU_R),
    .NEXTADDR(NEXTADDR),
    .D(D)
    );

    initial begin
        CLK = 0;
        Reset = 0;
        
        CLK = !CLK;  // 下降沿,使PC先清零
        Reset = 1;  // 清除保持信号
        forever #20
        begin 
             CLK = !CLK;
        end
    end
endmodule

四、控制器的设计

考虑各种指令的流程
CPU设计之一——VerilogHDL 开发单周期处理器(支持10条指令)_第4张图片

Regrt:所有的l,j型指令会写回到rt

Se:参考mips手册即可

Wreg:除了bne,beq,j,jr都需要写回寄存器堆

Aluqb:对于需要进行立即数扩展的指令就需要从扩展单元输入

Aluc:按自己的想法设计就好,我设计的方法如下
CPU设计之一——VerilogHDL 开发单周期处理器(支持10条指令)_第5张图片

Wmem:只有sw会用到

Pcsrc:也是根据自己的想法设计

Reg2reg:除了lw之外都是直接从ALU写回寄存器堆

五、结果验证

验证的汇编程序

首先通过addi给$1赋值,之后测试了addu,subu,ori,lw,sw,beq,lui,jal,jr等指令

addi $1,$0,8 #$1=8
ori $2,$0,12 #$2=12
addu $3,$1,$2 #$3=20
subu $4,$2,$1 #$4=4
and $5,$1,$2
or $6,$1,$2
lui $7 100 #$8=100<<64
bne $1,$2,p1
p1:
beq $1,$2,out
j p3
p3:
sw $2 10($8)
lw $4 10($8)
beq $2,$4,p4
p4:
andi $9,$2,9

loop:
jal minus
bne $1 $2 loop
j out
minus:
addi $1 $1 1
jr $ra

out:

模拟结果

CPU设计之一——VerilogHDL 开发单周期处理器(支持10条指令)_第6张图片

我的验证方法

  • 查看I_addu等信号来判断执行的是哪一条指令
  • 查看Regrt等信号是否正确
  • 查看寄存器堆的读写情况可以很好地判断进行的操作是否正确
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyppMd4c-1589006437771)(C:\Users\Y\AppData\Roaming\Typora\typora-user-images\image-20191128202442910.png)]
    通过检查,全部指令正确,尤其是针对jal和jr,在一个循环中使用了函数调用,最后循环四次之后退出,如下图,同样正确
    在这里插入图片描述
    至此,实验全部结束

六、实验总结

  1. 出现过很多bug不太明白错在哪里,善于利用$display能够进行调试
  2. 当指令数比较少的时候可以人工进行排查然后写excel表格,但当指令数达到几十条之后可能就需要通过代码来生成各种控制型号的值
  3. 在进行仿真的时候可以一条条地进行模拟,慢慢地完成
  4. 在进行bne指令的时候,不知道为什么根据手册上的形如 bne $1 $2 2 的指令的时候会报错,一定要跳转到一个label才行
  5. 通过这次试验也算是搞清楚了整个cpu的工作流程,期待接下来的多周期流水线实验
  6. 有一个问题尚未解决,就是在im_4k中用readmemh读取code.txt的时候采用相对路径不知道为什么读不进去,就只好改成绝对路径

在这里插入图片描述

在所有独立完成的大作业中这个的复杂程度应该算得上数一数二的了,所以想要写篇博客记录一下,之后也会更新支持50条指令的流水线CPU,也算留个纪念hhh,如有疏漏之处,欢迎指正,有问题也欢迎大家一起交流~

你可能感兴趣的:(现代处理器设计,Verilog,现代处理器设计,Modern,Processor,mips)