SNSへはこちら

STM32でARMパチパチマイコンを作る その2

その1 でやった内容を少し発展させてみました。

関数呼び出し

呼び出し方法/リターン方法

ARM においては関数呼び出しは単なるラベルへのジャンプです。強制ジャンプでは単純に pc にアドレスを打ち込んでしまえばいいのですが、関数として返ってくることが要求されるときはリンクレジスタ lr に返り先アドレスを代入しておく必要があります。というかそれは面倒なので、通常 bl or blx を使います。ラベルを書くときは必ず bl を使ってください。ラベルを引数にした blx では ARM モードに切り替わってしまいます。STM32 では Thumb2 モードしかサポートされていないため死にます。

関数から返ってくるときは、lrの値をpcに入れればいいです。これは脳死でbx lr とすればOKなんです。なお絶対ジャンプアドレスを指定するときは、Thumb2 モードのために 0 ビットめを1にしてください

Func:
    nop
    bx lr @ Funcからもとに戻る(次は ldr から実行)

bx Func @ Funcにジャンプ、lrに値を入れる
ldr r0, [pc, #4]
...

C言語における規約

C言語では関数コールの際に引数を渡すなどします。そういうこともあって、呼び出し先の関数やらで操作する汎用レジスタの決まりがあります。Procedure Call Standard for the ARM® Architectureによると

Variable Register 関数内ローカル変数の格納先として使用して良いものです。よって、呼び出し元関数のために、使用前は pop、使用後は push してあげなければなりません。

Scratch Register も同様ですが、これは値の保持を保証しません。よってここから関数を呼んだときに値が変わっていることがあります。ですのでローカル変数というより、関数呼び出しまでのほんの一部変数として使います。自由に破壊できるレジスタなので、退避の必要はありません。
Result Register/Argument Register は関数の返り値/引数を格納するものです。返り値は基本的に r0 で返ります。

int returnMe(int arg) {
    int a = arg + 1;
    return arg;
}

とすると、r0arg が入って、r0return する値を書きます。これをとりあえずアセンブると

returnMe:
    push {lr} @ lrを退避
    add r0, #1 @ 引数はr0にあるので1を足しておく
    pop {pc} @ 退避したlrをpcに入れてしまう bx lrの代わり

てなわけですね。ここで敢えて r4 を一時用で使うようにするとこんな感じですね。

returnMe:
    push {r4, lr}
    add r4, r0, #1 @ r4 <= r0 + 1
    ldr r0, r4 @ r0 <= r4
    pop {r4, pc}

lr は関数を呼ぶときに変更されてしまいますので、上の push {lr} がない場合は、呼び出し関数から更に関数を呼ぶ時、リターン先のアドレスが失われてしまいます。よって関数呼び出しのときは常に lrpush しておきましょう。

何でこの話をしたかと言うと、パチパチマイコンハードを作ったときに、一から LED をボヤァさせる関数 led_boya として中身を打ち込むのがつらいのではと感じたためです。ということで、予め led_boya 関数を作っておき、それをコールすれば良いのでは、ということで調べていきました。

ちなみに led_boya は以下のような内容です。あ、あと今後はC++ でファームウェアを書いていきます。

void led_boya() {
    for(int i=0; i<20; i++) {
        GPIOF->ODR = 1;
        peripheral::systick::ms_wait(i);
        GPIOF->ODR = 0;
        peripheral::systick::ms_wait(20-i);
    }
    for(int i=0; i<20; i++) {
        GPIOF->ODR = 0;
        peripheral::systick::ms_wait(i);
        GPIOF->ODR = 1;
        peripheral::systick::ms_wait(20-i);
    }
    GPIOF->ODR = 0;
}

実際に LED がボヤァとする関数を作成し、そこにジャンプするプログラムを作ってみました。以下ではそれを2回連続でコール、元の呼び出し先に戻っています。

00: B5 10  push {r4, lr} @ 途中で関数呼び出しがあるので r0 ではなく r4 を使う
02: 4C 02  ldr r4, [pc, #8] @ 関数アドレスをコピー
04: 47 A0  blx r4 @ 実行(1)
06: 47 A0  blx r4 @ 実行(2)
08: BD 10  pop {r4, pc}
0A: BF 00  nop @ alignment
0C: 08 00 04 61  Imm32(led_boya)

以上、C 言語に合わせたレジスタの取り扱いでした。これまでだと好き勝手にレジスタを使っていたので、この規約に慣れるという点で勉強になりました。