FPGA開発日記

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

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (9. RISC-Vはデバイスアクセスをどのようにして実現するか)

UCBが開発しているRISC-VのシミュレータSpikeや、Rocket-ChipのRTLデザインは通常はシステムコールを持っていない。 つまり、当然ながらC言語printf("Hello World\n");などと書いても動作しないのだが、そこはコンパイラとフロントエンドサーバfesvr、pk(Proxy Kernel)によって肩代わりすることでこれらのシステムコールを実現している。

Proxy Kernelを使えば、外部のホストインタフェースを使って入出力が可能だが、それでは複数のデバイスがつながっている場合はどうするのだろうか? 例えば、ディスクが接続されているときなど、コマンドをどのように出力して、どのメモリアドレスにリクエストを出せばよいのか。 それらについて調査した。

f:id:msyksphinz:20180802013846p:plain
図. Spikeシミュレータの構造。システムコールやデバイスアクセスはどのようにして実現しているのか。

最新のSpikeシミュレータではディスクアクセスをサポートしていない?

ソースコードを見てみると、以下のような記述があり、+diskが使えないように見える。ちゃんと確かめていないが...

  • riscv-tools/riscv-fesvr/fesvr/htif.cc
      case HTIF_LONG_OPTIONS_OPTIND + 1:
        // [TODO] Remove once disks are supported again
        throw std::invalid_argument("--disk/+disk unsupported (use a ramdisk)");
        dynamic_devices.push_back(new disk_t(optarg));
        break;

バイスリストを使って各種デバイスにアクセスする仕組み

Spikeシミュレータのソースコードを見る前に、まずはFESVR(RISC-Vのフロントエンドサーバ)を見てみよう。 すると、フロントエンドサーバの初期化時にいくつかのデバイスを接続していることが分かる。

以下のソースで接続されているのは、Proxy Kernelで使用されるシステムコールと、あとはbcdと呼ばれるデバイスだ。

  • riscv-tools/riscv-fesvr/fesvr/htif.cc
void htif_t::register_devices()
{
  device_list.register_device(&syscall_proxy);
  device_list.register_device(&bcd);
  for (auto d : dynamic_devices)
    device_list.register_device(d);
}

ここで言うbcdは、簡単なデバイスに相当するらしい。

  • riscv-tools/riscv-fesvr/fesvr/htif.cc
bcd_t::bcd_t()
{
  register_command(0, std::bind(&bcd_t::handle_read, this, _1), "read");
  register_command(1, std::bind(&bcd_t::handle_write, this, _1), "write");
}
...
void bcd_t::tick()
{
  int ch;
  if (!pending_reads.empty() && (ch = canonical_terminal_t::read()) != -1)
  {
    pending_reads.front().respond(0x100 | ch);
    pending_reads.pop();
  }
}

ディスクは現在は接続できないのだが、実装としては以下のようになっている。

  • riscv-tools/riscv-fesvr/fesvr/htif.cc
disk_t::disk_t(const char* fn)
{
  fd = ::open(fn, O_RDWR);
  if (fd < 0)
    throw std::runtime_error("could not open " + std::string(fn));

  register_command(0, std::bind(&disk_t::handle_read, this, _1), "read");
  register_command(1, std::bind(&disk_t::handle_write, this, _1), "write");

  struct stat st;
  if (fstat(fd, &st) < 0)
    throw std::runtime_error("could not stat " + std::string(fn));

  size = st.st_size;
  id = "disk size=" + std::to_string(size);
}

つまり、ディスクデバイスを接続すると、そのデバイスに相当するイメージファイルをOpenし、read/writeできるようにするというわけだ。

void disk_t::handle_read(command_t cmd)
{
  request_t req;
  cmd.htif()->memif().read(cmd.payload(), sizeof(req), &req);

  std::vector<uint8_t> buf(req.size);
  if ((size_t)::pread(fd, &buf[0], buf.size(), req.offset) != req.size)
    throw std::runtime_error("could not read " + id + " @ " + std::to_string(req.offset));

  cmd.htif()->memif().write(req.addr, buf.size(), &buf[0]);
  cmd.respond(req.tag);
}

void disk_t::handle_write(command_t cmd)
{
  request_t req;
  cmd.htif()->memif().read(cmd.payload(), sizeof(req), &req);

  std::vector<uint8_t> buf(req.size);
  cmd.htif()->memif().read(req.addr, buf.size(), &buf[0]);

  if ((size_t)::pwrite(fd, &buf[0], buf.size(), req.offset) != req.size)
    throw std::runtime_error("could not write " + id + " @ " + std::to_string(req.offset));

  cmd.respond(req.tag);
}

そして、システムコールが呼ばれた場合は、以下のようにデバイスリストの中から1つのデバイスが選択される。 つまり、コマンドの番号によって選択されるデバイスが変わるわけだ。 例えば、ディスクをデバイスリストの2番に接続すると、tohostレジスタにコマンドを投げるときに、CMDフィールドを2に設定すればよい。

  • riscv-tools/riscv-fesvr/fesvr/device.cc
void device_list_t::handle_command(command_t cmd)
{
  devices[cmd.device()]->handle_command(cmd);
}

void device_list_t::tick()
{
  for (size_t i = 0; i < num_devices; i++)
    devices[i]->tick();
}