Verylで作るCPU
Star

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

18.1 概要

18.1.1 仮想記憶システム

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

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

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

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

18.1.2 ページング方式

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

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

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

18.1.3 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

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

18.2 satpレジスタ

satpレジスタ

図18.2: satpレジスタ

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

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

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

方式MODE
Bare0
Sv398
Sv489
Sv5710

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

[*2] PTWはページエントリをキャッシュすることで高速化できます。ASIDが異なるときのキャッシュは利用することができません。キャッシュ機構(TLB)は応用編で実装します。

root PTEのアドレスはsatpレジスタと仮想アドレスから構成される

図18.3: root PTEのアドレスはsatpレジスタと仮想アドレスから構成される

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

18.3 Sv39のアドレス変換

仮想アドレス

図18.4: 仮想アドレス

物理アドレス

図18.5: 物理アドレス

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)例外が発生します*3。命令フェッチはInstruction page fault例外、ロード命令はLoad page fault例外、ストアとAMO命令はStore/AMO page fault例外が発生します。

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

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

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

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

18.3.2 PTEのフェッチ

PTEのアドレス

図18.6: PTEのアドレス

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

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

PTEのフィールド

図18.7: PTEのフィールド

PTEのフィールドは図18.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のときの物理アドレス

図18.8: levelが2のときの物理アドレス

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

levelが1のときの物理アドレス

図18.9: levelが1のときの物理アドレス

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

levelが0のときの物理アドレス

図18.10: levelが0のときの物理アドレス

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

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

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

18.4 実装順序

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

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

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

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

図18.11: PTWと他のモジュールの接続

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

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

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

リスト18.1: リスト18.1: CsrCause型にページフォルト例外を追加する (eei.veryl)
1:     INSTRUCTION_PAGE_FAULT = 12,
2:     LOAD_PAGE_FAULT = 13,
3:     STORE_AMO_PAGE_FAULT = 15,

18.5.1 例外を伝達する

構造体の定義

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

リスト18.2: リスト18.2: MemException型の定義 (eei.veryl)
1:     struct MemException {
2:         valid     : logic,
3:         page_fault: logic,
4:     }

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

リスト18.3: リスト18.3: MemException型を追加する (membus_if.veryl, core_data_if.veryl, core_inst_if.veryl)
1:     var expt  : eei::MemException                ;
リスト18.4: リスト18.4: masterにexptを追加する (membus_if.veryl, core_data_if.veryl, core_inst_if.veryl)
1:     modport master {
2:         ...
3:         expt        : input ,
4:         ...
5:     }
リスト18.5: リスト18.5: slaveにexptを追加する (membus_if.veryl)
1:     modport slave {
2:         ...
3:         expt        : output,
4:         ...
5:     }
リスト18.6: リスト18.6: responseにexptを追加する (membus_if.veryl)
1:     modport response {
2:         rvalid: output,
3:         rdata : output,
4:         expt  : output,
5:     }

mmio_controllerモジュールの対応

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

リスト18.7: リスト18.7: exptを0に設定する (membus_if.veryl)
1:     always_comb {
2:         req_core.ready  = 0;
3:         req_core.rvalid = 0;
4:         req_core.rdata  = 0;
5:         req_core.expt   = 0;

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

リスト18.8: リスト18.8: exptを伝達する (top.veryl)
1:     always_comb {
2:         i_membus.ready  = mmio_membus.ready && !d_membus.valid;
3:         i_membus.rvalid = mmio_membus.rvalid && memarb_last_i;
4:         i_membus.rdata  = mmio_membus.rdata;
5:         i_membus.expt   = mmio_membus.expt;
6: 
7:         d_membus.ready  = mmio_membus.ready;
8:         d_membus.rvalid = mmio_membus.rvalid && !memarb_last_i;
9:         d_membus.rdata  = mmio_membus.rdata;
10:         d_membus.expt   = mmio_membus.expt;

inst_fetcherモジュールの対応

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

リスト18.9: リスト18.9: fetch_firo_typeにMemException型を追加する (inst_fetcher.veryl)
1:     struct fetch_fifo_type {
2:         addr: Addr                           ,
3:         bits: logic       <MEMBUS_DATA_WIDTH>,
4:         expt: MemException                   ,
5:     }
リスト18.10: リスト18.10: issue_fifo_typeにMemException型を追加する (inst_fetcher.veryl)
1:     struct issue_fifo_type {
2:         addr  : Addr        ,
3:         bits  : Inst        ,
4:         is_rvc: logic       ,
5:         expt  : MemException,
6:     }

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

リスト18.11: リスト18.11: メモリの例外情報をfetch_fifoに保存する (inst_fetcher.veryl)
1:     always_comb {
2:         fetch_fifo_flush      = core_if.is_hazard;
3:         fetch_fifo_wvalid     = fetch_requested && mem_if.rvalid;
4:         fetch_fifo_wdata.addr = fetch_pc_requested;
5:         fetch_fifo_wdata.bits = mem_if.rdata;
6:         fetch_fifo_wdata.expt = mem_if.expt;
7:     }

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

リスト18.12: リスト18.12: fetch_fifoからissue_fifoに例外情報を伝達する (inst_fetcher.veryl)
1:     always_comb {
2:         let raddr : Addr                            = fetch_fifo_rdata.addr;
3:         let rdata : logic       <MEMBUS_DATA_WIDTH> = fetch_fifo_rdata.bits;
4:         let expt  : MemException                    = fetch_fifo_rdata.expt;
5:         let offset: logic       <3>                 = issue_pc_offset;
6: 
7:         fetch_fifo_rready     = 0;
8:         issue_fifo_wvalid     = 0;
9:         issue_fifo_wdata      = 0;
10:         issue_fifo_wdata.expt = expt;
リスト18.13: リスト18.13: offsetが6のときに例外が発生している場合、すぐにissue_fifoに例外を書き込む (inst_fetcher.veryl)
1:     fetch_fifo_rready = 1;
2:     if rvcc_is_rvc || expt.valid {
3:         issue_fifo_wvalid       = 1;
4:         issue_fifo_wdata.addr   = {raddr[msb:3], offset};
5:         issue_fifo_wdata.is_rvc = 1;
6:         issue_fifo_wdata.bits   = rvcc_inst32;
リスト18.14: リスト18.14: 例外が発生しているときは32ビット幅の命令の上位16ビットを取得しない (inst_fetcher.veryl)
1:     if issue_pc_offset == 6 && !rvcc_is_rvc && !issue_is_rdata_saved && !fetch_fifo_rdata.expt.valid {
2:         if fetch_fifo_rvalid {
3:             issue_is_rdata_saved = 1;

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

リスト18.15: リスト18.15: issue_fifoからcoreモジュールに例外情報を伝達する (inst_fetcher.veryl)
1:     always_comb {
2:         issue_fifo_flush  = core_if.is_hazard;
3:         issue_fifo_rready = core_if.rready;
4:         core_if.rvalid    = issue_fifo_rvalid;
5:         core_if.raddr     = issue_fifo_rdata.addr;
6:         core_if.rdata     = issue_fifo_rdata.bits;
7:         core_if.is_rvc    = issue_fifo_rdata.is_rvc;
8:         core_if.expt      = issue_fifo_rdata.expt;
9:     }

amounitモジュールの対応

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

リスト18.16: リスト18.16: slaveにexptを割り当てる (amounit.veryl)
1:     always_comb {
2:         slave.ready  = 0;
3:         slave.rvalid = 0;
4:         slave.rdata  = 0;
5:         slave.expt   = master.expt;
リスト18.17: リスト18.17: 例外が発生したらすぐに結果を返し、readyを0にする (amounit.veryl)
1:             default: {}
2:         }
3: 
4:         if state != State::Init && master.expt.valid {
5:             slave.ready  = 0;
6:             slave.rvalid = 1;
7:         }
8:     }
リスト18.18: リスト18.18: 例外が発生していたらmasterに要求するのをやめる (amounit.veryl)
1:             State::AMOStoreValid: accept_request_comb();
2:             default             : {}
3:         }
4: 
5:         if state != State::Init && master.expt.valid {
6:             reset_master();
7:         }
8:     }

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

リスト18.19: リスト18.19: 例外が発生していたらstateをInitにリセットする (amounit.veryl)
1:     function on_clock () {
2:         if state != State::Init && master.expt.valid {
3:             state = State::Init;
4:         } else {
5:             case state {
6:                 State::Init     : accept_request_ff();

Instruction page fault例外の実装

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

リスト18.20: リスト18.20: i_membusの例外をExceptionInfo型に設定する (core.veryl)
1:         if i_membus.expt.valid {
2:             // fault
3:             exq_wdata.expt.valid = 1;
4:             exq_wdata.expt.cause = CsrCause::INSTRUCTION_PAGE_FAULT;
5:             exq_wdata.expt.value = ids_pc;
6:         } else if !ids_inst_valid {

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

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

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

リスト18.21: リスト18.21: メモリアドレス、例外の監視用のポートを追加する (csrunit.veryl)
1: module csrunit (
2:     ...
3:     can_intr   : input   logic                   ,
4:     mem_addr   : input   Addr                    ,
5:     rdata      : output  UIntX                   ,
6:     ...
7:     membus     : modport core_data_if::master    ,
8: ) {
リスト18.22: リスト18.22: csrunitモジュールにメモリアドレスとインターフェースを割り当てる (core.veryl)
1:     inst csru: csrunit (
2:         ...
3:         mem_addr   : memu_addr            ,
4:         ...
5:         membus     : d_membus             ,
6:     );

例外を発生させます(リスト18.23リスト18.24)。

リスト18.23: リスト18.23: メモリアクセス中に例外が発生しているかをチェックする (csrunit.veryl)
1:     let expt_memory_fault    : logic = membus.rvalid && membus.expt.valid;
リスト18.24: リスト18.24: 例外を発生させる (csrunit.veryl)
1:     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);
2:     let expt_cause: UIntX = switch {
3:         ...
4:         expt_memory_fault      : if ctrl.is_load ? CsrCause::LOAD_PAGE_FAULT : CsrCause::STORE_AMO_PAGE_FAULT,
5:         default                : 0,
6:     };

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

リスト18.25: リスト18.25: 例外の原因を設定する (csrunit.veryl)
1:     let expt_value: UIntX = switch {
2:         expt_info.valid                             : expt_info.value,
3:         expt_cause == CsrCause::ILLEGAL_INSTRUCTION : {1'b0 repeat XLEN - $bits(Inst), inst_bits},
4:         expt_cause == CsrCause::LOAD_PAGE_FAULT     : mem_addr,
5:         expt_cause == CsrCause::STORE_AMO_PAGE_FAULT: mem_addr,
6:         default                                     : 0
7:     };

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

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

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

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

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

リスト18.26: リスト18.26: MemException型にaddr_offsetを追加する (eei.veryl)
1:     struct MemException {
2:         valid      : logic   ,
3:         page_fault : logic   ,
4:         addr_offset: logic<3>,
5:     }

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

リスト18.27: リスト18.27: オフセットを2に設定する (inst_fetcher.veryl)
1:     if issue_is_rdata_saved {
2:         issue_fifo_wvalid                 = 1;
3:         issue_fifo_wdata.addr             = {issue_saved_addr[msb:3], offset};
4:         issue_fifo_wdata.bits             = {rdata[15:0], issue_saved_bits};
5:         issue_fifo_wdata.is_rvc           = 0;
6:         issue_fifo_wdata.expt.addr_offset = 2;

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

リスト18.28: リスト18.28: 命令アドレスにオフセットを足す (core.veryl)
1:     exq_wdata.expt.valid = 1;
2:     exq_wdata.expt.cause = CsrCause::INSTRUCTION_PAGE_FAULT;
3:     exq_wdata.expt.value = ids_pc + {1'b0 repeat XLEN - 3, i_membus.expt.addr_offset};
リスト18.29: リスト18.29: ロードストア命令のメモリアドレスにオフセットを足す (csrunit.veryl)
1:     let expt_value: UIntX = switch {
2:         expt_info.valid                             : expt_info.value,
3:         expt_cause == CsrCause::ILLEGAL_INSTRUCTION : {1'b0 repeat XLEN - $bits(Inst), inst_bits},
4:         expt_cause == CsrCause::LOAD_PAGE_FAULT     : mem_addr + {1'b0 repeat XLEN - 3, membus.expt.addr_offset},
5:         expt_cause == CsrCause::STORE_AMO_PAGE_FAULT: mem_addr + {1'b0 repeat XLEN - 3, membus.expt.addr_offset},
6:         default                                     : 0
7:     };

18.6 satpレジスタの作成

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

リスト18.30: リスト18.30: satpレジスタを作成する (csrunit.veryl)
1:     var satp      : UIntX ;
リスト18.31: リスト18.31: satpレジスタを0でリセットする (csrunit.veryl)
1:     satp       = 0;
リスト18.32: リスト18.32: rdataにsatpレジスタの値を設定する (csrunit.veryl)
1:     CsrAddr::SATP      : satp,
リスト18.33: リスト18.33: 書き込みマスクの定義 (csrunit.veryl)
1:     const SATP_WMASK      : UIntX = 'hffff_ffff_ffff_ffff;
リスト18.34: リスト18.34: wmaskに書き込みマスクを設定する (csrunit.veryl)
1:     CsrAddr::SATP      : SATP_WMASK,

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

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

リスト18.35: リスト18.35: satに書き込む値を生成する関数 (csrunit.veryl)
1:     function validate_satp (
2:         satp : input UIntX,
3:         wdata: input UIntX,
4:     ) -> UIntX {
5:         // mode
6:         if wdata[msb-:4] != 0 && wdata[msb-:4] != 8 {
7:             return satp;
8:         }
9:         return wdata;
10:     }
リスト18.36: リスト18.36: satpレジスタに書き込む (csrunit.veryl)
1:     CsrAddr::SATP      : satp       = validate_satp(satp, wdata);

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

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

リスト18.37: リスト18.37: 書き込みマスクの変更 (csrunit.veryl)
1:     const MSTATUS_WMASK   : UIntX = 'h0000_0000_006e_19aa as UIntX;
リスト18.38: リスト18.38: 書き込みマスクの変更 (csrunit.veryl)
1:     const SSTATUS_WMASK   : UIntX = 'h0000_0000_000c_0122 as UIntX;

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

リスト18.39: リスト18.39: mstatusのMXR、SUM、MPRVビットを示す変数を作成する (csrunit.veryl)
1:     let mstatus_mxr : logic    = mstatus[19];
2:     let mstatus_sum : logic    = mstatus[18];
3:     let mstatus_mprv: logic    = mstatus[17];

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

リスト18.40: リスト18.40: mstatus.MPRVをMRET、SRET命令で0に設定する (csrunit.veryl)
1:     } else if trap_return {
2:         // set mstatus.mprv = 0 when new mode != M-mode
3:         if trap_mode_next <: PrivMode::M {
4:             mstatus[17] = 0;
5:         }
6:         if is_mret {

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

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

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

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

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

リスト18.41: リスト18.41: ptw_ctrl_if.veryl
1: import eei::*;
2: 
3: interface ptw_ctrl_if {
4:     var priv: PrivMode;
5:     var satp: UIntX   ;
6:     var mxr : logic   ;
7:     var sum : logic   ;
8:     var mprv: logic   ;
9:     var mpp : PrivMode;
10: 
11:     modport master {
12:         priv: output,
13:         satp: output,
14:         mxr : output,
15:         sum : output,
16:         mprv: output,
17:         mpp : output,
18:     }
19: 
20:     modport slave {
21:         is_enabled: import,
22:         ..converse(master)
23:     }
24: 
25:     function is_enabled (
26:         is_inst: input logic,
27:     ) -> logic {
28:         if satp[msb-:4] == 0 {
29:             return 0;
30:         }
31:         if is_inst {
32:             return priv <= PrivMode::S;
33:         } else {
34:             return (if mprv ? mpp : priv) <= PrivMode::S;
35:         }
36:     }
37: }

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

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

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

リスト18.42: リスト18.42: ポートの定義 (ptw.veryl)
1: import eei::*;
2: 
3: module ptw (
4:     clk    : input   clock             ,
5:     rst    : input   reset             ,
6:     is_inst: input   logic             ,
7:     slave  : modport Membus::slave     ,
8:     master : modport Membus::master    ,
9:     ctrl   : modport ptw_ctrl_if::slave,
10: ) {

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

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

リスト18.43: リスト18.43: ページングが有効かどうかを判定する (ptw.veryl)
1:     let paging_enabled: logic = ctrl.is_enabled(is_inst);

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

リスト18.44: リスト18.44: 状態の定義 (ptw.veryl)
1:     enum State {
2:         IDLE,
3:         EXECUTE_READY,
4:         EXECUTE_VALID,
5:     }
6: 
7:     var state: State;
State::IDLE
slaveから要求を受け付け、masterに物理アドレスでアクセスします。 masterready1ならState::EXECUTE_VALID0ならEXECUTE_READYに状態を移動します。
State::EXECUTE_READY
masterに物理アドレスでメモリアクセスを要求し続けます。 masterready1なら状態をState::EXECUTE_VALIDに移動します。
State::EXECUTE_VALID
masterからの結果を待ちます。 masterrvalid1のとき、 State::IDLEと同じようにslaveからの要求を受け付けます。 slaveが何も要求していないなら、状態をState::IDLEに移動します。

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

リスト18.45: リスト18.45: slaveを保存するためのインターフェースをインスタンス化する (ptw.veryl)
1:     inst slave_saved: Membus;

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

リスト18.46: リスト18.46: 物理アドレスを保存するためのレジスタを作成する (ptw.veryl)
1:     var physical_addr: Addr;
リスト18.47: リスト18.47: masterに要求を割り当てる (ptw.veryl)
1:     function assign_master (
2:         addr : input Addr                        ,
3:         wen  : input logic                       ,
4:         wdata: input logic<MEMBUS_DATA_WIDTH>    ,
5:         wmask: input logic<MEMBUS_DATA_WIDTH / 8>,
6:     ) {
7:         master.valid = 1;
8:         master.addr  = addr;
9:         master.wen   = wen;
10:         master.wdata = wdata;
11:         master.wmask = wmask;
12:     }
13: 
14:     function accept_request_comb () {
15:         if slave.ready && slave.valid && !paging_enabled {
16:             assign_master(slave.addr, slave.wen, slave.wdata, slave.wmask);
17:         }
18:     }
19: 
20:     always_comb {
21:         master.valid = 0;
22:         master.addr  = 0;
23:         master.wen   = 0;
24:         master.wdata = 0;
25:         master.wmask = 0;
26: 
27:         case state {
28:             State::IDLE         : accept_request_comb();
29:             State::EXECUTE_READY: assign_master      (physical_addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
30:             State::EXECUTE_VALID: if master.rvalid {
31:                 accept_request_comb();
32:             }
33:             default: {}
34:         }
35:     }

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

リスト18.48: リスト18.48: slaveに結果を割り当てる (ptw.veryl)
1:     always_comb {
2:         slave.ready  = 0;
3:         slave.rvalid = 0;
4:         slave.rdata  = 0;
5:         slave.expt   = 0;
6: 
7:         case state {
8:             State::IDLE         : slave.ready = 1;
9:             State::EXECUTE_VALID: {
10:                 slave.ready  = master.rvalid;
11:                 slave.rvalid = master.rvalid;
12:                 slave.rdata  = master.rdata;
13:                 slave.expt   = master.expt;
14:             }
15:             default: {}
16:         }
17:     }

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

リスト18.49: リスト18.49: 状態を遷移する (ptw.veryl)
1:     function accept_request_ff () {
2:         slave_saved.valid = slave.ready && slave.valid;
3:         if slave.ready && slave.valid {
4:             slave_saved.addr  = slave.addr;
5:             slave_saved.wen   = slave.wen;
6:             slave_saved.wdata = slave.wdata;
7:             slave_saved.wmask = slave.wmask;
8:             if paging_enabled {
9:                 // TODO
10:             } else {
11:                 state         = if master.ready ? State::EXECUTE_VALID : State::EXECUTE_READY;
12:                 physical_addr = slave.addr;
13:             }
14:         } else {
15:             state = State::IDLE;
16:         }
17:     }
18: 
19:     function on_clock () {
20:         case state {
21:             State::IDLE         : accept_request_ff();
22:             State::EXECUTE_READY: if master.ready {
23:                 state = State::EXECUTE_VALID;
24:             }
25:             State::EXECUTE_VALID: if master.rvalid {
26:                 accept_request_ff();
27:             }
28:             default: {}
29:         }
30:     }
31: 
32:     function on_reset () {
33:         state             = State::IDLE;
34:         physical_addr     = 0;
35:         slave_saved.valid = 0;
36:         slave_saved.addr  = 0;
37:         slave_saved.wen   = 0;
38:         slave_saved.wdata = 0;
39:         slave_saved.wmask = 0;
40:     }
41: 
42:     always_ff {
43:         if_reset {
44:             on_reset();
45:         } else {
46:             on_clock();
47:         }
48:     }

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

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

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

リスト18.50: リスト18.50: ptwモジュールとmmio_controllerモジュールの間のインターフェースを作成する (top.veryl)
1:     inst ptw_membus     : Membus;

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

リスト18.51: リスト18.51: 調停処理をptwモジュール向けのものに変更する (top.veryl)
1:     always_ff {
2:         if_reset {
3:             memarb_last_i = 0;
4:         } else {
5:             if ptw_membus.ready {
6:                 memarb_last_i = !d_membus.valid;
7:             }
8:         }
9:     }
10: 
11:     always_comb {
12:         i_membus.ready  = ptw_membus.ready && !d_membus.valid;
13:         i_membus.rvalid = ptw_membus.rvalid && memarb_last_i;
14:         i_membus.rdata  = ptw_membus.rdata;
15:         i_membus.expt   = ptw_membus.expt;
16: 
17:         d_membus.ready  = ptw_membus.ready;
18:         d_membus.rvalid = ptw_membus.rvalid && !memarb_last_i;
19:         d_membus.rdata  = ptw_membus.rdata;
20:         d_membus.expt   = ptw_membus.expt;
21: 
22:         ptw_membus.valid = i_membus.valid | d_membus.valid;
23:         if d_membus.valid {
24:             ptw_membus.addr  = d_membus.addr;
25:             ptw_membus.wen   = d_membus.wen;
26:             ptw_membus.wdata = d_membus.wdata;
27:             ptw_membus.wmask = d_membus.wmask;
28:         } else {
29:             ptw_membus.addr  = i_membus.addr;
30:             ptw_membus.wen   = 0; // 命令フェッチは常に読み込み
31:             ptw_membus.wdata = 'x;
32:             ptw_membus.wmask = 'x;
33:         }
34:     }

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

リスト18.52: リスト18.52: ptwモジュールが処理する要求が命令フェッチによるものかを判定する (top.veryl)
1:     let ptw_is_inst  : logic = (i_membus.ready && i_membus.valid) || // inst ack or
2:      !(d_membus.ready && d_membus.valid) && memarb_last_i; // data not ack & last ack is inst

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

リスト18.53: リスト18.53: ptwモジュールをインスタンス化する (top.veryl)
1:     inst ptw_ctrl: ptw_ctrl_if;
2:     inst paging_unit: ptw (
3:         clk                 ,
4:         rst                 ,
5:         is_inst: ptw_is_inst,
6:         slave  : ptw_membus ,
7:         master : mmio_membus,
8:         ctrl   : ptw_ctrl   ,
9:     );

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

リスト18.54: リスト18.54: coreモジュールにptw_ctrl_ifインターフェースを追加する (core.veryl)
1: module core (
2:     clk     : input   clock               ,
3:     rst     : input   reset               ,
4:     i_membus: modport core_inst_if::master,
5:     d_membus: modport core_data_if::master,
6:     led     : output  UIntX               ,
7:     aclint  : modport aclint_if::slave    ,
8:     ptw_ctrl: modport ptw_ctrl_if::master ,
9: ) {
リスト18.55: リスト18.55: ptw_ctrl_ifインターフェースを割り当てる (top.veryl)
1:     inst c: core (
2:         clk                      ,
3:         rst                      ,
4:         i_membus: i_membus_core  ,
5:         d_membus: d_membus_core  ,
6:         led                      ,
7:         aclint  : aclint_core_bus,
8:         ptw_ctrl                 ,
9:     );

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

リスト18.56: リスト18.56: csunitモジュールにptw_ctrl_ifインターフェースを追加する (csrunit.veryl)
1:     membus     : modport core_data_if::master    ,
2:     ptw_ctrl   : modport ptw_ctrl_if::master     ,
3: ) {
リスト18.57: リスト18.57: csrunitモジュールのインスタンスにptw_ctrl_ifインターフェースを割り当てる (core.veryl)
1:         membus     : d_membus             ,
2:         ptw_ctrl                          ,
3:     );
リスト18.58: リスト18.58: インターフェースにCSRの値を割り当てる (csrunit.veryl)
1:     always_comb {
2:         ptw_ctrl.priv = mode;
3:         ptw_ctrl.satp = satp;
4:         ptw_ctrl.mxr  = mstatus_mxr;
5:         ptw_ctrl.sum  = mstatus_sum;
6:         ptw_ctrl.mprv = mstatus_mprv;
7:         ptw_ctrl.mpp  = mstatus_mpp;
8:     }

18.9 Sv39の実装

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

18.9.1 定数の定義

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

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

リスト18.59: リスト18.59: sv39util.veryl
1: import eei::*;
2: package sv39util {
3:     const PAGESIZE: u32      = 12;
4:     const PTESIZE : u32      = 8;
5:     const LEVELS  : logic<2> = 3;
6: 
7:     type Level = logic<2>;
8: 
9:     // 有効な仮想アドレスか判定する
10:     function is_valid_vaddr (
11:         va: input Addr,
12:     ) -> logic {
13:         let hiaddr: logic<26> = va[msb:38];
14:         return &hiaddr || &~hiaddr;
15:     }
16: 
17:     // 仮想アドレスのVPN[level]フィールドを取得する
18:     function vpn (
19:         va   : input Addr ,
20:         level: input Level,
21:     ) -> logic<9> {
22:         return case level {
23:             0      : va[20:12],
24:             1      : va[29:21],
25:             2      : va[38:30],
26:             default: 0,
27:         };
28:     }
29: 
30:     // 最初にフェッチするPTEのアドレスを取得する
31:     function get_first_pte_address (
32:         satp: input UIntX,
33:         va  : input Addr ,
34:     ) -> Addr {
35:         return {
36:             1'b0 repeat XLEN - 44 - PAGESIZE,
37:             satp[43:0],
38:             vpn(va, 2),
39:             1'b0 repeat $clog2(PTESIZE)
40:         };
41:     }
42: }

18.9.2 PTEの定義

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

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

リスト18.60: リスト18.60: pte.veryl
1: import eei::*;
2: import sv39util::*;
3: 
4: interface PTE39 {
5:     var value: UIntX;
6: 
7:     function v () -> logic { return value[0]; }
8:     function r () -> logic { return value[1]; }
9:     function w () -> logic { return value[2]; }
10:     function x () -> logic { return value[3]; }
11:     function u () -> logic { return value[4]; }
12:     function a () -> logic { return value[6]; }
13:     function d () -> logic { return value[7]; }
14: 
15:     function reserved -> logic<10> { return value[63:54]; }
16: 
17:     function ppn2 () -> logic<26> { return value[53:28]; }
18:     function ppn1 () -> logic<9> { return value[27:19]; }
19:     function ppn0 () -> logic<9> { return value[18:10]; }
20:     function ppn  () -> logic<44> { return value[53:10]; }
21: }

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

リスト18.61: リスト18.61: PTEの値を使った関数を定義する (pte.veryl)
1:     // leaf PTEか判定する
2:     function is_leaf () -> logic { return r() || x(); }
3: 
4:     // leaf PTEのとき、PPNがページサイズに整列されているかどうかを判定する
5:     function is_ppn_aligned (
6:         level: input Level,
7:     ) -> logic {
8:         return case level {
9:             0      : 1,
10:             1      : ppn0() == 0,
11:             2      : ppn1() == 0 && ppn0() == 0,
12:             default: 1,
13:         };
14:     }
15: 
16:     // 有効なPTEか判定する
17:     function is_valid (
18:         level: input Level,
19:     ) -> logic {
20:         if !v() || reserved() != 0 || !r() && w() {
21:             return 0;
22:         }
23:         if is_leaf() && !is_ppn_aligned(level) {
24:             return 0;
25:         }
26:         if !is_leaf() && level == 0 {
27:             return 0;
28:         }
29:         return 1;
30:     }
31: 
32:     // 次のlevelのPTEのアドレスを得る
33:     function get_next_pte_addr (
34:         level: input Level,
35:         va   : input Addr ,
36:     ) -> Addr {
37:         return {
38:             1'b0 repeat XLEN - 44 - PAGESIZE,
39:             ppn(),
40:             vpn(va, level - 1),
41:             1'b0 repeat $clog2(PTESIZE)
42:         };
43:     }
44: 
45:     // PTEと仮想アドレスから物理アドレスを生成する
46:     function get_physical_address (
47:         level: input Level,
48:         va   : input Addr ,
49:     ) -> Addr {
50:         return {
51:             8'b0, ppn2(), case level {
52:                 0: {
53:                     ppn1(), ppn0()
54:                 },
55:                 1: {
56:                     ppn1(), vpn(va, 0)
57:                 },
58:                 2: {
59:                     vpn(va, 1), vpn(va, 0)
60:                 },
61:                 default: 18'b0,
62:             }, va[11:0]
63:         };
64:     }
65: 
66:     // A、Dビットを更新する必要があるかを判定する
67:     function need_update_ad (
68:         wen: input logic,
69:     ) -> logic {
70:         return !a() || wen && !d();
71:     }
72: 
73:     // A, Dビットを更新したPTEの下位8ビットを生成する
74:     function get_updated_ad (
75:         wen: input logic,
76:     ) -> logic<8> {
77:         let a: logic<8> = 1 << 6;
78:         let d: logic<8> = wen as u8 << 7;
79:         return value[7:0] | a | d;
80:     }

18.9.3 ptwモジュールの実装

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

リスト18.62: リスト18.62: sv39utilパッケージをimportする (ptw.veryl)
1: import sv39util::*;

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

リスト18.63: リスト18.63: PTE39インターフェースをインスタンス化する (ptw.veryl)
1:     inst pte      : PTE39;
2:     assign pte.value = master.rdata;
状態の遷移図 (点線の状態で新しく要求を受け付け、二重丸の状態で結果を返す)

図18.12: 状態の遷移図 (点線の状態で新しく要求を受け付け、二重丸の状態で結果を返す)

仮想アドレスを変換するための状態を追加します(リスト18.64)。本章ではページングが有効な時に、state図18.12のように遷移するようにします。

リスト18.64: リスト18.64: 状態の定義 (ptw.veryl)
1:     enum State {
2:         IDLE,
3:         WALK_READY,
4:         WALK_VALID,
5:         SET_AD,
6:         EXECUTE_READY,
7:         EXECUTE_VALID,
8:         PAGE_FAULT,
9:     }

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

リスト18.65: リスト18.65: レジスタの定義 (ptw.veryl)
1:     var physical_addr: Addr    ;
2:     var taddr        : Addr    ;
3:     var level        : Level   ;
4:     var wdata_ad     : logic<8>;
リスト18.66: リスト18.66: レジスタをリセットする (ptw.veryl)
1:     function on_reset () {
2:         state             = State::IDLE;
3:         physical_addr     = 0;
4:         taddr             = 0;
5:         level             = 0;
6:         wdata_ad          = 0;

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

リスト18.67: リスト18.67: masterに要求を割り当てる (ptw.veryl)
1: case state {
2:     State::IDLE      : accept_request_comb();
3:     State::WALK_READY: assign_master      (taddr, 0, 0, 0);
4:     State::SET_AD    : assign_master      (taddr, 1, // wen = 1
5:      {1'b0 repeat MEMBUS_DATA_WIDTH - 8, wdata_ad}, // wdata
6:      {1'b0 repeat XLEN / 8 - 1, 1'b1} // wmask
7:     );
8:     State::EXECUTE_READY: assign_master(physical_addr, slave_saved.wen, slave_saved.wdata, slave_saved.wmask);
9:     State::EXECUTE_VALID: if master.rvalid {
10:         accept_request_comb();
11:     }
12:     default: {}
13: }

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

リスト18.68: リスト18.68: ページフォルト例外のときの結果を割り当てる (ptw.veryl)
1: State::PAGE_FAULT: {
2:     slave.rvalid          = 1;
3:     slave.expt.valid      = 1;
4:     slave.expt.page_fault = 1;
5: }

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

リスト18.69: リスト18.69: ページングが有効なときの要求の受け入れ (ptw.veryl)
1: if paging_enabled {
2:     state = if is_valid_vaddr(slave.addr) ? State::WALK_READY : State::PAGE_FAULT;
3:     taddr = get_first_pte_address(ctrl.satp, slave.addr);
4:     level = LEVELS - 1;
5: } else {
6:     state         = if master.ready ? State::EXECUTE_VALID : State::EXECUTE_READY;
7:     physical_addr = slave.addr;
8: }

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

リスト18.70: リスト18.70: ページフォルト例外が発生したときの状態遷移 (ptw.veryl)
1: State::PAGE_FAULT: state = State::IDLE;

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

リスト18.71: リスト18.71: A、Dビットを更新したときの状態遷移 (ptw.veryl)
1: State::SET_AD: if master.ready {
2:     state = State::EXECUTE_READY;
3: }

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

リスト18.72: リスト18.72: ページにアクセスする権限があるかを判定する関数 (ptw.veryl)
1:     function check_permission (
2:         req: modport Membus::all_input,
3:     ) -> logic {
4:         let priv: PrivMode = if is_inst || !ctrl.mprv ? ctrl.priv : ctrl.mpp;
5: 
6:         // U-mode access with PTE.U=0
7:         let u_u0: logic = priv == PrivMode::U && !pte.u();
8:         // S-mode load/store with PTE.U=1 & sum=0
9:         let sd_u1: logic = !is_inst && priv == PrivMode::S && pte.u() && !ctrl.sum;
10:         // S-mode execute with PTE.U=1
11:         let si_u1: logic = is_inst && priv == PrivMode::S && pte.u();
12: 
13:         // execute without PTE.X
14:         let x: logic = is_inst && !pte.x();
15:         // write without PTE.W
16:         let w: logic = !is_inst && req.wen && !pte.w();
17:         // read without PTE.R (MXR)
18:         let r: logic = !is_inst && !req.wen && !pte.r() && !(pte.x() && ctrl.mxr);
19: 
20:         return !(u_u0 | sd_u1 | si_u1 | x | w | r);
21:     }

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

リスト18.73: リスト18.73: PTEのフェッチとPTEの確認 (ptw.veryl)
1: State::WALK_READY: if master.ready {
2:     state = State::WALK_VALID;
3: }
4: State::WALK_VALID: if master.rvalid {
5:     if !pte.is_valid(level) {
6:         state = State::PAGE_FAULT;
7:     } else {
8:         if pte.is_leaf() {
9:             if check_permission(slave_saved) {
10:                 physical_addr = pte.get_physical_address(level, slave_saved.addr);
11:                 if pte.need_update_ad(slave_saved.wen) {
12:                     state    = State::SET_AD;
13:                     wdata_ad = pte.get_updated_ad(slave_saved.wen);
14:                 } else {
15:                     state = State::EXECUTE_READY;
16:                 }
17:             } else {
18:                 state = State::PAGE_FAULT;
19:             }
20:         } else {
21:             // read next pte
22:             state = State::WALK_READY;
23:             taddr = pte.get_next_pte_addr(level, slave_saved.addr);
24:             level = level - 1;
25:         }
26:     }
27: }

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

18.10 SFENCE.VMA命令の実装

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

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

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

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

リスト18.74: リスト18.74: SFENCE.VMA命令を有効な命令としてデコードする (inst_decoder.veryl)
1:  bits == 32'h10200073 || //SRET
2:  bits == 32'h10500073 || // WFI
3:  f7 == 7'b0001001 && bits[11:7] == 0, // SFENCE.VMA

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

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

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

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

リスト18.75: リスト18.75: mstatusレジスタの書き込みマスクを変更する (csrunit.veryl)
1:     const MSTATUS_WMASK   : UIntX = 'h0000_0000_007e_19aa as UIntX;
リスト18.76: リスト18.76: mstatus.TVMを示す変数を作成する (csrunit.veryl)
1:     let mstatus_tvm : logic    = mstatus[20];

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

リスト18.77: リスト18.77: SFENCE.VMA命令かどうかを判定する (csrunit.veryl)
1:     let is_sfence_vma: logic = ctrl.is_csr && ctrl.funct7 == 7'b0001001 && ctrl.funct3 == 0 && rd_addr == 0;
リスト18.78: リスト18.78: SFENCE.VMA命令の例外を判定する (csrunit.veryl)
1:     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.79: リスト18.79: 例外を発生させる (csrunit.veryl)
1:     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);
2:     let expt_cause: UIntX = switch {
3:         ...
4:         expt_tvm               : CsrCause::ILLEGAL_INSTRUCTION,
5:         default                : 0,
6:     };

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

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

18.11.1 CSRの変更

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

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

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

リスト18.80: リスト18.80: csrunitモジュールのポートにフラッシュするためのフラグを追加する (csrunit.veryl)
1: flush      : output  logic                   ,
2: minstret   : input   UInt64                  ,
リスト18.81: リスト18.81: csru_flush変数の定義 (core.veryl)
1:     var csru_trap_return: logic   ;
2:     var csru_flush      : logic   ;
3:     var minstret        : UInt64  ;
リスト18.82: リスト18.82: csrunitモジュールのflushフラグをcsru_flushに割り当てる (core.veryl)
1: flush      : csru_flush           ,
2: minstret                          ,

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

リスト18.83: リスト18.83: satp、mstatus、sstatusレジスタが変更されるときにflushを1にする (csrunit.veryl)
1:     let wsc_flush: logic = is_wsc && (csr_addr == CsrAddr::SATP || csr_addr == CsrAddr::MSTATUS || csr_addr == CsrAddr::SSTATUS);
2:     assign flush     = valid && wsc_flush;

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

リスト18.84: リスト18.84: csru_flushが1のときにパイプラインをフラッシュする (core.veryl)
1:     assign control_hazard         = mems_valid && (csru_raise_trap || mems_ctrl.is_jump || memq_rdata.br_taken || csru_flush);
2:     assign control_hazard_pc_next = if csru_raise_trap ? csru_trap_vector : // trap
3:      if csru_flush ? mems_pc + 4 : memq_rdata.jump_addr; // flush or jump

18.11.2 FENCE.I命令の実装

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

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

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

リスト18.85: リスト18.85: FENCE.I命令かどうかを判定する (csrunit.veryl)
1:     let is_fence_i: logic = inst_bits[6:0] == OP_MISC_MEM && ctrl.funct3 == 3'b001;
リスト18.86: リスト18.86: FENCE.I命令のときにflushを1にする (csrunit.veryl)
1:     assign flush     = valid && (wsc_flush || is_fence_i);

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