2005. 2. 2

アセンブラ関数の書き方(avr-gcc)


普通はアセンブラ関数なんて書く機会は無いと思いますが、コンパイラの吐いたコードを読むことは良くあると思うので、知っておいて損はないでしょう。それどころか、それができないと根の深いバグに遭遇したときなどはお手上げです。ここでは、avr-gccにおけるアセンブラ関数の書き方を簡単に解説します。アセンブラ化のメリットは、メモリ消費量の削減とスピードの向上です。Cソースに直接埋め込むインライン・アセンブラよりも具体的でわかりやすいので、AVRASMから入門した人に向いていると思います。機能が固まっていて頻繁に使われる関数ならアセンブラ化してしまうのも良いでしょう。

ソースリストの書式

;-------------------------------------------------------;
; アセンブラ関数 sum
; 
; プロトタイプ: int16_t sum (int16_t a, int16_t b)
; 機能: a に b を足した値を戻す

.global sum			;シンボルsumを他のモジュールに公開
.func sum			;関数sum()の開始(.func/.endfuncは無くても可)
sum:				;シンボルsumの定義
	add	r24, r22	;a(r25:r24)にb(r23:r22)を足す
	adc	r25, r23	;/
	ret			;戻り値をr25:r24に入れて戻る
.endfunc


;-------------------------------------------------------;
; アセンブラ関数 タイマ0 オーバーフロー割り込み
; (シンボル名はデバイスにより異なるので注意)
;
; プロトタイプ: なし
; 機能: 100カウント毎に割り込み発生

.global TIMER0_OVF_vect
.func TIMER0_OVF_vect
TIMER0_OVF_vect:
	push	r0			;フラグと使用するレジスタを待避
	in	r0, _SFR_IO_ADDR(SREG)	;
	push	r24			;/

	ldi	r24, -100		;次の割り込みは100カウント後
	out	_SFR_IO_ADDR(TCNT0), r24;/

	pop	r24			;フラグと使用したレジスタ類を復帰
	out	_SFR_IO_ADDR(SREG), r0	;
	pop	r0			;/
	reti
.endfunc

;-------------------------------------------------------;
; その他データの定義

.section .data	;↓データRAM領域(有意値で初期化されるstatic変数)

val1:	.dc.w	1000		; int16_t val1 = 1000;
tbl1:	.dc.b	1,2,3,4		; int8_t tbl1[] = {1,2,3,4};
tbl2:	.dc.w	1,2,3,4		; int16_t tbl2[] = {1,2,3,4};


.section .bss	;↓データRAM領域(ゼロで初期化されるstatic変数)

tbl0:	.ds.b	5		; int8_t tbl0[5];


.section .text	;↓プログラムメモリ領域

tbl1_P:	.dc.b	0,1,2,3,4,5	; const uint8_t tbl1_P[] PROGMEM = {0,1,2,3,4,5};
tbl2_P:	.dc.w	0,1,2,3,4,5	; const uint16_t tbl2_P[] PROGMEM = {0,1,2,3,4,5};
str1_P:	.ascii	"STRING\0"	; const char str1_P[] PROGMEM = "STRING";

	.align	2		; 必要に応じてワード境界に整列しておく

	ldi	ZL, lo8(tbl1_P)	;16/32bit値の分割は上から順にhhi8, hlo8, hi8, lo8
	ldi	ZH, hi8(tbl1_P)	;/  (



;※auto変数は、レジスタやスタック・フレームに確保します。


;-------------------------------------------------------;
; ローカル・ラベル(数値ラベル)

1:
	cpi	r24, 100
	brne	2f		;前方の2へ
	inc	r24
2:
	add	r24, r23
	rjmp	1b		;後方の1へ

1:	;※数値は重複していてもよい


;-------------------------------------------------------;
; 特殊機能レジスタの参照

#include <avr/io.h>	/* ←io.hでターゲットデバイス固有情報が得られる */

	in	r24, _SFR_IO_ADDR(PORTB)	;I/Oアドレス参照

	sts	_SFR_MEM_ADDR(PORTF), r22	;メモリアドレス参照



; アセンブラスタイルコメント(cディレクティブの行には不可?)
// C++スタイルコメント
/* Cスタイルコメント */


レジスタの使われ方

レジスタ用途
R0ワーク(破壊)
R1ゼロ レジスタ(保存)
R2~R17変数/ワーク(保存)
R18~R25引数/戻り値(破壊)
R26~R27(X)変数/ワーク(破壊)
R28~R29(Y)フレーム ポインタ(保存)
R30~R31(Z)変数/ワーク(破壊)

アセンブラ関数を書くとき重要なのが命令表関数呼び出し規約(ABI)です。ABIの規定するようにレジスタの使用方法は決まっていて、アセンブラ関数についてもC関数とのインターフェースにおいてそれに従わなければなりません。保存と記されているレジスタは、関数呼び出しで変更されないことが保証されます。使用する場合は値を保存し、呼び出し元に戻る前に復帰する必要があります。破壊と記されているものは、関数呼び出しで破壊されます。当然ですが、割り込み関数では使用するレジスタ全てとSREGも保存します。

Z(r31:r30), X(r27:r26), r25:r18

C関数内ではワークとして使用。値を保存する必要はなく、呼び出し元は必要に応じて待避します。また、C関数を呼び出すときは、これらが破壊されるものとして扱う必要があります。

r17:r2

C関数内ではローカル変数として使用。値は保存されなければならず、使うときは呼び出し先が待避・復帰します。

Y(r29:r28)

C関数内ではフレーム ポインタとして使用されます。値は保存されなければならず、使うときは呼び出し先が待避・復帰します。

r1:r0

フラッシュメモリ参照や乗算など特殊用途でワークとして使われます。r1はゼロ レジスタ(常に0とみなす)としても使用されるので、関数呼び出し/復帰のときは常にゼロでなければなりません。

引数の渡され方

引数の渡され方の例

func (int16_t a, int32_t b, char* c);

      |   a   |       b       |   c   |
      |r25:r24|r23:r22:r21:r20|r19:r18|


func (int8_t a, int16_t b);

      |   | a |   b   |
      |r25|r24|r23:r22|


func (char a*, b, ...);  (可変長引数)

      | PC | a, b, ...
   SP^

引数はレジスタ渡しです。先頭の引数から順にr25:r8へ順に格納され、溢れた分はスタックに積まれます。構造体もこれに従って渡されて効率が悪いので、普通はポインタ渡しとします。また、printf(char *, ...);のように可変長引数で宣言された関数を呼ぶときは、レジスタは使用せず全てスタックに積まれます。この場合、SP+3で示すアドレスが引数の先頭となります(AVRはポスト デクリメント プッシュなので)。

引数のサイズは、char=8bit、int=16bit、long=32bit、long long=64bit、flaot=32bit、double=32bit、ポインタ=16bit となっています。奇数長の引数はintのサイズ(=16bit)にアライメントされます。これは、megaが現れた当時movw命令を効率よく使えるようにと変更されたようです。なお、レジスタ割り当ては avr-gcc 3.4.1 のデフォルト設定で説明しています。

戻り値の引き渡し

戻り値の渡し方

int8_t func ();          |r24|
int16_t func ();     |r25:r24|
int32_t func ();     |r25:r24:r23:r22|

戻り値は引数と同様な手順でレジスタにセットして返します。

戻る