AVR Libc Home Page AVRs AVR Libc Development Pages
Main Page FAQ Library Reference Additional Documentation Example Projects

インラインアセンブラ CookBook

AVR-GCC
インラインアセンブラ Cookbook

このドキュメントについて

Atmel AVR RISCプロセッサ用 GNU C コンパイラは、アセンブリコードをCプログラム内に埋め込む機能を提供しています。
このいかした機能は手動でタイムクリティカルな部分を最適化したり、C言語では利用不能な特別なプロセッサ命令を使ったりするのに使えます。

特にAVR版コンパイラに関しては、資料の不足により、コンパイラやアセンブラのソースコードを検討して実装の詳細を描き出すのに少々時間が掛かるかもしれません。
ネット上には利用できるサンプルプログラムも少ないです。このドキュメントがその数を増やすことになればいいなと思っています。

これを読むあなたはAVRアセンブラプログラムを書くのに慣れていることを前提としています。
これはAVRアセンブラプログラミング指導書ではありませんし、C言語の指導書でもありません。

このドキュメントは完全にアセンブリ言語で書かれたソースファイルについてはカバーしておりません。
これらについては avr-libc とアセンブラプログラム を参照ください。

Copyright (C) 2001-2002 by egnite Software GmbH

このマニュアル文書のコピーと配布は、コピーライトとこの許諾条件がすべてのコピーに保持される条件で、許可されています。
このマニュアル文書のコピーと改変版の配布は、すべての追加作業がこれと等価な許諾条件のもとでと行われる条件で、許可されます。

原文 : Copyright (C) 2001-2002 by egnite Software GmbH
Permission is granted to copy and distribute verbatim copies of this manual provided that the copyright notice and this permission notice are preserved on all copies. Permission is granted to copy and distribute modified versions of this manual provided that the entire resulting derived work is distributed under the terms of a permission notice identical to this one.

この文書は Version 3.3 のコンパイラについて説明しています。筆者が完全には理解していない部分があるかもしれません。
また、すべてのサンプルが充分に検証されたとは言えないかもしれません。
筆者はドイツ人で英語に慣れていないため、タイプミスや文法の誤りがあるかもしれません。
※訳者は日本人で英語にもドイツ語にも慣れていません(^^;)
プログラマとしての筆者は、誤った文書は時に存在しないより悪さをすることは知っています。
ですけど、彼はこのドキュメントをよりよくするため多くの意見をいただけることを期待して、少ない知識ですが公開することにしました。
気楽に筆者に e-mailでご連絡ください。最終版については http://www.ethernut.de/ をチェックしてください。

ヘルネにて。 17th of May 2002 Harald Kipp harald.kipp-at-egnite.de

Note:
26th of July 2002に、このドキュメントは avr-libcドキュメントに併合されました。現在では最終バージョンは http://savannah.nongnu.org/projects/avr-libc/ でも利用できます。

GCC のasm文

portDから値を読み込む簡単な例からはじめましょう:

asm("in %0, %1" : "=r" (value) : "I" (_SFR_IO_ADDR(PORTD)) );

asm ステートメントはコロン「:」により4つのパートに分けられます。

  1. アセンブラ命令。1つの文字列定数で定義されます:
     "in %0, %1" 
    
  2. 出力オペランドのリスト。(複数あれば)カンマ「,」で区切られますが、今回の例では1つだけです。
     "=r" (value) 
    
  3. 入力オペランドのリスト。(複数あれば)カンマ「,」で区切られますが、今回の例ではことらも1つだけです。
     "I" (_SFR_IO_ADDR(PORTD)) 
    
  4. クロバー(ぶち壊される) レジスタ。今回の例では空っぽのままにしておきます。※ここを省略する場合は3つめのコロンは省略できます

アセンブラ命令は、アセンブラプログラムを書くのと同様に書くことができます。
しかし、レジスタと定数についてCプログラムの表現を参照したい場合はちょっと異なった使い方をします。
レジスタとCオペランドの結合は asm 文の2番目と3番目のパートである入力リスト出力リストで指定されます。
一般的な形式は以下のようなものです。

asm(code : output operand list : input operand list [: clobber list]);

コードセクションでは、オペランドは パーセント記号とそれに続く1桁の数字で参照されます。数字が 0 なら最初の、1なら2番目のオペランドを参照し、以後同様です。

上記の例では:
%0
"=r" (value) を参照します。
%1"I" (_SFR_IO_ADDR(PORTD))を参照します。

今はこれは少々奇妙に見えるかもしれません。このオペランドリストの文法はおいおい説明していきます。
まずはこの例題で生成されたコンパイラリストを見てみましょう。

        lds r24,value
/* #APP */
        in r24, 12
/* #NOAPP */
        sts value,r24

インクルードされたコードがCコンパイラによって生成されたものではなく、インラインアセンブラによることをアセンブラに知らせるために、コンパイラによりコメントが加えられています。コンパイラはPORTDから読み込んだ値の保存場所として、レジスタ r24 を選びました。コンパイラは他のレジスタを選択するかもしれません。


コンパイラは明示的に value をロード・セーブしないかもしれないし、あなたが書いたアセンブラコードを全く組み込まないかも知れません。なぜならこれらの決定はコンパイラの最適化戦略の一部だからです。例えば、あなたが変数 value をCプログラムの残りの部分で全く使用しなかった場合は、最適化を切らない限りあなたの書いたコードは削除されるでしょう。これを避けるためには、asm文に volatile 属性を付加すればよいです。

asm volatile("in %0, %1" : "=r" (value) : "I" (_SFR_IO_ADDR(PORTD)));

オペランド(※演算対象、上の例ではIOレジスタや汎用レジスタ)にはそれぞれ名前が与えることができます。
与える名前は [ ] 内に納めてオペランドリストの頭に置きます。
%数字 形式の代わりにその名前を使ってコード部分からオペランドを参照させることができます。
これを使えば、上記の例題は下記のように書き表せます。
Alternatively, operands can be given names. The name is prepended in brackets to the constraints in the operand list, and references to the named operand use the bracketed name instead of a number after the % sign. Thus, the above example could also be written as

asm("in %[retval], %[port]" :
    [retval] "=r" (value) :
    [port] "I" (_SFR_IO_ADDR(PORTD)) );

asm命令の最後のパートであるクロバーリストは、主に、コンパイラにアセンブラによって行われた変更を伝えるために使用されます。このパートは省略可能ですが、それ以外のパート(入力オペランド以前)は必須です。ただし空白にすることは許されます。アセンブラルーチンが入力出力オペランドいずれも使用しない場合は、アセンブラコード文字列に続いて2つのコロンが置かれることになります。そのよい例が割り込みを禁止する簡単な命令文です:

asm volatile("cli"::);

アセンブラコード

お使いの他のAVRアセンブラで使えるのと同じアセンブラ命令語のニモニックが使えます。
フラッシュメモリ容量が許す限り複数のアセンブラ記述を1つのasm文に入れることが出来ます。

Note:
利用できるアセンブラ指令はアセンブラ毎に多少異なっています。

読みやすくするため、命令毎に行を分けた方が良いでしょう:(訳注1)

asm volatile("nop\n\t"
             "nop\n\t"
             "nop\n\t"
             "nop\n\t"
             ::);

ラインフィードとタブ文字の挿入は、コンパイラにが生成するアセンブラリスティングを読みやすくします。
これは最初は変に見えるかも知れません。しかしコンパイラもこのようにして(C言語から)アセンブラコードを生成しているのです。


(アセンブラコード内で)「特別なレジスタ」を使うことも出来ます。

Symbol Register
__SREG__ アドレス 0x3F にあるステータスレジスタ
__SP_H__ アドレス 0x3E にあるスタックポインタ上位バイト
__SP_L__ アドレス 0x3D にあるスタックポインタ下位バイト
__tmp_reg__ 汎用レジスタ r0。一時的保存に使えます
__zero_reg__ 汎用レジスタ r1。avrgccでは常に0として使えます

汎用レジスタ r0 はアセンブラコード内で自由に使えます。コードの前後で退避・復帰する必要はありません。

r0 , r1は、コード内では __tmp_reg__ ,__zero_reg__ と書いた方がよいでしょう。
コンパイラのバージョンアップでレジスタの使い方の定義が変更されるかもしれないからです。

入力・出力オペランド

入力・出力オペランドはconstraint(制約条件)文字列と、それに続く括弧に包まれたC言語の表現式からなります。
AVR-GCC 3.3 は下記のconstraintを認識できます:

Note:
constraint に関する最新で詳しい情報はgccマニュアル内にあります。

Xレジスタは r27:r26、Yレジスタは r29:r28、Zレジスタは r31:r30です。


Constraint Used for Range
a 単純な上位レジスタ(※Index/Pairレジスタ除く) r16 〜r23
b ベースポインタレジスタペア y, z
d 上位レジスタ(即値代入可能) r16 〜r31
e ポインタレジスタペア x, y, z
q スタックポインタレジスタ SPH:SPL
r 任意のレジスタ r0 〜 r31
t テンポラリレジスタ r0
w 特別な上位レジスタペア(※adiw/sbiw計算対象) r25:r24, x , y , z
x ポインタレジスタペア X x (r27:r26)
y ポインタレジスタペア Y y (r29:r28)
z ポインタレジスタペア Z z (r31:r30)
G 浮動小数点定数 0.0
I 6-bit の正の整数定数 (※ADIWなどに使用) 0 to 63
J 6-bit の負の整数定数 (※ADIWなどに使用) -63 to 0
K 整数定数 (2) 2
L 整数定数 (0) 0
l 下位レジスタ(※即値代入不可) r0 to r15
M 8-bit 整数定数 0 to 255
N 整数定数 (-1) -1
O 整数定数 8, 16, 24 ??
P 整数定数 (1) 1
Q (GCC >= 4.2.x) YまたはZポインタ+ディスプレースメントによるメモリアドレス  
R (GCC >= 4.3.x) 整数定数 -6 〜 5

 これらの定義はAVRインストラクションセットに適合しないように見えます。筆者は、このバージョンではコンパイラのこの部分は未だ完成していないと推測しています。勘違いかもしれませんが。

適切なconstraintの選択は、AVRの機械語命令が受け入れられる定数又はレジスタの範囲によって決まります。
Cコンパイラはアセンブラコードを全くチェックしませんが、constraintがCの表現に対して矛盾しないかどうかはチェックできます。
しかしながら、間違ったconstraintを指定した場合は、コンパイラは誤ったコードをアセンブラに黙って渡してしまいます。

 もちろんこのような場合はアセンブラはなぞめいた出力や内部エラーを吐いて終了してしまいます。
たとえば、constraint "r"を、"ori"命令に対して使った場合、コンパイラは任意のレジスタを選択します。
コンパイラが r2〜r15の下位レジスタを選択した場合(r0とr1は特殊な目的に使用されるため決して選択されません)、アセンブルは失敗します。
これがこのケースでのconstraintの正しい選択が "d"である理由です。
※訳注:ori命令は上位レジスタ(r16-r31)に対してのみ使用できる
また、constraint "M" を使う場合は、コンパイラは8bitの数値を渡すものと確信しています。あとで、アセンブラに多バイト表現式の結果を渡す方法についてお見せします。

下記はAVRアセンブラニモニックと、その要求するオペランド、それに対応するconstraintを表にしたものです。
version3.3では不適切な constraint定義のために充分しっくり来るものではありません。
たとえば、bit set/bit clearのために0〜7に制限された整数定数を表すconstraintがありません。
Mnemonic Constraints Mnemonic Constraints
adc r,r add r,r
adiw w,I and r,r
andi d,M asr r
bclr I bld r,I
brbc I,label brbs I,label
bset I bst r,I
cbi I,I cbr d,I
com r cp r,r
cpc r,r cpi d,M
cpse r,r dec r
elpm t,z eor r,r
in r,I inc r
ld r,e ldd r,b
ldi d,M lds r,label
lpm t,z lsl r
lsr r mov r,r
movw r,r mul r,r
neg r or r,r
ori d,M out I,r
pop r push r
rol r ror r
sbc r,r sbci d,M
sbi I,I sbic I,I
sbiw w,I sbr d,M
sbrc r,I sbrs r,I
ser d st e,r
std b,r sts label,r
sub r,r subi d,M
swap r

Constraint 文字の前に1つのconstraint修飾子が置かれることがあります。修飾子なしのcontraintは読み込み専用オペランドを指定します。
Modifier Specifies
= 書き込み専用オペランド。通常すべての出力オペランドに使用される。
+ 読み書きオペランド (インラインアセンブラではサポートされていない)
& レジスタは出力専用である

出力オペランドは書き込み専用で、対応するC表現はlvalueである必要があります。これはオペランドは代入式の左辺に置かれるもの(代入される側)であるということを意味します。コンパイラは、オペランドがアセンブラ命令内で妥当な型であるかどうかをチェックしないことに注意してください。

入力オペランドは、お察しの通り読み込み専用です。しかし、もしあなたが入力・出力双方に同じオペランドを充てたい場合はどうしたらいいでしょうか?(※ある変数を入力として充て、同じ変数に結果を返す場合)上で書いたように、読み書きオペランドはインラインアセンブラコードではサポートされていません。しかし他の解決方法があります。入力オペレータに、constraint文字として1桁の数字を置くことができます。数字 n を置くと、コンパイラはこの入力オペランドに対し n 番目(0から始まる)のオペランドと同じレジスタを使用します。例を挙げてみます。

asm volatile("swap %0" : "=r" (value) : "0" (value));

このインラインアセンブラはvalue という8bit変数の上位・下位nibble(4bit)を入れ替えるものです。constraint "0" はコンパイラに最初(0番目)のオペランドと同じレジスタを割り当てることをコンパイラに伝えます。しかしこれは自動的に逆の場合を示すものではありません??

コンパイラは、そうするよう指示しなかった場合でも入力と出力に同じレジスタを選んでしまうことがあります。多くの場合でこれは問題になりませんが、入力オペレータが使われる(参照される)前に出力オペレータが変更されてしまうと致命的な問題となります。入力出力で異なるレジスタ使用することが必要である状況では、"&" constraint修飾子を出力オペランドに加えなければなりません。以下の例はその問題を説明しています。

This statement will swap the nibbles of an 8-bit variable named value. Constraint "0" tells the compiler, to use the same input register as for the first operand. Note however, that this doesn't automatically imply the reverse case. The compiler may choose the same registers for input and output, even if not told to do so. This is not a problem in most cases, but may be fatal if the output operator is modified by the assembler code before the input operator is used. In the situation where your code depends on different registers used for input and output operands, you must add the & constraint modifier to your output operand. The following example demonstrates this problem:

asm volatile("in %0,%1"    "\n\t"
             "out %1, %2"  "\n\t" 
             : "=&r" (input) 
             : "I" (_SFR_IO_ADDR(port)), "r" (output)
            );

この例では、port からの値が変数 input に読み込まれ、変数 output の値が同じ port に書き込まれます。コンパイラが入力・出力双方に同じレジスタを選んでしまった場合、変数outputの値は最初のアセンブラ命令(in 共用されるレジスタ,port_addr)で壊されてしまいます。幸運なことに、この例ではコンパイラに変数outputに割り当てたレジスタ(入力オペランドに使われたレジスタ)を選択しないよう伝えるために "&"constraint修飾子を使っています(ので、この問題は起こりません)。

さて、ビット交換に戻りましょう。今度は16bitatain変数の上位下位バイトの交換です。

asm volatile("mov __tmp_reg__, %A0" "\n\t"
             "mov %A0, %B0"         "\n\t"
             "mov %B0, __tmp_reg__" "\n\t"
             : "=r" (value)
             : "0" (value)
            );

最初に、レジスタ __tmp_reg__ の使い方に注目してください。 アセンブラコード の章で他の特殊レジスタと一緒に紹介しましたね。このレジスタは内容の退避を行うことなく使うことができます。新しい文字、AとBが%A0,%B0の中で使われています。これらは2つの異なった8bitレジスタを参照します。両者は変数 value の一部を保持しています。

もう一つの例として32bit値swapの例を紹介します。

asm volatile("mov __tmp_reg__, %A0" "\n\t"
             "mov %A0, %D0"         "\n\t"
             "mov %D0, __tmp_reg__" "\n\t"
             "mov __tmp_reg__, %B0" "\n\t"
             "mov %B0, %C0"         "\n\t"
             "mov %C0, __tmp_reg__" "\n\t"
             : "=r" (value)
             : "0" (value)
            );

オペランドが1つのレジスタに当てはめられないとき(複数バイトデータの場合)、コンパイラは自動的にオペランド全体を保持するだけのレジスタを確保します。アセンブラコードで上では、%A0は最初のオペランドに対応するレジスタ群のうち、最下位バイトに対応するものを参照できます。%A1は2番目のオペランドの最下位バイト、以下同様です。最初のオペランドの2番目のバイトに対応するのは%B0になり、さらに次のバイトは%C0、以下同様です。

※たとえば、char c; uint32_t value; で、出力オペランドに "=r" (c) , "=r" (value)とした場合、以下のようになります。

  c     [1st byte]
        [  %0    ]
  value [1st byte] [2nd byte] [3rd byte] [4th byte]
        [  %A1   ] [  %B1   ] [  %C1   ] [  %D1   ] 

これは、入力オペランドの型を望むサイズに型変換する必要が出る場合もあることを意味します。
※8bit変数の値を16bit値として処理したい場合は16bit整数に型キャストして与えないとアセンブラ上で16bit分のレジスタ(%B0など)は割り当てられない。

最後の問題は、ポインタレジスタペアを使う場合に持ち上がります。入力オペランドを以下のように定義した場合、

"e" (ptr)

コンパイラはレジスタ Z (r30:r31)を選択し、

%A0r30 を参照
%B0r31 を参照  となります。

しかし、以下のような形でポインタレジスタとして利用しようとしても、コンパイラのアセンブリ段階でうまくいきません。※constraintはX,Y,Zのどれを採用したかわからない

ld r24,Z

その場合は以下のように書けばいいです。

ld r24, %a0

パーセント記号の後を英小文字とすればコンパイラは適切なアセンブラ行を生成してくれます。
※この部分はかなり意訳しています。訳者の解釈誤りの可能性もありますので原文も参照してください

クロバー

以前述べたように、asm 文の最後のパートは "clobber"のリストです。これは直前のコロン区切り子(:)を含め省略可能です。オペランドとして渡されていないレジスタを使いたい場合は、コンパイラにこのことを知らせねばなりません。下記の例はatomic(割り込みなどで分割されない)なインクリメントを行うものです。割り込みや、マルチスレッド環境の場合の他のスレッドに割り込まれることなく、ポインタ変数が指す8-bit値を1だけ増加させます。割り込みが許可される前に増加された値を変数に保存する必要があるので、ここではポインタを使わなければなりません。

※clobber : 1:n. 〔英話〕 装備, 7つ道具; 衣服, 装い.  2:vt. 〔俗〕 なぐり倒す; 打ち負かす; こき下ろす; 【コンピュータ】(ファイルを)消去する

asm volatile(
    "cli"               "\n\t"
    "ld r24, %a0"       "\n\t"
    "inc r24"           "\n\t"
    "st %a0, r24"       "\n\t"
    "sei"               "\n\t"
    :
    : "e" (ptr)
    : "r24"
);

コンパイラは以下のようなコードを生成するでしょう。

    cli
    ld r24, Z
    inc r24
    st Z, r24
    sei

r24レジスタの破壊を避けるために、コンパイラによって定義されている特殊なテンポラリレジスタ __tmp_reg__ を使う手があります。

asm volatile(
    "cli"                       "\n\t"
    "ld __tmp_reg__, %a0"       "\n\t"
    "inc __tmp_reg__"           "\n\t"
    "st %a0, __tmp_reg__"       "\n\t"
    "sei"                       "\n\t"
    :
    : "e" (ptr)
);

コンパイラは次回使用するときこのレジスタをリロードします。上記のコードの他の問題点は、これが割り込みが禁止されていて、割り込み禁止を維持する必要がある場所で使ってはいけないことです。このコードは最後に割り込みを許可してしまうからです。カレントステータス(STATUSレジスタにある)を保存・復帰するという手もありますが、それには他のレジスタが必要になります。(※もうレジスタを退避させるためのテンポラリレジスタはありません。)しかし、固定された(しかしコンパイラが自動選択した)レジスタを破壊せずにこの問題を解決する方法はまだまだあります。これはローカルなCの変数の助けを借りて行えます。

{
    uint8_t s;
    asm volatile(
        "in %0, __SREG__"           "\n\t"
        "cli"                       "\n\t"
        "ld __tmp_reg__, %a1"       "\n\t"
        "inc __tmp_reg__"           "\n\t"
        "st %a1, __tmp_reg__"       "\n\t"
        "out __SREG__, %0"          "\n\t"
        : "=&r" (s)
        : "e" (ptr)
    );
}

今度はすべてがうまくいくように見えます。しかし実際にはこれではうまくいかないのです。アセンブラコードは、ポインタ ptr が指している変数の値を変更します。コンパイラはこのことに気付くことはなく、この変数の値をどこか他のレジスタに保持しているかもしれません。コンパイラが誤った値で動くという問題だけでなく、アセンブラコード側にも同じ問題が起こりえます。Cプログラムは値を変更した(そしてそれを一時的にメモリではなくレジスタに保持していた)が、コンパイラはメモリ上の変数を参照するため値をアップデートできないと言うことも起こりえます。このケースでできる最悪の手は以下のようなものです。

{
    uint8_t s;
    asm volatile(
        "in %0, __SREG__"           "\n\t"
        "cli"                       "\n\t"
        "ld __tmp_reg__, %a1"       "\n\t"
        "inc __tmp_reg__"           "\n\t"
        "st %a1, __tmp_reg__"       "\n\t"
        "out __SREG__, %0"          "\n\t"
        : "=&r" (s)
        : "e" (ptr)
        : "memory"
    );
}

特別なクロバー "memory" は、アセンブラコードがメモリに対し変更を加えるかもしれないことをコンパイラに知らせます。これはアセンブラコード実行前に、レジスタ上に値を持っているすべての変数に対し値のアップデートをコンパイラに対し強制します。もちろん、コード終了時にすべての値がリロードされます。

しかしながら、多くの場合、ポインタを volatile として宣言する方がよいです。

volatile uint8_t *ptr;

この方法では、コンパイラはポインタ ptr が指し示す変数が変更されるものだと言うことを見込んで、参照時はいつでもメモリから読み出し、書き込み時はいつでもメモリに保存します。

クロバーを必要とする状況は大変まれです。多くのケースでは他にもっと良い方法があるでしょう。クロバー指定されたレジスタはアセンブラコード実行前にコンパイラにその値を保存させ、アセンブラコード終了時に復帰させます。クロバー指定を避けることはコンパイラによりよいコード最適化を行う自由を与えます。

(訳注3)

アセンブラマクロ

アセンブラ言語パーツを再利用するため、それをマクロとして定義し、インクルードファイルに仕立てると便利です。AVR Libc はそのようなマクロの山からできています。これらはディレクトリ avr/include 内で見ることができます。そのようなインクルードファイルを使う場合、もしstrict ANSI modeでコンパイルされたモジュール内で使われた場合はコンパイラ警告が出ることがあります。これを回避するためには、asmvolatileの代わりに __asm____volatile__ を使ってください。これらは等価な別名です。

再利用されるマクロのもう1つの問題は、ラベルを使ったときに持ち上がります。※同じモジュール内でマクロを使って2つの同じインラインアセンブラを展開すると同じ名前の2つのラベルができてしまうからです。このような場合は、特別なパターン「%=」を使います。これは各asm文毎に異なる数値に変換されます。以下のコードは avr/include/iomacros.h にあるものです:

#define loop_until_bit_is_clear(port,bit)  \
        __asm__ __volatile__ (             \
        "L_%=: " "sbic %0, %1" "\n\t"      \
                 "rjmp L_%="               \
                 : /* no outputs */        \
                 : "I" (_SFR_IO_ADDR(port)),  \
                   "I" (bit)    \
        )

これを最初に使ったときは、「L_%=」 は 「L_1404」に変換されます。次に使用した場合は 「L_1405」 などのように変換されます。こうしてラベルはasm毎に異なるものになります。

もう1つのオプションはUnixアセンブラスタイルの数値ラベルです。これらは FAQの アセンブラファイルを avr-gdbでトレースするには? でも紹介されています。上記のサンプルは以下のように書けます。

#define loop_until_bit_is_clear(port,bit)  \
        __asm__ __volatile__ (             \
        "1: " "sbic %0, %1" "\n\t"      \
                 "rjmp 1b"               \
                 : /* no outputs */        \
                 : "I" (_SFR_IO_ADDR(port)),  \
                   "I" (bit)    \
        )

C Stub Functions

マクロ定義は参照される場所すべてに同じアセンブラコードを展開します。これは大きいルーチンでは受け入れられないことです。このようなケースではC stub関数を定義すると良いです。これはアセンブラコード以外は何も含まれません。

void delay(uint8_t ms)
{
    uint16_t cnt;
    asm volatile (
        "\n"
        "L_dl1%=:" "\n\t"
        "mov %A0, %A2" "\n\t"
        "mov %B0, %B2" "\n"
        "L_dl2%=:" "\n\t"
        "sbiw %A0, 1" "\n\t"
        "brne L_dl2%=" "\n\t"
        "dec %1" "\n\t"
        "brne L_dl1%=" "\n\t"
        : "=&w" (cnt)
        : "r" (ms), "r" (delay_count)
        );
}

この関数の目的は、カウントループを使って、ミリ秒単位の数値を指定してプログラム実行にディレイをかけることです。グローバル16bit変数 delay_count には、このルーチンが最初に呼ばれる前に(CPUクロック周波数(Hz)/4000)を納めておく必要があります。clobberセクションで説明したように、このルーチンはローカル変数を一時的に値を保持するために使用します。

ローカル変数の他の使い方として、値を返す場合があります。下記の関数は2つの連続したポートアドレスから読み込んだ16bit値を返します。

uint16_t inw(uint8_t port)
{
    uint16_t result;
    asm volatile (
        "in %A0,%1" "\n\t"
        "in %B0,(%1) + 1"
        : "=r" (result)
        : "I" (_SFR_IO_ADDR(port))
        );
    return result;
}
Note:
inw() is supplied by avr-libc. ※inw()マクロ関数は以前はAVR-LibCでサポートされていましたが現在では削除されています。

アセンブラコードで使用されるCの名前

デフォルトでAVR-GCCは同じ関数や変数のシンボル名をCとアセンブラコードで使います。asm文の特別な形式により、アセンブラコードに対して異なるシンボル名を指定することもできます。

unsigned long value asm("clock") = 3686400;

この文は、シンボル名 clock をその値(3686400)の代わりに使うことをコンパイラに教えます。これはexternal または static な変数にしか使えません。ローカル変数はアセンブラコード内ではシンボル名を持たないからです。しかし、ローカル変数は特定のレジスタに置くこともできます。

AVR-GCC では、(変数に対して)特定のレジスタを指定することができます。

void Count(void)
{
    register unsigned char counter asm("r3");

    ... some code...
    asm volatile("clr r3");
    ... more code...
}

アセンブラ命令 "clr r3" は変数 counter をクリアします。AVR-GCCは指定されたレジスタを完全には予約しません。最適化プログラムがこの変数がもはや参照されていないと知ったとき、レジスタは再利用されることもあります。※他の変数にr3が使われる。しかしコンパイラはこの事前に定義したレジスタ使用の衝突をチェックできません。あまりにたくさんのレジスタをこのように予約した場合は、コンパイラはコード生成時レジスタが足りないと降参してしまうかもしれません。

関数の名前を変更するには、プロトタイプ宣言が必要です。コンパイラは関数宣言部でasmキーワードを受け入れないからです。

extern long Calc(void) asm ("CALCULATE");

関数 Calc() を呼ぶと、関数 CALCULATEを呼ぶアセンブラ命令が生成されます。

Links

インラインアセンブラに関するさらに徹底的な議論については、gcc user manualをご覧ください。gccマニュアルの最終バージョンはいつでもここで参照できます:
http://gcc.gnu.org/onlinedocs/


訳者による追加:

注1:間違いが起こりにくいように、以下のように書くと何かと便利です。

asm("in %0, %1"  "\n\t"
    : "=r" (value)              /* 0:value */
    : "I" (_SFR_IO_ADDR(PORTD)) /* 1:port_address */
);

注2:(→削除)

注3:GCCマニュアル(3.4.3)を見ると、クロバーリスト内でレジスタ名を並べればそのレジスタを保全する機能があるようですが、同バージョンを元にするavrgccには実装されていない模様です。以下のサンプルでエラーは出ませんが r16 レジスタを保護してくれませんでした。

 #include <avr/io.h>
 register uint8_t a asm("r16");
  int main(void)
 {
     a = 3;
     asm volatile (
         "in r16,%0" "\n\t"
         "out %0+1,r16"
         :
         : "I" (_SFR_IO_ADDR(PORTB)) 
         : "r16"
     );
     PORTB = ~a;
 }