Verylで作るCPU
Star

第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)

命令動作
MULrs1(符号付き) × rs2(符号付き)の結果(64ビット)の下位32ビットを求める
MULHrs1(符号付き) × rs2(符号付き)の結果(64ビット)の上位32ビットを求める
MULHUrs1(符号無し) × rs2(符号無し)の結果(64ビット)の上位32ビットを求める
MULHSUrs1(符号付き) × rs2(符号無し)の結果(64ビット)の上位32ビットを求める
DIVrs1(符号付き) / rs2(符号付き)を求める
DIVUrs1(符号無し) / rs2(符号無し)を求める
REMrs1(符号付き) % rs2(符号付き)を求める
REMUrs1(符号無し) % rs2(符号無し)を求める

表9.2: M拡張の命令 (XLEN=64)

命令動作
MULrs1(符号付き) × rs2(符号付き)の結果(128ビット)の下位64ビットを求める
MULWrs1[31:0](符号付き) × rs2[31:0](符号付き)の結果(64ビット)の下位32ビットを求める
結果は符号拡張する
MULHrs1(符号付き) × rs2(符号付き)の結果(128ビット)の上位64ビットを求める
MULHUrs1(符号無し) × rs2(符号無し)の結果(128ビット)の上位64ビットを求める
MULHSUrs1(符号付き) × rs2(符号無し)の結果(128ビット)の上位64ビットを求める
DIVrs1(符号付き) / rs2(符号付き)を求める
DIVWrs1[31:0](符号付き) / rs2[31:0](符号付き)を求める
結果は符号拡張する
DIVUrs1(符号無し) / rs2(符号無し)を求める
DIVWUrs1[31:0](符号無し) / rs2[31:0](符号無し)を求める
結果は符号拡張する
REMrs1(符号付き) % rs2(符号付き)を求める
REMWrs1[31:0](符号付き) % rs2[31:0](符号付き)を求める
結果は符号拡張する
REMUrs1(符号無し) % rs2(符号無し)を求める
REMUWrs1[31:0](符号無し) % rs2[31:0](符号無し)を求める
結果は符号拡張する

Verylには積、商、剰余を求める演算子*/%が定義されており、これを利用することで簡単に計算を実装できます(リスト9.1)。

リスト9.1: リスト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、MULW000
MULH001
MULHU010
MULHSU011
DIV、DIVW100
DIVU、DIVWU101
REM、REMW110
REMU、REMUW111

InstCtrl構造体に、M拡張の命令であることを示すis_muldivフラグを追加します (リスト9.2)。

リスト9.2: リスト9.2: is_muldivフラグを追加する (corectrl.veryl)
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がOPOP-32の場合はfunct7の値によってis_muldivを設定します(リスト9.3)。その他のopcodeのis_muldivFに設定してください。

リスト9.3: リスト9.3: is_muldivを設定する (inst_decoder.veryl) (一部)
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)。

リスト9.4: リスト9.4: muldivunit.veryl
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モジュールはready1のときに計算のリクエストを受け付けます。valid1なら計算を開始し、計算が終了したらrvalid1、計算結果をresultに設定します。

まだ計算処理を実装しておらず、resultは常に0を返します。次の計算を開始するまでresultの値を維持します。

9.3.2 EXステージを変更する

M拡張の命令がEXステージにあるとき、ALUの結果の代わりにmuldivunitモジュールの結果を利用するように変更します。

まず、muldivunitモジュールをインスタンス化します(リスト9.5)。

リスト9.5: リスト9.5: muldivunitモジュールをインスタンス化する (core.veryl)
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)。

リスト9.6: リスト9.6: exs_muldiv_is_requested変数 (core.veryl)
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)。

リスト9.7: リスト9.7: EXステージのストール条件の変更 (core.veryl)
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:     }
リスト9.8: リスト9.8: EXステージのストール条件の変更とM拡張の命令の結果の設定 (core.veryl)
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モジュールは計算が完了したクロックでしかrvalid1にしないため、既に計算が完了したことを示す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)。

リスト9.9: リスト9.9: muldivunit.veryl
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を計算するモジュールです。valid1になったら計算を開始し、計算が完了したらrvalid1resultWIDTH * 2ビットの計算結果に設定します。

積はWIDTH回の足し算をWIDTHクロックかけて行って求めています(図9.1)。計算を開始すると入力をゼロでWIDTH * 2ビットに拡張し、result0でリセットします。

State::AddLoopでは、次の操作をWIDTH回行います。i回目では次の操作を行います。

  1. op2[i-1]1ならresultop1を足す
  2. op1を1ビット左シフトする
  3. カウンタをインクリメントする
符号無し4ビットの乗算

図9.1: 符号無し4ビットの乗算

9.4.2 mulunitモジュールをインスタンス化する

mulunitモジュールをmuldivunitモジュールでインスタンス化します(リスト9.10)。まだ結果は利用しません。

リスト9.10: リスト9.10: mulunitモジュールをインスタンス化する (muldivunit.veryl)
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)。

リスト9.11: リスト9.11: MULHUモジュールの結果を取得する (muldivunit.veryl)
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モジュールを使って、次のように符号付き乗算を実現します。

  1. 符号付きのXLENビットの値を符号無しの値(絶対値)に変換する
  2. 符号無しで積を計算する
  3. 計算結果の符号を修正する

絶対値で計算することで符号ビットを考慮する必要がなくなり、既に実装してある符号無しの乗算器を変更せずに符号付きの乗算を実現できます。

9.6.2 符号付き乗算を実装する

WIDTHビットの符号付きの値をWIDTHビットの符号無しの絶対値に変換するabs関数を作成します(リスト9.12)。abs関数は、値のMSBが1ならビットを反転して1を足すことで符号を反転しています。最小値-2 ** (WIDTH - 1)の絶対値も求められることを確認してください。

リスト9.12: リスト9.12: abs関数を実装する (muldivunit.veryl)
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)。

リスト9.13: リスト9.13: op1とop2を生成する (muldivunit.veryl)
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:     };
リスト9.14: リスト9.14: mulunitに渡す値を変更する (muldivunit.veryl)
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:     );

計算結果の符号はop1op2の符号が異なる場合に負になります。後で符号の情報を利用するために、muldivunitモジュールが要求を受け入れる時に符号を保存します(リスト9.15リスト9.16リスト9.17)。

リスト9.15: リスト9.15: 符号を保存する変数を作成する (muldivunit.veryl)
1:     // saved_data
2:     var funct3_saved : logic<3>;
3:     var op1sign_saved: logic   ;
4:     var op2sign_saved: logic   ;
リスト9.16: リスト9.16: 変数のリセット (muldivunit.veryl)
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 {
リスト9.17: リスト9.17: 符号を変数に保存する (muldivunit.veryl)
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)。

リスト9.18: リスト9.18: 計算結果の符号を復元する (muldivunit.veryl)
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-mulrv64um-p-mulhを実行し、成功することを確認してください。

9.6.3 MULHSU命令の実装

MULHSU命令は、符号付きのXLENビットのrs1と符号無しのXLENビットのrs2の乗算を実行し、デスティネーションレジスタに結果の上位XLENビットを書き込む命令です。計算結果は符号付きの値になります。

MULHSU命令も、MUL、MULH命令と同様に符号無しの乗算器で実現します。

op1を絶対値に変換し、op2はそのままに設定します(リスト9.19)。

リスト9.19: リスト9.19: MULHSU命令用にop1、op2を設定する (muldivunit.veryl)
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)。

リスト9.20: リスト9.20: 計算結果の符号を復元する (muldivunit.veryl)
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)。

リスト9.21: リスト9.21: is_op32ポートを追加する (muldivunit.veryl)
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: ) {
リスト9.22: リスト9.22: is_op32ポートに値を割り当てる (core.veryl)
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)。

リスト9.23: リスト9.23: is_op32を保存する変数を作成する (muldivunit.veryl)
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   ;
リスト9.24: リスト9.24: 変数のリセット (muldivunit.veryl)
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 {
リスト9.25: リスト9.25: is_op32を変数に保存する (muldivunit.veryl)
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モジュールのop1op2に、64ビットの値の下位32ビットを符号拡張した値を割り当てます。符号拡張を行うsext関数を作成し、mu_op1mu_op2の割り当てに利用します(リスト9.26リスト9.27)。

リスト9.26: リスト9.26: 符号拡張する関数を作成する (muldivunit.veryl)
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:     }
リスト9.27: リスト9.27: MULW命令用にop1、op2を設定する (muldivunit.veryl)
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)。

リスト9.28: リスト9.28: 計算結果を符号拡張する (muldivunit.veryl)
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)。

リスト9.29: リスト9.29: muldivunit.veryl
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)を計算するモジュールです。valid1になったら計算を開始し、計算が完了したらrvalid1に設定します。

商と剰余はWIDTH回の引き算をWIDTHクロックかけて行って求めています。計算を開始すると被除数を0WIDTH * 2ビットに拡張し、除数をWIDTH-1ビット左シフトします。また、商を0でリセットします。

State::SubLoopでは、次の操作をWIDTH回行います。

  1. 被除数が除数よりも大きいなら、被除数から除数を引き、商のLSBを1にする
  2. 商を1ビット左シフトする
  3. 除数を1ビット右シフトする
  4. カウンタをインクリメントする

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)。まだ結果は利用しません。

リスト9.30: リスト9.30: divunitモジュールをインスタンス化する (muldivunit.veryl)
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)。

リスト9.31: リスト9.31: divunitモジュールの結果をresultに割り当てる (muldivunit.veryl)
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-divurv64um-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)。

リスト9.32: リスト9.32: 除数と被除数を生成する (muldivunit.veryl)
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);
リスト9.33: リスト9.33: divunitに渡す値を変更する (muldivunit.veryl)
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モジュールで計算を開始するようにします。

リスト9.34: リスト9.34: 符号付き除算がオーバーフローするか、ゼロ除算かどうかを判定する (muldivunit.veryl)
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:     }
リスト9.35: リスト9.35: 符号付き除算の例外的な結果を処理する (muldivunit.veryl)
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)。

リスト9.36: リスト9.36: 計算結果の符号を復元する (muldivunit.veryl)
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-divrv64um-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_op321なら値をDIV_WIDTHビットに拡張したものに変更します(リスト9.37)。

リスト9.37: リスト9.37: 除数、被除数を32ビットの値にする (muldivunit.veryl)
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)。

リスト9.38: リスト9.38: 32ビット演算のときの例外的な処理に対応する (muldivunit.veryl)
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ビットの結果を符号拡張したものが結果になります。

リスト9.39: リスト9.39: 32ビット演算のとき、結果を符号拡張する (muldivunit.veryl)
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拡張を実装できました。