Skip to content

S-modeの実装 (2. 仮想記憶システム)

概要

仮想記憶システム

仮想記憶(Virtual Memory)とは、メモリを管理する手法の一種です。 仮想的なアドレス(virtual address、仮想アドレス)を実際のアドレス(real address、実アドレス)に変換することにより、 実際のアドレス空間とは異なるアドレス空間を提供することができます。 実アドレスのことを物理アドレス(physical address)と呼ぶことがあります。

仮想記憶を利用すると、次のような動作を実現できます。

  1. 連続していない物理アドレス空間を仮想的に連続したアドレス空間として扱う。
  2. 特定のアドレスにしか配置できない(特定のアドレスで動くことを前提としている)プログラムを、そのアドレスとは異なる物理アドレスに配置して実行する。
  3. アプリケーションごとにアドレス空間を分離する。

一般的に仮想記憶システムはハードウェアによって提供されます。 メモリアクセスを処理するハードウェア部品のことをメモリ管理ユニット(Memory Management Unit, MMU)と呼びます。

ページング方式

仮想記憶システムを実現する方式の1つにページング方式(Paging)があります。 ページング方式は、物理アドレス空間の一部をページ(Page)という単位に割り当て、 ページを参照するための情報をページテーブル(Page Table)に格納します。 ページテーブルに格納する情報の単位のことをページテーブルエントリ(Page Table Entry、PTE)と呼びます。 仮想アドレスから物理アドレスへの変換はページテーブルにあるPTEを参照して行います(図1)。

仮想アドレスの変換にPTEを使う

RISC-Vの仮想記憶システム

RISC-Vの仮想記憶システムはページング方式を採用しており、 RV32I向けにはSv32、RV64I向けにはSv39、Sv48、Sv57が定義されています。

RISC-Vの仮想アドレスの変換を簡単に説明します。 仮想アドレスの変換は次のプロセスで行います。

(a) satpレジスタのPPNフィールドと仮想アドレスのフィールドからPTEの物理アドレスを作る。 (b) PTEを読み込む。PTEが有効なものか確認する。 (c) PTEがページを指しているとき、PTEに書かれている権限を確認してから物理アドレスを作り、アドレス変換終了。 (d) PTEが次のPTEを指しているとき、PTEのフィールドと仮想アドレスのフィールドから次のPTEの物理アドレスを作り、(b)に戻る。

satpレジスタは仮想記憶システムを制御するためのCSRです。 一番最初に参照するPTEのことをroot PTEと呼びます。 また、PTEがページを指しているとき、そのPTEのことをleaf PTEと呼びます。

RISC-Vのページングでは、 satpレジスタと仮想アドレス、PTEを使って多段階のPTEの参照を行い、 仮想アドレスを物理アドレスに変換します。 Sv39の場合、何段階で物理アドレスに変換できるかによってページサイズは4KiB、2MiB、1GiBと異なります。 これ以降、MMU内のページング方式を実現する部品のことをPTW(Page Table Walker)と呼びます[1]

satpレジスタ

satpレジスタ RISC-Vの仮想記憶システムはsatpレジスタによって制御します。

MODEは仮想アドレスの変換方式を指定するフィールドです。 方式と値は表1のように対応しています。 方式がBare(0)のときはアドレス変換を行いません(仮想アドレス=物理アドレス)。

表18.1: 方式とMODEの値の対応

方式MODE
Bare0
Sv398
Sv489
Sv5710
ASID(Address Space IDentifier)は仮想アドレスが属するアドレス空間のIDです。 動かすアプリケーションによってIDを変えることでMMUにアドレス変換の高速化のヒントを与えることができます。 本章ではキャッシュ機構を持たない単純なモジュールを実装するため、ASIDを無視したアドレス変換を実装します[^51]。

root PTEのアドレスはsatpレジスタと仮想アドレスから構成される PPN(Physical Page Number)はroot PTEの物理アドレスの一部を格納するフィールドです。 root PTEのアドレスは仮想アドレスのVPNビットと組み合わせて作られます(図3)。

Sv39のアドレス変換

仮想アドレス物理アドレス Sv39では39ビットの仮想アドレスを56ビットの物理アドレスに変換します。

ページの最小サイズは4096(2 ** 12)バイト、 PTEのサイズは8(2 ** 3)バイトです。 それぞれ12と8をPAGESIZE、PTESIZEという定数として定義します。

ページテーブルのサイズ(1つのページテーブルに含まれるPTEの数)は512(= 2 ** 9)個です。 1回のアドレス変換で、最大3回PTEをフェッチし、leaf PTEを見つけます。

アドレスの変換途中でPTEが不正な値だったり、ページが求める権限を持たずにページにアクセスしようとした場合、 アクセスする目的に応じたページフォルト(Page fault)例外が発生します[2]。 命令フェッチはInstruction page fault例外、ロード命令はLoad page fault例外、ストアとAMO命令はStore/AMO page fault例外が発生します。

ページングが有効になる条件

satpレジスタのMODEフィールドがSv39のとき、S-mode、U-modeでアドレス変換が有効になります。 ただし、ロードストアのときは、mstatus.MPRVが1なら特権レベルをmstatus.MPPとして判定します。

有効な仮想アドレスは、MSBでXLENビットに拡張された値である必要があります。 有効ではない仮想アドレスの場合、ページフォルト例外が発生します。

PTEのフェッチ

PTEのアドレス ページングが有効なとき、まずroot PTEをフェッチします。 ここでlevelという変数の値を2とします。

root PTEの物理アドレスは、 satpレジスタのPPNフィールドと仮想アドレスのVPN[level]フィールドを結合し、 log2(PTESIZE)だけ左シフトしたアドレスになります。 このアドレスは、PPNフィールドを12ビット左シフトしたアドレスに存在するページテーブルの、 VPN[level]番目のPTEのアドレスです。

PTEのフィールド PTEのフィールドは図7のようになっています。 このうちN、PBMT、Reservedは使用せず、0でなければページフォルト例外を発生させます。 RSWビットは無視します。

下位8ビットはPTEの状態と権限を表すビットです。

Vが1のとき、有効なPTEであることを示します。 0ならページフォルト例外を発生させます。

R、W、X、Uはページの権限を指定するビットです。 Rは読み込み許可、Wは書き込み許可、Xは実行許可、UはU-modeでアクセスできるかを示します。 書き込みできるPTEは読み込みできる必要があり、 Wが1なのにRが0ならページフォルト例外を発生させます。

RとXが0のとき、PTEは次のPTEを指しています。 このとき、levelが0ならこれ以上PTEを指すことはできない(VPN[-1]は無い)ので、ページフォルト例外を発生させます。 levelが1以上なら、levelから1を引いてPTEをフェッチします。 次のPTEのアドレスは、PTEのPPN[2]、PPN[1]、PPN[0]と仮想アドレスのVPN[level]を結合し、 log2(PTESIZE)だけ左シフトしたアドレスになります。

PTEのRかXが1のとき、PTEはleaf PTEで、ページを指し示しています。

物理アドレスを計算する前に、R、W、X、Uビットで権限を確認します。 命令フェッチのときはX、ロードのときはR、ストアのときはW、U-modeのときはUが立っている必要があります。 S-modeのときは、Uが立っているページにmstatus.SUMが0の状態でアクセスできません。 S-modeのときは、Uが立っているページの実行はできません。 これらに違反した場合、ページフォルト例外が発生します。

levelが2のときの物理アドレス levelが2なら、物理アドレスはPTEのPPN[2]、仮想アドレスのVPN[1]、VPN[0]、page offsetを結合した値になります(図8)。

levelが1のときの物理アドレス levelが1なら、物理アドレスはPTEのPPN[2]、PPN[1]、仮想アドレスのVPN[0]、page offsetを結合した値になります(図9)。

levelが0のときの物理アドレス levelが0なら、物理アドレスはPTEのPPN[2]、PPN[1]、PPN[0]、仮想アドレスのpage offsetを結合した値になります(図10)。

leaf PTEの使わないPPNフィールドは0である必要があり、0ではないならページフォルト例外を発生させます。

求めた物理アドレスにアクセスする前に、leaf PTEのA、Dビットを確認します。 Aはページがこれまでにアクセスされたか、Dはページがこれまでに書き換えられたかを示すビットです。 Aが0のとき、Aを1に設定します。 Dが0でストアするとき、Dを1に設定します。 Aは投機的に1に変更できますが、Dは命令が実行された場合にしか1に変更できません。

実装順序

RISC-Vでは命令フェッチ、データのロードストアの両方でページングを利用できます。 命令フェッチ、データのロードストアのそれぞれのために2つのPTWを用意してもいいですが、 シンプルなアーキテクチャにするために本章では1つのPTWを共有することにします。

inst_fetcherモジュール、amounitモジュールは仮想アドレスを扱うことがありますが、 mmio_controllerモジュールは常に物理アドレス空間を扱います。 そのため、inst_fetcherモジュール、amounitモジュールとmmio_controllerモジュールの間にPTWを配置します(図11)。

本章では、仮想記憶システムを次の順序で実装します。

  1. PTWで発生する例外をcsrunitモジュールに伝達する
  2. Bareにだけ対応したアドレス変換モジュール(ptw)を実装する
  3. satpレジスタ、mstatusのMXR、SUM、MPRVビットを実装する
  4. Sv39を実装する
  5. SFENCE.VMA命令、FENCEI命令を実装する

PTWと他のモジュールの接続

メモリで発生する例外の実装

PTWで発生した例外は、最終的にcsrunitモジュールで処理します。 そのために、例外の情報をメモリのインターフェースを使って伝達します。

ページングによって発生する例外のcauseをCsrCause型に追加します (リスト1)。

▼リスト18.1: CsrCause型にページフォルト例外を追加する (eei.veryl) 差分をみる

veryl
INSTRUCTION_PAGE_FAULT = 12,
LOAD_PAGE_FAULT = 13,
STORE_AMO_PAGE_FAULT = 15,

例外を伝達する

構造体の定義

MemException構造体を定義します (リスト2)。 メモリアクセス中に発生する例外の情報はこの構造体で管理します。

▼リスト18.2: MemException型の定義 (eei.veryl) 差分をみる

veryl
struct MemException {
    valid     : logic,
    page_fault: logic,
}

membus_ifcore_data_ifcore_inst_ifインターフェースにMemException構造体を追加します ( リスト3、 リスト4、 リスト5 )。 インターフェースのrvalid1で、 構造体のvalidis_page_fault1なら ページフォルト例外が発生したことを示します。

▼リスト18.3: MemException型を追加する (membus_if.veryl, core_data_if.veryl, core_inst_if.veryl) 差分をみる

veryl
var expt  : eei::MemException                ;

▼リスト18.4: masterにexptを追加する (membus_if.veryl, core_data_if.veryl, core_inst_if.veryl) 差分をみる

veryl
modport master {
    ...
    expt        : input ,
    ...
}

▼リスト18.5: responseにexptを追加する (membus_if.veryl) 差分をみる

veryl
modport response {
    rvalid: output,
    rdata : output,
    expt  : output,
}

mmio_controllerモジュールの対応

mmio_controllerモジュールで構造体の値をすべて0に設定します ( リスト6、 リスト7 )。 いまのところ、デバイスは例外を発生させません。

▼リスト18.6: exptを0に設定する (membus_if.veryl)

veryl
always_comb {
    req_core.ready  = 0;
    req_core.rvalid = 0;
    req_core.rdata  = 0;
    req_core.expt   = 0;

mmio_controllerモジュールからの例外情報を core_data_ifcore_inst_ifインターフェースに伝達します。

▼リスト18.7: exptを伝達する (top.veryl) 差分をみる

veryl
always_comb {
    i_membus.ready  = mmio_membus.ready && !d_membus.valid;
    i_membus.rvalid = mmio_membus.rvalid && memarb_last_i;
    i_membus.rdata  = mmio_membus.rdata;
    i_membus.expt   = mmio_membus.expt;

    d_membus.ready  = mmio_membus.ready;
    d_membus.rvalid = mmio_membus.rvalid && !memarb_last_i;
    d_membus.rdata  = mmio_membus.rdata;
    d_membus.expt   = mmio_membus.expt;

inst_fetcherモジュールの対応

inst_fetcherモジュールからcoreモジュールに例外情報を伝達します。 まず、FIFOの型に例外情報を追加します ( リスト8、 リスト9) )。

▼リスト18.8: fetch_firo_typeにMemException型を追加する (inst_fetcher.veryl) 差分をみる

veryl
struct fetch_fifo_type {
    addr: Addr                           ,
    bits: logic       <MEMBUS_DATA_WIDTH>,
    expt: MemException                   ,
}

▼リスト18.9: issue_fifo_typeにMemException型を追加する (inst_fetcher.veryl) 差分をみる

veryl
struct issue_fifo_type {
    addr  : Addr        ,
    bits  : Inst        ,
    is_rvc: logic       ,
    expt  : MemException,
}

メモリからの例外情報をfetch_fifoに保存します (リスト10)。

▼リスト18.10: メモリの例外情報をfetch_fifoに保存する (inst_fetcher.veryl) 差分をみる

veryl
always_comb {
    fetch_fifo_flush      = core_if.is_hazard;
    fetch_fifo_wvalid     = fetch_requested && mem_if.rvalid;
    fetch_fifo_wdata.addr = fetch_pc_requested;
    fetch_fifo_wdata.bits = mem_if.rdata;
    fetch_fifo_wdata.expt = mem_if.expt;
}

fetch_fifoからissue_fifoに例外情報を伝達します ( リスト11)、 リスト12、 リスト13 )。 offsetが6で例外が発生しているとき、 32ビット幅の命令の上位16ビットを取得せずにすぐにissue_fifoに例外を書き込みます。

▼リスト18.11: fetch_fifoからissue_fifoに例外情報を伝達する (inst_fetcher.veryl) 差分をみる

veryl
always_comb {
    let raddr : Addr                            = fetch_fifo_rdata.addr;
    let rdata : logic       <MEMBUS_DATA_WIDTH> = fetch_fifo_rdata.bits;
    let expt  : MemException                    = fetch_fifo_rdata.expt;
    let offset: logic       <3>                 = issue_pc_offset;

    fetch_fifo_rready     = 0;
    issue_fifo_wvalid     = 0;
    issue_fifo_wdata      = 0;
    issue_fifo_wdata.expt = expt;

▼リスト18.12: offsetが6のときに例外が発生している場合、すぐにissue_fifoに例外を書き込む (inst_fetcher.veryl) 差分をみる

veryl
fetch_fifo_rready = 1;
if rvcc_is_rvc || expt.valid {
    issue_fifo_wvalid       = 1;
    issue_fifo_wdata.addr   = {raddr[msb:3], offset};
    issue_fifo_wdata.is_rvc = 1;
    issue_fifo_wdata.bits   = rvcc_inst32;

▼リスト18.13: 例外が発生しているときは32ビット幅の命令の上位16ビットを取得しない (inst_fetcher.veryl) 差分をみる

veryl
if issue_pc_offset == 6 && !rvcc_is_rvc && !issue_is_rdata_saved && !fetch_fifo_rdata.expt.valid {
    if fetch_fifo_rvalid {
        issue_is_rdata_saved = 1;

issue_fifoからcoreモジュールに例外情報を伝達します (リスト14)。

▼リスト18.14: issue_fifoからcoreモジュールに例外情報を伝達する (inst_fetcher.veryl) 差分をみる

veryl
always_comb {
    issue_fifo_flush  = core_if.is_hazard;
    issue_fifo_rready = core_if.rready;
    core_if.rvalid    = issue_fifo_rvalid;
    core_if.raddr     = issue_fifo_rdata.addr;
    core_if.rdata     = issue_fifo_rdata.bits;
    core_if.is_rvc    = issue_fifo_rdata.is_rvc;
    core_if.expt      = issue_fifo_rdata.expt;
}

amounitモジュールの対応

stateState::Init以外の時に例外が発生した場合、 すぐに結果を返すようにします ( リスト15、 リスト16、 リスト17、 )。 例外が発生したクロックでは要求を受け付けないようにします。

▼リスト18.15: slaveにexptを割り当てる (amounit.veryl) 差分をみる

veryl
always_comb {
    slave.ready  = 0;
    slave.rvalid = 0;
    slave.rdata  = 0;
    slave.expt   = master.expt;

▼リスト18.16: 例外が発生したらすぐに結果を返し、readyを0にする (amounit.veryl) 差分をみる

veryl
        default: {}
    }

    if state != State::Init && master.expt.valid {
        slave.ready  = 0;
        slave.rvalid = 1;
    }
}

▼リスト18.17: 例外が発生していたらmasterに要求するのをやめる (amounit.veryl) 差分をみる

veryl
        State::AMOStoreValid: accept_request_comb();
        default             : {}
    }

    if state != State::Init && master.expt.valid {
        reset_master();
    }
}

例外が発生したら、stateState::Initにリセットします (リスト18)。

▼リスト18.18: 例外が発生していたらstateをInitにリセットする (amounit.veryl) 差分をみる

veryl
function on_clock () {
    if state != State::Init && master.expt.valid {
        state = State::Init;
    } else {
        case state {
            State::Init     : accept_request_ff();

Instruction page fault例外の実装

命令フェッチ処理中にページフォルト例外が発生していたとき、 Instruction page fault例外を発生させます。 xtvalには例外が発生したアドレスを設定します (リスト19)。

▼リスト18.19: i_membusの例外をExceptionInfo型に設定する (core.veryl) 差分をみる

veryl
if i_membus.expt.valid {
    // fault
    exq_wdata.expt.valid = 1;
    exq_wdata.expt.cause = CsrCause::INSTRUCTION_PAGE_FAULT;
    exq_wdata.expt.value = ids_pc;
} else if !ids_inst_valid {

ロード、ストア命令のpage fault例外の実装

ロード命令、ストア命令、A拡張の命令のメモリアクセス中にページフォルト例外が発生していたとき、 Load page fault例外、Store/AMO page fault例外を発生させます。

csrunitモジュールに、 メモリにアクセスする命令の例外情報を監視するためのポートを作成します ( リスト20、 リスト21 )。

▼リスト18.20: メモリアドレス、例外の監視用のポートを追加する (csrunit.veryl) 差分をみる

veryl
module csrunit (
    ...
    can_intr   : input   logic                   ,
    mem_addr   : input   Addr                    ,
    rdata      : output  UIntX                   ,
    ...
    membus     : modport core_data_if::master    ,
) {

▼リスト18.21: csrunitモジュールにメモリアドレスとインターフェースを割り当てる (core.veryl) 差分をみる

veryl
inst csru: csrunit (
    ...
    mem_addr   : memu_addr            ,
    ...
    membus     : d_membus             ,
);

例外を発生させます (リスト22、 リスト23)。

▼リスト18.22: メモリアクセス中に例外が発生しているかをチェックする (csrunit.veryl) 差分をみる

veryl
let expt_memory_fault    : logic = membus.rvalid && membus.expt.valid;

▼リスト18.23: 例外を発生させる (csrunit.veryl) 差分をみる

veryl
let raise_expt: logic = valid && (expt_info.valid || expt_write_readonly_csr || expt_csr_priv_violation || expt_zicntr_priv || expt_trap_return_priv || expt_memory_fault);
let expt_cause: UIntX = switch {
    ...
    expt_memory_fault      : if ctrl.is_load ? CsrCause::LOAD_PAGE_FAULT : CsrCause::STORE_AMO_PAGE_FAULT,
    default                : 0,
};

xtvalに例外が発生したアドレスを設定します (リスト24)。

▼リスト18.24: 例外の原因を設定する (csrunit.veryl) 差分をみる

veryl
let expt_value: UIntX = switch {
    expt_info.valid                             : expt_info.value,
    expt_cause == CsrCause::ILLEGAL_INSTRUCTION : {1'b0 repeat XLEN - $bits(Inst), inst_bits},
    expt_cause == CsrCause::LOAD_PAGE_FAULT     : mem_addr,
    expt_cause == CsrCause::STORE_AMO_PAGE_FAULT: mem_addr,
    default                                     : 0
};

ページフォルトが発生した正確なアドレスを特定する

ページフォルト例外が発生したとき、 xtvalにはページフォルトが発生した仮想アドレスを格納します。

実は現状の実装では、 メモリにアクセスする操作がページの境界をまたぐとき、 ページフォルトが発生した正確な仮想アドレスをxtvalに格納できていません。

例えば、inst_fetcherモジュールで32ビット幅の命令を2回のメモリ読み込みでフェッチするとき、 1回目(下位16ビット)のロードは成功して、2回目(上位16ビット)のロードでページフォルトが発生したとします。 このとき、ページフォルトが発生したアドレスは2回目のロードでアクセスしたアドレスなのに、 xtvalには1回目のロードでアクセスしたアドレスが書き込まれます。

これに対処するために、例外が発生したアドレスのオフセットを例外情報に追加します ( リスト25 )。

▼リスト18.25: MemException型にaddr_offsetを追加する (eei.veryl) 差分をみる

veryl
struct MemException {
    valid      : logic   ,
    page_fault : logic   ,
    addr_offset: logic<3>,
}

inst_fetcherモジュールで、 32ビット幅の命令の上位16ビットを読み込んでissue_fifoに書き込むときに、 オフセットを2に設定します (リスト26)。

▼リスト18.26: オフセットを2に設定する (inst_fetcher.veryl) 差分をみる

veryl
if issue_is_rdata_saved {
    issue_fifo_wvalid                 = 1;
    issue_fifo_wdata.addr             = {issue_saved_addr[msb:3], offset};
    issue_fifo_wdata.bits             = {rdata[15:0], issue_saved_bits};
    issue_fifo_wdata.is_rvc           = 0;
    issue_fifo_wdata.expt.addr_offset = 2;

xtvalを生成するとき、オフセットを足します ( リスト27、 リスト28 )。

▼リスト18.27: 命令アドレスにオフセットを足す (core.veryl) 差分をみる

veryl
exq_wdata.expt.valid = 1;
exq_wdata.expt.cause = CsrCause::INSTRUCTION_PAGE_FAULT;
exq_wdata.expt.value = ids_pc + {1'b0 repeat XLEN - 3, i_membus.expt.addr_offset};

▼リスト18.28: ロードストア命令のメモリアドレスにオフセットを足す (csrunit.veryl) 差分をみる

veryl
let expt_value: UIntX = switch {
    expt_info.valid                             : expt_info.value,
    expt_cause == CsrCause::ILLEGAL_INSTRUCTION : {1'b0 repeat XLEN - $bits(Inst), inst_bits},
    expt_cause == CsrCause::LOAD_PAGE_FAULT     : mem_addr + {1'b0 repeat XLEN - 3, membus.expt.addr_offset},
    expt_cause == CsrCause::STORE_AMO_PAGE_FAULT: mem_addr + {1'b0 repeat XLEN - 3, membus.expt.addr_offset},
    default                                     : 0
};

satpレジスタの作成

satpレジスタを実装します ( リスト29、 リスト30、 リスト31、 リスト32、 リスト33 )。 すべてのフィールドを読み書きできるように設定して、 値を0でリセットします。

▼リスト18.29: satpレジスタを作成する (csrunit.veryl) 差分をみる

veryl
var satp      : UIntX ;

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

veryl
satp       = 0;

▼リスト18.31: rdataにsatpレジスタの値を設定する (csrunit.veryl) 差分をみる

veryl
CsrAddr::SATP      : satp,

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

veryl
const SATP_WMASK      : UIntX = 'hffff_ffff_ffff_ffff;

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

veryl
CsrAddr::SATP      : SATP_WMASK,

satpレジスタは、 MODEフィールドに書き込もうとしている値がサポートしないMODEなら、 satpレジスタの変更を全ビットについて無視すると定められています。

本章ではBareとSv39だけをサポートするため、 MODEには08のみ書き込めるようにして、 それ以外の値を書き込もうとしたらsatpレジスタへの書き込みを無視します ( リスト34、 リスト35 )。

▼リスト18.34: satに書き込む値を生成する関数 (csrunit.veryl) 差分をみる

veryl
function validate_satp (
    satp : input UIntX,
    wdata: input UIntX,
) -> UIntX {
    // mode
    if wdata[msb-:4] != 0 && wdata[msb-:4] != 8 {
        return satp;
    }
    return wdata;
}

▼リスト18.35: satpレジスタに書き込む (csrunit.veryl) 差分をみる

veryl
CsrAddr::SATP      : satp       = validate_satp(satp, wdata);

mstatusのMXR、SUM、MPRVビットの実装

mstatusレジスタのMXR、SUM、MPRVビットを変更できるようにします ( リスト36、 リスト37 )。

▼リスト18.36: 書き込みマスクの変更 (csrunit.veryl) 差分をみる

veryl
const MSTATUS_WMASK   : UIntX = 'h0000_0000_006e_19aa as UIntX;

▼リスト18.37: 書き込みマスクの変更 (csrunit.veryl) 差分をみる

veryl
const SSTATUS_WMASK   : UIntX = 'h0000_0000_000c_0122 as UIntX;

それぞれのビットを示す変数を作成します ( リスト38、 リスト39 )。

▼リスト18.38: mstatusのMXR、SUM、MPRVビットを示す変数を作成する (csrunit.veryl) 差分をみる

veryl
let mstatus_mxr : logic    = mstatus[19];
let mstatus_sum : logic    = mstatus[18];
let mstatus_mprv: logic    = mstatus[17];

mstatus.MPRVは、M-mode以外のモードに戻るときに0に設定されると定められています。 そのため、trap_mode_nextを確認して0を設定します。

▼リスト18.39: mstatus.MPRVをMRET、SRET命令で0に設定する (csrunit.veryl) 差分をみる

veryl
} else if trap_return {
    // set mstatus.mprv = 0 when new mode != M-mode
    if trap_mode_next <: PrivMode::M {
        mstatus[17] = 0;
    }
    if is_mret {

アドレス変換モジュール(PTW)の実装

ページテーブルエントリをフェッチしてアドレス変換を行うptwモジュールを作成します。 まず、MODEがBareのとき(仮想アドレス = 物理アドレス)の動作を実装し、 Sv39を「18.9 Sv39の実装」で実装します。

CSRのインターフェースを実装する

ページングで使用するCSRを、csrunitモジュールからptwモジュールに渡すためのインターフェースを定義します。

src/ptw_ctrl_if.verylを作成し、次のように記述します (リスト40)。

▼リスト18.40: ptw_ctrl_if.veryl 差分をみる

veryl
import eei::*;

interface ptw_ctrl_if {
    var priv: PrivMode;
    var satp: UIntX   ;
    var mxr : logic   ;
    var sum : logic   ;
    var mprv: logic   ;
    var mpp : PrivMode;

    modport master {
        priv: output,
        satp: output,
        mxr : output,
        sum : output,
        mprv: output,
        mpp : output,
    }

    modport slave {
        is_enabled: import,
        ..converse(master)
    }

    function is_enabled (
        is_inst: input logic,
    ) -> logic {
        if satp[msb-:4] == 0 {
            return 0;
        }
        if is_inst {
            return priv <= PrivMode::S;
        } else {
            return (if mprv ? mpp : priv) <= PrivMode::S;
        }
    }
}

is_enabledは、CSRとアクセス目的からページングがページングが有効かどうかを判定する関数です。 Bareかどうかを判定した後に、命令フェッチかどうか(is_inst)によって分岐しています。 命令フェッチのときはS-mode以下の特権レベルのときにページングが有効になります。 ロードストアのとき、mstatus.MPRVが1ならmstatus.MPP、0なら現在の特権レベルがS-mode以下ならページングが有効になります。

Bareだけに対応するアドレス変換モジュールを実装する

src/ptw.verylを作成し、次のようなポートを記述します (リスト41)。

▼リスト18.41: ポートの定義 (ptw.veryl) 差分をみる

veryl
import eei::*;

module ptw (
    clk    : input   clock             ,
    rst    : input   reset             ,
    is_inst: input   logic             ,
    slave  : modport Membus::slave     ,
    master : modport Membus::master    ,
    ctrl   : modport ptw_ctrl_if::slave,
) {

slaveはcoreモジュール側からの仮想アドレスによる要求を受け付けるためのインターフェースです。 masterはmmio_conterollerモジュール側に物理アドレスによるアクセスを行うためのインターフェースです。

is_instを使い、ページングが有効かどうか判定します (リスト42)。

▼リスト18.42: ページングが有効かどうかを判定する (ptw.veryl) 差分をみる

veryl
let paging_enabled: logic = ctrl.is_enabled(is_inst);

状態の管理のためにState型を定義します (リスト43)。

▼リスト18.43: 状態の定義 (ptw.veryl) 差分をみる

veryl
enum State {
    IDLE,
    EXECUTE_READY,
    EXECUTE_VALID,
}

var state: State;
`State::IDLE`
`slave`から要求を受け付け、`master`に物理アドレスでアクセスします。 `master`の`ready`が`1`なら`State::EXECUTE_VALID`、 `0`なら`EXECUTE_READY`に状態を移動します。
`State::EXECUTE_READY`
`master`に物理アドレスでメモリアクセスを要求し続けます。 `master`の`ready`が`1`なら状態を`State::EXECUTE_VALID`に移動します。
`State::EXECUTE_VALID`
`master`からの結果を待ちます。 `master`の`rvalid`が`1`のとき、 `State::IDLE`と同じように`slave`からの要求を受け付けます。 `slave`が何も要求していないなら、状態を`State::IDLE`に移動します。

slaveからの要求を保存しておくためのインターフェースをインスタンス化します (リスト44)。

▼リスト18.44: slaveを保存するためのインターフェースをインスタンス化する (ptw.veryl) 差分をみる

veryl
inst slave_saved: Membus;

状態に基づいて、masterに要求を割り当てます ( リスト45、 リスト46 )。 State::EXECUTE_READYmasterに要求を割り当てるとき、 physical_addrレジスタの値をアドレスに割り当てるようにします。

▼リスト18.45: 物理アドレスを保存するためのレジスタを作成する (ptw.veryl) 差分をみる

veryl
var physical_addr: Addr;

▼リスト18.46: masterに要求を割り当てる (ptw.veryl) 差分をみる

veryl
function assign_master (
    addr : input Addr                        ,
    wen  : input logic                       ,
    wdata: input logic<MEMBUS_DATA_WIDTH>    ,
    wmask: input logic<MEMBUS_DATA_WIDTH / 8>,
) {
    master.valid = 1;
    master.addr  = addr;
    master.wen   = wen;
    master.wdata = wdata;
    master.wmask = wmask;
}

function accept_request_comb () {
    if slave.ready && slave.valid && !paging_enabled {
        assign_master(slave.addr, slave.wen, slave.wdata, slave.wmask);
    }
}

always_comb {
    master.valid = 0;
    master.addr  = 0;
    master.wen   = 0;
    master.wdata = 0;
    master.wmask = 0;

    case state {
        State::IDLE         : accept_request_comb();
        State::EXECUTE_READY: assign_master      (physical_addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
        State::EXECUTE_VALID: if master.rvalid {
            accept_request_comb();
        }
        default: {}
    }
}

状態に基づいて、readyと結果をslaveに割り当てます (リスト47)。

▼リスト18.47: slaveに結果を割り当てる (ptw.veryl) 差分をみる

veryl
always_comb {
    slave.ready  = 0;
    slave.rvalid = 0;
    slave.rdata  = 0;
    slave.expt   = 0;

    case state {
        State::IDLE         : slave.ready = 1;
        State::EXECUTE_VALID: {
            slave.ready  = master.rvalid;
            slave.rvalid = master.rvalid;
            slave.rdata  = master.rdata;
            slave.expt   = master.expt;
        }
        default: {}
    }
}

状態を遷移する処理を記述します (リスト48)。 要求を受け入れるとき、slave_savedに要求を保存します。

▼リスト18.48: 状態を遷移する (ptw.veryl) 差分をみる

veryl
function accept_request_ff () {
    slave_saved.valid = slave.ready && slave.valid;
    if slave.ready && slave.valid {
        slave_saved.addr  = slave.addr;
        slave_saved.wen   = slave.wen;
        slave_saved.wdata = slave.wdata;
        slave_saved.wmask = slave.wmask;
        if paging_enabled {
            // TODO
        } else {
            state         = if master.ready ? State::EXECUTE_VALID : State::EXECUTE_READY;
            physical_addr = slave.addr;
        }
    } else {
        state = State::IDLE;
    }
}

function on_clock () {
    case state {
        State::IDLE         : accept_request_ff();
        State::EXECUTE_READY: if master.ready {
            state = State::EXECUTE_VALID;
        }
        State::EXECUTE_VALID: if master.rvalid {
            accept_request_ff();
        }
        default: {}
    }
}

function on_reset () {
    state             = State::IDLE;
    physical_addr     = 0;
    slave_saved.valid = 0;
    slave_saved.addr  = 0;
    slave_saved.wen   = 0;
    slave_saved.wdata = 0;
    slave_saved.wmask = 0;
}

always_ff {
    if_reset {
        on_reset();
    } else {
        on_clock();
    }
}

ptwモジュールをインスタンス化する

topモジュールで、ptwモジュールをインスタンス化します。

ptwモジュールはmmio_controllerモジュールの前で仮想アドレスを物理アドレスに変換するモジュールです。 ptwモジュールとmmio_controllerモジュールの間のインターフェースを作成します (リスト49)。

▼リスト18.49: ptwモジュールとmmio_controllerモジュールの間のインターフェースを作成する (top.veryl) 差分をみる

veryl
inst ptw_membus     : Membus;

調停処理をptwモジュール向けのものに変更します (リスト50)。

▼リスト18.50: 調停処理をptwモジュール向けのものに変更する (top.veryl) 差分をみる

veryl
always_ff {
    if_reset {
        memarb_last_i = 0;
    } else {
        if ptw_membus.ready {
            memarb_last_i = !d_membus.valid;
        }
    }
}

always_comb {
    i_membus.ready  = ptw_membus.ready && !d_membus.valid;
    i_membus.rvalid = ptw_membus.rvalid && memarb_last_i;
    i_membus.rdata  = ptw_membus.rdata;
    i_membus.expt   = ptw_membus.expt;

    d_membus.ready  = ptw_membus.ready;
    d_membus.rvalid = ptw_membus.rvalid && !memarb_last_i;
    d_membus.rdata  = ptw_membus.rdata;
    d_membus.expt   = ptw_membus.expt;

    ptw_membus.valid = i_membus.valid | d_membus.valid;
    if d_membus.valid {
        ptw_membus.addr  = d_membus.addr;
        ptw_membus.wen   = d_membus.wen;
        ptw_membus.wdata = d_membus.wdata;
        ptw_membus.wmask = d_membus.wmask;
    } else {
        ptw_membus.addr  = i_membus.addr;
        ptw_membus.wen   = 0; // 命令フェッチは常に読み込み
        ptw_membus.wdata = 'x;
        ptw_membus.wmask = 'x;
    }
}

今処理している要求、 または今のクロックから処理し始める要求が命令フェッチによるものか判定する変数を作成します (リスト51)。

▼リスト18.51: ptwモジュールが処理する要求が命令フェッチによるものかを判定する (top.veryl) 差分をみる

veryl
let ptw_is_inst  : logic = (i_membus.ready && i_membus.valid) || // inst ack or
 !(d_membus.ready && d_membus.valid) && memarb_last_i; // data not ack & last ack is inst

ptwモジュールをインスタンス化します (リスト52)。

▼リスト18.52: ptwモジュールをインスタンス化する (top.veryl) 差分をみる

veryl
inst ptw_ctrl: ptw_ctrl_if;
inst paging_unit: ptw (
    clk                 ,
    rst                 ,
    is_inst: ptw_is_inst,
    slave  : ptw_membus ,
    master : mmio_membus,
    ctrl   : ptw_ctrl   ,
);

csrunitモジュールとptwモジュールをptw_ctrl_ifインターフェースで接続するために、 coreモジュールにポートを追加します ( リスト53、 リスト54 )。

▼リスト18.53: coreモジュールにptw_ctrl_ifインターフェースを追加する (core.veryl) 差分をみる

veryl
module core (
    clk     : input   clock               ,
    rst     : input   reset               ,
    i_membus: modport core_inst_if::master,
    d_membus: modport core_data_if::master,
    led     : output  UIntX               ,
    aclint  : modport aclint_if::slave    ,
    ptw_ctrl: modport ptw_ctrl_if::master ,
) {

▼リスト18.54: ptw_ctrl_ifインターフェースを割り当てる (top.veryl) 差分をみる

veryl
inst c: core (
    clk                      ,
    rst                      ,
    i_membus: i_membus_core  ,
    d_membus: d_membus_core  ,
    led                      ,
    aclint  : aclint_core_bus,
    ptw_ctrl                 ,
);

csrunitモジュールにポートを追加し、CSRを割り当てます ( リスト55、 リスト56、 リスト57 )。

▼リスト18.55: csunitモジュールにptw_ctrl_ifインターフェースを追加する (csrunit.veryl) 差分をみる

veryl
    membus     : modport core_data_if::master    ,
    ptw_ctrl   : modport ptw_ctrl_if::master     ,
) {

▼リスト18.56: csrunitモジュールのインスタンスにptw_ctrl_ifインターフェースを割り当てる (core.veryl) 差分をみる

veryl
    membus     : d_membus             ,
    ptw_ctrl                          ,
);

▼リスト18.57: インターフェースにCSRの値を割り当てる (csrunit.veryl) 差分をみる

veryl
always_comb {
    ptw_ctrl.priv = mode;
    ptw_ctrl.satp = satp;
    ptw_ctrl.mxr  = mstatus_mxr;
    ptw_ctrl.sum  = mstatus_sum;
    ptw_ctrl.mprv = mstatus_mprv;
    ptw_ctrl.mpp  = mstatus_mpp;
}

Sv39の実装

ptwモジュールに、Sv39を実装します。 ここで定義する関数は、コメントと「18.3 Sv39のアドレス変換」を参考に動作を確認してください。

定数の定義

ptwモジュールで使用する定数と関数を実装します。

src/sv39util.verylを作成し、次のように記述します (リスト58)。 定数は「18.3 Sv39のアドレス変換」で使用しているものと同じです。

▼リスト18.58: sv39util.veryl

veryl
import eei::*;
package sv39util {
    const PAGESIZE: u32      = 12;
    const PTESIZE : u32      = 8;
    const LEVELS  : logic<2> = 3;

    type Level = logic<2>;

    // 有効な仮想アドレスか判定する
    function is_valid_vaddr (
        va: input Addr,
    ) -> logic {
        let hiaddr: logic<26> = va[msb:38];
        return &hiaddr || &~hiaddr;
    }

    // 仮想アドレスのVPN[level]フィールドを取得する
    function vpn (
        va   : input Addr ,
        level: input Level,
    ) -> logic<9> {
        return case level {
            0      : va[20:12],
            1      : va[29:21],
            2      : va[38:30],
            default: 0,
        };
    }

    // 最初にフェッチするPTEのアドレスを取得する
    function get_first_pte_address (
        satp: input UIntX,
        va  : input Addr ,
    ) -> Addr {
        return {
            1'b0 repeat XLEN - 44 - PAGESIZE,
            satp[43:0],
            vpn(va, 2),
            1'b0 repeat $clog2(PTESIZE)
        };
    }
}

PTEの定義

Sv39のPTEのビットを分かりやすく取得するために、 次のインターフェースを定義します。

src/pte.verylを作成し、次のように記述します (リスト59)。

▼リスト18.59: pte.veryl 差分をみる

veryl
import eei::*;
import sv39util::*;

interface PTE39 {
    var value: UIntX;

    function v () -> logic { return value[0]; }
    function r () -> logic { return value[1]; }
    function w () -> logic { return value[2]; }
    function x () -> logic { return value[3]; }
    function u () -> logic { return value[4]; }
    function a () -> logic { return value[6]; }
    function d () -> logic { return value[7]; }

    function reserved -> logic<10> { return value[63:54]; }

    function ppn2 () -> logic<26> { return value[53:28]; }
    function ppn1 () -> logic<9> { return value[27:19]; }
    function ppn0 () -> logic<9> { return value[18:10]; }
    function ppn  () -> logic<44> { return value[53:10]; }
}

PTEの値を使った関数を定義します (リスト60)。

▼リスト18.60: PTEの値を使った関数を定義する (pte.veryl)

veryl
// leaf PTEか判定する
function is_leaf () -> logic { return r() || x(); }

// leaf PTEのとき、PPNがページサイズに整列されているかどうかを判定する
function is_ppn_aligned (
    level: input Level,
) -> logic {
    return case level {
        0      : 1,
        1      : ppn0() == 0,
        2      : ppn1() == 0 && ppn0() == 0,
        default: 1,
    };
}

// 有効なPTEか判定する
function is_valid (
    level: input Level,
) -> logic {
    if !v() || reserved() != 0 || !r() && w() {
        return 0;
    }
    if is_leaf() && !is_ppn_aligned(level) {
        return 0;
    }
    if !is_leaf() && level == 0 {
        return 0;
    }
    return 1;
}

// 次のlevelのPTEのアドレスを得る
function get_next_pte_addr (
    level: input Level,
    va   : input Addr ,
) -> Addr {
    return {
        1'b0 repeat XLEN - 44 - PAGESIZE,
        ppn(),
        vpn(va, level - 1),
        1'b0 repeat $clog2(PTESIZE)
    };
}

// PTEと仮想アドレスから物理アドレスを生成する
function get_physical_address (
    level: input Level,
    va   : input Addr ,
) -> Addr {
    return {
        8'b0, ppn2(), case level {
            0: {
                ppn1(), ppn0()
            },
            1: {
                ppn1(), vpn(va, 0)
            },
            2: {
                vpn(va, 1), vpn(va, 0)
            },
            default: 18'b0,
        }, va[11:0]
    };
}

// A、Dビットを更新する必要があるかを判定する
function need_update_ad (
    wen: input logic,
) -> logic {
    return !a() || wen && !d();
}

// A, Dビットを更新したPTEの下位8ビットを生成する
function get_updated_ad (
    wen: input logic,
) -> logic<8> {
    let a: logic<8> = 1 << 6;
    let d: logic<8> = wen as u8 << 7;
    return value[7:0] | a | d;
}

ptwモジュールの実装

sv39utilパッケージをimportします (リスト61)。

▼リスト18.61: sv39utilパッケージをimportする (ptw.veryl) 差分をみる

veryl
import sv39util::*;

PTE39インターフェースをインスタンス化します (リスト62)。 valueにはmasterのロード結果を割り当てます。

▼リスト18.62: PTE39インターフェースをインスタンス化する (ptw.veryl) 差分をみる

veryl
inst pte      : PTE39;
assign pte.value = master.rdata;

状態の遷移図 (点線の状態で新しく要求を受け付け、二重丸の状態で結果を返す) 仮想アドレスを変換するための状態を追加します (リスト63)。 本章ではページングが有効な時に、 stateが図12のように遷移するようにします。

▼リスト18.63: 状態の定義 (ptw.veryl) 差分をみる

veryl
enum State {
    IDLE,
    WALK_READY,
    WALK_VALID,
    SET_AD,
    EXECUTE_READY,
    EXECUTE_VALID,
    PAGE_FAULT,
}

現在のPTEのlevel(level)、 PTEのアドレス(taddr)、 要求によって更新されるPTEの下位8ビット(wdata_ad)を格納するためのレジスタを定義します ( リスト64、 リスト65 )。

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

veryl
var physical_addr: Addr    ;
var taddr        : Addr    ;
var level        : Level   ;
var wdata_ad     : logic<8>;

▼リスト18.65: レジスタをリセットする (ptw.veryl)

veryl
function on_reset () {
    state             = State::IDLE;
    physical_addr     = 0;
    taddr             = 0;
    level             = 0;
    wdata_ad          = 0;

PTEのフェッチとA、Dビットの更新のためにmasterに要求を割り当てます (リスト66)。 PTEはtaddrを使ってアクセスし、 A、Dビットの更新では下位8ビットのみの書き込みマスクを設定します。

▼リスト18.66: masterに要求を割り当てる (ptw.veryl) 差分をみる

veryl
case state {
    State::IDLE      : accept_request_comb();
    State::WALK_READY: assign_master      (taddr, 0, 0, 0);
    State::SET_AD    : assign_master      (taddr, 1, // wen = 1
     {1'b0 repeat MEMBUS_DATA_WIDTH - 8, wdata_ad}, // wdata
     {1'b0 repeat XLEN / 8 - 1, 1'b1} // wmask
    );
    State::EXECUTE_READY: assign_master(physical_addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
    State::EXECUTE_VALID: if master.rvalid {
        accept_request_comb();
    }
    default: {}
}

slaveへの結果の割り当てで、ページフォルト例外が発生していた場合の結果を割り当てます (リスト67)。

▼リスト18.67: ページフォルト例外のときの結果を割り当てる (ptw.veryl) 差分をみる

veryl
State::PAGE_FAULT: {
    slave.rvalid          = 1;
    slave.expt.valid      = 1;
    slave.expt.page_fault = 1;
}

ページングが有効なときに要求を受け入れる動作を実装します (リスト68)。 仮想アドレスが有効かどうかでページフォルト例外を判定し、taddrレジスタに最初のPTEのアドレスを割り当てます。 levelの初期値はLEVELS - 1とします。

▼リスト18.68: ページングが有効なときの要求の受け入れ (ptw.veryl) 差分をみる

veryl
if paging_enabled {
    state = if is_valid_vaddr(slave.addr) ? State::WALK_READY : State::PAGE_FAULT;
    taddr = get_first_pte_address(ctrl.satp, slave.addr);
    level = LEVELS - 1;
} else {
    state         = if master.ready ? State::EXECUTE_VALID : State::EXECUTE_READY;
    physical_addr = slave.addr;
}

ページフォルト例外が発生したとき、状態をState::IDLEに戻します (リスト69)。

▼リスト18.69: ページフォルト例外が発生したときの状態遷移 (ptw.veryl) 差分をみる

veryl
State::PAGE_FAULT: state = State::IDLE;

A、Dビットを更新するとき、メモリが書き込み要求を受け入れたら、状態をState::EXECUTE_READYに移動します (リスト70)。

▼リスト18.70: A、Dビットを更新したときの状態遷移 (ptw.veryl) 差分をみる

veryl
State::SET_AD: if master.ready {
    state = State::EXECUTE_READY;
}

ページにアクセスする権限があるかをPTEと要求から判定する関数を定義します (リスト71)。 条件の詳細は「18.3 Sv39のアドレス変換」を確認してください。

▼リスト18.71: ページにアクセスする権限があるかを判定する関数 (ptw.veryl) 差分をみる

veryl
function check_permission (
    req: modport Membus::all_input,
) -> logic {
    let priv: PrivMode = if is_inst || !ctrl.mprv ? ctrl.priv : ctrl.mpp;

    // U-mode access with PTE.U=0
    let u_u0: logic = priv == PrivMode::U && !pte.u();
    // S-mode load/store with PTE.U=1 & sum=0
    let sd_u1: logic = !is_inst && priv == PrivMode::S && pte.u() && !ctrl.sum;
    // S-mode execute with PTE.U=1
    let si_u1: logic = is_inst && priv == PrivMode::S && pte.u();

    // execute without PTE.X
    let x: logic = is_inst && !pte.x();
    // write without PTE.W
    let w: logic = !is_inst && req.wen && !pte.w();
    // read without PTE.R (MXR)
    let r: logic = !is_inst && !req.wen && !pte.r() && !(pte.x() && ctrl.mxr);

    return !(u_u0 | sd_u1 | si_u1 | x | w | r);
}

PTEをフェッチしてページフォルト例外を判定し、次のPTEのフェッチ、A、Dビットを更新する状態への遷移を実装します (リスト72)。

▼リスト18.72: PTEのフェッチとPTEの確認 (ptw.veryl) 差分をみる

veryl
State::WALK_READY: if master.ready {
    state = State::WALK_VALID;
}
State::WALK_VALID: if master.rvalid {
    if !pte.is_valid(level) {
        state = State::PAGE_FAULT;
    } else {
        if pte.is_leaf() {
            if check_permission(slave_saved) {
                physical_addr = pte.get_physical_address(level, slave_saved.addr);
                if pte.need_update_ad(slave_saved.wen) {
                    state    = State::SET_AD;
                    wdata_ad = pte.get_updated_ad(slave_saved.wen);
                } else {
                    state = State::EXECUTE_READY;
                }
            } else {
                state = State::PAGE_FAULT;
            }
        } else {
            // read next pte
            state = State::WALK_READY;
            taddr = pte.get_next_pte_addr(level, slave_saved.addr);
            level = level - 1;
        }
    }
}

これでSv39をptwモジュールに実装できました。

SFENCE.VMA命令の実装

SFENCE.VMA命令は、 SFENCE.VMA命令を実行する以前のストア命令がMMUに反映されたことを保証する命令です。 S-mode以上の特権レベルのときに実行できます。

基本編ではすべてのメモリアクセスを直列に行い、 仮想アドレスを変換するために毎回PTEをフェッチしなおすため、何もしない命令として定義します。

SFENCE.VMA命令をデコードする

SFENCE.VMA命令を有効な命令としてデコードします (リスト73)。

▼リスト18.73: SFENCE.VMA命令を有効な命令としてデコードする (inst_decoder.veryl) 差分をみる

veryl
bits == 32'h10200073 || //SRET
bits == 32'h10500073 || // WFI
f7 == 7'b0001001 && bits[11:7] == 0, // SFENCE.VMA

特権レベルの確認、mstatus.TVMを実装する

S-mode未満の特権レベルでSFENCE.VMA命令を実行しようとしたとき、 Illegal instruction例外が発生します。

mstatus.TVMはS-modeのときにsatpレジスタにアクセスできるか、 SFENCE.VMA命令を実行できるかを制御するビットです。 mstatus.TVMが1にされているとき、Illegal instruction例外が発生します。

mstatus.TVMを書き込めるようにします (リスト74)。

▼リスト18.74: mstatusレジスタの書き込みマスクを変更する (csrunit.veryl) 差分をみる

veryl
const MSTATUS_WMASK   : UIntX = 'h0000_0000_007e_19aa as UIntX;

▼リスト18.75: mstatus.TVMを示す変数を作成する (csrunit.veryl) 差分をみる

veryl
let mstatus_tvm : logic    = mstatus[20];

特権レベルを確認して、例外を発生させます ( リスト76、 リスト77、 リスト78 )。

▼リスト18.76: SFENCE.VMA命令かどうかを判定する (csrunit.veryl) 差分をみる

veryl
let is_sfence_vma: logic = ctrl.is_csr && ctrl.funct7 == 7'b0001001 && ctrl.funct3 == 0 && rd_addr == 0;

▼リスト18.77: SFENCE.VMA命令の例外を判定する (csrunit.veryl) 差分をみる

veryl
let expt_tvm: logic = (is_sfence_vma && mode <: PrivMode::S) || (mstatus_tvm && mode == PrivMode::S && (is_wsc && csr_addr == CsrAddr::SATP || is_sfence_vma));

▼リスト18.78: 例外を発生させる (csrunit.veryl) 差分をみる

veryl
let raise_expt: logic = valid && (expt_info.valid || expt_write_readonly_csr || expt_csr_priv_violation || expt_zicntr_priv || expt_trap_return_priv || expt_memory_fault || expt_tvm);
let expt_cause: UIntX = switch {
    ...
    expt_tvm               : CsrCause::ILLEGAL_INSTRUCTION,
    default                : 0,
};

パイプラインをフラッシュする

本書はパイプライン化したCPUを実装しているため、 命令フェッチは前の命令を待たずに次々に行われます。

CSRの変更

mstatusレジスタのMXR、SUM、TVMビット、 satpレジスタを書き換えたとき、 CSRを書き換える命令の後ろの命令は、 CSRの変更が反映されていない状態でアドレス変換してフェッチした命令になっている可能性があります。

CSRの書き換えをページングに反映するために、 特定のCSRを書き換えたらパイプラインをフラッシュするようにします。

csrunitモジュールに、フラッシュするためのフラグを追加します ( リスト79、 リスト80、 リスト81 )。

▼リスト18.79: csrunitモジュールのポートにフラッシュするためのフラグを追加する (csrunit.veryl) 差分をみる

veryl
flush      : output  logic                   ,
minstret   : input   UInt64                  ,

▼リスト18.80: csru_flush変数の定義 (core.veryl) 差分をみる

veryl
var csru_trap_return: logic   ;
var csru_flush      : logic   ;
var minstret        : UInt64  ;

▼リスト18.81: csrunitモジュールのflushフラグをcsru_flushに割り当てる (core.veryl) 差分をみる

veryl
flush      : csru_flush           ,
minstret                          ,

satp、mstatus、sstatusレジスタが変更されるときにflush1にします (リスト82)。

▼リスト18.82: satp、mstatus、sstatusレジスタが変更されるときにflushを1にする (csrunit.veryl)

veryl
let wsc_flush: logic = is_wsc && (csr_addr == CsrAddr::SATP || csr_addr == CsrAddr::MSTATUS || csr_addr == CsrAddr::SSTATUS);
assign flush     = valid && wsc_flush;

flush1のとき、制御ハザードが発生したことにしてパイプラインをフラッシュします (リスト83)。

▼リスト18.83: csru_flushが1のときにパイプラインをフラッシュする (core.veryl) 差分をみる

veryl
assign control_hazard         = mems_valid && (csru_raise_trap || mems_ctrl.is_jump || memq_rdata.br_taken || csru_flush);
assign control_hazard_pc_next = if csru_raise_trap ? csru_trap_vector : // trap
 if csru_flush ? mems_pc + 4 : memq_rdata.jump_addr; // flush or jump

FENCE.I命令の実装

あるアドレスにデータを書き込むとき、 データを書き込んだ後の命令が、 書き換えられたアドレスにある命令だった場合、 命令のビット列がデータが書き換えられる前のものになっている可能性があります。

FENCE.I命令は、FENCE.I命令の後の命令のフェッチ処理がストア命令の完了後に行われることを保証する命令です。 例えばユーザーのアプリケーションのプログラムをページに書き込んで実行するとき、 ページへの書き込みを反映させるために使用します。

FENCE.I命令を判定し、パイプラインをフラッシュする条件に設定します ( リスト84、 リスト85 )。

▼リスト18.84: FENCE.I命令かどうかを判定する (csrunit.veryl) 差分をみる

veryl
let is_fence_i: logic = inst_bits[6:0] == OP_MISC_MEM && ctrl.funct3 == 3'b001;

▼リスト18.85: FENCE.I命令のときにflushを1にする (csrunit.veryl) 差分をみる

veryl
assign flush     = valid && (wsc_flush || is_fence_i);

riscv-testsの-v-を含むテストを実行し、実装している命令のテストに成功することを確認してください。


  1. ページテーブルをたどってアドレスを変換するのでPage Table Walkerと呼びます。アドレスを変換することをPage Table Walkと呼ぶこともあります。 ↩︎

  2. RISC-VのMMUはPMP、PMAという仕組みで物理アドレス空間へのアクセスを制限することができ、それに違反した場合にアクセスフォルト例外を発生させます。本章ではPMP、PMAを実装していないのでアクセスフォルト例外に関する機能について説明せず、実装もしません。これらの機能は応用編で実装します。 ↩︎