ArduinoのdigitalWrite関数やdigitalRead関数が遅いのはよく知られた事実です。特に、8ビットで低速のArduino Unoの場合、遅さの影響が際立ってきます。
現在、Arduino Uno用の、digitalWrite関数とdigitalRead関数を高速化するためのライブラリを試作していますので、中間的な成果を報告します。
なお、製作中のライブラリは、将来的には他のArduino(dueなど)にも対応する予定です。
2019年8月7日追記:
この記事で紹介しているGPIOの高速化ライブラリは、FASTIOライブラリ(1)というページで配布しています。
サンプルスケッチ
とりあえず、サンプルという事で、次の2つのファイルをコンパイルして動かしてみてください。
まず、メインファイルの”test.ino”です。
// test.ino
#include "fastio.h"
void setup()
{
Serial.begin(9600);
pinMode( 7,INPUT_PULLUP);
pinMode(13,OUTPUT);
} // setup
void loop()
{
uint32_t StartTime, EndTime;
StartTime=millis();
for(int i=0; i<10000; i++) {
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
digitalWrite(13,digitalRead(7));
} // for i;
EndTime=millis();
Serial.print(F("Normal:"));
Serial.println(EndTime-StartTime);
StartTime=millis();
for(int i=0; i<10000; i++) {
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
digitalWrite<13>(digitalRead<7>());
} // for i;
EndTime=millis();
Serial.print(F("Enhanced:"));
Serial.println(EndTime-StartTime);
} // loop
次に、digitalWrite関数とdigitalRead関数の高速化に必要なヘッダファイル、”fastio.h”です。
// fastio.h
//
// This hedder file is a test file for FastIO library.
// This file is distributed at https://synapse.kyoto
#ifndef __FASTIO_H__
#define __FASTIO_H__
#include
#if(__cplusplus<201103L)
#error This program requires C++11. Use newer ARDUINO IDE.
#endif
#if defined(__AVR_ATmega48__)|| defined(__AVR_ATmega48P__) || defined(__AVR_ATmega88__) || defined(__AVR_ATmega88P__)|| defined(__AVR_ATmega168__)|| defined(__AVR_ATmega168P__) || defined(__AVR_ATmega328__) || defined(__AVR_ATmega328P__)
#define ARDUINO_STANDARD
#endif
#ifndef ARDUINO_STANDARD
#error this program is only for standard Arduino.
#endif
unsigned int constexpr pin_to_PORT_address(int pin);
unsigned int constexpr pin_to_PIN_address(int pin);
int constexpr pin_to_mask(int pin);
unsigned int constexpr pin_to_PORT_address(int pin)
{
return pin< 0 ? NULL :
pin< 8 ? (uint16_t)&PORTD :
pin<14 ? (uint16_t)&PORTB :
pin<20 ? (uint16_t)&PORTC :
NULL;
} // pin_to_PORT_address
unsigned int constexpr pin_to_PIN_address(int pin)
{
return pin< 0 ? NULL :
pin< 8 ? (uint16_t)&PIND :
pin<14 ? (uint16_t)&PINB :
pin<20 ? (uint16_t)&PINC :
NULL;
} // pin_to_PIN_address
int constexpr pin_to_mask(int pin)
{
return pin< 0 ? -1 :
pin< 8 ? _BV(pin) : // PORTD
pin<14 ? _BV(pin-8) : // PORTB
pin<20 ? _BV(pin-14) : // PORTC
-1;
} // pin_to_mask
template void digitalWrite(int value) __attribute__((always_inline));
template int digitalRead(void) __attribute__((always_inline));
template void digitalWrite(int value)
{
static_assert(pin_to_PORT_address(pin)!=NULL,"invalid pin number.");
volatile uint8_t &PORT=*(uint8_t *)pin_to_PORT_address(pin);
constexpr uint8_t mask=pin_to_mask(pin);
if(value==LOW) {
PORT&=~mask;
} else {
PORT|= mask;
} // if
} // digitalWrite;
template int digitalRead(void)
{
static_assert(pin_to_PIN_address(pin)!=NULL,"invalid pin number.");
volatile uint8_t &PIN=*(uint8_t *)pin_to_PIN_address(pin);
constexpr uint8_t mask=pin_to_mask(pin);
return (PIN&mask)!=LOW;
} // digitalRead
#endif
これら2つのファイルを、testという名前のフォルダの中に保存して、test.inoをArduino IDEで開いてコンパイルし、Arduino UNOに書き込むと動作します。
対応するArduino IDE
今回作ったプログラムは、C++11の機能を使っているので、古いArduino IDEでは動作しません。Arduino IDE 1.8系は大丈夫なはずです。Arduino IDE 1.0系や1.7系では動作しません。Arduino IDE 1.6系は、新しい物なら動作します。Arduino IDE 1.6.11で動作を確認しています。
サンプルスケッチでやっている事
サンプルスケッチでは、通常のdigitalWrite関数やdigitalRead関数の実行時間と、高速化したdigitalWrite関数やdigitalRead関数の実行時間を計測しています。
実行してシリアルモニタを開くと、次の様な画面になります。
例えば”Normal:783″と書いてあるのは、通常のdigitalWrite関数とdigitalRead関数を10万回ずつ実行するのにかかった時間の合計をms単位で表示したものです。つまり、digitalWrite関数1回と、digitalRead関数1回の実行に合計7.83μsかかった事になります。
一方で、”Enhanced:40″と書いてあるのは、高速化したdigitalWrite関数と、digitalRead関数を10万回ずつ実行するのにかかった時間の合計をms単位で表示しています。つまり、高速化したdigitalWrite関数1回と、高速化したdigitalRead関数1回の実行に合計0.40μsかかった事になります。
この結果を見ると、今回の高速化により19.5倍程度の高速化ができたといえそうです。
なお、サンプルスケッチでは7番ピンのプルアップ抵抗を有効にして、7番ピンから読み取った情報を13番ピンに出力する事を繰り返しています。13番ピンにはLEDが付いていますので、7番ピンをGNDに落とすかどうかでLEDの点灯状態が変わります。
高速化したdigitalWrite関数とdigitalRead関数の使い方
自分の作ったスケッチのdigitalWrite関数やdigitalRead関数を高速化したい場合は、まず、スケッチ(.inoファイル)があるのと同じフォルダにfastio.hをコピーしてください。
現在は試作段階なので、普通のライブラリと違い、fastio.hをスケッチと同じフォルダにコピーする必要があります。将来的には、通常のライブラリと同様に、”fastio.h”をインクルードするだけで使えるようにする予定です。
そしてスケッチの冒頭で
#include "fastio.h"
と、先ほどのヘッダファイルをインクルードします。
それから、digitalWrite関数とdigitalRead関数を少し書き換える必要があります。
例えば
digitalWrite(5,HIGH);
は、
digitalWrite<5>(HIGH);
と書き換える必要があります。
また
int i=digitalRead(8);
は、
int i=digitalRead<8>();
と書き換える必要があります。
制限事項
高速化したdigitalWrite関数とdigitalRead関数は、ピン番号が定数式でないと動作しません。
例えば、0~4番のピンをHIGHにしようとして、次の様にdigitalWrite関数のピンの指定に、変数を使うとエラーになります。
for(int i=0; i<5; i++) {
digitalWrite<i>(HIGH);
}
この場合は、通常のdigitalWrite関数を使うか、次の様に高速化したdigitalWrite関数を5つ並べる必要があります。
digitalWrite<0>(HIGH);
digitalWrite<1>(HIGH);
digitalWrite<2>(HIGH);
digitalWrite<3>(HIGH);
digitalWrite<4>(HIGH);
高速化のからくり
Arduinoは、ピンの指定を、普通のマイコンの様に、ポート名とビット番号で行いません。例えばPB5(PORTBの5ビット目)というピンは、Arduino Unoの場合、13番ピンと呼ばれます。
ピンを0から連続した1つの数字で表すArduinoの流儀は、ユーザーにマイコンの細かい仕組みを意識させる事がありませんし、別の種類のマイコンが載ったArduinoにプログラムを移植する際にも役立ちます。
一方で、Arduinoのソフトにとってみれば、ピンの番号を指定されても、本当にGPIOの制御に必要なのはポート名とビット番号なので、困るわけです。そこで、ピン番号とポート番号およびビット番号を対応付ける表を、Arduinoはフラッシュメモリ中に持っており、digitalWrite関数やdigitalRead関数が呼び出される度に、その表を引いているので、実行が遅くなるのです。(他にも実行が遅くなる要因はありますが、ここでは深入りしません)
今回私が作ったプログラムは、コンパイル時にポート名とビット番号の対応表をコンパイラに引かせる工夫をしてあります。その分、実行時に無駄な作業が省けて高速になりますが、逆にピン番号を変数で指定されると、コンパイル時に表が引けず、エラーになるのです。
よく、Arduino UnoのdigitalWriteをポートの直叩きで高速化する方法として、次の様なコードを書く方法が紹介されています。
PORTB|=_BV(5);
このコードからはポートBの5ビット目を1にしている事は読み取れますが、13番ピンをHIGHにしている事を読み取るには、ピン番号とポート名およびビット番号の対応表が必要です。
それを
digitalWrite<13>(HIGH);
と書けば、表を引くのはコンパイラがやってくれますので、実行時間は変わらずに可読性が向上します。
ついでにいうと、
digitalWrite<133>(HIGH);
の様に、実在しないピン番号を指定すると、コンパイル時にピン番号が不正である事がチェックされて、エラーになります。通常のdigitalWrite関数を使えば、実行しても動かない事に気が付いた時点で問題が発覚するでしょう。