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

# simpleTD4とASFRV32Iとの比較

[simpleTD4](https://github.com/asfdrwe/simpleTD4)は4bit CPU TD4のシングルサイクルのVerilogによる60行程度の1ファイルの実装です。

[ASFRV32I](https://github.com/asfdrwe/ASFRV32I/)はRISC-V RV32IのシングルサイクルのVerilogによる190行程度の1ファイルの実装です。

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

[simpleTD4の解説](https://qiita.com/asfdrwe/items/8dd955240b8291346384) と [ASFRV32Iの設計と実装の解説](https://qiita.com/asfdrwe/items/595c871611e6603741fa) も参考にしながら読んでみてください。

## simpleTD4のブロックダイアグラム
![TD4_block_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882900/01dae66b-6053-8d8f-aa9b-8a501f2f7e6f.png)

## ASFRV32Iのブロックダイアグラム
![ASFRV32I_block_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882900/a041e13f-ad3d-8392-dddd-ecdd360c46fa.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行目で１命令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の学習の第一歩となると思います。