FPGA開発日記

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

freedom-u-sdkのLinuxを立ち上げながらLinuxのブートプロセスを学ぶ(4. デバイスの初期化について)

私の開発したRISC-VシミュレータはLinuxを立ち上げることができる。シミュレータのデバッグ時には相当中身を読み込んだのだが、きちんと文章化していない挙句、大昔のプロジェクトなのでもう忘れかけている。

Linuxのブートの方法から各種プロセスの取り扱いまで、思い出しながらRISC-Vシミュレータを動かしていき、ちゃんと文章化しておきたいと思った。

バイスの初期化

さて、HARTの初期化部分に入って行こう。まずはhart_init()から。

  • freedom-u-sdk/riscv-pk/machine/minit.c
static void hart_init()
{
  mstatus_init();     // CSR mstatusレジスタの初期化
  fp_init();          // 浮動小数点命令の設定
  delegate_traps();   // Delegationの設定
}

mstatusRISC-Vで最も必須となる制御レジスタで、現在のCPUの動作モードや各種割り込みレジスタなどの情報を要約している。ここではmstatusに適切な値を設定するための処理を行っている。

static void mstatus_init()
{
  // Enable FPU
  if (supports_extension('D') || supports_extension('F'))
    write_csr(mstatus, MSTATUS_FS);

  // Enable user/supervisor use of perf counters
  if (supports_extension('S'))
    write_csr(scounteren, -1);
  write_csr(mcounteren, -1);

  // Enable software interrupts
  write_csr(mie, MIP_MSIP);

  // Disable paging
  if (supports_extension('S'))
    write_csr(sptbr, 0);
}

supports_extension()は、当該CPUがどの拡張機能をサポートしているかを確認するための関数だが、具体的にはMISAレジスタをチェックする。RISC-VのMISAレジスタには、Aから順番に拡張機能の有効無効が設定してあり、例えばDというのは倍精度浮動小数点命令を意味し、

  • freedom-u-sdk/riscv-pk/machine/mtrap.h
static inline int supports_extension(char ext)
{
  return read_const_csr(misa) & (1 << (ext - 'A'));
}

Dに該当するビットが1になっていれば当該CPUは倍精度浮動小数点命令をサポートしていることを意味する。この場合、MSTATUS_FSビットを設定するようになっているが、FSビットというのはコンテキストスイッチ時に浮動小数レジスタのスピルアウとをするかしないかをチェックするための機能だ。詳細はここでは省略する。

次にスーパバイザモードの確認。スーパバイザモードが有効であれば、スーパバイザモード用のカウンタを有効化する。

さらにMIEレジスタの設定。MIP_MSIPというのはMachine Mode Interrupt Pending Gesiter for Software Interruptで、マシンモードでのソフトウェアによる割込みの発生(ECALLとか)を許可する。

次にsptbrというレジスタはスーパーバイザよりも下の権限で利用できる仮想アドレスモードを使用する際の、仮想アドレスを変換するための規定となるアドレスを格納しているレジスタだ(現在はsptbrではなくsatpという名前に変更されている)。

ここではまだページングは使用しないので、すべてのビットを0に落とすことによって仮想アドレスの使用を禁止している(特にsatpレジスタの上位ビットを落とすと、スーパバイザモードでも仮想アドレスの変換機能が無効化される)。

f:id:msyksphinz:20200422005028p:plain
f:id:msyksphinz:20200422005044p:plain

次はFPUの初期化だ。fp_init()では、FPUが定義されている場合にレジスタの初期化とFCSRレジスタの初期化が行われる。

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
}

init_fp_reg(i)はマクロとして定義されており、すこし難しいのだが、展開された命令列を見てみればたやすい。

  • freedom-u-sdk/riscv-pk/machine/fp_emulation.h
# define SET_F32_REG(insn, pos, regs, val) ({ \
  register uint32_t value asm("a0") = (val); \
  uintptr_t offset = SHIFT_RIGHT(insn, (pos)-3) & 0xf8; \
  uintptr_t tmp; \
  asm volatile ("1: auipc %0, %%pcrel_hi(put_f32_reg); add %0, %0, %2; jalr t0, %0, %%pcrel_lo(1b)" : "=&r"(tmp) : "r"(value), "r"(offset) : "t0"); })
# define init_fp_reg(i) SET_F32_REG((i) << 3, 3, 0, 0)

つまりマクロ部分では3つの命令が展開されており、

1:  auipc %0, %%pcrel_hi(put_f32_reg)
    add %0, %0, %2
    jalr t0, %0, %%pcrel_lo(1b)

もう少し展開すると、

1:  auipc tmp, %%pcrel_hi(put_f32_reg)
    add tmp, tmp, offset
    jalr t0, tmp, %%pcrel_lo(1b)

で、put_f32_regにジャンプする。ジャンプする先は更新するFPレジスタのインデックスから逆算し、インデックス \times 8バイトの場所へジャンプするように計算してある。 また引数レジスタa0valで初期化してあるのでこれで任意のFPレジスタに任意の値を設定するわけだ。put_f32_regsfreedom-u-sdk/riscv-pk/machine/fp_asm.Sに定義してある。

  • freedom-u-sdk/riscv-pk/machine/fp_asm.S
#define put_f32(which) fmv.s.x which, a0; jr t0
...
  put_f32_reg:
    put_f32(f0)
    put_f32(f1)
    put_f32(f2)
...
    put_f32(f29)
    put_f32(f30)
    put_f32(f31)

FPUの初期化は完了した。次はdelegate_traps()に進もう。

delegateというのは、通常RISC-Vの割り込みは常にマシンモードに対してはいるものを、自動的に権限の弱いスーパバイザモードやユーザモードに移譲することができる仕組みのことを言う。あまり強い権限の必要ない割り込みをマシンモードで処理しても仕方がないので、割り込み要因に応じてスーパバイザモードに切り替わり処理を行うのだが、それを自動的に行ってくれる。delegate_trapsでは、特定の割り込み要因・例外要因に対してMIDELEG, MEDELEGレジスタを設定し、当該要因が入ってくればマシンモードではなく自動的にスーパバイザモードにジャンプする。これにより割り込み要因の処理時間を削減するというのがこの機能の狙いだ。

  • freedom-u-sdk/riscv-pk/machine/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_USER_ECALL);

  write_csr(mideleg, interrupts);
  write_csr(medeleg, exceptions);
  assert(read_csr(mideleg) == interrupts);
  assert(read_csr(medeleg) == exceptions);
}

つぎは各種デバイスの探索だ。DTBを探索しながらデバイスや各種コントローラの場所を探索していく。

  // Find the power button early as well so die() works
  query_finisher(dtb);

  query_mem(dtb);
  query_harts(dtb);
  query_clint(dtb);
  query_plic(dtb);
  • query_mem(dtb) : メモリ領域を探索し、minit.c内のmem_sizeを設定します。
  • query_harts(dtb) : システム内のHARTSを探索し、fdt.c内のhart_maskhart_phandlesを初期化します。
  • query_clint(dtb) : コア内のCLINT(Core Local Interruptor)の場所を探索します。mtime, ipi, timecmpの設定を行います。
  • query_plic(dtb) : システム内のPLIC(Platform Level Interrupt Controller)を探索します。PLIC内の割り込み発生方式の設定などを行います。

次のwake_hartsは、システム全体の初期化を行っているCPUが他のCPUを呼び起こす。

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
}

IPIという場所を叩いていることが分かるが、実はこれはCLINTの設定部分、query_clintで初期化を行っている。実はquery_clint()ではhlt_tとよばれる他のHARTに関する情報を格納するためのデータ型を定義して、そこに様々な情報を格納している。

  • freedom-u-sdk/riscv-pk/machine/mtrap.h
typedef struct {
  volatile uint32_t* ipi;
  volatile int mipi_pending;

  volatile uint64_t* timecmp;

  volatile uint32_t* plic_m_thresh;
  volatile uintptr_t* plic_m_ie;
  volatile uint32_t* plic_s_thresh;
  volatile uintptr_t* plic_s_ie;
} hls_t;

この構造体の要素ipiには、各HARTに対して割込みを挿入するためのアドレスが格納される。

  • freedom-u-sdk/riscv-pk/machine/fdt.c
static void clint_done(const struct fdt_scan_node *node, void *extra)
{
  struct clint_scan *scan = (struct clint_scan *)extra;
...
    if (hart < MAX_HARTS) {
      hls_t *hls = OTHER_HLS(hart);
      hls->ipi = (void*)((uintptr_t)scan->reg + index * 4);
      hls->timecmp = (void*)((uintptr_t)scan->reg + 0x4000 + (index * 8));
    }

この計算式(void*)((uintptr_t)scan->reg + index * 4)というのが、CLINTのmsipのアドレスマップに相当する。したがって、ここを叩くことによって他のHARTを起こすことができるという訳だ。

f:id:msyksphinz:20200422005118p:plain

次はPLICの初期化だ。こちらはあまり設定することがない。今のところは流しておいても問題ない。

  plic_init();
  hart_plic_init();
static void plic_init()
{
  for (size_t i = 1; i <= plic_ndevs; i++)
    plic_priorities[i] = 1;
}

static void hart_plic_init()
{
  // clear pending interrupts
  *HLS()->ipi = 0;
  *HLS()->timecmp = -1ULL;
  write_csr(mip, 0);

  if (!plic_ndevs)
    return;

  size_t ie_words = plic_ndevs / sizeof(uintptr_t) + 1;
  for (size_t i = 0; i < ie_words; i++)
    HLS()->plic_s_ie[i] = ULONG_MAX;
  *HLS()->plic_m_thresh = 1;
  *HLS()->plic_s_thresh = 0;
}