Arduino スケッチで AVR インライン・アセンブラを使う
Arduino IDE では、特に「ファイルをプロジェクトに追加」のような操作をしなくても、メインのスケッチ (.pde / .ino ファイル) があるフォルダに含まれている C/C++ ソース・ファイル (.c / .cpp) を拾ってくれて自動的にコンパイル/リンクしてくれます。
ただし、アセンブラ・ソースファイルである .s / .S ファイルは無視されます。
IDE のメニューバーから Sketch / Add File... とたどっていくと、あたかも指定のファイルをプロジェクトに追加してくれるような雰囲気となりますが、実際にはそのファイルをエディットできるだけで、.s / .S ファイルは拾ってくれません。
したがって、どうしてもアセンブラで記述したい場合には、gcc 拡張機能のインライン・アセンブラを利用することになります。
割り込み応答のオーバーヘッドを限界まで切り詰めたい場合を想定して、ISR (Interrupt Service Routine: 割り込みサービス・ルーチン) をフルにインライン・アセンブラで記述する簡単な例を示します。
スケッチを下に示します。 プログラムの機能自体は単なる「LED チカチカ」です。
// AVR inline assembler test // (LED blinking at 2 Hz) volatile int16_t gi16; ISR(INT0_vect) { } // ISR(INT0_vect) ISR(INT1_vect, ISR_NAKED) { __asm__ __volatile__ ( "lds r0, gi16 \n\t" "lds r1, gi16+1 \n\t" ); reti(); } // ISR(INT1_vect, ) ISR(TIMER1_OVF_vect, ISR_NAKED) { __asm__ __volatile__ ( "PINB = 0x03 \n\t" // PINB "sbi PINB,5 \n\t" // PINB5 (D13) "reti \n\t" ); } // ISR(TIMER1_OVF_vect, ) void setup( void ) { // LED = D13 = PORTB5 pinMode(13, OUTPUT); // TIMER1 setup TCCR1A = 0x00; // normal mode TCCR1B = 0x03; // clk = 16 MHz / 64 TIMSK1 = 0x01; // OVF int enable } // void setup() void loop( void ) { } // void loop()
ISR(INT0_vect) と ISR(INT1_vect, ) は説明のために付け加えたもので、「LED チカチカ」の機能には寄与していません。 ISR(TIMER1_OVF_vect, ) と setup() 関数だけが重要です。
まず、ISR(INT0_vect) は「普通の C 関数」として記述した、中身が「空」の ISR です。
コンパイル結果を avr-objdump で逆アセンブルした結果は次のようになります。
00000100 <__vector_1>: 100: 1f 92 push r1 102: 0f 92 push r0 104: 0f b6 in r0, 0x3f ; 63 106: 0f 92 push r0 108: 11 24 eor r1, r1 10a: 0f 90 pop r0 10c: 0f be out 0x3f, r0 ; 63 10e: 0f 90 pop r0 110: 1f 90 pop r1 112: 18 95 reti
ソースの中身は空ですが、SREG / R0 / R1 をセーブ / リストアするプロローグ / エピローグ・コードが出力されています。 (0x100 〜 0x109 までがプロローグ・コード、0x10a 〜 0x111 までがエピローグ・コード)
中身が空なので、これだけですが、普通は ISR の中で使用されるレジスタのセーブ / リストアのためのコードも出力されます。
そんなに無駄なコードというわけではありませんが、それも切り詰めたいという場合には、ISR() マクロに ISR_NAKED という引数を追加して、プロローグ / エピローグ・コードのない「裸」の状態にすることができます。
その指定をしたものが、ISR(TIMER1_OVF_vect, ISR_NAKED) です。
逆アセンブル結果を下に示します。
0000011e <__vector_13>: 11e: 1d 9a sbi 0x03, 5 ; 3 120: 18 95 reti
ここに現れているのは、ソースで指定したインライン・アセンブラの
__asm__ __volatile__ ( "PINB = 0x03 \n\t" // PINB "sbi PINB,5 \n\t" // PINB5 (D13) "reti \n\t" );
中の sbi 命令と reti 命令の 2 つのアセンブル結果そのものです。
もし、ソースの中身が空なら、(reti 命令さえ生成されずに) オブジェクトの中身も空となります。
__asm__ __volatile__ ("") が gcc のインライン・アセンブラの構文です。
普通は、インライン・アセンブラでも C で記述した計算の結果を利用したり、アセンブラで記述した計算の結果を C 側の変数に渡したりと、両者が協調して動作するように「拡張アセンブリ構文」と呼ばれる記述を使いますが、ここでは「基本インライン構文」を使います。
基本インライン構文では、丸括弧 () の中に記述した文字列がそのまま avr-as アセンブラへ渡されます。
アセンブラ・ソースとして見やすいように、各行の終わりに改行文字 (\n) とタブ文字 (\t) を付加しておきます。
また、アセンブラ・ソースの 1 行分を、C ソース・プログラムの 1 行内で完結する 1 つのストリングとして記述しておきます。
avr-as アセンブラ用のレジスタ定義は用意されていないので、上の例のように B ポートの PIN レジスタをシンボル「PINB」で扱いたければ「PINB = 0x03」のように「自前」で定義する必要があります。
この ISR では、約 0.25 秒周期で発生する TIMER1 のオーバーフロー割り込みごとに PINB のビット 5 をセットしています。
ポート B のビット 5 は Arduino では D13 ピンであり LED が接続されているので、約 0.25 秒 ごとにトグルされて約 0.5 秒周期で点滅することになります。
PINB5 のセットを sbi 命令で行なっているので、レジスタもフラグも不変であり、レジスタ類のセーブ / リストアは全て省略しています。
ISR(INT1_vect, ISR_NAKED) はグローバル変数をアクセスするための記述のテスト用で、特に意味はありません。
__asm__ __volatile__ ( "lds r0, gi16 \n\t" "lds r1, gi16+1 \n\t" ); reti();
のように、普通に書いてアクセスできるようです。 逆アセンブル結果を下に示します。
00000114 <__vector_2>: 114: 00 90 00 01 lds r0, 0x0100 118: 10 90 01 01 lds r1, 0x0101 11c: 18 95 reti
この例では、最後の reti 命令をインライン・アセンブラではなく、C のマクロの reti() を使って発生させています。 (reti() マクロの定義の中身はインライン・アセンブラ)