はじめに
RISC-VというかFE310の割り込み周りについて勉強する。
取っ掛かりとしてはここが分かりやすかった。
割り込みの種類
RISC-V ISA
ではグローバル割り込み(global interrupt)
とローカル割り込み(local interrupt)
が定義されている。
HART(HARdware Thread)とか割り込みソースとか色々言葉が出てきてややこしい。
HARTは複数いる可能性がある。HART毎にローカル割り込みソースがあって、それらはグローバル割り込みを介さない。
グローバル割り込みソースはPLIC(Platform-Level Interrupt Controller)が処理して、これは主にペリフェラルの割り込みを処理する。
HARTはローカル割り込みソースを複数持てるが、FE310ではローカル割り込みソースはCLINT(Coreplex Local INTerrupts)しかいない。
FE310ではCLINTは次の3つしか処理しない。
- ソフトウェア割り込み(SI)
- タイマー割り込み(TI)
- 外部割り込み(EI)
HARTもHART0しか見当たらない。
グローバル割り込みソースであるところのPLICで処理された割り込みはCLINTの外部割り込み(EI)に投げられてくる。
こんな理解。
でも、SiFive FE310-G000 Manual v2p3では「Figure 9.1: E31 Interrupt Architecture Block Diagram.」となっていた。
まぁ、きっとそういうことなんだろう。
割り込みベクタ
割り込みが発生するとMTVEC(Machine Trap VECtor)に登録されたアドレスに処理が飛ぶ。
割り込みベクタにはシングルモードとマルチモードがある。 マルチモードの場合はMTVECに登録されたアドレスはベクターテーブルの先頭アドレスとなり、要因によってオフセットされたアドレスに処理が飛ぶことになる。
参考になる実装を見てみる
@LDScellさんに教えてもらったmichaeljclark/riscv-probeを見てみることにする。
割り込みベクタの初期化
env/qemu-sifive_e/crt.s
.include "crtm.s"
本体はenv/common/crtm.s
MTVECを初期化している部分はここ
# # start of trap handler # .section .text.init,"ax",@progbits .globl _start _start: # setup default trap vector la t0, trap_vector csrw mtvec, t0 # set up stack pointer based on hartid csrr t0, mhartid slli t0, t0, STACK_SHIFT la sp, stacks + STACK_SIZE add sp, sp, t0 # park all harts excpet hart 0 csrr a0, mhartid bnez a0, park # jump to libfemto_start_main j libfemto_start_main # sleeping harts mtvec calls trap_fn upon receiving IPI park: wfi j park
HARTごとにスタック割り当ててるが、FE310ではHART0しか無い。
割り込みベクタ
MTVECにtrap_vector
を設定している。
trap_vector: # Save registers. addi sp, sp, -CONTEXT_SIZE sxsp ra, 0 sxsp a0, 1 sxsp a1, 2 sxsp a2, 3 sxsp a3, 4 sxsp a4, 5 sxsp a5, 6 sxsp a6, 7 sxsp a7, 8 sxsp t0, 9 sxsp t1, 10 sxsp t2, 11 sxsp t3, 12 sxsp t4, 13 sxsp t5, 14 sxsp t6, 15 # Invoke the handler. mv a0, sp csrr a1, mcause csrr a2, mepc jal trap_handler # Restore registers. lxsp ra, 0 lxsp a0, 1 lxsp a1, 2 lxsp a2, 3 lxsp a3, 4 lxsp a4, 5 lxsp a5, 6 lxsp a6, 7 lxsp a7, 8 lxsp t0, 9 lxsp t1, 10 lxsp t2, 11 lxsp t3, 12 lxsp t4, 13 lxsp t5, 14 lxsp t6, 15 addi sp, sp, CONTEXT_SIZE # Return mret
次のことを行っている。
trap_handlerの呼び出し
trap_handlerの呼び出しはここ。
# Invoke the handler. mv a0, sp csrr a1, mcause csrr a2, mepc jal trap_handler
C言語だと次のようなイメージ。
trap_handler(sp, mcause, mepc);
spはスタックポインタ、mepcとmcauseはCSRのレジスタ
レジスタ | Description |
---|---|
mepc | Machine exception program counter. |
mcause | Machine trap cause. |
mcauseは割り込み要因。 mepcは例外時のプログラムカウンタ。
exception? は割り込み発生時のPCという理解で良いのだろうか。
The RISC-V Instruction Set Manualによると、
When a trap is taken into M-mode, mepc is written with the virtual address of the instruction that encountered the exception.
なるほど。わからん。
SiFive FE310-G000 Manual v2p3の「9.2 Interrupt Entry and Exit」にわかりやすい説明があった。
The current pc is copied into the mepc register, and then pc is set to the value of mtvec. In the case where vectored interrupts are enabled, pc is set to mtvec.BASE + 4×exception code.
やはりその理解で良さそうだ。
プログラムへの復帰
割り込み発生前のプログラムに復帰するのはmret
らしい。
# Return mret
mretを実行すると次のことが行われる。
- The privilege mode is set to the value encoded in mstatus.MPP.
- The value of mstatus.MPIE is copied into mstatus.MIE.
- The pc is set to the value of mepc.
この時点でmepc
に設定されているアドレスに復帰するということか。
割り込みハンドラ
trap_handlerの定義を探す。./libfemto/arch/riscv/trap.c
にあった。
...(snip)... static trap_fn tfn = 0; ...(snip)... trap_fn get_trap_fn() { return tfn; } void set_trap_fn(trap_fn fn) { tfn = fn; } void trap_handler(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc) { if (tfn) { tfn(regs, mcause, mepc); } else { die("machine mode: unhandlable trap %d @ %p", mcause, mepc); } }
static変数としてユーザーが定義するハンドラのポインタとなるtfn
を定義。
set_trap_fn()
で設定。
trap_handler
が割り込みベクタから直接呼び出される。trap_handler
ではtfnに設定されている関数を呼び出す。
このようになるイメージ。
使用例
割り込みの使用例はexamples/probe/probe.c
...(snip)... static void probe_all_csrs() { int *csrenum = csr_enum_array(); const char **csrnames = csr_name_array(); const char* ws = " "; set_trap_fn(trap_save_cause); //ここでハンドラを設定 while (*csrenum != csr_none) { save_mcause = MCAUSE_UNSET; long value = read_csr_enum(*csrenum); const char* csrname = csrnames[*csrenum]; if (save_mcause != MCAUSE_UNSET) { int async = save_mcause < 0; int cause = save_mcause & (((uintptr_t)-1) >> async); printf("csr: %s%s %s cause=%ld mtval=0x%lx\n", csrname, ws + strlen(csrname), cause < 16 ? (async ? riscv_intr_names : riscv_excp_names)[cause] : "(unknown)", save_mcause, read_csr_enum(csr_mtval)); } else { printf("csr: %s%s 0x%lx\n", csrname, ws + strlen(csrname), value); } csrenum++; } } ...(snip)...
設定されたハンドラはtrap_save_cause()
static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc) { save_mcause = mcause; write_csr(mepc, mepc + 4); }
#define write_csr(reg, val) ({ \ asm volatile ("csrw " #reg ", %0" :: "rK"(val)); })
write_csr()
の第1引数は#reg
によって文字列化するのでレジスタ名と解釈される。
write_csr(mepc, mepc + 4);
はmepcにmepc+4
を書き込むということになる。
割り込み前の位置の1つ次の命令のアドレス(mepc+4)に復帰するようにmepcを書き換えている。
まとめ
CLINT完全に理解した