3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

4bit TD4と32bit RISC-V RV32Iの実装の比較

Last updated at Posted at 2021-04-24

simpleTD4とASFRV32Iとの比較

simpleTD4は4bit CPU TD4のシングルサイクルのVerilogによる60行程度の1ファイルの実装です。

ASFRV32IはRISC-V RV32IのシングルサイクルのVerilogによる190行程度の1ファイルの実装です。

同一の設計方針で記述された4bit CPUと32bit CPUの2つの実装を比較検討することで、CPUの構造の理解をより深めることができるかと思います。

simpleTD4の解説ASFRV32Iの設計と実装の解説 も参考にしながら読んでみてください。

simpleTD4のブロックダイアグラム

TD4_block_diagram.png

ASFRV32Iのブロックダイアグラム

ASFRV32I_block_diagram.png

simpleTD4とASFRV32Iとの比較

シングルサイクルなので全体的な流れは同じです。

どちらも1クロックの間に、命令読み出し(Fetch)、解読(Decode)、演算(Execute)、メモリの読み書き(Memory ASFRV32Iのみ)、レジスタへの書き出し(Write Back)、次のPC書き出し(Next PC)のステージ順に実行します。

命令メモリ

simpleTD4は10~11行目です。ROM.binに書かれた8bit2進数の命令を読み込みます。

  reg [7:0] rom[0:15];
  initial $readmemb("ROM.bin", rom);

ASFRV32Iは8~9行目です。データメモリと共通で8bit16進数で命令を読み込みます。

  reg [7:0] mem[0:4095]; // MEMORY 4KB
  initial $readmemh("test.hex", mem);

Fetch

simpleTD4は13~14行目で1命令8bit固定でpcに従い読み込みopcodeを出力します。

  wire [7:0] opcode;
  assign opcode = rom[pc];

ASFRV32Iは11~12行目で1命令32bit固定でpcに従い読み込みopcodeを出力します。

  wire [31:0] opcode;
  assign opcode = {mem[pc + 3], mem[pc + 2], mem[pc + 1], mem[pc]};

Decode

simpleTD4は17~23行目で上位4bitが命令、下位4bitが即値、上位4bitから
alu_sel,load_sel,jmpを出力しています。

  wire [1:0] alu_sel, load_sel;
  wire jmp;
  wire [3:0] im; // IMMEDIATE 
  assign alu_sel = (opcode[7:6] == 2'b11) ? 2'b11 : opcode[5:4];
  assign load_sel = opcode[7:6];
  assign jmp = opcode[4];
  assign im = opcode[3:0];

ASFRV32Iは15~68行目で、32bit命令からRFORMAT等の各命令フォーマットに従って、読み出しや書き出しに使うレジスタを指定するr_addr1, r_addr2, w_addr、即値(imm)、加法やシフト演算などALUへの指示を示すalucon、分岐命令やメモリ読み書き内容を示すfunct3、ALUへの入力のセレクターop1sel, op2selへの指示、メモリへ書き込むかどうかを決めるmem_rw, レジスタへ書き込むかどうかを決めるrf_wen、レジステに書き込む内容を選択するwb_sel、次のPCを決めるpc_selが出力されます。

  wire [5:0] r_addr1, r_addr2, w_addr;
  wire [31:0] imm;
  wire [3:0] alucon;
  wire [2:0] funct3;
  wire op1sel, op2sel, mem_rw, rf_wen;
  wire [1:0] wb_sel, pc_sel;

  wire [6:0] op;
  assign op = opcode[6:0];

  localparam [6:0] RFORMAT       = 7'b0110011;
  localparam [6:0] IFORMAT_ALU   = 7'b0010011;
  localparam [6:0] IFORMAT_LOAD  = 7'b0000011;
  localparam [6:0] SFORMAT       = 7'b0100011;
  localparam [6:0] SBFORMAT      = 7'b1100011;
  localparam [6:0] UFORMAT_LUI   = 7'b0110111;
  localparam [6:0] UFORMAT_AUIPC = 7'b0010111;
  localparam [6:0] UJFORMAT      = 7'b1101111;
  localparam [6:0] IFORMAT_JALR  = 7'b1100111;
  localparam [6:0] ECALLEBREAK   = 7'b1110011;
  localparam [6:0] FENCE         = 7'b0001111;

  assign r_addr1 = (op == UFORMAT_LUI) ? 5'b0 : opcode[19:15];
  assign r_addr2 = opcode[24:20];
  assign w_addr =  opcode[11:7];

  wire [31:0] i_sext, s_sext, sb_sext, u_sext, uj_sext;

  assign i_sext = ((op == IFORMAT_ALU) && ((opcode[14:12] == 3'b001) || (opcode[14:12] == 3'b101))) ? {27'b0, opcode[24:20]} :  // SLLI or SRLI or SRAI
                                                                           (opcode[31] == 1'b1) ? {20'hfffff, opcode[31:20]} : {20'h00000, opcode[31:20]};
  assign s_sext = (opcode[31] == 1'b1) ? {20'hfffff, opcode[31:25],opcode[11:7]} : {20'h00000, opcode[31:25],opcode[11:7]};
  assign sb_sext = (opcode[31] == 1'b1) ? {19'h7ffff, opcode[31], opcode[7], opcode[30:25], opcode[11:8], 1'b0} : {19'h00000, opcode[31], opcode[7], opcode[30:25], opcode[11:8], 1'b0};
  assign u_sext = {opcode[31:12], 12'b0};
  assign uj_sext = (opcode[31] == 1'b1) ? {11'h7ff, opcode[31], opcode[19:12], opcode[20], opcode[30:21], 1'b0} : {11'h000, opcode[31], opcode[19:12], opcode[20], opcode[30:21], 1'b0};
  assign imm = ((op == IFORMAT_ALU) || (op == IFORMAT_LOAD) || (op == IFORMAT_JALR))  ? i_sext :
                (op == SFORMAT)        ? s_sext :
                (op == SBFORMAT)       ? sb_sext :
               ((op == UFORMAT_LUI) || (op == UFORMAT_AUIPC)) ? u_sext :
                (op == UJFORMAT)       ? uj_sext : 32'b0;
  assign alucon = (op == RFORMAT) ? {opcode[30], opcode[14:12]} :
                  (op == IFORMAT_ALU) ? ((opcode[14:12] == 3'b101) ? {opcode[30], opcode[14:12]} : // SRLI or SRAI
                                        {1'b0, opcode[14:12]}) : 4'b0;
  assign funct3 = opcode[14:12];
  assign op1sel = ((op == SBFORMAT) || (op == UFORMAT_AUIPC) || (op == UJFORMAT)) ? 1'b1 : 1'b0;
  assign op2sel = (op == RFORMAT) ? 1'b0 : 1'b1;
  assign mem_rw = (op == SFORMAT) ? 1'b1 : 1'b0;
  assign wb_sel = (op == IFORMAT_LOAD) ? 2'b01 :
                  ((op == UJFORMAT) || (op == IFORMAT_JALR)) ? 2'b10 : 2'b00;
  assign rf_wen = (((op == RFORMAT) && ({opcode[31],opcode[29:25]} == 6'b000000)) ||
                   ((op == IFORMAT_ALU) && (({opcode[31:25], opcode[14:12]} == 10'b00000_00_001) || ({opcode[31], opcode[29:25], opcode[14:12]} == 9'b0_000_00_101) ||  // SLLI or SRLI or SRAI
                                            (opcode[14:12] == 3'b000) || (opcode[14:12] == 3'b010) || (opcode[14:12] == 3'b011) || (opcode[14:12] == 3'b100) || (opcode[14:12] == 3'b110) || (opcode[14:12] == 3'b111))) ||
                   (op == IFORMAT_LOAD) || (op == UFORMAT_LUI) || (op == UFORMAT_AUIPC) || (op == UJFORMAT) || (op == IFORMAT_JALR)) ? 1'b1 : 1'b0;
  assign pc_sel = (op == SBFORMAT) ? 2'b01 :
                  ((op == UJFORMAT) || (op == IFORMAT_JALR) || (op == ECALLEBREAK)) ? 2'b10 : 2'b00;

Execute

simpleTD4は25~34行目です。演算は加算のみなのでalu_selに従いALUへの入力を選択し加算しalu_outとnextcflagを出力します。分岐はNext PCで処理します。

  wire [3:0] alu_in;
  assign alu_in = (alu_sel == 2'b00) ? reg_a : // from A
                  (alu_sel == 2'b01) ? reg_b : // from B
                  (alu_sel == 2'b10) ? in_port : // from input port
                                       4'b0000; // zero
  
  wire [3:0] alu_out;
  wire nextcflag;
  assign {nextcflag, alu_out} = alu_in + im;
  assign alu_data = alu_out;

ASFRV32Iは70~146行目です。
70~72行目でレジスタからの読み込みをr_data1とr_data2に出力し、74~76行目でop1selに従いr_data1かpcかを決めてs_data1に出力しop2selに従いr_data2かimmかを決めてs_data2を出力し、78~110行目でaluconに従いs_data1とs_data2に対して加算や論理演算、シフト演算などの算術論理演算を実行しalu_dataに出力します。
113~146行目は分岐処理で、分岐命令が入ったfunct3と通常命令か分岐命令か無条件ジャンプかを入れたpc_selとレジスタから読み出したr_data1とr_data2から、次のPCをどうするかのpc_sel2を出力しています。

  wire [31:0] r_data1, r_data2;
  assign r_data1 = (r_addr1 == 5'b00000) ? 32'b0 : regs[r_addr1]; 
  assign r_data2 = (r_addr2 == 5'b00000) ? 32'b0 : regs[r_addr2]; 

  wire [31:0] s_data1, s_data2;
  assign s_data1 = (op1sel == 1'b1) ? pc : r_data1;
  assign s_data2 = (op2sel == 1'b1) ? imm : r_data2;

  wire [31:0] alu_data;

  function [31:0] ALU_EXEC( input [3:0] control, input [31:0] data1, input [31:0] data2);
    case(control)
    4'b0000: // ADD ADDI (ADD)
      ALU_EXEC = data1 + data2;
    4'b1000: // SUB (SUB)
      ALU_EXEC = data1 - data2;
    4'b0001: begin // SLL SLLI (SHIFT LEFT (LOGICAL))
      ALU_EXEC = data1 << data2[4:0];
    end
    4'b0010: begin // SLT SLTI (SET_ON_LESS_THAN (SIGNED))
      ALU_EXEC = ($signed(data1) < $signed(data2)) ? 32'b1 :32'b0;
    end
    4'b0011: // SLTU SLTUI (SET_ON_LESS_THAN (UNSIGNED))
      ALU_EXEC = (data1 < data2) ? 32'b1 :32'b0;
    4'b0100: // XOR XORI (XOR)
      ALU_EXEC = data1 ^ data2;
    4'b0101: // SRL SRLI (SHIFT RIGHT (LOGICAL))
      ALU_EXEC = data1 >> data2[4:0];
    4'b1101: begin // SRA SRAI (SHIFT RIGHT (ARITHMETIC))
      ALU_EXEC = $signed(data1[31:0]) >>> data2[4:0];
    end
    4'b0110: // OR ORI (OR)
      ALU_EXEC = data1 | data2;
    4'b0111: // AND ANDI (AND)
      ALU_EXEC = data1 & data2;
    default: // ILLEGAL
      ALU_EXEC = 32'b0;
    endcase
  endfunction

  assign alu_data = ALU_EXEC(alucon, s_data1, s_data2);
  assign alu_out = alu_data; // for DEBUG

  wire pc_sel2;

  function BRANCH_EXEC( input [2:0] branch_op, input [31:0] data1, input [31:0] data2, input [1:0] pc_sel);
    case(pc_sel)
    2'b00: // PC + 4
      BRANCH_EXEC = 1'b0;
    2'b01: begin // BRANCH
      case(branch_op)
      3'b000: // BEQ
        BRANCH_EXEC = (data1 == data2) ? 1'b1 : 1'b0;
      3'b001: // BNE
        BRANCH_EXEC = (data1 != data2) ? 1'b1 : 1'b0;
      3'b100: begin // BLT
        BRANCH_EXEC = ($signed(data1) < $signed(data2)) ? 1'b1 : 1'b0;
      end
      3'b101: begin // BGE
        BRANCH_EXEC = ($signed(data1) >= $signed(data2)) ? 1'b1 : 1'b0;
      end
      3'b110: // BLTU
        BRANCH_EXEC = (data1 < data2) ? 1'b1 : 1'b0;
      3'b111: // BGEU
        BRANCH_EXEC = (data1 >= data2) ? 1'b1 : 1'b0;
      default: // ILLEGAL
        BRANCH_EXEC = 1'b0;
      endcase
    end 
    2'b10: // JAL JALR
      BRANCH_EXEC = 1'b1;
    default: // ILLEGAL
      BRANCH_EXEC = 1'b0;
    endcase
  endfunction

  assign pc_sel2 = BRANCH_EXEC(funct3, r_data1, r_data2, pc_sel);

Memory

simpleTD4は命令メモリからの読み込みのみでデータメモリを持たないので実装されていません。

ASFRV32Iは148~159行目までがメモリからの読み込みです。メモリへの読み書き内容を示す
funct3をmem_valに入れ、メモリアドレスはalu_data、メモリからのデータをmem_dataに出力します。
173~182行目でクロックごとにメモリへの書き込みを行います。mem_rwが1'b1のときmem_valに従いalu_dataで指定されたアドレスにr_data2を書き込みます。

  wire [2:0] mem_val;
  wire [31:0] mem_data;
  wire [31:0] mem_addr;
  assign mem_val = funct3;
  assign mem_addr = alu_data;
  assign mem_data = (mem_rw == 1'b1) ? 32'b0 : // when MEMORY WRITE, the output from MEMORY is 32'b0
                     (mem_val == 3'b000) ?  (mem[mem_addr][7] == 1'b1 ? {24'hffffff, mem[mem_addr]} : {24'h000000, mem[mem_addr]}) : // LB
                     (mem_val == 3'b001) ?  (mem[mem_addr + 1][7] == 1'b1 ? {16'hffff, mem[mem_addr + 1], mem[mem_addr]} : {16'h0000, mem[mem_addr + 1], mem[mem_addr]}) : // LH
                     (mem_val == 3'b010) ? {mem[mem_addr + 3], mem[mem_addr + 2], mem[mem_addr + 1], mem[mem_addr]} : // LW
                     (mem_val == 3'b100) ? {24'h000000, mem[mem_addr]} : // LBU
                     (mem_val == 3'b101) ? {16'h0000, mem[mem_addr + 1], mem[mem_addr]} : // LHU
                                           32'b0;
      if (mem_rw == 1'b1) begin
        case (mem_val)
        3'b000: // SB
          mem[mem_addr] <= #1 r_data2[7:0];
        3'b001: // SH
          {mem[mem_addr + 1], mem[mem_addr]} <= #1 r_data2[15:0];
        3'b010: // SW
          {mem[mem_addr + 3], mem[mem_addr + 2], mem[mem_addr + 1], mem[mem_addr]} <= #1 r_data2;
        default: begin end // ILLEGAL
        endcase
      end

Write Back

simpleTD4は36~39行目でload_selからデータバスの出力先を決定しreg_a、reg_b、reg_out、 cflagを設定し、53~56行目でクロックごとに書き込みます。

  wire load_a, load_b, load_out, load_pc;
  assign load_a = (load_sel == 2'b00) ? 1'b0 : 1'b1; // negative logic
  assign load_b = (load_sel == 2'b01) ? 1'b0 : 1'b1; // negative logic
  assign load_out = (load_sel == 2'b10) ? 1'b0 : 1'b1; // negative logic
  assign load_pc = (load_sel == 2'b11 && (jmp == 1'b1 || cflag)) ? 1'b0 : 1'b1; // negative logic
      reg_a <= #1 (load_a == 1'b0) ? alu_out : reg_a;
      reg_b <= #1 (load_b == 1'b0) ? alu_out : reg_b;
      reg_out <= #1 (load_out == 1'b0) ? alu_out : reg_out;
      cflag  <= #1 ~nextcflag; // negative logic carry 

ASFRV32Iは161~164行目でwb_selに従いalu_dataかmem_dataかpc+4かどのデータを書き込むか決定し、184~185行目でrf_wenが1'b1で書き込み先がゼロレジスタでないならクロックごとに書き込みます。

  wire [31:0] w_data;
  assign w_data = (wb_sel == 2'b00) ? alu_data : 
                  (wb_sel == 2'b01) ? mem_data :
                  (wb_sel == 2'b10) ? pc + 4 : 32'b0; // ILLEGAL
      if ((rf_wen == 1'b1) && (w_addr != 5'b00000))
        regs[w_addr] <= #1 w_data;

Next PC

simpleTD4は40~43行目で分岐かどうか判定し分岐ならalu_out、そうでないならpc+1を次のPCにして、57行目でクロックごとにpcの値を設定しています。

  assign load_pc = (load_sel == 2'b11 && (jmp == 1'b1 || cflag)) ? 1'b0 : 1'b1; // negative logic

  wire [3:0] next_pc;
  assign next_pc = (load_pc == 1'b0) ? alu_out : pc + 1;
      pc <= #1 next_pc;

ASFRV32Iは166~167行目でpc_sel2に従い分岐ならalu_data(最下位1bitは0)、そうでないならpc+4に次のPCにして、186行目でクロックごとにpcの値を設定しています。

  wire [31:0] next_pc;
  assign next_pc = (pc_sel2 == 1'b1) ? {alu_data[31:1], 1'b0} : pc + 4;
      pc <= #1 next_pc; 

考察

simpleTD4からASFRV32Iへ大きく変化しているのはDecodeとExecuteです。実行する命令の数や複雑さが異なるのでDecodeとExecuteの記述の分量や内容が大幅に複雑化しています。またASFRV32Iメモリへの読み書きが追加されている分記述が増えています。それ以外はレジスタ数の違い程度です。

TD4とRV32Iとの間に巨大なギャップがあるようには感じません。割り込み処理やメモリ保護など複雑な要素がなければ、4bit CPU TD4を十分理解できていれば、RV32I以外の他の命令セットアーキテクチャもシングルサイクルの実装ならばあまり理解に苦しむことはないのではないかと思います。

まずはTD4をしっかりと学習することがCPUの学習の第一歩となると思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?