先日 [tech] Arduino の digitalWrite をコンパイルタイムに解決する | Tue, Dec 15. 2015 - 氾濫原 というのをコンセプト的にやってみたが割と良さそうな気がしたので、.hpp なライブラリにしてみた。

include してコンパイル時に -std=c++11 を指定すれば使える。

想定する使いかた

#include <Arduino.h>
#include "Arduino-meta.hpp"

constexpr uint8_t LED_RED = 0;
constexpr uint8_t LED_GREEN = 1;
constexpr uint8_t LED_BLUE = 2;

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);

    // static version of pinMode()
    pinMode(
        LED_RED, OUTPUT,
        LED_GREEN, OUTPUT,
        LED_BLUE, OUTPUT
    );
}

void loop() {
    // this is also static
    digitalWrite(LED_RED, HIGH, LED_GREEN, LOW, LED_BLUE, LOW);
    delay(1000);
    digitalWrite(LED_RED, LOW, LED_GREEN, HIGH, LED_BLUE, LOW);
    delay(1000);
    digitalWrite(LED_RED, LOW, LED_GREEN, LOW, LED_BLUE, HIGH);
    delay(1000);

    // original version of digitalWrite()
    digitalWriteDynamic(LED_BUILTIN, HIGH);
    delay(1000);
    digitalWriteDynamic(LED_BUILTIN, LOW);
    delay(1000);
}

デフォルトの digitalWrite / pinMode / digitalRead をマクロで置き換えるようにして、完全にインターフェイスを互換にしたので、基本的に include したらあとはほぼ気にする必要がない。

ただ、静的に解決できない引数を指定するとコンパイルエラーになるので、この場合は何も考えず Dynamic をサフィックスにして元の Arduino 組込み関数をよぶようにすればいい。

基本 include するだけで最適化されたコードが生成されるようになり、必要に応じて複数ピンの同時操作もできる。

そして引数がおかしい場合コンパイルエラーになってくれるので便利。

何も失うものはない。

実装

前のエントリに書いた通り、C++ の template を使ってコンパイル時にビットマスクとかを計算しているだけ。pinMode も面倒くさいけど実装したり、digitalWrite/digitalRead 前に timer 付きピンなら無効にするような処理を入れたり (互換性のため) した。

Arduino.h を include すると、PROGMEM 指定されている const なテーブルが見えるので、本当はそれを直接コンパイル時にも使いたいのだけれど、どうしても無理っぽいので、独自にマップをコピペして持っている。無駄感

最終的には使う側の見た目をあわせるためプリプロセッサマクロを使って、マクロ関数の引数をテンプレートパラメータとして渡している。

static constexpr void (*digitalWriteDynamic)(uint8_t, uint8_t)  = *digitalWrite;
#define digitalWrite(...) digitalWriteMulti<__VA_ARGS__>()
static constexpr void (*pinModeDynamic)(uint8_t, uint8_t)  = *pinMode;
#define pinMode(...) pinModeMulti<__VA_ARGS__>()
static constexpr int (*digitalReadDynamic)(uint8_t)  = *digitalRead;
#define digitalRead(pin) digitalReadX<pin>()

テスト

一応テストも書いてあって

test(<<-'EOS')
	void expected() {
		DDRD  = (DDRD  & 0b11111100) | (0b00000000);
		PORTD = (PORTD & 0b11111100) | (0b00000010);
	}

	void result() {
		pinMode(0, INPUT, 1, INPUT_PULLUP);
	}
EOS

こんな感じで、想定する展開コードとテンプレート関数とをコンパイルしたあと obj-dump して突き合せしている。これら2つの関数は全くバイナリ的に同一になることをテストしている。

備考

最初からテンプレートを駆使するつもりで AVR のライブラリを設計すればもっと良いものができそうだが、Arduino が覇権を握っているので、Arduino のインターフェイスにあわせるほうが嬉しい人が多いだろう。Arduino の基板上にはピン番号が書いてあるので、いちいちAVRのポート名とかとのマッピングを見ながら書くよりは、このほうが楽だ。

そしてテンプレート駆使しまくりすぎたライブラリは理解がつらいのが明かなので、このぐらいのショボいラッパぐらいがちょうどいいような気もする。一方でそれはそれで反知性的であるともいえそうだしなんともいえない。

  1. トップ
  2. tech
  3. Arduino の digitalWrite / pinMode / digitalRead をコンパイル時に展開する
▲ この日のエントリ