AVR-GCCインラインアセンブラ便利帳

Ver 1.2

 

この文章に関して

Atmel AVR RISCプロセッサー用のGNU CコンパイラーはCプログラム中にアセンブラ言語を埋め込むことができます。この便利な機能は手動でタイムクリティカルな部分を適合させたり、C言語では不可能な特別な命令を使用するのに使えます。

参考資料の不足のために、特にAVR版に関してですが、コンパイラやアセンブラソースを調べるために、仕様の詳細がわかるのに時間がかかると思われます。いくつかのサンプルプログラムはネット上にあることはあります。この文章がこれに数えられたらいいですね。

読者はAVRアセンブラに関してなれていることとします。なぜなら、この文書はAVRアセンブラのチュートリアルではないからです。またC言語のチュートリアルでもありません。

 

著作権に関して(Copyright(C) 2001 by egnite Software GmbH)

この著作権記述すべてが複製されるもとで、このマニュアルそのものを複製したり配布する権利が認められています。すべての追加作業がこれと同一の権利のもとで配布されることにより、このマニュアルを改変したものを複製したり配布する権利が認められています。

Copyright (C) 2001 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.

この文書は初期配布版です。これは2.9.5.2バージョンのコンパイラについて述べられています。しかし、ほとんどの部分はバージョン2.9.6に関しても有効です。作者本人によって完全に理解されていない部分があると思われます。そしてすべてのサンプルを試したわけでもありません。この作者はドイツ人であり英語になれていないため、この文書には明らかな誤植や文章間違いがあります。プログラマとしてこの著者は知っています、コード中の間違ったコメントなら、これは無いほうがましなことを。けれど、この文書をよりよくするための十分な意見を得られたらと、少ない知識ですが公開することに決めました。Eメールでお気軽に著者までご連絡ください。最新バージョンはhttp://www.egnite.de/をごらんください。

ヘルネにて、2001/05/07
Harald Kipp
harald.kipp@egnite.de

日本語訳 : まろり
http://ww2.tiki.ne.jp/~maro/
maro@mx2.tiki.ne.jp

訳者過去記録
2001/10/30 : Ver 1.0 公開
2001/12/10 : 著作権の原文を追加

 

目次

1 GCCアセンブラ文法
2 アセンブラコード
3 入出力オペランド
4 クロバー
5 アセンブラマクロ
6 索引

過去記録
2001/05/07 V 1.1 : 1章の入出力順を訂正。マルチバイトオペランドを追加。索引を追加。いくつかの誤植を訂正。
2001/05/07 V 1.2 : クロバーの例でsubiをincにより置き換えた。ポインタの型が必要。

 

1 GCCアセンブラ文法

さあ、ポートDから値を読む簡単な例からはじめましょう。

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

それぞれの記述がコロンにより4つの部分に分けられています。

1. アセンブラの命令で、ひとつの文字列で定義されます。

"in %0, %1"

2. 出力オペランドで、カンマにより区切ることができます。この例ではひとつです(のでカンマで区切られてはいません)。

"=r"(value)

3. カンマで区切られた入力オペランド。この例でもひとつです。

4. クロバーレジスタ、この例では空のままです。

通常のアセンブラと同じ方法でアセンブラ命令を記述できます。しかし、Cコード中のものを参照するときは、レジスタと定数は異なる方法で与えます。レジスタとCオペランドとの連結は入力と出力リストによる、アセンブラの2番目と3番目の部分で定義します。一般的な形は

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

コード部ではオペランドはパーセント文字とそれに続く数字で参照されます。%0は初めの、そして%1は2番目のオペランドを表し、と続きます。上の例は

%0 は "=r"(value) を表し、%1 は "I"(PORTD) を表します。

これは少し変な表現に見えるでしょう。しかしオペランドリストの表記はすぐに説明します。さあ、まずコンパイラリストを見ましょう。これにはここでの例題の内容が含まれていることでしょう。

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

このコメントはアセンブラであることを知らせるためにコンパイラにより追加されたもので、C文章をコンパイルして生成されたものでない、インライン文章からのコードが含まれます。コンパイラはr24を値を得るために選択しました。しかし、これはほかのレジスタが選ばれるかもしれません。値は明確には記述されないかも知れません。そしてアセンブラコード中にまったく含めないかもしれません。これらすべての決定はコンパイラの最適化方法の一部であります。たとえば、もし残りのプログラム中で変数の値を使わないならばコンパイラはコードを取り除いてしまうでしょう、そうしたくなければコンパイラスイッチを切るしかないです。これを避けるためにアセンブラ文章にvolatile属性を加えます。

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

アセンブラコードに続く部分、クロバーリストはおもにコンパイラにアセンブラ内で変更が行われたことを伝えるために使用されます。ほかの部分は必要なのですが、この部分は無視して空のままでもいいです。もしアセンブラコードがC記述に対して入出力を行わないならばアセンブラコード文字列に続いて2つのコロンで続けます。良い例は割り込みを不許可にする単一の記述です。

asm volatile("cli"::);

 

2 アセンブラコード

記述にはAVRアセンブラで使ったものと同じアセンブラ命令と同じものを使えます。そしてフラッシュメモリの分だけ好きなだけの複数のアセンブラ記述をひとつの文字列に記述できます。

読みやすくするため、それぞれの記述にラインセパレータを置くべきでしょう。

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

行送りとタブはコンパイラにより生成されたアセンブラリストをより読みやすくするでしょう。初めは変に見えるかもしれません。しかしこの方法によりコンパイラはアセンブラコードを生成しているんです。

特別なレジスタを使用することもできます。

symbol register
__SREG__ アドレス0x3Fのステータスレジスタ
__SP_H__ アドレス0x3Eのスタックポインタ上位バイト
__SP_L__ アドレス0x3Dのスタックポインタ下位バイト
__tmp_reg__ レジスタr0、一時保存のために使う
__zero_reg__ レジスタr1、常に0
__PC__ プログラムカウンタ?

レジスタr0はアセンブラコード内で自由にしようしてもよく、コードの終わりに元に戻さ無くてもいいです。r0とr1の替わりに__tmp_reg__と__zeto_reg__を使うのはよい考え方です、というのは新しいコンパイラでレジスタの定義が変わるかもしれないのですから。

 

3 入出力オペランド

入力と出力のオペランドは括弧内にCでの表記を続けた文字列で表記します。AVR-GCC 2.9.5.2は下の制御子(constraint)を認識します。

制御子(constraint) 使い方 値の範囲
a 基本上位レジスタ r16からr23
b ポインタレジスタセット y,z
d 上位レジスタ r16からr31
e ポインタレジスタセット x,y,z
G 浮動小数点定数 0.0
I 6ビットの正数の定数 0から63
J 6ビットの負数の定数 -63から0
K 定数 2(プログラムカウンタか?)
L 定数 0
l 下位レジスタ r0からr15
M 8ビットの定数 0から255
N 定数 -1
O 定数 8,16,24
P 定数 1
r レジスタ r0からr31
t 一時保存レジスタ r0
w 特別な上位レジスタ r24,r26,r28,r30
x ポインタセットX x (r27:r26)
y ポインタセットY y (r29:r28)
z ポインタセットZ z (r31:r30)

これらの定義はAVRの命令セットに対して適切に対応しないように思われます。著者の意見としては、コンパイラのこの部分はこのバージョンでは完全には終わっていないというものです。しかしこの意見は間違っている。適切は制御子(constraint)は定数やレジスタの範囲によるものであり、これを使用するAVRの命令セットが受け入れるものでなくてはいけないのです。Cコンパイラはアセンブラコードのチェックはしません。しかしC表現に対する制御子はチェックできます。しかしながら、もし間違った制御子なら、コンパイラはだまってアセンブラに間違ったコードを送ることになります。そして、もちろんアセンブラではなぞめいた出力や内部エラーにより失敗するでしょう。たとえばもしアセンブラコード内で制御子"r"指定をして、このレジスタを"ori"命令に使用したならば、コンパイラは自由にレジスタを選んでしまうでしょう。もしコンパイラがr2からr15を選んでしまったら、失敗します。(r0とr1は選択されません。なぜならこれらは特別な用途で使用されるからです。)これが、この場合で正しい制御子が"d"であることの理由です。いかえれば、もし制御子"M"を使ってしまっても、コンパイラは8ビット値としてのみ扱うことになります。後ほど、どのようにしてアセンブラコードなるマルチバイト表現が通るかを説明します。

下の表はすべてのAVRアセンブラの命令と、オペランドや関連する制御子を示しています。バージョン2.9.5.2の不完全な制御子の定義のためにこれらは、十分に厳密ではありません。たとえば、ビットセットとビットクリア操作のための0から7の定数のための制御子がありません。

命令 修飾
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
elmp 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
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

制御子文字は前に付加する修飾子(modifier)を持てます。修飾子のない制御子はread-onlyのオペランドになります。修飾子は

修飾子 機能
= Write-onlyのオペランド
+ Read-onlyのオペランド
(インラインアセンブラではサポートされません)
& 出力のみに使用されるべきレジスタ

出力オペランドはwrite-onlyでなくてはならず、Cでの表現はl-valueでなくてはなりません。これは左づめにおいて有効であるということです。ただし、コンパイラはアセンブラの中では、その主の操作のために有効であるかどうかについてはチェックを行いません。

入力オペランドは、ご想像どおり、read-onlyです。しかし、もし入出力が同じオペランドが必要なとき、read-writeオペランドはサポートされてないのでしょうか?上に述べたようにread-writeオペランドはインラインアセンブラではサポートされていません。しかし、ほかの解決法があります。入力オペランドに関しては文字列により一つの数字を使用できます。数字nを使用することはコンパイラーにn番目のオペランドに関して同じレジスタを使用することを伝えます。これは0から始まる数字です。ここに例があります。

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

この記述はvalueという名前の8bitの変数のビットを交換します。定数"0"はコンパイラに1番目のオペランドと同じ重力レジスタを使用することを伝えます。しかしながら、これは逆の場合について、自然にこたえるものではありません。このように指定しなくてもコンパイラは入出力に同じレジスタを選ぶかもしれません。これはほとんどの場合は問題にはなりませんが、重要です。もし出力命令が入力命令が使用される前にアセンブラコードにより変更されたときです。入出力オペランドが異なるレジスターであることに影響する場合は、おぺらんどに&修飾子を加えなくてはなりません。下の例はこの問題を検証します。

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

この例では入力値はポートから読み取られ、その後、出力値が同じポートに書き込まれます。もしコンパイラーが入出力に同じレジスタを選んでしまったなら出力値は最初のアセンブラ命令で破壊されてしますでしょう。幸運にもこの例では&修飾子が、コンパイラに出力値のためのレジスタを選定し無いように指示するために、使用されています。これはどんな入力オペランドに関しても使えます。交換することに戻ります。これは16bit値の高位と低位を入れ替えるためのコードです。

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__レジスタの使用に気づくでしょう。これは2章で特別レジスタの中で挙げたものです。このレジスタは内容をセーブせずに使用できます。完全に新規のものは%A0と%B0のAとBの文字です。これらは2つの異なる8bitレジスタを参照します。どちらも値の一部に含まれます。

その他の例として32bit値を交換するものです。

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)
	);

さて、この追加文字の説明はややこしいことです。もし8bitレジスタに指定されたオペランドがマルチバイトオペランドだったら、コンパイラは自動的にすべてのオペランドを維持するのに十分な割り当てを行います。アセンブラコード内で%A0は最初のオペランドの最も低いバイトを参照します。%A1は2番目のオペランドの最初のバイトを参照します。最初のオペランドの次のバイトは%B0で,その次は%C0などとなります。

これは、入力オペランドの型を目的のサイズに合わせる必要がある、という意味を含みます。

 

4 クロバー

前に述べたように、アセンブラの最後の部分であるクロバーリストは、コロンも含めてはぶかれることがあります。しかしながらもしオペランドを通らないレジスターを使用するときコンパイラーにこれを伝える必要があります。次の例は単一インクリメントを行います。ポインタの指し示す8bit値を割り込みルーチンやマルチスレッド環境においてほかのスレッドに干渉されずにインクリメントします。ただしポインタを使わなくてはいけません、なぜならインクリメント値は割り込みが有効になる前にストアされなくてはならないからです。

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

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

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

r24を壊されない(clobbering)ひとつの簡単な解決法は、コンパイラにより定義される__tmp_reg__レジスタを使用するようにすることです。

asm voatile(
	"cli"	"\n\t"
	"ld __tmp_reg__, %a0"	"\n\t"
	"inc __tmp_reg__"	"\n\t"
	"st %a0, __tmp_reg__"	"\n\t"
	"sei"
	:
	: "z" (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" (t)
		: "z" (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" (t)
		: "z" (ptr)
		: "memory"
	);
}

この特殊なクロバー"memory"はコンパイラーに、アセンブラコードはメモリの場所を変更するということを伝えます。アセンブラコードを実行する前に、コンパイラに現在レジスタに置かれているすべての値をアップデートさせます。そしてもちろん、このコードのあとでもすべてリロードされます。

この場合に関して、もっと良い解決法はポインタの指示それ自身をvolatileとして宣言することです。

volatile uint8_t *ptr;

このように、コンパイラはptrにより指し示される値が変更されるものであることを予想し、使うときいつもそれをロードし、変更するといいつもストアするでしょう。

クロバーが必要な状況はまれです。ほとんどの場合もっと良い方法があるでしょう。クロバーレジスタはコンパイラにアセンブラコードの前にその値をストアさせ、アセンブラコードの後にそれらをリロードさせます。これを避けることで最適化の能力をフルに発揮します。

 

5 アセンブラマクロ

アセンブラ言語のパーツを再利用するために、それらをマクロとして定義しインクルートファイルに入れるを便利です。AVR-GCCはそれらのモジュールからなり、avr/includeディレクトリに見られます。それらのインクルードファイルは、それらが厳密なANSIモードでコンパイルされてながらモジュール内で使用されるとコンパイラ警告を生成するかもしれません。これを避けるためにasmの替わりに__asm__を、そしてvolatileの替わりに__volatile__を書くことができます。これらは等価な別名です。

マクロを再利用するほかの問題として、ラベルを使う問題があります。特殊なパターン%=を利用するような場合です。これはそれぞれのアセンブラ記述で個別のナンバーに置き換えられるものです。次のコードはavr/include/iomacro.hから持ってきたものです。

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

もし最初に使用されるとき、L_%=はL_1404に変換されたとします。つぎはL_1405などが生成されるかもしれません。どんな場合でもこのラベルは唯一のものになります。

 

6 インデックス

(htmlのため省略いたします。)