私の開発した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の設定 }
mstatus
はRISC-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レジスタの上位ビットを落とすと、スーパバイザモードでも仮想アドレスの変換機能が無効化される)。
次は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レジスタのインデックスから逆算し、インデックス 8バイトの場所へジャンプするように計算してある。 また引数レジスタa0
はval
で初期化してあるのでこれで任意のFPレジスタに任意の値を設定するわけだ。put_f32_regs
はfreedom-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_mask
とhart_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を起こすことができるという訳だ。
次は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; }