Arduinoでデジタル新語の出力を行う標準関数として、digitalWriteが用意されていますが、この関数よりもっと直接的で高速な可能とする関数portOutputRegisterです。
portOutputRegister()の使い方と関数仕様
portOutputRegisterは、Arduinoおよび他のAVRベースのマイクロコントローラで使用されるマクロで、指定されたデジタルポートのデータ出力レジスタ(PORTレジスタ)のアドレスを返します。このマクロを使用すると、プログラムからポートに接続されているピンの出力状態(HIGHまたはLOW)を直接制御することができます。例えば、デジタルポートDに対してportOutputRegister(PORTD)を使用すると、PORTDレジスタへのポインタが得られ、そのポートの全ピンに対して一括で出力を設定することが可能になります。
仕様
- 説明: 指定されたポートの出力レジスタへのポインタを返します。
- 引数:
port
:ピンが属するポート。これは通常、digitalPinToPort(pin)
関数を使用して取得されます。 - 戻り値:指定されたポートのデータ出力レジスタに対するポインタ。
サンプルプログラム
Arduino Uno のデジタルピン 13(多くのボードで LED に接続されている)を制御するプログラムを標準関数と portOutputRegister を使用したもので比べてみます。処理速度も比べたいので、10000回出力した場合にかかった時間をmillis()で調べてみました。
portOutputRegisterを使った場合
#include<Arduino.h>
void setup() {
uint8_t pin = 13; // 多くのArduinoボードにおいてLEDが接続されているピン
volatile uint8_t *reg, *out;
reg = portModeRegister(digitalPinToPort(pin));
out = portOutputRegister(digitalPinToPort(pin));
uint8_t bit = digitalPinToBitMask(pin);
*reg |= bit; // ピンを出力として設定
*out |= bit; // ピンをHIGH(点灯)に設定
Serial.begin(115200);
}
void loop() {
uint8_t pin = 13;
volatile uint8_t *out;
unsigned long tc=0;
for(int i=0;i<10000;i++){
out = portOutputRegister(digitalPinToPort(pin));
}
tc=millis();
Serial.println(tc);
while(1){}
}
コンパイルサイズが1790バイト、実行速度は12ミリ秒でした。
解説
setup() 関数
uint8_t pin = 13;
ピン番号13を変数pin
に格納します。これは多くのArduinoボードでオンボードLEDに接続されているピンです。volatile uint8_t *reg, *out;
ポインタreg
とout
を宣言します。これらはそれぞれピンのモードと出力を制御するレジスタを指し示すために使用されます。reg = portModeRegister(digitalPinToPort(pin));
reg
ポインタにピンのモードレジスタのアドレスを格納します。digitalPinToPort()
関数でピンが属するポートを取得し、portModeRegister()
でそのポートのモードレジスタのアドレスを取得します。out = portOutputRegister(digitalPinToPort(pin));
out
ポインタにピンの出力レジスタのアドレスを格納します。これもdigitalPinToPort()
とportOutputRegister()
を使って取得します。uint8_t bit = digitalPinToBitMask(pin);
ピンに対応するビットマスクを取得します。このマスクは後でレジスタに対するビット操作で使用します。*reg |= bit;
ピンのモードを出力に設定します。ビットマスクを用いて、対応するビットだけを1にセットします。*out |= bit;
ピンをHIGHに設定し、接続されているLEDを点灯させます。Serial.begin(115200);
シリアル通信を115200 bpsで開始します。
loop() 関数
volatile uint8_t *out;
出力レジスタを指すためのポインタout
を再宣言します(スコープに注意)。unsigned long tc=0;
時間を記録するための変数tc
を宣言し、0で初期化します。- トグルループ:
for(int i=0; i<10000; i++)
ループ内でout = portOutputRegister(digitalPinToPort(pin));
を10000回呼び出します tc=millis();
トグル操作後の現在のミリ秒をtc
に記録します。Serial.println(tc);
記録した時間tc
をシリアルモニタに出力します。while(1){}
無限ループでプログラムを終了させます(これ以上のコード実行を停止)。
digitalWriteを使ってみる
#include<Arduino.h>
void setup() {
int pin = 13; // 多くのArduinoボードにおいてLEDが接続されているピン
pinMode(pin,OUTPUT);
digitalWrite(pin,HIGH)
; Serial.begin(115200);
}
void loop() {
int pin = 13;
int out = HIGH ;
unsigned long tc=0;
for(int i=0;i<10000;i++){
digitalWrite(pin,out);
}
tc=millis();
Serial.println(tc);
while(1){}
}
コンパイルサイズが1910バイト、実行速度は17ミリ秒でした。
比較結果
portOutputRegister | digitalWrite | |
サイズ | 1780 | 1910 |
処理速度 | 12ミリ秒 | 31ミリ秒 |
明らかにportOutputRegisterを使った方がサイズは小さく処理も早くなりました。
ATmega328PのポートとDDRについて
ATmega328Pは、以下の主要なI/Oポートを持っています:
- PORTB: ピン8から13(Arduino UNOのデジタルピン)
- PORTC: アナログピン0から5(Arduino UNOのアナログピン)
- PORTD: ピン0から7(Arduino UNOのデジタルピン)
注意点
- portOutputRegister関数はArduinoの標準ライブラリには含まれていませんが、Arduinoのソースコード内で定義されている場合があります。
- 直接ポート操作を行う際は、ハードウェアの誤操作に注意し、プログラムの移植性が低下することを意識する必要があります。また、間違ったポート操作によってハードウェアを損傷する可能性があるため、注意深く使用することが求められます。
- 複雑性の増加: 直接ポート操作は、通常のArduino関数を使うよりも複雑です。間違ったポートやビットに書き込むと、ハードウェアを損傷する可能性があります。
- ポータビリティの低下: プログラムのポータビリティが低下します。異なるマイクロコントローラやArduinoモデルにプログラムを移植する際に、ポートの設定を変更する必要があります。
- デバッグの困難さ: プログラムのバグを見つけることが難しくなる場合があります。特に、ポート操作に関連するバグは見つけにくいことが多いです。
メリット
直接ポート操作を行うことの主なメリットは、高速性と効率の向上です。portOutputRegister()
関数を利用することで得られる具体的な利点を以下にまとめました。
1. 高速性
- 即時のレジスタアクセス:
digitalWrite()
やpinMode()
のようなArduino標準の関数では、関数内部でピン番号を解析し、対応するポートやレジスタを決定するための計算が行われます。これに対して、portOutputRegister()
を用いた直接ポート操作では、事前にレジスタのアドレスを取得し、その後は直接そのアドレスにアクセスするため、余計な計算が不要になります。これにより、ピンの状態を変更する処理が非常に高速に行えるようになります。
2. 効率性
- 少ないリソース消費: マイクロコントローラのリソースは限られているため、プログラムのメモリ使用量を減らすことは重要です。
digitalWrite()
関数などは内部的に多くの処理を行うため、プログラムのフットプリントが大きくなりがちです。直接ポート操作を行うことで、これらの関数を回避し、プログラムサイズを小さく保つことができます。
3. 制御の精度
- 細かな制御: 直接ポート操作を行うことで、ビットレベルでピンの状態を制御することが可能になります。特定のアプリケーションにおいては、複数のピンの状態を同時に変更することが求められる場合があり、このような場合にも直接ポート操作が非常に有効です。