2020年3月29日日曜日

Arduino(atmega328p)の反応速度を調べてみた



背景

Arduinoとは電子工作をするときに便利な開発ボードです。
Arduinoで外部割り込み(信号を受信してから応答するまで)と、それに従って処理を行うのにどれくらいの時間がかかるか気になったので調べてみました。
備忘録を兼ねて内容を共有します。

なお、先に要約を共有すると、この記事で分かるのは下記の内容です。
  • 設定した信号線の状態が変わってからattachInterruptで登録した関数が呼ばれるのに基本約3us、時々約7usかかる。
  • digitalReadは約3.4us、digitalWriteは約5.4usかかる。
  • digitalReadとdigitalWriteの処理は、ポートレジスタを使う処理に置き換えれば約0.14usでできる。
  • 関数呼び出しは約0.4usかかる。
(上記の時間はatmega328pを16MHzで動かしたときの情報です。)

使ったもの


回路

このように接続しました。
役割 Arduino Nano Analog Discovery2
GND GND GND
定期信号パルス D3 W1 (20Hzの信号を供給。W1を2+に、2-をGNDに接続)
出力 D5 1+ (1-はGNDに接続)
設定 D6 未接続かGND

Arduino Nanoに使われているatmega328pで割り込み処理を設定できるのはD2かD3なので、今回はD3でパルスを受信して割り込み処理を設定します。

繋ぐとこうなりました。


行う処理

下記動作の処理速度を見ます。
  1. パルス受信で割り込み処理を開始
  2. 出力ピンをON・OFF
  3. 設定ピンの状態を読み取る
  4. 設定ピンがHIGH(未接続)だったら2回、LOWなら1回出力ピンをON・OFF

Analog Discoveryの設定


WavegenでW1から20Hzのパルス波を出します。


1番ピンでパルス波を、2番ピンでArduinoからの信号を見ます。


digitalReadとdigitalWriteを利用したプログラム

割り込み処理の設定はattachInterrupt関数を利用します。
上記の関数とdigitalReadやdigitalWriteを組み合わせて、下記のプログラムを動かしてみます。

#define PIN_PULSE_IN 3
#define PIN_PULSE_OUT 5
#define PIN_CONFIG 6

void createPulse() {
  digitalWrite(PIN_PULSE_OUT, HIGH);
  digitalWrite(PIN_PULSE_OUT, LOW);
}

void onRising() {
  createPulse();
  if (digitalRead(PIN_CONFIG) == HIGH) {
    createPulse();
  }
  createPulse();
}

void setup() {
  pinMode(PIN_PULSE_IN, INPUT);
  pinMode(PIN_PULSE_OUT, OUTPUT);
  pinMode(PIN_CONFIG, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_PULSE_IN), onRising, RISING);
}

void loop() {
  delay(1000);
}

上記のプログラムで生成される波形を観測するとこのようになりました。


パルスの電位が高くなったときを起点とする約39usの処理で、このような各処理が行われいているのだと推測します。
  • digitarlWriteが呼ばれてからGPIOのピンの電位が変わる時間: 約5.4us
  • digiralReadでピンの状態を判別する時間: 約3.4us
  • パルスの電位が変わってからonRising関数が呼ばれるまでの時間: 約2.6us

Arduino Nanoは16MHzで動いているので1クロック67nsです。
digitalWriteが5.4usだとすると、80クロックくらいかかっていることになります。

ポートレジスタを参照・変更するプログラム

Arduino Nanoに使われているatmega328pというチップはポートという概念があり、GPIOピンはそのポートに割り当てられています。
Arduinoではポートレジスタを参照・変更する仕組みが準備されているので、それを利用して信号のON・OFFと、GPIOピンの電位読み取りを行ってみます。

Arduino Nanoの回路図(PDF)を見ると、信号出力に利用しているD5はPORT Dの5、D6はPORT Dの6だと分かります。


D5(PORT Dの5)を使っているPIN_PULSE_OUTの切替処理
digitalWrite(PIN_PULSE_OUT, HIGH);
digitalWrite(PIN_PULSE_OUT, LOW);

PORTD |= 0b00100000;
PORTD &= 0b11011111;
に書き換えられます。

D6(PORT Dの6)を使っているPIN_CONFIGの読み取り処理
digitalRead(PIN_CONFIG) == HIGH 

PIND & 0b01000000 != 0
に書き換えられます。

ということで、このようなプログラムを動かしてみます。

#define PIN_PULSE_IN 3
#define PIN_PULSE_OUT 5
#define PIN_CONFIG 6

void createPulse() {
  // digitalWrite(PIN_PULSE_OUT, HIGH);
  // digitalWrite(PIN_PULSE_OUT, LOW);
  PORTD |= 0b00100000;
  PORTD &= 0b11011111;
}

void onRising() {
  createPulse();
  // if (digitalRead(PIN_CONFIG) == HIGH) {
  if (PIND & 0b01000000 != 0) {
    createPulse();
  }
  createPulse();
}

void setup() {
  pinMode(PIN_PULSE_IN, INPUT);
  pinMode(PIN_PULSE_OUT, OUTPUT);
  pinMode(PIN_CONFIG, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_PULSE_IN), onRising, RISING);
}

void loop() {
  delay(1000);
}

このような波形になりました。


パルスの電位が高くなったときを起点にすると5.1usで処理を終えているので、digitalReadとdigitalWriteを使ったときの約7倍速くなっています。
波の間隔をプログラムと参照すると、各処理の時間はこのようになっていると推測できます。
  • PORTDへの書き込みとPINDの参照時間:  約0.14us(140ns)
  • onRisingの処理が始まるまでの時間: 約2.9us
  • createPulse関数の処理が始まるまでの時間: 約0.4us

PORTDの書き込みが約140nsとなり、16MHzが1クロック67nsなので、2クロックでポートの電位切り替えられるようになりました。
ポートの切替が早くなった分、今度は関数呼び出しと割り込みの起動時間が処理全体の半分以上を占めるようになりました。

全体の時間が短くなったことにより、処理の終了が約5.0usから5.1usでぶれつつ、たまに約7.8usになることがありました。
ピンの電位変化による割り込みより優先度の高い割り込みと競合した場合に、処理の待ち時間が3usほど発生することがあるものと推測します。

関数はなるべく使わず処理を記述

出力のON・OFFを関数で呼び出せるようにしていましたが、関数呼び出しに0.4usほどかかるので、関数は使わず記述してみました。

#define PIN_PULSE_IN 3
#define PIN_PULSE_OUT 5
#define PIN_CONFIG 6

void onRising() {
  PORTD |= 0b00100000;
  PORTD &= 0b11011111;
  if (PIND & 0b01000000 != 0) {
    PORTD |= 0b00100000;
    PORTD &= 0b11011111;
  }
  PORTD |= 0b00100000;
  PORTD &= 0b11011111;
}

void setup() {
  pinMode(PIN_PULSE_IN, INPUT);
  pinMode(PIN_PULSE_OUT, OUTPUT);
  pinMode(PIN_CONFIG, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_PULSE_IN), onRising, RISING);
}

void loop() {
  delay(1000);
}

これにより、プログラムは冗長になりましたが、パルスの電位が高くなってから他の処理と競合しなければ約4usで処理を終えられるようになりました。


調べたものの効果が無かったこと

avg_gccのビルド最適化設定: 標準で最高

avr_gccは-Oで最適化設定ができます。
しかし、platformioを-vを付けてコマンドを表示しながら実行してみると、avr_gccは既に最高に最適化を行うsが-Oとして設定された上で実行されていることが分かりました。


arrachInterruptの優先度設定: 無さそう

Arduino Nanoのビルドに使われるAVRCoreのattachInterruptには優先度を帰るような設定は見当たらず、外部割り込み設定(EICRA)についてマイコン(atmega328p)のデータシートを見てみても、それに関する優先度の設定は無さそうでした。

おわり

割り込み処理(attachInterrupt)、GPIOの操作(digtalRead、digitalWrite)について、これらのことが分かりました。

  • 設定した信号線の状態が変わってからattachInterruptで登録した関数が呼ばれるのに基本約3us、時々約7usかかる。
  • digitalReadは約3.4us、digitalWriteは約5.4usかかる。
  • digitalReadとdigitalWriteの処理は、ポートレジスタを使う処理に置き換えれば約0.14usでできる。
  • 関数呼び出しは約0.4usかかる。
(上記の時間はatmega328pを16MHzで動かしたときの情報です。)

ポートレジスタを使えばGPIOの状態の参照・変更は数クロックで行なえましたが、attachInterruptによる外部割り込みは約45クロック(3us)より短くできなさそうでした。(1us以下にしたかったです。)
割り込み処理の開始までの時間を短くする方法をご存知でしたら、コメントなどで共有していただけると嬉しいです。


後日ピックマイコンの反応速度も確認してみました。
良かったらこちらもどうぞ。

ピックマイコンPIC16F1829の反応速度を調べてみた

2 件のコメント :

Unknown さんのコメント...

Arduino の割り込み処理の反応速度について非常に参考になりました。
ここまで速い処理ができるのですね。

いちいち細かいことで申し訳ありませんが、
周波数を表す単位記号はHzです。hzでは有りません。

以上よろしくお願いします。

Asuki Kono さんのコメント...

コメントありがとうございます。
参考になって良かったです。

> 周波数を表す単位記号はHzです。hzでは有りません。
指摘ありがとうございます。
2箇所間違えていましたので修正しました。