M-modeの実装 (1. CSRの実装)
概要
「第II部 RV64IMACの実装」では、RV64IMACと例外、メモリマップドI/Oを実装しました。 「第III部 特権/割り込みの実装」では、次の機能を実装します。
- 特権レベル (M-mode、S-mode、U-mode)
- 仮想記憶システム(ページング)
- 割り込み(CLINT、PLIC)
これらの機能を実装したCPUはOSを動かせる十分な機能を持っています。 第III部の最後ではLinuxを動かします。
特権レベルとは何か?
CPUで動くアプリケーションは様々ですが、 多くのアプリケーションはOS(Operating System、オペレーティングシステム)の上で動かすことを前提に作成されています。 「OSの上で動かす」とは、アプリケーションはOSの機能を使い、OSに管理されながら実行されるということです。
多くのOSはデバイスやメモリなどのリソースの管理を行い、簡単にそれを扱うためのインターフェースをアプリケーションに提供します。 また、アプリケーションのデータを別のアプリケーションから保護したり、 OSが提供する方法でしかデバイスにアクセスできなくするなどのセキュリティ機能も備えています。
セキュリティ機能を実現するためには、OSがアプリケーションを実行するときにCPUが提供する一部の機能を制限する機能が必要です。 RISC-Vでは、この機能を特権レベル(privilege level)という機能、枠組みによって提供しています。 ほとんどの特権レベルの機能はCSRを通じて提供されます。
特権レベルはM-mode、S-mode、U-modeの3種類[1]が用意されています。 それぞれの特権レベルは2ビットの数値で表すことができます(リスト1)。 数値が大きい方が高い特権レベルです。
高い特権レベルには低い特権レベルの機能を制限する機能があったり、高い特権レベルでしか利用できない機能が定義されています。
特権レベルを表すPrivMode型をeeiパッケージに定義してください (リスト1)。
▼リスト14.1: PrivMode型の定義 (eei.veryl) 差分をみる
enum PrivMode: logic<2> {
M = 2'b11,
S = 2'b01,
U = 2'b00,
}
特権レベルの実装順序
RISC-VのCPUに特権レベルを実装するとき、表1のいずれかの構成にする必要があります。 特権レベルを実装していないときはM-modeだけが実装されているように扱います。
表14.1: RISC-VのCPUがとれる構成
| 存在する特権レベル | 実装する章 |
|---|---|
| M-mode | 第14章「M-modeの実装 (1. CSRの実装)」 |
| M-mode、U-mode | 第16章「U-modeの実装」 |
| M-mode、S-mode、U-mode | 第17章「S-modeの実装 (1. CSRの実装)」 |
▼リスト14.2: 現在の特権レベルを示すレジスタの定義 (csrunit.veryl) 差分をみる
var mode: PrivMode;
▼リスト14.3: レジスタをM-modeでリセットする (csrunit.veryl)
always_ff {
if_reset {
mode = PrivMode::M;
本書で実装するM-modeのCSRのアドレスをすべて定義します (リスト4)。 本章ではこの中の一部のCSRを実装し、 新しく実装する機能で使うタイミングで他のCSRを解説、実装します
▼リスト14.4: CSRのアドレスを定義する (eei.veryl)
enum CsrAddr: logic<12> {
// Machine Information Registers
MIMPID = 12'hf13,
MHARTID = 12'hf14,
// Machine Trap Setup
MSTATUS = 12'h300,
MISA = 12'h301,
MEDELEG = 12'h302,
MIDELEG = 12'h303,
MIE = 12'h304,
MTVEC = 12'h305,
MCOUNTEREN = 12'h306,
// Machine Trap Handling
MSCRATCH = 12'h340,
MEPC = 12'h341,
MCAUSE = 12'h342,
MTVAL = 12'h343,
MIP = 12'h344,
// Machine Counter/Timers
MCYCLE = 12'hB00,
MINSTRET = 12'hB02,
// Custom
LED = 12'h800,
}
XLENの定義
M-modeのCSRの多くは、特権レベルがM-modeのときのXLENであるMXLENをビット幅として定義されています。 S-mode、U-modeのときのXLENはそれぞれSXLEN、UXLENと定義されており、MXLEN >= SXLEN >= UXLENを満たします。 仕様上はmstatusレジスタを使用してSXLEN、UXLENを変更できるように実装できますが、 本書ではMXLEN、SXLEN、UXLENが常に64(eeiパッケージに定義しているXLEN)になるように実装します。
misaレジスタ (Machine ISA)
misaレジスタは、ハードウェアスレッドがサポートするISAを表すMXLENビットのレジスタです。 MXLフィールドにはMXLENを表す数値(表2)が格納されています。 Extensionsフィールドは下位ビットからそれぞれアルファベットのA、B、 Cと対応していて、 それぞれのビットはそのアルファベットが表す拡張(例えばA拡張ならAビット、C拡張ならC)が実装されているなら1に設定されています。 仕様上はExtensionsフィールドを書き換えられるように実装できますが、本書では書き換えられないようにします。
表14.2: XLENと数値の対応
| XLEN | 数値 |
|---|---|
| 32 | 1 |
| 64 | 2 |
| 128 | 3 |
▼リスト14.5: misaレジスタの定義 (csrunit.veryl) 差分をみる
let misa : UIntX = {2'd2, 1'b0 repeat XLEN - 28, 26'b00000000000001000100000101}; // M, I, C, A
▼リスト14.6: misaレジスタを読めるようにする (csrunit.veryl) 差分をみる
rdata = case csr_addr {
CsrAddr::MISA : misa,
これ以降、AというCSRのBフィールド、ビットのことをA.Bと表記することがあります。
mimpidレジスタ (Machine Implementation ID)
mimpidレジスタは、プロセッサ実装のバージョンを表す値を格納しているMXLENビットのレジスタです。 値が0のときは、mimpidレジスタが実装されていないことを示します。
他にもプロセッサの実装の情報を表すレジスタ(mvendorid[2]、marchid[3])がありますが、本書では実装しません。
せっかくなので、適当な値を設定しましょう。 eeiパッケージにIDを定義して、読み込めるようにします ( リスト7、 リスト8 )。
▼リスト14.7: IDを適当な値で定義する (eei.veryl) 差分をみる
// Machine Implementation ID
const MACHINE_IMPLEMENTATION_ID: UIntX = 1;
▼リスト14.8: mipmidレジスタを読めるようにする (csrunit.veryl) 差分をみる
rdata = case csr_addr {
CsrAddr::MISA : misa,
CsrAddr::MIMPID: MACHINE_IMPLEMENTATION_ID,
mhartidレジスタ (Hart ID)
mhartidレジスタは、今実行しているハードウェアスレッド(hart)のIDを格納しているMXLENビットのレジスタです。 複数のプロセッサ、ハードウェアスレッドが存在するときに、それぞれを区別するために使用します。 IDはどんな値でも良いですが、環境内にIDが0のハードウェアスレッドが1つ存在する必要があります。 基本編で作るCPUは1コア1ハードウェアスレッドであるためmhartidレジスタに0を設定します。
mhartレジスタを作成し、読み込めるようにします ( リスト9、 リスト10 )。
▼リスト14.9: mhartidレジスタの定義 (csrunit.veryl) 差分をみる
let mhartid: UIntX = 0;
▼リスト14.10: mhartidレジスタを読めるようにする (csrunit.veryl) 差分をみる
rdata = case csr_addr {
CsrAddr::MISA : misa,
CsrAddr::MIMPID : MACHINE_IMPLEMENTATION_ID,
CsrAddr::MHARTID: mhartid,
mstatusレジスタ (Machine Status)
mstatusレジスタは、拡張の設定やトラップ、状態などを管理するMXLENビットのレジスタです。 基本編では図4に示しているフィールドを、そのフィールドが必要になったときに実装します。 とりあえず今のところは読み込みだけできるようにします ( リスト11、 リスト12、 リスト13、 リスト14、 リスト15、 リスト16 )。
▼リスト14.11: 書き込みマスクの定義 (csrunit.veryl) 差分をみる
const MSTATUS_WMASK: UIntX = 'h0000_0000_0000_0000 as UIntX;
▼リスト14.12: 書き込みマスクを設定する (csrunit.veryl) 差分をみる
wmask = case csr_addr {
CsrAddr::MSTATUS: MSTATUS_WMASK,
▼リスト14.13: mstatusレジスタの定義 (csrunit.veryl) 差分をみる
var mstatus: UIntX;
▼リスト14.14: mstatusレジスタを読めるようにする (csrunit.veryl) 差分をみる
rdata = case csr_addr {
CsrAddr::MISA : misa,
CsrAddr::MIMPID : MACHINE_IMPLEMENTATION_ID,
CsrAddr::MHARTID: mhartid,
CsrAddr::MSTATUS: mstatus,
▼リスト14.15: mstatusレジスタのリセット (csrunit.veryl) 差分をみる
always_ff {
if_reset {
mode = PrivMode::M;
mstatus = 0;
▼リスト14.16: mstatusレジスタの書き込み (csrunit.veryl) 差分をみる
if is_wsc {
case csr_addr {
CsrAddr::MSTATUS: mstatus = wdata;
CsrAddr::MTVEC : mtvec = wdata;
ハードウェアパフォーマンスモニタ
RISC-Vには、ハードウェアの性能評価指標を得るためにmcycleとminstret、それぞれ29個のmhpmcounter、mhpmeventレジスタが定義されています。 それぞれ次の値を得るために利用できます。
- mcycleレジスタ (64ビット)
- ハードウェアスレッドが起動(リセット)されてから経過したサイクル数
- minstretレジスタ (64ビット)
- ハードウェアスレッドがリタイア(実行完了)した命令数
- mhpmcounter、mhpmeventレジスタ (64ビット)
- mhpmeventレジスタで選択された指標がmhpmcounterレジスタに反映されます。
基本編ではmcycle、minstretレジスタを実装します。 mhpmcounter、mhpmeventレジスタは表示するような指標がないため実装しません。 また、mcountinhibitレジスタを使うとカウントを停止するかを制御できますが、これも実装しません。
mcycleレジスタ
mcycleレジスタを定義して読み込めるようにします。 ( リスト17、 リスト18 )。
▼リスト14.17: mcycleレジスタの定義 (csrunit.veryl) 差分をみる
var mcycle : UInt64;
▼リスト14.18: rdataの割り当てで、mcycleレジスタを読めるようにする (csrunit.veryl) 差分をみる
CsrAddr::MCYCLE : mcycle,
always_ffブロックで、クロックごとに値を更新します ( リスト19 )。
▼リスト14.19: mcycleレジスタのリセットとインクリメント (csrunit.veryl) 差分をみる
always_ff {
if_reset {
mode = PrivMode::M;
mstatus = 0;
mtvec = 0;
mcycle = 0;
mepc = 0;
mcause = 0;
mtval = 0;
led = 0;
} else {
mcycle += 1;
minstretレジスタ
coreモジュールでinstretレジスタを作成し、 トラップが発生していない命令がWBステージに到達した場合にインクリメントします ( リスト20、 リスト21 )。
▼リスト14.20: minstretレジスタの定義 (core.veryl) 差分をみる
var minstret : UInt64;
▼リスト14.21: minstretレジスタのインクリメント (core.veryl) 差分をみる
always_ff {
if_reset {
minstret = 0;
} else {
if wbq_rvalid && wbq_rready && !wbq_rdata.raise_trap {
minstret += 1;
}
}
}
minstretの値をcsrunitモジュールに渡し、読み込めるようにします ( リスト22、 リスト23、 リスト24 )。
▼リスト14.22: csrunitモジュールのポートにminstretを追加する (csrunit.veryl) 差分をみる
minstret : input UInt64 ,
▼リスト14.23: csrunitモジュールのインスタンスにminstretレジスタを渡す (core.veryl) 差分をみる
minstret ,
▼リスト14.24: minstretレジスタを読めるようにする (csrunit.veryl) 差分をみる
CsrAddr::MCYCLE : mcycle,
CsrAddr::MINSTRET: minstret,
CsrAddr::MEPC : mepc,
csrunitモジュールはMRET命令でもraise_trapフラグを立てるため、 このままではMRET命令でminstretがインクリメントされません。 そのため、トラップから戻る命令であることを示すフラグをcsrunitモジュールに作成し、 正しくインクリメントされるようにします ( リスト25、 リスト26、 リスト27、 リスト28 )。
▼リスト14.25: csrunitモジュールのポートにtrap_returnを追加する (csrunit.veryl) 差分をみる
trap_return: output logic ,
▼リスト14.26: MRET命令の時にtrap_returnを1にする (csrunit.veryl) 差分をみる
// Trap Return
assign trap_return = valid && is_mret && !raise_expt;
// Trap
assign raise_trap = raise_expt || trap_return;
▼リスト14.27: csrunitモジュールのインスタンスからtrap_returnを受け取る (core.veryl) 差分をみる
trap_return: csru_trap_return ,
▼リスト14.28: MRET命令ならraise_trapフラグを立てないようにする (core.veryl) 差分をみる
wbq_wdata.raise_trap = csru_raise_trap && !csru_trap_return;
mscratchレジスタ (Machine Scratch)
mscratchレジスタは、M-modeのときに自由に読み書きできるMXLENビットのレジスタです。
mscratchレジスタの典型的な用途はコンテキストスイッチです。 コンテキストスイッチとは、実行しているアプリケーションAを別のアプリケーションBに切り替えることを指します。 多くの場合、コンテキストスイッチはトラップによって開始しますが、 Aの実行途中の状態(レジスタの値)を保存しないとAを実行再開できなくなります。 そのため、コンテキストスイッチが始まったとき、つまりトラップが発生したときにレジスタの値をメモリに保存する必要があります。 しかし、ストア命令はアドレスの指定にレジスタの値を使うため、 アドレスの指定のために少なくとも1つのレジスタの値を犠牲にしなければならず、 すべてのレジスタの値を完全に保存できません[4]。
この問題を回避するために、一時的な値の保存場所としてmscratchレジスタが使用されます。 事前にmscratchレジスタにメモリアドレス(やメモリアドレスを得るための情報)を格納しておき、 CSRRW命令でmscratchレジスタの値とレジスタの値を交換することで任意の場所にレジスタの値を保存できます。
mscratchレジスタを定義し、自由に読み書きできるようにします ( リスト29、 リスト30、 リスト31、 リスト32、 リスト33、 リスト34 )。
▼リスト14.29: mscratchレジスタの定義 (csrunit.veryl) 差分をみる
var mcycle : UInt64;
var mscratch: UIntX ;
var mepc : UIntX ;
▼リスト14.30: mscratchレジスタを0でリセットする (csrunit.veryl) 差分をみる
mtvec = 0;
mscratch = 0;
mcycle = 0;
▼リスト14.31: mscratchレジスタを読めるようにする (csrunit.veryl) 差分をみる
CsrAddr::MINSTRET: minstret,
CsrAddr::MSCRATCH: mscratch,
CsrAddr::MEPC : mepc,
▼リスト14.32: 書き込みマスクの定義 (csrunit.veryl) 差分をみる
const MTVEC_WMASK : UIntX = 'hffff_ffff_ffff_fffc;
const MSCRATCH_WMASK: UIntX = 'hffff_ffff_ffff_ffff;
const MEPC_WMASK : UIntX = 'hffff_ffff_ffff_fffe;
▼リスト14.33: 書き込みマスクをwmaskに割り当てる (csrunit.veryl) 差分をみる
CsrAddr::MTVEC : MTVEC_WMASK,
CsrAddr::MSCRATCH: MSCRATCH_WMASK,
CsrAddr::MEPC : MEPC_WMASK,
▼リスト14.34: mscratchレジスタの書き込み (csrunit.veryl) 差分をみる
CsrAddr::MTVEC : mtvec = wdata;
CsrAddr::MSCRATCH: mscratch = wdata;
CsrAddr::MEPC : mepc = wdata;