目的
たまにしか asm() 使わないせいで、いろいろ忘れてしまうので、書く前にとりあえず見とけ的な個人的メモです。AVR な想定です。
参考
もっとしっかり思い出したいときは以下
- AVR.jp: https://avr.jp/user/DS/PDF/mega328P.pdf
- AVR Libc Reference Manual
Inline Assembler Cookbook: https://www.microchip.com/webdoc/avrlibcreferencemanual/inline_asm.html - AVRアセンブラでLチカ: https://qiita.com/TakeoChan/items/8362eaf53cf199262cb7
とりあえず見ておけ的コード
Arduino UNO or Arduino Pro Mini 5V (ATmega328p/5V/16MHz) で 0.6ms 毎に D7 ピン (PD7) の出力を on/off する。WS2812B とか SK6812 に信号として入れると、(r,g,b) = (255,255,255)
で光ります。
void sample( byte count )
{
volatile byte hi, lo, mask = 0x80;
hi = PORTD | mask;
lo = PORTD & ~mask;
noInterrupts();
asm volatile(
"head:" "\n\t" // clock
"out %[port] , %[hi]" "\n\t" // 0
"nop" "\n\t" // 1
"nop" "\n\t" // 2
"nop" "\n\t" // 3
"nop" "\n\t" // 4
"nop" "\n\t" // 5
"nop" "\n\t" // 6
"nop" "\n\t" // 7
"nop" "\n\t" // 8
"nop" "\n\t" // 9
"out %[port] , %[lo]" "\n\t" // 10
"nop" "\n\t" // 11
"nop" "\n\t" // 12
"nop" "\n\t" // 13
"nop" "\n\t" // 14
"nop" "\n\t" // 15
"nop" "\n\t" // 16
"dec %[cnt]" "\n\t" // 17
"brne head" "\n" // 18,19
: [cnt] "+r" (count)
: [port] "I" (_SFR_IO_ADDR(PORTD)),
[hi] "r" (hi),
[lo] "r" (lo)
);
interrupts();
}
ざっくり説明
インラインアセンブラ的なことをするには asm()
を使う。
asm voltile (
"op_1" "\n\t"
"op_2" "\n\t"
...
"op_n" "\n"
: [マクロ名1] "制約" (変数名1), // 出力オペランド
[マクロ名2] "制約" (変数名2)
...
: [マクロ名n] "制約" (変数名n), // 入力オペランド
...
: [レジスタ名1], // clobber
...
);
- 1 clock は 16MHz の場合 0.0625us = 62.5ns
-
nop
ひとつで 1clock = 0.0625us の wait 相当 -
PORTD
は最下位ビットが D0, 最上位ビットが D7 (PD7) に対応。- `D0: RXD, D1: TXD なので通常は D2 から D7 を使う。
-
"op_1 \n\t"
と分割しないで書いても良い。- 命令が1行だけなら
"\n"
は不要。
- 命令が1行だけなら
-
volatile
はコンパイラが最適化時にこのコードを消さないようにするためのもの。おまじない的に常に書いておく。-
volatile
がないと、例えばasm("nop");
のような、1クロックだけ待たせるために nop を書いたとしても、コンパイラは何の処理もしていないコードと判断して、最適化時に消してしまったりする。
-
余談: そもそもアセンブリ言語的に書くなら
asm volatile(
"\t" "mov %[x], %[y]\n"
...
"\t" "ret"
);
みたいな書き方になると思うのだけど、"\n\t"
とまとめて書きたくて、いつしかこうなったののかな? (見た目だけの問題だけど)
余談2: 基本形は以下。
byte x = 0, y = 1;
asm voltile ( "mov %0, %1" : "+r" (x) : "r" (y) );
よく使うニーモニック
- データシートから抜粋: https://avr.jp/user/DS/PDF/mega328P.pdf
ニモニック | オペランド | 動作 | 意味 | 補足 |
---|---|---|---|---|
nop | - | - | 何もしない | 時間調整用 |
mov | Rd,Rr | Rd = Rr | レジスタ間代入のみ | - |
ldi | Rd,k | Rd = k | 定数の代入 | - |
ld | Rd,addr | Rd = *addr | アドレスからレジスタへの代入 | - |
st | addr,Rr | *addr = Rr | レジスタからアドレスへの代入 | - |
in | Rd,port | Rd = port | ポートの値をレジスタに代入 | - |
out | port,Rr | port = Rr | ポートにレジスタの値を出力 | - |
sbi | port,b | port[b] = 1 | ポートの b 番目のビットを 1 に | - |
cbi | port,b | port[b] = 0 | ポートの b 番目のビットを 0 に | - |
push | Rr | - | Rr をスタックに積む | - |
pop | Rd | - | スタックから Rd に復帰 | - |
lsl | Rd | Rd <<= 1 | 左シフト | - |
lsr | Rd | Rd >>= 1 | 右シフト | - |
swap | Rd | - | 上位4ビットと下位4ビットを入れ替え | - |
add | Rd,Rr | Rd += Rr | レジスタ間加算 | - |
addiw | Rd,k | Rd -= k | レジスタからwork長の定数を加算 | addi は無い |
sub | Rd,Rr | Rd -= Rr | レジスタ間減算 | - |
subi | Rd,k | Rd -= k | レジスタから定数を減算 | - |
sbiw | Rd,k | Rd -= k | レジスタからwork長の定数を減算 | アドレス計算用 |
and | Rd,Rr | Rd &= Rr | レジスタ間論理積 | - |
andi | Rd,k | Rd &= k | レジスタと定数の論理積 | - |
or | Rd,Rr | - | レジスタ間論理和 | - |
ori | Rd,k | - | レジスタと定数の論理和 | - |
eor | Rd,Rr | Rd ^= Rr | レジスタ間排他的論理和 | - |
com | Rd | Rd = ~Rd | 論理反転 | - |
neg | Rd | Rd = ~Rd + 1 | 2の補数 | - |
inc | Rd | Rd++ | インクリメント | - |
dec | Rd | Rd-- | デクリメント | - |
tst | Rd | Rd & Rd | ビットテスト(代入なし) | - |
clr | Rd | Rd = 0 | 全ビットを 0 に | - |
ser | Rd | Rd = ~0 | 全ビットを 1 に | - |
mul | Rd,Rr | Rd *= Rr | レジスタ間符号無し乗算 | - |
muls | Rd,Rr | Rd *= Rr | レジスタ間符号あり乗算 | - |
rjmp | k | PC += k + 1 | 相対アドレス無条件ジャンプ | - |
rcall | k | push PC, rjump k | 相対サブルーチンコール | - |
ret | - | pop PC | サブルーチンからの復帰 | - |
条件分岐
下記から抜粋: https://avr.jp/user/DS/PDF/AVRinst.pdf
条件分岐で飛べる先のアドレスは、現在のアドレス (PC )から 7bit (-64〜63) でしか指定できない。つまり、アドレスが遠いところへは飛ぶことができない。少し長いコードを書くとすぐ飛べなくなるので、asm の外で飛ぶような制御を書くか、rjmp, rcall を使って対処する。
rjmp と rcall は 12bit (-2048〜2047) で指定できる。とはいえ ATMega328 でもフラッシュは 32KB あるので、全アドレス空間は飛べない。
マクロと制約
普通の gcc の __asm__
とは微妙に(かなり?)違うぽいので要注意。
- 出力オペランドと入力オペランドの両方に使うものは、必ず出力オペランド側に書く。
- 入力オペランドに "+r" など読み書きできる制約を指定するとエラーになる。
- Rr は入力オペランド、Rd は出力オペランド
- オペランドが存在しない場合はコロンごと省略可能。
- 出力なしで入力のみの場合は、
:: [x] "r" (x)
のようにコロンを 2 つ書く。
制約 | 意味 | 補足 |
---|---|---|
+r | 汎用レジスタに割り当て(読み書き両方) | 出力オペランド |
r | 汎用レジスタに割り当て(読み出しのみ) | 入力オペランド |
+w | 汎用レジスタ2つにwordサイズで割り当て(読み書き両方) | 出力オペランド |
w | 汎用レジスタ2うにwordサイズで割り当て(読み出しのみ) | 入力オペランド |
+e | アドレスレジスタ(X,Y,Z) に割り当て(読み書き両方) | 出力オペランド |
e | アドレスレジスタ(X,Y,Z) に割り当て(読み出しのみ) | 入力オペランド |
I | ポートのアドレス(読み出しのみ) | 入力オペランド |
これ以外にもいろいろあるけど、使っているのを見たことない。後述するように=r
とかで書き込み専用にもできるけど、マクロを明示的に割り当てる場合は、一律 "+r" でいいような気がする。(だめなのかなー?)
- ポートは間接参照扱いで、ポートのアドレス(やI/Oレジスタ)は変更されない(書きこまれない)ため、入力オペランド扱いになる。
- ATMega328 の場合、portb の I/Oレジスタは 0x08、 portc は 0x06, portd は 0x0B
-
out 0x0b, %[x]
のように即値(定数)でポート (この場合は portD) を指定することもできる。 - 間接参照で使うだけのアドレスレジスタは入力扱いになり、制約は "e" にする。。
- ただし、参照後にインクリメンタル/デクリメンタルするようなニーモニック (
ld %[x], %a[ptr]+
みたいな) で使う場合は、出力レジスタ扱いにして 制約を "+e" にする必要がある。 - そういえば、ld や st で間接参照するときは
ld %[x], %a[ptr]
のように%
の後にa
が必要。とても忘れやすい。 - 間接参照でなければ、普通に
%[ptr]
と書く。たとえばdec %[ptr]
など。
- あたりまえだけど、レジスタの数を越えた割り当てはできない。
- ATMega328 の場合、汎用レジスタは R0-R31 の 32 本 (8bit)。
- word サイズで割り当てるときは、連続する2つのレジスタが上位8bit、下位8bitとして使われる。
- R26:R27 が X, R28:R29 が Y, R30:R31 は Z レジスタとしても使われる。
制約使用上の注意
マクロ名と変数名の対応付けを書いても、制約が正しく書かれていなかったり、出力と入力を間違って書くと、予想しない結果になることがある。
たとえば、
byte x = 1, y = 2, z = 3;
asm volatile (
"mov %[y], %[x] \n\t"
"mov %[z], %[y] \n"
: [z] "+r" (z),
[y] "+r" (y)
: [x] "r" (x)
);
Serial.print( x );
Serial.print( y );
Serial.println( z );
これは 111
と出力されるけど、
byte x = 1, y = 2, z = 3;
asm volatile (
"mov %[y], %[x] \n\t"
"mov %[z], %[y] \n"
: [z] "+r" (z)
: [y] "r" (y),
[x] "r" (x)
);
Serial.print( x );
Serial.print( y );
Serial.println( z );
この結果は 121
になる。
これは y
が入力オペランドになっているため mov %[y], %[x]
の %[y]
と変数の y
が対応付けられないためぽい。%[y]
には適当なレジスタが割り当てられてしまって、結果的に y
の値は 2 のままになる。でも z
には正しい値が入る。という感じの変なことがおこる。
単純に、
asm volatile (
"mov %[y], %[x] \n\t"
"mov %[z], %[y] \n"
: [z] "+r" (z)
: [x] "r" (x)
);
これだと、%[y]
の定義が存在しないので、コンパイラ(?)がエラーとして弾いてくれるのだけど、先のコードだと %[y]
というマクロ自体の定義は存在しているため、エラーにならない。制約について理解していないと、これでハマったりする。
内部的には(おそらく)、
asm volatile (
"mov %0, %[x] \n\t"
"mov %[z], %0 \n"
: [z] "+r" (z)
: [x] "r" (x)
);
こんな感じに解釈されて、実行されているぽい。
余談:マクロの指定は必須ではない。
asm volatile (
"mov %0, %1 \n\t"
: "+r" (z)
: "r" (x)
);
こんなコードも通る。この場合、%0 に割り当て可能なのは、出力オペランドの z しかないので、%0 は自動的に z に割り当てられる。同様の理由で x は %1 に割りあてられる。
asm volatile (
"mov %0, %1 \n\t"
"mov %2, %0 \n\t"
: "+r" (z)
"+r" (y)
: "r" (x)
);
この場合は、%0 と %2 は y, z のどちらにでも割り当て可能なので、コンパイラの気分で(?)どちらかに割りあてられてしまう。x は %1 になる。
asm volatile (
"mov %0, %1 \n\t"
"mov %2, %0 \n\t"
: "+r" (z),
"=r" (y)
: "r" (x)
);
こうすると、y は書き込み専用になるので、%2 にしか割りあてられなくなる。その結果、%0, %1, %2 の割り当てが一意に決まって、%0 = z, %1 = x, %2 = y になる。ちなみに、
asm volatile (
"mov r1, %1 \n\t"
"mov %2, r1 \n\t"
: "+r" (z)
: "r" (x)
: "r1"
);
こんなふうに、clobber を指定すると、少なくとも x と z はレジスタ r1 には割り当てられなくなる。(コード中で r1 を使うことはできる)
Arduino でマクロ名を使わない書き方をすることは無い気がするけど、メカニズムが分かってないと、謎のバグで悩まされることがあるのよね…(自戒)。
ラベルについて
ラベルは C 言語的に言うところのラベルと同じ扱いで、複数の asm() 間をまたがって参照したり、スコープが違うところを跨って参照することもできる。できるけど、goto
無し教との戦いになるので個人的にはやりたくはない。
byte x = 1, y = 2;
asm volatile(
"h1:" "\n\t"
"inc %[x]" "\n"
: [x] "+r" (x)
);
Serial.print( x );
Serial.print( y );
asm volatile(
"h2:" "\n\t"
"dec %[y]" "\n\t"
"brne h1" "\n"
: [y] "+r" (y)
);
Serial.print( x );
Serial.print( y );
これは普通にいける。22211120
と表示される。
分岐のところでも書いているように、brne とかだと遠いラベルは参照できない。飛び先のアドレスが -64〜+63 以内でないと、リンカでエラーが出る。
asm volatile(
"h1:" "\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"brne h1" "\n"
);
このコードはエラーにならないが、
asm volatile(
"h1:" "\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"brne h1" "\n"
);
これは、Aelocation truncated to fit: R_AVR_7_PCREL against
no symbol` みたいなエラーが出る。これも、知らないとハマりやすい。
どうしても遠くに飛びたければ、-2028〜2048 の範囲で飛べる rcall を使って、例えば次みたいに書く。
asm volatile(
"h0:" "\n\t"
"rcall h1" "\n\t"
"dec %[x]" "\n\t"
"brne h0" "\n\t"
"ret" "\n\t"
"h1:" "\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\t"
"ret\n\t"
: [x] "+r" (x)
);
このコードは問題なくコンパイルできる。何げに ret は全アドレス空間飛べるので、めちゃくちゃ長いサブルーチンを書いても、復帰は問題なくできる。
サブルーチン化する場合、 rcall と ret の挿入によって、ループの処理時間がその分だけ増えるという問題はある。とはいえ、64byte 分もあるコードなら、4 clock くらい頑張って最適化して縮められるよね(適当)。
まとめ
だいたいこんな感じでしょうかね…。また思いついたことがあったら追記しときます。