전공 이야기/Digital System Design

#3 MIPS Pipeline Processor Design (1) - IF Stage, Sub Unit

[감자] 2023. 12. 26. 19:42

다음 내용은 Digital System Design 수업 기말 팀 프로젝트로 제출한 과제로

모든 코드를 학부생 두 명이 직접 만들다보니 미처 확인하지 못한 오류가 있을 수 있다.

 

하지만 여러 명령어 시나리오에서 다음의 명령어들이 제대로 작동하는 모습을 보였으며

Data Hazard, Control Hazard 등을 잘 해결하는 모습을 보였다.

주어진 코드를 잘 따라한다면 Verilog에서의 MIPS 명령어 체계의 Pipeline processor 구현이 가능하다.

 

 

 

MIPS 는 MIPS사에서 개발한 instruction set으로

Computer Architecture의 Pipeline, Hazard 등의 내용을 안다고 가정하고 진행한다.

 

완성된 Full Datapath는 다음과 같다.

 

 

 

 

 

 

1. Verilog를 이용한 MIPS 5단계 Pipeline 설계

 

1.1 Sub Unit

 MIPS 5 단계 파이프라인을 설계하기 위해서는 Mux, D – flip flop 와 같은 다양한 하위 
유닛들의 설계를 우선 진행해야 한다.

 

 

1.1.1 D - filp flop

D–flip flop 동작 설명
 D 플립플롭은 데이터 신호(D)를 저장하고, 클럭 신호(clk)의 상승 에지에서 저장된 데이터를 출력한다. 이 모듈은 추가로 리셋 신호(rst)와 활성화를 위한 인에이블 신호(En)를 사용한다. 리셋 신호가 활성화되면 Q를 0으로 설정하고, 그렇지 않고 인에이블 신호가 활성화되면 D의 값을 Q에 복사한다

 

module dff(
 
    output reg [31:0] Q,
    input [31:0] D,
    input clk, En, rst
 
);
    always @(posedge clk, posedge rst)
      if(rst) Q <= 32'd0;
      else if(En) Q <= D;
 
endmodule

 

 

 

 

1.1.2 Decoder

Decoder 동작 설명

 디코더(Decoder)는 다수의 입력 중에서 특정 패턴의 입력에 대응하는 출력을 생성하는 디지털 논리 회로이다. MIPS 5단계 파이프라인 설계를 위해 사용될 Decoder에는 2개의 입력으로 4가지 값 중 하나를 선택하는 Decoder_2to4, 3개의 입력으로 8가지 값 중 하나를 선택하는 Decoder_3to8, 그리고 5개의 입력을 받아 32가지 값 중 하나를 선택하는 Decoder_5to32 이렇게 세 가지 종류가 사용된다.

 

module Decoder_2to4(
    output reg [3:0] Y,
    input [1:0] X,
    input En
    );
    always @*
    if(En)
      case(X)
        0: Y = 2'b01;
        default: Y = 2'b10;
      endcase
    else
      Y = 0;
 
endmodule
module Decoder_3to8(
    output reg [7:0] Y,
    input [2:0] X,
    input En
    );
always @*
    if(En)
      case(X)
        0: Y = 8'b00000001;
        1: Y = 8'b00000010;
        2: Y = 8'b00000100;
        3: Y = 8'b00001000;
        4: Y = 8'b00010000;
        5: Y = 8'b00100000;
        6: Y = 8'b01000000;
        default: Y = 8'b10000000;
      endcase
    else
      Y = 0;
endmodule
module Decoder_5to32(
    output [31:0] Y,
    input [4:0] X,
    input En
);
    wire [3:0] YtoEn;
   
    Decoder_2to4 U0 (.Y(YtoEn), .X(X[4:3]), .En(En));
   
    Decoder_3to8 U1 (.Y(Y[7:0]), .X(X[2:0]), .En(YtoEn[0]));
    Decoder_3to8 U2 (.Y(Y[15:8]), .X(X[2:0]), .En(YtoEn[1]));
    Decoder_3to8 U3 (.Y(Y[23:16]), .X(X[2:0]), .En(YtoEn[2]));
    Decoder_3to8 U4 (.Y(Y[31:24]), .X(X[2:0]), .En(YtoEn[3]));
   
endmodule

 

 

 

 

1.1.3 Multiplexer

Multiplexer 동작 설명

 멀티플렉서(Multiplexer 또는 Mux)는 여러 개의 입력 중 하나를 선택하여 출력으로 전달하는 디지털 논리 회로이다. 이는 데이터 선택기 또는 데이터 스위치라고도 불린다. 주로 컴퓨터 시스템에서 다양한 용도로 사용된다. MIPS 5단계 파이프라인 설계를 위해 사용할 Mux의 종류로는 1비트 신호로 2개의 입력을 사용하는 5bit Mux_2와 32bit Mux_2, 4개의 입력 신호 중 하나를 선택하는 Mux_4, 32개의 입력신호 중 하나를 선택하는 Mux_32가 있다.

 

module Mux_2(
    input wire [31:0] X0, X1,
    input wire sel,
    output reg [31:0] Y
    );
   
    always @*
      if (sel)
        Y = X1;
      else
        Y = X0;
   
endmodule
module Mux5_2(
    input wire [4:0] X0, X1,
    input wire sel,
    output reg [4:0] Y
    );
always @*
      if (sel)
        Y = X1;
      else
        Y = X0;
   
endmodule
module Mux_4(
    input wire [31:0] X0, X1, X2, X3,
    input wire [1:0] sel,
    output reg [31:0] Y
    );
   
    always @*
      case(sel)
        0: Y = X0;
        1: Y = X1;
        2: Y = X2;
        default: Y = X3;
      endcase
endmodule
module Mux_32(
    output reg [31:0] Y,
    input [31:0] X0, X1, X2, X3, X4, X5, X6, X7, X8,
    input [31:0] X9, X10, X11, X12, X13, X14, X15, X16,
    input [31:0] X17, X18, X19, X20, X21, X22, X23, X24,
    input [31:0] X25, X26, X27, X28, X29, X30, X31,
    input [4:0] Sel
    );
always @(*)
      case(Sel)
        0: Y = X0;
        1: Y = X1;
        2: Y = X2;
        3: Y = X3;
        4: Y = X4;
        5: Y = X5;
        6: Y = X6;
        7: Y = X7;
        8: Y = X8;
        9: Y = X9;
        10: Y = X10;
        11: Y = X11;
        12: Y = X12;
        13: Y = X13;
        14: Y = X14;
        15: Y = X15;
        16: Y = X16;
        17: Y = X17;
        18: Y = X18;
        19: Y = X19;
        20: Y = X20;
        21: Y = X21;
        22: Y = X22;
        23: Y = X23;
        24: Y = X24;
        25: Y = X25;
        26: Y = X26;
        27: Y = X27;
        28: Y = X28;
        29: Y = X29;
        30: Y = X30;
        default: Y = X31;
      endcase
 
endmodule

 

 

1.1.4 Shifting

 

Shifting module 동작 설명

 시프트(Shifting) 모듈은 입력 데이터 비트들을 특정 방향으로 이동시키는 논리 회로이다. 주로 데이터 이동 및 회전과 같은 연산에 사용된다. 입력 값 left에 따라 bit를 왼쪽으로 이동시킬지 오른쪽으로 이동시킬지가 결정된다.

module Shifting(
    input [31:0] Din,
    input [4:0] shamt,
    input left,
    output reg [31:0] Dout
    );
   
    always @*
      if(left)
        Dout = Din << shamt;
      else
        Dout = Din >> shamt;
endmodule

 

 

 

 

 

 

 

1.2 IF Stage

 

 

 

 IF_Stage는 Program Counter (PC)에 저장된 값에 따라 Instruction Memory에서 명령어를 추출하고, 이를 현재의 Program Counter 값 및 현재의 명령어와 함께 IF_ID 레지스터로 전송한다. 동시에 Adder를 통해 Program Counter 값을 4만큼 증가시키며, PCSrc 값에 따라 다음 Program Counter 값을 결정한다. PCSrc 값이 1인 경우, Beq 명령어로 인해 이동된 값을 사용하고, 0인 경우에는 기본적으로 4가 증가된 값을 사용한다. 이렇게 다음 Program Counter 값을 설정하여 다음 명령어를 가져온다.

 

 IF_Stage의 Input은 clk와 rst, Hazard가 발생한 경우 Program Counter를 Stall하는 PCwrite, Branch의 값과 adder를 거친 값 중에서 다음 Program Counter을 결정하기 위한 mux의 값을 결정하는 PCSrc, Beq instruction으로 Program Counter의 값이 변경될 때 이동할 주소 값으로 이루어져 있다.

 

 Output은 IF_ID 레지스터로 전달되는 값인 IFtoID_PC와 IFtoID_inst, 그리고 현재 Program Counter의 값을 출력하기 위한 값인 PC_current로 구성되어 있다.

 

 가산기(Adder)의 출력 값, 즉 덧셈 결과는 add_result라는 wire 변수에 연결되어 있다. 이 add_result는 mux의 입력값으로 사용된다. 또한, MuxtoPC라는 wire 변수는 Mux에서 선택된 다음 주소 값을 Program Counter의 입력으로 전달하고 있으며, PCtoInst는 PC의 값을 Adder와 Instruction memory로 전송된다.

 

module IF_Stage(
        input wire clk,
        input wire rst,
       
        input wire PCWrite,
        input wire PCSrc,
        input wire [31:0] Branch,
       
        output wire [31:0] IFtoID_PC,
        output wire [31:0] IFtoID_inst,
       
        output wire [31:0] PC_current
    );
 
        wire [31:0] add_result;
        wire [31:0] Branch_value;
       
        wire ID_PCSrc;
       
        wire [31:0] MuxtoPC;
        wire [31:0] PCtoInst;
       
        assign Branch_value = Branch;
        assign ID_PCSrc = PCSrc;
 
        Mux_2 u0 (
            .X0(add_result),
            .X1(Branch_value),
            .sel(ID_PCSrc),
            .Y(MuxtoPC)
        );
 
ProgramCounter u1 (
            .clk(clk), .rst(rst),
            .PCWriteValue(MuxtoPC),
            .PCWrite(PCWrite),
            .pc(PCtoInst)
        );
 
   
        Adder u2 (
            .operandA(PCtoInst),
            .operandB(4),
            .sum(add_result)
        );
   
   
        InstructionMemory u3 (
            .clk(clk),
            .address(PCtoInst),
            .instruction(IFtoID_inst)
        );
       
        assign IFtoID_PC = add_result;
        assign PC_current = PCtoInst;
   
    endmodule

 

 

 

1.2.1 Program Counter

Program Counter 동작 설명

 프로그램 카운터(Program Counter)는 다음에 실행할 명령어의 주소를 저장하는 레지스터로, 현재 실행 중인 명령어의 다음 주소를 가리킨다.

 

Input 값 으로 클럭(clk)과 리셋(rst)과 함께 32비트 PCWriteValue가 들어온다. PCWriteValue는 프로그램 카운터의 다음 주소 값과 연결되어 있으며, 일반적으로는 Adder를 통해 기존 주소값에 4를 더한 값을 가져오지만 beq 명령어를 통해 다음 주소 값이 변경되는 경우에는 주소값이 주어진 값으로 조정된다. PCWrite 또한 프로그램 카운터의 또 다른 입력 신호로,  Hazard Detection Unit에서 전송되는 신호다. PCWrite는 계속 1로 값을 가지고 있지만, Lw 명령어에서 Hazard가 발생하는 경우 PCWrite의 값을 0으로 변환 시키며, 한 주기 동안 Stall하여 안전하게 진행되도록 하는 역할을 한다.

 

Output 값으로는 현재 프로그램 카운터의 변경된 값이 나온다. 이 값은 명령어 메모리, Adder와 연결되어 있으며, 현재 PC 값을 출력하기 위해 MIPS top 모듈에도 연결되어 있다.

 

module ProgramCounter(
    input wire clk,
    input wire rst,
   
    input wire [31:0] PCWriteValue,  // PC에 쓰여질 값
    input wire PCWrite,
   
    output reg [31:0] pc                // 현재 PC 값
);
    always @(posedge clk or posedge rst) begin
        if (rst == 1)
            pc <= 0;
   
        else if (PCWrite)                 // PCwrite Control unit이 활성화되면
            pc <= PCWriteValue;      // 그 값으로 pc 덮어쓰기
    end
 
endmodule

 

 

1.2.2 Adder

Adder 동작 설명

 가산기(Adder)는 피연산자 Operand A와 Operand B의 값을 받아 더한 후 덧셈 결과를 출력하는 모듈이다. Operand B는 IF_stage 코드에서 4로 설정되어 있으며, Operand A에는 Program Counter의 출력 값, 즉 현재의 PC 주소 값이 입력된다. 가산기 모듈의 Output인 덧셈 결과는 다음 IF_ID 파이프 레지스터에 저장되고 동시에 Program Counter로 다시 전달된다.

 

Operand B의 값을 4로 고정시킨 이유는 메모리의 각 주소에는 1바이트의 데이터 값이 저장되므로, 1개의 완전한 명령어를 얻기 위해서는 4바이트가 필요하다. 따라서 다음 Program Counter의 값은 현재 값에 4를 더한 값이 되므로 Operand B에 4를 지정한다.

 

module Adder(
    input wire [31:0] operandA, // 피연산자 A
    input wire [31:0] operandB, // 피연산자 B
    output wire [31:0] sum       // 덧셈 결과
);
 
    assign sum = operandA + operandB;
 
endmodule

 

 

 

1.2.3 Instruction Memory

Instruction Memory 동작 설명

 

  Instruction Memory는 Program Counter로부터 전달받은 주소값을 사용하여 메모리에서 명령어를 가져오는 역할을 한다. 일반적으로는 Memory rom 모듈을 사용하여 명령어를 가져오는 과정을 거치겠지만, 이번에 구현한 MIPS에서는 Initial begin 문을 활용하여 직접 Instruction Memory 내부에 명령어를 할당한다.

 

클럭(clk) 값과 함께 Program Counter로부터 전달받은 현재 주소값이 Input으로 제공되며, Output으로는 Program Counter 값을 읽고 해당 주소에서 명령어를 Memory에서 찾아내어 내보낸다.

module InstructionMemory(
    input wire clk,          
    input wire [31:0] address,
    output reg [31:0] instruction
);
 
    reg [7:0] memory [0:1023];
 
always @(negedge clk) begin
        instruction <= {memory[address], memory[address+1], memory[address+2], memory[address+3]};
    end
 
   Initial begin
   //소스코드 예제
   end
 
endmodule

 

사용할 예제 소스코드는 다음과 같다

        memory[0] = 8'b00000000;
        memory[1] = 8'b00000000;
        memory[2] = 8'b00000000;
        memory[3] = 8'b00000000;     // dummy
        //000000 00000 00000 00000 00000 000000  
        memory[4] = 8'b00100001;
        memory[5] = 8'b00001000;
        memory[6] = 8'b00000000;
        memory[7] = 8'b00001000;     // addi $t0, $t0, 8
        //001000 01000 01000 0000000000001000
        memory[8] = 8'b00100001;
        memory[9] = 8'b00101001;
        memory[10] = 8'b00000000;
        memory[11] = 8'b00001000;     // addi $t1, $t1, 8
        //001000 01001 01001 0000000000001000   
        memory[12] = 8'b00010001;
        memory[13] = 8'b00001001;
        memory[14] = 8'b00000000;
        memory[15] = 8'b01100100;     // beq $t0, $t1, 100
        //000100 01000 01001 0000000001100100
        memory[16] = 8'b00000001;
        memory[17] = 8'b00101000;
        memory[18] = 8'b01011000;
        memory[19] = 8'b00100000;    // add $t3, $t1, $t0
        //000000 01001 01000 01011 00000 100000
        memory[20] = 8'b00000001;
        memory[21] = 8'b00101011;
        memory[22] = 8'b01011000;
        memory[23] = 8'b00100000;    // add $t3, $t3, $t1
        //000000 01001 01011 01011 00000 100000
        memory[416] = 8'b00000001;
        memory[417] = 8'b00101000;
        memory[418] = 8'b01010000;
        memory[419] = 8'b00100000;   // add $t2, $t1, $t0
        //000000 01001 01000 01010 00000 100000
        memory[420] = 8'b00100001;
        memory[421] = 8'b00001000;
        memory[422] = 8'b00000000;
        memory[423] = 8'b00001111;       // addi $t0, $t0, 15
        //001000 01000 01000 0000000000001000
        memory[424] = 8'b00100001;
        memory[425] = 8'b00101001;
        memory[426] = 8'b00000000;
        memory[427] = 8'b00000101;       // addi $t1, $t1, 5
        //001000 01001 01001 0000000000001000 
        memory[428] = 8'b00000001;
        memory[429] = 8'b00101000;
        memory[430] = 8'b01010000;
        memory[431] = 8'b00101010;      // slt $t2, $t1, $t0
        //000000 01001 01000 01010 00000 101010
        memory[432] = 8'b00010001;
        memory[433] = 8'b00001001;
        memory[434] = 8'b00000000;
        memory[435] = 8'b00001010;     // beq $t0, $t1, 10
        //000100 01000 01001 0000000001100100
        memory[436] = 8'b00000001;
        memory[437] = 8'b00001001;
        memory[438] = 8'b01011000;
        memory[439] = 8'b00100010;    // sub $t3, $t0, $t1
        //000000 01000 01001 01011 00000 100010
        memory[476] = 8'b00100000;
        memory[477] = 8'b00001000;
        memory[478] = 8'b00000000;
        memory[479] = 8'b00001111;   // addi $t0, $zero, 15
       
        memory[480] = 8'b00100000;
        memory[481] = 8'b00001001;
        memory[482] = 8'b00000000;
        memory[483] = 8'b00001111;   // addi $t1, $zero, 16
        memory[0] = 8'b00000000;
        memory[1] = 8'b00000000;
        memory[2] = 8'b00000000;
        memory[3] = 8'b00000000;     // dummy
        //000000 00000 00000 00000 00000 000000       
        memory[4] = 8'b00100001;
        memory[5] = 8'b00001000;
        memory[6] = 8'b00000000;
        memory[7] = 8'b00000000;     // addi $t0, $t0, 0
        //001000 01000 01000 0000000000000000
        memory[8] = 8'b00100001;
        memory[9] = 8'b00101001;
        memory[10] = 8'b00000000;
        memory[11] = 8'b00000001;     // addi $t1, $t1, 1
        //001000 01001 01001 0000000000000001       
        memory[12] = 8'b00000001;
        memory[13] = 8'b00001001;
        memory[14] = 8'b01010000;
        memory[15] = 8'b00100100;    // and $t2, $t0, $t1
        //000000 01000 01001 01010 00000 100100
        memory[16] = 8'b00000001;
        memory[17] = 8'b00001001;
        memory[18] = 8'b01011000;
        memory[19] = 8'b00100101;    // or $t3, $t0, $t1
        //000000 01000 01001 01011 00000 100101
        memory[20] = 8'b00000001;
        memory[21] = 8'b00101001;
        memory[22] = 8'b01010000;
        memory[23] = 8'b00100100;    // and $t2, $t1, $t1
        //000000 01001 01001 01010 00000 100100
        memory[24] = 8'b00000001;
        memory[25] = 8'b00001000;
        memory[26] = 8'b01011000;
        memory[27] = 8'b00100101;    // or $t3, $t0, $t0
        //000000 01000 01000 01011 00000 100101
        memory[28] = 8'b00000001;
        memory[29] = 8'b00001001;
        memory[30] = 8'b01000000;
        memory[31] = 8'b00101010;    // slt $t0, $t0, $t1
        //000000 01000 01001 01000 00000 101010
memory[32] = 8'b00000001;
        memory[33] = 8'b00001001;
        memory[34] = 8'b01011000;
        memory[35] = 8'b00100100;    // and $t3, $t0, $t1
        //000000 01000 01001 01011 00000 100100
        memory[36] = 8'b00000001;
        memory[37] = 8'b00101000;
        memory[38] = 8'b01010000;
        memory[39] = 8'b00100000;    // add $t2, $t1, $t0
        //000000 01001 01000 01010 00000 100000
        memory[40] = 8'b00000001;
        memory[41] = 8'b01001001;
        memory[42] = 8'b01011000;
        memory[43] = 8'b00100000;    // add $t3, $t2, $t1
        //000000 01010 01001 01011 00000 100000
        memory[44] = 8'b00000001;
        memory[45] = 8'b01101001;
        memory[46] = 8'b01011000;
        memory[47] = 8'b00100010;    // sub $t3, $t3, $t1
        //000000 01011 01001 01011 00000 100010
        memory[48] = 8'b10001101;
        memory[49] = 8'b00001001;
        memory[50] = 8'b00000000;
        memory[51] = 8'b00000111;    // lw $t1, 7($t0)
        //100011 01000 01001 00000000000111
        memory[52] = 8'b00000001;
        memory[53] = 8'b00101010;
        memory[54] = 8'b01000000;
        memory[55] = 8'b00100010;    // sub $t0, $t1, $t2
        //000000 01001 01010 01000 00000 100010
        memory[56] = 8'b10101101;
        memory[57] = 8'b00001000;
        memory[58] = 8'b00000000;
        memory[59] = 8'b00000110;    // sw $t0, 6($t0)
        //101011 01000 01000 0000000000000101

 

 

2번 시나리오에서는 각 명령어에서 발생하는 Hazard에 중점을 둔다. R-type instruction의 경우, 이전 단계에서 계산된 값을 레지스터에 저장하기 전에 미리 forwarding unit을 통해 가져오게 된다. lw와 sw instruction의 경우 forwarding에 더불어 Hazard detection unit을 사용하여 Program Counter을 한 번 Stall시키게 된다.