SNSへはこちら

ARMのSTM32マイコンでアセンブリしたい!(インラインアセンブラ編)

2018/1/25 追記: 一番参考にしていた id さんの記事を参考としてリンクの貼り忘れがありました。お詫びして訂正します。m(_ _)m

どうも。最近なんと マイコンプログラミングにおいてC言語を使うことにも違和感を感じ始めました。そのことをツイートした所、情報理工学徒にも「重症患者」と言われたのがつい最近です。

とはいってもじゃあ一からプログラムを組む知識あるのかと言われれば無いので、とりあえずということでインラインアセンブラを勉強してみました。以下その際のノートです。

インラインアセンブラの書き方

基本的に各命令は \n\t で区切って書きます。\t は必須ではないですが、コンパイルエラー時の見栄えが良いということらしいです。また、各オペランドの区切り文字として comma を用いることになっています。

asm 文は __asm__ でも __asm でも良いですが、どちらも volatile 修飾子を後置して付けること。付けないと gcc がおせっかい的な、アセンブリコードを全削除という究極の最適化が行われるらしいです。命令コードの書き方は以下のような感じ。

asm volatile(
    "opcode1\n\t"
    "opcode2\n\t"
    // ...
    : output : input // : optional
    );

コード中に即値を書く場合は、必須ではないが # を前置しましょう。

入出力オペランド制約

ここで上の outputinput に相当する部分について説明。これはレジスタと RAM 変数の入出力関係を記述するもので、コード内から名前をつけて参照することが出来ます。例えば次の例。

uint64_t in, out;
asm volatile(
    "mov %[valDest], %[valIn]\n\t"
    : [valDest]"=r"(out)
    : [valIn]"r"(in)
    );

これは変数 in の値を out にコピーするものです。mov は move から来ていますが、実際上の意味的にはコピー命令なのでご注意を。コード中には %[hoge] という怪しげな記述がありますが、これが名前をつけた参照のことです。コードを書いた後、コロン(:)の後に制約を書くのですが、ここでは2つほど記述をしています。

: [valDest]"=r"(out) の部分に関して説明しますね。コード直後にコロンを置いて制約の記述があるのでこれは出力オペランド制約といいます。一般にコロンの後に [valDest] と書くことでその名前の制約についての記述となることを覚えておきましょう。その後制約条件を文字列リテラルで記述し、更に続けて(out) とすることで、その制約を変数に設定します。r汎用レジスタのことであり、=出力を意味する修飾です。

続いて: [valIn]=r"(in) の部分に関してです。出力オペランド制約に続いて、コロンを置いて記述をするとそれは入力オペランド制約となります。書き方は上と同様ですが、入力の制約ということなので、出力の意味である = は書かないでくださいね。

その他の修飾子として入出力双方向を意味する + があります。これは出力オペランド制約として書きます。便利ですね。入力として使わなくても + を書いてOKです。...多分。値としては大丈夫でした。仕様としてどうかは別ですが。

「じゃあ r 以外なんかあるのか」という方はこの記事一番下の「参考」から一番上のリンク(こちら)をどうぞ。

asm volatile(
        "add %[val], %[val], #1\n\t"
        : [val]"+r"(tmp)
        ); // tmp++

即値の表現

即値は前述の通り hash(#)を前置して表現します。整数の場合はシフト演算も C 言語同様に記述可能なよう。

mov r0, #(1 << 5)

小数の場合は通常通りの表記。なお数値の後に f を後置したらエラーとなるのでこれは書かないでください。基本単精度浮動小数点数型のみのサポート。

ARM のモード

なんかよく分からないのですが、ARM モードと Thumb 2 モードがあるらしいです。STM32 では ARM モードはなく、 Thumb 2 モード、Thumb モードのみを実行できるとのこと。Thumb モードはコードサイズが 32bit である一方、Thumb 2 モードではコードサイズが 16bit のみと容量を余り食わないので効率がいいらしい。なお ARM モードでは以下のコードのように、直前の演算についてフラグを見て、条件を満たしていたら実行をするということが可能。なんだそれ便利。

asm volatile(
    "add %[dest], %[in], #10\n\t" // dest <- in + 10
    "cmp %[in], #100\n\t" // dest == 100 ?
    "moveq %[dest], #0\n\t" // If so, dest <- 0
    : [dest]"=r"(out)
    : [in]"r"(in)
    );

なお Thumb 2 モードでに似たようなことをするには、後述する IT 命令 と言うものが必要になります。

レジスタに名前付きアクセス

変数に関して、register 装飾子を付け、asm を後置すれば行えます。register なんてどっかで見たことありますよね。

register int R3 asm("r3");

各種命令に関して学んだこと

ローテート

左ローテート命令は存在しないということに要注意です。多分ループとかでシコシコ書かねばならないんだと思います。右ローテートは ROR でできるのですが、その書き方は以下のような感じ。

ror Rd, Rs, <Rn|sh> @ 第3オペランドはローテート回数を保持するレジスタ or 即値

これで、RdRs を第3オペランドの回数だけ右ローテートしたものが代入されます。変数長は 64bit ですからこれも注意。int 型でやろうと思ったらローテートせずにゼロになってご臨終ということがあるので 64bit 変数で用いましょう。当然ながら右に溢れた桁は即時一番左の位に挿入されます。

ムーブ

データをコピーする命令です。さっき述べましたね。

mov Rd, op2

op2 は即値あるいはシフト命令を込みで指定できます。つまりシフトしながら値をコピーできるということですね。但しシフト命令を書く場合はその引数はレジスタ値でなくてはならないのに注意。すみません。即値で mov r0, r0, ror 1 でも OK っぽいです。

mov Rd, Rm @ Rd <- Rm
mov Rd, #0x80 @ Rd に即値 0x80 を代入
mov Rd, Rm, lsl Rs @ Rd <- (Rm << Rs) 論理左シフト
mov Rd, Rm, lsr Rs @ Rd <- (Rm >> Rs) 論理右シフト
mov Rd, Rm, asr Rs @ Rd <- arith(Rm >> Rs) 算術右シフト
mov Rd, Rm, ror Rs @ Rd <- rotate(Rm << Rs) 右ローテート

分岐

b を用います。この直後に条件ニーモニックを書きます。例えば「等しい」であれば beq のように。一般的な書式を以下の表にまとめました(抜粋であり、全てではないことに注意)。

b{cond} addr

addr はジャンプ先の命令アドレス or ラベルとします。

ニーモニック 意味
EQ 等しい
NE 等しくない
CS キャリーあり
CC キャリーなし
MI 負である
PL 正またはゼロである
VS オーバーフローあり(浮動小数点では NaN である)
VC オーバーフローなし
LE 小さいか等しい
GE 大きいか等しい
LT 小さい
GT 大きい
AL 常に(省略可)

IT

前述の ARM モード限定の条件を含んだ命令を Thumb 2 モードあるいは Thumb モードで実行できるようにする命令です。If Then の略。

it <cond>

<cond> には条件を書きます。例えば以下のような感じ。

it eq
    moveq r0, r1

こうすると、直前にゼロフラグが立っている時に mov が実行されます。感覚的には以下の擬似コードみたいな感じでしょう。

If eq Then
    mov r0, r1
EndIf

この Then の T を増やすことで、最大4命令をこのブロックに含めることが出来ます。なんか面白いですね。イタタタタみたいな感じで。

itttt eq
    moveq r0 r1
    moveq r0 r2
    moveq r0 r3
    moveq r0 r4

加算

キャリー付きとキャリー無しがあります。

add Rs, Rm, op2 @ Rd <- Rm + op2
adc Rs, Rm, op2 @ Rd <- Rm + op2 + carry

op2 は ムーブ命令の箇所を参照。

減算

キャリー、オペランドの関係については加算と同じです。

sub Rs, Rm, op2

乗算

掛け算も出来ます。大学の実験で使った 8bit マイコンの KUE-CHIP2 では非常に面倒くさかったです。

mul Rs, Rm, op2

この命令に関しては色々なバリエーションがあるので参考リンクのクイックリファレンスをどうぞ。

ステータスフラグに反映させる

加減算等、大体の命令が s をつければステータスフラグが更新されるようになるのでいちいち cmp しなくても良くなります。

subs r0, r0, #1 @ r0--
beq Loop @ If r0 == 0 then goto Loop

どの命令がこう書けるかは、この記事一番下「参考」のクイックリファレンスを参照してください。

FPU を用いた命令(VFP 命令)

Cortex-M3 等に限定されますが、浮動小数点計算ユニットでの命令を述べたいと思います。

基本的な書き方

Vop.Fxx op...

Vop は VFP 命令。Fxx には Fxxk...じゃなくて、精度を指定します。f32 は単精度(float)、f64 は倍精度(double)です。各精度は CPU によってサポートされているか異なるので注意しましょう。STM32 では f32 のみサポート。

また、インラインアセンブラでは通常のレジスタ r0 等ではなく VFP 用のレジスタ s0 等を用いることにもご注意。それ故オペランド制約を r ではなく w としなければなりません。

ニーモニックについては頭に V が付く。

math.h の sqrt 関数

中身を逆アセンブルしてみました。ただし単精度浮動小数点数型(float)のみ。double だと別の処理が入って遅いので今回は扱いません。その中身は...

vsqrt.f32 s0, s0

とこんな風に FPU を用いる計算をしているのが分かりますね。良い感じだ。

ムーブ

VFP のレジスタに値をロード。ロードする値は通常の汎用レジスタでも即値でも良い。

vmov.f32 dest, src @ レジスタからロード
vmov.f32 dest, #1.0 @ 即値1.0をロード

ビット列そのままでコピーされるので、整数値をコピーした場合等には変換が必要となることに注意が必要です(次の項)。

型変換

vcvt.{dest_type}.{src_type} dest, src

型名指定は以下の候補があります。

型名ニーモニック 意味
S16 符号付き16bit整数
U16 符号なし16bit整数
S32 符号付き32bit整数
U32 符号なし32bit整数

例えば r0 から s0 へ符号なし16bit整数を単精度浮動小数点数型としてコピーしたいとき、以下のコードを実行するのが正しい。というかイディオムとして覚えてしまってもいいでしょうね。

vmov.f32 s0, r0
vcvt.f32.u16 s0, s0

平方根

vsqrt.f32 dest, src

src に負の値を指定した場合は NaN を返します。そりゃそうだ。

否定、絶対値

否定とは符号を変えることを意味します(negnegate の略?)。-11.4514 の否定は 11.514 。書き方は以下を見てください。。

vneg.f32 dest, src

絶対値は以下の書式で書く。

vabs.f32 dest, src

四則演算

mnemoniq dest, op1, op2 @ dest <- op1 (mnemoniq) op2

destop1 が同一の場合は dest を省略できます。

プログラム例

以下、センスのない自作プログラムです。

64bit変数を無限に右ローテートし、その平方根の負の方を求める

uint64_t tmp = 1;
float flt = 0.0f;

asm volatile(
        "ror %[val], %[val], #1\n\t" // right roate of tmp
        "vmov.f32 %[flt], %[val]\n\t" // flt <- tmp
        "vcvt.f32.u32 %[flt], %[flt]\n\t" // convert unsigned int -> float
        "vsqrt.f32 %[flt], %[flt]\n\t" // flt <- sqrt(flt)
        "vneg.f32 %[flt], %[flt]\n\t" // flt <- -flt
        : [val] "+r"(tmp), [flt] "+w"(flt) :
);

LED チカチカ

asm volatile(
        "eor %[odr], %[odr], #(1 << 7)"
        : [odr] "+r"(GPIOA->ODR)
);

参考

コメント

  1. 匿名 より:

    古いハード技術屋です.
    ARMは以前ら使っていますが,最近はファームを若い人に任せて放任していました.
    少し厳しい要求があったので,一部をニモニックにしてみたら? と言ってみましたが,
    担当者は意味が分からない様子です.
    仕方がないので,明日から私がニモニックで書いて見せるしかありません.
    記事は大変参考になりました.ありがとうございました.