[tech] 割込みと WFI 命令を使った sleep の実装 | Wed, Feb 26. 2014 - 氾濫原 で WFI 命令があるのでそれ使えばよさそうみたいなことを書いたけど、Which architectures support the WFI instruction? を読んでいたら、Raspberry Pi が WFI 命令をサポートしていないことに気付いてしまった……

Raspberry Pi は ARMv6K というアーキテクチャの ARM1176JZ-F というプロセッサらしい。無印の ARMv6 は WFI 命令をサポートしない。ただ、ARM1176JZ-F は別の方法で使うことができる。

いろいろ書いてあってややこしいが、重要なのはここ

ARMv6K and ARMv6T2 include the WFI instruction, meaning that these processors do not cause an undefined instruction exception when the WFI instruction is executed. However, the ARM1136J(F)-S rev 1 and ARM1176JZ(F)-S (architecture ARMv6K), as well as the ARM1156T2(F)-S (architecture ARMv6T2) treat the WFI as a NOP, and implement the CP15 method for entering "wait for interrupt" mode.

なので、WFI 命令は NOP として扱われてた…… 気を使って NOP でも普通に動くコードを書いた結果気付きにくいバグを作っていた。

mov r1, #0;
mcr p15, #0, r1, c7, c0, #4;

とすれば WFI 相当のことができるみたい。コプロセッサってなんだよって感じだけど、書いてある通り書いたらエラーはでなかった。

mcr 命令は ARM レジスタからコプロセッサへデータを転送する命令らしい。上の mcr 命令の場合

  • C15 (p15) コプロセッサの
  • レジスタ7 (c7) = レジスタ 7: キャッシュ管理機能 に対し
  • 割込み待ち (c0, #4 ) を転送する

転送する値はなんでもいい?のかな。r1 に転送する値を入れるけど 0 にしてる。mcr の第2引数もコプロセッサのオペコードっっぽいけどよくわからない。

  1. トップ
  2. tech
  3. 続・割込みと WFI 命令を使った sleep の実装

この GCC (ビルド済み) を使えば fpu がどうとかも何も問題なくうまくいく。

ただし、gdb の simulator がついてないのが不便…… だけど、むしろ gdb でやるより、qemu とかでエミュレーションしたほうがよさそう…… 環境つくるのめんどいけど

  1. トップ
  2. tech
  3. arm-none-eabi クロスコンパイル環境

Raspberry Pi で bare metal をやっているこのコードを読んで理解したいと思います。README に殆ど書いてありますが、ちょっとよくわからないところがあったのでさらに詳しくしてみます。

前提として、ld スクリプト の通り、このプログラムの冒頭は 0x8000 からはじまっています。

割込みハンドラの実行

まず _start の最初には割込みハンドラの定義が書いてあります。

_start:
    ldr pc,reset_handler
    ldr pc,undefined_handler
    ldr pc,swi_handler
    ldr pc,prefetch_handler
    ldr pc,data_handler
    ldr pc,unused_handler
    ldr pc,irq_handler
    ldr pc,fiq_handler

reset_handler:      .word reset
undefined_handler:  .word hang
swi_handler:        .word hang
prefetch_handler:   .word hang
data_handler:       .word hang
unused_handler:     .word hang
irq_handler:        .word irq
fiq_handler:        .word fiq

これは、README によるとちょっとしたハックになっていて、若干トリッキーな動きをします。

まず最初に _start が実行されはじめると、最初にあるのが ldr pc,reset_handler なので、すぐに reset: に実行が移り、後続のコードは実行されません。

そして reset: の冒頭では以下のようになっています (コメントはこちらでつけたものです)

reset:
    /* Set interrupt handler to radical address from 0x8000
     */
    mov r0,#0x8000
    mov r1,#0x0000
    /* copy machine code 32bytes bytes at once */
    ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9} /* load machine code from 0x8000 */
    stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9} /* store code to 0x0000 */
    /* more 32 bytes */
    ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
    stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}

割込みハンドラは本来、0x0000 から初まるアドレスに指定通り配置する必要があります (すなわり、割込みが入ると、決め打ちのアドレスが実行される)。これは ARM のアーキテクチャマニュアルに書いてあるマジックナンバーです。なので、0x0000 へ、割込みハンドラをコピーする必要があり、それがこの部分になっています。

ldmia は LDM 命令 + IA (インクリメントアフター) という命令で、指定されたメモリアドレス (ここでは r0!) から、指定したレジスタリストに値をロードします。r0 はロードされるごとにインクリメントされます。

そして stmia は STM 命令 + IA という命令で、指定したメモリアドレス (ここでは r1!) に、指定したレジスタリストを書きこみます。ここでは一括して1度にレジスタ8個に対し処理が行われているので、8 * 4bytes = 32bytes がコピーされます。1セット目の ldmia/stmia で割込みハンドラ8個分がコピーされ、2セット目でその後に続くハンドラのアドレスリストがコピーされています。

C的には memcpy ですかね。

なぜこのようなハックが必要か?というと、プログラムが必ず 0x8000 にロードされて、直接 0x0000 には書きこめないからみたいです。

スタックポインタの初期化

続いて以下のような似たような3つの塊がでてきます。

    /* IRQ Mode (0b11010010),   */
    mov r0,#0xD2
    msr CPSR_c,r0
    mov sp,#0x8000

    /* FIQ Mode (0b11010001) */
    mov r0,#0xD1
    msr CPSR_c,r0
    mov sp,#0x4000

    /* Supervisor Mode (0b11010011) */
    mov r0,#0xD3
    msr CPSR_c,r0
    mov sp,#0xF000000

msr 命令はプログラムステータスレジスタに値を書きこむ命令で、ここでは特にモードの切り替えを行っています。CPSR_c は カレントプログラムステータスレジスタの下位 8bit に書きこむという意味です。

カレントプログラムステータスレジスタの下位 8bit は以下のような構造になっています。

    /* 
     *  CPSR register lowest 8bit: (page A2-11)
     *      I    [7]   -> IRQ disabled (set 1 to disable)
     *      F    [6]   -> FIQ disabled (set 1 to disable)
     *      T    [5]   -> Always set 0 in ARM state
     *      MODE [4:0] -> Mode bit
     */

下位4bitでモードを指定するようになっており、それぞれコメントにあるようなモードに遷移します。

ARM では例外モードと呼ばれるモードそれぞれに対して sp レジスタは別々に存在しています。ここでは sp をそれぞれ別のアドレスを示すように初期化しているようです。

notmain の呼び出し

その後、スーパーバイザーモードのまま、C で定義された notmain 関数が呼ばれて終わりです。

本来、.bss の初期化などがあるはずですが、この例では使ってないので書いてないようです。

  1. トップ
  2. tech
  3. Raspberry Pi で bare metal している blinker05 の ARM ブートコードを読む