Verylで作るCPU
Star

第14章
M-modeの実装 (1. CSRの実装)

14.1 概要

「第II部 RV64IMACの実装」では、RV64IMACと例外、メモリマップドI/Oを実装しました。「第III部 特権/割り込みの実装」では、次の機能を実装します。

  • 特権レベル (M-mode、S-mode、U-mode)
  • 仮想記憶システム(ページング)
  • 割り込み(CLINT、PLIC)

これらの機能を実装したCPUはOSを動かせる十分な機能を持っています。第III部の最後ではLinuxを動かします。

14.1.1 特権レベルとは何か?

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ビットの数値で表すことができます(リスト14.1)。数値が大きい方が高い特権レベルです。

[*1] V拡張が実装されている場合、さらに仮想化のための特権レベルが定義されます。

高い特権レベルには低い特権レベルの機能を制限する機能があったり、高い特権レベルでしか利用できない機能が定義されています。

特権レベルを表すPrivMode型をeeiパッケージに定義してください(リスト14.1)。

リスト14.1: リスト14.1: PrivMode型の定義 (eei.veryl)
1:     enum PrivMode: logic<2> {
2:         M = 2'b11,
3:         S = 2'b01,
4:         U = 2'b00,
5:     }

14.1.2 特権レベルの実装順序

RISC-VのCPUに特権レベルを実装するとき、表14.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の実装)」

CPUがリセット(起動)したときの特権レベルはM-modeです。現在の特権レベルを保持するレジスタをcsrunitモジュールに作成します(リスト14.2リスト14.3)。

リスト14.2: リスト14.2: 現在の特権レベルを示すレジスタの定義 (csrunit.veryl)
1:     var mode: PrivMode;
リスト14.3: リスト14.3: レジスタをM-modeでリセットする (csrunit.veryl)
1:     always_ff {
2:         if_reset {
3:              mode    = PrivMode::M;

本書で実装するM-modeのCSRのアドレスをすべて定義します(リスト14.4)。本章ではこの中の一部のCSRを実装し、新しく実装する機能で使うタイミングで他のCSRを解説、実装します

リスト14.4: リスト14.4: CSRのアドレスを定義する (eei.veryl)
1:     enum CsrAddr: logic<12> {
2:         // Machine Information Registers
3:         MIMPID = 12'hf13,
4:         MHARTID = 12'hf14,
5:         // Machine Trap Setup
6:         MSTATUS = 12'h300,
7:         MISA = 12'h301,
8:         MEDELEG = 12'h302,
9:         MIDELEG = 12'h303,
10:         MIE = 12'h304,
11:         MTVEC = 12'h305,
12:         MCOUNTEREN = 12'h306,
13:         // Machine Trap Handling
14:         MSCRATCH = 12'h340,
15:         MEPC = 12'h341,
16:         MCAUSE = 12'h342,
17:         MTVAL = 12'h343,
18:         MIP = 12'h344,
19:         // Machine Counter/Timers
20:         MCYCLE = 12'hB00,
21:         MINSTRET = 12'hB02,
22:         // Custom
23:         LED = 12'h800,
24:     }

14.1.3 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)になるように実装します。

14.2 misaレジスタ (Machine ISA)

misaレジスタ

図14.1: misaレジスタ

misaレジスタは、ハードウェアスレッドがサポートするISAを表すMXLENビットのレジスタです。MXLフィールドにはMXLENを表す数値(表14.2)が格納されています。Extensionsフィールドは下位ビットからそれぞれアルファベットのA、B、 Cと対応していて、それぞれのビットはそのアルファベットが表す拡張(例えばA拡張ならAビット、C拡張ならC)が実装されているなら1に設定されています。仕様上はExtensionsフィールドを書き換えられるように実装できますが、本書では書き換えられないようにします。

表14.2: XLENと数値の対応

XLEN数値
321
642
1283

misaレジスタを作成し、読み込めるようにします(リスト14.5リスト14.6)。CPUはRV64IMACなのでMXLフィールドに64を表す2を設定し、ExtensionsフィールドのM拡張(M)、基本整数命令セット(I)、C拡張(C)、A拡張(A)のビットを1にしています。

リスト14.5: リスト14.5: misaレジスタの定義 (csrunit.veryl)
1:     let misa  : UIntX = {2'd2, 1'b0 repeat XLEN - 28, 26'b00000000000001000100000101}; // M, I, C, A
リスト14.6: リスト14.6: misaレジスタを読めるようにする (csrunit.veryl)
1:         rdata = case csr_addr {
2:             CsrAddr::MISA  : misa,

これ以降、AというCSRのBフィールド、ビットのことをA.Bと表記することがあります。

14.3 mimpidレジスタ (Machine Implementation ID)

mimpidレジスタ

図14.2: mimpidレジスタ

mimpidレジスタは、プロセッサ実装のバージョンを表す値を格納しているMXLENビットのレジスタです。値が0のときは、mimpidレジスタが実装されていないことを示します。

他にもプロセッサの実装の情報を表すレジスタ(mvendorid*2、marchid*3)がありますが、本書では実装しません。

[*2] 製造業者のID(JEDEC ID)を格納します

[*3] マイクロアーキテクチャの種類を示すIDを格納します

せっかくなので、適当な値を設定しましょう。eeiパッケージにIDを定義して、読み込めるようにします(リスト14.7リスト14.8)。

リスト14.7: リスト14.7: IDを適当な値で定義する (eei.veryl)
1:     // Machine Implementation ID
2:     const MACHINE_IMPLEMENTATION_ID: UIntX = 1;
リスト14.8: リスト14.8: mipmidレジスタを読めるようにする (csrunit.veryl)
1:         rdata = case csr_addr {
2:             CsrAddr::MISA  : misa,
3:             CsrAddr::MIMPID: MACHINE_IMPLEMENTATION_ID,

14.4 mhartidレジスタ (Hart ID)

mhartidレジスタ

図14.3: mhartidレジスタ

mhartidレジスタは、今実行しているハードウェアスレッド(hart)のIDを格納しているMXLENビットのレジスタです。複数のプロセッサ、ハードウェアスレッドが存在するときに、それぞれを区別するために使用します。IDはどんな値でも良いですが、環境内にIDが0のハードウェアスレッドが1つ存在する必要があります。基本編で作るCPUは1コア1ハードウェアスレッドであるためmhartidレジスタに0を設定します。

mhartレジスタを作成し、読み込めるようにします(リスト14.9リスト14.10)。

リスト14.9: リスト14.9: mhartidレジスタの定義 (csrunit.veryl)
1:     let mhartid: UIntX = 0;
リスト14.10: リスト14.10: mhartidレジスタを読めるようにする (csrunit.veryl)
1:         rdata = case csr_addr {
2:             CsrAddr::MISA   : misa,
3:             CsrAddr::MIMPID : MACHINE_IMPLEMENTATION_ID,
4:             CsrAddr::MHARTID: mhartid,

14.5 mstatusレジスタ (Machine Status)

mstatusレジスタ

図14.4: mstatusレジスタ

mstatusレジスタは、拡張の設定やトラップ、状態などを管理するMXLENビットのレジスタです。基本編では図14.4に示しているフィールドを、そのフィールドが必要になったときに実装します。とりあえず今のところは読み込みだけできるようにします(リスト14.11リスト14.12リスト14.13リスト14.14リスト14.15リスト14.16)。

リスト14.11: リスト14.11: 書き込みマスクの定義 (csrunit.veryl)
1:     const MSTATUS_WMASK: UIntX = 'h0000_0000_0000_0000 as UIntX;
リスト14.12: リスト14.12: 書き込みマスクを設定する (csrunit.veryl)
1:         wmask = case csr_addr {
2:             CsrAddr::MSTATUS: MSTATUS_WMASK,
リスト14.13: リスト14.13: mstatusレジスタの定義 (csrunit.veryl)
1:     var mstatus: UIntX;
リスト14.14: リスト14.14: mstatusレジスタを読めるようにする (csrunit.veryl)
1:         rdata = case csr_addr {
2:             CsrAddr::MISA   : misa,
3:             CsrAddr::MIMPID : MACHINE_IMPLEMENTATION_ID,
4:             CsrAddr::MHARTID: mhartid,
5:             CsrAddr::MSTATUS: mstatus,
リスト14.15: リスト14.15: mstatusレジスタのリセット (csrunit.veryl)
1:     always_ff {
2:         if_reset {
3:             mode    = PrivMode::M;
4:             mstatus = 0;
リスト14.16: リスト14.16: mstatusレジスタの書き込み (csrunit.veryl)
1:     if is_wsc {
2:         case csr_addr {
3:             CsrAddr::MSTATUS: mstatus = wdata;
4:             CsrAddr::MTVEC  : mtvec   = wdata;

14.6 ハードウェアパフォーマンスモニタ

RISC-Vには、ハードウェアの性能評価指標を得るためにmcycleとminstret、それぞれ29個のmhpmcounter、mhpmeventレジスタが定義されています。それぞれ次の値を得るために利用できます。

mcycleレジスタ (64ビット)
ハードウェアスレッドが起動(リセット)されてから経過したサイクル数
minstretレジスタ (64ビット)
ハードウェアスレッドがリタイア(実行完了)した命令数
mhpmcounter、mhpmeventレジスタ (64ビット)
mhpmeventレジスタで選択された指標がmhpmcounterレジスタに反映されます。

基本編ではmcycle、minstretレジスタを実装します。mhpmcounter、mhpmeventレジスタは表示するような指標がないため実装しません。また、mcountinhibitレジスタを使うとカウントを停止するかを制御できますが、これも実装しません。

14.6.1 mcycleレジスタ

mcycleレジスタを定義して読み込めるようにします。(リスト14.17リスト14.18)。

リスト14.17: リスト14.17: mcycleレジスタの定義 (csrunit.veryl)
1:     var mcycle : UInt64;
リスト14.18: リスト14.18: rdataの割り当てで、mcycleレジスタを読めるようにする (csrunit.veryl)
1:     CsrAddr::MCYCLE : mcycle,

always_ffブロックで、クロックごとに値を更新します(リスト14.19)。

リスト14.19: リスト14.19: mcycleレジスタのリセットとインクリメント (csrunit.veryl)
1:     always_ff {
2:         if_reset {
3:             mode    = PrivMode::M;
4:             mstatus = 0;
5:             mtvec   = 0;
6:             mcycle  = 0;
7:             mepc    = 0;
8:             mcause  = 0;
9:             mtval   = 0;
10:             led     = 0;
11:         } else {
12:             mcycle += 1;

14.6.2 minstretレジスタ

coreモジュールでinstretレジスタを作成し、トラップが発生していない命令がWBステージに到達した場合にインクリメントします(リスト14.20リスト14.21)。

リスト14.20: リスト14.20: minstretレジスタの定義 (core.veryl)
1:     var minstret        : UInt64;
リスト14.21: リスト14.21: minstretレジスタのインクリメント (core.veryl)
1:     always_ff {
2:         if_reset {
3:             minstret = 0;
4:         } else {
5:             if wbq_rvalid && wbq_rready && !wbq_rdata.raise_trap {
6:                 minstret += 1;
7:             }
8:         }
9:     }

minstretの値をcsrunitモジュールに渡し、読み込めるようにします(リスト14.22リスト14.23リスト14.24)。

リスト14.22: リスト14.22: csrunitモジュールのポートにminstretを追加する (csrunit.veryl)
1:     minstret   : input  UInt64           ,
リスト14.23: リスト14.23: csrunitモジュールのインスタンスにminstretレジスタを渡す (core.veryl)
1:         minstret                          ,
リスト14.24: リスト14.24: minstretレジスタを読めるようにする (csrunit.veryl)
1:     CsrAddr::MCYCLE  : mcycle,
2:     CsrAddr::MINSTRET: minstret,
3:     CsrAddr::MEPC    : mepc,

csrunitモジュールはMRET命令でもraise_trapフラグを立てるため、このままではMRET命令でminstretがインクリメントされません。そのため、トラップから戻る命令であることを示すフラグをcsrunitモジュールに作成し、正しくインクリメントされるようにします(リスト14.25リスト14.26リスト14.27リスト14.28)。

リスト14.25: リスト14.25: csrunitモジュールのポートにtrap_returnを追加する (csrunit.veryl)
1:     trap_return: output logic            ,
リスト14.26: リスト14.26: MRET命令の時にtrap_returnを1にする (csrunit.veryl)
1:     // Trap Return
2:     assign trap_return = valid && is_mret && !raise_expt;
3: 
4:     // Trap
5:     assign raise_trap  = raise_expt || trap_return;
リスト14.27: リスト14.27: csrunitモジュールのインスタンスからtrap_returnを受け取る (core.veryl)
1:         trap_return: csru_trap_return     ,
リスト14.28: リスト14.28: MRET命令ならraise_trapフラグを立てないようにする (core.veryl)
1:         wbq_wdata.raise_trap = csru_raise_trap && !csru_trap_return;

14.7 mscratchレジスタ (Machine Scratch)

mscratchレジスタ

図14.5: mscratchレジスタ

mscratchレジスタは、M-modeのときに自由に読み書きできるMXLENビットのレジスタです。

mscratchレジスタの典型的な用途はコンテキストスイッチです。コンテキストスイッチとは、実行しているアプリケーションAを別のアプリケーションBに切り替えることを指します。多くの場合、コンテキストスイッチはトラップによって開始しますが、Aの実行途中の状態(レジスタの値)を保存しないとAを実行再開できなくなります。そのため、コンテキストスイッチが始まったとき、つまりトラップが発生したときにレジスタの値をメモリに保存する必要があります。しかし、ストア命令はアドレスの指定にレジスタの値を使うため、アドレスの指定のために少なくとも1つのレジスタの値を犠牲にしなければならず、すべてのレジスタの値を完全に保存できません*4

[*4] x0と即値を使うとアドレス0付近にすべてのレジスタの値を保存できますが、一般的な方法ではありません

この問題を回避するために、一時的な値の保存場所としてmscratchレジスタが使用されます。事前にmscratchレジスタにメモリアドレス(やメモリアドレスを得るための情報)を格納しておき、CSRRW命令でmscratchレジスタの値とレジスタの値を交換することで任意の場所にレジスタの値を保存できます。

mscratchレジスタを定義し、自由に読み書きできるようにします(リスト14.29リスト14.30リスト14.31リスト14.32リスト14.33リスト14.34)。

リスト14.29: リスト14.29: mscratchレジスタの定義 (csrunit.veryl)
1:     var mcycle  : UInt64;
2:     var mscratch: UIntX ;
3:     var mepc    : UIntX ;
リスト14.30: リスト14.30: mscratchレジスタを0でリセットする (csrunit.veryl)
1:     mtvec    = 0;
2:     mscratch = 0;
3:     mcycle   = 0;
リスト14.31: リスト14.31: mscratchレジスタを読めるようにする (csrunit.veryl)
1:     CsrAddr::MINSTRET: minstret,
2:     CsrAddr::MSCRATCH: mscratch,
3:     CsrAddr::MEPC    : mepc,
リスト14.32: リスト14.32: 書き込みマスクの定義 (csrunit.veryl)
1:     const MTVEC_WMASK   : UIntX = 'hffff_ffff_ffff_fffc;
2:     const MSCRATCH_WMASK: UIntX = 'hffff_ffff_ffff_ffff;
3:     const MEPC_WMASK    : UIntX = 'hffff_ffff_ffff_fffe;
リスト14.33: リスト14.33: 書き込みマスクをwmaskに割り当てる (csrunit.veryl)
1:     CsrAddr::MTVEC   : MTVEC_WMASK,
2:     CsrAddr::MSCRATCH: MSCRATCH_WMASK,
3:     CsrAddr::MEPC    : MEPC_WMASK,
リスト14.34: リスト14.34: mscratchレジスタの書き込み (csrunit.veryl)
1:     CsrAddr::MTVEC   : mtvec    = wdata;
2:     CsrAddr::MSCRATCH: mscratch = wdata;
3:     CsrAddr::MEPC    : mepc     = wdata;