FPGA開発日記

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

Rocket Chipの足回りを理解する (6. デバッグモジュールによるRocket Chipの内部レジスタアクセス)

RISC-VのフラグシップモデルであるRocket Chip Generatorは、RISC-Vのほぼ全ての仕様を網羅している非常に優れているデザインだが、最初はなかなか取っつきにくい部分がある。

それはいくつかあるのだが、

  • ChiselというScala拡張の言語で記述されており、Verilogに慣れている人にとって見れば読みにくい。
  • DPI-Cが多用されており、プログラムのロードやデバッグ機構がどのように実現してあるのかが読みにくい。

例によって私も苦戦しているのだが、特にVerilatorのオプションで指定したelfファイルが、どのようにしてRocket Chipにロードされ、実行されるのかを理解するのはなかなか難しい。 現在、RISC-VのC言語の実装にprintf()を挿入しながら(やり方がダサいが...)、Rocket Chipがどのようにしてプログラムをロードして実行されているのかを解析している。

解析を進めていくにつれて、プログラムのロードや制御について理解するためにはRISC-Vのデバッグ仕様について理解することが不可欠だということが分かってきた。 すなわち、これまで正直殆どノーマークであった、デバッグ仕様について理解する必要がある。

  • RISC-V External Debug SupportVersion 0.13

https://static.dev.sifive.com/riscv-debug-spec-0.13.b4f1f43.pdf

Rocket Chipの制御機構とDPI-C

Rocket Chipの制御機構は、全体をまとめるTestDriverというモジュールに対しTestHarnessというモジュールが含まれている。 TestHarness にはRocket Chipの実体である ExampleRocketSystemと、メモリであるSRAMモジュール、さらにデバッグと制御を司る SimDTM がインスタンスされている。

このSimDTMが今回の主役なのだが、SimDTM.vというモジュールがインスタンスされており、この中身は debug_ticks()というC/C++で記述されたDPI関数が毎サイクル呼ばれている。これによりプログラムの制御や、デバッグモジュールの動作、プログラムのロード処理などが行われている。

f:id:msyksphinz:20171003015225p:plain

  • vsrc/SimDTM.v
  always @(posedge clk)
  begin
    r_reset <= reset;
    if (reset || r_reset)
    begin
      __debug_req_valid = 0;
      __debug_resp_ready = 0;
      __exit = 0;
    end
    else
    begin
      __exit = debug_tick(
        __debug_req_valid,
        __debug_req_ready,
        __debug_req_bits_addr,
...

debug_tick()の中身は実際には csrc/SimDTM.ccに記述されており、これも中身を読んでみるとC/C++で書かれた制御ルーチンと、SimDTM経由で接続されているデバッグ専用モジュールを接続しているという構成になる。

さて、ここで具体的にどのようにしてVerilatorから指定したelfファイルをRocket Chipに渡しているかということだが、まずはRISC-Vのフロントエンドサーバ Fesvrについて理解する必要がある。

riscv-fesvrはRokect Chipと外界を接続するためのインタフェースなのだが、Rocket ChipではDPI-Cインタフェースを通じてインスタンスされており、riscv-fesvr/fesvr/dtm.cc内で、 htif_t型のオブジェクトとしてインスタンスされている。 htif_tはコンストラクタによって、load_program()というそれっぽい関数が呼ばれているので、これを追いかけていくことにする。load_program()htif.cc内で定義されている。

void htif_t::load_program()
{

...
  std::map<std::string, uint64_t> symbols = load_elf(path.c_str(), &mem, &entry);
...

load_elf()elfloader.cc内で定義されているのだが、さあここからが大変だ。Rocket Chipのデバッグモジュールの仕様について理解していく必要がある。

  • riscv-fesvr/fesvr/elfloader.cc
std::map<std::string, uint64_t> load_elf(const char* fn, memif_t* memif, reg_t* entry)
{
...
  #define LOAD_ELF(ehdr_t, phdr_t, shdr_t, sym_t) do { \
    ehdr_t* eh = (ehdr_t*)buf; \
    phdr_t* ph = (phdr_t*)(buf + eh->e_phoff); \
    *entry = eh->e_entry; \
    assert(size >= eh->e_phoff + eh->e_phnum*sizeof(*ph)); \
    for (unsigned i = 0; i < eh->e_phnum; i++) { \
      if(ph[i].p_type == PT_LOAD && ph[i].p_memsz) { \
        if (ph[i].p_filesz) { \
          assert(size >= ph[i].p_offset + ph[i].p_filesz); \
          memif->write(ph[i].p_paddr, ph[i].p_filesz, (uint8_t*)buf + ph[i].p_offset); \
        } \
        zeros.resize(ph[i].p_memsz - ph[i].p_filesz); \
...

memif->write()という関数が呼ばれた。これがRocket Chipのメモリインタフェースとのやり取りをするための関数だが、これはデバッグプロトコルを用いてメモリとのやり取りが行われている。 さらに潜ってみよう。このmemif->write()memif.ccで定義されている。いくつか条件があるが、ここでは 0x8000_0000から0x19cバイトのプログラムを書き込み(addr= 0x80000000, len=0x01c9)するとする。

  • riscv-fesvr/fesvr/memif.cc
void memif_t::write(addr_t addr, size_t len, const void* bytes)
{

  size_t align = htif->chunk_align();

...
  if (len & (align-1))
  {
    size_t this_len = len & (align-1);
    size_t start = len - this_len;
    uint8_t chunk[align];

    htif->read_chunk(addr + start, align, chunk);
    memcpy(chunk, (char*)bytes + start, this_len);
    htif->write_chunk(addr + start, align, chunk);

    len -= this_len;
  }

chunk_align()はデータを転送する場合のアラインを指定するらしい。ここでは値を取得してみると8バイトであった。これはデータ転送量の0x19cのサイズと一致していないため、最後の0x198から0x1a0までのデータを先に取得してマージし、先に一番後ろのデータから書き込むという謎の動作からスタートする。

これで残りのデータ全てはアライン書き込みすることが出来るようになったため、一気にプログラムを書き込む。

  • riscv-fesvr/fesvr/memif.cc
    size_t max_chunk = htif->chunk_max_size();
    for (size_t pos = 0; pos < len; pos += max_chunk)
      htif->write_chunk(addr + pos, std::min(max_chunk, len - pos), (char*)bytes + pos);
  }

やっと本題に戻ってwrite_chunk()の中身だが、これはdtm.ccに戻ってくる。

  • riscv-fesvr/fesvr/dtm.cc
void dtm_t::write_chunk(uint64_t taddr, size_t len, const void* src)
{
  uint32_t prog[ram_words];
  uint32_t data[data_words];

  const uint8_t * curr = (const uint8_t*) src;

  halt(current_hart);

  uint64_t s0 = save_reg(S0);
  uint64_t s1 = save_reg(S1);
...

まずはsave_reg(S0), save_reg(S1)を見てみよう。これはRocket Chip内のs0, s1レジスタを取得して一時的に保存してくための命令だ。実体は、

uint64_t dtm_t::save_reg(unsigned regno)
{
  uint32_t data[xlen/(8*4)];
  uint32_t command = AC_ACCESS_REGISTER_TRANSFER | AC_AR_SIZE(xlen) | AC_AR_REGNO(regno);
  RUN_AC_OR_DIE(command, 0, 0, data, xlen / (8*4));

  uint64_t result = data[0];
  if (xlen > 32) {
    result |= ((uint64_t)data[1]) << 32;
  }
  return result;
}

AC_ACCESS_REGISTER_TRANSFERriscv-fesvr/fesvr/debug_defines.h で定義されている。

  • riscv-fesvr/fesvr/debug_defines.h
#define AC_ACCESS_REGISTER_TRANSFER_OFFSET  17
#define AC_ACCESS_REGISTER_TRANSFER_LENGTH  1
#define AC_ACCESS_REGISTER_TRANSFER         (0x1 << AC_ACCESS_REGISTER_TRANSFER_OFFSET)

ここでRISC-Vのデバッグレジスタの仕様を見てみる。RISC-Vにおいて、デバッグモジュール(DMI)へのコマンド転送は以下のプロトコルが定義されている。

f:id:msyksphinz:20171003022322p:plain

17bit目が、TRANSFERコマンド、つまりWriteコマンドを示す。さらにデータサイズ AC_AR_SIZE, AC_AR_REGNO により対象のレジスタの値を転送する。

さて、やっとデバッグモジュールを通じてデータの読み書きができるところまで来た。次は、データの書き込みについて調査していく。