Skip to content

M拡張の実装

概要

「第I部 RV32I / RV64Iの実装」ではRV64IのCPUを実装しました。 「第II部 RV64IMACの実装」では、次のような機能を実装します。

  • 乗算、除算、剰余演算命令 (M拡張)
  • 不可分操作命令 (A拡張)
  • 圧縮命令 (C拡張)
  • 例外
  • Memory-mapped I/O

本章では積、商、剰余を求める命令を実装します。 RISC-Vの乗算、除算、剰余演算を行う命令はM拡張に定義されており、 M拡張を実装したRV64IのISAのことをRV64IMと表記します。

M拡張には、XLENが32のときは表1の命令が定義されています。 XLENが64のときは表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には積、商、剰余を求める演算子`*`、`/`、`%`が定義されており、 これを利用することで簡単に計算を実装できます(リスト1)。

▼リスト9.1: 演算子による実装例

by
assign mul = op1 * op2;
assign div = op1 / op2;
assign rem = op1 % op2;

例えば乗算回路をFPGA上に実装する場合、通常は合成系によってFPGAに搭載されている乗算器が自動的に利用されます[1]。 これにより、低遅延、低リソースコストで効率的な乗算回路を自動的に実現できます。 しかし、32ビットや64ビットの乗算を実装する際、 FPGA上の乗算器の数が不足すると、LUTを用いた大規模な乗算回路が構築されることがあります。 このような大規模な回路はFPGAのリソースの使用量や遅延に大きな影響を与えるため好ましくありません。 除算や剰余演算でも同じ問題[2]が生じることがあります。

*/%演算子がどのような回路に合成されるかは、 合成系が全体の実装を考慮して自動的に決定するため、 その挙動をコントロールするのは難しいです。 そこで本章では、*/%演算子を使用せず、 足し算やシフト演算などの基本的な論理だけを用いて同等の演算を実装します。

基本編では積、商、剰余を効率よく[3]求める実装は検討せず、できるだけ単純な方法で実装します。

命令のデコード

まず、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で区別します(表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`フラグを追加します (リスト2)。

▼リスト9.2: is_muldivフラグを追加する (corectrl.veryl) 差分をみる

veryl
// 制御に使うフラグ用の構造体
struct InstCtrl {
    itype    : InstType   , // 命令の形式
    rwb_en   : logic      , // レジスタに書き込むかどうか
    is_lui   : logic      , // LUI命令である
    is_aluop : logic      , // ALUを利用する命令である
    is_muldiv: logic      , // M拡張の命令である
    is_op32  : logic      , // OP-32またはOP-IMM-32である
    is_jump  : logic      , // ジャンプ命令である
    is_load  : logic      , // ロード命令である
    is_csr   : logic      , // CSR命令である
    funct3   : logic   <3>, // 命令のfunct3フィールド
    funct7   : logic   <7>, // 命令のfunct7フィールド
}

inst_decoderモジュールのInstCtrlを生成している部分を変更します。 opcodeがOPOP-32の場合はfunct7の値によってis_muldivを設定します(リスト3)。 その他のopcodeのis_muldivFに設定してください。

▼リスト9.3: is_muldivを設定する (inst_decoder.veryl) (一部) 差分をみる

veryl
OP_OP: {
    InstType::R, T, F, T, f7 == 7'b0000001, F, F, F, F
},
OP_OP_IMM: {
    InstType::I, T, F, T, F, F, F, F, F
},
OP_OP_32: {
    InstType::R, T, F, T, f7 == 7'b0000001, T, F, F, F
},

muldivunitモジュールの実装

muldivunitモジュールを作成する

M拡張の計算を処理するモジュールを作成し、 M拡張の命令がALUの結果ではなくモジュールの結果を利用するように変更します。

src/muldivunit.verylを作成し、次のように記述します(リスト4)。

▼リスト9.4: muldivunit.veryl 差分をみる

veryl
import eei::*;

module muldivunit (
    clk   : input  clock   ,
    rst   : input  reset   ,
    ready : output logic   ,
    valid : input  logic   ,
    funct3: input  logic<3>,
    op1   : input  UIntX   ,
    op2   : input  UIntX   ,
    rvalid: output logic   ,
    result: output UIntX   ,
) {

    enum State {
        Idle,
        WaitValid,
        Finish,
    }

    var state: State;

    // saved_data
    var funct3_saved: logic<3>;

    always_comb {
        ready  = state == State::Idle;
        rvalid = state == State::Finish;
    }

    always_ff {
        if_reset {
            state        = State::Idle;
            result       = 0;
            funct3_saved = 0;
        } else {
            case state {
                State::Idle: if ready && valid {
                    state        = State::WaitValid;
                    funct3_saved = funct3;
                }
                State::WaitValid: state = State::Finish;
                State::Finish   : state = State::Idle;
                default         : {}
            }
        }
    }
}

muldivunitモジュールはready1のときに計算のリクエストを受け付けます。 valid1なら計算を開始し、 計算が終了したらrvalid1、計算結果をresultに設定します。

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

EXステージを変更する

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

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

▼リスト9.5: muldivunitモジュールをインスタンス化する (core.veryl) 差分をみる

veryl
let exs_muldiv_valid : logic = exs_valid && exs_ctrl.is_muldiv && !exs_data_hazard && !exs_muldiv_is_requested;
var exs_muldiv_ready : logic;
var exs_muldiv_rvalid: logic;
var exs_muldiv_result: UIntX;

inst mdu: muldivunit (
    clk                      ,
    rst                      ,
    valid : exs_muldiv_valid ,
    ready : exs_muldiv_ready ,
    funct3: exs_ctrl.funct3  ,
    op1   : exs_op1          ,
    op2   : exs_op2          ,
    rvalid: exs_muldiv_rvalid,
    result: exs_muldiv_result,
);

muldivunitモジュールで計算を開始するのは、 EXステージに命令が存在し(exs_valid)、 命令がM拡張の命令であり(exs_ctrl.is_muldiv)、 データハザードが発生しておらず(!exs_data_hazard)、 既に計算を要求していない(!exs_muldiv_is_requested) 場合です。

exs_muldiv_is_requested変数を定義し、 ステージの遷移条件とmuldivunitに計算を要求したかの状態によって値を更新します(リスト6)。

▼リスト9.6: exs_muldiv_is_requested変数 (core.veryl) 差分をみる

veryl
var exs_muldiv_is_requested: logic;

always_ff {
    if_reset {
        exs_muldiv_is_requested = 0;
    } else {
        // 次のステージに遷移
        if exq_rvalid && exq_rready {
            exs_muldiv_is_requested = 0;
        } else {
            // muldivunitにリクエストしたか判定する
            if exs_muldiv_valid && exs_muldiv_ready {
                exs_muldiv_is_requested = 1;
            }
        }
    }
}

muldivunitモジュールはALUのように1クロックの間に入力から出力を生成しないため、 計算中はEXステージをストールさせる必要があります。 そのためにexs_muldiv_stall変数を定義して、ストールの条件に追加します(リスト7、リスト8)。 また、M拡張の命令の場合はMEMステージに渡すalu_resultの値をmuldivunitモジュールの結果に設定します(リスト8)。

▼リスト9.7: EXステージのストール条件の変更 (core.veryl) 差分をみる

veryl
var exs_muldiv_rvalided: logic;
let exs_muldiv_stall   : logic = exs_ctrl.is_muldiv && !exs_muldiv_rvalid && !exs_muldiv_rvalided;

always_ff {
    if_reset {
        exs_muldiv_rvalided = 0;
    } else {
        // 次のステージに遷移
        if exq_rvalid && exq_rready {
            exs_muldiv_rvalided = 0;
        } else {
            // muldivunitの処理が完了していたら1にする
            exs_muldiv_rvalided |= exs_muldiv_rvalid;
        }
    }
}

▼リスト9.8: EXステージのストール条件の変更とM拡張の命令の結果の設定 (core.veryl) 差分をみる

veryl
let exs_stall: logic = exs_data_hazard || exs_muldiv_stall;

always_comb {
    // EX -> MEM
    exq_rready            = memq_wready && !exs_stall;
    memq_wvalid           = exq_rvalid && !exs_stall;
    memq_wdata.addr       = exq_rdata.addr;
    memq_wdata.bits       = exq_rdata.bits;
    memq_wdata.ctrl       = exq_rdata.ctrl;
    memq_wdata.imm        = exq_rdata.imm;
    memq_wdata.rs1_addr   = exs_rs1_addr;
    memq_wdata.rs1_data   = exs_rs1_data;
    memq_wdata.rs2_data   = exs_rs2_data;
    memq_wdata.alu_result = if exs_ctrl.is_muldiv ? exs_muldiv_result : exs_alu_result;
    memq_wdata.br_taken   = exs_ctrl.is_jump || inst_is_br(exs_ctrl) && exs_brunit_take;
    memq_wdata.jump_addr  = if inst_is_br(exs_ctrl) ? exs_pc + exs_imm : exs_alu_result & ~1;
}

muldivunitモジュールは計算が完了したクロックでしかrvalid1にしないため、 既に計算が完了したことを示すexs_muldiv_rvalided変数で完了状態を管理します。 これにより、M拡張の命令によってストールする条件は、 命令がM拡張の命令であり(exs_ctrl.is_muldiv)、 現在のクロックで計算が完了しておらず(!exs_muldiv_rvalid)、 以前のクロックでも計算が完了していない(!exs_muldiv_rvalided) 場合になります。

符号無しの乗算器の実装

mulunitモジュールを実装する

WIDTHビットの符号無しの値同士の積を計算する乗算器を実装します。

src/muldivunit.verylの中にmulunitモジュールを作成します(リスト9)。

▼リスト9.9: muldivunit.veryl

veryl
module mulunit #(
    param WIDTH: u32 = 0,
) (
    clk   : input  clock           ,
    rst   : input  reset           ,
    valid : input  logic           ,
    op1   : input  logic<WIDTH>    ,
    op2   : input  logic<WIDTH>    ,
    rvalid: output logic           ,
    result: output logic<WIDTH * 2>,
) {
    enum State {
        Idle,
        AddLoop,
        Finish,
    }

    var state: State;

    var op1zext: logic<WIDTH * 2>;
    var op2zext: logic<WIDTH * 2>;

    always_comb {
        rvalid = state == State::Finish;
    }

    var add_count: logic<32>;

    always_ff {
        if_reset {
            state     = State::Idle;
            result    = 0;
            add_count = 0;
            op1zext   = 0;
            op2zext   = 0;
        } else {
            case state {
                State::Idle: if valid {
                    state     = State::AddLoop;
                    result    = 0;
                    add_count = 0;
                    op1zext   = {1'b0 repeat WIDTH, op1};
                    op2zext   = {1'b0 repeat WIDTH, op2};
                }
                State::AddLoop: if add_count == WIDTH {
                    state = State::Finish;
                } else {
                    if op2zext[add_count] {
                        result += op1zext;
                    }
                    op1zext   <<= 1;
                    add_count +=  1;
                }
                State::Finish: state = State::Idle;
                default      : {}
            }
        }
    }
}

mulunitモジュールはop1 * op2を計算するモジュールです。 valid1になったら計算を開始し、 計算が完了したらrvalid1resultWIDTH * 2ビットの計算結果に設定します。

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

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

  1. op2[i-1]1ならresultop1を足す
  2. op1を1ビット左シフトする
  3. カウンタをインクリメントする

符号無し4ビットの乗算

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

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

▼リスト9.10: mulunitモジュールをインスタンス化する (muldivunit.veryl) 差分をみる

veryl
// multiply unit
const MUL_OP_WIDTH : u32 = XLEN;
const MUL_RES_WIDTH: u32 = MUL_OP_WIDTH * 2;

let is_mul   : logic                = if state == State::Idle ? !funct3[2] : !funct3_saved[2];
var mu_rvalid: logic               ;
var mu_result: logic<MUL_RES_WIDTH>;

inst mu: mulunit #(
    WIDTH: MUL_OP_WIDTH,
) (
    clk                             ,
    rst                             ,
    valid : ready && valid && is_mul,
    op1   : op1                     ,
    op2   : op2                     ,
    rvalid: mu_rvalid               ,
    result: mu_result               ,
);

MULHU命令の実装

MULHU命令は、2つの符号無しのXLENビットの値の乗算を実行し、 デスティネーションレジスタに結果(XLEN * 2ビット)の上位XLENビットを書き込む命令です。 funct3の下位2ビットによってmulunitモジュールの結果を選択するようにします (リスト11)。

▼リスト9.11: MULHUモジュールの結果を取得する (muldivunit.veryl)

veryl
State::WaitValid: if is_mul && mu_rvalid {
    state  = State::Finish;
    result = case funct3_saved[1:0] {
        2'b11  : mu_result[XLEN+:XLEN], // MULHU
        default: 0,
    };
}

riscv-testsのrv64um-p-mulhuを実行し、成功することを確認してください。

MUL、MULH命令の実装

符号付き乗算を符号無し乗算器で実現する

MUL、MULH命令は、2つの符号付きのXLENビットの値の乗算を実行し、 デスティネーションレジスタにそれぞれ結果の下位XLENビット、上位XLENビットを書き込む命令です。

本章ではmulunitモジュールを使って、次のように符号付き乗算を実現します。

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

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

符号付き乗算を実装する

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

▼リスト9.12: abs関数を実装する (muldivunit.veryl) 差分をみる

veryl
function abs::<WIDTH: u32> (
    value: input logic<WIDTH>,
) -> logic<WIDTH> {
    return if value[msb] ? ~value + 1 : value;
}

abs関数を利用して、MUL、MULH命令のときにmulunitに渡す値を絶対値に設定します (リスト13、リスト14)。

▼リスト9.13: op1とop2を生成する (muldivunit.veryl) 差分をみる

veryl
let mu_op1: logic<MUL_OP_WIDTH> = case funct3[1:0] {
    2'b00, 2'b01: abs::<XLEN>(op1), // MUL, MULH
    2'b11       : op1, // MULHU
    default     : 0,
};
let mu_op2: logic<MUL_OP_WIDTH> = case funct3[1:0] {
    2'b00, 2'b01: abs::<XLEN>(op2), // MUL, MULH
    2'b11       : op2, // MULHU
    default     : 0,
};

▼リスト9.14: mulunitに渡す値を変更する (muldivunit.veryl) 差分をみる

veryl
inst mu: mulunit #(
    WIDTH: MUL_OP_WIDTH,
) (
    clk                             ,
    rst                             ,
    valid : ready && valid && is_mul,
    op1   : mu_op1                  ,
    op2   : mu_op2                  ,
    rvalid: mu_rvalid               ,
    result: mu_result               ,
);

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

▼リスト9.15: 符号を保存する変数を作成する (muldivunit.veryl) 差分をみる

veryl
// saved_data
var funct3_saved : logic<3>;
var op1sign_saved: logic   ;
var op2sign_saved: logic   ;

▼リスト9.16: 変数のリセット (muldivunit.veryl) 差分をみる

veryl
always_ff {
    if_reset {
        state         = State::Idle;
        result        = 0;
        funct3_saved  = 0;
        op1sign_saved = 0;
        op2sign_saved = 0;
    } else {

▼リスト9.17: 符号を変数に保存する (muldivunit.veryl) 差分をみる

veryl
case state {
    State::Idle: if ready && valid {
        state         = State::WaitValid;
        funct3_saved  = funct3;
        op1sign_saved = op1[msb];
        op2sign_saved = op2[msb];
    }

保存した符号を利用して計算結果の符号を復元します (リスト18)。

▼リスト9.18: 計算結果の符号を復元する (muldivunit.veryl) 差分をみる

veryl
State::WaitValid: if is_mul && mu_rvalid {
    let res_signed: logic<MUL_RES_WIDTH> = if op1sign_saved != op2sign_saved ? ~mu_result + 1 : mu_result;
    state      = State::Finish;
    result     = case funct3_saved[1:0] {
        2'b00  : res_signed[XLEN - 1:0], // MUL
        2'b01  : res_signed[XLEN+:XLEN], // MULH
        2'b11  : mu_result[XLEN+:XLEN], // MULHU
        default: 0,
    };
}

riscv-testsのrv64um-p-mulrv64um-p-mulhを実行し、成功することを確認してください。

MULHSU命令の実装

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

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

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

▼リスト9.19: MULHSU命令用にop1、op2を設定する (muldivunit.veryl) 差分をみる

veryl
let mu_op1: logic<MUL_OP_WIDTH> = case funct3[1:0] {
    2'b00, 2'b01, 2'b10: abs::<XLEN>(op1), // MUL, MULH, MULHSU
    2'b11              : op1, // MULHU
    default            : 0,
};
let mu_op2: logic<MUL_OP_WIDTH> = case funct3[1:0] {
    2'b00, 2'b01: abs::<XLEN>(op2), // MUL, MULH
    2'b11, 2'b10: op2, // MULHU, MULHSU
    default     : 0,
};

計算結果はop1の符号にします (リスト20)。

▼リスト9.20: 計算結果の符号を復元する (muldivunit.veryl) 差分をみる

veryl
State::WaitValid: if is_mul && mu_rvalid {
    let res_signed: logic<MUL_RES_WIDTH> = if op1sign_saved != op2sign_saved ? ~mu_result + 1 : mu_result;
    let res_mulhsu: logic<MUL_RES_WIDTH> = if op1sign_saved == 1 ? ~mu_result + 1 : mu_result;
    state      = State::Finish;
    result     = case funct3_saved[1:0] {
        2'b00  : res_signed[XLEN - 1:0], // MUL
        2'b01  : res_signed[XLEN+:XLEN], // MULH
        2'b10  : res_mulhsu[XLEN+:XLEN], // MULHSU
        2'b11  : mu_result[XLEN+:XLEN], // MULHU
        default: 0,
    };
}

riscv-testsのrv64um-p-mulhsuを実行し、成功することを確認してください。

MULW命令の実装

MULW命令は、2つの符号付きの32ビットの値の乗算を実行し、 デスティネーションレジスタに結果の下位32ビットを符号拡張した値を書き込む命令です。

32ビット演算の命令であることを判定するために、 muldivunitモジュールにis_op32ポートを作成します ( リスト21、 リスト22 )。

▼リスト9.21: is_op32ポートを追加する (muldivunit.veryl) 差分をみる

veryl
module muldivunit (
    clk    : input  clock   ,
    rst    : input  reset   ,
    ready  : output logic   ,
    valid  : input  logic   ,
    funct3 : input  logic<3>,
    is_op32: input  logic   ,
    op1    : input  UIntX   ,
    op2    : input  UIntX   ,
    rvalid : output logic   ,
    result : output UIntX   ,
) {

▼リスト9.22: is_op32ポートに値を割り当てる (core.veryl) 差分をみる

veryl
inst mdu: muldivunit (
    clk                       ,
    rst                       ,
    valid  : exs_muldiv_valid ,
    ready  : exs_muldiv_ready ,
    funct3 : exs_ctrl.funct3  ,
    is_op32: exs_ctrl.is_op32 ,
    op1    : exs_op1          ,
    op2    : exs_op2          ,
    rvalid : exs_muldiv_rvalid,
    result : exs_muldiv_result,
);

muldivunitモジュールが要求を受け入れる時にis_op32を保存します ( リスト23、 リスト24、 リスト25 )。

▼リスト9.23: is_op32を保存する変数を作成する (muldivunit.veryl) 差分をみる

veryl
// saved_data
var funct3_saved : logic<3>;
var is_op32_saved: logic   ;
var op1sign_saved: logic   ;
var op2sign_saved: logic   ;

▼リスト9.24: 変数のリセット (muldivunit.veryl) 差分をみる

veryl
always_ff {
    if_reset {
        state         = State::Idle;
        result        = 0;
        funct3_saved  = 0;
        is_op32_saved = 0;
        op1sign_saved = 0;
        op2sign_saved = 0;
    } else {

▼リスト9.25: is_op32を変数に保存する (muldivunit.veryl) 差分をみる

veryl
State::Idle: if ready && valid {
    state         = State::WaitValid;
    funct3_saved  = funct3;
    is_op32_saved = is_op32;
    op1sign_saved = op1[msb];
    op2sign_saved = op2[msb];
}

mulunitモジュールのop1op2に、64ビットの値の下位32ビットを符号拡張した値を割り当てます。 符号拡張を行うsext関数を作成し、mu_op1mu_op2の割り当てに利用します ( リスト26、 リスト27 )。

▼リスト9.26: 符号拡張する関数を作成する (muldivunit.veryl) 差分をみる

veryl
function sext::<WIDTH_IN: u32, WIDTH_OUT: u32> (
    value: input logic<WIDTH_IN>,
) -> logic<WIDTH_OUT> {
    return {value[msb] repeat WIDTH_OUT - WIDTH_IN, value};
}

▼リスト9.27: MULW命令用にop1、op2を設定する (muldivunit.veryl) 差分をみる

veryl
let mu_op1: logic<MUL_OP_WIDTH> = case funct3[1:0] {
    2'b00, 2'b01, 2'b10: abs::<XLEN>(if is_op32 ? sext::<32, XLEN>(op1[31:0]) : op1), // MUL, MULH, MULHSU, MULW
    2'b11              : op1, // MULHU
    default            : 0,
};
let mu_op2: logic<MUL_OP_WIDTH> = case funct3[1:0] {
    2'b00, 2'b01: abs::<XLEN>(if is_op32 ? sext::<32, XLEN>(op2[31:0]) : op2), // MUL, MULH, MULW
    2'b11, 2'b10: op2, // MULHU, MULHSU
    default     : 0,
};

最後に、計算結果を符号拡張した値に設定します (リスト28)。

▼リスト9.28: 計算結果を符号拡張する (muldivunit.veryl) 差分をみる

veryl
State::WaitValid: if is_mul && mu_rvalid {
    let res_signed: logic<MUL_RES_WIDTH> = if op1sign_saved != op2sign_saved ? ~mu_result + 1 : mu_result;
    let res_mulhsu: logic<MUL_RES_WIDTH> = if op1sign_saved == 1 ? ~mu_result + 1 : mu_result;
    state      = State::Finish;
    result     = case funct3_saved[1:0] {
        2'b00  : if is_op32_saved ? sext::<32, 64>(res_signed[31:0]) : res_signed[XLEN - 1:0], // MUL, MULW
        2'b01  : res_signed[XLEN+:XLEN], // MULH

riscv-testsのrv64um-p-mulwを実行し、成功することを確認してください。

符号無し除算の実装

divunitモジュールを実装する

WIDTHビットの除算を計算する除算器を実装します。

src/muldivunit.verylの中にdivunitモジュールを作成します (リスト29)。

▼リスト9.29: muldivunit.veryl 差分をみる

veryl
module divunit #(
    param WIDTH: u32 = 0,
) (
    clk      : input  clock       ,
    rst      : input  reset       ,
    valid    : input  logic       ,
    dividend : input  logic<WIDTH>,
    divisor  : input  logic<WIDTH>,
    rvalid   : output logic       ,
    quotient : output logic<WIDTH>,
    remainder: output logic<WIDTH>,
) {
    enum State {
        Idle,
        ZeroCheck,
        SubLoop,
        Finish,
    }

    var state: State;

    var dividend_saved: logic<WIDTH * 2>;
    var divisor_saved : logic<WIDTH * 2>;

    always_comb {
        rvalid    = state == State::Finish;
        remainder = dividend_saved[WIDTH - 1:0];
    }

    var sub_count: u32;

    always_ff {
        if_reset {
            state          = State::Idle;
            quotient       = 0;
            sub_count      = 0;
            dividend_saved = 0;
            divisor_saved  = 0;
        } else {
            case state {
                State::Idle: if valid {
                    state          = State::ZeroCheck;
                    dividend_saved = {1'b0 repeat WIDTH, dividend};
                    divisor_saved  = {1'b0, divisor, 1'b0 repeat WIDTH - 1};
                    quotient       = 0;
                    sub_count      = 0;
                }
                State::ZeroCheck: if divisor_saved == 0 {
                    state    = State::Finish;
                    quotient = '1;
                } else {
                    state = State::SubLoop;
                }
                State::SubLoop: if sub_count == WIDTH {
                    state = State::Finish;
                } else {
                    if dividend_saved >= divisor_saved {
                        dividend_saved -= divisor_saved;
                        quotient       =  (quotient << 1) + 1;
                    } else {
                        quotient <<= 1;
                    }
                    divisor_saved >>= 1;
                    sub_count     +=  1;
                }
                State::Finish: state = State::Idle;
                default      : {}
            }
        }
    }
}

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ビットの除算の結果は表4のようになると定められています。 このうちdivunitモジュールは符号無しの除算(DIVU、REMU命令)のゼロ除算だけを対処しています。

表9.4: 除算の例外的な動作と結果

操作ゼロ除算オーバーフロー
符号付き除算-1-2**(L-1)
符号付き剰余被除数0
符号無し除算2**L-1発生しない
符号無し剰余被除数発生しない

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

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

▼リスト9.30: divunitモジュールをインスタンス化する (muldivunit.veryl) 差分をみる

veryl
// divider unit
const DIV_WIDTH: u32 = XLEN;

var du_rvalid   : logic           ;
var du_quotient : logic<DIV_WIDTH>;
var du_remainder: logic<DIV_WIDTH>;

inst du: divunit #(
    WIDTH: DIV_WIDTH,
) (
    clk                                 ,
    rst                                 ,
    valid    : ready && valid && !is_mul,
    dividend : op1                      ,
    divisor  : op2                      ,
    rvalid   : du_rvalid                ,
    quotient : du_quotient              ,
    remainder: du_remainder             ,
);

DIVU、REMU命令の実装

DIVU、REMU命令は、符号無しのXLENビットのrs1(被除数)と符号無しのXLENビットのrs2(除数)の商、剰余を計算し、 デスティネーションレジスタにそれぞれ結果を書き込む命令です。

muldivunitモジュールで、divunitモジュールの処理が終わったら結果をresultレジスタに割り当てるようにします (リスト31)。

▼リスト9.31: divunitモジュールの結果をresultに割り当てる (muldivunit.veryl) 差分をみる

veryl
State::WaitValid: if is_mul && mu_rvalid {
    ...
} else if !is_mul && du_rvalid {
    result = case funct3_saved[1:0] {
        2'b01  : du_quotient, // DIVU
        2'b11  : du_remainder, // REMU
        default: 0,
    };
    state = State::Finish;
}

riscv-testsのrv64um-p-divurv64um-p-remuを実行し、成功することを確認してください。

DIV、REM命令の実装

符号付き除算を符号無し除算器で実現する

DIV、REM命令は、それぞれDIVU、REMU命令の動作を符号付きに変えた命令です。 本章では、符号付き乗算と同じように値を絶対値に変換して計算することで符号付き除算を実現します。

RISC-Vの符号付き除算の結果は0の方向に丸められた整数になり、剰余演算の結果は被除数と同じ符号になります。 符号付き剰余の絶対値は符号無し剰余の結果と一致するため、 絶対値で計算してから符号を戻すことで、符号無し除算器だけで符号付きの剰余演算を実現できます。

符号付き除算を実装する

abs関数を利用して、DIV、REM命令のときにdivunitモジュールに渡す値を絶対値に設定します ( リスト32 リスト33 )。

▼リスト9.32: 除数と被除数を生成する (muldivunit.veryl) 差分をみる

veryl
function generate_div_op (
    funct3: input logic<3>   ,
    value : input logic<XLEN>,
) -> logic<DIV_WIDTH> {
    return case funct3[1:0] {
        2'b00, 2'b10: abs::<DIV_WIDTH>(value), // DIV, REM
        2'b01, 2'b11: value, // DIVU, REMU
        default     : 0,
    };
}

let du_dividend: logic<DIV_WIDTH> = generate_div_op(funct3, op1);
let du_divisor : logic<DIV_WIDTH> = generate_div_op(funct3, op2);

▼リスト9.33: divunitに渡す値を変更する (muldivunit.veryl) 差分をみる

veryl
inst du: divunit #(
    WIDTH: DIV_WIDTH,
) (
    clk                                                     ,
    rst                                                     ,
    valid    : ready && valid && !is_mul && !du_signed_error,
    dividend : du_dividend                                  ,
    divisor  : du_divisor                                   ,
    rvalid   : du_rvalid                                    ,
    quotient : du_quotient                                  ,
    remainder: du_remainder                                 ,
);

表4にあるように、符号付き演算は結果がオーバーフローする場合とゼロで割る場合の結果が定められています。 その場合には、divunitモジュールで除算を実行せず、muldivunitで計算結果を直接生成するようにします ( リスト34 リスト35 )。 符号付き演算かどうかをfunct3のLSBで確認し、例外的な処理ではない場合にのみdivunitモジュールで計算を開始するようにします。

▼リスト9.34: 符号付き除算がオーバーフローするか、ゼロ除算かどうかを判定する (muldivunit.veryl) 差分をみる

veryl
var du_signed_overflow: logic;
var du_signed_divzero : logic;
var du_signed_error   : logic;

always_comb {
    du_signed_overflow = !funct3[0] && op1[msb] == 1 && op1[msb - 1:0] == 0 && &op2;
    du_signed_divzero  = !funct3[0] && op2 == 0;
    du_signed_error    = du_signed_overflow || du_signed_divzero;
}

▼リスト9.35: 符号付き除算の例外的な結果を処理する (muldivunit.veryl) 差分をみる

veryl
State::Idle: if ready && valid {
    funct3_saved  = funct3;
    is_op32_saved = is_op32;
    op1sign_saved = op1[msb];
    op2sign_saved = op2[msb];
    if is_mul {
        state = State::WaitValid;
    } else {
        if du_signed_overflow {
            state  = State::Finish;
            result = if funct3[1] ? 0 : {1'b1, 1'b0 repeat XLEN - 1}; // REM : DIV
        } else if du_signed_divzero {
            state  = State::Finish;
            result = if funct3[1] ? op1 : '1; // REM : DIV
        } else {
            state = State::WaitValid;
        }
    }
}

計算が終了したら、商と剰余の符号を復元します。 商の符号は除数と被除数の符号が異なる場合に負になります。 剰余の符号は被除数の符号にします (リスト36)。

▼リスト9.36: 計算結果の符号を復元する (muldivunit.veryl) 差分をみる

veryl
} else if !is_mul && du_rvalid {
    let quo_signed: logic<DIV_WIDTH> = if op1sign_saved != op2sign_saved ? ~du_quotient + 1 : du_quotient;
    let rem_signed: logic<DIV_WIDTH> = if op1sign_saved == 1 ? ~du_remainder + 1 : du_remainder;
    result     = case funct3_saved[1:0] {
        2'b00  : quo_signed[XLEN - 1:0], // DIV
        2'b01  : du_quotient[XLEN - 1:0], // DIVU
        2'b10  : rem_signed[XLEN - 1:0], // REM
        2'b11  : du_remainder[XLEN - 1:0], // REMU
        default: 0,
    };
    state = State::Finish;
}

riscv-testsのrv64um-p-divrv64um-p-remを実行し、成功することを確認してください。

DIVW、DIVUW、REMW、REMUW命令の実装

DIVW、DIVUW、REMW、REMUW命令は、それぞれDIV、DIVU、REM、REMU命令の動作を32ビット同士の演算に変えた命令です。 32ビットの結果をXLENビットに符号拡張した値をデスティネーションレジスタに書き込みます。

generate_div_op関数にis_op32フラグを追加して、 is_op321なら値をDIV_WIDTHビットに拡張したものに変更します (リスト37)。

▼リスト9.37: 除数、被除数を32ビットの値にする (muldivunit.veryl) 差分をみる

veryl
function generate_div_op (
    is_op32: input logic      ,
    funct3 : input logic<3>   ,
    value  : input logic<XLEN>,
) -> logic<DIV_WIDTH> {
    return case funct3[1:0] {
        2'b00, 2'b10: abs::<DIV_WIDTH>(if is_op32 ? sext::<32, DIV_WIDTH>(value[31:0]) : value), // DIV, REM
        2'b01, 2'b11: if is_op32 ? {1'b0 repeat DIV_WIDTH - 32, value[31:0]} : value, // DIVU, REMU
        default     : 0,
    };
}

let du_dividend: logic<DIV_WIDTH> = generate_div_op(is_op32, funct3, op1);
let du_divisor : logic<DIV_WIDTH> = generate_div_op(is_op32, funct3, op2);

符号付き除算のオーバーフローとゼロ除算の判定をis_op32で変更します (リスト38)。

▼リスト9.38: 32ビット演算のときの例外的な処理に対応する (muldivunit.veryl) 差分をみる

veryl
always_comb {
    if is_op32 {
        du_signed_overflow = !funct3[0] && op1[31] == 1 && op1[31:0] == 0 && &op2[31:0];
        du_signed_divzero  = !funct3[0] && op2[31:0] == 0;
    } else {
        du_signed_overflow = !funct3[0] && op1[msb] == 1 && op1[msb - 1:0] == 0 && &op2;
        du_signed_divzero  = !funct3[0] && op2 == 0;
    }
    du_signed_error = du_signed_overflow || du_signed_divzero;
}

最後に、32ビットの結果をXLENビットに符号拡張します (リスト39)。 符号付き、符号無し演算のどちらも32ビットの結果を符号拡張したものが結果になります。

▼リスト9.39: 32ビット演算のとき、結果を符号拡張する (muldivunit.veryl) 差分をみる

veryl
} else if !is_mul && du_rvalid {
    let quo_signed: logic<DIV_WIDTH> = if op1sign_saved != op2sign_saved ? ~du_quotient + 1 : du_quotient;
    let rem_signed: logic<DIV_WIDTH> = if op1sign_saved == 1 ? ~du_remainder + 1 : du_remainder;
    let resultX   : UIntX            = case funct3_saved[1:0] {
        2'b00  : quo_signed[XLEN - 1:0], // DIV
        2'b01  : du_quotient[XLEN - 1:0], // DIVU
        2'b10  : rem_signed[XLEN - 1:0], // REM
        2'b11  : du_remainder[XLEN - 1:0], // REMU
        default: 0,
    };
    state  = State::Finish;
    result = if is_op32_saved ? sext::<32, 64>(resultX[31:0]) : resultX;
}

riscv-testsのrv64um-p-から始まるテストを実行し、成功することを確認してください。

これでM拡張を実装できました。


  1. 手動で何をどのように利用するかを選択することもできます。既に用意された回路(IP)を使うこともできますが、本書は自作することを主軸としているため利用しません。 ↩︎

  2. そもそも除算器が搭載されていない場合があります。 ↩︎

  3. 「効率」は、計算に要する時間やスループット、回路面積のことです。効率的に計算する方法については応用編で検討します。 ↩︎