Zicsr拡張の実装
CSRとは何か?
前の章では、RISC-Vの基本整数命令セットであるRV32Iを実装しました。 既に簡単なプログラムを動かせますが、 例外や割り込み、ページングなどの機能がありません[1]。 このような機能はCSRを介して提供されます。
RISC-Vには、CSR(Control and Status Register)というレジスタが4096個存在しています。 例えばmtvecというレジスタは、例外や割り込みが発生したときのジャンプ先のアドレスを格納しています。 RISC-VのCPUは、CSRの読み書きによって制御(Control)や状態(Status)の読み取りを行います。
CSRの読み書きを行う命令は、Zicsr拡張によって定義されています(表1)。 本章では、Zicsrに定義されている命令、 RV32Iに定義されているECALL命令、 MRET命令、 mtvecレジスタ、 mepcレジスタ、 mcauseレジスタを実装します。
表4.1: Zicsr拡張に定義されている命令
| 命令 | 作用 |
|---|---|
| CSRRW | CSRにrs1を書き込み、元のCSRの値をrdに書き込む |
| CSRRWI | CSRRWのrs1を、即値をゼロ拡張した値に置き換えた動作 |
| CSRRS | CSRとrs1をビットORした値をCSRに書き込み、元のCSRの値をrdに書き込む |
| CSRRSI | CSRRSのrs1を、即値をゼロ拡張した値に置き換えた動作 |
| CSRRC | CSRと~rs1(rs1のビットNOT)をビットANDした値をCSRに書き込み、 元のCSRの値をrdに書き込む |
| CSRRCI | CSRRCのrs1を、即値をゼロ拡張した値に置き換えた動作 |
CSR命令のデコード
まず、Zicsrに定義されている命令(表1)をデコードします。
これらの命令のopcodeはSYSTEM(7'b1110011)です。 この値をeeiパッケージに定義します(リスト1)。
▼リスト4.1: opcode用の定数の定義 (eei.veryl) 差分をみる
const OP_SYSTEM: logic<7> = 7'b1110011;
次に、InstCtrl構造体に、 CSRを制御する命令であることを示すis_csrフラグを追加します(リスト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を生成している部分を変更します(リスト3)。
▼リスト4.3: OP_SYSTEMとis_csrを追加する (inst_decoder.veryl) 差分をみる
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
};
リスト3では、 opcodeがOP_SYSTEMな命令を、 I形式、レジスタに結果を書き込む、CSRを操作する命令であるということにしています。 他のopcodeの命令はCSRを操作しない命令であるということにしています。
CSRRW、CSRRS、CSRRC命令は、 rs1レジスタの値を利用します。 CSRRWI、CSRRSI、CSRRCI命令は、 命令のビット列中のrs1にあたるビット列(5ビット)を0で拡張した値を利用します。 それぞれの命令はfunct3で区別できます(表2)。
表4.2: Zicsrに定義されている命令(funct3による区別)
| funct3 | 命令 |
|---|---|
| 3'b001 | CSRRW |
| 3'b101 | CSRRWI |
| 3'b010 | CSRRS |
| 3'b110 | CSRRSI |
| 3'b011 | CSRRC |
| 3'b111 | CSRRCI |
csrunitモジュールの実装
CSRを操作する命令のデコードができたので、 CSR関連の処理を行うモジュールを作成します。
csrunitモジュールを作成する
src/csrunit.verylを作成し、 次のように記述します(リスト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モジュールの主要なポートの定義は表3のとおりです。 まだcsrunitモジュールにはCSRが一つもないため、中身が空になっています。
表4.3: csrunitモジュールのポート定義
| ポート名 | 型 | 向き | 意味 |
|---|---|---|---|
| valid | logic | input | 命令が供給されているかどうか |
| ctrl | InstCtrl | input | 命令のInstCtrl |
| csr_addr | logic<12> | input | 命令が指定するCSRのアドレス (命令の上位12ビット) |
| rs1 | UIntX | input | CSRR(W|S|C)のときrs1の値、 CSRR(W|S|C)Iのとき即値(5ビット)をゼロで拡張した値 |
| rdata | UIntX | output | CSR命令よるCSR読み込みの結果 |
▼リスト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で拡張する
: 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_csrが1のとき、 wb_dataがcsru_rdataになるようにします (リスト6)。
▼リスト4.6: CSR命令の結果がライトバックされるようにする (core.veryl) 差分をみる
let rd_addr: logic<5> = inst_bits[11:7];
let wb_data: UIntX = switch {
inst_ctrl.is_lui : inst_imm,
inst_ctrl.is_jump: inst_pc + 4,
inst_ctrl.is_load: memu_rdata,
inst_ctrl.is_csr : csru_rdata,
default : alu_result
};
最後に、デバッグ用の表示を追加します。 デバッグ表示用のalways_ffブロックに、 次のコードを追加してください(リスト7)。
▼リスト4.7: rdataをデバッグ表示する (core.veryl) 差分をみる
if inst_ctrl.is_csr {
$display(" csr rdata : %h", csru_rdata);
}
これらのテストは、 csrunitモジュールにCSRを追加してから行います。
mtvecレジスタを実装する
csrunitモジュールには、まだCSRが定義されていません。 1つ目のCSRとして、mtvecレジスタを実装します。
mtvecレジスタ、トラップ

▲図1: mtvecのエンコーディング
mtvecは、MXLENビットのWARLなレジスタです。 mtvecのアドレスは12'h305です。
MXLENはmisaレジスタに定義されていますが、 今のところはXLENと等しいという認識で問題ありません。 WARLはWrite Any Values, Reads Legal Valuesの略です。 その名の通り、好きな値を書き込めますが 読み出すときには合法な値[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になります。
mtvecレジスタの実装
それでは、mtvecレジスタを実装します。 まず、CSRのアドレスを表す列挙型を定義します (リスト8)。
▼リスト4.8: CsrAddr型を定義する (csrunit.veryl) 差分をみる
// CSRのアドレス
enum CsrAddr: logic<12> {
MTVEC = 12'h305,
}
次に、mtvecレジスタを作成します。 MXLEN=XLENとしているので、 型はUIntXにします (リスト9)。
▼リスト4.9: mtvecレジスタの定義 (csrunit.veryl) 差分をみる
// CSR
var mtvec: UIntX;
MODEはDirectモード(2'b00)しか対応していません。 mtvecはWARLなレジスタなので、 MODEフィールドには書き込めないようにする必要があります。 これを制御するためにmtvecレジスタの書き込みマスク用の定数を定義します (リスト10)。
▼リスト4.10: mtvecレジスタの書き込みマスクの定義 (csrunit.veryl) 差分をみる
// wmasks
const MTVEC_WMASK: UIntX = 'hffff_fffc;
次に、書き込むデータwdataの生成と、 mtvecレジスタの読み込みを実装します(リスト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 | (rdata & ~wmask);
}
always_combブロックで、 rdataポートにcsr_addrに応じたCSRの値を割り当てます。 wdataには、CSRに書き込むデータを割り当てます。 CSRに書き込むデータは、 書き込む命令(CSRRW[I]、CSRRS[I]、CSRRC[I])によって異なります。 rs1ポートにはrs1の値か即値が供給されているため、 これとrdataを利用してwdataを生成しています。 funct3と演算の種類の関係は 表2を参照してください。
最後に、mtvecレジスタへの書き込み処理を実装します。 mtvecへの書き込みは、 命令がCSR命令である場合(is_wsc)にのみ行います (リスト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になります。
csrunitモジュールをテストする
mtvecレジスタの書き込み、 読み込みができることを確認します。
test/sample_csr.hexを作成し、 次のように記述します(リスト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の値を変更せずに読み込みます。
シミュレータを実行し、結果を確かめます(リスト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が読み込まれることを確認できます。
ECALL命令の実装
せっかくmtvecレジスタを実装したので、これを使う命令を実装します。
ECALL命令とは何か?
RV32Iには、意図的に例外を発生させる命令としてECALL命令が定義されています。 ECALL命令を実行すると、 現在の権限レベル(Privilege Level)に応じて表4のような例外が発生します。
権限レベルとは、 権限(特権)を持つソフトウェアを実装するための機能です。 例えばOS上で動くソフトウェアは、 セキュリティのために、他のソフトウェアのメモリを侵害できないようにする必要があります。 権限レベル機能があると、このような保護を、 権限のあるOSが権限のないソフトウェアを管理するという風に実現できます。
権限レベルはいくつか定義されていますが、 本章では最高の権限レベルであるMachineレベル(M-mode)しかないものとします。
表4.4: 権限レベルとECALLによる例外
| 権限レベル | ECALLによって発生する例外 |
|---|---|
| M | Environment call from M-mode |
| S | Environment call from S-mode |
| U | Environment 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が割り当てられています。
トラップを実装する
それでは、ECALL命令とトラップの仕組みを実装します。
定数の定義
まず、mepcとmcauseのアドレスをCsrAddr型に追加します (リスト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例外しかありません (リスト16)。
▼リスト4.16: CsrCause型の定義 (csrunit.veryl) 差分をみる
enum CsrCause: UIntX {
ENVIRONMENT_CALL_FROM_M_MODE = 11,
}
最後に、mepcとmcauseの書き込みマスクを定義します (リスト17)。 mepcに格納されるのは例外が発生した時の命令のアドレスです。 命令は4バイトに整列して配置されているため、 mepcの下位2ビットは常に2'b00になるようにします。
▼リスト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とします (リスト18)。
▼リスト4.18: mepcとmcauseレジスタの定義 (csrunit.veryl) 差分をみる
// CSR
var mtvec : UIntX;
var mepc : UIntX;
var mcause: UIntX;
次に、mepcとmcauseの読み込み処理と、書き込みマスクの割り当てを実装します。 どちらもcase文にアドレスと値のペアを追加するだけです ( リスト19、 リスト20 )。
▼リスト4.19: mepcとmcauseの読み込み (csrunit.veryl) 差分をみる
rdata = case csr_addr {
CsrAddr::MTVEC : mtvec,
CsrAddr::MEPC : mepc,
CsrAddr::MCAUSE: mcause,
default : 'x,
};
▼リスト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の場合を実装します (リスト27)。
例外の実装
ECALL命令と、それによって発生するトラップを実装します。 まず、csrunitモジュールにポートを追加します (リスト21)。
▼リスト4.21: 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モジュールと接続します ( リスト22、 リスト23 )。
▼リスト4.22: csrunitモジュールのポートの定義を変更する ① (core.veryl) 差分をみる
var csru_rdata : UIntX;
var csru_raise_trap : logic;
var csru_trap_vector: Addr ;
▼リスト4.23: 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で拡張する
: rs1_data,
rdata : csru_rdata ,
raise_trap : csru_raise_trap ,
trap_vector: csru_trap_vector,
);
次に、トラップするときにトラップ先にジャンプさせます。
例外が発生するとき、 csru_raise_trapが1になり、 csru_trap_vectorがトラップ先になります。 トラップするときの動作には、 ジャンプと分岐命令の仕組みを利用します。 control_hazardの条件にcsru_raise_trapを追加して、 トラップするときにcontrol_hazard_pc_nextをcsru_trap_vectorに設定します (リスト24)。
▼リスト4.24: 例外の発生時にジャンプさせる (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 = switch {
csru_raise_trap : csru_trap_vector,← トラップするとき、trap_vectorに飛ぶ
inst_is_br(inst_ctrl): inst_pc + inst_imm,
default : alu_result & ~1
};
▲図2: ECALL命令のエンコーディング
それでは、csrunitモジュールにトラップの処理を実装します。
ECALL命令は、 I形式、 即値は0、 rs1とrdは0、 funct3は0、 opcodeはSYSTEMな命令です(図2)。 これを判定するための変数を作成します(リスト25)。
▼リスト4.25: 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は実質的に定数になっています (リスト26)。
▼リスト4.26: 例外とトラップの判定 (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レジスタにトラップの発生原因を格納します (リスト27)。
▼リスト4.27: CSR、トラップ発生時の処理を実装する (csrunit.veryl) 差分をみる
always_ff {
if_reset {
mtvec = 0;
mepc = 0; ← CSRの初期化
mcause = 0;
} else {
if valid {
if raise_trap { ← トラップが発生したときの処理
if raise_expt {
mepc = pc;
mcause = trap_cause;
}
} else {
if is_wsc {
case csr_addr {
CsrAddr::MTVEC : mtvec = wdata;
CsrAddr::MEPC : mepc = wdata; ← CSRの書き込み
CsrAddr::MCAUSE: mcause = wdata;
default : {}
}
}
}
}
}
}
ECALL命令をテストする
ECALL命令をテストする前に、 デバッグのために$displayシステムタスクで、 例外が発生したかどうかと、 トラップ先を表示します (リスト28)。
▼リスト4.28: トラップの情報をデバッグ表示する (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を作成し、 次のように記述します (リスト29)。
▼リスト4.29: 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レジスタの値を読み取ります。
シミュレータを実行し、結果を確かめます(リスト30)。
▼リスト4.30: 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になるため問題ありません。
MRET命令の実装
MRET命令[4]は、 トラップ先からトラップ元に戻るための命令です。 MRET命令を実行すると、 mepcレジスタに格納されたアドレスにジャンプします[5]。 例えば、権限のあるOSから権限のないユーザー空間に戻るために利用します。
MRET命令を実装する
▲図3: MRET命令のエンコーディング
まず、 csrunitモジュールに供給されている命令がMRET命令かどうかを判定する変数is_mretを作成します (リスト31)。 MRET命令は、上位12ビットは12'b001100000010、 rs1は0、funct3は0、rdは0です(図3)。
▼リスト4.31: 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にジャンプする仕組みを実装します。 ジャンプするための仕組みには、トラップによってジャンプする仕組みを利用します (リスト32)。 raise_trapにis_mretを追加し、トラップ先も変更します。
▼リスト4.32: 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 : mepc;
例外が優先
trap_vectorには、 is_mretのときにmepcを割り当てるのではなく、 raise_exptのときにmtvecを割り当てています。 これは、MRET命令によって発生する例外があるからです。 MRET命令の判定を優先すると、例外が発生するのにmepcにジャンプしてしまいます。
MRET命令をテストする
mepcに値を設定してからMRET命令を実行することで mepcにジャンプするようなテストを作成します (リスト33)。
▼リスト4.33: sample_mret.hex 差分をみる
34185073 // 0: csrrwi x0, mepc, 0x10
30200073 // 4: mret
00000000 // 8:
00000000 // c:
00000013 // 10: addi x0, x0, 0
シミュレータを実行し、結果を確かめます(リスト34)。
▼リスト4.34: 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番目のレジスタが指定されるため問題ありません。