Arduino UNOでインラインアセンブラを使用してLEDを点滅させる

UNO-ATmega328

プログラミング言語の中の、CPUに最も近い言語はアセンブラです。
アセンブラは低級言語と呼ばれますが、CPUに近いため低級とよばれるのですが、言語への知識テクニックはとても高級(高度)が必要です。

今日はこのアセンブラ言語を使ってLチカをさせ、Arduinoの標準関数を使った場合とどのくらい差が出るのかをh隠してみたいと思います。

アセンブラについて

そもそもアセンブラ(Assembler)とは、プログラミング言語の一種で、特に低レベルのプログラミングに用いられる言語です。これを使って書かれたプログラムは、アセンブリ言語と呼ばれます。アセンブリ言語は、機械語(コンピュータのCPUが直接理解できる命令コード)に非常に近い形で命令を記述することができ、コンピュータのハードウェアを直接制御する詳細な操作が可能です。

主な特徴

  • ハードウェア指向: アセンブリ言語は、特定のCPUアーキテクチャに依存しており、そのプロセッサ固有の命令セットを直接使用することができます。
  • 効率的: アセンブリ言語は、コンピュータのリソースを極めて効率的に使うプログラムを作成することができます。これは、命令がCPUに直接対応しているため、実行速度が高速になります。
  • 制御の精度: メモリの個々のアドレスやCPUのレジスタなど、システムの低レベルの要素を細かく制御することができます。

用途

  • 組み込みシステム: リアルタイムでの処理が求められる組み込みシステムや、限られたリソースを効率的に利用する必要がある場面でよく使用されます。
  • パフォーマンス最適化: アプリケーションの特定部分の性能を向上させるために、アセンブリ言語で書かれることがあります。
  • オペレーティングシステム: OSの核心部分やデバイスドライバなど、システムレベルのプログラミングに使用されます。

アセンブラとコンパイラの違い

  • アセンブラはアセンブリ言語のコードを機械語に変換するプログラムです。
  • コンパイラは、CやJavaなどの高レベル言語で書かれたプログラムを機械語に変換するプログラムです。

アセンブリ言語は学習が難しく、デバッグが複雑ですが、システムの理解を深める上で非常に価値のあるスキルです。現代では多くのアプリケーションで高レベル言語が使用されるため、アセンブリ言語が必要とされるのは特定の専門的な領域に限られます。

ArduinoIDEでアセンブラを使う方法

Arduinoでは、CやC++を使ってプログラミングを行うことが基本ですが、このC言語プログラムのコード中にアセンブラ言語を使うことができます。この方法をインラインアセンブラと呼びます。インラインアセンブラを使うことで、Arduinoプログラムにおいて、高い制御精度やパフォーマンスの向上が可能になります。特に、タイミングが厳しい操作や、マイクロコントローラの特定のハードウェア機能を利用する際に有用です。

基本的な使用方法

インラインアセンブラは、asm キーワードを使用してC/C++のコード内に記述します。シンプルな形式は以下の通りです。

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番目のビットをセットします。

使用上の注意

    • デバイス依存性: インラインアセンブラは、使用しているプロセッサのアーキテクチャに依存します。したがって、ATmega328P用に書かれたコードは、他のマイクロコントローラでは動作しない可能性があります。
    • 複雑さとデバッグ: アセンブリ言語はデバッグが困難であり、エラーが原因の特定が難しいことがあります。また、プログラムの可読性が低下するため、保守が困難になることがあります。
  • 最適化: コンパイラの最適化を回避するためにvolatileを適切に使用することが重要です。

インラインアセンブラでLチカをさせるコード

Arduino UnoのATmega328Pで13番ピンのLEDを点滅させるプログラムをインラインアセンブラを使用して記述してみました。
直接レジスタ操作を行いながら、アセンブリ言語のコードを挿入してピンの状態を変更することができます。以下は、C++のコード中にインラインアセンブラを使用して、ピン13(ポートBの5番ビット)の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秒間待機
}

説明

1秒間隔でLEDをちかちかと点滅させるプログラムです。

  1. セットアップ (setup 関数):
    • DDRB レジスタ(データ方向レジスタB)を操作して、ピン13(PB5)を出力として設定します。ここで |= 演算子とシフト演算を使用して、特定のビットのみを変更しています。
  2. メインループ (loop 関数):
    • asm volatile 文を使って、アセンブリ命令を直接挿入します。sbi (Set Bit in I/O Register) 命令でPORTBの5ビット目をセットし、LEDを点灯させます。
    • 1秒間待機 (delay(1000))。
    • cbi (Clear Bit in I/O Register) 命令でPORTBの5ビット目をクリアし、LEDを消灯させます。
    • 再び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以下という恐るべき速度アップとなりました。

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