Arduino core の実装メモ

備忘のため、以下の Arduino のメソッド(と呼ぶのかな?)がおおまかにどんなことをしているかメモっておきます。
高速化のために調べたので、パフォーマンス観点からの文章になっています。

  • pinMode()
  • digitalWrite()
  • analogRead()
  • tone()

pinMode

まずは書き やすいのから。

pinMode は、I/Oピンの入出力モードを切り替えるためのメソッドです。これぐらいじゃ重くならないだろうとつい思っちゃいますが、そうはいきません。

pinMode のシンタックスは、

pinMode(pin, mode)

ですが、ピン番号というのは Arduino の仕様であって、AVR には理解できない番号です。
理解できるのは、ポート種別 (PORTB とか PORTD とか) とビット番号です。

てことで、Arduino の中でピン番号からポートレジスタとビット番号への解決が行われてます。
なんとこれ、pinMode() 実行のたびに解決するのですね。
まあ、キャッシュを持つと、どこにキャッシュを置いておくんだ、という問題が出ちゃいますから、pinMode の設計としてはそれで良いと思うのですがやはり実行が遅くはなります。

pinMode 関数は小さいものなのでここに載せてみます。


void pinMode(uint8_t pin, uint8_t mode)
{
        uint8_t bit = digitalPinToBitMask(pin);
        uint8_t port = digitalPinToPort(pin);
        volatile uint8_t *reg;

        if (port == NOT_A_PIN) return;

        // JWS: can I let the optimizer do this?
        reg = portModeRegister(port);

        if (mode == INPUT) *reg &= ~bit;
        else *reg |= bit;
}


ピン番号を解決するのは最初の二行です。あまり深く潜っていないのですが
digitalPinToBitMask と digitalPinToPort が使うマッピング情報は、フラッシュメモリに書き込んであるようです。
これなら Arduino に新しいラインナップが追加されても新しいマップを追加するだけで、気にせず今までどおり pinMode で行けちゃうわけです。よくできてるなあ。
ちなみに、ポートレジスタへのマッピングは、一旦 port 番号にマップしてからさらに reg にマップしなおしています。なぜこうなっているかはまだ理解していません。

さらにオーバーヘッドという観点からは、ありえないピン番号を指定されたときの用心のために、マップした port 番号をチェックするという処理も入っています。

モード切替の処理自体は最後の二行だけです。かなりオーバーヘッドの大きな関数だということがわかります。
キャッシュの置き場所があるならばキャッシュを使えば速くなります。が、そのときには今度はメモリの使いすぎに注意しなくてはいけないかもしれません。
あと、ピンの用途がはっきりしていて変更される心配がないハードウェア向けのソフトを書く場合は、不正マップの確認を実行時に行う必要もありません。

digitalWrite

これも短いから一気に載せちゃいます。

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);

        if (val == LOW) *out &= ~bit;
        else *out |= bit;
}

ピンからポート&ビットへのマップまでは pinMode と同じです。

そして、目的のピンが PWM 出力だったら PWM 出力を止める処理も入っています。
これもハードウェア固定で、きちんとデバッグされていれば必要ない不正チェックです。

ということで、このルーチンもまた実質的な処理は最後の二行だけなのでありました。

digitalWrite は大雑把に呼んでもちゃんと動く大変便利な、いってみればこの関数こそが Arduino を使いやすくしているのだと思いますが、
パフォーマンスについては改善の余地がたっぷりあります。

analogRead

不要なコメントや本題と関係ない部分を削除すると analogRead() 関数もかなり短いです。以下、流れを壊さずに analogRead から抜粋です。

int analogRead(uint8_t pin)
{
        uint8_t low, high;

        // set the analog reference (high two bits of ADMUX) and select the
        // channel (low 4 bits).  this also sets ADLAR (left-adjust result)
        // to 0 (the default).
        ADMUX = (analog_reference << 6) | (pin & 0x07);

       // start the conversion
        sbi(ADCSRA, ADSC);

        // ADSC is cleared when the conversion finishes
        while (bit_is_set(ADCSRA, ADSC));

        // we have to read ADCL first; doing so locks both ADCL
        // and ADCH until ADCH is read.  reading ADCL second would
        // cause the results of each conversion to be discarded,
        // as ADCL and ADCH would be locked when it completed.
        low = ADCL;
        high = ADCH;

        // combine the two bytes
        return (high << 8) | low;
}


ここでも動的な ピン番号→ADC のマッピングがありますが、ここは実は簡単には外れないかもしれません。
データシートによると、ATMega*8 の ADC モジュールは実は一台しかなく、アナログ入力ピンは8本ありますが、一度に一個ずつ切り替えて使うようです。
ということで、マッピングを固定できるのは、ADCピンを一本だけしか使わない時、ということで、ここを外せる状況はかなり限られていそうです。

マッピングはこの切り替えのための処理で、省くことはできません。
フラッシュメモリも使っていません。マッピングの規則が単純なので、非常に簡単な演算でマッピングできてしまうようです。

でもこの関数、遅いという噂をききます。(私自身が計測したわけではありませんが)
なんででしょう?

つらつらーっと処理の流れを見てみると、以下のようになっているようです。

  1. 使う ADC ピンと ADC モードを決定する
  2. ADCSRA レジスタの ADSC ビットをセットしてADC開始!
  3. ADCSRAレジスタの ADSC ビットをプロセッサがリセットされるのを待つ(プロセッサはADCを終えるとリセットする)
  4. 値を読み取る

一見、ひどく無駄な処理をしている部分はないように見えますが、問題があるとしたら、step3 の、待ち時間です。
ここで処理はブロックされるため、CPUが無駄に遊んでいます。
データシートによると、この読み取りにかかる時間は 25 ADC クロックとのこと。
ここをブロックしないで非同期の仕組みにすると、待ち時間に他の処理を入れることもできますが、そうするとこんどは ADC ピン同士の競合管理をしなくてはいけません。
設計がちょっと複雑にはなってしまいますが、おそらくここもパフォーマンス改善ポイントです。

tone

tone もとても便利なメソッドですが、やはりかなりオーバーヘッドが大きいです。ちょっと大きなルーチンなので、ここにソースコードは載せず、作りを簡単にまとめるとこんなです。

  • タイマー使う。基本 timer2 を使うのだが状況に応じて timer1 も使える。
  • 与えられた周波数から、使うタイマーにかんがみてタイマークロックの適切なプリスケールを決定する。
  • ピン番号を解決
  • タイマ割り込みごとに出力ピンを反転する(矩形波になる)

なにいっているかわからん、ソースコードはどうなっているんだ、というむきにはこちら

とても汎用性が高いのですが、その分オーバーヘッドは大きいです。上記の処理から用途に応じて汎用的な部分を限定してゆくと速くなります。

Arduino core の実装メモ」への11件のフィードバック

  1. 素人だけど

    Arduinoの情報を集めていて、たまたま見たんですが、、、
    馬鹿の一つ覚えみたいにパフォーマンス、パフォーマンスっていうならArduinoなんて使わずにATMEL用のコンパイラーで書けばいいじゃないですか。
    それだけの実力はない、と。
    Arduinoのソースに文句いって、俺は誰よりも賢いっていいたいんですか?なんか読んでいて、専門バカ丸出しに見えたので思わずコメント

  2. 参考になります

    大変参考になります。
    心無い雑音など気にせず、頑張ってください。

  3. Gan 投稿作成者

    どうもありがとうございます。
    罵倒を浴びることもあり、こうして励ましていただくこともあり、とにかく記事を読んでいただけるのはありがたいことです。

  4. Sky

    解りやすい解説に感謝しています。
    高速化に興味があり、大変参考になっています。

    記事を参考に調べてみましたが、わからない点がありました。
    よろしければアドバイスをお願い致します。
    質問の表現に間違いがあったらおゆるしください。

    例:pinMode 関数のソースコード:void pinMode(uint8_t pin, uint8_t mode) は、
    どこにあるのでしょうか?

    Arduino_1.0.1_rc2 の場合は、以下でよろしいでしょうか?
    Contents>Resources>Java>hardware>arduino>cores>arduino>wiring_digital.c

    ソースコードは、以下でよろしいでしょうか?
    http://code.google.com/p/arduino/source/browse/trunk/hardware/arduino/cores/arduino/wiring_digital.c

    デジタルピンの読み込みは、高速化の為に、digitalRead() でなく、PIND とかを使用しています。
    Arduinoの性能の中で、パフォーマンスを上げる記事は楽しいです。

    この記事の次を期待しています。

  5. Gan 投稿作成者

    Sky さん

    ソースコードは code.google.com のところを私も参照しています。今後記事を書くときにはソースコードの参照先も載せるようにしますね。
    今の版はこの記事と若干内容が変わっていますね。r977 が記事のものと同じようです。変更後も記事の内容は生きているようです。

    analogRead のほうは、
    http://code.google.com/p/arduino/source/browse/trunk/hardware/arduino/cores/arduino/wiring_analog.c です。

    Arduino でもレジスタを直接制御すれば普通に高速なコードがかけますがその分移植性が落ちたり、ライブラリとの競合が起こったりとちょっとコツがいりますね。そういったことを工夫するのも楽しみの部分の一つかもしれませんね。

  6. inouetaichi

    素人だけどさんが居らっしゃらなかったらあえてコメントは残さなかったかもしれませんが
    内部処理についてピックアップしてwebにのせてくださり、思いがけずgoogleの検索結果に表示され
    問題解決の突破口になりました。ありがとうございました。

  7. Gan 投稿作成者

    お役に立ててうれしいです。コメントありがとうございます。

  8. tossiy

    Ganさん。
    非常に興味深く読ませて頂いておりますが、1つ質問でございます。
    Arduino-dueを使用しておりますが、Digital-I/Oピンをしようする上で、先ずI/Oの初期化はどうやって行えば宜しいのでしょうか?現在HPから様々なソースを頂いてそれで動作確認中ですが、個々のそういったサンプルソースでのプラットフォームは簡単で良いのですが、I/Oマップやメモリマップをどのように作成すれば良いのか解らないところです。どうぞご教授下さいませ。

  9. Gan 投稿作成者

    I/O ポートの初期化とは pinMode() に相当する操作をさしているでしょうか?
    初期値を設定したいなら digitalWrite() に相当する操作ですね。

    pinMode() と同等の操作を Arduino の関数を使わずに行う場合、
    – 入力モードにするなら何もする必要がありません。
    – 出力モードにする場合、
    1. Arduino のI/O ポートがプロセッサのどのI/Oピンにあたるか回路図から調べる
    due の回路図ここにありました http://arduino.cc/en/uploads/Main/arduino-Due-schematic.pdf
    2. そのピンにあたる DDR (Data Direction Register) ビットを 1 にする
    たとえば PD1 を変えたい場合

    DDRD |= _BV(DDD1)

    のようにすればよいです。詳しくはプロセッサのデータシートを見てください。

    値を設定するには例えば PORTD1 の場合

    PORTD |= _BV(PORTD1); // H にする場合
    PORTD &= ~_BV(PORTD1); // L にする場合

    これも詳しくはデータシートをみてください

  10. kou

    質問させてください。
    AtmelStudioで、Arduino開発はできるのでしょうか?
    AtmelStudio内で、328p用にライブラリーを軽量なものにかい改良するとか、クロック変更したりできればいいなとおもっております。

  11. Gan 投稿作成者

    kou さん:
    独自のライブラリの開発やクロック変更などの AVR レジスタの直接操作などは Arduino ソフトウェアを使ってもできますが(自前ライブラリ開発は Arduino がサポートしていますし、レジスタ操作などはスケッチに直接埋め込んで大丈夫です)、最近は Atmel Studio でも Arduino 対応しているようです。
    http://playground.arduino.cc/Main/DevelopmentTools

    やりたいことに対してどこをどうすれば良いのかは、このページの説明で感覚をつかめるんじゃないかと思います。
    http://playground.arduino.cc/Main/CustomizeArduinoIDE

    私はやったことがありませんが、Arduino のコアのソフトウェアを改造する場合には、Arduino ソフトウェアを改造することになるようです。興味があれば、以下のページあたりが良い出発点になるのではないでしょうか。
    http://arduino.cc/en/main/software
    https://github.com/arduino/Arduino

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください