第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)。
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_WIDTH
、MEM_ADDR_WIDTH
を使っている部分をMEMBUS_DATA_WIDTH
、XLEN
に置き換えます。MEMBUS_DATA_WIDTH
とXLEN
を使うmembus_ifインターフェースに別名Membus
をつけて利用します(リスト11.2、リスト11.3、リスト11.4、リスト11.5)。
1: alias interface Membus = membus_if::<eei::MEMBUS_DATA_WIDTH, eei::XLEN>;
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: ) {
1: membus: modport Membus::master, // メモリとのinterface
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)。
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、)。
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: }
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)。
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 ,
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)。
1: if (argc < 2) { 2: std::cout << "Usage: " << argv[0] << " RAM_FILE_PATH [CYCLE]" << std::endl; 3: return 1; 4: }
1: // 環境変数でメモリの初期化用ファイルを指定する 2: const char* original_env = getenv("RAM_FILE_PATH"); 3: setenv("RAM_FILE_PATH", memory_file_path.c_str(), 1);
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)。
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)。
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
)だけ定義しています。
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_requested
が0
のときにデバイスに要求を割り当て、is_requested
が1
かつrvalid
が1
のときに結果を返します。
まだアクセス先のデバイスを実装していないため、常に0
を読み込み、ready
とrvalid
は常に1
にして、書き込みは無視します。
11.4 RAMの接続
11.4.1 mmio_controllerモジュールにRAMを追加する
mmio_controllerモジュールにRAMとのインターフェースを実装します。
Device
型にRAMを追加して、アドレスにRAMをマップします(リスト11.17、リスト11.18)。
1: enum Device { 2: UNKNOWN, 3: RAM, 4: }
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)。
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: ) {
1: function reset_all_device_masters () { 2: reset_membus_master(ram_membus); 3: }
ready
、rvalid
を取得する関数にRAMを登録します(リスト11.21、リスト11.22)。
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: }
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のrvalid
、rdata
をreq_core
に割り当てます(リスト11.23)。
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_BEGIN
が0
になるようにしています。
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
)を定義し、membus
をram_membus
に改名します(リスト11.25、リスト11.26)。
1: inst mmio_membus : Membus; 2: inst mmio_ram_membus: Membus; 3: inst ram_membus : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
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)。
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のアドレスへの変換は調停処理から接続部分に移動しています。
1: inst mmioc: mmio_controller ( 2: clk , 3: rst , 4: req_core : mmio_membus , 5: ram_membus: mmio_ram_membus, 6: );
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)。
1: // pc on reset 2: const INITIAL_PC: Addr = MMAP_RAM_BEGIN;
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)。
1: OUTPUT_ARCH( "riscv" )
2: ENTRY(_start)
3:
4: SECTIONS
5: {
6: . = 0x00000000; ← 先頭を0x80000000に変更する (戻す)
riscv-testsをビルドしなおし、成果物をtestディレクトリに配置してください。ビルドしなおしたので、HEXファイルを再度生成します(リスト11.33)。
$ 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)。
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)。
1: enum Device { 2: UNKNOWN, 3: RAM, 4: ROM, 5: }
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関数でインターフェースをリセットします。
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: ) {
1: function reset_all_device_masters () { 2: reset_membus_master(ram_membus); 3: reset_membus_master(rom_membus); 4: }
ready
、rvalid
を取得する関数にROMを登録します(リスト11.39、リスト11.40)。
1: case device { 2: Device::RAM: return ram_membus.ready; 3: Device::ROM: return rom_membus.ready; 4: default : {} 5: }
1: case device { 2: Device::RAM: return ram_membus.rvalid; 3: Device::ROM: return rom_membus.rvalid; 4: default : {} 5: }
ROMのrvalid
、rdata
をreq_core
に割り当てます(リスト11.41)。
1: case device { 2: Device::RAM: req <> ram_membus; 3: Device::ROM: req <> rom_membus; 4: default : {} 5: }
ROMのインターフェースに要求を割り当てます(リスト11.42)。RAMと同じようにメモリマップのベースアドレスを引いたアドレスを割り当てます。
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)。
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)。
1: if (argc < 3) { 2: std::cout << "Usage: " << argv[0] << " ROM_FILE_PATH RAM_FILE_PATH [CYCLE]" << std::endl; 3: return 1; 4: }
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: }
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: }
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);
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
にしておきます。
1: parser.add_argument("--rom", default="bootrom.hex", help="hex file of rom")
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
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)。
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モジュールのパラメータを割り当てます。
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)。
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)。
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)。
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)。
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)。
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)。
1: enum Device { 2: UNKNOWN, 3: RAM, 4: ROM, 5: DEBUG, 6: }
ポートにインターフェースとデバイスのアドレスを追加します(リスト11.60、リスト11.61)。
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: ) {
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)。
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を追加したときとほとんど同じです。
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: }
1: case device { 2: Device::RAM : req <> ram_membus; 3: Device::ROM : req <> rom_membus; 4: Device::DEBUG: req <> dbg_membus; 5: default : {} 6: }
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: }
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)。
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;
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)。
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)。
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!
と出力するプログラムです。
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)。
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)。
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ファイルにコンパイルされてしまいます。
$ 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)。
$ 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でインストールできます
1: from elftools.elf.elffile import ELFFile
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ファイルのパスを得るための引数に変更します。
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)。
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)。
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)
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
にした値を返します。
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)。
1: #include <fcntl.h> 2: #include <termios.h> 3: #include <signal.h>
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: }
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)。
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
をつけます。
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
を足した値を出力するプログラムです。
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)。
$ make build sim VERILATOR_FLAGS="-DENABLE_DEBUG_INPUT" ← 入力を有効にしてシミュレータをビルド $ ./obj_dir/sim bootrom.hex test/test.bin.hex ← (事前にHEXファイルを作成しておく bcd← abcと入力して改行 efg← defと入力する