Verylで作るCPU
Star

第4章
Zicsr拡張の実装

4.1 CSRとは何か?

前の章では、RISC-Vの基本整数命令セットであるRV32Iを実装しました。既に簡単なプログラムを動かせますが、例外や割り込み、ページングなどの機能がありません*1。このような機能はCSRを介して提供されます。

[*1] それぞれの機能は実装するときに解説します

RISC-Vには、CSR(Control and Status Register)というレジスタが4096個存在しています。例えばmtvecというレジスタは、例外や割り込みが発生したときのジャンプ先のアドレスを格納しています。RISC-VのCPUは、CSRの読み書きによって制御(Control)や状態(Status)の読み取りを行います。

CSRの読み書きを行う命令は、Zicsr拡張によって定義されています(表4.1)。本章では、Zicsrに定義されている命令、RV32Iに定義されているECALL命令、MRET命令、mtvecレジスタ、mepcレジスタ、mcauseレジスタを実装します。

表4.1: Zicsr拡張に定義されている命令

命令作用
CSRRWCSRにrs1を書き込み、元のCSRの値をrdに書き込む
CSRRWICSRRWのrs1を、即値をゼロ拡張した値に置き換えた動作
CSRRSCSRとrs1をビットORした値をCSRに書き込み、元のCSRの値をrdに書き込む
CSRRSICSRRSのrs1を、即値をゼロ拡張した値に置き換えた動作
CSRRCCSRと~rs1(rs1のビットNOT)をビットANDした値をCSRに書き込み、
元のCSRの値をrdに書き込む
CSRRCICSRRCのrs1を、即値をゼロ拡張した値に置き換えた動作

4.2 CSR命令のデコード

まず、Zicsrに定義されている命令(表4.1)をデコードします。

これらの命令のopcodeはSYSTEM(7'b1110011)です。この値をeeiパッケージに定義します(リスト4.1)。

リスト4.1: リスト4.1: opcode用の定数の定義 (eei.veryl)
    const OP_SYSTEM: logic<7> = 7'b1110011;

次に、InstCtrl構造体に、CSRを制御する命令であることを示すis_csrフラグを追加します(リスト4.2)。

リスト4.2: リスト4.2: is_csrを追加する (corectrl.veryl)
    // 制御に使うフラグ用の構造体
    struct InstCtrl {
        itype   : InstType   , // 命令の形式
        rwb_en  : logic      , // レジスタに書き込むかどうか
        is_lui  : logic      , // LUI命令である
        is_aluop: logic      , // ALUを利用する命令である
        is_jump : logic      , // ジャンプ命令である
        is_load : logic      , // ロード命令である
        is_csr  : logic      , // CSR命令である
        funct3  : logic   <3>, // 命令のfunct3フィールド
        funct7  : logic   <7>, // 命令のfunct7フィールド
    }

これでデコード処理を書く準備が整いました。inst_decoderモジュールのInstCtrlを生成している部分を変更します(リスト4.3)。

リスト4.3: リスト4.3: OP_SYSTEMとis_csrを追加する (inst_decoder.veryl)
                                           is_csrを追加
    ctrl = {case op {                           ↓
        OP_LUI   : {InstType::U, T, T, F, F, F, F},
        OP_AUIPC : {InstType::U, T, F, F, F, F, F},
        OP_JAL   : {InstType::J, T, F, F, T, F, F},
        OP_JALR  : {InstType::I, T, F, F, T, F, F},
        OP_BRANCH: {InstType::B, F, F, F, F, F, F},
        OP_LOAD  : {InstType::I, T, F, F, F, T, F},
        OP_STORE : {InstType::S, F, F, F, F, F, F},
        OP_OP    : {InstType::R, T, F, T, F, F, F},
        OP_OP_IMM: {InstType::I, T, F, T, F, F, F},
        OP_SYSTEM: {InstType::I, T, F, F, F, F, T},
        default  : {InstType::X, F, F, F, F, F, F},
    }, f3, f7};

リスト4.3では、opcodeがOP_SYSTEMな命令を、I形式、レジスタに結果を書き込む、CSRを操作する命令であるということにしています。他のopcodeの命令はCSRを操作しない命令であるということにしています。

CSRRW、CSRRS、CSRRC命令は、rs1レジスタの値を利用します。CSRRWI、CSRRSI、CSRRCI命令は、命令のビット列中のrs1にあたるビット列(5ビット)を0で拡張した値を利用します。それぞれの命令はfunct3で区別できます(表4.2)。

表4.2: Zicsrに定義されている命令(funct3による区別)

funct3命令
3'b001CSRRW
3'b101CSRRWI
3'b010CSRRS
3'b110CSRRSI
3'b011CSRRC
3'b111CSRRCI

操作対象のCSRのアドレス(12ビット)は、命令のビットの上位12ビット(I形式の即値)をそのまま利用します。

4.3 csrunitモジュールの実装

CSRを操作する命令のデコードができたので、CSR関連の処理を行うモジュールを作成します。

4.3.1 csrunitモジュールを作成する

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

リスト4.4: リスト4.4: csrunit.veryl
import eei::*;
import corectrl::*;

module csrunit (
    clk     : input  clock       ,
    rst     : input  reset       ,
    valid   : input  logic       ,
    ctrl    : input  InstCtrl    ,
    csr_addr: input  logic   <12>,
    rs1     : input  UIntX       ,
    rdata   : output UIntX       ,
) {
    // CSRR(W|S|C)[I]命令かどうか
    let is_wsc: logic = ctrl.is_csr && ctrl.funct3[1:0] != 0;
}

csrunitモジュールの主要なポートの定義は表4.3のとおりです。まだcsrunitモジュールにはCSRが一つもないため、中身が空になっています。

表4.3: csrunitモジュールのポート定義

ポート名向き意味
validlogicinput命令が供給されているかどうか
ctrlInstCtrlinput命令のInstCtrl
csr_addrlogic<12>input命令が指定するCSRのアドレス (命令の上位12ビット)
rs1UIntXinputCSRR(W|S|C)のときrs1の値、
CSRR(W|S|C)Iのとき即値(5ビット)をゼロで拡張した値
rdataUIntXoutputCSR命令よるCSR読み込みの結果

csrunitモジュールを、coreモジュールの中でインスタンス化します(リスト4.5)。

リスト4.5: リスト4.5: csrunitモジュールのインスタンス化 (core.veryl)
    var csru_rdata: UIntX;

    inst csru: csrunit (
        clk                       ,
        rst                       ,
        valid   : inst_valid      ,
        ctrl    : inst_ctrl       ,
        csr_addr: inst_bits[31:20],
        rs1     : if inst_ctrl.funct3[2] == 1 && inst_ctrl.funct3[1:0] != 0 {
            {1'b0 repeat XLEN - $bits(rs1_addr), rs1_addr} // rs1を0で拡張する
        } else {
            rs1_data
        },
        rdata: csru_rdata,
    );

CSR命令の結果の受け取りのために変数csru_rdataを作成し、csrunitモジュールをインスタンス化しています。

csr_addrポートには命令の上位12ビットを設定しています。rs1ポートには、即値を利用する命令(CSRR(W|S|C)I)の場合はrs1_addrを0で拡張した値を、それ以外の命令の場合はrs1のデータを設定しています。

次に、CSRを読み込んだデータをレジスタにライトバックします。具体的には、InstCtrl.is_csr1のとき、wb_datacsru_rdataになるようにします(リスト4.6)。

リスト4.6: リスト4.6: CSR命令の結果がライトバックされるようにする (core.veryl)
    let rd_addr: logic<5> = inst_bits[11:7];
    let wb_data: UIntX    = if inst_ctrl.is_lui {
        inst_imm
    } else if inst_ctrl.is_jump {
        inst_pc + 4
    } else if inst_ctrl.is_load {
        memu_rdata
    } else if inst_ctrl.is_csr {
        csru_rdata
    } else {
        alu_result
    };

最後に、デバッグ用の表示を追加します。デバッグ表示用のalways_ffブロックに、次のコードを追加してください(リスト4.7)。

リスト4.7: リスト4.7: rdataをデバッグ表示する (core.veryl)
    if inst_ctrl.is_csr {
        $display("  csr rdata : %h", csru_rdata);
    }

これらのテストは、csrunitモジュールにCSRを追加してから行います。

4.3.2 mtvecレジスタを実装する

csrunitモジュールには、まだCSRが定義されていません。1つ目のCSRとして、mtvecレジスタを実装します。

mtvecレジスタ、トラップ

mtvecのエンコーディング<a href="bib.html#bib-isa-manual.2.fig10">[9]</a>

図4.1: mtvecのエンコーディング[9]

mtvecレジスタは、仕様書[10]に定義されています。mtvecは、MXLENビットのWARLなレジスタです。mtvecのアドレスは12'h305です。

MXLENはmisaレジスタに定義されていますが、今のところはXLENと等しいという認識で問題ありません。WARLはWrite Any Values, Reads Legal Valuesの略です。その名の通り、好きな値を書き込めますが読み出すときには合法な値*2になっているという認識で問題ありません。

[*2] 合法な値とは実装がサポートしている有効な値のことです

mtvecは、トラップ(Trap)が発生したときのジャンプ先(Trap-Vector)の基準となるアドレスを格納するレジスタです。トラップとは、例外(Exception)、または割り込み(Interrupt)により、CPUの制御を変更することです*3。トラップが発生するとき、CPUはCSRを変更した後、mtvecに格納されたアドレスにジャンプします。

例外とは、命令の実行によって引き起こされる異常な状態(unusual condition)のことです。例えば、不正な命令を実行しようとしたときにはIllegal Instruction例外が発生します。CPUは、例外が発生したときのジャンプ先(対処方法)を決めておくことで、CPUが異常な状態に陥ったままにならないようにしています。

mtvecはBASEとMODEの2つのフィールドで構成されています。MODEはジャンプ先の決め方を指定するためのフィールドですが、簡単のために常に2'b00(Directモード)になるようにします。Directモードのとき、トラップ時のジャンプ先はBASE << 2になります。

[*3] トラップや例外、割り込みはVolume Iの1.6Exceptions, Traps, and Interruptsに定義されています

mtvecレジスタの実装

それでは、mtvecレジスタを実装します。まず、CSRのアドレスを表す列挙型を定義します(リスト4.8)。

リスト4.8: リスト4.8: CsrAddr型を定義する (csrunit.veryl)
    // CSRのアドレス
    enum CsrAddr: logic<12> {
        MTVEC = 12'h305,
    }

次に、mtvecレジスタを作成します。MXLEN=XLENとしているので、型はUIntXにします(リスト4.9)。

リスト4.9: リスト4.9: mtvecレジスタの定義 (csrunit.veryl)
    // CSR
    var mtvec: UIntX;

MODEはDirectモード(2'b00)しか対応していません。mtvecはWARLなレジスタなので、MODEフィールドには書き込めないようにする必要があります。これを制御するためにmtvecレジスタの書き込みマスク用の定数を定義します(リスト4.10)。

リスト4.10: リスト4.10: mtvecレジスタの書き込みマスクの定義 (csrunit.veryl)
    // wmasks
    const MTVEC_WMASK: UIntX = 'hffff_fffc;

次に、書き込むデータwdataの生成と、mtvecレジスタの読み込みを実装します(リスト4.11)。

リスト4.11: リスト4.11: レジスタの読み込みと書き込むデータの作成 (csrunit.veryl)
    var wmask: UIntX; // write mask
    var wdata: UIntX; // write data

     always_comb {
        // read
        rdata = case csr_addr {
            CsrAddr::MTVEC: mtvec,
            default       : 'x,
        };
        // write
        wmask = case csr_addr {
            CsrAddr::MTVEC: MTVEC_WMASK,
            default       : 0,
        };
        wdata = case ctrl.funct3[1:0] {
            2'b01  : rs1,
            2'b10  : rdata | rs1,
            2'b11  : rdata & ~rs1,
            default: 'x,
        } & wmask;
    }

always_combブロックで、rdataポートにcsr_addrに応じたCSRの値を割り当てます。wdataには、CSRに書き込むデータを割り当てます。CSRに書き込むデータは、書き込む命令(CSRRW[I]、CSRRS[I]、CSRRC[I])によって異なります。rs1ポートにはrs1の値か即値が供給されているため、これとrdataを利用してwdataを生成しています。funct3と演算の種類の関係は表4.2を参照してください。

最後に、mtvecレジスタへの書き込み処理を実装します。mtvecへの書き込みは、命令がCSR命令である場合(is_wsc)にのみ行います(リスト4.12)。

リスト4.12: リスト4.12: CSRへの書き込み処理 (csrunit.veryl)
    always_ff {
        if_reset {
            mtvec = 0;
        } else {
            if valid {
                if is_wsc {
                    case csr_addr {
                        CsrAddr::MTVEC: mtvec = wdata;
                        default       : {}
                    }
                }
            }
        }
    }

mtvecの初期値は0です。mtvecにwdataを書き込むとき、MODEが常に2'b00になります。

4.3.3 csrunitモジュールをテストする

mtvecレジスタの書き込み、読み込みができることを確認します。

test/sample_csr.hexを作成し、次のように記述します(リスト4.13)。

リスト4.13: リスト4.13: sample_csr.hex
305bd0f3 // 0: csrrwi x1, mtvec, 0b10111
30502173 // 4: csrrs  x2, mtvec, x0

テストでは、CSRRWI命令でmtvecに32'b10111を書き込んだ後、CSRRS命令でmtvecの値を読み込みます。CSRRS命令で読み込むとき、rs1をx0(ゼロレジスタ)にすることで、mtvecの値を変更せずに読み込みます。

シミュレータを実行し、結果を確かめます(リスト4.14)。

リスト4.14: リスト4.14: mtvecの読み込み/書き込みテストの実行
$ make build
$ make sim
$ ./obj_dir/sim test/sample_csr.hex 5
#                    4
00000000 : 305bd0f3 ← mtvecに32'b10111を書き込む
  itype     : 000010
  rs1[23]   : 00000000 ← CSRRWIなので、mtvecに32'b10111(=23)を書き込む
  csr rdata : 00000000 ← mtvecの初期値(0)が読み込まれている
  reg[ 1] <= 00000000
#                    5
00000004 : 30502173 ← mtvecを読み込む
  itype     : 000010
  csr rdata : 00000014 ← mtvecに書き込まれた値を読み込んでいる
  reg[ 2] <= 00000014 ← 32'b10111のMODE部分がマスクされて、32'b10100 = 14になっている

mtvecのBASEフィールドにのみ書き込みが行われ、32'h00000014が読み込まれることを確認できます。

4.4 ECALL命令の実装

せっかくmtvecレジスタを実装したので、これを使う命令を実装します。

4.4.1 ECALL命令とは何か?

RV32Iには、意図的に例外を発生させる命令としてECALL命令が定義されています。ECALL命令を実行すると、現在の権限レベル(Privilege Level)に応じて表4.4のような例外が発生します。

権限レベルとは、権限(特権)を持つソフトウェアを実装するための機能です。例えばOS上で動くソフトウェアは、セキュリティのために、他のソフトウェアのメモリを侵害できないようにする必要があります。権限レベル機能があると、このような保護を、権限のあるOSが権限のないソフトウェアを管理するという風に実現できます。

権限レベルはいくつか定義されていますが、本章では最高の権限レベルであるMachineレベル(M-mode)しかないものとします。

表4.4: 権限レベルとECALLによる例外

権限レベルECALLによって発生する例外
MEnvironment call from M-mode
SEnvironment call from S-mode
UEnvironment call from U-mode

mcause、mepcレジスタ

ECALL命令を実行すると例外が発生します。例外が発生するとmtvecにジャンプし、例外が発生した時の処理を行います。これだけでもいいのですが、例外が発生したときに、どこで(PC)、どのような例外が発生したのかを知りたいことがあります。これを知るために、RISC-Vには、どこで例外が発生したかを格納するmepcレジスタと、例外の発生原因を格納するmcauseレジスタが存在しています。

CPUは例外が発生すると、mtvecにジャンプする前に、mepcに現在のPC、mcauseに発生原因を格納します。これにより、mtvecにジャンプしてから例外に応じた処理を実行できるようになります。

例外の発生原因は数値で表現されており、Environment call from M-mode例外には11が割り当てられています。

4.4.2 トラップを実装する

それでは、ECALL命令とトラップの仕組みを実装します。

定数の定義

まず、mepcとmcauseのアドレスをCsrAddr型に追加します(リスト4.15)。

リスト4.15: リスト4.15: mepcとmcauseのアドレスを追加する (csrunit.veryl)
    // CSRのアドレス
    enum CsrAddr: logic<12> {
        MTVEC = 12'h305,
        MEPC = 12'h341,
        MCAUSE = 12'h342,
    }

次に、トラップの発生原因を表現する型CsrCauseを定義します。今のところ、発生原因はECALL命令によるEnvironment Call From M-mode例外しかありません(リスト4.16)。

リスト4.16: リスト4.16: CsrCause型の定義 (csrunit.veryl)
    enum CsrCause: UIntX {
        ENVIRONMENT_CALL_FROM_M_MODE = 11,
    }

最後に、mepcとmcauseの書き込みマスクを定義します(リスト4.17)。mepcに格納されるのは例外が発生した時の命令のアドレスです。命令は4バイトに整列して配置されているため、mepcの下位2ビットは常に2'b00になるようにします。

リスト4.17: リスト4.17: mepcとmcauseの書き込みマスクの定義 (csrunit.veryl)
    const MTVEC_WMASK : UIntX = 'hffff_fffc;
    const MEPC_WMASK  : UIntX = 'hffff_fffc;
    const MCAUSE_WMASK: UIntX = 'hffff_ffff;

mepcとmcauseレジスタの実装

mepcとmcauseレジスタを作成します。サイズはMXLEN(=XLEN)なため、型はUIntXとします(リスト4.18)。

リスト4.18: リスト4.18: mepcとmcauseレジスタの定義 (csrunit.veryl)
    // CSR
    var mtvec : UIntX;
    var mepc  : UIntX;
    var mcause: UIntX;

次に、mepcとmcauseの読み込み処理と、書き込みマスクの割り当てを実装します。どちらもcase文にアドレスと値のペアを追加するだけです(リスト4.19リスト4.20)。

リスト4.19: リスト4.19: mepcとmcauseの読み込み (csrunit.veryl)
    rdata = case csr_addr {
        CsrAddr::MTVEC : mtvec,
        CsrAddr::MEPC  : mepc,
        CsrAddr::MCAUSE: mcause,
        default        : 'x,
    };
リスト4.20: リスト4.20: mepcとmcauseの書き込みマスクの設定 (csrunit.veryl)
    wmask = case csr_addr {
        CsrAddr::MTVEC : MTVEC_WMASK,
        CsrAddr::MEPC  : MEPC_WMASK,
        CsrAddr::MCAUSE: MCAUSE_WMASK,
        default        : 0,
    };

最後に、mepcとmcauseの書き込みを実装します。if_resetで値を0に初期化し、case文にmepcとmcauseの場合を実装します(リスト4.21)。

リスト4.21: リスト4.21: mepcとmcauseの書き込み (csrunit.veryl)
always_ff {
    if_reset {
        mtvec  = 0;
        mepc   = 0;
        mcause = 0;
    } else {
        if valid {
            if is_wsc {
                case csr_addr {
                    CsrAddr::MTVEC : mtvec  = wdata;
                    CsrAddr::MEPC  : mepc   = wdata;
                    CsrAddr::MCAUSE: mcause = wdata;
                    default        : {}
                }
            }
        }
    }
}

例外の実装

ECALL命令と、それによって発生するトラップを実装します。まず、csrunitモジュールにポートを追加します(リスト4.22)。

リスト4.22: リスト4.22: csrunitモジュールにポートを追加する (csrunit.veryl)
module csrunit (
    clk        : input  clock       ,
    rst        : input  reset       ,
    valid      : input  logic       ,
    pc         : input  Addr        ,
    ctrl       : input  InstCtrl    ,
    rd_addr    : input  logic   <5> ,
    csr_addr   : input  logic   <12>,
    rs1        : input  UIntX       ,
    rdata      : output UIntX       ,
    raise_trap : output logic       ,
    trap_vector: output Addr        ,
) {

それぞれの用途は次の通りです。

pc
現在処理している命令のアドレスを受け取ります。
例外が発生するとき、mepcにPCを格納するために使います。
rd_addr
現在処理している命令のrdの番号を受け取ります。
命令がECALL命令かどうかを判定するために使います。
raise_trap
例外が発生するとき、値を1にします。
trap_vector
例外が発生するとき、ジャンプ先のアドレスを出力します。

csrunitモジュールの中身を実装する前に、coreモジュールに例外発生時の動作を実装します。

csrunitモジュールと接続するための変数を定義してcsrunitモジュールと接続します(リスト4.23リスト4.24)。

リスト4.23: リスト4.23: csrunitモジュールのポートの定義を変更する ① (core.veryl)
    var csru_rdata      : UIntX;
    var csru_raise_trap : logic;
    var csru_trap_vector: Addr ;
リスト4.24: リスト4.24: csrunitモジュールのポートの定義を変更する ② (core.veryl)
    inst csru: csrunit (
        clk                       ,
        rst                       ,
        valid   : inst_valid      ,
        pc      : inst_pc         ,
        ctrl    : inst_ctrl       ,
        rd_addr                   ,
        csr_addr: inst_bits[31:20],
        rs1     : if inst_ctrl.funct3[2] == 1 && inst_ctrl.funct3[1:0] != 0 {
            {1'b0 repeat XLEN - $bits(rs1_addr), rs1_addr} // rs1を0で拡張する
        } else {
            rs1_data
        },
        rdata      : csru_rdata,
        raise_trap : csru_raise_trap,
        trap_vector: csru_trap_vector,
    );

次に、トラップするときにトラップ先にジャンプさせます。

例外が発生するとき、csru_raise_trap1になり、csru_trap_vectorがトラップ先になります。トラップするときの動作には、ジャンプと分岐命令の仕組みを利用します。control_hazardの条件にcsru_raise_trapを追加して、トラップするときにcontrol_hazard_pc_nextcsru_trap_vectorに設定します(リスト4.25)。

リスト4.25: リスト4.25: 例外の発生時にジャンプさせる (core.veryl)
    assign control_hazard = inst_valid && (
        csru_raise_trap ||
        inst_ctrl.is_jump ||
        inst_is_br(inst_ctrl) && brunit_take
    );
    assign control_hazard_pc_next = if csru_raise_trap {
        csru_trap_vector ← トラップするとき、trap_vectorに飛ぶ
    } else if inst_is_br(inst_ctrl) {
        inst_pc + inst_imm
    } else {
        alu_result
    };
ECALL命令のフォーマット<a href="bib.html#bib-isa-manual.1.37">[6]</a>

図4.2: ECALL命令のフォーマット[6]

それでは、csrunitモジュールにトラップの処理を実装します。

ECALL命令は、I形式、即値は0、rs1とrdは0、funct3は0、opcodeはSYSTEMな命令です(図4.2)。これを判定するための変数を作成します(リスト4.26)。

リスト4.26: リスト4.26: ECALL命令かどうかの判定 (csrunit.veryl)
    // ECALL命令かどうか
    let is_ecall: logic = ctrl.is_csr && csr_addr == 0 && rs1[4:0] == 0 && ctrl.funct3 == 0 && rd_addr == 0;

次に、例外が発生するかどうかを示すraise_exptと、例外の発生の原因を示すexpt_causeを作成します。今のところ、例外はECALL命令によってのみ発生するため、expt_causeは実質的に定数になっています(リスト4.27)。

リスト4.27: リスト4.27: 例外とトラップの判定 (csrunit.veryl)
    // Exception
    let raise_expt: logic = valid && is_ecall;
    let expt_cause: UIntX = CsrCause::ENVIRONMENT_CALL_FROM_M_MODE;

    // Trap
    assign raise_trap  = raise_expt;
    let trap_cause : UIntX = expt_cause;
    assign trap_vector = mtvec;

トラップが発生するかどうかを示すraise_trapには、例外が発生するかどうかを割り当てます。トラップの原因を示すtrap_causeには、例外の発生原因を割り当てます。また、トラップ先にはmtvecを割り当てます。

最後に、トラップに伴うCSRの変更を実装します。トラップが発生するとき、mepcレジスタにPC、mcauseレジスタにトラップの発生原因を格納します(リスト4.28)。

リスト4.28: リスト4.28: トラップが発生したらCSRを変更する (csrunit.veryl)
always_ff {
    if_reset {
        ...
    } else {
        if valid {
            if raise_trap { ← トラップ時の動作
                mepc   = pc;
                mcause = trap_cause;
            } else {
                if is_wsc {
                    ...

4.4.3 ECALL命令をテストする

ECALL命令をテストする前に、デバッグのために$displayシステムタスクで、例外が発生したかどうかと、トラップ先を表示します(リスト4.29)。

リスト4.29: リスト4.29: トラップの情報をデバッグ表示する (core.veryl)
    if inst_ctrl.is_csr {
        $display("  csr rdata : %h", csru_rdata);
        $display("  csr trap  : %b", csru_raise_trap);
        $display("  csr vec   : %h", csru_trap_vector);
    }

test/sample_ecall.hexを作成し、次のように記述します(リスト4.30)。

リスト4.30: リスト4.30: sample_ecall.hex
30585073 //  0: csrrwi x0, mtvec, 0x10
00000073 //  4: ecall
00000000 //  8:
00000000 //  c:
342020f3 // 10: csrrs x1, mcause, x0
34102173 // 14: csrrs x2, mepc, x0

CSRRWI命令でmtvecレジスタに値を書き込み、ECALL命令で例外を発生させてジャンプします。ジャンプ先では、mcauseレジスタとmepcレジスタの値を読み取ります。

シミュレータを実行し、結果を確かめます(リスト4.31)。

リスト4.31: リスト4.31: ECALL命令のテストの実行
$ make build
$ make sim
$ ./obj_dir/sim test/sample_ecall.hex 10
#                    4
00000000 : 30585073 ← CSRRWIでmtvecに書き込み
  rs1[16]   : 00000000 ← 10(=16)をmtvecに書き込む
  csr trap  : 0
  csr vec   : 00000000
  reg[ 0] <= 00000000
#                    5
00000004 : 00000073
  csr trap  : 1 ← ECALL命令により、例外が発生する
  csr vec   : 00000010 ← ジャンプ先は0x10
  reg[ 0] <= 00000000
#                    9
00000010 : 342020f3
  csr rdata : 0000000b ← CSRRSでmcauseを読み込む
  reg[ 1] <= 0000000b ← Environment call from M-modeなのでb(=11)
#                   10
00000014 : 34102173
  csr rdata : 00000004 ← CSRRSでmepcを読み込む
  reg[ 2] <= 00000004 ← 例外はアドレス4で発生したので4

ECALL命令によって例外が発生し、mcauseとmepcに書き込みが行われてからmtvecにジャンプしていることを確認できます。

ECALL命令の実行時にレジスタに値がライトバックされてしまっていますが、ECALL命令のrdは常に0番目のレジスタであり、0番目のレジスタは常に値が0になるため問題ありません。

4.5 MRET命令の実装

MRET命令*4は、トラップ先からトラップ元に戻るための命令です。MRET命令を実行すると、mepcレジスタに格納されたアドレスにジャンプします*5。例えば、権限のあるOSから権限のないユーザー空間に戻るために利用します。

[*4] MRET命令はVolume IIの3.3.2. Trap-Return Instructionsに定義されています

[*5] 他のCSRや権限レベルが実装されている場合は、他にも行うことがあります

4.5.1 MRET命令を実装する

MRET命令のフォーマット<a href="bib.html#bib-isa-manual.2.15">[11]</a>

図4.3: MRET命令のフォーマット[11]

まず、csrunitモジュールに供給されている命令がMRET命令かどうかを判定する変数is_mretを作成します(リスト4.32)。MRET命令は、上位12ビットは12'b001100000010、rs1は0、funct3は0、rdは0です(図4.3)。

リスト4.32: リスト4.32: MRET命令の判定 (csrunit.veryl)
    // MRET命令かどうか
    let is_mret: logic = ctrl.is_csr && csr_addr == 12'b0011000_00010 && rs1[4:0] == 0 && ctrl.funct3 == 0 && rd_addr == 0;

次に、csrunitモジュールにMRET命令が供給されているときにmepcにジャンプする仕組みを実装します。ジャンプするための仕組みには、トラップによってジャンプする仕組みを利用します(リスト4.33)。raise_trapis_mretを追加し、トラップ先も変更します。

リスト4.33: リスト4.33: MRET命令によってジャンプさせる (csrunit.veryl)
    // Trap
    assign raise_trap  = raise_expt || (valid && is_mret);
    let trap_cause : UIntX = expt_cause;
    assign trap_vector = if raise_expt {
        mtvec
    } else {
        mepc
    };
例外が優先

trap_vectorには、is_mretのときにmepcを割り当てるのではなく、raise_exptのときにmtvecを割り当てています。これは、MRET命令によって発生する例外があるからです。MRET命令の判定を優先すると、例外が発生するのにmepcにジャンプしてしまいます。

4.5.2 MRET命令をテストする

mepcに値を設定してからMRET命令を実行することでmepcにジャンプするようなテストを作成します(リスト4.34)。

リスト4.34: リスト4.34: sample_mret.hex
34185073 //  0: csrrwi x0, mepc, 0x10
30200073 //  4: mret
00000000 //  8:
00000000 //  c:
00000013 // 10: addi x0, x0, 0

シミュレータを実行し、結果を確かめます(リスト4.35)。

リスト4.35: リスト4.35: MRET命令のテストの実行
$ make build
$ make sim
$ ./obj_dir/sim test/sample_mret.hex 9
#                    4
00000000 : 34185073 ← CSRRWIでmepcに書き込み
  rs1[16]   : 00000000 ← 0x10(=16)をmepcに書き込む
  csr trap  : 0
  csr vec   : 00000000
  reg[ 0] <= 00000000
#                    5
00000004 : 30200073
  csr trap  : 1 ← MRET命令によってmepcにジャンプする
  csr vec   : 00000010 ← 10にジャンプする
#                    9
00000010 : 00000013 ← 10にジャンプしている

MRET命令によってmepcにジャンプすることを確認できます。

MRET命令はレジスタに値をライトバックしていますが、ECALL命令と同じく0番目のレジスタが指定されるため問題ありません。