Verylで作るCPU
Star

第11章
Memory-mapped I/Oの実装

11.1 Memory-mapped I/Oとは何か?

これまでの実装では、CPUに内蔵された1つの大きなメモリ空間、1つのメモリデバイス(memoryモジュール)に命令データを格納、実行し、データのロードストア命令も同じメモリに対して実行してきました。

一般に流通するコンピュータは複数のデバイスに接続されています。CPUが起動すると、読み込み専用の小さなメモリ(ROM)に格納されたプログラムから命令の実行を開始します。プログラムは周辺デバイスの初期化などを行ったあと、動かしたいアプリケーションの命令やデータをRAMに展開して、制御をアプリケーションに移します。

CPUがデバイスにアクセスする方法にはCSRやメモリ空間を経由する方法があります。一般的な方法はメモリ空間を通じてデバイスにアクセスする方法であり、この方式のことをメモリマップドIO(Memory-mapped I/O, MMIO)と呼びます。メモリ空間の一部を、デバイスにアクセスするための空間として扱うことを、メモリ(またはアドレス)にマップすると呼びます。RAMとROMもメモリデバイスであり、異なるアドレスにマップされています。

メモリマップ

図11.1: メモリマップ

本章ではCPUのメモリ部分をRAM(Random Access Memory)*1とROM(Read Only Memory)に分割し、アクセスするアドレスに応じてアクセスするデバイスを切り替える機能を実装します。また、デバッグ用の入出力デバイス(64ビットのレジスタ)も実装します。デバイスとメモリ空間の対応は図11.1のように設定します。図11.1のようにメモリがどのように配置されているかを示す図のことをメモリマップ(Memory map)と呼びます。あるメモリ空間の先頭アドレスのことをベースアドレス(base address)と呼ぶことがあります。

[*1] 本章では実際のRAMデバイスへのアクセスを実装せずmemoryモジュールで代用します。FPGAに合成するときに実際のデバイスへのアクセスに置き換えます。

11.2 定数の定義

eeiパッケージに定義しているメモリの定数をRAM用の定数に変更します。また、新しくRAMのベースアドレス、メモリバスのデータ幅、ROMのメモリマップを示す定数を定義してください(リスト11.1)。デバッグ入出力デバイス(レジスタ)の位置は、topモジュールのポートで定義します(リスト11.9)。

リスト11.1: リスト11.1: メモリマップの定義 (eei.veryl)
1:     // メモリバスのデータ幅
2:     const MEMBUS_DATA_WIDTH: u32 = 64;
3:     // メモリのアドレス幅
4:     const MEM_ADDR_WIDTH: u32 = 16;
5: 
6:     // RAM
7:     const RAM_ADDR_WIDTH: u32  = 16;
8:     const RAM_DATA_WIDTH: u32  = 64;
9:     const MMAP_RAM_BEGIN: Addr = 'h8000_0000 as Addr;
10: 
11:     // ROM
12:     const ROM_ADDR_WIDTH: u32  = 9;
13:     const ROM_DATA_WIDTH: u32  = 64;
14:     const MMAP_ROM_BEGIN: Addr = 'h1000 as Addr;
15:     const MMAP_ROM_END  : Addr = MMAP_ROM_BEGIN + 'h3ff as Addr;

MEM_DATA_WIDTHMEM_ADDR_WIDTHを使っている部分をMEMBUS_DATA_WIDTHXLENに置き換えます。MEMBUS_DATA_WIDTHXLENを使うmembus_ifインターフェースに別名Membusをつけて利用します(リスト11.2リスト11.3リスト11.4リスト11.5)。

リスト11.2: リスト11.2: 別名の定義 (membus_if.veryl)
1: alias interface Membus = membus_if::<eei::MEMBUS_DATA_WIDTH, eei::XLEN>;
リスト11.3: リスト11.3: Membusに置き換える (core.veryl)
1: module core (
2:     clk     : input   clock                          ,
3:     rst     : input   reset                          ,
4:     i_membus: modport membus_if::<ILEN, XLEN>::master,
5:     d_membus: modport Membus::master                 ,
6:     led     : output  UIntX                          ,
7: ) {
リスト11.4: リスト11.4: Membusに置き換える (memunit.veryl)
1:     membus: modport Membus::master, // メモリとのinterface
リスト11.5: リスト11.5: 定数名を変更する (memunit.veryl)
1:     var req_wen  : logic                       ;
2:     var req_addr : Addr                        ;
3:     var req_wdata: logic<MEMBUS_DATA_WIDTH>    ;
4:     var req_wmask: logic<MEMBUS_DATA_WIDTH / 8>;
5: 
6:     const W   : u32                      = XLEN;
7:     let D   : logic<MEMBUS_DATA_WIDTH> = membus.rdata;
8:     let sext: logic                    = ctrl.funct3[2] == 1'b0;

topモジュールでインスタンス化しているmembus_ifインターフェースのジェネリックパラメータを変更します(リスト11.6)。

リスト11.6: リスト11.6: ジェネリックパラメータを変更する / Membusに置き換える (top.veryl)
1:     inst membus  : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
2:     inst i_membus: membus_if::<ILEN, XLEN>; // 命令フェッチ用
3:     inst d_membus: Membus; // ロードストア命令用

addr_to_memaddr関数をジェネリック関数にして、呼び出すときにRAMのパラメータを使用するように変更します(リスト11.7リスト11.8、)。

リスト11.7: リスト11.7: addr_to_memaddr関数をジェネリック関数に変更する (top.veryl)
1:     // アドレスをデータ単位でのアドレスに変換する
2:     function addr_to_memaddr::<DATA_WIDTH: u32, ADDR_WIDTH: u32> (
3:         addr: input logic<XLEN>,
4:     ) -> logic<ADDR_WIDTH> {
5:         return addr[$clog2(DATA_WIDTH / 8)+:ADDR_WIDTH];
6:     }
リスト11.8: リスト11.8: ジェネリックパラメータを指定する (top.veryl)
1:         membus.valid = i_membus.valid | d_membus.valid;
2:         if d_membus.valid {
3:             membus.addr  = addr_to_memaddr::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>(d_membus.addr);
4:             membus.wen   = d_membus.wen;
5:             membus.wdata = d_membus.wdata;
6:             membus.wmask = d_membus.wmask;
7:         } else {
8:             membus.addr  = addr_to_memaddr::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>(i_membus.addr);
9:             membus.wen   = 0; // 命令フェッチは常に読み込み
10:             membus.wdata = 'x;
11:             membus.wmask = 'x;
12:         }

メモリに読み込むHEXファイルを指定するパラメータの名前を変更します(リスト11.9リスト11.10)。

リスト11.9: リスト11.9: パラメータ名を変更する (top.veryl)
1: module top #(
2:     param RAM_FILEPATH_IS_ENV: bit    = 1              ,
3:     param RAM_FILEPATH       : string = "RAM_FILE_PATH",
4: ) (
5:     clk          : input clock,
6:     rst          : input reset,
7:     MMAP_DBG_ADDR: input Addr ,
リスト11.10: リスト11.10: パラメータ名を変更する (top.veryl)
1:     inst ram: memory::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH> #(
2:         FILEPATH_IS_ENV: RAM_FILEPATH_IS_ENV,
3:         FILEPATH       : RAM_FILEPATH       ,
4:     ) (

シミュレータ用のC++プログラムも変更します(リスト11.11リスト11.12リスト11.13)。

リスト11.11: リスト11.11: 引数の名称を変える (tb_verilator.cpp)
1:     if (argc < 2) {
2:         std::cout << "Usage: " << argv[0] << " RAM_FILE_PATH [CYCLE]" << std::endl;
3:         return 1;
4:     }
リスト11.12: リスト11.12: 環境変数名を変える (tb_verilator.cpp)
1:     // 環境変数でメモリの初期化用ファイルを指定する
2:     const char* original_env = getenv("RAM_FILE_PATH");
3:     setenv("RAM_FILE_PATH", memory_file_path.c_str(), 1);
リスト11.13: リスト11.13: 環境変数名を変える (tb_verilator.cpp)
1:     // 環境変数を元に戻す
2:     if (original_env != nullptr){
3:         setenv("RAM_FILE_PATH", original_env, 1);
4:     }

11.3 mmio_controllerモジュールの作成

アクセスするアドレスに応じてアクセス先のデバイスを切り替えるモジュールを実装します。

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

リスト11.14: リスト11.14: mmio_controller.veryl
1: import eei::*;
2: 
3: module mmio_controller (
4:     clk     : input   clock        ,
5:     rst     : input   reset        ,
6:     req_core: modport Membus::slave,
7: ) {
8: 
9:     enum Device {
10:         UNKNOWN,
11:     }
12: 
13:     inst req_saved: Membus;
14: 
15:     var last_device : Device;
16:     var is_requested: logic ;
17: 
18:     // masterを0でリセットする
19:     function reset_membus_master (
20:         master: modport Membus::master_output,
21:     ) {
22:         master.valid = 0;
23:         master.addr  = 0;
24:         master.wen   = 0;
25:         master.wdata = 0;
26:         master.wmask = 0;
27:     }
28: 
29:     // すべてのデバイスのmasterをリセットする
30:     function reset_all_device_masters () {}
31: 
32:     // アドレスからデバイスを取得する
33:     function get_device (
34:         addr: input Addr,
35:     ) -> Device {
36:         return Device::UNKNOWN;
37:     }
38: 
39:     // デバイスのmasterにreqの情報を割り当てる
40:     function assign_device_master (
41:         req: modport Membus::all_input,
42:     ) {}
43: 
44:     // デバイスのrvalid、rdataをreqに割り当てる
45:     function assign_device_slave (
46:         device: input   Device          ,
47:         req   : modport Membus::response,
48:     ) {
49:         req.rvalid = 1;
50:         req.rdata  = 0;
51:     }
52: 
53:     // デバイスのreadyを取得する
54:     function get_device_ready (
55:         device: input Device,
56:     ) -> logic {
57:         return 1;
58:     }
59: 
60:     // デバイスのrvalidを取得する
61:     function get_device_rvalid (
62:         device: input Device,
63:     ) -> logic {
64:         return 1;
65:     }
66: 
67:     // req_coreの割り当て
68:     always_comb {
69:         req_core.ready  = 0;
70:         req_core.rvalid = 0;
71:         req_core.rdata  = 0;
72: 
73:         if req_saved.valid {
74:             if is_requested {
75:                 // 結果を返す
76:                 assign_device_slave(last_device, req_core);
77:                 req_core.ready      = get_device_rvalid(last_device);
78:             }
79:         } else {
80:             req_core.ready = 1;
81:         }
82:     }
83: 
84:     // デバイスのmasterの割り当て
85:     always_comb {
86:         reset_all_device_masters();
87:         if req_saved.valid {
88:             if is_requested {
89:                 if get_device_rvalid(last_device) {
90:                     // 新しく要求を受け入れる
91:                     if req_core.ready && req_core.valid {
92:                         assign_device_master(req_core);
93:                     }
94:                 }
95:             } else {
96:                 // デバイスにreq_savedを割り当てる
97:                 assign_device_master(req_saved);
98:             }
99:         } else {
100:             // 新しく要求を受け入れる
101:             if req_core.ready && req_core.valid {
102:                 assign_device_master(req_core);
103:             }
104:         }
105:     }
106: 
107:     // 新しく要求を受け入れる
108:     function accept_request () {
109:         req_saved.valid = req_core.ready && req_core.valid;
110:         if req_core.ready && req_core.valid {
111:             last_device  = get_device(req_core.addr);
112:             is_requested = get_device_ready(last_device);
113:             // reqを保存
114:             req_saved.addr  = req_core.addr;
115:             req_saved.wen   = req_core.wen;
116:             req_saved.wdata = req_core.wdata;
117:             req_saved.wmask = req_core.wmask;
118:         }
119:     }
120: 
121:     function on_clock () {
122:         if req_saved.valid {
123:             if is_requested {
124:                 if get_device_rvalid(last_device) {
125:                     accept_request();
126:                 }
127:             } else {
128:                 is_requested = get_device_ready(last_device);
129:             }
130:         } else {
131:             accept_request();
132:         }
133:     }
134: 
135:     function on_reset () {
136:         last_device         = Device::UNKNOWN;
137:         is_requested        = 0;
138:         reset_membus_master(req_saved);
139:     }
140: 
141:     always_ff {
142:         if_reset {
143:             on_reset();
144:         } else {
145:             on_clock();
146:         }
147:     }
148: }

mmio_controllerモジュールの関数の引数にmembus_ifインターフェースを使うために、新しくmodportを宣言します(リスト11.15)。

リスト11.15: リスト11.15: modport宣言を追加する (membus_if.veryl)
1:     modport all_input {
2:         ..input
3:     }
4: 
5:     modport response {
6:         rvalid: output,
7:         rdata : output,
8:     }
9: 
10:     modport slave_output {
11:         ready: output,
12:         ..same(response)
13:     }
14: 
15:     modport master_output {
16:         valid: output,
17:         addr : output,
18:         wen  : output,
19:         wdata: output,
20:         wmask: output,
21:     }

mmio_controllerモジュールはreq_coreからメモリアクセス要求を受け付け、アクセス対象のモジュールからの結果を返すモジュールです。

Device型は実装しているデバイスを表現するための列挙型です(リスト11.16)。まだデバイスを接続していないので、不明なデバイス(Device::UNKNOWN)だけ定義しています。

リスト11.16: リスト11.16: Device型の定義 (mmio_controller.veryl)
1:     enum Device {
2:         UNKNOWN,
3:     }

reset_membus_master、reset_all_device_masters関数はインターフェースの値の割り当てを0でリセットするためのユーティリティ関数です。名前がget_device_、assign_deviceから始まる関数は、デバイスの状態を取得したり、インターフェースに値を割り当てる関数です。get_device関数はアドレスに対応するDeviceを取得する関数です。

always_comb、always_ffブロックはこれらの関数を利用してメモリアクセスを制御します。

always_ffブロックは、メモリアクセス要求の処理中ではない場合とメモリアクセスが終わった場合にメモリアクセス要求を受け入れます。要求を受け入れるとき、req_coreの値をreq_savedに保存します。

always_combブロックはデバイスにアクセスしreq_coreに結果を返します。is_requestedは、メモリアクセス要求を処理している場合に既にデバイスが要求を受け入れたかを示すフラグです。新しく要求を受け入れるときとis_requested0のときにデバイスに要求を割り当て、is_requested1かつrvalid1のときに結果を返します。

まだアクセス先のデバイスを実装していないため、常に0を読み込み、readyrvalidは常に1にして、書き込みは無視します。

11.4 RAMの接続

11.4.1 mmio_controllerモジュールにRAMを追加する

mmio_controllerモジュールにRAMとのインターフェースを実装します。

Device型にRAMを追加して、アドレスにRAMをマップします(リスト11.17リスト11.18)。

リスト11.17: リスト11.17: Device型にRAMを追加する (mmio_controller.veryl)
1:     enum Device {
2:         UNKNOWN,
3:         RAM,
4:     }
リスト11.18: リスト11.18: get_device関数でRAMの範囲を定義する (mmio_controller.veryl)
1:     function get_device (
2:         addr: input Addr,
3:     ) -> Device {
4:         if addr >= MMAP_RAM_BEGIN {
5:             return Device::RAM;
6:         }
7:         return Device::UNKNOWN;
8:     }

RAMとのインターフェースを追加し、reset_all_device_masters関数に要求をリセットするコードを追加します(リスト11.19リスト11.20)。

リスト11.19: リスト11.19: RAMとのインターフェースを追加する (mmio_controller.veryl)
1: module mmio_controller (
2:     clk       : input   clock         ,
3:     rst       : input   reset         ,
4:     req_core  : modport Membus::slave ,
5:     ram_membus: modport Membus::master,
6: ) {
リスト11.20: リスト11.20: インターフェースの要求部分をリセットする (mmio_controller.veryl)
1:     function reset_all_device_masters () {
2:         reset_membus_master(ram_membus);
3:     }

readyrvalidを取得する関数にRAMを登録します(リスト11.21リスト11.22)。

リスト11.21: リスト11.21: インターフェースのreadyを返す (mmio_controller.veryl)
1:     function get_device_ready (
2:         device: input Device,
3:     ) -> logic {
4:         case device {
5:             Device::RAM: return ram_membus.ready;
6:             default    : {}
7:         }
8:         return 1;
9:     }
リスト11.22: リスト11.22: インターフェースのrvalidを返す (mmio_controller.veryl)
1:     function get_device_rvalid (
2:         device: input Device,
3:     ) -> logic {
4:         case device {
5:             Device::RAM: return ram_membus.rvalid;
6:             default    : {}
7:         }
8:         return 1;
9:     }

RAMのrvalidrdatareq_coreに割り当てます(リスト11.23)。

リスト11.23: リスト11.23: RAMへのアクセス結果をreqに割り当てる (mmio_controller.veryl)
1:     function assign_device_slave (
2:         device: input   Device          ,
3:         req   : modport Membus::response,
4:     ) {
5:         req.rvalid = 1;
6:         req.rdata  = 0;
7:         case device {
8:             Device::RAM: req <> ram_membus;
9:             default    : {}
10:         }
11:     }

RAMのインターフェースに要求を割り当てます(リスト11.24)。ここでRAMのベースアドレスを引いたアドレスを割り当てることで、MMAP_RAM_BEGIN0になるようにしています。

リスト11.24: リスト11.24: RAMにreqを割り当ててアクセス要求する (mmio_controller.veryl)
1:     function assign_device_master (
2:         req: modport Membus::all_input,
3:     ) {
4:         case get_device(req.addr) {
5:             Device::RAM: {
6:                 ram_membus      <> req;
7:                 ram_membus.addr -= MMAP_RAM_BEGIN;
8:             }
9:             default: {}
10:         }
11:     }

11.4.2 RAMとmmio_controllerモジュールを接続する

topモジュールにmmio_controllerモジュールをインスタンス化し、RAMとmmio_controllerモジュール、mmio_controllerモジュールとcoreモジュールを接続します。

RAMとmmio_controllerモジュールを接続するインターフェース(mmio_ram_membus)、coreモジュールとmmio_controllerモジュールを接続するインターフェース(mmio_membus)を定義し、membusram_membusに改名します(リスト11.25リスト11.26)。

リスト11.25: リスト11.25: インターフェースの定義 / インスタンス名を変更する (top.veryl)
1:     inst mmio_membus    : Membus;
2:     inst mmio_ram_membus: Membus;
3:     inst ram_membus     : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
リスト11.26: リスト11.26: ポート名を変更する (top.veryl)
1:     inst ram: memory::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH> #(
2:         FILEPATH_IS_ENV: RAM_FILEPATH_IS_ENV,
3:         FILEPATH       : RAM_FILEPATH       ,
4:     ) (
5:         clk               ,
6:         rst               ,
7:         membus: ram_membus,
8:     );

coreモジュールからRAMへのメモリアクセスを調停する処理を、coreモジュールからmmio_controllerモジュールへのアクセスを調停する処理に変更します(リスト11.27)。

リスト11.27: リスト11.27: 調停する対象をmmio_membusに変更する (top.veryl)
1:     // mmio_controllerへのメモリアクセスを調停する
2:     always_ff {
3:         if_reset {
4:             memarb_last_i     = 0;
5:             memarb_last_iaddr = 0;
6:         } else {
7:             if mmio_membus.ready {
8:                 memarb_last_i     = !d_membus.valid;
9:                 memarb_last_iaddr = i_membus.addr;
10:             }
11:         }
12:     }
13: 
14:     always_comb {
15:         i_membus.ready  = mmio_membus.ready && !d_membus.valid;
16:         i_membus.rvalid = mmio_membus.rvalid && memarb_last_i;
17:         i_membus.rdata  = if memarb_last_iaddr[2] == 0 ? mmio_membus.rdata[31:0] : mmio_|membus.rdata[63:32];
18: 
19:         d_membus.ready  = mmio_membus.ready;
20:         d_membus.rvalid = mmio_membus.rvalid && !memarb_last_i;
21:         d_membus.rdata  = mmio_membus.rdata;
22: 
23:         mmio_membus.valid = i_membus.valid | d_membus.valid;
24:         if d_membus.valid {
25:             mmio_membus.addr  = d_membus.addr;
26:             mmio_membus.wen   = d_membus.wen;
27:             mmio_membus.wdata = d_membus.wdata;
28:             mmio_membus.wmask = d_membus.wmask;
29:         } else {
30:             mmio_membus.addr  = i_membus.addr;
31:             mmio_membus.wen   = 0; // 命令フェッチは常に読み込み
32:             mmio_membus.wdata = 'x;
33:             mmio_membus.wmask = 'x;
34:         }
35:     }

mmio_controllerをインスタンス化し、RAMと接続します。(リスト11.28リスト11.29)。RAMのアドレスへの変換は調停処理から接続部分に移動しています。

リスト11.28: リスト11.28: mmio_controllerモジュールをインスタンス化する (top.veryl)
1:     inst mmioc: mmio_controller (
2:         clk                        ,
3:         rst                        ,
4:         req_core  : mmio_membus    ,
5:         ram_membus: mmio_ram_membus,
6:     );
リスト11.29: リスト11.29: mmio_controllerモジュールとRAMを接続する (top.veryl)
1:     always_comb {
2:         // mmio <> RAM
3:         ram_membus.valid       = mmio_ram_membus.valid;
4:         mmio_ram_membus.ready  = ram_membus.ready;
5:         ram_membus.addr        = addr_to_memaddr::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>(mmio_ram_membus.addr);
6:         ram_membus.wen         = mmio_ram_membus.wen;
7:         ram_membus.wdata       = mmio_ram_membus.wdata;
8:         ram_membus.wmask       = mmio_ram_membus.wmask;
9:         mmio_ram_membus.rvalid = ram_membus.rvalid;
10:         mmio_ram_membus.rdata  = ram_membus.rdata;
11:     }

11.4.3 PCの初期値の変更

PCの初期値をMMAP_RAM_BEGINにすることで、RAMのベースアドレスからプログラムの実行を開始するように変更します。eeiパッケージにINITIAL_PCを定義し、PCのリセット時に利用します(リスト11.30リスト11.31)。

リスト11.30: リスト11.30: PCの初期値を定義する (eei.veryl)
1:     // pc on reset
2:     const INITIAL_PC: Addr = MMAP_RAM_BEGIN;
リスト11.31: リスト11.31: PCの初期値を設定する (core.veryl)
1:     always_ff {
2:         if_reset {
3:             if_pc           = INITIAL_PC;
4:             if_is_requested = 0;
5:             if_pc_requested = 0;
6:             if_fifo_wvalid  = 0;
7:             if_fifo_wdata   = 0;
8:         } else {

riscv-testsを実行してRAMにアクセスできているか確認します。今のところriscv-testsはアドレス0から配置されるようにリンクしているため、riscv-testsのenv/p/link.ldを変更します(リスト11.32)。

リスト11.32: リスト11.32: プログラムの先頭のアドレスを変更する (riscv-tests/env/p/link.ld)
1: OUTPUT_ARCH( "riscv" )
2: ENTRY(_start)
3: 
4: SECTIONS
5: {
6:   . = 0x00000000; ← 先頭を0x80000000に変更する (戻す)

riscv-testsをビルドしなおし、成果物をtestディレクトリに配置してください。ビルドしなおしたので、HEXファイルを再度生成します(リスト11.33)。

リスト11.33: リスト11.33: HEXファイルの再生成
$ cd test
$ find share/ -type f -not -name "*.dump" -exec riscv64-unknown-elf-objcopy -O binary { {}.bin \;}
$ find share/ -type f -name "*.bin" -exec sh -c "python3 bin2hex.py 8 { > {}.hex" \;}

riscv-testsの終了判定用のアドレスをMMAP_RAM_BEGIN基準のアドレスに変更します(リスト11.34)。

リスト11.34: リスト11.34: .tohostのアドレスを変更する (top.veryl)
1:     #[ifdef(TEST_MODE)]
2:     always_ff {
3:         let RISCVTESTS_TOHOST_ADDR: Addr = MMAP_RAM_BEGIN + 'h1000 as Addr;
4:         if d_membus.valid && d_membus.ready && d_membus.wen == 1 && d_membus.addr == RISCVTESTS_TOHOST_ADDR && d_membus.wdata[lsb] == 1'b1 {
5:             test_success = d_membus.wdata == 1;
6:             if d_membus.wdata == 1 {
7:                 $display("riscv-tests success!");
8:             } else {
9:                 $display("riscv-tests failed!");
10:                 $error  ("wdata : %h", d_membus.wdata);
11:             }
12:             $finish();
13:         }
14:     }

riscv-testsを実行し、RAMにアクセスできてテストに成功することを確認してください。

11.5 ROMの実装

11.5.1 mmio_controllerモジュールにROMを追加する

mmio_controllerモジュールにROMとのインターフェースを実装します。

Device型にROMを追加して、アドレスにROMをマップします(リスト11.35リスト11.36)。

リスト11.35: リスト11.35: Device型にROMを変更する (mmio_controller.veryl)
1:     enum Device {
2:         UNKNOWN,
3:         RAM,
4:         ROM,
5:     }
リスト11.36: リスト11.36: get_device関数でROMの範囲を定義する (mmio_controller.veryl)
1:     function get_device (
2:         addr: input Addr,
3:     ) -> Device {
4:         if MMAP_ROM_BEGIN <= addr && addr <= MMAP_ROM_END {
5:             return Device::ROM;
6:         }
7:         if addr >= MMAP_RAM_BEGIN {
8:             return Device::RAM;
9:         }
10:         return Device::UNKNOWN;
11:     }

ROMとのインターフェースを追加します(リスト11.37リスト11.38)。reset_all_device_masters関数でインターフェースをリセットします。

リスト11.37: リスト11.37: ROMとのインターフェースを追加する (mmio_controller.veryl)
1: module mmio_controller (
2:     clk       : input   clock         ,
3:     rst       : input   reset         ,
4:     req_core  : modport Membus::slave ,
5:     ram_membus: modport Membus::master,
6:     rom_membus: modport Membus::master,
7: ) {
リスト11.38: リスト11.38: インターフェースの要求部分をリセットする (mmio_controller.veryl)
1:     function reset_all_device_masters () {
2:         reset_membus_master(ram_membus);
3:         reset_membus_master(rom_membus);
4:     }

readyrvalidを取得する関数にROMを登録します(リスト11.39リスト11.40)。

リスト11.39: リスト11.39: インターフェースのreadyを返す (mmio_controller.veryl)
1:         case device {
2:             Device::RAM: return ram_membus.ready;
3:             Device::ROM: return rom_membus.ready;
4:             default    : {}
5:         }
リスト11.40: リスト11.40: インターフェースのrvalidを返す (mmio_controller.veryl)
1:         case device {
2:             Device::RAM: return ram_membus.rvalid;
3:             Device::ROM: return rom_membus.rvalid;
4:             default    : {}
5:         }

ROMのrvalidrdatareq_coreに割り当てます(リスト11.41)。

リスト11.41: リスト11.41: assign_device_slave関数でROMの結果をreqに割り当てる (mmio_controller.veryl)
1:         case device {
2:             Device::RAM: req <> ram_membus;
3:             Device::ROM: req <> rom_membus;
4:             default    : {}
5:         }

ROMのインターフェースに要求を割り当てます(リスト11.42)。RAMと同じようにメモリマップのベースアドレスを引いたアドレスを割り当てます。

リスト11.42: リスト11.42: get_device関数でROMにreqを割り当ててアクセス要求する (mmio_controller.veryl)
1:         case get_device(req.addr) {
2:             Device::RAM: {
3:                 ram_membus      <> req;
4:                 ram_membus.addr -= MMAP_RAM_BEGIN;
5:             }
6:             Device::ROM: {
7:                 rom_membus      <> req;
8:                 rom_membus.addr -= MMAP_ROM_BEGIN;
9:             }
10:             default: {}
11:         }

11.5.2 ROMの初期値のパラメータを作成する

topモジュールにROMの初期値を指定するパラメータを定義します(リスト11.43)。

リスト11.43: リスト11.43: パラメータを定義する (top.veryl)
1: module top #(
2:     param RAM_FILEPATH_IS_ENV: bit    = 1              ,
3:     param RAM_FILEPATH       : string = "RAM_FILE_PATH",
4:     param ROM_FILEPATH_IS_ENV: bit    = 1              ,
5:     param ROM_FILEPATH       : string = "ROM_FILE_PATH",
6: ) (

RAMと同じように、シミュレータ用のプログラムでROMのHEXファイルのパスを指定するようにします。1番目の引数をROM用のHEXファイルのパスに変更し、環境変数ROM_FILE_PATHをその値に設定します(リスト11.44リスト11.45リスト11.46リスト11.47リスト11.48)。

リスト11.44: リスト11.44: 引数の名称を変える (tb_verilator.cpp)
1:     if (argc < 3) {
2:         std::cout << "Usage: " << argv[0] << " ROM_FILE_PATH RAM_FILE_PATH [CYCLE]" << std::endl;
3:         return 1;
4:     }
リスト11.45: リスト11.45: ROMのHEXファイルのパスを生成する (tb_verilator.cpp)
1:     // メモリの初期値を格納しているファイル名
2:     std::string rom_file_path = argv[1];
3:     std::string ram_file_path = argv[2];
4:     try {
5:         // 絶対パスに変換する
6:         rom_file_path = fs::absolute(rom_file_path).string();
7:         ram_file_path = fs::absolute(ram_file_path).string();
8:     } catch (const std::exception& e) {
9:         std::cerr << "Invalid memory file path : " << e.what() << std::endl;
10:         return 1;
11:     }
リスト11.46: リスト11.46: 引数の数が変わったのでインデックスを変更する (tb_verilator.cpp)
1:     unsigned long long cycles = 0;
2:     if (argc >= 4) {
3:         std::string cycles_string = argv[3];
4:         try {
5:             cycles = stoull(cycles_string);
6:         } catch (const std::exception& e) {
7:             std::cerr << "Invalid number: " << argv[3] << std::endl;
8:             return 1;
9:         }
10:     }
リスト11.47: リスト11.47: 環境変数を変更する (tb_verilator.cpp)
1:     const char* original_env_rom = getenv("ROM_FILE_PATH");
2:     const char* original_env_ram = getenv("RAM_FILE_PATH");
3:     setenv("ROM_FILE_PATH", rom_file_path.c_str(), 1);
4:     setenv("RAM_FILE_PATH", ram_file_path.c_str(), 1);
リスト11.48: リスト11.48: 環境変数を元に戻す (tb_verilator.cpp)
1:     if (original_env_rom != nullptr){
2:         setenv("ROM_FILE_PATH", original_env_rom, 1);
3:     }
4:     if (original_env_ram != nullptr){
5:         setenv("RAM_FILE_PATH", original_env_ram, 1);
6:     }

テストを実行するためのPythonプログラムでROMのHEXファイルを指定できるようにします(リスト11.49リスト11.50リスト11.51)。デフォルト値はカレントディレクトリのbootrom.hexにしておきます。

リスト11.49: リスト11.49: 引数--romを追加する (test/test.py)
1: parser.add_argument("--rom", default="bootrom.hex", help="hex file of rom")
リスト11.50: リスト11.50: シミュレータにROMのHEXファイルのパスを渡す (test/test.py)
1: def test(romhex, file_name):
2:     result_file_path = os.path.join(args.output_dir, file_name.replace(os.sep, "_") + ".txt")
3:     cmd = f"{args.sim_path} {romhex} {file_name} 0"
4:     success = False
リスト11.51: リスト11.51: test関数にROMのHEXファイルのパスを渡す (test/test.py)
1:     for hexpath in dir_walk(args.dir):
2:         f, s = test(os.path.abspath(args.rom), os.path.abspath(hexpath))
3:         res_strs.append(("PASS" if s else "FAIL") + " : " + f)
4:         res_statuses.append(s)

11.5.3 ROMとmmio_controllerモジュールを接続する

ROMをインスタンス化してmmio_controllerモジュールと接続します。

ROMとmmio_controllerモジュールを接続するインターフェース(mmio_rom_membus)、ROMのインターフェース(rom_membus)を定義します(リスト11.52)。

リスト11.52: リスト11.52: ROMのインターフェースの定義 (top.veryl)
1:     inst mmio_membus    : Membus;
2:     inst mmio_ram_membus: Membus;
3:     inst mmio_rom_membus: Membus;
4:     inst ram_membus     : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
5:     inst rom_membus     : membus_if::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH>;

ROMをインスタンス化します(リスト11.53)。パラメータにはtopモジュールのパラメータを割り当てます。

リスト11.53: リスト11.53: ROMをインスタンス化する (top.veryl)
1:     inst rom: memory::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH> #(
2:         FILEPATH_IS_ENV: ROM_FILEPATH_IS_ENV,
3:         FILEPATH       : ROM_FILEPATH       ,
4:     ) (
5:         clk               ,
6:         rst               ,
7:         membus: rom_membus,
8:     );

mmio_controllerモジュールにrom_membusを接続します(リスト11.54)。

リスト11.54: リスト11.54: ROMのインターフェースを接続する (top.veryl)
1:     inst mmioc: mmio_controller (
2:         clk                        ,
3:         rst                        ,
4:         req_core  : mmio_membus    ,
5:         ram_membus: mmio_ram_membus,
6:         rom_membus: mmio_rom_membus,
7:     );

mmio_controllerモジュールとROMを接続します。アドレスの変換のためにaddr_to_memaddr関数を使用しています(リスト11.55)。

リスト11.55: リスト11.55: mmio_controllerモジュールとROMを接続する (top.veryl)
1:     always_comb {
2:         // mmio <> ROM
3:         rom_membus.valid       = mmio_rom_membus.valid;
4:         mmio_rom_membus.ready  = rom_membus.ready;
5:         rom_membus.addr        = addr_to_memaddr::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH>(mmio_rom_membus.addr);
6:         rom_membus.wen         = 0;
7:         rom_membus.wdata       = 0;
8:         rom_membus.wmask       = 0;
9:         mmio_rom_membus.rvalid = rom_membus.rvalid;
10:         mmio_rom_membus.rdata  = rom_membus.rdata;
11:     }

11.5.4 ROMからRAMにジャンプする

PCの初期値をROMのベースアドレスに変更し、ROMからRAMにジャンプする仕組みを実現します。

一般的にCPUの電源をつけると、CPUはROMのようなメモリデバイスに入ったソフトウェアから実行を開始します。そのソフトウェアは次に実行するソフトウェアを外部記憶装置から読み取り、RAMにソフトウェアを適切にコピー、配置して実行します。

本章ではRAM、ROMともに$readmemhシステムタスクで初期化するように実装しているので、RAMのベースアドレスにジャンプするだけのプログラムをROMに設定します。

ROMに設定するためのHEXファイルを作成します(リスト11.56)。

リスト11.56: リスト11.56: RAMの開始アドレスにジャンプするプログラム (bootrom.hex)
1: 00409093080000b7 // 0: lui x1, 0x08000 4: slli x1, x1, 4
2: 0000000000008067 // 8: jalr x0, 0(x1)  c:
3: 0000000000000000 // zero
4: 

PCの初期値をROMのベースアドレスに変更します(リスト11.57)。

リスト11.57: リスト11.57: PCの初期値の変更 (eei.veryl)
1:     const INITIAL_PC: Addr = MMAP_ROM_BEGIN;

riscv-testsを実行し、ROM(0x1000)から実行を開始してRAM(0x80000000)にジャンプしてテストを開始していることを確かめてください。

11.6 デバッグ用の入出力デバイスの実装

CPUが文字を送信したり受信するためのデバッグ用の入出力デバイスを実装します。今のところriscv-testsの結果を受け取るためのアドレスをRAMのベースアドレス + 0x1000にしていますが、この処理もデバイスに実装します。

本章では、デバッグ用の入出力デバイスに次のような64ビットレジスタを実装します。

上位20ビットが20'h01010な値を書き込み
下位8ビットを文字として解釈し$writeシステムタスクで出力します。
上位20ビットが20'h01010ではないLSBが1な値を書き込み
今までのriscv-testsの終了判定処理を行います。
読み込み
C++プログラムの関数を利用して1文字入力を受け取ります。 有効な入力の場合は上位20ビットが20'h01010、無効な入力の場合は0になります。

11.6.1 デバイスのアドレスを設定する

リスト11.9でデバイスのアドレスをポートで設定できるようにしたので、tb_verilator.cppで環境変数の値をデバイスのアドレスに設定するようにします。

環境変数DBG_ADDRを読み込み、DBG_ADDRポートに設定します(リスト11.58)。

リスト11.58: リスト11.58: DBG_ADDRポートに環境変数の値を設定する (tb_verilator.cpp)
1:     // デバッグ用の入出力デバイスのアドレスを取得する
2:     const char* dbg_addr_c = getenv("DBG_ADDR");
3:     const unsigned long long DBG_ADDR = dbg_addr_c == nullptr ? 0 : std::strtoull(dbg_addr_c, nullptr, 0);
4: 
5:     // top
6:     Vcore_top *dut = new Vcore_top();
7:     dut->MMAP_DBG_ADDR = DBG_ADDR;

11.6.2 mmio_controllerモジュールにデバイスを追加する

mmio_controllerモジュールにデバイスを追加します。

Device型にDevice::DEBUGを追加します(リスト11.59)。

リスト11.59: リスト11.59: Device型にデバッグ用の入出力デバイスを追加する (mmio_controller.veryl)
1:     enum Device {
2:         UNKNOWN,
3:         RAM,
4:         ROM,
5:         DEBUG,
6:     }

ポートにインターフェースとデバイスのアドレスを追加します(リスト11.60リスト11.61)。

リスト11.60: リスト11.60: DBG_ADDR、インターフェースを追加する (mmio_controller.veryl)
1: module mmio_controller (
2:     clk       : input   clock         ,
3:     rst       : input   reset         ,
4:     DBG_ADDR  : input   Addr          ,
5:     req_core  : modport Membus::slave ,
6:     ram_membus: modport Membus::master,
7:     rom_membus: modport Membus::master,
8:     dbg_membus: modport Membus::master,
9: ) {
リスト11.61: リスト11.61: インターフェースの要求部分をリセットする (mmio_controller.veryl)
1:     function reset_all_device_masters () {
2:         reset_membus_master(ram_membus);
3:         reset_membus_master(rom_membus);
4:         reset_membus_master(dbg_membus);
5:     }

デバイスの位置を設定します。最初にチェックすることで、他のデバイスとアドレスを被らせたとしてもデバッグ用の入出力デバイスを優先します(リスト11.62)。

リスト11.62: リスト11.62: get_device関数でデバイスの範囲を定義する (mmio_controller.veryl)
1:     function get_device (
2:         addr: input Addr,
3:     ) -> Device {
4:         if DBG_ADDR <= addr && addr <= DBG_ADDR + 7 {
5:             return Device::DEBUG;
6:         }
7:         if MMAP_ROM_BEGIN <= addr && addr <= MMAP_ROM_END {
8:             return Device::ROM;
9:         }
10:         if addr >= MMAP_RAM_BEGIN {
11:             return Device::RAM;
12:         }
13:         return Device::UNKNOWN;
14:     }

インターフェースを設定します(リスト11.63リスト11.64リスト11.65リスト11.66)。この変更はROMを追加したときとほとんど同じです。

リスト11.63: リスト11.63: assign_device_master関数の変更 (mmio_controller.veryl)
1:         case get_device(req.addr) {
2:             Device::RAM: {
3:                 ram_membus      <> req;
4:                 ram_membus.addr -= MMAP_RAM_BEGIN;
5:             }
6:             Device::ROM: {
7:                 rom_membus      <> req;
8:                 rom_membus.addr -= MMAP_ROM_BEGIN;
9:             }
10:             Device::DEBUG: {
11:                 dbg_membus      <> req;
12:                 dbg_membus.addr -= DBG_ADDR;
13:             }
14:             default: {}
15:         }
リスト11.64: リスト11.64: assign_device_slave関数の変更 (mmio_controller.veryl)
1:         case device {
2:             Device::RAM  : req <> ram_membus;
3:             Device::ROM  : req <> rom_membus;
4:             Device::DEBUG: req <> dbg_membus;
5:             default      : {}
6:         }
リスト11.65: リスト11.65: get_device_ready関数の変更 (mmio_controller.veryl)
1:         case device {
2:             Device::RAM  : return ram_membus.ready;
3:             Device::ROM  : return rom_membus.ready;
4:             Device::DEBUG: return dbg_membus.ready;
5:             default      : {}
6:         }
リスト11.66: リスト11.66: get_device_rvalid関数の変更 (mmio_controller.veryl)
1:         case device {
2:             Device::RAM  : return ram_membus.rvalid;
3:             Device::ROM  : return rom_membus.rvalid;
4:             Device::DEBUG: return dbg_membus.rvalid;
5:             default      : {}
6:         }

topモジュールにデバッグ用の入出力デバイスのインターフェース(dbg_membus)を定義し、mmio_controllerモジュールと接続します(リスト11.67リスト11.68)。

リスト11.67: リスト11.67: インターフェースのインスタンス化 (top.veryl)
1:     inst ram_membus     : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
2:     inst rom_membus     : membus_if::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH>;
3:     inst dbg_membus     : Membus;
リスト11.68: リスト11.68: インターフェースを接続する (top.veryl)
1:     inst mmioc: mmio_controller (
2:         clk                        ,
3:         rst                        ,
4:         DBG_ADDR  : MMAP_DBG_ADDR  ,
5:         req_core  : mmio_membus    ,
6:         ram_membus: mmio_ram_membus,
7:         rom_membus: mmio_rom_membus,
8:         dbg_membus                 ,
9:     );

11.6.3 出力を実装する

dbg_membusを使い、デバッグ出力処理を実装します。既存のriscv-testsの終了検知処理を次のように書き換えます(リスト11.69)。

リスト11.69: リスト11.69: riscv-testsの終了検知処理をデバッグ用の入出力デバイスに変更する (top.veryl)
1:     // デバッグ用のIO
2:     always_ff {
3:         dbg_membus.ready  = 1;
4:         dbg_membus.rvalid = dbg_membus.valid;
5:         if dbg_membus.valid {
6:             if dbg_membus.wen {
7:                 if dbg_membus.wdata[MEMBUS_DATA_WIDTH - 1-:20] == 20'h01010 {
8:                     $write("%c", dbg_membus.wdata[7:0]);
9:                 } else if dbg_membus.wdata[lsb] == 1'b1 {
10:                     #[ifdef(TEST_MODE)]
11:                     {
12:                         test_success = dbg_membus.wdata == 1;
13:                     }
14:                     if dbg_membus.wdata == 1 {
15:                         $display("test success!");
16:                     } else {
17:                         $display("test failed!");
18:                         $error  ("wdata : %h", dbg_membus.wdata);
19:                     }
20:                     $finish();
21:                 }
22:             }
23:         }
24:     }

常に要求を受け付け、書き込みの時は書き込むデータ(wdata)を確認します。wdataの上位20ビットが20'h01010なら下位8ビットを出力し、LSBが1ならテストの成功判定をして$finishシステムタスクを呼び出します。

11.6.4 出力をテストする

実装した出力デバイスで文字を出力できることを確認します。

デバッグ用に$displayシステムタスクで表示している情報が邪魔になるので、デバッグ情報の表示を環境変数PRINT_DEBUGで制御できるようにします(リスト11.70)。

リスト11.70: リスト11.70: デバッグ出力をdefineで囲う (core.veryl)
1:     ///////////////////////////////// DEBUG /////////////////////////////////
2:     #[ifdef(PRINT_DEBUG)]
3:     {
4:         var clock_count: u64;
5: 
6:         always_ff {
7:             if_reset {
8:                 clock_count = 1;
9:             } else {
10:                 clock_count = clock_count + 1;
11: 
12:                 $display("");
13:                 $display("# %d", clock_count);

test/debug_output.cを作成し、次のように記述します(リスト11.71)。これはHello,world!と出力するプログラムです。

リスト11.71: リスト11.71: Hello,world!を出力するプログラム (test/debug_output.c)
1: #define DEBUG_REG ((volatile unsigned long long*)0x40000000)
2: 
3: void main(void) {
4:     int strlen = 13;
5:     unsigned char str[13];
6: 
7:     str[0]  = 'H';
8:     str[1]  = 'e';
9:     str[2]  = 'l';
10:     str[3]  = 'l';
11:     str[4]  = 'o';
12:     str[5]  = ',';
13:     str[6]  = 'w';
14:     str[7]  = 'o';
15:     str[8]  = 'r';
16:     str[9]  = 'l';
17:     str[10] = 'd';
18:     str[11] = '!';
19:     str[12] = '\n';
20: 
21:     for (int i = 0; i < strlen; i++) {
22:         unsigned long long c = str[i];
23:         *DEBUG_REG = c | (0x01010ULL << 44);
24:     }
25:     *DEBUG_REG = 1;
26: }

DEBUG_REGは出力デバイスのアドレスです。ここに0x01010を44ビット左シフトした値と文字をOR演算した値を書き込むことで文字を出力します。最後に1を書き込み、テストを終了しています。

main関数をそのままコンパイルしてRAMに配置すると、スタックポインタ(stack pointer, sp)の値が適切に設定されていないのでうまく動きません。スタックポインタとは、プログラムが一時的に利用する値を格納しておくためのメモリ(スタック)のアドレスへのポインタのことです。RISC-Vの規約ではsp(x2)レジスタをスタックポインタとして利用することが定められています。

そのため、レジスタの値を適切な値にリセットしてmain関数を呼び出す別のプログラムが必要です。test/entry.Sを作成し、次のように記述します(リスト11.72)。

リスト11.72: リスト11.72: test/entry.S
1: .global _start
2: .section .text.init
3: _start:
4:     add x1, x0, x0
5:     la  x2, _stack_bottom
6:     add x3, x0, x0
7:     add x4, x0, x0
8:     add x5, x0, x0
9:     add x6, x0, x0
10:     add x7, x0, x0
11:     add x8, x0, x0
12:     add x9, x0, x0
13:     add x10, x0, x0
14:     add x11, x0, x0
15:     add x12, x0, x0
16:     add x13, x0, x0
17:     add x14, x0, x0
18:     add x15, x0, x0
19:     add x16, x0, x0
20:     add x17, x0, x0
21:     add x18, x0, x0
22:     add x19, x0, x0
23:     add x20, x0, x0
24:     add x21, x0, x0
25:     add x22, x0, x0
26:     add x23, x0, x0
27:     add x24, x0, x0
28:     add x25, x0, x0
29:     add x26, x0, x0
30:     add x27, x0, x0
31:     add x28, x0, x0
32:     add x29, x0, x0
33:     add x30, x0, x0
34:     add x31, x0, x0
35:     call main

このアセンブリはsp(x2)レジスタを_stack_bottomのアドレスに設定し、他のレジスタを0でリセットしたあとにmainにジャンプします。

_stack_bottomは、リンカの設定ファイルに記述します。test/link.ldを作成し、次のように記述します(リスト11.73)。

リスト11.73: リスト11.73: test/link.ld
1: OUTPUT_ARCH( "riscv" )
2: ENTRY(_start)
3: 
4: SECTIONS
5: {
6:   . = 0x80000000;
7:   .text.init : { *(.text.init) }
8:   .text : { *(.text*) }
9:   .data : { *(.data*) }
10:   .bss : {*(.bss*)}
11:   .stack : {
12:     . = ALIGN(0x10);
13:     _stack_top = .;
14:     . += 4K;
15:     _stack_bottom = .;
16:   }
17:   _end = .;
18: }

_stack_bottom_stack_topの間は4KBあるので、スタックのサイズは4KBになります。_start.text.initに配置し(リスト11.72)、SECTIONSの先頭に.text.initを配置しているため、アドレス0x80000000_startが配置されます。

これらのファイルを利用し、テストプログラムをコンパイルします(リスト11.74)。gccの-marchフラグではC拡張を抜いたISAを指定しています。このフラグを記述しないと、まだ実装していない命令が含まれたELFファイルにコンパイルされてしまいます。

リスト11.74: リスト11.74: テストプログラムをコンパイル、HEXファイルに変換する
$ cd test
$ riscv64-unknown-elf-gcc -nostartfiles -nostdlib -mcmodel=medany -T link.ld -march=rv64imad debug_output.c entry.S
$ riscv64-unknown-elf-objcopy a.out -O binary test.bin
$ python3 bin2hex.py 8 test.bin > test.bin.hex ← HEXファイルに変換する

シミュレータをビルドし、テストプログラムを実行します(リスト11.75)。

リスト11.75: リスト11.75: テストプログラムを実行する
$ make build sim
$ DBG_ADDR=0x40000000 ./obj_dir/sim bootrom.hex test/test.bin.hex
Hello,world!
- ~/core/src/top.sv:62: Verilog $finish

Hello,world!と出力されたあと、プログラムが終了しました。

11.6.5 riscv-testsに対応する

riscv-testsを実行するとき、終了判定用のレジスタの位置をDBG_ADDRに設定するようにします。

test/test.pyを、ELFファイルを探して自動でDBG_ADDRを設定してテストを実行するプログラムに変更します。

elftools*2を使用し、ELFファイルの判定、セクションのアドレスを取得する関数を定義します(リスト11.76リスト11.77)。

[*2] pipでインストールできます

リスト11.76: リスト11.76: elftoolsのimport (test/test.py)
1: from elftools.elf.elffile import ELFFile
リスト11.77: リスト11.77: ELFの判定、セクションのアドレスを取得する関数の定義 (test/test.py)
1: def is_elf(filepath):
2:     try:
3:         with open(filepath, 'rb') as f:
4:             magic_number = f.read(4)
5:             return magic_number == b'\x7fELF'
6:     except:
7:         return False
8: 
9: def get_section_address(filepath, section_name):
10:     try:
11:         with open(filepath, 'rb') as f:
12:             elffile = ELFFile(f)
13:             for section in elffile.iter_sections():
14:                 if section.name == section_name:
15:                     return section.header['sh_addr']
16:             return 0
17:     except:
18:         return 0

デバッグ用の入出力デバイスのセクション名を指定する引数を作成します(リスト11.78)。また、テストするファイルの拡張子を指定していた引数を、ELFファイルに付加することでHEXファイルのパスを得るための引数に変更します。

リスト11.78: リスト11.78: オプションを追加する (test/test.py)
1: parser.add_argument("-e", "--extension", default=".bin.hex", help="hex file extension")
2: parser.add_argument("-d", "--debug_label", default=".tohost", help="debug device label")

dir_walk関数を、ELFファイルを探す関数に変更します(リスト11.79)。

リスト11.79: リスト11.79: dir_walk関数でELFファイルを探す (test/test.py)
1: if entry.is_file():
2:     if not is_elf(entry.path):
3:         continue
4:     if len(args.files) == 0:
5:         yield entry.path

シミュレータの実行でDBG_ADDRを指定するようにします(リスト11.80リスト11.81)。

リスト11.80: リスト11.80: DBG_ADDRをシミュレータに渡す (test/test.py)
1: def test(dbg_addr, romhex, file_name):
2:     result_file_path = os.path.join(args.output_dir, file_name.replace(os.sep, "_") + ".txt")
3:     env = f"DBG_ADDR={dbg_addr} "
4:     cmd = f"{args.sim_path} {romhex} {file_name} 0"
5:     success = False
6:     with open(result_file_path, "w") as f:
7:         no = f.fileno()
8:         p = subprocess.Popen(" ".join([env, "exec", cmd]), shell=True, stdout=no, stderr=no)
リスト11.81: リスト11.81: DBG_ADDRをtest関数に渡す (test/test.py)
1: for elfpath in dir_walk(args.dir):
2:     hexpath = elfpath + args.extension
3:     if not os.path.exists(hexpath):
4:         print("SKIP :", elfpath)
5:         continue
6:     dbg_addr = get_section_address(elfpath, args.debug_label)
7:     f, s = test(dbg_addr, os.path.abspath(args.rom), os.path.abspath(hexpath))
8:     res_strs.append(("PASS" if s else "FAIL") + " : " + f)
9:     res_statuses.append(s)

VERILATOR_FLAGS="-DTEST_MODE"をつけてシミュレータをビルドし、riscv-testsが正常終了することを確かめてください。

11.6.6 入力を実装する

dbg_membusを使い、デバッグ入力処理を実装します。

まず、src/tb_verilator.cppに、標準入力から1文字取得する関数を定義します(リスト11.82)。入力がない場合は0、ある場合は上位20ビットを0x01010にした値を返します。

リスト11.82: リスト11.82: 標準入力を1文字取得する関数の定義 (src/tb_verilator.cpp)
1: extern "C" const unsigned long long get_input_dpic() {
2:     unsigned char c = 0;
3:     ssize_t bytes_read = read(STDIN_FILENO, &c, 1);
4: 
5:     if (bytes_read == 1) {
6:         return static_cast<unsigned long long>(c) | (0x01010ULL << 44);
7:     }
8:     return 0;
9: }

ここで、read関数の呼び出しでシミュレータを止めず(O_NONBLOCK)、シェルが入力をバッファリングしなくする(~ICANON)ために設定を変えるコードを挿入します。また、シェルが文字列をローカルエコー(入力した文字列を表示)しないようにします(~ECHO)(リスト11.83リスト11.84リスト11.85)。

リスト11.83: リスト11.83: includeを追加する (src/tb_verilator.cpp)
1: #include <fcntl.h>
2: #include <termios.h>
3: #include <signal.h>
リスト11.84: リスト11.84: 設定を変更、復元する関数の定義 (src/tb_verilator.cpp)
1: struct termios old_setting;
2: 
3: void restore_termios_setting(void) {
4:     tcsetattr(STDIN_FILENO, TCSANOW, &old_setting);
5: }
6: 
7: void sighandler(int signum) {
8:     restore_termios_setting();
9:     exit(signum);
10: }
11: 
12: void set_nonblocking(void) {
13:     struct termios new_setting;
14: 
15:     if (tcgetattr(STDIN_FILENO, &old_setting) == -1) {
16:         perror("tcgetattr");
17:         return;
18:     }
19:     new_setting = old_setting;
20:     new_setting.c_lflag &= ~(ICANON | ECHO);
21:     if (tcsetattr(STDIN_FILENO, TCSANOW, &new_setting) == -1) {
22:         perror("tcsetattr");
23:         return;
24:     }
25:     signal(SIGINT, sighandler);
26:     signal(SIGTERM, sighandler);
27:     signal(SIGQUIT, sighandler);
28:     atexit(restore_termios_setting);
29: 
30:     int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
31:     if (flags == -1) {
32:         perror("fcntl(F_GETFL)");
33:         return;
34:     }
35:     if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) {
36:         perror("fcntl(F_SETFL)");
37:         return;
38:     }
39: }
リスト11.85: リスト11.85: 設定を変える関数をmain関数から呼び出す (src/tb_verilator.cpp)
1: int main(int argc, char** argv) {
2:     Verilated::commandArgs(argc, argv);
3: 
4:     if (argc < 3) {
5:         std::cout << "Usage: " << argv[0] << " ROM_FILE_PATH RAM_FILE_PATH [CYCLE]" << std::endl;
6:         return 1;
7:     }
8: 
9:     #ifdef ENABLE_DEBUG_INPUT
10:         set_nonblocking();
11:     #endif

src/util.verylにget_input_dpic関数を呼び出す関数を実装します(リスト11.86)。

リスト11.86: リスト11.86: get_input関数を定義する (src/util.veryl)
1: embed (inline) sv{{{
2:     package svutil;
3:         ...
4:         import "DPI-C" context function longint get_input_dpic();
5:         function longint get_input();
6:             return get_input_dpic();
7:         endfunction
8:     endpackage
9: }}}
10: 
11: package util {
12:     ...
13:     function get_input () -> u64 {
14:         return $sv::svutil::get_input();
15:     }
16: }

デバッグ用の入出力デバイスのロードでutil::get_inputの結果を返すようにします(リスト11.87)。このコードは合成できないので、有効化オプションENABLE_DEBUG_INPUTをつけます。

リスト11.87: リスト11.87: 読み込みでget_input関数を呼び出す (src/top.veryl)
1:     always_ff {
2:         dbg_membus.ready  = 1;
3:         dbg_membus.rvalid = dbg_membus.valid;
4:         if dbg_membus.valid {
5:             if dbg_membus.wen {
6:                 ...
7:             } else {
8:                 #[ifdef(ENABLE_DEBUG_INPUT)]
9:                 {
10:                     dbg_membus.rdata = util::get_input();
11:                 }
12:             }
13:         }
14:     }

11.6.7 入力をテストする

実装した入出力デバイスで文字を入出力できることを確認します。

test/debug_input.cを作成し、次のように記述します(リスト11.88)。これは入力された文字に1を足した値を出力するプログラムです。

リスト11.88: リスト11.88: test/debug_input.c
1: #define DEBUG_REG ((volatile unsigned long long*)0x40000000)
2: 
3: void main(void) {
4:     while (1) {
5:         unsigned long long c = *DEBUG_REG;
6:         if (c & (0x01010ULL << 44) == 0) {
7:             continue;
8:         }
9:         c = c & 255;
10:         *DEBUG_REG = (c + 1) | (0x01010ULL << 44);
11:     }
12: }

プログラムをコンパイルしてシミュレータを実行し、入力した文字が1文字ずれて表示されることを確認してください(リスト11.89)。

リスト11.89: リスト11.89: テストプログラムを実行する
$ make build sim VERILATOR_FLAGS="-DENABLE_DEBUG_INPUT" ← 入力を有効にしてシミュレータをビルド
$ ./obj_dir/sim bootrom.hex test/test.bin.hex ← (事前にHEXファイルを作成しておく
bcd← abcと入力して改行
   efg← defと入力する