第18章
S-modeの実装 (2. 仮想記憶システム)
18.1 概要
18.1.1 仮想記憶システム
仮想記憶(Virtual Memory)とは、メモリを管理する手法の一種です。仮想的なアドレス(virtual address、仮想アドレス)を実際のアドレス(real address、実アドレス)に変換することにより、実際のアドレス空間とは異なるアドレス空間を提供することができます。実アドレスのことを物理アドレス(physical address)と呼ぶことがあります。
仮想記憶を利用すると、次のような動作を実現できます。
- 連続していない物理アドレス空間を仮想的に連続したアドレス空間として扱う。
- 特定のアドレスにしか配置できない(特定のアドレスで動くことを前提としている)プログラムを、そのアドレスとは異なる物理アドレスに配置して実行する。
- アプリケーションごとにアドレス空間を分離する。
一般的に仮想記憶システムはハードウェアによって提供されます。メモリアクセスを処理するハードウェア部品のことをメモリ管理ユニット(Memory Management Unit, MMU)と呼びます。
18.1.2 ページング方式
仮想記憶システムを実現する方式の1つにページング方式(Paging)があります。ページング方式は、物理アドレス空間の一部をページ(Page)という単位に割り当て、ページを参照するための情報をページテーブル(Page Table)に格納します。ページテーブルに格納する情報の単位のことをページテーブルエントリ(Page Table Entry、PTE)と呼びます。仮想アドレスから物理アドレスへの変換はページテーブルにあるPTEを参照して行います(図18.1)。

図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レジスタ

図18.2: satpレジスタ
RISC-Vの仮想記憶システムはsatpレジスタによって制御します。
MODEは仮想アドレスの変換方式を指定するフィールドです。方式と値は表18.1のように対応しています。方式がBare(0
)のときはアドレス変換を行いません(仮想アドレス=物理アドレス)。
表18.1: 方式とMODEの値の対応
方式 | MODE |
---|---|
Bare | 0 |
Sv39 | 8 |
Sv48 | 9 |
Sv57 | 10 |
ASID(Address Space IDentifier)は仮想アドレスが属するアドレス空間のIDです。動かすアプリケーションによってIDを変えることでMMUにアドレス変換の高速化のヒントを与えることができます。本章ではキャッシュ機構を持たない単純なモジュールを実装するため、ASIDを無視したアドレス変換を実装します*2。
[*2] PTWはページエントリをキャッシュすることで高速化できます。ASIDが異なるときのキャッシュは利用することができません。キャッシュ機構(TLB)は応用編で実装します。

図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のフェッチ

図18.6: PTEのアドレス
ページングが有効なとき、まずroot PTEをフェッチします。ここでlevelという変数の値を2
とします。
root PTEの物理アドレスは、satpレジスタのPPNフィールドと仮想アドレスのVPN[level]
フィールドを結合し、log2(PTESIZE)
だけ左シフトしたアドレスになります。このアドレスは、PPNフィールドを12ビット左シフトしたアドレスに存在するページテーブルの、VPN[level]番目の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が立っているページの実行はできません。これらに違反した場合、ページフォルト例外が発生します。

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

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

図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)。
本章では、仮想記憶システムを次の順序で実装します。
- PTWで発生する例外をcsrunitモジュールに伝達する
- Bareにだけ対応したアドレス変換モジュール(ptw)を実装する
- satpレジスタ、mstatusのMXR、SUM、MPRVビットを実装する
- Sv39を実装する
- SFENCE.VMA命令、FENCEI命令を実装する

図18.11: PTWと他のモジュールの接続
18.5 メモリで発生する例外の実装
PTWで発生した例外は、最終的にcsrunitモジュールで処理します。そのために、例外の情報をメモリのインターフェースを使って伝達します。
ページングによって発生する例外のcauseをCsrCause
型に追加します(リスト18.1)。
1: INSTRUCTION_PAGE_FAULT = 12, 2: LOAD_PAGE_FAULT = 13, 3: STORE_AMO_PAGE_FAULT = 15,
18.5.1 例外を伝達する
構造体の定義
MemException
構造体を定義します(リスト18.2)。メモリアクセス中に発生する例外の情報はこの構造体で管理します。
1: struct MemException { 2: valid : logic, 3: page_fault: logic, 4: }
membus_if
、core_data_if
、core_inst_if
インターフェースにMemException
構造体を追加します(リスト18.3、リスト18.4、リスト18.5、リスト18.6)。インターフェースのrvalid
が1
で、構造体のvalid
とis_page_fault
が1
ならページフォルト例外が発生したことを示します。
1: var expt : eei::MemException ;
1: modport master { 2: ... 3: expt : input , 4: ... 5: }
1: modport slave { 2: ... 3: expt : output, 4: ... 5: }
1: modport response { 2: rvalid: output, 3: rdata : output, 4: expt : output, 5: }
mmio_controllerモジュールの対応
mmio_controllerモジュールで構造体の値をすべて0
に設定します(リスト18.7、リスト18.8)。いまのところ、デバイスは例外を発生させません。
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_if
、core_inst_if
インターフェースに伝達します。
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))。
1: struct fetch_fifo_type { 2: addr: Addr , 3: bits: logic <MEMBUS_DATA_WIDTH>, 4: expt: MemException , 5: }
1: struct issue_fifo_type { 2: addr : Addr , 3: bits : Inst , 4: is_rvc: logic , 5: expt : MemException, 6: }
メモリからの例外情報をfetch_fifo
に保存します(リスト18.11)。
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
に例外を書き込みます。
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;
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;
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)。
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モジュールの対応
state
がState::Init
以外の時に例外が発生した場合、すぐに結果を返すようにします(リスト18.16、リスト18.17、リスト18.18、)。例外が発生したクロックでは要求を受け付けないようにします。
1: always_comb { 2: slave.ready = 0; 3: slave.rvalid = 0; 4: slave.rdata = 0; 5: slave.expt = master.expt;
1: default: {} 2: } 3: 4: if state != State::Init && master.expt.valid { 5: slave.ready = 0; 6: slave.rvalid = 1; 7: } 8: }
1: State::AMOStoreValid: accept_request_comb(); 2: default : {} 3: } 4: 5: if state != State::Init && master.expt.valid { 6: reset_master(); 7: } 8: }
例外が発生したら、state
をState::Init
にリセットします(リスト18.19)。
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)。
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)。
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: ) {
1: inst csru: csrunit ( 2: ... 3: mem_addr : memu_addr , 4: ... 5: membus : d_membus , 6: );
1: let expt_memory_fault : logic = membus.rvalid && membus.expt.valid;
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)。
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)。
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)。
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)。
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};
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
でリセットします。
1: var satp : UIntX ;
1: satp = 0;
1: CsrAddr::SATP : satp,
1: const SATP_WMASK : UIntX = 'hffff_ffff_ffff_ffff;
1: CsrAddr::SATP : SATP_WMASK,
satpレジスタは、MODEフィールドに書き込もうとしている値がサポートしないMODEなら、satpレジスタの変更を全ビットについて無視すると定められています。
本章ではBareとSv39だけをサポートするため、MODEには0
と8
のみ書き込めるようにして、それ以外の値を書き込もうとしたらsatpレジスタへの書き込みを無視します(リスト18.35、リスト18.36)。
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: }
1: CsrAddr::SATP : satp = validate_satp(satp, wdata);
18.7 mstatusのMXR、SUM、MPRVビットの実装
mstatusレジスタのMXR、SUM、MPRVビットを変更できるようにします(リスト18.37、リスト18.38)。
1: const MSTATUS_WMASK : UIntX = 'h0000_0000_006e_19aa as UIntX;
1: const SSTATUS_WMASK : UIntX = 'h0000_0000_000c_0122 as UIntX;
それぞれのビットを示す変数を作成します(リスト18.39、リスト18.40)。
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
を設定します。
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)。
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)。
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)。
1: let paging_enabled: logic = ctrl.is_enabled(is_inst);
状態の管理のためにState
型を定義します(リスト18.44)。
1: enum State { 2: IDLE, 3: EXECUTE_READY, 4: EXECUTE_VALID, 5: } 6: 7: 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
からの要求を保存しておくためのインターフェースをインスタンス化します(リスト18.45)。
1: inst slave_saved: Membus;
状態に基づいて、master
に要求を割り当てます(リスト18.46、リスト18.47)。State::EXECUTE_READY
でmaster
に要求を割り当てるとき、physical_addr
レジスタの値をアドレスに割り当てるようにします。
1: var physical_addr: Addr;
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)。
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
に要求を保存します。
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)。
1: inst ptw_membus : Membus;
調停処理をptwモジュール向けのものに変更します(リスト18.51)。
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)。
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)。
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)。
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: ) {
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)。
1: membus : modport core_data_if::master , 2: ptw_ctrl : modport ptw_ctrl_if::master , 3: ) {
1: membus : d_membus , 2: ptw_ctrl , 3: );
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のアドレス変換」で使用しているものと同じです。
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)。
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)。
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)。
1: import sv39util::*;
PTE39インターフェースをインスタンス化します(リスト18.63)。value
にはmaster
のロード結果を割り当てます。
1: inst pte : PTE39; 2: assign pte.value = master.rdata;

図18.12: 状態の遷移図 (点線の状態で新しく要求を受け付け、二重丸の状態で結果を返す)
仮想アドレスを変換するための状態を追加します(リスト18.64)。本章ではページングが有効な時に、state
が図18.12のように遷移するようにします。
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)。
1: var physical_addr: Addr ; 2: var taddr : Addr ; 3: var level : Level ; 4: var wdata_ad : logic<8>;
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ビットのみの書き込みマスクを設定します。
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)。
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
とします。
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)。
1: State::PAGE_FAULT: state = State::IDLE;
A、Dビットを更新するとき、メモリが書き込み要求を受け入れたら、状態をState::EXECUTE_READY
に移動します(リスト18.71)。
1: State::SET_AD: if master.ready { 2: state = State::EXECUTE_READY; 3: }
ページにアクセスする権限があるかをPTEと要求から判定する関数を定義します(リスト18.72)。条件の詳細は「18.3 Sv39のアドレス変換」を確認してください。
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)。
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)。
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)。
1: const MSTATUS_WMASK : UIntX = 'h0000_0000_007e_19aa as UIntX;
1: let mstatus_tvm : logic = mstatus[20];
特権レベルを確認して、例外を発生させます(リスト18.77、リスト18.78、リスト18.79)。
1: let is_sfence_vma: logic = ctrl.is_csr && ctrl.funct7 == 7'b0001001 && ctrl.funct3 == 0 && rd_addr == 0;
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));
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)。
1: flush : output logic , 2: minstret : input UInt64 ,
1: var csru_trap_return: logic ; 2: var csru_flush : logic ; 3: var minstret : UInt64 ;
1: flush : csru_flush , 2: minstret ,
satp、mstatus、sstatusレジスタが変更されるときにflush
を1
にします(リスト18.83)。
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;
flush
が1
のとき、制御ハザードが発生したことにしてパイプラインをフラッシュします(リスト18.84)。
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)。
1: let is_fence_i: logic = inst_bits[6:0] == OP_MISC_MEM && ctrl.funct3 == 3'b001;
1: assign flush = valid && (wsc_flush || is_fence_i);
riscv-testsの-v-
を含むテストを実行し、実装している命令のテストに成功することを確認してください。