みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

RISC-Vのベアメタル入門(自分用) 割り込み(CLINT)編

はじめに

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つしか処理しない。

  1. ソフトウェア割り込み(SI)
  2. タイマー割り込み(TI)
  3. 外部割り込み(EI)

HARTもHART0しか見当たらない。

グローバル割り込みソースであるところのPLICで処理された割り込みはCLINTの外部割り込み(EI)に投げられてくる。

こんな理解。

f:id:mickey_happygolucky:20190315164120p:plain

でも、SiFive FE310-G000 Manual v2p3では「Figure 9.1: E31 Interrupt Architecture Block Diagram.」となっていた。

f:id:mickey_happygolucky:20190315165457p:plain

まぁ、きっとそういうことなんだろう。

割り込みベクタ

割り込みが発生すると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

次のことを行っている。

  1. レジスタの退避
  2. trap_handlerの呼び出し
  3. レジスタの復帰
  4. プログラムへの復帰

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に設定されている関数を呼び出す。

f:id:mickey_happygolucky:20190315164046p:plain

このようになるイメージ。

使用例

割り込みの使用例は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完全に理解した

参考資料