第9章
M拡張の実装
9.1 概要
「第I部 RV32I / RV64Iの実装」ではRV64IのCPUを実装しました。「第II部 RV64IMACの実装」では、次のような機能を実装します。
- 乗算、除算、剰余演算命令 (M拡張)
- 不可分操作命令 (A拡張)
- 圧縮命令 (C拡張)
- 例外
- Memory-mapped I/O
本章では積、商、剰余を求める命令を実装します。RISC-Vの乗算、除算、剰余演算を行う命令はM拡張に定義されており、M拡張を実装したRV64IのISAのことをRV64IM
と表記します。
M拡張には、XLENが32
のときは表9.1の命令が定義されています。XLENが64
のときは表9.2の命令が定義されています。
表9.1: M拡張の命令 (XLEN=32)
命令 | 動作 |
---|---|
MUL | rs1(符号付き) × rs2(符号付き)の結果(64ビット)の下位32ビットを求める |
MULH | rs1(符号付き) × rs2(符号付き)の結果(64ビット)の上位32ビットを求める |
MULHU | rs1(符号無し) × rs2(符号無し)の結果(64ビット)の上位32ビットを求める |
MULHSU | rs1(符号付き) × rs2(符号無し)の結果(64ビット)の上位32ビットを求める |
DIV | rs1(符号付き) / rs2(符号付き)を求める |
DIVU | rs1(符号無し) / rs2(符号無し)を求める |
REM | rs1(符号付き) % rs2(符号付き)を求める |
REMU | rs1(符号無し) % rs2(符号無し)を求める |
表9.2: M拡張の命令 (XLEN=64)
命令 | 動作 |
---|---|
MUL | rs1(符号付き) × rs2(符号付き)の結果(128ビット)の下位64ビットを求める |
MULW | rs1[31:0](符号付き) × rs2[31:0](符号付き)の結果(64ビット)の下位32ビットを求める 結果は符号拡張する |
MULH | rs1(符号付き) × rs2(符号付き)の結果(128ビット)の上位64ビットを求める |
MULHU | rs1(符号無し) × rs2(符号無し)の結果(128ビット)の上位64ビットを求める |
MULHSU | rs1(符号付き) × rs2(符号無し)の結果(128ビット)の上位64ビットを求める |
DIV | rs1(符号付き) / rs2(符号付き)を求める |
DIVW | rs1[31:0](符号付き) / rs2[31:0](符号付き)を求める 結果は符号拡張する |
DIVU | rs1(符号無し) / rs2(符号無し)を求める |
DIVWU | rs1[31:0](符号無し) / rs2[31:0](符号無し)を求める 結果は符号拡張する |
REM | rs1(符号付き) % rs2(符号付き)を求める |
REMW | rs1[31:0](符号付き) % rs2[31:0](符号付き)を求める 結果は符号拡張する |
REMU | rs1(符号無し) % rs2(符号無し)を求める |
REMUW | rs1[31:0](符号無し) % rs2[31:0](符号無し)を求める 結果は符号拡張する |
Verylには積、商、剰余を求める演算子*
、/
、%
が定義されており、これを利用することで簡単に計算を実装できます(リスト9.1)。
1: assign mul = op1 * op2; 2: assign div = op1 / op2; 3: assign rem = op1 % op2;
例えば乗算回路をFPGA上に実装する場合、通常は合成系によってFPGAに搭載されている乗算器が自動的に利用されます*1。これにより、低遅延、低リソースコストで効率的な乗算回路を自動的に実現できます。しかし、32ビットや64ビットの乗算を実装する際、FPGA上の乗算器の数が不足すると、LUTを用いた大規模な乗算回路が構築されることがあります。このような大規模な回路はFPGAのリソースの使用量や遅延に大きな影響を与えるため好ましくありません。除算や剰余演算でも同じ問題*2が生じることがあります。
[*1] 手動で何をどのように利用するかを選択することもできます。既に用意された回路(IP)を使うこともできますが、本書は自作することを主軸としているため利用しません。
[*2] そもそも除算器が搭載されていない場合があります。
*
、/
、%
演算子がどのような回路に合成されるかは、合成系が全体の実装を考慮して自動的に決定するため、その挙動をコントロールするのは難しいです。そこで本章では、*
、/
、%
演算子を使用せず、足し算やシフト演算などの基本的な論理だけを用いて同等の演算を実装します。
基本編では積、商、剰余を効率よく*3求める実装は検討せず、できるだけ単純な方法で実装します。
[*3] 「効率」は、計算に要する時間やスループット、回路面積のことです。効率的に計算する方法については応用編で検討します。
9.2 命令のデコード
まず、M拡張の命令をデコードします。M拡張の命令はすべてR形式であり、レジスタの値同士の演算を行います。funct7は7'b0000001
です。MUL、MULH、MULHSU、MULHU、DIV、DIVU、REM、REMU命令のopcodeは7'b0110011
(OP)で、MULW、DIVW、DIVUW、REMW、REMUW命令のopcodeは7'b0111011
(OP-32)です。
それぞれの命令はfunct3で区別します(表9.3)。乗算命令のfunct3はMSBが0
、除算と剰余演算命令は1
になっています。
表9.3: M拡張の命令の区別
命令 | funct3 |
---|---|
MUL、MULW | 000 |
MULH | 001 |
MULHU | 010 |
MULHSU | 011 |
DIV、DIVW | 100 |
DIVU、DIVWU | 101 |
REM、REMW | 110 |
REMU、REMUW | 111 |
InstCtrl
構造体に、M拡張の命令であることを示すis_muldiv
フラグを追加します (リスト9.2)。
1: // 制御に使うフラグ用の構造体 2: struct InstCtrl { 3: itype : InstType , // 命令の形式 4: rwb_en : logic , // レジスタに書き込むかどうか 5: is_lui : logic , // LUI命令である 6: is_aluop : logic , // ALUを利用する命令である 7: is_muldiv: logic , // M拡張の命令である 8: is_op32 : logic , // OP-32またはOP-IMM-32である 9: is_jump : logic , // ジャンプ命令である 10: is_load : logic , // ロード命令である 11: is_csr : logic , // CSR命令である 12: funct3 : logic <3>, // 命令のfunct3フィールド 13: funct7 : logic <7>, // 命令のfunct7フィールド 14: }
inst_decoderモジュールのInstCtrl
を生成している部分を変更します。opcodeがOP
かOP-32
の場合はfunct7の値によってis_muldiv
を設定します(リスト9.3)。その他のopcodeのis_muldiv
はF
に設定してください。
1: OP_OP: { 2: InstType::R, T, F, T, f7 == 7'b0000001, F, F, F, F 3: }, 4: OP_OP_IMM: { 5: InstType::I, T, F, T, F, F, F, F, F 6: }, 7: OP_OP_32: { 8: InstType::R, T, F, T, f7 == 7'b0000001, T, F, F, F 9: },
9.3 muldivunitモジュールの実装
9.3.1 muldivunitモジュールを作成する
M拡張の計算を処理するモジュールを作成し、M拡張の命令がALUの結果ではなくモジュールの結果を利用するように変更します。
src/muldivunit.veryl
を作成し、次のように記述します(リスト9.4)。
1: import eei::*; 2: 3: module muldivunit ( 4: clk : input clock , 5: rst : input reset , 6: ready : output logic , 7: valid : input logic , 8: funct3: input logic<3>, 9: op1 : input UIntX , 10: op2 : input UIntX , 11: rvalid: output logic , 12: result: output UIntX , 13: ) { 14: 15: enum State { 16: Idle, 17: WaitValid, 18: Finish, 19: } 20: 21: var state: State; 22: 23: // saved_data 24: var funct3_saved: logic<3>; 25: 26: always_comb { 27: ready = state == State::Idle; 28: rvalid = state == State::Finish; 29: } 30: 31: always_ff { 32: if_reset { 33: state = State::Idle; 34: result = 0; 35: funct3_saved = 0; 36: } else { 37: case state { 38: State::Idle: if ready && valid { 39: state = State::WaitValid; 40: funct3_saved = funct3; 41: } 42: State::WaitValid: state = State::Finish; 43: State::Finish : state = State::Idle; 44: default : {} 45: } 46: } 47: } 48: }
muldivunitモジュールはready
が1
のときに計算のリクエストを受け付けます。valid
が1
なら計算を開始し、計算が終了したらrvalid
を1
、計算結果をresult
に設定します。
まだ計算処理を実装しておらず、result
は常に0
を返します。次の計算を開始するまでresult
の値を維持します。
9.3.2 EXステージを変更する
M拡張の命令がEXステージにあるとき、ALUの結果の代わりにmuldivunitモジュールの結果を利用するように変更します。
まず、muldivunitモジュールをインスタンス化します(リスト9.5)。
1: let exs_muldiv_valid : logic = exs_valid && exs_ctrl.is_muldiv && !exs_data_hazard && !exs_muldiv_is_requested; 2: var exs_muldiv_ready : logic; 3: var exs_muldiv_rvalid: logic; 4: var exs_muldiv_result: UIntX; 5: 6: inst mdu: muldivunit ( 7: clk , 8: rst , 9: valid : exs_muldiv_valid , 10: ready : exs_muldiv_ready , 11: funct3: exs_ctrl.funct3 , 12: op1 : exs_op1 , 13: op2 : exs_op2 , 14: rvalid: exs_muldiv_rvalid, 15: result: exs_muldiv_result, 16: );
muldivunitモジュールで計算を開始するのは、EXステージに命令が存在し(exs_valid
)、命令がM拡張の命令であり(exs_ctrl.is_muldiv
)、データハザードが発生しておらず(!exs_data_hazard
)、既に計算を要求していない(!exs_muldiv_is_requested
)場合です。
exs_muldiv_is_requested
変数を定義し、ステージの遷移条件とmuldivunitに計算を要求したかの状態によって値を更新します(リスト9.6)。
1: var exs_muldiv_is_requested: logic; 2: 3: always_ff { 4: if_reset { 5: exs_muldiv_is_requested = 0; 6: } else { 7: // 次のステージに遷移 8: if exq_rvalid && exq_rready { 9: exs_muldiv_is_requested = 0; 10: } else { 11: // muldivunitにリクエストしたか判定する 12: if exs_muldiv_valid && exs_muldiv_ready { 13: exs_muldiv_is_requested = 1; 14: } 15: } 16: } 17: }
muldivunitモジュールはALUのように1クロックの間に入力から出力を生成しないため、計算中はEXステージをストールさせる必要があります。そのためにexs_muldiv_stall
変数を定義して、ストールの条件に追加します(リスト9.7、リスト9.8)。また、M拡張の命令の場合はMEMステージに渡すalu_result
の値をmuldivunitモジュールの結果に設定します(リスト9.8)。
1: var exs_muldiv_rvalided: logic; 2: let exs_muldiv_stall : logic = exs_ctrl.is_muldiv && !exs_muldiv_rvalid && !exs_muldiv_rvalided; 3: 4: always_ff { 5: if_reset { 6: exs_muldiv_rvalided = 0; 7: } else { 8: // 次のステージに遷移 9: if exq_rvalid && exq_rready { 10: exs_muldiv_rvalided = 0; 11: } else { 12: // muldivunitの処理が完了していたら1にする 13: exs_muldiv_rvalided |= exs_muldiv_rvalid; 14: } 15: } 16: }
1: let exs_stall: logic = exs_data_hazard || exs_muldiv_stall; 2: 3: always_comb { 4: // EX -> MEM 5: exq_rready = memq_wready && !exs_stall; 6: memq_wvalid = exq_rvalid && !exs_stall; 7: memq_wdata.addr = exq_rdata.addr; 8: memq_wdata.bits = exq_rdata.bits; 9: memq_wdata.ctrl = exq_rdata.ctrl; 10: memq_wdata.imm = exq_rdata.imm; 11: memq_wdata.rs1_addr = exs_rs1_addr; 12: memq_wdata.rs1_data = exs_rs1_data; 13: memq_wdata.rs2_data = exs_rs2_data; 14: memq_wdata.alu_result = if exs_ctrl.is_muldiv ? exs_muldiv_result : exs_alu_result; 15: memq_wdata.br_taken = exs_ctrl.is_jump || inst_is_br(exs_ctrl) && exs_brunit_take; 16: memq_wdata.jump_addr = if inst_is_br(exs_ctrl) ? exs_pc + exs_imm : exs_alu_result & ~1; 17: }
muldivunitモジュールは計算が完了したクロックでしかrvalid
を1
にしないため、既に計算が完了したことを示すexs_muldiv_rvalided
変数で完了状態を管理します。これにより、M拡張の命令によってストールする条件は、命令がM拡張の命令であり(exs_ctrl.is_muldiv
)、現在のクロックで計算が完了しておらず(!exs_muldiv_rvalid
)、以前のクロックでも計算が完了していない(!exs_muldiv_rvalided
)場合になります。
9.4 符号無しの乗算器の実装
9.4.1 mulunitモジュールを実装する
WIDTH
ビットの符号無しの値同士の積を計算する乗算器を実装します。
src/muldivunit.veryl
の中にmulunitモジュールを作成します(リスト9.9)。
1: module mulunit #( 2: param WIDTH: u32 = 0, 3: ) ( 4: clk : input clock , 5: rst : input reset , 6: valid : input logic , 7: op1 : input logic<WIDTH> , 8: op2 : input logic<WIDTH> , 9: rvalid: output logic , 10: result: output logic<WIDTH * 2>, 11: ) { 12: enum State { 13: Idle, 14: AddLoop, 15: Finish, 16: } 17: 18: var state: State; 19: 20: var op1zext: logic<WIDTH * 2>; 21: var op2zext: logic<WIDTH * 2>; 22: 23: always_comb { 24: rvalid = state == State::Finish; 25: } 26: 27: var add_count: logic<32>; 28: 29: always_ff { 30: if_reset { 31: state = State::Idle; 32: result = 0; 33: add_count = 0; 34: op1zext = 0; 35: op2zext = 0; 36: } else { 37: case state { 38: State::Idle: if valid { 39: state = State::AddLoop; 40: result = 0; 41: add_count = 0; 42: op1zext = {1'b0 repeat WIDTH, op1}; 43: op2zext = {1'b0 repeat WIDTH, op2}; 44: } 45: State::AddLoop: if add_count == WIDTH { 46: state = State::Finish; 47: } else { 48: if op2zext[add_count] { 49: result += op1zext; 50: } 51: op1zext <<= 1; 52: add_count += 1; 53: } 54: State::Finish: state = State::Idle; 55: default : {} 56: } 57: } 58: } 59: }
mulunitモジュールはop1 * op2
を計算するモジュールです。valid
が1
になったら計算を開始し、計算が完了したらrvalid
を1
、result
をWIDTH * 2
ビットの計算結果に設定します。
積はWIDTH
回の足し算をWIDTH
クロックかけて行って求めています(図9.1)。計算を開始すると入力をゼロでWIDTH * 2
ビットに拡張し、result
を0
でリセットします。
State::AddLoop
では、次の操作をWIDTH
回行います。i
回目では次の操作を行います。
op2[i-1]
が1
ならresult
にop1
を足すop1
を1ビット左シフトする- カウンタをインクリメントする

図9.1: 符号無し4ビットの乗算
9.4.2 mulunitモジュールをインスタンス化する
mulunitモジュールをmuldivunitモジュールでインスタンス化します(リスト9.10)。まだ結果は利用しません。
1: // multiply unit 2: const MUL_OP_WIDTH : u32 = XLEN; 3: const MUL_RES_WIDTH: u32 = MUL_OP_WIDTH * 2; 4: 5: let is_mul : logic = if state == State::Idle ? !funct3[2] : !funct3_saved[2]; 6: var mu_rvalid: logic ; 7: var mu_result: logic<MUL_RES_WIDTH>; 8: 9: inst mu: mulunit #( 10: WIDTH: MUL_OP_WIDTH, 11: ) ( 12: clk , 13: rst , 14: valid : ready && valid && is_mul, 15: op1 : op1 , 16: op2 : op2 , 17: rvalid: mu_rvalid , 18: result: mu_result , 19: );
9.5 MULHU命令の実装
MULHU命令は、2つの符号無しのXLENビットの値の乗算を実行し、デスティネーションレジスタに結果(XLEN * 2ビット)の上位XLENビットを書き込む命令です。funct3の下位2ビットによってmulunitモジュールの結果を選択するようにします(リスト9.11)。
1: State::WaitValid: if is_mul && mu_rvalid { 2: state = State::Finish; 3: result = case funct3_saved[1:0] { 4: 2'b11 : mu_result[XLEN+:XLEN], // MULHU 5: default: 0, 6: }; 7: }
riscv-testsのrv64um-p-mulhu
を実行し、成功することを確認してください。
9.6 MUL、MULH命令の実装
9.6.1 符号付き乗算を符号無し乗算器で実現する
MUL、MULH命令は、2つの符号付きのXLENビットの値の乗算を実行し、デスティネーションレジスタにそれぞれ結果の下位XLENビット、上位XLENビットを書き込む命令です。
本章ではmulunitモジュールを使って、次のように符号付き乗算を実現します。
- 符号付きのXLENビットの値を符号無しの値(絶対値)に変換する
- 符号無しで積を計算する
- 計算結果の符号を修正する
絶対値で計算することで符号ビットを考慮する必要がなくなり、既に実装してある符号無しの乗算器を変更せずに符号付きの乗算を実現できます。
9.6.2 符号付き乗算を実装する
WIDTH
ビットの符号付きの値をWIDTH
ビットの符号無しの絶対値に変換するabs関数を作成します(リスト9.12)。abs関数は、値のMSBが1
ならビットを反転して1
を足すことで符号を反転しています。最小値-2 ** (WIDTH - 1)
の絶対値も求められることを確認してください。
1: function abs::<WIDTH: u32> ( 2: value: input logic<WIDTH>, 3: ) -> logic<WIDTH> { 4: return if value[msb] ? ~value + 1 : value; 5: }
abs関数を利用して、MUL、MULH命令のときにmulunitに渡す値を絶対値に設定します(リスト9.13、リスト9.14)。
1: let mu_op1: logic<MUL_OP_WIDTH> = case funct3[1:0] { 2: 2'b00, 2'b01: abs::<XLEN>(op1), // MUL, MULH 3: 2'b11 : op1, // MULHU 4: default : 0, 5: }; 6: let mu_op2: logic<MUL_OP_WIDTH> = case funct3[1:0] { 7: 2'b00, 2'b01: abs::<XLEN>(op2), // MUL, MULH 8: 2'b11 : op2, // MULHU 9: default : 0, 10: };
1: inst mu: mulunit #( 2: WIDTH: MUL_OP_WIDTH, 3: ) ( 4: clk , 5: rst , 6: valid : ready && valid && is_mul, 7: op1 : mu_op1 , 8: op2 : mu_op2 , 9: rvalid: mu_rvalid , 10: result: mu_result , 11: );
計算結果の符号はop1
とop2
の符号が異なる場合に負になります。後で符号の情報を利用するために、muldivunitモジュールが要求を受け入れる時に符号を保存します(リスト9.15、リスト9.16、リスト9.17)。
1: // saved_data 2: var funct3_saved : logic<3>; 3: var op1sign_saved: logic ; 4: var op2sign_saved: logic ;
1: always_ff { 2: if_reset { 3: state = State::Idle; 4: result = 0; 5: funct3_saved = 0; 6: op1sign_saved = 0; 7: op2sign_saved = 0; 8: } else {
1: case state { 2: State::Idle: if ready && valid { 3: state = State::WaitValid; 4: funct3_saved = funct3; 5: op1sign_saved = op1[msb]; 6: op2sign_saved = op2[msb]; 7: }
保存した符号を利用して計算結果の符号を復元します(リスト9.18)。
1: State::WaitValid: if is_mul && mu_rvalid { 2: let res_signed: logic<MUL_RES_WIDTH> = if op1sign_saved != op2sign_saved ? ~mu_result + 1 : mu_result; 3: state = State::Finish; 4: result = case funct3_saved[1:0] { 5: 2'b00 : res_signed[XLEN - 1:0], // MUL 6: 2'b01 : res_signed[XLEN+:XLEN], // MULH 7: 2'b11 : mu_result[XLEN+:XLEN], // MULHU 8: default: 0, 9: }; 10: }
riscv-testsのrv64um-p-mul
とrv64um-p-mulh
を実行し、成功することを確認してください。
9.6.3 MULHSU命令の実装
MULHSU命令は、符号付きのXLENビットのrs1と符号無しのXLENビットのrs2の乗算を実行し、デスティネーションレジスタに結果の上位XLENビットを書き込む命令です。計算結果は符号付きの値になります。
MULHSU命令も、MUL、MULH命令と同様に符号無しの乗算器で実現します。
op1
を絶対値に変換し、op2
はそのままに設定します(リスト9.19)。
1: let mu_op1: logic<MUL_OP_WIDTH> = case funct3[1:0] { 2: 2'b00, 2'b01, 2'b10: abs::<XLEN>(op1), // MUL, MULH, MULHSU 3: 2'b11 : op1, // MULHU 4: default : 0, 5: }; 6: let mu_op2: logic<MUL_OP_WIDTH> = case funct3[1:0] { 7: 2'b00, 2'b01: abs::<XLEN>(op2), // MUL, MULH 8: 2'b11, 2'b10: op2, // MULHU, MULHSU 9: default : 0, 10: };
計算結果はop1
の符号にします(リスト9.20)。
1: State::WaitValid: if is_mul && mu_rvalid { 2: let res_signed: logic<MUL_RES_WIDTH> = if op1sign_saved != op2sign_saved ? ~mu_result + 1 : mu_result; 3: let res_mulhsu: logic<MUL_RES_WIDTH> = if op1sign_saved == 1 ? ~mu_result + 1 : mu_result; 4: state = State::Finish; 5: result = case funct3_saved[1:0] { 6: 2'b00 : res_signed[XLEN - 1:0], // MUL 7: 2'b01 : res_signed[XLEN+:XLEN], // MULH 8: 2'b10 : res_mulhsu[XLEN+:XLEN], // MULHSU 9: 2'b11 : mu_result[XLEN+:XLEN], // MULHU 10: default: 0, 11: }; 12: }
riscv-testsのrv64um-p-mulhsu
を実行し、成功することを確認してください。
9.6.4 MULW命令の実装
MULW命令は、2つの符号付きの32ビットの値の乗算を実行し、デスティネーションレジスタに結果の下位32ビットを符号拡張した値を書き込む命令です。
32ビット演算の命令であることを判定するために、muldivunitモジュールにis_op32
ポートを作成します(リスト9.21、リスト9.22)。
1: module muldivunit ( 2: clk : input clock , 3: rst : input reset , 4: ready : output logic , 5: valid : input logic , 6: funct3 : input logic<3>, 7: is_op32: input logic , 8: op1 : input UIntX , 9: op2 : input UIntX , 10: rvalid : output logic , 11: result : output UIntX , 12: ) {
1: inst mdu: muldivunit ( 2: clk , 3: rst , 4: valid : exs_muldiv_valid , 5: ready : exs_muldiv_ready , 6: funct3 : exs_ctrl.funct3 , 7: is_op32: exs_ctrl.is_op32 , 8: op1 : exs_op1 , 9: op2 : exs_op2 , 10: rvalid : exs_muldiv_rvalid, 11: result : exs_muldiv_result, 12: );
muldivunitモジュールが要求を受け入れる時にis_op32
を保存します(リスト9.23、リスト9.24、リスト9.25)。
1: // saved_data 2: var funct3_saved : logic<3>; 3: var is_op32_saved: logic ; 4: var op1sign_saved: logic ; 5: var op2sign_saved: logic ;
1: always_ff { 2: if_reset { 3: state = State::Idle; 4: result = 0; 5: funct3_saved = 0; 6: is_op32_saved = 0; 7: op1sign_saved = 0; 8: op2sign_saved = 0; 9: } else {
1: State::Idle: if ready && valid { 2: state = State::WaitValid; 3: funct3_saved = funct3; 4: is_op32_saved = is_op32; 5: op1sign_saved = op1[msb]; 6: op2sign_saved = op2[msb]; 7: }
mulunitモジュールのop1
とop2
に、64ビットの値の下位32ビットを符号拡張した値を割り当てます。符号拡張を行うsext関数を作成し、mu_op1
、mu_op2
の割り当てに利用します(リスト9.26、リスト9.27)。
1: function sext::<WIDTH_IN: u32, WIDTH_OUT: u32> ( 2: value: input logic<WIDTH_IN>, 3: ) -> logic<WIDTH_OUT> { 4: return {value[msb] repeat WIDTH_OUT - WIDTH_IN, value}; 5: }
1: let mu_op1: logic<MUL_OP_WIDTH> = case funct3[1:0] { 2: 2'b00, 2'b01, 2'b10: abs::<XLEN>(if is_op32 ? sext::<32, XLEN>(op1[31:0]) : op1), // MUL, MULH, MULHSU, MULW 3: 2'b11 : op1, // MULHU 4: default : 0, 5: }; 6: let mu_op2: logic<MUL_OP_WIDTH> = case funct3[1:0] { 7: 2'b00, 2'b01: abs::<XLEN>(if is_op32 ? sext::<32, XLEN>(op2[31:0]) : op2), // MUL, MULH, MULW 8: 2'b11, 2'b10: op2, // MULHU, MULHSU 9: default : 0, 10: };
最後に、計算結果を符号拡張した値に設定します(リスト9.28)。
1: State::WaitValid: if is_mul && mu_rvalid { 2: let res_signed: logic<MUL_RES_WIDTH> = if op1sign_saved != op2sign_saved ? ~mu_result + 1 : mu_result; 3: let res_mulhsu: logic<MUL_RES_WIDTH> = if op1sign_saved == 1 ? ~mu_result + 1 : mu_result; 4: state = State::Finish; 5: result = case funct3_saved[1:0] { 6: 2'b00 : if is_op32_saved ? sext::<32, 64>(res_signed[31:0]) : res_signed[XLEN - 1:0], // MUL, MULW 7: 2'b01 : res_signed[XLEN+:XLEN], // MULH
riscv-testsのrv64um-p-mulw
を実行し、成功することを確認してください。
9.7 符号無し除算の実装
9.7.1 divunitモジュールを実装する
WIDTH
ビットの除算を計算する除算器を実装します。
src/muldivunit.veryl
の中にdivunitモジュールを作成します(リスト9.29)。
1: module divunit #( 2: param WIDTH: u32 = 0, 3: ) ( 4: clk : input clock , 5: rst : input reset , 6: valid : input logic , 7: dividend : input logic<WIDTH>, 8: divisor : input logic<WIDTH>, 9: rvalid : output logic , 10: quotient : output logic<WIDTH>, 11: remainder: output logic<WIDTH>, 12: ) { 13: enum State { 14: Idle, 15: ZeroCheck, 16: SubLoop, 17: Finish, 18: } 19: 20: var state: State; 21: 22: var dividend_saved: logic<WIDTH * 2>; 23: var divisor_saved : logic<WIDTH * 2>; 24: 25: always_comb { 26: rvalid = state == State::Finish; 27: remainder = dividend_saved[WIDTH - 1:0]; 28: } 29: 30: var sub_count: u32; 31: 32: always_ff { 33: if_reset { 34: state = State::Idle; 35: quotient = 0; 36: sub_count = 0; 37: dividend_saved = 0; 38: divisor_saved = 0; 39: } else { 40: case state { 41: State::Idle: if valid { 42: state = State::ZeroCheck; 43: dividend_saved = {1'b0 repeat WIDTH, dividend}; 44: divisor_saved = {1'b0, divisor, 1'b0 repeat WIDTH - 1}; 45: quotient = 0; 46: sub_count = 0; 47: } 48: State::ZeroCheck: if divisor_saved == 0 { 49: state = State::Finish; 50: quotient = '1; 51: } else { 52: state = State::SubLoop; 53: } 54: State::SubLoop: if sub_count == WIDTH { 55: state = State::Finish; 56: } else { 57: if dividend_saved >= divisor_saved { 58: dividend_saved -= divisor_saved; 59: quotient = (quotient << 1) + 1; 60: } else { 61: quotient <<= 1; 62: } 63: divisor_saved >>= 1; 64: sub_count += 1; 65: } 66: State::Finish: state = State::Idle; 67: default : {} 68: } 69: } 70: } 71: }
divunitモジュールは被除数(dividend
)と除数(divisor
)の商(quotient
)と剰余(remainder
)を計算するモジュールです。valid
が1
になったら計算を開始し、計算が完了したらrvalid
を1
に設定します。
商と剰余はWIDTH
回の引き算をWIDTH
クロックかけて行って求めています。計算を開始すると被除数を0
でWIDTH * 2
ビットに拡張し、除数をWIDTH-1
ビット左シフトします。また、商を0
でリセットします。
State::SubLoop
では、次の操作をWIDTH
回行います。
- 被除数が除数よりも大きいなら、被除数から除数を引き、商のLSBを1にする
- 商を1ビット左シフトする
- 除数を1ビット右シフトする
- カウンタをインクリメントする
RISC-Vでは、除数が0
だったり結果がオーバーフローするようなLビットの除算の結果は表9.4のようになると定められています。このうちdivunitモジュールは符号無しの除算(DIVU、REMU命令)のゼロ除算だけを対処しています。
表9.4: 除算の例外的な動作と結果
操作 | ゼロ除算 | オーバーフロー |
---|---|---|
符号付き除算 | -1 | -2**(L-1) |
符号付き剰余 | 被除数 | 0 |
符号無し除算 | 2**L-1 | 発生しない |
符号無し剰余 | 被除数 | 発生しない |
9.7.2 divunitモジュールをインスタンス化する
divunitモジュールをmuldivunitモジュールでインスタンス化します(リスト9.30)。まだ結果は利用しません。
1: // divider unit 2: const DIV_WIDTH: u32 = XLEN; 3: 4: var du_rvalid : logic ; 5: var du_quotient : logic<DIV_WIDTH>; 6: var du_remainder: logic<DIV_WIDTH>; 7: 8: inst du: divunit #( 9: WIDTH: DIV_WIDTH, 10: ) ( 11: clk , 12: rst , 13: valid : ready && valid && !is_mul, 14: dividend : op1 , 15: divisor : op2 , 16: rvalid : du_rvalid , 17: quotient : du_quotient , 18: remainder: du_remainder , 19: );
9.8 DIVU、REMU命令の実装
DIVU、REMU命令は、符号無しのXLENビットのrs1(被除数)と符号無しのXLENビットのrs2(除数)の商、剰余を計算し、デスティネーションレジスタにそれぞれ結果を書き込む命令です。
muldivunitモジュールで、divunitモジュールの処理が終わったら結果をresult
レジスタに割り当てるようにします(リスト9.31)。
1: State::WaitValid: if is_mul && mu_rvalid { 2: ... 3: } else if !is_mul && du_rvalid { 4: result = case funct3_saved[1:0] { 5: 2'b01 : du_quotient, // DIVU 6: 2'b11 : du_remainder, // REMU 7: default: 0, 8: }; 9: state = State::Finish; 10: }
riscv-testsのrv64um-p-divu
、rv64um-p-remu
を実行し、成功することを確認してください。
9.9 DIV、REM命令の実装
9.9.1 符号付き除算を符号無し除算器で実現する
DIV、REM命令は、それぞれDIVU、REMU命令の動作を符号付きに変えた命令です。本章では、符号付き乗算と同じように値を絶対値に変換して計算することで符号付き除算を実現します。
RISC-Vの符号付き除算の結果は0の方向に丸められた整数になり、剰余演算の結果は被除数と同じ符号になります。符号付き剰余の絶対値は符号無し剰余の結果と一致するため、絶対値で計算してから符号を戻すことで、符号無し除算器だけで符号付きの剰余演算を実現できます。
9.9.2 符号付き除算を実装する
abs関数を利用して、DIV、REM命令のときにdivunitモジュールに渡す値を絶対値に設定します(リスト9.32リスト9.33)。
1: function generate_div_op ( 2: funct3: input logic<3> , 3: value : input logic<XLEN>, 4: ) -> logic<DIV_WIDTH> { 5: return case funct3[1:0] { 6: 2'b00, 2'b10: abs::<DIV_WIDTH>(value), // DIV, REM 7: 2'b01, 2'b11: value, // DIVU, REMU 8: default : 0, 9: }; 10: } 11: 12: let du_dividend: logic<DIV_WIDTH> = generate_div_op(funct3, op1); 13: let du_divisor : logic<DIV_WIDTH> = generate_div_op(funct3, op2);
1: inst du: divunit #( 2: WIDTH: DIV_WIDTH, 3: ) ( 4: clk , 5: rst , 6: valid : ready && valid && !is_mul && !du_signed_error, 7: dividend : du_dividend , 8: divisor : du_divisor , 9: rvalid : du_rvalid , 10: quotient : du_quotient , 11: remainder: du_remainder , 12: );
表9.4にあるように、符号付き演算は結果がオーバーフローする場合とゼロで割る場合の結果が定められています。その場合には、divunitモジュールで除算を実行せず、muldivunitで計算結果を直接生成するようにします(リスト9.34リスト9.35)。符号付き演算かどうかをfunct3
のLSBで確認し、例外的な処理ではない場合にのみdivunitモジュールで計算を開始するようにします。
1: var du_signed_overflow: logic; 2: var du_signed_divzero : logic; 3: var du_signed_error : logic; 4: 5: always_comb { 6: du_signed_overflow = !funct3[0] && op1[msb] == 1 && op1[msb - 1:0] == 0 && &op2; 7: du_signed_divzero = !funct3[0] && op2 == 0; 8: du_signed_error = du_signed_overflow || du_signed_divzero; 9: }
1: State::Idle: if ready && valid { 2: funct3_saved = funct3; 3: is_op32_saved = is_op32; 4: op1sign_saved = op1[msb]; 5: op2sign_saved = op2[msb]; 6: if is_mul { 7: state = State::WaitValid; 8: } else { 9: if du_signed_overflow { 10: state = State::Finish; 11: result = if funct3[1] ? 0 : {1'b1, 1'b0 repeat XLEN - 1}; // REM : DIV 12: } else if du_signed_divzero { 13: state = State::Finish; 14: result = if funct3[1] ? op1 : '1; // REM : DIV 15: } else { 16: state = State::WaitValid; 17: } 18: } 19: }
計算が終了したら、商と剰余の符号を復元します。商の符号は除数と被除数の符号が異なる場合に負になります。剰余の符号は被除数の符号にします(リスト9.36)。
1: } else if !is_mul && du_rvalid { 2: let quo_signed: logic<DIV_WIDTH> = if op1sign_saved != op2sign_saved ? ~du_quotient + 1 : du_quotient; 3: let rem_signed: logic<DIV_WIDTH> = if op1sign_saved == 1 ? ~du_remainder + 1 : du_remainder; 4: result = case funct3_saved[1:0] { 5: 2'b00 : quo_signed[XLEN - 1:0], // DIV 6: 2'b01 : du_quotient[XLEN - 1:0], // DIVU 7: 2'b10 : rem_signed[XLEN - 1:0], // REM 8: 2'b11 : du_remainder[XLEN - 1:0], // REMU 9: default: 0, 10: }; 11: state = State::Finish; 12: }
riscv-testsのrv64um-p-div
、rv64um-p-rem
を実行し、成功することを確認してください。
9.10 DIVW、DIVUW、REMW、REMUW命令の実装
DIVW、DIVUW、REMW、REMUW命令は、それぞれDIV、DIVU、REM、REMU命令の動作を32ビット同士の演算に変えた命令です。32ビットの結果をXLENビットに符号拡張した値をデスティネーションレジスタに書き込みます。
generate_div_op関数にis_op32
フラグを追加して、is_op32
が1
なら値をDIV_WIDTH
ビットに拡張したものに変更します(リスト9.37)。
1: function generate_div_op ( 2: is_op32: input logic , 3: funct3 : input logic<3> , 4: value : input logic<XLEN>, 5: ) -> logic<DIV_WIDTH> { 6: return case funct3[1:0] { 7: 2'b00, 2'b10: abs::<DIV_WIDTH>(if is_op32 ? sext::<32, DIV_WIDTH>(value[31:0]) : value), // DIV, REM 8: 2'b01, 2'b11: if is_op32 ? {1'b0 repeat DIV_WIDTH - 32, value[31:0]} : value, // DIVU, REMU 9: default : 0, 10: }; 11: } 12: 13: let du_dividend: logic<DIV_WIDTH> = generate_div_op(is_op32, funct3, op1); 14: let du_divisor : logic<DIV_WIDTH> = generate_div_op(is_op32, funct3, op2);
符号付き除算のオーバーフローとゼロ除算の判定をis_op32
で変更します(リスト9.38)。
1: always_comb { 2: if is_op32 { 3: du_signed_overflow = !funct3[0] && op1[31] == 1 && op1[31:0] == 0 && &op2[31:0]; 4: du_signed_divzero = !funct3[0] && op2[31:0] == 0; 5: } else { 6: du_signed_overflow = !funct3[0] && op1[msb] == 1 && op1[msb - 1:0] == 0 && &op2; 7: du_signed_divzero = !funct3[0] && op2 == 0; 8: } 9: du_signed_error = du_signed_overflow || du_signed_divzero; 10: }
最後に、32ビットの結果をXLENビットに符号拡張します(リスト9.39)。符号付き、符号無し演算のどちらも32ビットの結果を符号拡張したものが結果になります。
1: } else if !is_mul && du_rvalid { 2: let quo_signed: logic<DIV_WIDTH> = if op1sign_saved != op2sign_saved ? ~du_quotient + 1 : du_quotient; 3: let rem_signed: logic<DIV_WIDTH> = if op1sign_saved == 1 ? ~du_remainder + 1 : du_remainder; 4: let resultX : UIntX = case funct3_saved[1:0] { 5: 2'b00 : quo_signed[XLEN - 1:0], // DIV 6: 2'b01 : du_quotient[XLEN - 1:0], // DIVU 7: 2'b10 : rem_signed[XLEN - 1:0], // REM 8: 2'b11 : du_remainder[XLEN - 1:0], // REMU 9: default: 0, 10: }; 11: state = State::Finish; 12: result = if is_op32_saved ? sext::<32, 64>(resultX[31:0]) : resultX; 13: }
riscv-testsのrv64um-p-
から始まるテストを実行し、成功することを確認してください。
これでM拡張を実装できました。