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のブロックダイアグラム
ASFRV32Iのブロックダイアグラム
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の学習の第一歩となると思います。