実践・実録！Arduino処理速度改善！！（関数展開、digitalwrite()編） 


# はじめに
おはこんばんちは。BlueToneです。
やってまいりました！アドベントカレンダー2019！！
去年はArduinoのカレンダーがなくて寂しかったので、作ってみました。
まだ空いているひがあるので、お気軽に参加してみてください！！

# お題
コーヒーの自家焙煎の自動化の記事を書く予定でしたが、
まだ完成していないので、別ネタで。

昔に作った、Arduinoと赤外線センサを使った、テルミン[「スペース・デジミン」](https://letsmakebt.blogspot.com/2017/03/arduino.html)の開発秘話。

SRAMのライブラリ[23LC1024 SRAM Library](https://forum.arduino.cc/index.php/topic,182918.0.html)の処理速度が遅くてノイズが出る問題があったのですが、
ライブラリのコードを見て、処理速度改善に取り組んだ話をしたいと思います。

## 小咄
Arduinoは16Mなので、簡単なことをするには十分な処理能力を持っていますが、
用途によってはパワー不足は否めません。
音はCDレベルのクオリティ(44.1kHz)なら、1秒間に44100回の音を作る必要があり、ちとパワー不足ですね。
最適化することにより、マイコンが持っているパワーを最大限引き出すことが可能です！
（界王拳３倍だーーー！！！）

# 問題点、ボトルネック調査
処理問題は「計測」して「定量的に」ボトルネックを特定することが定石ですね。
「技術者のカン」も大事な時がありますが、計測による裏付け大事ですね。

## 処理速度を計測
ループ処理内の処理時間を計測して、どの部分が遅いかを切り分けてていきます。

```c++:
  unsigned long startTime = micros(); 
  // 何か処理
  unsigned long endTime = micros(); 
  Serial.println(endTime - startTime);
```

処理が小さすぎて、micros()で計測しにくい場合は、1000回とかループで回してあげましょう。

# 処理速度改善
## 改善前 
普通に実装するとこんな感じ。
currentTapはSRAMのアドレス位置の変数です。

```c++:
  unsigned long startTime = micros(); 
  
  for(int i = 0; i < 1000; i++){
    readVal = sRam.readByte(currentTap); // SRAM読込み
  }

  unsigned long endTime = micros(); 
  Serial.println(endTime - startTime);
```
-> 256µs
1000回ループで256µsかかっています。これを速度改善していきます。

## 関数展開

ライブラリの関数を見てみましょう。

```c++:\libraries\SpiRAM\SpiRAM.cpp
byte SpiRAM::readByte(uint32_t address) {
  byte data_byte;
  readBuffer(address, (char*) &data_byte, 1);
  return data_byte;
}
・・・
void SpiRAM::readBuffer(uint32_t address, char *buffer, uint32_t length) {
  digitalWrite(_ss_Pin, LOW);
  setAddressMode(address, READ);
  for (uint32_t i = 0; i < length; i++) buffer[i] = SPI.transfer(0x00);
  digitalWrite(_ss_Pin, HIGH);
}
```


まずは関数展開するとこんな感じかな。

```c++:
  unsigned long startTime = micros(); 
  
  for(int i = 0; i < 1000; i++){
    // readVal = sRam.readByte(currentTap);
    digitalWrite(SDRDY, LOW);
    sRam.setAddressMode(currentTap, READ);
    readVal = SPI.transfer(0x00);
    digitalWrite(SDRDY, HIGH);
  }

  unsigned long endTime = micros(); 
  Serial.println(endTime - startTime);
```
-> 232µs

おおー。24µs早くなった。for文が消えてるのは、１バイトだからです。

setAddressMode()も展開してみましょう。
元のソースはこちら。

```c++:\libraries\SpiRAM\SpiRAM.cpp
void SpiRAM::setAddressMode(uint32_t address, byte mode) {
  SPI.transfer(mode);
  SPI.transfer((byte)(address >> 16));
  SPI.transfer((byte)(address >> 8));
  SPI.transfer((byte)address);
}
```

setAddressMode()展開後

```c++:
  unsigned long startTime = micros(); 
  
  for(int i = 0; i < 1000; i++){
    // readVal = sRam.readByte(currentTap);
    digitalWrite(SDRDY, LOW);
    // sRam.setAddressMode(currentTap, READ);
      SPI.transfer(READ);
      SPI.transfer((byte)(currentTap >> 16));
      SPI.transfer((byte)(currentTap >> 8));
      SPI.transfer((byte)currentTap);
    readVal = SPI.transfer(0x00);
    digitalWrite(SDRDY, HIGH);
  }

  unsigned long endTime = micros(); 
  Serial.println(endTime - startTime);
```
-> 232µs
変わってないな。関数呼び出しのオーバーヘッドだけではそんなに変わんないのかな？コンパイラで最適化されてるのかな？

## digitalwrite()高速化

digitalwrite()も高速化してみましょう。
詳しくは「digitalwrite 高速化」で検索。

```c++:
  unsigned long startTime = micros(); 
  
  for(int i = 0; i < 1000; i++){
    // 	readVal= sRam.readByte(currentTap);
  	SS_PORT &= ~_BV(SS_PORT_PIN);//digitalWrite(SDRDY, LOW);
  		//setAddressMode(currentTap, READ);
  		SPI.transfer(READ);
  		SPI.transfer((byte)(currentTap >> 16));
  		SPI.transfer((byte)(currentTap >> 8));
  		SPI.transfer((byte)currentTap);
  	readVal= SPI.transfer(0x00);
  	SS_PORT |= _BV(SS_PORT_PIN);//digitalWrite(SDRDY, HIGH);
  }

  unsigned long endTime = micros(); 
  Serial.println(endTime - startTime);
```
-> 212µs
やったね！！！
256µsから212µsに、44µsの高速化に成功しました！！！

今回はReadとWrite1か所ずつだったので、直接展開しましたが、改修箇所が多い場合はインライン関数やマクロなども検討してみてください。


# まとめ
・計測大事。
・地道な処理改善。そして計測。
・digitalwriteは遅い。

おまけ：
・ビットシフトもいいぞ！早いぞ！
・もっと処理速度を上げたい場合は、アセンブラで書くと良いらしい。
・ソフトで頑張るより、マイコンボードを変える案もある。