FPGA開発日記

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

RISC-V SpikeシミュレータでC/C++のprintfを実現する仕組み (3. RISC-Vのブートシーケンス)

Hello Worldのプログラムを動かしながら、RISC-V Spikeシミュレータのログを追っていき、RISC-Vのブートシーケンスを追っていく。

f:id:msyksphinz:20180613232411p:plain

riscv-isa-sim/riscv/sim.cc 内のリセットベクタが最初に動作する。

  • riscv-isa-sim/riscv/sim.cc
void sim_t::make_dtb()
{
...
  uint32_t reset_vec[reset_vec_size] = {
    0x297,                                      // auipc  t0,0x0
    0x28593 + (reset_vec_size * 4 << 20),       // addi   a1, t0, &dtb
    0xf1402573,                                 // csrr   a0, mhartid
    get_core(0)->get_xlen() == 32 ?
      0x0182a283u :                             // lw     t0,24(t0)
      0x0182b283u,                              // ld     t0,24(t0)
    0x28067,                                    // jr     t0
    0,
    (uint32_t) (start_pc & 0xffffffff),
    (uint32_t) (start_pc >> 32)
  };

その直後にコンパイルしたDTB(Device Tree Blob)が挿入される。

  • riscv-isa-sim/riscv/sim.cc
void sim_t::make_dtb()
{
...
  dts = s.str();
  std::string dtb = dts_compile(dts);

  rom.insert(rom.end(), dtb.begin(), dtb.end());
  const int align = 0x1000;
  rom.resize((rom.size() + align - 1) / align * align);

ここから先はpk(Proxy Kernel)のエントリポイントに移動する。エントリポイントではまずdo_resetにジャンプする。

  • ${RISCV}/riscv64-unknown-elf/bin/pk.dmp
0000000080000000 <_ftext>:
    80000000:   1e80006f            j   800001e8 <do_reset>
  • ${RISCV}/riscv64-unknown-elf/bin/pk.dmp
00000000800001e8 <do_reset>:
    800001e8:   00000093            li  ra,0
    800001ec:   00000113            li  sp,0
    800001f0:   00000193            li  gp,0
    800001f4:   00000213            li  tp,0
...
    800002a8:   00070463            beqz    a4,800002b0 <do_reset+0xc8>
    800002ac:   6f60206f            j   800029a2 <init_first_hart>
    800002b0:   00800613            li  a2,8
    800002b4:   30461073            csrw    mie,a2

init_first_hart()にジャンプする。init_first_hart()riscv-pk/machine/minit.cに定義されている。

void init_first_hart(uintptr_t hartid, uintptr_t dtb)
{
  // Confirm console as early as possible
  query_uart(dtb);
  query_htif(dtb);

  hart_init();
  hls_init(0); // this might get called again from parse_config_string
...

init_first_hart()での仕事

init_first_hart()は以下の関数が並んでいる。

  • query_uart(dtb)

    • fdt_scan()を呼び出して設定する。

    • そもそもFDT(Flatten Device Tree)とは。

      • Device Treeのバイナリ表現がFDT。

      • fdt_cb構造体の表現

      • riscv-pk/machine/fdt.h

      struct fdt_cb {
        void (*open)(const struct fdt_scan_node *node, void *extra);
        void (*prop)(const struct fdt_scan_prop *prop, void *extra);
        void (*done)(const struct fdt_scan_node *node, void *extra); // last property was seen
        int  (*close)(const struct fdt_scan_node *node, void *extra); // -1 => delete the node + children
        void *extra;
      };
    void query_uart(uintptr_t fdt)
    {
      struct fdt_cb cb;
      struct uart_scan scan;
    
      memset(&cb, 0, sizeof(cb));
      cb.open = uart_open;
      cb.prop = uart_prop;
      cb.done = uart_done;
      cb.extra = &scan;
    
      fdt_scan(fdt, &cb);
    }
  • query_htif(dtb)

    static void htif_done(const struct fdt_scan_node *node, void *extra)
    {
      struct htif_scan *scan = (struct htif_scan *)extra;
      if (!scan->compat) return;
    
      htif = 1;
    }
  • hart_init()

    • hartの初期化を行う。具体的には以下の通り。

      • minit.c
    static void hart_init()
    {
      mstatus_init();
      fp_init();
      delegate_traps();
    }
  • fp_init():minit.c
      static void fp_init()
      {
        if (!supports_extension('D') && !supports_extension('F'))
          return;
      
        assert(read_csr(mstatus) & MSTATUS_FS);
      
      #ifdef __riscv_flen
        for (int i = 0; i < 32; i++)
          init_fp_reg(i);
        write_csr(fcsr, 0);
      #else
        uintptr_t fd_mask = (1 << ('F' - 'A')) | (1 << ('D' - 'A'));
        clear_csr(misa, fd_mask);
        assert(!(read_csr(misa) & fd_mask));
      #endif
      }
  • delegate_traps():minit.c
      // send S-mode interrupts and most exceptions straight to S-mode
      static void delegate_traps()
      {
        if (!supports_extension('S'))
          return;
      
        uintptr_t interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP;
        uintptr_t exceptions =
          (1U << CAUSE_MISALIGNED_FETCH) |
          (1U << CAUSE_FETCH_PAGE_FAULT) |
          (1U << CAUSE_BREAKPOINT) |
          (1U << CAUSE_LOAD_PAGE_FAULT) |
          (1U << CAUSE_STORE_PAGE_FAULT) |
          (1U << CAUSE_BREAKPOINT) |
          (1U << CAUSE_USER_ECALL);
      
        write_csr(mideleg, interrupts);
        write_csr(medeleg, exceptions);
        assert(read_csr(mideleg) == interrupts);
        assert(read_csr(medeleg) == exceptions);
      }
  • hls_init(0); // this might get called again from parse_config_string

    • riscv-pk/machine/minit.c
    hls_t* hls_init(uintptr_t id)
    {
      hls_t* hls = OTHER_HLS(id);
      memset(hls, 0, sizeof(*hls));
      return hls;
    }
    #define OTHER_HLS(id) ((hls_t*)((void*)HLS() + RISCV_PGSIZE * ((id) - read_const_csr(mhartid))))
  • query_finisher(dtb)

    • finisherのアドレスを記録しておくらしい。finisherはPowerOffボタンのようなものなのか?

    • riscv-pk/machine/finisher.c

    static void finisher_done(const struct fdt_scan_node *node, void *extra)
    {
      struct finisher_scan *scan = (struct finisher_scan *)extra;
      if (!scan->compat || !scan->reg || finisher) return;
      finisher = (uint32_t*)(uintptr_t)scan->reg;
    }
  • query_mem(dtb)

    • メモリの構成を記述している。

    • riscv-pk/machine/fdt.c

    static void mem_prop(const struct fdt_scan_prop *prop, void *extra)
    {
      struct mem_scan *scan = (struct mem_scan *)extra;
      if (!strcmp(prop->name, "device_type") && !strcmp((const char*)prop->value, "memory")) {
        scan->memory = 1;
      } else if (!strcmp(prop->name, "reg")) {
        scan->reg_value = prop->value;
        scan->reg_len = prop->len;
      }
    }
  • riscv-pk/machine/fdt.c 別に何かが設定された様子も無いけど、どうなっているんだろう?っていか、下記のSpikeのDTSだと0x0-0x80000000までを2回設定しているんだけど、これはいかに?
    static void mem_done(const struct fdt_scan_node *node, void *extra)
    {
      struct mem_scan *scan = (struct mem_scan *)extra;
      const uint32_t *value = scan->reg_value;
      const uint32_t *end = value + scan->reg_len/4;
      uintptr_t self = (uintptr_t)mem_done;
    
      if (!scan->memory) return;
      assert (scan->reg_value && scan->reg_len % 4 == 0);
    
      while (end - value > 0) {
        uint64_t base, size;
        value = fdt_get_address(node->parent, value, &base);
        value = fdt_get_size   (node->parent, value, &size);
        if (base <= self && self <= base + size) { mem_size = size; }
      }
      assert (end == value);
    }
  • query_harts(dtb) CPUのコンフィグレーションについて設定を行っている。"device_type=cpu"の部分のDTSを読み取っている。
    • riscv-pk/machine/fdt.c
  static void hart_prop(const struct fdt_scan_prop *prop, void *extra)
  {
  struct hart_scan *scan = (struct hart_scan *)extra;
  if (!strcmp(prop->name, "device_type") && !strcmp((const char*)prop->value, "cpu")) {
    assert (!scan->cpu);
    scan->cpu = prop->node;
  } else if (!strcmp(prop->name, "interrupt-controller")) {
    assert (!scan->controller);
    scan->controller = prop->node;
  } else if (!strcmp(prop->name, "#interrupt-cells")) {
    scan->cells = bswap(prop->value[0]);
  } else if (!strcmp(prop->name, "phandle")) {
    scan->phandle = bswap(prop->value[0]);
  } else if (!strcmp(prop->name, "reg")) {
    uint64_t reg;
    fdt_get_address(prop->node->parent, prop->value, &reg);
    scan->hart = reg;
  }
  }
static void hart_done(const struct fdt_scan_node *node, void *extra)
{
  struct hart_scan *scan = (struct hart_scan *)extra;

  if (scan->cpu == node) {
    assert (scan->hart >= 0);
  }

  if (scan->controller == node && scan->cpu) {
    assert (scan->phandle > 0);
    assert (scan->cells == 1);

    if (scan->hart < MAX_HARTS) {
      hart_phandles[scan->hart] = scan->phandle;
      hart_mask |= 1 << scan->hart;
      hls_init(scan->hart);
    }
  }
}
  • query_clint(dtb)

  • query_plic(dtb)

  • wake_harts() CPUを起動する。はずなのだが、HLSというのは何なのか良く分からない。

  static void wake_harts()
  {
  for (int hart = 0; hart < MAX_HARTS; ++hart)
    if ((((~disabled_hart_mask & hart_mask) >> hart) & 1))
      *OTHER_HLS(hart)->ipi = 1; // wakeup the hart
  }
  • plic_init()

  • hart_plic_init()

  • //prci_test()

  • memory_init() メモリの初期化なのだが、実際にはメモリサイズの設定をしているように見える。

    • riscv-pk/machine/minit.c
  static void memory_init()
  {
  mem_size = mem_size / MEGAPAGE_SIZE * MEGAPAGE_SIZE;
  }
  • boot_loader(dtb) boot_loader()riscv-pk/pk/pk.cに定義されている。riscv-pk/bbl/bbl.cにも定義されているがこれは別物らしい。オブジェクトをダンプしてみると中身が異なっていた。

    • riscv-pk/pk/pk.c
void boot_loader(uintptr_t dtb)
{
  extern char trap_entry;
  write_csr(stvec, &trap_entry);
  write_csr(sscratch, 0);
  write_csr(sie, 0);
  set_csr(sstatus, SSTATUS_SUM);
  
  file_init();
  enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0);
}
  • file_init() stdin, stdout, stderr を作成する。files_tという型名でグローバル領域に領域が確保されている。file_tの型は以下の通り。
    • riscv-pk/pk/file.h
typedef struct file
{
  int kfd; // file descriptor on the host side of the HTIF
  uint32_t refcnt;
} file_t;
  • riscv-pk/pk/file.c : file_get_free()参照カウントがまだ0になっている構造体を取得する。dupはインクリメントの回数を増やす?
static file_t* file_get_free()
{
  for (file_t* f = files; f < files + MAX_FILES; f++)
    if (atomic_read(&f->refcnt) == 0 && atomic_cas(&f->refcnt, 0, 2) == 0)
      return f;
  return NULL;
}
int file_dup(file_t* f)
{
  for (int i = 0; i < MAX_FDS; i++)
  {
    if (atomic_cas(&fds[i], 0, f) == 0)
    {
      file_incref(f);
      return i;
    }
  }
  return -1;
}
  • enter_supervisor_mode()ではプログラムをロードして実行するのだが、mepcに関数の先頭アドレスをロードして、mretで実行する。 enter_supervisor_mode(rest_of_boot_loader, pk_vm_init(), 0) としており、rest_of_boot_loader()の内容についてみていく。
    • riscv-pk/pk/pk.c
    static void rest_of_boot_loader(uintptr_t kstack_top)
    {
      arg_buf args;
      size_t argc = parse_args(&args);
      if (!argc)
    panic("tell me what ELF to load!");
  // load program named by argv[0]
  long phdrs[128];
  current.phdr = (uintptr_t)phdrs;
  current.phdr_size = sizeof(phdrs);
  load_elf(args.argv[0], &current);

  run_loaded_program(argc, args.argv, kstack_top);
}

fdt_scan()の実装

Flatten Device Tree の構造しかParseしない。

まずはFTDのヘッダを見ていくと、構造体としては以下のように定義されている。この辺りは、以下のサイトが非常に詳細に解説しており、役に立った。

http://masahir0y.blogspot.com/2014/05/device-tree.html

  • riscv-pk/machine/fdt.h
  struct fdt_header {
    uint32_t magic;
    uint32_t totalsize;
    uint32_t off_dt_struct;
    uint32_t off_dt_strings;
    uint32_t off_mem_rsvmap;
    uint32_t version;
    uint32_t last_comp_version; /* <= 17 */
    uint32_t boot_cpuid_phys;
    uint32_t size_dt_strings;
    uint32_t size_dt_struct;
  };
  • riscv-pk/machine/fdt.c
void fdt_scan(uintptr_t fdt, const struct fdt_cb *cb)
{
  struct fdt_header *header = (struct fdt_header *)fdt;

  // Only process FDT that we understand
  if (bswap(header->magic) != FDT_MAGIC ||
      bswap(header->last_comp_version) > FDT_VERSION) return;
  const char *strings = (const char *)(fdt + bswap(header->off_dt_strings));
  uint32_t *lex = (uint32_t *)(fdt + bswap(header->off_dt_struct));

  fdt_scan_helper(lex, strings, 0, cb);
}

そこから先はFDTのバイナリを読み取ってひたすらfdt_scan_helper()を実行する。

lexにはそのDevice Treeの構造を示す先頭のポインタが入り、stringにはDevice Treeの内容を示す文字列が入る。fdt_scan_helper()はデータ構造の役割を定義してあり、

  • riscv-pk/machine/fdt.h
#define FDT_BEGIN_NODE  1
#define FDT_END_NODE   2
#define FDT_PROP   3
#define FDT_NOP        4
#define FDT_END        9

となっている。たとえば、FDT_BEGIN_NODEの場合には、やたらとopen()close()をしているけどこれは何だろう。

  • riscv/pk/machine/fdt.c
static uint32_t *fdt_scan_helper(
  uint32_t *lex,
  const char *strings,
  struct fdt_scan_node *node,
  const struct fdt_cb *cb)
{
...
      case FDT_BEGIN_NODE: {
        uint32_t *lex_next;
        if (!last && node && cb->done) cb->done(node, cb->extra);
        last = 1;
        child.name = (const char *)(lex+1);
        if (cb->open) cb->open(&child, cb->extra);
        lex_next = fdt_scan_helper(
          lex + 2 + strlen(child.name)/4,
          strings, &child, cb);
        if (cb->close && cb->close(&child, cb->extra) == -1)
          while (lex != lex_next) *lex++ = bswap(FDT_NOP);
        lex = lex_next;
        break;
      }
...
}

uartの場合、それぞれ、cb->open()cb->prop()cb->done()で使用されている関数は以下となる。

  • riscv-pk/machine/uart.c
static void uart_open(const struct fdt_scan_node *node, void *extra)
{
  struct uart_scan *scan = (struct uart_scan *)extra;
  memset(scan, 0, sizeof(*scan));
}
static void uart_prop(const struct fdt_scan_prop *prop, void *extra)
{
  struct uart_scan *scan = (struct uart_scan *)extra;
  if (!strcmp(prop->name, "compatible") && !strcmp((const char*)prop->value, "sifive,uart0")) {
    scan->compat = 1;
  } else if (!strcmp(prop->name, "reg")) {
    fdt_get_address(prop->node->parent, prop->value, &scan->reg);
  }
}
static void uart_done(const struct fdt_scan_node *node, void *extra)
{
  struct uart_scan *scan = (struct uart_scan *)extra;
  if (!scan->compat || !scan->reg || uart) return;

  // Enable Rx/Tx channels
  uart = (void*)(uintptr_t)scan->reg;
  uart[UART_REG_TXCTRL] = UART_TXEN;
  uart[UART_REG_RXCTRL] = UART_RXEN;
}

prop()では、Device Tree内にUARTを示す記述が存在するかどうかをチェックしているようだ。最後にuart[...]で何かの設定をしている。uartグローバル変数として定義されている。

  • riscv-pk/machine/uart.c
  volatile uint32_t* uart;

compatibleの意味は良く分からない。regが指定してあると、割り当てられているアドレスを探索するようだ。

  • riscv-pk/machine/fdt.c
const uint32_t *fdt_get_address(const struct fdt_scan_node *node, const uint32_t *value, uint64_t *result)
{
  *result = 0;
  for (int cells = node->address_cells; cells > 0; --cells)
    *result = (*result << 32) + bswap(*value++);
  return value;
}

おまけ Proxy Kernelのグローバル変数の配置領域

  • pk.dmp
  000000008000c000 <fds>:
  000000008000c400 <magic_mem.2181>:
  000000008000d000 <stacks>:
  0000000080015000 <hart_phandles>:
  0000000080015020 <lock.2182>:
  0000000080015028 <free_pages>:
  0000000080015030 <next_free_page>:
  0000000080015038 <first_free_page>:
  0000000080015040 <vmrs>:
  0000000080015048 <vm_lock>:
  0000000080015050 <htif_lock>:
  0000000080015058 <disabled_hart_mask>:
  0000000080015060 <current>:
  00000000800150d0 <first_free_paddr>:
  00000000800150d8 <plic_ndevs>:
  00000000800150e0 <plic_priorities>:
  00000000800150e8 <mem_size>:
  00000000800150f0 <mtime>:
  00000000800150f8 <root_page_table>:
  0000000080015100 <htif>:
  0000000080015108 <htif_console_buf>:
  0000000080015110 <uart>:
  0000000080015118 <finisher>:
  0000000080015120 <hart_mask>:

おまけ Spikeが生成するDevice Tree Source

$ spike --dump-dts hoge
/dts-v1/;

/ {
  #address-cells = <2>;
  #size-cells = <2>;
  compatible = "ucbbar,spike-bare-dev";
  model = "ucbbar,spike-bare";
  cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    timebase-frequency = <10000000>;
    CPU0: cpu@0 {
      device_type = "cpu";
      reg = <0>;
      status = "okay";
      compatible = "riscv";
      riscv,isa = "rv64imafdc";
      mmu-type = "riscv,sv48";
      clock-frequency = <1000000000>;
      CPU0_intc: interrupt-controller {
        #interrupt-cells = <1>;
        interrupt-controller;
        compatible = "riscv,cpu-intc";
      };
    };
  };
  memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x80000000>;
  };
  soc {
    #address-cells = <2>;
    #size-cells = <2>;
    compatible = "ucbbar,spike-bare-soc", "simple-bus";
    ranges;
    clint@2000000 {
      compatible = "riscv,clint0";
      interrupts-extended = <&CPU0_intc 3 &CPU0_intc 7 >;
      reg = <0x0 0x2000000 0x0 0xc0000>;
    };
  };
  htif {
    compatible = "ucb,htif0";
  };
};