私の開発したRISC-VシミュレータはLinuxを立ち上げることができる。シミュレータのデバッグ時には相当中身を読み込んだのだが、きちんと文章化していない挙句、大昔のプロジェクトなのでもう忘れかけている。
Linuxのブートの方法から各種プロセスの取り扱いまで、思い出しながらRISC-Vシミュレータを動かしていき、ちゃんと文章化しておきたいと思った。
init_first_hart()で実行される処理
init_first_hart()は先頭のHARTでしか実行されない。つまりCPU0に相当する初期化を行うためのCPUでしか実行されないルーチンだ。大きく分けて以下の処理を実行する。
- UARTの初期化
- DTBによるハードウェアデバイスの初期化
- HARTの初期化。システムレジスタの初期化など。
- デバイスの探索
- Finisher(リセットボタン)
- メモリ領域
- 他のHARTSの探索
- CLINT領域の探索
- PLICの探索
- 他のHARTSを起動させる。
- PLICの初期化
- メモリ領域の初期化
- ブートローダの実行
DTBとは何か
DTBというのはDevice Tree Blobで、Linuxなどのオペレーティングシステムが、そのチップに接続されているデバイスを認識するためのファイル。dtbは、テキストファイルで表現されるdts(Device Tree Source)からコンパイルすることで得られるバイナリ形式のファイルである。このコンパイル処理はdtc : Device Tree Compilerによって行われる。
このdtbはLinuxを立ち上げるシステムに依存しているのだが、このinit_first_hart(uintptr_t hartid, uintptr_t dtb)の第2引数に設定されるようになっている。
do_reset: li x1, 0 li x2, 0 ... li x9, 0 // save a0 and a1; arguments from previous boot loader stage: // li x10, 0 // li x11, 0 li x12, 0 li x13, 0 ... # Boot on the first hart beqz a3, init_first_hart
do_resetを実行するより前に、x10に自分のhartid、x11にdtbの場所を格納しておく必要がある。Swimmer-RISCVでは、一番最初のブートROMにこれを設定する必要があります。
0:M:MBar:00001000:00000297:auipc x05,0x00000 :r05<=0000000000001000 1:M:MBar:00001004:02028593:addi x11,x05,0x020 :r11<=0000000000001020 2:M:MBar:00001008:f1402573:csrrs x10,0xf14,x00 :r10<=0000000000000000
HARTIDは0、dtbの場所は0x00001020に格納されていることが分かる。Swimmer-RISCVでは、以下のようなdtsをDevice Tree Compilerでコンパイルして0x00001020に格納している。
/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";
};
};
このdtsには、大きく分けて4つのデバイスが宣言されていることが分かる。
cpus:CPUそのものmemory:外部メモリsoc:外部デバイス。CLINT(Core Local Interrupterが格納されている)htif:HART Interface。外部に定義されてデバイスとの通信を行うために使用される。
UARTの初期化
init_first_hart()では2種類のUARTデバイスを探索して初期化する。
query_uart()query_uart16550()
の2つがある。1つ目のquery_uart()の中身をのぞいてみると、
freedom-u-sdk/riscv-pk/machine/uart.c
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); }
関数ポインタばかりで訳が分からないが、
uart_open(): デバイスをオープンするために必要な初期化関数。uart_prop(): dtbの中に該当するデバイスが存在するかどうかを判定する関数。uart_done(): UARTを発見した場合にデバイスをオープンするための関数。
となっている。理解するミソはuart_prop()で、dtb内の情報を探索してパタンとヒットすればデバイスが見つかったということである。
freedom-u-sdk/riscv-pk/machine/uart.c
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); } }
しかしここではquery_uart()もquery_uart16550()で探索するデバイスは発見できない。query_uart()ではcompatible = sifive,uart0デバイスを、query_uart16550()ではcompatible = ns16550aを探索しているのだがどちらも該当しない。
ヒットするのは、最後のquery_htif()だ。query_htifではcompatible = ucb,htif0を探索するので、これはdtbに相当する。
freedom-u-sdk/riscv-pk/machine/htif.c
static void htif_prop(const struct fdt_scan_prop *prop, void *extra) { struct htif_scan *scan = (struct htif_scan *)extra; if (!strcmp(prop->name, "compatible") && !strcmp((const char*)prop->value, "ucb,htif0")) { scan->compat = 1; } }
dtb内の以下にマッチする。
htif {
compatible = "ucb,htif0";
};
これでデバイスhtifを発見した。init_first_hart()内ではデバイスを発見した後にprintmを実行して画面表示を行うとするのだが、これをどのように実現するのか次に解説していく。