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

# 目的

たまにしか 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)` で光ります。

```c: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()` を使う。

```c: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 を書いたとしても、コンパイラは何の処理もしていないコードと判断して、最適化時に消してしまったりする。

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

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

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

余談2: 基本形は以下。

```c:basic.ino
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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/2ecb2e52-601c-2004-b0a3-dc1e108b5147.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 レジスタとしても使われる。


## 制約使用上の注意

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

たとえば、

```c: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` と出力されるけど、

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

単純に、

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

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

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

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

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

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

```c:sample1x.ino
  asm volatile (
    "mov %0, %1  \n\t"
    : "+r" (z)
    : "r"  (x)
  );
```
こんなコードも通る。この場合、%0 に割り当て可能なのは、出力オペランドの z しかないので、%0 は自動的に z に割り当てられる。同様の理由で x は %1 に割りあてられる。

```c: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 になる。

```c: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 になる。ちなみに、

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

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

```c: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"
  );
```

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

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

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

# まとめ

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