プログラミング言語の中の、CPUに最も近い言語はアセンブラです。
アセンブラは低級言語と呼ばれますが、CPUに近いため低級とよばれるのですが、言語への知識テクニックはとても高級(高度)が必要です。
今日はこのアセンブラ言語を使ってLチカをさせ、Arduinoの標準関数を使った場合とどのくらい差が出るのかを比較してみたいと思います。
ArduinoIDEでアセンブラを使う方法
Arduinoでは、CやC++を使ってプログラミングを行うことが基本ですが、ArduinoではこのIDEのC言語プログラムのコード中にアセンブラ言語を使うことができます。
この方法をインラインアセンブラと呼び、インラインアセンブラを使うことで、Arduinoプログラムにおいて、高い制御精度やパフォーマンスの向上が可能になります。特に、タイミングが厳しい操作や、マイクロコントローラの特定のハードウェア機能を利用する際に有用です。
以下はIDEにおいてCのソースコード中にアセンブラコードを埋め込んだイメージです。
具体的なお作法
Cソースコード内にアセンブラを埋め込むインラインアセンブラは、asm
キーワードを使用して記述します。シンプルな形式は以下の通りです。
asm("アセンブリ命令");
より複雑な形式では、出力、入力やレジスタなどを直接指定することができます。
asm volatile (
"アセンブリ命令"
: "出力オペランド" // 出力
: "入力オペランド" // 入力
: "レジスタ" // clobbered registers
);
キーワードや注意点など
- asm または __asm__で アセンブリセクションの開始を示します。
- volatileを使用して、コンパイラが最適化で命令を省略しないよう指示します。
例
以下は、Arduinoでピン13(ATmega328PのポートBの5ビット目)をHIGHに設定するインラインアセンブラの例です。
void setPinHigh() {
asm volatile (
"sbi %0, %1" :: "I" (_SFR_IO_ADDR(PORTB)), "I" (5)
);
}
このコードは、sbi(Set Bit in I/O Register)命令を使い、PORTBの5番目のビットをセットします。
インラインアセンブラでLチカをさせるコード
Arduino UnoのATmega328Pで13番ピンのLEDを点滅させるプログラムをインラインアセンブラを使用して記述してみました。
このコードはArduino Unoでしか動作しません。異なるボードで試したい場合には、LEDが接続されているピンのポートレジスタを適切なアドレスに変更してください。
サンプルプログラム
void setup() {
// ピン13を出力として設定
DDRB |= (1 << 5); // PB5 (ピン13) を出力に設定
}
void loop() {
// LEDをON
asm volatile (
"sbi %0, %1" "\n" // PORTBの5ビット目をセット
:
: "I" (_SFR_IO_ADDR(PORTB)), "I" (5)
);
delay(1000); // 1秒間待機
// LEDをOFF
asm volatile (
"cbi %0, %1" "\n" // PORTBの5ビット目をクリア
:
: "I" (_SFR_IO_ADDR(PORTB)), "I" (5)
);
delay(1000); // 1秒間待機
}
コンパイル結果
IDEのコンパイラでコンパイルした結果を見てみます。
コンパイル後のサイズは640バイトです。
Arduinoの標準関数を使って同じ動作をするプログラムを書いてみます。
void setup() {
pinMode(13,OUTPUT);
}
void loop() {
digitalWrite(13,HIGH);
delay(1000); // 1秒間待機
digitalWrite(13,LOW);
delay(1000); // 1秒間待機
}
コンパイル結果は924バイトなのでアセンブラコードの方が284バイトほど小さくなりました。
ちなみにSetup関数の pinMode をレジスタ直接に書き換えると826バイトなので98バイトだけ小さくなっています。
実行スピードを比較してみる
次に、10000回の点滅を、Arduinoの標準関数とアセンブラでの処理での実行速度を比較してみました。果たして本当に速いのでしょうか?
標準関数のコードとその実行速度
void setup() {
pinMode(13,OUTPUT);
Serial.begin(115200);
}
void loop() {
unsigned long tm;
for(int i=0;i<10000;i++){
digitalWrite(13,HIGH);
digitalWrite(13,LOW);
}
tm=millis();
Serial.print("time : ");
Serial.println(tm);
while(1){}
}
上記のコードを実行した結果は私のUNOにおいては何度かやってみましたが実行速度は67ミリ秒でした。
アセンブラコードとその結果
void setup() {
DDRB |= (1 << 5); // PB5 (ピン13) を出力に設定
Serial.begin(115200);
}
void loop() {
unsigned long tm;
for(int i=0;i<10000;i++){
// LEDをON
asm volatile (
"sbi %0, %1" "\n" // PORTBの5ビット目をセット
:
: "I" (_SFR_IO_ADDR(PORTB)), "I" (5)
);
// LEDをOFF
asm volatile (
"cbi %0, %1" "\n" // PORTBの5ビット目をクリア
:
: "I" (_SFR_IO_ADDR(PORTB)), "I" (5)
);
}
tm=millis();
Serial.print("time : ");
Serial.println(tm);
while(1){}
}
上記のコードを実行した結果は私のUNOにおいては何度かやってみましたがなんと4ミリ秒でした。
結果アセンブラコードでの実行速度はArduino標準関数の速度の1/10以下という恐るべき速度アップとなりました。
やはり、圧倒的にアセンブラの処理速度は速くてサイズも小さいものだということがわかりました。