Arduino 複数の信号遷移の高速化

#Arduino IDE: digitalWrite()は遅い
Arduinoで信号遷移をさせる場合、digitalWrite() では遅い場合があります。とくに、複数の信号を一度に変更する場合は、digitalWrite()で1ピンずつ書いているとタイミングの規格に間に合わないことがあります。検証してみました。

Digitalピンの8番と9番を同時にLOW→HIGH→LOWと変化させるケースを例とします。使用したのは Arduino UNO 16MHz版です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513938/64f2a243-cd14-bf30-f64d-91509f05eb88.png)

##コード1: digitalWrite()
loop()の中に whileはいらなかったかも...。ループ内はコード上はすぐにLOW/HIGHをぐるぐる切り替えるようになっています。

```c++:code1.ino
void setup() {
  pinMode (D8, OUTPUT);
  pinMode (D9, OUTPUT);
  digitalWrite(D8,LOW);
  digitalWrite(D9,LOW);
}
void loop() {
  while(1) {
  digitalWrite(D8,HIGH);
  digitalWrite(D9,HIGH);
  digitalWrite(D8,LOW);
  digitalWrite(D9,LOW);   
  }
}
```

**ロジックアナライザの結果:**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513938/806679ec-3f65-7fca-70e6-180048d4fbbd.png)

digitalWrite() コマンド一つに 4us くらいかかってますね。D8→D9 の信号遷移の間隔は 4us もあります。250kHzくらいになります。1MHzクロックだと 4サイクル通過してしまいます。

##コード2: ポートレジスタへのビット演算
ポート出力レジスタ(PORTx というシステム変数)を ビット演算にすることで、LOW/HIGHをぐるぐる切り替えます。特定のビット(ArduinoのD8はATMEGA328PのPB0, D9はPB1) だけを遷移させるので |= や &= ~ を使います。ちょっとトリッキー。

```c++:code2.ino
const int D8 = 8 ; // PB0 of ATMEGA328P
const int D9 = 9 ; // PB1 of ATMEGA328P
void setup() {
  pinMode (D8, OUTPUT);
  pinMode (D9, OUTPUT);
  digitalWrite(D8,LOW);
  digitalWrite(D9,LOW);
}
void loop() {
  while(1) {
    PORTB |=  _BV(0) ; // digitalWrite(D8,HIGH);
    PORTB |=  _BV(1) ; // digitalWrite(D9,HIGH);
    PORTB &= ~_BV(0) ; // digitalWrite(D8,LOW);
    PORTB &= ~_BV(1) ; // digitalWrite(D9,LOW);
  }
}
```


**ロジックアナライザの結果:**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513938/2e29eaec-a965-d772-34b1-262e74e357ac.png)

20倍以上高速になり、D8→D9のそれぞれの立ち上がりエッジの差は ロジアナ上は129us でした。サンプリング周波数が高くないので、実際は125～165nsくらいの間隔になっているでしょう。

##コード3: アセンブリ言語
特定ビットの変更をアセンブリ言語で書くとどうなるか、をインラインアセンブラ(\_\_asm\_\_)で書いてみました。PORTBは 0x03なんだそうです。sbiは指定ビットを HIGH(1)にセットし、cbiはLOW(0)にクリアするオペコードです。

```c++:code3.ino
const int D8 = 8 ; // PB0 of ATMEGA328P
const int D9 = 9 ; // PB1 of ATMEGA328P
void setup() {
  pinMode (D8, OUTPUT);
  pinMode (D9, OUTPUT);
  digitalWrite(D8,LOW);
  digitalWrite(D9,LOW);
}
void loop() {
  while(1) {
     __asm__ __volatile__ (
    "PINB = 0x03      \n\t" // PINB
    "sbi  PINB,0      \n\t" // digitalWrite(D8,HIGH)
    "sbi  PINB,1      \n\t" // digitalWrite(D9,HIGH)
    "cbi  PINB,0      \n\t" // digitalWrite(D8,LOW)
    "cbi  PINB,1      \n\t" // digitalWrite(D9,LOW)
    );
  }
}
```

**ロジックアナライザの結果:**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513938/9a1fb295-8e6e-d328-370f-69c290a0a0b5.png)

コード2のビット演算と変わりないですね。サンプリング周波数が24MHzなので41psごとに標本化しています。○がサンプリングポイントです。
ビット演算(コード2)より速いことを期待しましたが、同着です。コンパイラの最適化機能が優秀というか、無理してインラインアセンブラを駆使する必要はなさそうです。とはいえ、同一クロックサイクル内に同時に遷移するような規格の場合は165ns だと 6MHzくらいまでになってしまいます。

##コード4: 2本を同時に遷移させる
論理ビット演算が速かったので、(D8,D9)を同時に(LOW,LOW)→(HIGH,HIGH)→(LOW,LOW)させるコードを書いてみましょう。ビット演算をカッコつきで計算するだけですね。代入を1回にするのがポイントです。

```c++:code4.ino
const int D8 = 8 ; // PB0 of ATMEGA328P
const int D9 = 9 ; // PB1 of ATMEGA328P
void setup() {
  pinMode (D8, OUTPUT);
  pinMode (D9, OUTPUT);
  digitalWrite(D8,LOW);
  digitalWrite(D9,LOW);
}
void loop() {
  while(1) {
    PORTB |= ( _BV(0) |  _BV(1) ) ; // digitalWrite(D8,HIGH); digitalWrite(D9,HIGH);
    PORTB &= (~_BV(0) & ~_BV(1) ) ; // digitalWrite(D8,LOW); digitalWrite(D9,LOW);
  }
}
```

**ロジックアナライザの結果:**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513938/3c151eb9-ff81-bc0e-120b-c286ec587052.png)

すばらしい！ というか当然ですかね。同時に遷移できているのが確認できました。

##結論
高速に同時に信号遷移する場合は、アセンブラ記述を使わなくてもビット演算で同等の処理ができる。

---
#追加検証しました
コメントいただきましたので、検証してみました。
なお、このコンパイル環境と実機動作は Arduino IDE 1.8.5 + 純正Arduino UNO (ATMEGA328PU, 16MHz) です。
測定しているロジアナは24MHz でサンプルしています。(もうちょっと速い装置が欲しいですねぇ...)
コメントいただいた方々、どうもありがとうございました。

##コード5: (検証) 2本を同時に遷移させる その2
ビット演算というよりもそのまま代入というパターンですね。@ttatsfさんありがとうございます。

```c++:code5.ino
const int D8 = 8 ; // PB0 of ATMEGA328P
const int D9 = 9 ; // PB1 of ATMEGA328P

void setup() {
  pinMode (D8, OUTPUT);
  pinMode (D9, OUTPUT);
  digitalWrite(D8,LOW);
  digitalWrite(D9,LOW);
}

void loop() {
  while(1) {
    PORTB |= B00000011 ; // digitalWrite(D8,HIGH); digitalWrite(D9,HIGH);
    PORTB &= B11111100 ; // digitalWrite(D8,LOW); digitalWrite(D9,LOW);
  }
}
```

**ロジックアナライザの結果:**
![code5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513938/dfdf5715-bae5-b2f9-ac2c-dc2674cff581.png)

測定結果は、コード４と同じでした。コンパイラ出力が同じになっているのだろうと予想します。
書きやすい・わかりやすいほうで書き分ければいいのではないでしょうか。
Hi期間よりLo期間が長いのは ループで戻る jmp 命令分の時間なのでしょう。

##コード6: (検証) 2本を同時に遷移させる その3
あらかじめ処理するパターン(例では on, off)を定義しておいてあとで代入というパターンですね。@fujitanozomuさんありがとうございます。

```c++:code6.ino
const int D8 = 8 ; // PB0 of ATMEGA328P
const int D9 = 9 ; // PB1 of ATMEGA328P

void setup() {
  pinMode (D8, OUTPUT);
  pinMode (D9, OUTPUT);
  digitalWrite(D8,LOW);
  digitalWrite(D9,LOW);
}

void loop() {
  noInterrupts();
  byte on = PORTB | ( _BV(0) |  _BV(1) ) ;
  byte off = on & (~_BV(0) & ~_BV(1) ) ;
  while(1) {
    PORTB = on;
    PORTB = off;
  }
}
```

**ロジックアナライザの結果:**
![code6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513938/3616c77e-beb9-177e-354c-e4fc10216db5.png)

ループのなかの語数が減った効果でさらに高速になっています。素晴らしい！