レジスタ操作によるdigitalWriteの最適化: Arduino UNOでLEDを点滅させる高速化テクニック

UNO-ATmega328

digitalWrite()を小さく軽くしたい思い

Arduinoの標準関数で用意されている誰もが必ず使う digitalWrite は簡単で汎用性がありとても使いやすい関数ですが、どのボード・CPUででも使えるように、関数内には多くのオーバーヘッドが含まれています。そのため、実行速度はAVRのレジスタ制御で直接ポートをたたいて制御するより遅く、またその実行速度は遅いものになります。

ただ、先にも書きましたように、汎用性があるため、ArduinoのボードやCPUが変わってもソースコードはそのまま流用できるため、移植性もよくとても便利です。ただArduinoの標準関数を多様化すると、あっという間にプログラム領域を圧迫して、実際にプログラムが作り切れなくなる場合もよく遭遇します。

このような場合、CPUが完全に決まっており、プログラムサイズをできるだけ小さくしたい場合にはdigitalWriteよりはるかにAVRレジスターをたたく方が有効的なため、ここではその実験を行い、ちょっと使える汎用的な関数を作ってみたいと思います。

※ただし、CPUはArduinoUNOに使われているATmega328Pに限ってのコードとします。ほかのCPUをお使いの皆様は、このコードを参考にCPUに合わせて書き換えてお使いいただければと思います。

digitalWriteを使ったLチカ

ArduinoUNOの13番に装備されているLEDをチカチカとさせる簡単なプログラムを書いてみます。これは誰でもがまず最初に作って試すプログラムではないでしょうか?

void setup() {
  pinMode(13,OUTPUT);
}
void loop() {
    digitalWrite(13,HIGH);
    delay(100);
    digitalWrite(13,LOW);
    delay(100);
}

このコードをコンパイルしたサイズは下記のとおりです。


※最大32256バイトのフラッシュメモリのうち、スケッチが922バイト(2%)を使っています。と表示されています。

AVRモードで制御する関数を考えてみる

レジスタを直接たたいて書くコードはよく見かけますが、ここでは少し汎用的に使いたいため、digitalWriteと同じ形式で使える avr_digitalWrite を全く同じ引数の形式で作って置き換えてみたいと思います。

関数仕様

関数形式 :void avr_digitalWrite(uint8_t pin, uint8_t level)
引数:uint8_t pin….ピン番号  level….HIGH | LOW
コード:

#include <avr/io.h>

void avr_digitalWrite(uint8_t pin, uint8_t level) {
  uint8_t bit = digitalPinToBitMask(pin); // ピンに対応するビットマスクを取得    
  volatile uint8_t  *port = portOutputRegister(digitalPinToPort(pin)); // ピンに対応するポートのアドレス    
  if (port == NOT_A_PIN) return; // ピンが無効な場合は何もしない    
  if (level == HIGH) {        
    *port |= bit; // 対応するビットをセットしてピンをHIGHにする    
  } else {
    *port &= ~bit; // 対応するビットをクリアしてピンをLOWにする    
  }
}

このコード例では、digitalPinToBitMask()とportOutputRegister()というArduinoの内部関数を使用して、与えられたピン番号に対応するビットマスクとポートのアドレスを動的に取得しています。これにより、ATmega328Pベースのボードであれば、任意のデジタルピンに対して直接ポート操作を行うことができます。

注意点

  • このコードはATmega328PベースのArduinoボードに限定されます。他のモデルやマイクロコントローラには対応していません。
  • digitalPinToBitMask()digitalPinToPort()およびportOutputRegister()関数はArduinoの内部実装に依存するため、これらの関数についての詳細なドキュメントは公式なものが少ないです。これらの関数はArduinoのソースコードを通じて理解することができます。

使用した内部関数について

digitalPinToBitMask()は、Arduinoで使われる関数で、指定されたデジタルピンに対応するビットマスクを生成します。このビットマスクは、ピンの状態を操作する際に、特定のビットだけを効率的に操作するために使用されます。たとえば、デジタルピンがポートのどのビットに対応しているかを示すマスクを返します。例えば、デジタルピン8がポートBのビット0に対応している場合、この関数は0x01(バイナリで00000001)を返します。このマスクは、ポートレジスタに対するビット操作(セット、クリア、トグル)で直接使用することができます。

digitalPinToPort()は、Arduinoの内部関数で、指定されたデジタルピンが属するポートを特定するために使用されます。この関数は、デジタルピン番号を入力として受け取り、そのピンが接続されているマイクロコントローラのポートを識別するための参照(通常はマクロ定数)を返します。例えば、Arduino Unoのピン番号13はポートBに属しているため、この関数はPORTBを返します。この情報は、直接ポート操作を行う際に非常に重要です。

portOutputRegister() は、Arduinoで利用されるマクロで、指定されたデジタルポートの出力レジスタへのポインタを返します。この関数は、デジタルピンが属するポートを入力として受け取り、そのポートの出力データレジスタのアドレスを返します。このアドレスを用いて、プログラムから直接ポートのピンの出力状態を制御することが可能です。これにより、digitalWrite()よりも高速にピンの状態を変更することができます。

Lチカをさせるコード

この関数を利用し13番ピンのLEDを点滅させるプログラムを作ってみました。

#include <avr/io.h>
void avr_digitalWrite(uint8_t pin, uint8_t level){ 
  uint8_t bit = digitalPinToBitMask(pin); // ピンに対応するビットマスクを取得 
  volatile uint8_t *port = portOutputRegister(digitalPinToPort(pin)); // ピンに対応するポートのアドレスを取得 
  if (port == NOT_A_PIN) return; // ピンが無効な場合は何もしない 
  if (level == HIGH) { 
    *port |= bit; // 対応するビットをセットしてピンをHIGHにする 
  } else { 
    *port &= ~bit; // 対応するビットをクリアしてピンをLOWにする 
  } 
} 
void setup() { 
  pinMode(13, OUTPUT); // 例としてピン13を出力として設定 
} 
void loop() { 
  avr_digitalWrite(13, HIGH); // ピン13をHIGHに 
  delay(100); 
  avr_digitalWrite(13, LOW); // ピン13をLOWに 
  delay(100); 
}

このコードをコンパイルした結果は下記の通りです。

最大32256バイトのフラッシュメモリのうち、スケッチが826バイト(2%)を使っています。ということで先ほどの922バイトから96バイト削減できました。

そしてなによりこのコードは、 digitalWriteを使った関数より早くてメモリーの消費量も小さいく、直接AVRレジスタに書き込むことでdigitalWrite()関数をバイパスすることは、実行速度を向上させ、メモリ消費量を削減する可能性があります。

なぜ速度アップとサイズダウンが見込めるのか

作ってみたところでもう一度おさらいをしておきます。

速度アップが見込める理由

  • 直接レジスタ操作digitalWrite()関数は、ピン番号をポートとビットにマッピングし、そのピンが出力か入力か、さらにはHIGHかLOWかを決定するためにいくつかのチェックを内部で実行します。これらの追加チェックは便利ですが、それぞれのdigitalWrite()呼び出しにおいてわずかな遅延を引き起こします。一方、レジスタを直接操作する場合、これらのチェックをすべてバイパスするため、ピンの状態変更がより迅速になります。
  • 関数呼び出しのオーバーヘッド削減digitalWrite()を使用すると、関数呼び出しのオーバーヘッドが発生します。直接レジスタ操作ではこのオーバーヘッドがないため、特に多数のピン操作を迅速に行う必要がある場合に性能が向上します。

サイズダウンが見込める理由

  • コードサイズの削減digitalWrite()関数は多くの機能を提供しますが、それには多くのコードが必要です。直接レジスタ操作を行う場合、その操作に必要なコードはごくわずかであり、結果として生成されるプログラムのサイズが小さくなります。これは、特にプログラムメモリ(フラッシュメモリ)が限られているマイクロコントローラでは有利です。
  • 実行時メモリの節約:直接レジスタ操作により、実行時に消費されるRAMの量も最小限に抑えられます。これは、digitalWrite()関数の内部で使用される一時変数やマッピングのためのデータ構造が不要になるためです。

使用にあたっての注意点

速度アップ・サイズダウンといいとこずくしですが、下記の点も注意しておく必要があります。

  • ポータビリティの低下:直接レジスタを操作することでポータビリティが低下します。つまり、特定のマイクロコントローラに特化したコードになり、他の種類のArduinoボードやマイクロコントローラにそのまま移植することができなくなります。
  • バグの可能性:レジスタを直接操作することで、誤ったレジスタへの書き込みや、意図しない副作用によるバグのリスクが高まります。正確な動作を理解し、慎重にコーディングする必要があります。

最後に

次回は機会を見つけて実行速度を比較してみたいと考えております。また、インラインアセンブルも可能なので、そちらにも腕を磨いて挑戦してみたいと考えています。

コメント

タイトルとURLをコピーしました