Skip to content

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) 差分をみる

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の実装)」
CPUがリセット(起動)したときの特権レベルはM-modeです。 現在の特権レベルを保持するレジスタをcsrunitモジュールに作成します ( リスト2、 リスト3 )。

▼リスト14.2: 現在の特権レベルを示すレジスタの定義 (csrunit.veryl) 差分をみる

veryl
var mode: PrivMode;

▼リスト14.3: レジスタをM-modeでリセットする (csrunit.veryl)

veryl
always_ff {
    if_reset {
         mode    = PrivMode::M;

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

▼リスト14.4: CSRのアドレスを定義する (eei.veryl)

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

表14.2: XLENと数値の対応

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

▼リスト14.5: misaレジスタの定義 (csrunit.veryl) 差分をみる

veryl
let misa  : UIntX = {2'd2, 1'b0 repeat XLEN - 28, 26'b00000000000001000100000101}; // M, I, C, A

▼リスト14.6: misaレジスタを読めるようにする (csrunit.veryl) 差分をみる

veryl
rdata = case csr_addr {
    CsrAddr::MISA  : misa,

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

mimpidレジスタ (Machine Implementation ID)

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

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

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

▼リスト14.7: IDを適当な値で定義する (eei.veryl) 差分をみる

veryl
// Machine Implementation ID
const MACHINE_IMPLEMENTATION_ID: UIntX = 1;

▼リスト14.8: mipmidレジスタを読めるようにする (csrunit.veryl) 差分をみる

veryl
rdata = case csr_addr {
    CsrAddr::MISA  : misa,
    CsrAddr::MIMPID: MACHINE_IMPLEMENTATION_ID,

mhartidレジスタ (Hart ID)

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

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

▼リスト14.9: mhartidレジスタの定義 (csrunit.veryl) 差分をみる

veryl
let mhartid: UIntX = 0;

▼リスト14.10: mhartidレジスタを読めるようにする (csrunit.veryl) 差分をみる

veryl
rdata = case csr_addr {
    CsrAddr::MISA   : misa,
    CsrAddr::MIMPID : MACHINE_IMPLEMENTATION_ID,
    CsrAddr::MHARTID: mhartid,

mstatusレジスタ (Machine Status)

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

▼リスト14.11: 書き込みマスクの定義 (csrunit.veryl) 差分をみる

veryl
const MSTATUS_WMASK: UIntX = 'h0000_0000_0000_0000 as UIntX;

▼リスト14.12: 書き込みマスクを設定する (csrunit.veryl) 差分をみる

veryl
wmask = case csr_addr {
    CsrAddr::MSTATUS: MSTATUS_WMASK,

▼リスト14.13: mstatusレジスタの定義 (csrunit.veryl) 差分をみる

veryl
var mstatus: UIntX;

▼リスト14.14: mstatusレジスタを読めるようにする (csrunit.veryl) 差分をみる

veryl
rdata = case csr_addr {
    CsrAddr::MISA   : misa,
    CsrAddr::MIMPID : MACHINE_IMPLEMENTATION_ID,
    CsrAddr::MHARTID: mhartid,
    CsrAddr::MSTATUS: mstatus,

▼リスト14.15: mstatusレジスタのリセット (csrunit.veryl) 差分をみる

veryl
always_ff {
    if_reset {
        mode    = PrivMode::M;
        mstatus = 0;

▼リスト14.16: mstatusレジスタの書き込み (csrunit.veryl) 差分をみる

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) 差分をみる

veryl
var mcycle : UInt64;

▼リスト14.18: rdataの割り当てで、mcycleレジスタを読めるようにする (csrunit.veryl) 差分をみる

veryl
CsrAddr::MCYCLE : mcycle,

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

▼リスト14.19: mcycleレジスタのリセットとインクリメント (csrunit.veryl) 差分をみる

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) 差分をみる

veryl
var minstret        : UInt64;

▼リスト14.21: minstretレジスタのインクリメント (core.veryl) 差分をみる

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) 差分をみる

veryl
minstret   : input  UInt64           ,

▼リスト14.23: csrunitモジュールのインスタンスにminstretレジスタを渡す (core.veryl) 差分をみる

veryl
minstret                          ,

▼リスト14.24: minstretレジスタを読めるようにする (csrunit.veryl) 差分をみる

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) 差分をみる

veryl
trap_return: output logic            ,

▼リスト14.26: MRET命令の時にtrap_returnを1にする (csrunit.veryl) 差分をみる

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) 差分をみる

veryl
trap_return: csru_trap_return     ,

▼リスト14.28: MRET命令ならraise_trapフラグを立てないようにする (core.veryl) 差分をみる

veryl
wbq_wdata.raise_trap = csru_raise_trap && !csru_trap_return;

mscratchレジスタ (Machine Scratch)

mscratchレジスタ 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) 差分をみる

veryl
var mcycle  : UInt64;
var mscratch: UIntX ;
var mepc    : UIntX ;

▼リスト14.30: mscratchレジスタを0でリセットする (csrunit.veryl) 差分をみる

veryl
mtvec    = 0;
mscratch = 0;
mcycle   = 0;

▼リスト14.31: mscratchレジスタを読めるようにする (csrunit.veryl) 差分をみる

veryl
CsrAddr::MINSTRET: minstret,
CsrAddr::MSCRATCH: mscratch,
CsrAddr::MEPC    : mepc,

▼リスト14.32: 書き込みマスクの定義 (csrunit.veryl) 差分をみる

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) 差分をみる

veryl
CsrAddr::MTVEC   : MTVEC_WMASK,
CsrAddr::MSCRATCH: MSCRATCH_WMASK,
CsrAddr::MEPC    : MEPC_WMASK,

▼リスト14.34: mscratchレジスタの書き込み (csrunit.veryl) 差分をみる

veryl
CsrAddr::MTVEC   : mtvec    = wdata;
CsrAddr::MSCRATCH: mscratch = wdata;
CsrAddr::MEPC    : mepc     = wdata;

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

  2. 製造業者のID(JEDEC ID)を格納します ↩︎

  3. マイクロアーキテクチャの種類を示すIDを格納します ↩︎

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