Memory-mapped I/Oの実装
Memory-mapped I/Oとは何か?
これまでの実装では、 CPUに内蔵された1つの大きなメモリ空間、 1つのメモリデバイス(memoryモジュール)に命令データを格納、実行し、 データのロードストア命令も同じメモリに対して実行してきました。
一般に流通するコンピュータは複数のデバイスに接続されています。 CPUが起動すると、読み込み専用の小さなメモリ(ROM)に格納されたプログラムから命令の実行を開始します。 プログラムは周辺デバイスの初期化などを行ったあと、 動かしたいアプリケーションの命令やデータをRAMに展開して、 制御をアプリケーションに移します。
CPUがデバイスにアクセスする方法にはCSRやメモリ空間を経由する方法があります。 一般的な方法はメモリ空間を通じてデバイスにアクセスする方法であり、 この方式のことをメモリマップドIO(Memory-mapped I/O, MMIO)と呼びます。 メモリ空間の一部を、デバイスにアクセスするための空間として扱うことを、メモリ(またはアドレス)にマップすると呼びます。 RAMとROMもメモリデバイスであり、異なるアドレスにマップされています。
本章ではCPUのメモリ部分をRAM(Random Access Memory)[1]とROM(Read Only Memory)に分割し、 アクセスするアドレスに応じてアクセスするデバイスを切り替える機能を実装します。 また、デバッグ用の入出力デバイス(64ビットのレジスタ)も実装します。 デバイスとメモリ空間の対応は図1のように設定します。 図1のようにメモリがどのように配置されているかを示す図のことをメモリマップ(Memory map)と呼びます。 あるメモリ空間の先頭アドレスのことをベースアドレス(base address)と呼ぶことがあります。
定数の定義
eeiパッケージに定義しているメモリの定数をRAM用の定数に変更します。 また、新しくRAMのベースアドレス、メモリバスのデータ幅、ROMのメモリマップを示す定数を定義してください (リスト1)。 デバッグ入出力デバイス(レジスタ)の位置は、topモジュールのポートで定義します (リスト9)。
▼リスト11.1: メモリマップの定義 (eei.veryl) 差分をみる
// メモリバスのデータ幅
const MEMBUS_DATA_WIDTH: u32 = 64;
// メモリのアドレス幅
const MEM_ADDR_WIDTH: u32 = 16;
// RAM
const RAM_ADDR_WIDTH: u32 = 16;
const RAM_DATA_WIDTH: u32 = 64;
const MMAP_RAM_BEGIN: Addr = 'h8000_0000 as Addr;
// ROM
const ROM_ADDR_WIDTH: u32 = 9;
const ROM_DATA_WIDTH: u32 = 64;
const MMAP_ROM_BEGIN: Addr = 'h1000 as Addr;
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をつけて利用します ( リスト2、 リスト3、 リスト4、 リスト5 )。
▼リスト11.2: 別名の定義 (membus_if.veryl) 差分をみる
alias interface Membus = membus_if::<eei::MEMBUS_DATA_WIDTH, eei::XLEN>;
▼リスト11.3: Membusに置き換える (core.veryl) 差分をみる
module core (
clk : input clock ,
rst : input reset ,
i_membus: modport membus_if::<ILEN, XLEN>::master,
d_membus: modport Membus::master ,
led : output UIntX ,
) {
▼リスト11.4: Membusに置き換える (memunit.veryl) 差分をみる
membus: modport Membus::master, // メモリとのinterface
▼リスト11.5: 定数名を変更する (memunit.veryl) 差分をみる
var req_wen : logic ;
var req_addr : Addr ;
var req_wdata: logic<MEMBUS_DATA_WIDTH> ;
var req_wmask: logic<MEMBUS_DATA_WIDTH / 8>;
const W : u32 = XLEN;
let D : logic<MEMBUS_DATA_WIDTH> = membus.rdata;
let sext: logic = ctrl.funct3[2] == 1'b0;
topモジュールでインスタンス化しているmembus_ifインターフェースのジェネリックパラメータを変更します (リスト6)。
▼リスト11.6: ジェネリックパラメータを変更する / Membusに置き換える (top.veryl) 差分をみる
inst membus : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
inst i_membus: membus_if::<ILEN, XLEN>; // 命令フェッチ用
inst d_membus: Membus; // ロードストア命令用
addr_to_memaddr関数をジェネリック関数にして、呼び出すときにRAMのパラメータを使用するように変更します ( リスト7、 リスト8、 )。
▼リスト11.7: addr_to_memaddr関数をジェネリック関数に変更する (top.veryl) 差分をみる
// アドレスをデータ単位でのアドレスに変換する
function addr_to_memaddr::<DATA_WIDTH: u32, ADDR_WIDTH: u32> (
addr: input logic<XLEN>,
) -> logic<ADDR_WIDTH> {
return addr[$clog2(DATA_WIDTH / 8)+:ADDR_WIDTH];
}
▼リスト11.8: ジェネリックパラメータを指定する (top.veryl) 差分をみる
membus.valid = i_membus.valid | d_membus.valid;
if d_membus.valid {
membus.addr = addr_to_memaddr::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>(d_membus.addr);
membus.wen = d_membus.wen;
membus.wdata = d_membus.wdata;
membus.wmask = d_membus.wmask;
} else {
membus.addr = addr_to_memaddr::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>(i_membus.addr);
membus.wen = 0; // 命令フェッチは常に読み込み
membus.wdata = 'x;
membus.wmask = 'x;
}
メモリに読み込むHEXファイルを指定するパラメータの名前を変更します ( リスト9、 リスト10 )。
▼リスト11.9: パラメータ名を変更する (top.veryl) 差分をみる
module top #(
param RAM_FILEPATH_IS_ENV: bit = 1 ,
param RAM_FILEPATH : string = "RAM_FILE_PATH",
) (
▼リスト11.10: パラメータ名を変更する (top.veryl) 差分をみる
inst ram: memory::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH> #(
FILEPATH_IS_ENV: RAM_FILEPATH_IS_ENV,
FILEPATH : RAM_FILEPATH ,
) (
シミュレータ用のC++プログラムも変更します ( リスト11、 リスト12、 リスト13 )。
▼リスト11.11: 引数の名称を変える (tb_verilator.cpp) 差分をみる
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " RAM_FILE_PATH [CYCLE]" << std::endl;
return 1;
}
▼リスト11.12: 環境変数名を変える (tb_verilator.cpp) 差分をみる
// 環境変数でメモリの初期化用ファイルを指定する
const char* original_env = getenv("RAM_FILE_PATH");
setenv("RAM_FILE_PATH", memory_file_path.c_str(), 1);
▼リスト11.13: 環境変数名を変える (tb_verilator.cpp) 差分をみる
// 環境変数を元に戻す
if (original_env != nullptr){
setenv("RAM_FILE_PATH", original_env, 1);
}
mmio_controllerモジュールの作成
アクセスするアドレスに応じてアクセス先のデバイスを切り替えるモジュールを実装します。
src/mmio_controller.verylを作成し、次のように記述します ( リスト14 )。
▼リスト11.14: mmio_controller.veryl 差分をみる
import eei::*;
module mmio_controller (
clk : input clock ,
rst : input reset ,
req_core: modport Membus::slave,
) {
enum Device {
UNKNOWN,
}
inst req_saved: Membus;
var last_device : Device;
var is_requested: logic ;
// masterを0でリセットする
function reset_membus_master (
master: modport Membus::master_output,
) {
master.valid = 0;
master.addr = 0;
master.wen = 0;
master.wdata = 0;
master.wmask = 0;
}
// すべてのデバイスのmasterをリセットする
function reset_all_device_masters () {}
// アドレスからデバイスを取得する
function get_device (
addr: input Addr,
) -> Device {
return Device::UNKNOWN;
}
// デバイスのmasterにreqの情報を割り当てる
function assign_device_master (
req: modport Membus::all_input,
) {}
// デバイスのrvalid、rdataをreqに割り当てる
function assign_device_slave (
device: input Device ,
req : modport Membus::response,
) {
req.rvalid = 1;
req.rdata = 0;
}
// デバイスのreadyを取得する
function get_device_ready (
device: input Device,
) -> logic {
return 1;
}
// デバイスのrvalidを取得する
function get_device_rvalid (
device: input Device,
) -> logic {
return 1;
}
// req_coreの割り当て
always_comb {
req_core.ready = 0;
req_core.rvalid = 0;
req_core.rdata = 0;
if req_saved.valid {
if is_requested {
// 結果を返す
assign_device_slave(last_device, req_core);
req_core.ready = get_device_rvalid(last_device);
}
} else {
req_core.ready = 1;
}
}
// デバイスのmasterの割り当て
always_comb {
reset_all_device_masters();
if req_saved.valid {
if is_requested {
if get_device_rvalid(last_device) {
// 新しく要求を受け入れる
if req_core.ready && req_core.valid {
assign_device_master(req_core);
}
}
} else {
// デバイスにreq_savedを割り当てる
assign_device_master(req_saved);
}
} else {
// 新しく要求を受け入れる
if req_core.ready && req_core.valid {
assign_device_master(req_core);
}
}
}
// 新しく要求を受け入れる
function accept_request () {
req_saved.valid = req_core.ready && req_core.valid;
if req_core.ready && req_core.valid {
last_device = get_device(req_core.addr);
is_requested = get_device_ready(last_device);
// reqを保存
req_saved.addr = req_core.addr;
req_saved.wen = req_core.wen;
req_saved.wdata = req_core.wdata;
req_saved.wmask = req_core.wmask;
}
}
function on_clock () {
if req_saved.valid {
if is_requested {
if get_device_rvalid(last_device) {
accept_request();
}
} else {
is_requested = get_device_ready(last_device);
}
} else {
accept_request();
}
}
function on_reset () {
last_device = Device::UNKNOWN;
is_requested = 0;
reset_membus_master(req_saved);
}
always_ff {
if_reset {
on_reset();
} else {
on_clock();
}
}
}
mmio_controllerモジュールの関数の引数にmembus_ifインターフェースを使うために、 新しくmodportを宣言します (リスト15)。
▼リスト11.15: modport宣言を追加する (membus_if.veryl) 差分をみる
modport all_input {
..input
}
modport response {
rvalid: output,
rdata : output,
}
modport slave_output {
ready: output,
..same(response)
}
modport master_output {
valid: output,
addr : output,
wen : output,
wdata: output,
wmask: output,
}
mmio_controllerモジュールはreq_coreからメモリアクセス要求を受け付け、 アクセス対象のモジュールからの結果を返すモジュールです。
Device型は実装しているデバイスを表現するための列挙型です (リスト16)。 まだデバイスを接続していないので、不明なデバイス(Device::UNKNOWN)だけ定義しています。
▼リスト11.16: Device型の定義 (mmio_controller.veryl) 差分をみる
enum Device {
UNKNOWN,
}
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にして、書き込みは無視します。
RAMの接続
mmio_controllerモジュールにRAMを追加する
mmio_controllerモジュールにRAMとのインターフェースを実装します。
Device型にRAMを追加して、アドレスにRAMをマップします (リスト17、 リスト18 )。
▼リスト11.17: Device型にRAMを追加する (mmio_controller.veryl) 差分をみる
enum Device {
UNKNOWN,
RAM,
}
▼リスト11.18: get_device関数でRAMの範囲を定義する (mmio_controller.veryl) 差分をみる
function get_device (
addr: input Addr,
) -> Device {
if addr >= MMAP_RAM_BEGIN {
return Device::RAM;
}
return Device::UNKNOWN;
}
RAMとのインターフェースを追加し、 reset_all_device_masters関数に要求をリセットするコードを追加します ( リスト19、 リスト20 )。
▼リスト11.19: RAMとのインターフェースを追加する (mmio_controller.veryl) 差分をみる
module mmio_controller (
clk : input clock ,
rst : input reset ,
req_core : modport Membus::slave ,
ram_membus: modport Membus::master,
) {
▼リスト11.20: インターフェースの要求部分をリセットする (mmio_controller.veryl) 差分をみる
function reset_all_device_masters () {
reset_membus_master(ram_membus);
}
ready、rvalidを取得する関数にRAMを登録します ( リスト21、 リスト22 )。
▼リスト11.21: インターフェースのreadyを返す (mmio_controller.veryl) 差分をみる
function get_device_ready (
device: input Device,
) -> logic {
case device {
Device::RAM: return ram_membus.ready;
default : {}
}
return 1;
}
▼リスト11.22: インターフェースのrvalidを返す (mmio_controller.veryl) 差分をみる
function get_device_rvalid (
device: input Device,
) -> logic {
case device {
Device::RAM: return ram_membus.rvalid;
default : {}
}
return 1;
}
RAMのrvalid、rdataをreq_coreに割り当てます (リスト23)。
▼リスト11.23: RAMへのアクセス結果をreqに割り当てる (mmio_controller.veryl) 差分をみる
function assign_device_slave (
device: input Device ,
req : modport Membus::response,
) {
req.rvalid = 1;
req.rdata = 0;
case device {
Device::RAM: req <> ram_membus;
default : {}
}
}
RAMのインターフェースに要求を割り当てます (リスト24)。 ここでRAMのベースアドレスを引いたアドレスを割り当てることで、MMAP_RAM_BEGINが0になるようにしています。
▼リスト11.24: RAMにreqを割り当ててアクセス要求する (mmio_controller.veryl) 差分をみる
function assign_device_master (
req: modport Membus::all_input,
) {
case get_device(req.addr) {
Device::RAM: {
ram_membus <> req;
ram_membus.addr -= MMAP_RAM_BEGIN;
}
default: {}
}
}
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に改名します ( リスト25、 リスト26 )。
▼リスト11.25: インターフェースの定義 / インスタンス名を変更する (top.veryl) 差分をみる
inst mmio_membus : Membus;
inst mmio_ram_membus: Membus;
inst ram_membus : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
▼リスト11.26: ポート名を変更する (top.veryl) 差分をみる
inst ram: memory::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH> #(
FILEPATH_IS_ENV: RAM_FILEPATH_IS_ENV,
FILEPATH : RAM_FILEPATH ,
) (
clk ,
rst ,
membus: ram_membus,
);
coreモジュールからRAMへのメモリアクセスを調停する処理を、 coreモジュールからmmio_controllerモジュールへのアクセスを調停する処理に変更します (リスト27)。
▼リスト11.27: 調停する対象をmmio_membusに変更する (top.veryl) 差分をみる
// mmio_controllerへのメモリアクセスを調停する
always_ff {
if_reset {
memarb_last_i = 0;
memarb_last_iaddr = 0;
} else {
if mmio_membus.ready {
memarb_last_i = !d_membus.valid;
memarb_last_iaddr = i_membus.addr;
}
}
}
always_comb {
i_membus.ready = mmio_membus.ready && !d_membus.valid;
i_membus.rvalid = mmio_membus.rvalid && memarb_last_i;
i_membus.rdata = if memarb_last_iaddr[2] == 0 ? mmio_membus.rdata[31:0] : mmio_|membus.rdata[63:32];
d_membus.ready = mmio_membus.ready;
d_membus.rvalid = mmio_membus.rvalid && !memarb_last_i;
d_membus.rdata = mmio_membus.rdata;
mmio_membus.valid = i_membus.valid | d_membus.valid;
if d_membus.valid {
mmio_membus.addr = d_membus.addr;
mmio_membus.wen = d_membus.wen;
mmio_membus.wdata = d_membus.wdata;
mmio_membus.wmask = d_membus.wmask;
} else {
mmio_membus.addr = i_membus.addr;
mmio_membus.wen = 0; // 命令フェッチは常に読み込み
mmio_membus.wdata = 'x;
mmio_membus.wmask = 'x;
}
}
mmio_controllerをインスタンス化し、RAMと接続します。 ( リスト28、 リスト29 )。 RAMのアドレスへの変換は調停処理から接続部分に移動しています。
▼リスト11.28: mmio_controllerモジュールをインスタンス化する (top.veryl) 差分をみる
inst mmioc: mmio_controller (
clk ,
rst ,
req_core : mmio_membus ,
ram_membus: mmio_ram_membus,
);
▼リスト11.29: mmio_controllerモジュールとRAMを接続する (top.veryl) 差分をみる
always_comb {
// mmio <> RAM
ram_membus.valid = mmio_ram_membus.valid;
mmio_ram_membus.ready = ram_membus.ready;
ram_membus.addr = addr_to_memaddr::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>(mmio_ram_membus.addr);
ram_membus.wen = mmio_ram_membus.wen;
ram_membus.wdata = mmio_ram_membus.wdata;
ram_membus.wmask = mmio_ram_membus.wmask;
mmio_ram_membus.rvalid = ram_membus.rvalid;
mmio_ram_membus.rdata = ram_membus.rdata;
}
PCの初期値の変更
PCの初期値をMMAP_RAM_BEGINにすることで、RAMのベースアドレスからプログラムの実行を開始するように変更します。 eeiパッケージにINITIAL_PCを定義し、PCのリセット時に利用します ( リスト30、 リスト31 )。
▼リスト11.30: PCの初期値を定義する (eei.veryl) 差分をみる
// pc on reset
const INITIAL_PC: Addr = MMAP_RAM_BEGIN;
▼リスト11.31: PCの初期値を設定する (core.veryl) 差分をみる
always_ff {
if_reset {
if_pc = INITIAL_PC;
if_is_requested = 0;
if_pc_requested = 0;
if_fifo_wvalid = 0;
if_fifo_wdata = 0;
} else {
riscv-testsを実行してRAMにアクセスできているか確認します。 今のところriscv-testsはアドレス0から配置されるようにリンクしているため、 riscv-testsのenv/p/link.ldを変更します (リスト32)。
▼リスト11.32: プログラムの先頭のアドレスを変更する (riscv-tests/env/p/link.ld)
OUTPUT_ARCH( "riscv" )
ENTRY(_start)
SECTIONS
{
. = 0x00000000; ← 先頭を0x80000000に変更する (戻す)
riscv-testsをビルドしなおし、成果物をtestディレクトリに配置してください。 ビルドしなおしたので、HEXファイルを再度生成します (リスト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基準のアドレスに変更します (リスト34)。
▼リスト11.34: .tohostのアドレスを変更する (top.veryl) 差分をみる
#[ifdef(TEST_MODE)]
always_ff {
let RISCVTESTS_TOHOST_ADDR: Addr = MMAP_RAM_BEGIN + 'h1000 as Addr;
if d_membus.valid && d_membus.ready && d_membus.wen == 1 && d_membus.addr == RISCVTESTS_TOHOST_ADDR && d_membus.wdata[lsb] == 1'b1 {
test_success = d_membus.wdata == 1;
if d_membus.wdata == 1 {
$display("riscv-tests success!");
} else {
$display("riscv-tests failed!");
$error ("wdata : %h", d_membus.wdata);
}
$finish();
}
}
riscv-testsを実行し、RAMにアクセスできてテストに成功することを確認してください。
ROMの実装
mmio_controllerモジュールにROMを追加する
mmio_controllerモジュールにROMとのインターフェースを実装します。
Device型にROMを追加して、アドレスにROMをマップします ( リスト35、 リスト36 )。
▼リスト11.35: Device型にROMを変更する (mmio_controller.veryl) 差分をみる
enum Device {
UNKNOWN,
RAM,
ROM,
}
▼リスト11.36: get_device関数でROMの範囲を定義する (mmio_controller.veryl) 差分をみる
function get_device (
addr: input Addr,
) -> Device {
if MMAP_ROM_BEGIN <= addr && addr <= MMAP_ROM_END {
return Device::ROM;
}
if addr >= MMAP_RAM_BEGIN {
return Device::RAM;
}
return Device::UNKNOWN;
}
ROMとのインターフェースを追加します ( リスト37、 リスト38 )。 reset_all_device_masters関数でインターフェースをリセットします。
▼リスト11.37: ROMとのインターフェースを追加する (mmio_controller.veryl) 差分をみる
module mmio_controller (
clk : input clock ,
rst : input reset ,
req_core : modport Membus::slave ,
ram_membus: modport Membus::master,
rom_membus: modport Membus::master,
) {
▼リスト11.38: インターフェースの要求部分をリセットする (mmio_controller.veryl) 差分をみる
function reset_all_device_masters () {
reset_membus_master(ram_membus);
reset_membus_master(rom_membus);
}
ready、rvalidを取得する関数にROMを登録します ( リスト39、 リスト40 )。
▼リスト11.39: インターフェースのreadyを返す (mmio_controller.veryl) 差分をみる
case device {
Device::RAM: return ram_membus.ready;
Device::ROM: return rom_membus.ready;
default : {}
}
▼リスト11.40: インターフェースのrvalidを返す (mmio_controller.veryl) 差分をみる
case device {
Device::RAM: return ram_membus.rvalid;
Device::ROM: return rom_membus.rvalid;
default : {}
}
ROMのrvalid、rdataをreq_coreに割り当てます ( リスト41 )。
▼リスト11.41: assign_device_slave関数でROMの結果をreqに割り当てる (mmio_controller.veryl) 差分をみる
case device {
Device::RAM: req <> ram_membus;
Device::ROM: req <> rom_membus;
default : {}
}
ROMのインターフェースに要求を割り当てます ( リスト42 )。 RAMと同じようにメモリマップのベースアドレスを引いたアドレスを割り当てます。
▼リスト11.42: get_device関数でROMにreqを割り当ててアクセス要求する (mmio_controller.veryl) 差分をみる
case get_device(req.addr) {
Device::RAM: {
ram_membus <> req;
ram_membus.addr -= MMAP_RAM_BEGIN;
}
Device::ROM: {
rom_membus <> req;
rom_membus.addr -= MMAP_ROM_BEGIN;
}
default: {}
}
ROMの初期値のパラメータを作成する
topモジュールにROMの初期値を指定するパラメータを定義します ( リスト43 )。
▼リスト11.43: パラメータを定義する (top.veryl) 差分をみる
module top #(
param RAM_FILEPATH_IS_ENV: bit = 1 ,
param RAM_FILEPATH : string = "RAM_FILE_PATH",
param ROM_FILEPATH_IS_ENV: bit = 1 ,
param ROM_FILEPATH : string = "ROM_FILE_PATH",
) (
RAMと同じように、シミュレータ用のプログラムでROMのHEXファイルのパスを指定するようにします。 1番目の引数をROM用のHEXファイルのパスに変更し、環境変数ROM_FILE_PATHをその値に設定します ( リスト44、 リスト45、 リスト46、 リスト47、 リスト48 )。
▼リスト11.44: 引数の名称を変える (tb_verilator.cpp) 差分をみる
if (argc < 3) {
std::cout << "Usage: " << argv[0] << " ROM_FILE_PATH RAM_FILE_PATH [CYCLE]" << std::endl;
return 1;
}
▼リスト11.45: ROMのHEXファイルのパスを生成する (tb_verilator.cpp) 差分をみる
// メモリの初期値を格納しているファイル名
std::string rom_file_path = argv[1];
std::string ram_file_path = argv[2];
try {
// 絶対パスに変換する
rom_file_path = fs::absolute(rom_file_path).string();
ram_file_path = fs::absolute(ram_file_path).string();
} catch (const std::exception& e) {
std::cerr << "Invalid memory file path : " << e.what() << std::endl;
return 1;
}
▼リスト11.46: 引数の数が変わったのでインデックスを変更する (tb_verilator.cpp) 差分をみる
unsigned long long cycles = 0;
if (argc >= 4) {
std::string cycles_string = argv[3];
try {
cycles = stoull(cycles_string);
} catch (const std::exception& e) {
std::cerr << "Invalid number: " << argv[3] << std::endl;
return 1;
}
}
▼リスト11.47: 環境変数を変更する (tb_verilator.cpp) 差分をみる
const char* original_env_rom = getenv("ROM_FILE_PATH");
const char* original_env_ram = getenv("RAM_FILE_PATH");
setenv("ROM_FILE_PATH", rom_file_path.c_str(), 1);
setenv("RAM_FILE_PATH", ram_file_path.c_str(), 1);
▼リスト11.48: 環境変数を元に戻す (tb_verilator.cpp) 差分をみる
if (original_env_rom != nullptr){
setenv("ROM_FILE_PATH", original_env_rom, 1);
}
if (original_env_ram != nullptr){
setenv("RAM_FILE_PATH", original_env_ram, 1);
}
テストを実行するためのPythonプログラムでROMのHEXファイルを指定できるようにします ( リスト49、 リスト50、 リスト51 )。 デフォルト値はカレントディレクトリのbootrom.hexにしておきます。
▼リスト11.49: 引数--romを追加する (test/test.py) 差分をみる
parser.add_argument("--rom", default="bootrom.hex", help="hex file of rom")
▼リスト11.50: シミュレータにROMのHEXファイルのパスを渡す (test/test.py) 差分をみる
def test(romhex, file_name):
result_file_path = os.path.join(args.output_dir, file_name.replace(os.sep, "_") + ".txt")
cmd = f"{args.sim_path} {romhex} {file_name} 0"
success = False
▼リスト11.51: test関数にROMのHEXファイルのパスを渡す (test/test.py) 差分をみる
for hexpath in dir_walk(args.dir):
f, s = test(os.path.abspath(args.rom), os.path.abspath(hexpath))
res_strs.append(("PASS" if s else "FAIL") + " : " + f)
res_statuses.append(s)
ROMとmmio_controllerモジュールを接続する
ROMをインスタンス化してmmio_controllerモジュールと接続します。
ROMとmmio_controllerモジュールを接続するインターフェース(mmio_rom_membus)、 ROMのインターフェース(rom_membus)を定義します (リスト52)。
▼リスト11.52: ROMのインターフェースの定義 (top.veryl) 差分をみる
inst mmio_membus : Membus;
inst mmio_ram_membus: Membus;
inst mmio_rom_membus: Membus;
inst ram_membus : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
inst rom_membus : membus_if::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH>;
ROMをインスタンス化します ( リスト53 )。 パラメータにはtopモジュールのパラメータを割り当てます。
▼リスト11.53: ROMをインスタンス化する (top.veryl) 差分をみる
inst rom: memory::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH> #(
FILEPATH_IS_ENV: ROM_FILEPATH_IS_ENV,
FILEPATH : ROM_FILEPATH ,
) (
clk ,
rst ,
membus: rom_membus,
);
mmio_controllerモジュールにrom_membusを接続します ( リスト54 )。
▼リスト11.54: ROMのインターフェースを接続する (top.veryl) 差分をみる
inst mmioc: mmio_controller (
clk ,
rst ,
req_core : mmio_membus ,
ram_membus: mmio_ram_membus,
rom_membus: mmio_rom_membus,
);
mmio_controllerモジュールとROMを接続します。 アドレスの変換のためにaddr_to_memaddr関数を使用しています ( リスト55 )。
▼リスト11.55: mmio_controllerモジュールとROMを接続する (top.veryl) 差分をみる
always_comb {
// mmio <> ROM
rom_membus.valid = mmio_rom_membus.valid;
mmio_rom_membus.ready = rom_membus.ready;
rom_membus.addr = addr_to_memaddr::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH>(mmio_rom_membus.addr);
rom_membus.wen = 0;
rom_membus.wdata = 0;
rom_membus.wmask = 0;
mmio_rom_membus.rvalid = rom_membus.rvalid;
mmio_rom_membus.rdata = rom_membus.rdata;
}
ROMからRAMにジャンプする
PCの初期値をROMのベースアドレスに変更し、 ROMからRAMにジャンプする仕組みを実現します。
一般的にCPUの電源をつけると、CPUはROMのようなメモリデバイスに入ったソフトウェアから実行を開始します。 そのソフトウェアは次に実行するソフトウェアを外部記憶装置から読み取り、 RAMにソフトウェアを適切にコピー、配置して実行します。
本章ではRAM、ROMともに$readmemhシステムタスクで初期化するように実装しているので、 RAMのベースアドレスにジャンプするだけのプログラムをROMに設定します。
ROMに設定するためのHEXファイルを作成します (リスト56)。
▼リスト11.56: RAMの開始アドレスにジャンプするプログラム (bootrom.hex) 差分をみる
00409093080000b7 // 0: lui x1, 0x08000 4: slli x1, x1, 4
0000000000008067 // 8: jalr x0, 0(x1) c:
0000000000000000 // zero
PCの初期値をROMのベースアドレスに変更します ( リスト57 )。
▼リスト11.57: PCの初期値の変更 (eei.veryl) 差分をみる
const INITIAL_PC: Addr = MMAP_ROM_BEGIN;
riscv-testsを実行し、 ROM(0x1000)から実行を開始して RAM(0x80000000)にジャンプしてテストを開始していることを確かめてください。
デバッグ用の入出力デバイスの実装
CPUが文字を送信したり受信するためのデバッグ用の入出力デバイスを実装します。 今のところriscv-testsの結果を受け取るためのアドレスをRAMのベースアドレス + 0x1000にしていますが、 この処理もデバイスに実装します。
本章では、デバッグ用の入出力デバイスに次のような64ビットレジスタを実装します。
- 上位20ビットが`20'h01010`な値を書き込み
- 下位8ビットを文字として解釈し`$write`システムタスクで出力します。
- 上位20ビットが`20'h01010`ではないLSBが`1`な値を書き込み
- 今までのriscv-testsの終了判定処理を行います。
- 読み込み
- C++プログラムの関数を利用して1文字入力を受け取ります。 有効な入力の場合は上位20ビットが`20'h01010`、無効な入力の場合は`0`になります。
デバイスのアドレスを設定する
リスト9でデバイスのアドレスをポートで設定できるようにしたので、 tb_verilator.cppで環境変数の値をデバイスのアドレスに設定するようにします。
環境変数DBG_ADDRを読み込み、DBG_ADDRポートに設定します ( リスト58 )。
▼リスト11.58: DBG_ADDRポートに環境変数の値を設定する (tb_verilator.cpp) 差分をみる
// デバッグ用の入出力デバイスのアドレスを取得する
const char* dbg_addr_c = getenv("DBG_ADDR");
const unsigned long long DBG_ADDR = dbg_addr_c == nullptr ? 0 : std::strtoull(dbg_addr_c, nullptr, 0);
// top
Vcore_top *dut = new Vcore_top();
dut->MMAP_DBG_ADDR = DBG_ADDR;
mmio_controllerモジュールにデバイスを追加する
mmio_controllerモジュールにデバイスを追加します。
Device型にDevice::DEBUGを追加します ( リスト59 )。
▼リスト11.59: Device型にデバッグ用の入出力デバイスを追加する (mmio_controller.veryl) 差分をみる
enum Device {
UNKNOWN,
RAM,
ROM,
DEBUG,
}
ポートにインターフェースとデバイスのアドレスを追加します ( リスト60、 リスト61 )。
▼リスト11.60: DBG_ADDR、インターフェースを追加する (mmio_controller.veryl)
module mmio_controller (
clk : input clock ,
rst : input reset ,
DBG_ADDR : input Addr ,
req_core : modport Membus::slave ,
ram_membus: modport Membus::master,
rom_membus: modport Membus::master,
dbg_membus: modport Membus::master,
) {
▼リスト11.61: インターフェースの要求部分をリセットする (mmio_controller.veryl) 差分をみる
function reset_all_device_masters () {
reset_membus_master(ram_membus);
reset_membus_master(rom_membus);
reset_membus_master(dbg_membus);
}
デバイスの位置を設定します。 最初にチェックすることで、他のデバイスとアドレスを被らせたとしてもデバッグ用の入出力デバイスを優先します ( リスト62 )。
▼リスト11.62: get_device関数でデバイスの範囲を定義する (mmio_controller.veryl) 差分をみる
function get_device (
addr: input Addr,
) -> Device {
if DBG_ADDR <= addr && addr <= DBG_ADDR + 7 {
return Device::DEBUG;
}
if MMAP_ROM_BEGIN <= addr && addr <= MMAP_ROM_END {
return Device::ROM;
}
if addr >= MMAP_RAM_BEGIN {
return Device::RAM;
}
return Device::UNKNOWN;
}
インターフェースを設定します ( リスト63、 リスト64、 リスト65、 リスト66 )。 この変更はROMを追加したときとほとんど同じです。
▼リスト11.63: assign_device_master関数の変更 (mmio_controller.veryl) 差分をみる
case get_device(req.addr) {
Device::RAM: {
ram_membus <> req;
ram_membus.addr -= MMAP_RAM_BEGIN;
}
Device::ROM: {
rom_membus <> req;
rom_membus.addr -= MMAP_ROM_BEGIN;
}
Device::DEBUG: {
dbg_membus <> req;
dbg_membus.addr -= DBG_ADDR;
}
default: {}
}
▼リスト11.64: assign_device_slave関数の変更 (mmio_controller.veryl) 差分をみる
case device {
Device::RAM : req <> ram_membus;
Device::ROM : req <> rom_membus;
Device::DEBUG: req <> dbg_membus;
default : {}
}
▼リスト11.65: get_device_ready関数の変更 (mmio_controller.veryl) 差分をみる
case device {
Device::RAM : return ram_membus.ready;
Device::ROM : return rom_membus.ready;
Device::DEBUG: return dbg_membus.ready;
default : {}
}
▼リスト11.66: get_device_rvalid関数の変更 (mmio_controller.veryl) 差分をみる
case device {
Device::RAM : return ram_membus.rvalid;
Device::ROM : return rom_membus.rvalid;
Device::DEBUG: return dbg_membus.rvalid;
default : {}
}
topモジュールにデバッグ用の入出力デバイスのインターフェース(dbg_membus)を定義し、 mmio_controllerモジュールと接続します ( リスト67、 リスト68 )。
▼リスト11.67: インターフェースのインスタンス化 (top.veryl) 差分をみる
inst ram_membus : membus_if::<RAM_DATA_WIDTH, RAM_ADDR_WIDTH>;
inst rom_membus : membus_if::<ROM_DATA_WIDTH, ROM_ADDR_WIDTH>;
inst dbg_membus : Membus;
▼リスト11.68: インターフェースを接続する (top.veryl) 差分をみる
inst mmioc: mmio_controller (
clk ,
rst ,
DBG_ADDR : MMAP_DBG_ADDR ,
req_core : mmio_membus ,
ram_membus: mmio_ram_membus,
rom_membus: mmio_rom_membus,
dbg_membus ,
);
出力を実装する
dbg_membusを使い、デバッグ出力処理を実装します。 既存のriscv-testsの終了検知処理を次のように書き換えます ( リスト69 )。
▼リスト11.69: riscv-testsの終了検知処理をデバッグ用の入出力デバイスに変更する (top.veryl) 差分をみる
// デバッグ用のIO
always_ff {
dbg_membus.ready = 1;
dbg_membus.rvalid = dbg_membus.valid;
if dbg_membus.valid {
if dbg_membus.wen {
if dbg_membus.wdata[MEMBUS_DATA_WIDTH - 1-:20] == 20'h01010 {
$write("%c", dbg_membus.wdata[7:0]);
} else if dbg_membus.wdata[lsb] == 1'b1 {
#[ifdef(TEST_MODE)]
{
test_success = dbg_membus.wdata == 1;
}
if dbg_membus.wdata == 1 {
$display("test success!");
} else {
$display("test failed!");
$error ("wdata : %h", dbg_membus.wdata);
}
$finish();
}
}
}
}
常に要求を受け付け、書き込みの時は書き込むデータ(wdata)を確認します。 wdataの上位20ビットが20'h01010なら下位8ビットを出力し、 LSBが1ならテストの成功判定をして$finishシステムタスクを呼び出します。
出力をテストする
実装した出力デバイスで文字を出力できることを確認します。
デバッグ用に$displayシステムタスクで表示している情報が邪魔になるので、 デバッグ情報の表示を環境変数PRINT_DEBUGで制御できるようにします ( リスト70 )。
▼リスト11.70: デバッグ出力をdefineで囲う (core.veryl)
///////////////////////////////// DEBUG /////////////////////////////////
#[ifdef(PRINT_DEBUG)]
{
var clock_count: u64;
always_ff {
if_reset {
clock_count = 1;
} else {
clock_count = clock_count + 1;
$display("");
$display("# %d", clock_count);
test/debug_output.cを作成し、次のように記述します ( リスト71 )。 これはHello,world!と出力するプログラムです。
▼リスト11.71: Hello,world!を出力するプログラム (test/debug_output.c) 差分をみる
#define DEBUG_REG ((volatile unsigned long long*)0x40000000)
void main(void) {
int strlen = 13;
unsigned char str[13];
str[0] = 'H';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = ',';
str[6] = 'w';
str[7] = 'o';
str[8] = 'r';
str[9] = 'l';
str[10] = 'd';
str[11] = '!';
str[12] = '\n';
for (int i = 0; i < strlen; i++) {
unsigned long long c = str[i];
*DEBUG_REG = c | (0x01010ULL << 44);
}
*DEBUG_REG = 1;
}
DEBUG_REGは出力デバイスのアドレスです。 ここに0x01010を44ビット左シフトした値と文字をOR演算した値を書き込むことで文字を出力します。 最後に1を書き込み、テストを終了しています。
main関数をそのままコンパイルしてRAMに配置すると、 スタックポインタ(stack pointer, sp)の値が適切に設定されていないのでうまく動きません。 スタックポインタとは、プログラムが一時的に利用する値を格納しておくためのメモリ(スタック)のアドレスへのポインタのことです。 RISC-Vの規約ではsp(x2)レジスタをスタックポインタとして利用することが定められています。
そのため、レジスタの値を適切な値にリセットしてmain関数を呼び出す別のプログラムが必要です。 test/entry.Sを作成し、次のように記述します ( リスト72 )。
▼リスト11.72: test/entry.S 差分をみる
.global _start
.section .text.init
_start:
add x1, x0, x0
la x2, _stack_bottom
add x3, x0, x0
add x4, x0, x0
add x5, x0, x0
add x6, x0, x0
add x7, x0, x0
add x8, x0, x0
add x9, x0, x0
add x10, x0, x0
add x11, x0, x0
add x12, x0, x0
add x13, x0, x0
add x14, x0, x0
add x15, x0, x0
add x16, x0, x0
add x17, x0, x0
add x18, x0, x0
add x19, x0, x0
add x20, x0, x0
add x21, x0, x0
add x22, x0, x0
add x23, x0, x0
add x24, x0, x0
add x25, x0, x0
add x26, x0, x0
add x27, x0, x0
add x28, x0, x0
add x29, x0, x0
add x30, x0, x0
add x31, x0, x0
call main
このアセンブリはsp(x2)レジスタを_stack_bottomのアドレスに設定し、 他のレジスタを0でリセットしたあとにmainにジャンプします。
_stack_bottomは、リンカの設定ファイルに記述します。 test/link.ldを作成し、次のように記述します ( リスト73 )。
▼リスト11.73: test/link.ld 差分をみる
OUTPUT_ARCH( "riscv" )
ENTRY(_start)
SECTIONS
{
. = 0x80000000;
.text.init : { *(.text.init) }
.text : { *(.text*) }
.data : { *(.data*) }
.bss : {*(.bss*)}
.stack : {
. = ALIGN(0x10);
_stack_top = .;
. += 4K;
_stack_bottom = .;
}
_end = .;
}
_stack_bottomと_stack_topの間は4KBあるので、スタックのサイズは4KBになります。 _startを.text.initに配置し(リスト72)、 SECTIONSの先頭に.text.initを配置しているため、 アドレス0x80000000に_startが配置されます。
これらのファイルを利用し、テストプログラムをコンパイルします (リスト74)。 gccの-marchフラグではC拡張を抜いたISAを指定しています。 このフラグを記述しないと、まだ実装していない命令が含まれたELFファイルにコンパイルされてしまいます。
▼リスト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ファイルに変換する
シミュレータをビルドし、テストプログラムを実行します (リスト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!と出力されたあと、プログラムが終了しました。
riscv-testsに対応する
riscv-testsを実行するとき、 終了判定用のレジスタの位置をDBG_ADDRに設定するようにします。
test/test.pyを、 ELFファイルを探して自動でDBG_ADDRを設定してテストを実行するプログラムに変更します。
elftools[2]を使用し、ELFファイルの判定、セクションのアドレスを取得する関数を定義します ( リスト76、 リスト77 )。
▼リスト11.76: elftoolsのimport (test/test.py)
from elftools.elf.elffile import ELFFile
▼リスト11.77: ELFの判定、セクションのアドレスを取得する関数の定義 (test/test.py)
def is_elf(filepath):
try:
with open(filepath, 'rb') as f:
magic_number = f.read(4)
return magic_number == b'\x7fELF'
except:
return False
def get_section_address(filepath, section_name):
try:
with open(filepath, 'rb') as f:
elffile = ELFFile(f)
for section in elffile.iter_sections():
if section.name == section_name:
return section.header['sh_addr']
return 0
except:
return 0
デバッグ用の入出力デバイスのセクション名を指定する引数を作成します ( リスト78 )。 また、テストするファイルの拡張子を指定していた引数を、 ELFファイルに付加することでHEXファイルのパスを得るための引数に変更します。
▼リスト11.78: オプションを追加する (test/test.py)
parser.add_argument("-e", "--extension", default=".bin.hex", help="hex file extension")
parser.add_argument("-d", "--debug_label", default=".tohost", help="debug device label")
dir_walk関数を、ELFファイルを探す関数に変更します ( リスト79 )。
▼リスト11.79: dir_walk関数でELFファイルを探す (test/test.py)
if entry.is_file():
if not is_elf(entry.path):
continue
if len(args.files) == 0:
yield entry.path
シミュレータの実行でDBG_ADDRを指定するようにします ( リスト80、 リスト81 )。
▼リスト11.80: DBG_ADDRをシミュレータに渡す (test/test.py)
def test(dbg_addr, romhex, file_name):
result_file_path = os.path.join(args.output_dir, file_name.replace(os.sep, "_") + ".txt")
env = f"DBG_ADDR={dbg_addr} "
cmd = f"{args.sim_path} {romhex} {file_name} 0"
success = False
with open(result_file_path, "w") as f:
no = f.fileno()
p = subprocess.Popen(" ".join([env, "exec", cmd]), shell=True, stdout=no, stderr=no)
▼リスト11.81: DBG_ADDRをtest関数に渡す (test/test.py)
for elfpath in dir_walk(args.dir):
hexpath = elfpath + args.extension
if not os.path.exists(hexpath):
print("SKIP :", elfpath)
continue
dbg_addr = get_section_address(elfpath, args.debug_label)
f, s = test(dbg_addr, os.path.abspath(args.rom), os.path.abspath(hexpath))
res_strs.append(("PASS" if s else "FAIL") + " : " + f)
res_statuses.append(s)
VERILATOR_FLAGS="-DTEST_MODE"をつけてシミュレータをビルドし、 riscv-testsが正常終了することを確かめてください。
入力を実装する
dbg_membusを使い、デバッグ入力処理を実装します。
まず、src/tb_verilator.cppに、標準入力から1文字取得する関数を定義します ( リスト82 )。 入力がない場合は0、ある場合は上位20ビットを0x01010にした値を返します。
▼リスト11.82: 標準入力を1文字取得する関数の定義 (src/tb_verilator.cpp)
extern "C" const unsigned long long get_input_dpic() {
unsigned char c = 0;
ssize_t bytes_read = read(STDIN_FILENO, &c, 1);
if (bytes_read == 1) {
return static_cast<unsigned long long>(c) | (0x01010ULL << 44);
}
return 0;
}
ここで、read関数の呼び出しでシミュレータを止めず(O_NONBLOCK)、 シェルが入力をバッファリングしなくする(~ICANON)ために設定を変えるコードを挿入します。 また、シェルが文字列をローカルエコー(入力した文字列を表示)しないようにします(~ECHO) ( リスト83、 リスト84、 リスト85 )。
▼リスト11.83: includeを追加する (src/tb_verilator.cpp) 差分をみる
#include <fcntl.h>
#include <termios.h>
#include <signal.h>
▼リスト11.84: 設定を変更、復元する関数の定義 (src/tb_verilator.cpp) 差分をみる
struct termios old_setting;
void restore_termios_setting(void) {
tcsetattr(STDIN_FILENO, TCSANOW, &old_setting);
}
void sighandler(int signum) {
restore_termios_setting();
exit(signum);
}
void set_nonblocking(void) {
struct termios new_setting;
if (tcgetattr(STDIN_FILENO, &old_setting) == -1) {
perror("tcgetattr");
return;
}
new_setting = old_setting;
new_setting.c_lflag &= ~(ICANON | ECHO);
if (tcsetattr(STDIN_FILENO, TCSANOW, &new_setting) == -1) {
perror("tcsetattr");
return;
}
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
signal(SIGQUIT, sighandler);
atexit(restore_termios_setting);
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
if (flags == -1) {
perror("fcntl(F_GETFL)");
return;
}
if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl(F_SETFL)");
return;
}
}
▼リスト11.85: 設定を変える関数をmain関数から呼び出す (src/tb_verilator.cpp) 差分をみる
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
if (argc < 3) {
std::cout << "Usage: " << argv[0] << " ROM_FILE_PATH RAM_FILE_PATH [CYCLE]" << std::endl;
return 1;
}
#ifdef ENABLE_DEBUG_INPUT
set_nonblocking();
#endif
src/util.verylにget_input_dpic関数を呼び出す関数を実装します ( リスト86 )。
▼リスト11.86: get_input関数を定義する (src/util.veryl) 差分をみる
embed (inline) sv{{{
package svutil;
...
import "DPI-C" context function longint get_input_dpic();
function longint get_input();
return get_input_dpic();
endfunction
endpackage
}}}
package util {
...
function get_input () -> u64 {
return $sv::svutil::get_input();
}
}
デバッグ用の入出力デバイスのロードでutil::get_inputの結果を返すようにします ( リスト87 )。 このコードは合成できないので、有効化オプションENABLE_DEBUG_INPUTをつけます。
▼リスト11.87: 読み込みでget_input関数を呼び出す (src/top.veryl) 差分をみる
always_ff {
dbg_membus.ready = 1;
dbg_membus.rvalid = dbg_membus.valid;
if dbg_membus.valid {
if dbg_membus.wen {
...
} else {
#[ifdef(ENABLE_DEBUG_INPUT)]
{
dbg_membus.rdata = util::get_input();
}
}
}
}
入力をテストする
実装した入出力デバイスで文字を入出力できることを確認します。
test/debug_input.cを作成し、次のように記述します ( リスト88 )。 これは入力された文字に1を足した値を出力するプログラムです。
▼リスト11.88: test/debug_input.c 差分をみる
#define DEBUG_REG ((volatile unsigned long long*)0x40000000)
void main(void) {
while (1) {
unsigned long long c = *DEBUG_REG;
if (c & (0x01010ULL << 44) == 0) {
continue;
}
c = c & 255;
*DEBUG_REG = (c + 1) | (0x01010ULL << 44);
}
}
プログラムをコンパイルしてシミュレータを実行し、入力した文字が1文字ずれて表示されることを確認してください (リスト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と入力する