第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レジスタを実装します。
命令 | 作用 |
---|---|
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を、即値をゼロ拡張した値に置き換えた動作 |
4.2 CSR命令のデコード
まず、Zicsrに定義されている命令(表4.1)をデコードします。
これらの命令のopcodeはSYSTEM
(7'b1110011
)です。この値をeeiパッケージに定義します(リスト4.1)。
次に、InstCtrl
構造体に、CSRを制御する命令であることを示すis_csr
フラグを追加します(リスト4.2)。
これでデコード処理を書く準備が整いました。inst_decoderモジュールのInstCtrl
を生成している部分を変更します(リスト4.3)。
リスト4.3では、opcodeがOP_SYSTEM
な命令を、I形式、レジスタに結果を書き込む、CSRを操作する命令であるということにしています。他のopcodeの命令はCSRを操作しない命令であるということにしています。
CSRRW、CSRRS、CSRRC命令は、rs1レジスタの値を利用します。CSRRWI、CSRRSI、CSRRCI命令は、命令のビット列中のrs1にあたるビット列(5ビット)を0
で拡張した値を利用します。それぞれの命令はfunct3で区別できます(表4.2)。
funct3 | 命令 |
---|---|
3'b001 | CSRRW |
3'b101 | CSRRWI |
3'b010 | CSRRS |
3'b110 | CSRRSI |
3'b011 | CSRRC |
3'b111 | CSRRCI |
操作対象のCSRのアドレス(12ビット)は、命令のビットの上位12ビット(I形式の即値)をそのまま利用します。
4.3 csrunitモジュールの実装
CSRを操作する命令のデコードができたので、CSR関連の処理を行うモジュールを作成します。
4.3.1 csrunitモジュールを作成する
src/csrunit.veryl
を作成し、次のように記述します(リスト4.4)。
csrunitモジュールの主要なポートの定義は表4.3のとおりです。まだcsrunitモジュールにはCSRが一つもないため、中身が空になっています。
ポート名 | 型 | 向き | 意味 |
---|---|---|---|
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読み込みの結果 |
csrunitモジュールを、coreモジュールの中でインスタンス化します(リスト4.5)。
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
になるようにします(リスト4.6)。
最後に、デバッグ用の表示を追加します。デバッグ表示用のalways_ffブロックに、次のコードを追加してください(リスト4.7)。
これらのテストは、csrunitモジュールにCSRを追加してから行います。
4.3.2 mtvecレジスタを実装する
csrunitモジュールには、まだCSRが定義されていません。1つ目のCSRとして、mtvecレジスタを実装します。
mtvecレジスタ、トラップ
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)。
次に、mtvecレジスタを作成します。MXLEN=XLENとしているので、型はUIntX
にします(リスト4.9)。
MODEはDirectモード(2'b00
)しか対応していません。mtvecはWARLなレジスタなので、MODEフィールドには書き込めないようにする必要があります。これを制御するためにmtvecレジスタの書き込みマスク用の定数を定義します(リスト4.10)。
次に、書き込むデータwdata
の生成と、mtvecレジスタの読み込みを実装します(リスト4.11)。
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)。
mtvecの初期値は0
です。mtvecにwdata
を書き込むとき、MODEが常に2'b00
になります。
4.3.3 csrunitモジュールをテストする
mtvecレジスタの書き込み、読み込みができることを確認します。
test/sample_csr.hex
を作成し、次のように記述します(リスト4.13)。
テストでは、CSRRWI命令でmtvecに32'b10111
を書き込んだ後、CSRRS命令でmtvecの値を読み込みます。CSRRS命令で読み込むとき、rs1をx0(ゼロレジスタ)にすることで、mtvecの値を変更せずに読み込みます。
シミュレータを実行し、結果を確かめます(リスト4.14)。
$ 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)しかないものとします。
権限レベル | 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が割り当てられています。
4.4.2 トラップを実装する
それでは、ECALL命令とトラップの仕組みを実装します。
定数の定義
まず、mepcとmcauseのアドレスをCsrAddr
型に追加します(リスト4.15)。
次に、トラップの発生原因を表現する型CsrCause
を定義します。今のところ、発生原因はECALL命令によるEnvironment Call From M-mode例外しかありません(リスト4.16)。
最後に、mepcとmcauseの書き込みマスクを定義します(リスト4.17)。mepcに格納されるのは例外が発生した時の命令のアドレスです。命令は4バイトに整列して配置されているため、mepcの下位2ビットは常に2'b00
になるようにします。
mepcとmcauseレジスタの実装
mepcとmcauseレジスタを作成します。サイズはMXLEN(=XLEN)なため、型はUIntX
とします(リスト4.18)。
次に、mepcとmcauseの読み込み処理と、書き込みマスクの割り当てを実装します。どちらもcase文にアドレスと値のペアを追加するだけです(リスト4.19、リスト4.20)。
最後に、mepcとmcauseの書き込みを実装します。if_resetで値を0
に初期化し、case文にmepcとmcauseの場合を実装します(リスト4.21)。
例外の実装
ECALL命令と、それによって発生するトラップを実装します。まず、csrunitモジュールにポートを追加します(リスト4.22)。
それぞれの用途は次の通りです。
- pc
-
現在処理している命令のアドレスを受け取ります。
例外が発生するとき、mepcにPCを格納するために使います。 - rd_addr
-
現在処理している命令のrdの番号を受け取ります。
命令がECALL命令かどうかを判定するために使います。 - raise_trap
-
例外が発生するとき、値を
1
にします。 - trap_vector
- 例外が発生するとき、ジャンプ先のアドレスを出力します。
csrunitモジュールの中身を実装する前に、coreモジュールに例外発生時の動作を実装します。
csrunitモジュールと接続するための変数を定義してcsrunitモジュールと接続します(リスト4.23、リスト4.24)。
次に、トラップするときにトラップ先にジャンプさせます。
例外が発生するとき、csru_raise_trap
が1
になり、csru_trap_vector
がトラップ先になります。トラップするときの動作には、ジャンプと分岐命令の仕組みを利用します。control_hazard
の条件にcsru_raise_trap
を追加して、トラップするときにcontrol_hazard_pc_next
をcsru_trap_vector
に設定します(リスト4.25)。
それでは、csrunitモジュールにトラップの処理を実装します。
ECALL命令は、I形式、即値は0
、rs1とrdは0
、funct3は0
、opcodeはSYSTEM
な命令です(図4.2)。これを判定するための変数を作成します(リスト4.26)。
次に、例外が発生するかどうかを示すraise_expt
と、例外の発生の原因を示すexpt_cause
を作成します。今のところ、例外はECALL命令によってのみ発生するため、expt_cause
は実質的に定数になっています(リスト4.27)。
トラップが発生するかどうかを示すraise_trap
には、例外が発生するかどうかを割り当てます。トラップの原因を示すtrap_cause
には、例外の発生原因を割り当てます。また、トラップ先にはmtvec
を割り当てます。
最後に、トラップに伴うCSRの変更を実装します。トラップが発生するとき、mepcレジスタにPC、mcauseレジスタにトラップの発生原因を格納します(リスト4.28)。
4.4.3 ECALL命令をテストする
ECALL命令をテストする前に、デバッグのために$display
システムタスクで、例外が発生したかどうかと、トラップ先を表示します(リスト4.29)。
test/sample_ecall.hex
を作成し、次のように記述します(リスト4.30)。
CSRRWI命令でmtvecレジスタに値を書き込み、ECALL命令で例外を発生させてジャンプします。ジャンプ先では、mcauseレジスタとmepcレジスタの値を読み取ります。
シミュレータを実行し、結果を確かめます(リスト4.31)。
$ 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命令を実装する
まず、csrunitモジュールに供給されている命令がMRET命令かどうかを判定する変数is_mret
を作成します(リスト4.32)。MRET命令は、上位12ビットは12'b001100000010
、rs1は0
、funct3は0
、rdは0
です(図4.3)。
次に、csrunitモジュールにMRET命令が供給されているときにmepcにジャンプする仕組みを実装します。ジャンプするための仕組みには、トラップによってジャンプする仕組みを利用します(リスト4.33)。raise_trap
にis_mret
を追加し、トラップ先も変更します。
例外が優先
trap_vectorには、is_mret
のときにmepc
を割り当てるのではなく、raise_expt
のときにmtvec
を割り当てています。これは、MRET命令によって発生する例外があるからです。MRET命令の判定を優先すると、例外が発生するのにmepcにジャンプしてしまいます。
4.5.2 MRET命令をテストする
mepcに値を設定してからMRET命令を実行することでmepcにジャンプするようなテストを作成します(リスト4.34)。
シミュレータを実行し、結果を確かめます(リスト4.35)。
$ 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番目のレジスタが指定されるため問題ありません。