みつきんのメモ

組み込みエンジニアです。Interface誌で「My オリジナルLinuxの作り方」連載中

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

はじめに

追記しました(3/28)

題名がもうなんだかわからなくなってきたが、とりあえずCLINT編の続き。

前回、michaeljclark/riscv-probeの割り込みベクタとハンドラの部分を調査した。

実際にこの部分を動かしているサンプルであるprobe.cを実行してみたところ、割り込みの発生要因がIllegal instructionしかなかった。

自分で任意にソフトウェア割り込みを発生させて、それをトラップしてみたいと思ったので実験することにした。

割り込みベクタの処理を読み直す

前回は、trap_vectorの処理をさらっと読みすぎて重要なことを素通りしてしまった。

sxsp/lxspって何?

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

スタックポインタに書き込む命令なんだと思ったが、Google先生に聞いても何一つ情報が見つからない。

それもそのはず、自前のマクロだった。

riscv-probe/env/common/rv32/macros.sriscv-probe/env/common/rv64/macros.sにそれぞれ定義があった。 v32のmacros.sを示す。

.equ REGBYTES, 4

.macro lx a, b
lw \a, \b
.endm

.macro sx a, b
sw \a, \b
.endm

.macro lxsp a, b
lw \a, ((\b)*REGBYTES)(sp)
.endm

.macro sxsp a, b
sw \a, ((\b)*REGBYTES)(sp)
.endm

.macro .ptr a
.4byte \a
.endm

次にrv64のmacro.s

.equ REGBYTES, 8

.macro lx a, b
ld \a, \b
.endm

.macro sx a, b
sd \a, \b
.endm

.macro lxsp a, b
ld \a, ((\b)*REGBYTES)(sp)
.endm

.macro sxsp a, b
sd \a, ((\b)*REGBYTES)(sp)
.endm

.macro .ptr a
.8byte \a
.endm

要するに32/64のビット幅を吸収している。

やっている事自体は、スタックにレジスタを退避/復帰しているってことで間違いはない。

自分でソフトウェア割り込みを発生する

トラップの部分はriscv-probeをそのまま使用できるので、割り込みを自分で発生させる方法を調べた。

処理手順としては次のような感じ。

f:id:mickey_happygolucky:20190328115103p:plain

msipのLSBを1に設定すると割り込みが発生する。

この処理を追加するのはどこでも良かったのだが、とりあえずriscv-probe/env/qemu-sifive_e/crt.sに こんな感じで処理を追加した。

    .equ   RISCV_MSTATUS_MIE, 0x08
    .equ   RISCV_MIE_MSIE,    0x08
    
    .text
    .globl soft_intr
    .type soft_intr,@function
soft_intr:
    li    t0, RISCV_MSTATUS_MIE
    csrrs zero, mstatus, t0 /* mstatus.mie = 1 */
    
    li    t0, RISCV_MIE_MSIE
    csrrs zero, mie, t0     /* mie.msie = 1 */
    
    lui t0, 0x02000         /* t0 = 0x02000000(msip for hart0)*/
    li  t1, 0x1             /* t1 = 1 */
    sw  t1, (t0)            /* *(0x02000000) = 1 */
    ret

超ベタ書き。

割り込みハンドラの方で、要因をクリアしないと割り込みが入り続けるので次のようにした。

static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc)
{
    unsigned int mie;

    printf(".");

    mie = read_csr(mie); /* mie読み込み */
    mie &= ~0x8;         /* mie.msieクリア */
    write_csr(mie, mie); /* mie書き込み */
    write_csr(mepc, mepc + 4);
}

csrrcを使えば1発でクリアできるので、少し冗長だが今回は実験なのでこれでよし。

これは間違い。これでは次にmie.msieに1をセットした時点で割り込みが発生してしまう。 ただ割り込みを許可しただけで次の割り込みが発生するのはおかしい。

あと、復帰時点のPCを設定するためのwrite_csr(mepc, mepc + 4);の行も不要。これがあるとおかしくなる。 何もしなくてもちゃんと割り込み時点のpcの次の命令から実行してくれるっぽい。

最終的にはこんな感じ。

static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc)
{
    unsigned int mie;
    volatile unsigned int *msip = (volatile unsigned int *)0x2000000;

    printf(":(0x%08x):\n", *msip);

    *msip = 0;
    write_csr(mcause, 0x0);
    clear_csr(mie, 0x8);
    /*write_csr(mepc, mepc + 4);*/
}

感触としてはmcauseのクリアは不要な気がする。 次の割り込みが入った時点で上書きされるだろうし。

CLINT.msipをクリアしてやる必要があるというの正しい答えだと思われる。

実験に使ったメイン関数。

int main(int argc, char **argv)
{
    char buf[32];
    unsigned int mcause;
    printf("isa: %s\n", isa_string(buf, sizeof(buf)));
    set_trap_fn(trap_save_cause);

    mcause = read_csr(mcause);
    printf("01 mcause(%08x)\n", mcause);

    soft_intr();

    mcause = read_csr(mcause);
    printf("02 mcause(%08x)\n", mcause);
    write_csr(mie, 0x8);
    printf("done\n");

    return 0;
}

実行結果はこうなる。

isa: rv32imacu
01 mcause(00000000)
:(0x00000001):
02 mcause(00000000)
done

write_csr(mie, 0x8)の行で割り込みが発生していない。

まとめ

CLINT完全に理解してなかった。 写経だけでは不十分なこともある。