ArduinoのdigitalWriteを35倍高速化するライブラリの作成 - fastestDigitalWrite -


ArduinoのdigitalWriteは便利ですが非常に処理が重いと思います。なので高速版を自作し、オリジナルと速さを比較してみました。

・digitalWriteテストプログラム

```cpp:testDigitalWrite.cpp
#include <Arduino.h>

#define DOUT_PIN (10) // 出力ピン
#define DIN_PIN  (12)

void setup() {
  pinMode(DOUT_PIN, OUTPUT);
  pinMode(DIN_PIN, INPUT);
}

void loop() {
  for(;;) {
    digitalWrite(DOUT_PIN, HIGH);
    digitalWrite(DOUT_PIN, LOW);
    digitalWrite(DOUT_PIN, HIGH);
    digitalWrite(DOUT_PIN, LOW);
  }
}

```
上記プログラムはピン10の状態をHIGH→LOW→HIGH→LOW・・・のように交互に切り替えます。以後はHIGH, LOWをそれぞれ1, 0で表すことがあります。
このピン出力(信号)をオシロスコープで計測したものが以下です。
![Normal_digitalWrite.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/8627ba68-5c29-3856-c619-8859d60f52d5.jpeg)

HIGH→LOW→HIGHの区間、水色の縦線で囲まれた部分が、
　digitalWrite(10, 1);　が実行され、
　digitalWrite(10, 0);　が実行され、
　digitalWrite(10, 1);　が実行される手前まで
の処理に相当します。
つまりdigitalWriteを2回実行するのにかかる時間の区間です。
この区間の時間が8.743usとなっていますので、1/2を掛けるとdigitalWrite1回の実行時間 8.743 * 1/2 = 4.372us が求められます。後の比較のために単位をナノ秒に変えると 4372ns となります。

次に、筆者が作成したdigitalWriteの高速版fastestDigitalWriteで同じような計測をしてみます。
・高速化版テストプログラム

```cpp:testFastestDigitalWrite.cpp
#include <Arduino.h>
#include "fastestDigitalRW.hpp"

#define DOUT_PIN (10) // 出力ピン
#define DIN_PIN  (12)

void setup() {
  pinMode(DOUT_PIN, OUTPUT);
  pinMode(DIN_PIN, INPUT);
}

void loop() {
  for(;;) {
    fastestDigitalWrite(DOUT_PIN, HIGH);
    fastestDigitalWrite(DOUT_PIN, LOW);
    fastestDigitalWrite(DOUT_PIN, HIGH);
    fastestDigitalWrite(DOUT_PIN, LOW);
  }
}
```
上記プログラムのピン出力(信号)をオシロスコープで計測したものが以下です。
![fastestDigitalWrite01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/31110e2d-2cf0-c07b-0ee1-b015bc548764.jpeg)

1, 0の切り替えの間隔がとても短くなっているのが分かりますが詰まっていて分かりづらいので下記に表示を調整したものを載せます。
![fastestDigitalWrite02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/ec8681b1-13cd-8b24-2b48-acc70d7ce0cd.jpeg)

fastestDigitalWrite1回の実行時間は、249.298ns * 1/2 = 124.469ns となります。周波数では1 / 249.298 = 4.01MHz 出ています。

ここでfastestDigitalWrite実行時間がdigitalWrite実行時間の何倍になっているかを計算すると、
4327 / 124.469 = 34.76 となり、約35倍高速化されたことが分かります。


・作成したデジタル入出力高速化ライブラリ

```cpp:fastestDigitalRW.hpp
#ifndef _HPP_FASTEST_DIGITAL_RW_HPP_
#define _HPP_FASTEST_DIGITAL_RW_HPP_
#include <Arduino.h>

#define _INLINE_ inline __attribute__((always_inline)) 
_INLINE_ void fastestDigitalWrite(uint8_t pin, uint8_t val)
{
#if defined(__AVR_ATmega328P__)
	switch(val) {
		case HIGH:
			switch(pin) {
				// PORTD
				case  0: asm volatile("sbi 0x0b, 0x00"); break;
				case  1: asm volatile("sbi 0x0b, 0x01"); break;
				case  2: asm volatile("sbi 0x0b, 0x02"); break;
				case  3: asm volatile("sbi 0x0b, 0x03"); break;
				case  4: asm volatile("sbi 0x0b, 0x04"); break;
				case  5: asm volatile("sbi 0x0b, 0x05"); break;
				case  6: asm volatile("sbi 0x0b, 0x06"); break;
				case  7: asm volatile("sbi 0x0b, 0x07"); break;
				// PORTB
				case  8: asm volatile("sbi 0x05, 0x00"); break;
				case  9: asm volatile("sbi 0x05, 0x01"); break;
				case 10: asm volatile("sbi 0x05, 0x02"); break;
				case 11: asm volatile("sbi 0x05, 0x03"); break;
				case 12: asm volatile("sbi 0x05, 0x04"); break;
				case 13: asm volatile("sbi 0x05, 0x05"); break;
				// PORTC
				case 14: asm volatile("sbi 0x08, 0x00"); break;
				case 15: asm volatile("sbi 0x08, 0x01"); break;
				case 16: asm volatile("sbi 0x08, 0x02"); break;
				case 17: asm volatile("sbi 0x08, 0x03"); break;
				case 18: asm volatile("sbi 0x08, 0x04"); break;
				case 19: asm volatile("sbi 0x08, 0x05"); break;
			}
		break;
		case LOW:
			switch(pin) {
				// PORTD
				case  0: asm volatile("cbi 0x0b, 0x00"); break;
				case  1: asm volatile("cbi 0x0b, 0x01"); break;
				case  2: asm volatile("cbi 0x0b, 0x02"); break;
				case  3: asm volatile("cbi 0x0b, 0x03"); break;
				case  4: asm volatile("cbi 0x0b, 0x04"); break;
				case  5: asm volatile("cbi 0x0b, 0x05"); break;
				case  6: asm volatile("cbi 0x0b, 0x06"); break;
				case  7: asm volatile("cbi 0x0b, 0x07"); break;
				// PORTB
				case  8: asm volatile("cbi 0x05, 0x00"); break;
				case  9: asm volatile("cbi 0x05, 0x01"); break;
				case 10: asm volatile("cbi 0x05, 0x02"); break;
				case 11: asm volatile("cbi 0x05, 0x03"); break;
				case 12: asm volatile("cbi 0x05, 0x04"); break;
				case 13: asm volatile("cbi 0x05, 0x05"); break;
				// PORTC
				case 14: asm volatile("cbi 0x08, 0x00"); break;
				case 15: asm volatile("cbi 0x08, 0x01"); break;
				case 16: asm volatile("cbi 0x08, 0x02"); break;
				case 17: asm volatile("cbi 0x08, 0x03"); break;
				case 18: asm volatile("cbi 0x08, 0x04"); break;
				case 19: asm volatile("cbi 0x08, 0x05"); break;
			}
		break;
		default:
		break;
	}
#else
	digitalWrite(pin, val);
#endif
}

_INLINE_ int fastestDigitalRead(uint8_t pin)
{
#if defined(__AVR_ATmega328P__)
	switch(pin) {
		// PIND
		case  0: return (PIND >> 0) & 1;
		case  1: return (PIND >> 1) & 1;
		case  2: return (PIND >> 2) & 1;
		case  3: return (PIND >> 3) & 1;
		case  4: return (PIND >> 4) & 1;
		case  5: return (PIND >> 5) & 1;
		case  6: return (PIND >> 6) & 1;
		case  7: return (PIND >> 7) & 1;
		// PINB
		case  8: return (PINB >> 0) & 1;
		case  9: return (PINB >> 1) & 1;
		case 10: return (PINB >> 2) & 1;
		case 11: return (PINB >> 3) & 1;
		case 12: return (PINB >> 4) & 1;
		case 13: return (PINB >> 5) & 1;
		// PINC
		case 14: return (PINC >> 0) & 1;
		case 15: return (PINC >> 1) & 1;
		case 16: return (PINC >> 2) & 1;
		case 17: return (PINC >> 3) & 1;
		case 18: return (PINC >> 4) & 1;
		case 19: return (PINC >> 5) & 1;
		default:
		return 0;
	}
#else
	return digitalRead(pin);
#endif
}

#endif  /* _HPP_FASTEST_DIGITAL_RW_HPP_ */

```

・なぜこんなにも高速化できるのか？
オリジナルのdigitalWriteは以下のような内容です。ご覧のとおりピン出力以外に何やらいろいろやっています。出力ピンを引数から決めるのに何度も配列を参照しますし、PWMや割込み状態を制御しています。Arduino開発者の設計思想なのでしょうが、個人的にはピン出力にしては複雑すぎます。

```c:writing_digital.c
void digitalWrite(uint8_t pin, uint8_t val)
{
	uint8_t timer = digitalPinToTimer(pin);
	uint8_t bit = digitalPinToBitMask(pin);
	uint8_t port = digitalPinToPort(pin);
	volatile uint8_t *out;

	if (port == NOT_A_PIN) return;

	// If the pin that support PWM output, we need to turn it off
	// before doing a digital write.
	if (timer != NOT_ON_TIMER) turnOffPWM(timer);

	out = portOutputRegister(port);

	uint8_t oldSREG = SREG;
	cli();
        // 隣接ピンの出力状態を変えないように工夫している
	if (val == LOW) {
		*out &= ~bit; // ピンを0にする処理
	} else {
		*out |= bit;  // ピンを1にする処理
	}

	SREG = oldSREG;
}
```

そこでこの処理の中からなくてもピン出力が正しくできる処理を捨てます。
そしてピン出力処理自体をC言語でなくインラインアセンブラで記述します。更に作成した関数自体をインライン指定して最速化を目指すことにします。
AVRのアセンブリ言語命令にはビットセット/ビットクリアの専用命令がありますから、これ1つだけで単一ピンの出力ができます。今回のfastestDigitalWriteではこのsbi/cbiを使用しました。
以下は上記のfastestDigitalWriteテストプログラムのアセンブリリストの抜萃(赤枠内はfor(;;) {...}部分に対応)です。
![asm_for.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/b1f8d6e0-0845-af71-3991-273a42af1cec.jpeg)

1つのfastestDigitalWrite関数呼び出しが1つのsbiまたはcbi命令にコンパイルされています(rjmpはループの頭に戻している)。また、fastestDigitalWrite関数はライン数も分岐数も多く一見メモリ喰いのようですが、ここまで最適化されるので実はオリジナルのditigalWriteより省メモリでもあります。単一ピンの制御でこれより速い方法はおそらくないと思います。

終。