FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

riscv-isa-simに外部デバイスプラグインを導入する方法

riscv-isa-sim(いわゆるspikeシミュレータ)は便利なツールだが、例えば外部デバイスとして任意の場所に自作のデバイスを追加することができる。 例えば、自分の場合だと0x5400_0000に自作のシリアルデバイスを追加したい場合はどうするか。いろいろ情報は少ないのだが、spikeのソースコードとhelpファイルを見ながらいろいろ試行してみる。

$ spike --help
Spike RISC-V ISA Simulator 1.0.1-dev

usage: spike [host options] <target program> [target options]
Host Options:
...
  --device=<P,B,A>      Attach MMIO plugin device from an --extlib library
                          P -- Name of the MMIO plugin
                          B -- Base memory address of the device
                          A -- String arguments to pass to the plugin
                          This flag can be used multiple times.
                          The extlib flag for the library must come first.
  --log-cache-miss      Generate a log of cache miss
  --extension=<name>    Specify RoCC Extension
                          This flag can be used multiple times.
  --extlib=<name>       Shared library to load
                        This flag can be used multiple times.

まずは--extlibは外部デバイスを指定する。これは共有ライブラリの形式で指定する。この共有ライブラリはどのように作るかというと、abstract_device_tを継承したクラスを作成してコンパイルする。

// See LICENSE for license details.
#ifndef _RISCV_SERIALDEVICE_H
#define _RISCV_SERIALDEVICE_H

#include <riscv/mmio_plugin.h>

#include "abstract_device.h"
#include "mmu.h"

class sim_t;
class bus_t;

class serialdevice_t : public abstract_device_t {
 public:
  serialdevice_t(std::string name);
  bool load(reg_t addr, size_t len, uint8_t* bytes);
  bool store(reg_t addr, size_t len, const uint8_t* bytes);
 private:
};


serialdevice_t::serialdevice_t(std::string name)
{
  std::cerr << "serialdevice: " << name << " loaded\n";
}

bool serialdevice_t::load(reg_t addr, size_t len, uint8_t* bytes)
{
  // std::cerr << "serialdevice_t::load called : addr = " << std::hex << addr << '\n';
  switch (addr) {
    case 5 : return 0x20;
    default : return 0x0;
  }

  return true;
}

bool serialdevice_t::store(reg_t addr, size_t len, const uint8_t* bytes)
{
  // std::cerr << "serialdevice_t::store called : addr = " << std::hex << addr << '\n';
  std::cout << bytes[0];
  return true;
}

static mmio_plugin_registration_t<serialdevice_t> serialdevice_mmio_plugin_registration("serialdevice");

#endif // _RISCV_SERIALDEVICE_H

ビルドコマンドは以下。

g++ -o libserialdevice.so -fPIC -shared serialdevice.cc -I./riscv-isa-sim/riscv -I./riscv-isa-sim -I./riscv-isa-sim/softfloat -I./riscv-isa-sim/fesvr

ポイントとなるのは、

  • serialdevice_tabstract_device_t を継承している。
  • 必要なルーチンはコンストラクタ、load()store() のメソッド群。
  • 最後にmmio_plugin_registration_t型のラッパーで囲む。

このラッパーは何をしているかというと、クラスを定義することによって、自動的にdevice_mapにクラスの情報が追加される。

  • mmio_plugin.h
  mmio_plugin_registration_t(const std::string& name)
  {
    mmio_plugin_t plugin = {
      mmio_plugin_registration_t<T>::alloc,
      mmio_plugin_registration_t<T>::load,
      mmio_plugin_registration_t<T>::store,
      mmio_plugin_registration_t<T>::dealloc,
    };

    register_mmio_plugin(name.c_str(), &plugin);
  }

register_mmio_plugin()は何をしているのか一瞬分らなかったのだが、static mmio_plugin_map_tのマップを作ってデバイスのマップ(というか一覧)を作成し追加している。

この操作をしているのが、spike.ccの引数処理の部分。--extlibで追加したプラグインに対して、そのプラグインをどの名前、どのアドレスで使用するかを指定している。 device_parser()では最後にmmio_plugin_device_tインスタンスを作成しているのがポイント。

  • spike.cc
  parser.option(0, "device", 1, device_parser);
...
  auto const device_parser = [&plugin_devices](const char *s) {
    const std::string str(s);
    std::istringstream stream(str);

    // We are parsing a string like name,base,args.

    // Parse the name, which is simply all of the characters leading up to the
    // first comma. The validity of the plugin name will be checked later.
    std::string name;
    std::getline(stream, name, ',');
    if (name.empty()) {
      throw std::runtime_error("Plugin name is empty.");
    }

    // Parse the base address. First, get all of the characters up to the next
    // comma (or up to the end of the string if there is no comma). Then try to
    // parse that string as an integer according to the rules of strtoull. It
    // could be in decimal, hex, or octal. Fail if we were able to parse a
    // number but there were garbage characters after the valid number. We must
    // consume the entire string between the commas.
    std::string base_str;
    std::getline(stream, base_str, ',');
    if (base_str.empty()) {
      throw std::runtime_error("Device base address is empty.");
    }
    char* end;
    reg_t base = static_cast<reg_t>(strtoull(base_str.c_str(), &end, 0));
    if (end != &*base_str.cend()) {
      throw std::runtime_error("Error parsing device base address.");
    }

    // The remainder of the string is the arguments. We could use getline, but
    // that could ignore newline characters in the arguments. That should be
    // rare and discouraged, but handle it here anyway with this weird in_avail
    // technique. The arguments are optional, so if there were no arguments
    // specified we could end up with an empty string here. That's okay.
    auto avail = stream.rdbuf()->in_avail();
    std::string args(avail, '\0');
    stream.readsome(&args[0], avail);

    plugin_devices.emplace_back(base, new mmio_plugin_device_t(name, args));
  };

実行方法:

./riscv-isa-sim/spike --extlib=libserialdevice.so --device=serialdevice,1409286144,uart --isa=rv64imac --dtb ../dts/rv64imac.dtb /home/msyksphinz/work/riscv/linux/riscv64-linux/output_spike/images/fw_jump.elf
serialdevice: uart loaded