9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Arduino で inline assembler 的なことをする前に見るメモ(AVR)

Last updated at Posted at 2020-01-10

目的

たまにしか asm() 使わないせいで、いろいろ忘れてしまうので、書く前にとりあえず見とけ的な個人的メモです。AVR な想定です。

参考

もっとしっかり思い出したいときは以下

とりあえず見ておけ的コード

Arduino UNO or Arduino Pro Mini 5V (ATmega328p/5V/16MHz) で 0.6ms 毎に D7 ピン (PD7) の出力を on/off する。WS2812B とか SK6812 に信号として入れると、(r,g,b) = (255,255,255) で光ります。

all_white.ino
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.ino
  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" は不要。
  • volatile はコンパイラが最適化時にこのコードを消さないようにするためのもの。おまじない的に常に書いておく。
    • volatile がないと、例えば asm("nop"); のような、1クロックだけ待たせるために nop を書いたとしても、コンパイラは何の処理もしていないコードと判断して、最適化時に消してしまったりする。

余談: そもそもアセンブリ言語的に書くなら

legacy_asm.ino
   asm volatile(
      "\t"   "mov %[x], %[y]\n"
      ...
      "\t"   "ret"
   );

みたいな書き方になると思うのだけど、"\n\t" とまとめて書きたくて、いつしかこうなったののかな? (見た目だけの問題だけど)

余談2: 基本形は以下。

basic.ino
byte x = 0, y = 1;
asm voltile ( "mov %0, %1" : "+r" (x) : "r" (y) );

よく使うニーモニック

ニモニック オペランド 動作 意味 補足
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

image.png

条件分岐で飛べる先のアドレスは、現在のアドレス (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 レジスタとしても使われる。

制約使用上の注意

マクロ名と変数名の対応付けを書いても、制約が正しく書かれていなかったり、出力と入力を間違って書くと、予想しない結果になることがある。

たとえば、

sample01.ino
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 と出力されるけど、

sample02.ino
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 には正しい値が入る。という感じの変なことがおこる。

単純に、

sample0.ino
  asm volatile (
    "mov %[y], %[x]  \n\t"
    "mov %[z], %[y]  \n"
    : [z] "+r" (z)
    : [x] "r"  (x)
  );

これだと、%[y] の定義が存在しないので、コンパイラ(?)がエラーとして弾いてくれるのだけど、先のコードだと %[y] というマクロ自体の定義は存在しているため、エラーにならない。制約について理解していないと、これでハマったりする。

内部的には(おそらく)、

sample1.ino
  asm volatile (
    "mov %0, %[x]  \n\t"
    "mov %[z], %0  \n"
    : [z] "+r" (z)
    : [x] "r"  (x)
  );

こんな感じに解釈されて、実行されているぽい。

余談:マクロの指定は必須ではない。

sample1x.ino
  asm volatile (
    "mov %0, %1  \n\t"
    : "+r" (z)
    : "r"  (x)
  );

こんなコードも通る。この場合、%0 に割り当て可能なのは、出力オペランドの z しかないので、%0 は自動的に z に割り当てられる。同様の理由で x は %1 に割りあてられる。

sample1y.ino
  asm volatile (
    "mov %0, %1  \n\t"
    "mov %2, %0  \n\t"
    : "+r" (z)
      "+r" (y)
    : "r"  (x)
  );

この場合は、%0 と %2 は y, z のどちらにでも割り当て可能なので、コンパイラの気分で(?)どちらかに割りあてられてしまう。x は %1 になる。

sample1z.ino
  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 になる。ちなみに、

sample1y.ino
  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 無し教との戦いになるので個人的にはやりたくはない。

sample2.ino
  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 以内でないと、リンカでエラーが出る。

sample3.ino
  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"
  );

このコードはエラーにならないが、

sample4.ino
  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 を使って、例えば次みたいに書く。

sample5.ino
  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 くらい頑張って最適化して縮められるよね(適当)。

まとめ

だいたいこんな感じでしょうかね…。また思いついたことがあったら追記しときます。

9
6
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?