トップ 新規 編集 差分 一覧 ソース 検索 ヘルプ PDF RSS ログイン

avrgccでアセンブラを使う

最終更新時間:2007年08月26日 13時12分53秒

avrgccでアセンブラを使う(インラインアセンブラでない方)

 なぜアセンブラ?

  • Cにあるいろんなオーバーヘッドを回避してコードを小さくする
  • キャリーを駆使したシフト演算など、Cでは効率よくできない演算ができる
  • そこにavr-asがあるから(w

 アセンブラによる関数の書き方

参照

使用可能レジスタの割り当て

関数内で使用されるレジスタ r0,r18〜r25、r26:r27(Xレジスタ)、r30:r31(Zレジスタ)

サブルーチン側では退避不要。呼び出す側では破壊されることを前提とする。上記のうちr18〜r25については、C言語側からアセンブラ関数に渡す引数の授受にも使われる。

関数内で保存する必要があるレジスタ r2〜r17、r28:r29(Yレジスタ)

サブルーチン側では退避必要。呼び出す側では破壊されないことを前提とする。使用する場合は関数先頭で値をどこかに保存(pushなど)し、関数終了時に復帰(popなど)すること。上記のうちr8〜r17については、C言語側からアセンブラ関数に渡す引数の授受にも使われる。

固定値レジスタ r1

C言語側では常に0と見なして使っているレジスタ。退避は不要だが、関数終了時に0に戻しておく必要がある。アセンブラならclr r1

関数と割り込みハンドラ
  • アセンブラからCの関数を呼び出す場合は、呼び出し側は呼び出す前にr18-r27,r30-r31を自由に使えるように、使用中のレジスタがあれば退避してやらなければならない。callから帰ってきたら退避分を復帰させる。
  • 割り込みハンドラとして書かれたアセンブラの場合は、上記のレジスタ使用規則が破られている状態でコールされているケースがあり得るので、すべてのレジスタが不定と考えてプログラムする必要がある。r1は0とは限らないし、どのレジスタも変更するならば事前に値を保存し、処理終了時値を復帰させなければならない。
状況 r0/r18-25/X/Z r2-17/Y/(r1)
C言語から呼び出されるアセンブラ関数 自由に使用 変更する分は保存・復帰
アセンブラによる割り込みハンドラ 変更する分は保存・復帰 変更する分は保存・復帰
Cの関数を呼び出すアセンブラ call前にすべて保存/call後に復帰 無処理
アセンブラと呼び出されるサブルーチン お互いの状況次第 お互いの状況次第

引数の渡し方

r8〜r25が使用される。レジスタ2個ずつがセットになっていて9セット使える。大きな番号のレジスタペアから順番に使われる。(a0=変数aの最下位バイト,a1=次のバイト,以下同様)

hogehoge(uint16_t a , uint16_t b)

16bitの場合はそのままペアレジスタに割り当てられる
レジスタ r8-r9 ・・・ r18-r19 r20-r21 r22-r23 r24-r25
変数 XX-XX ・・・ XX-XX XX-XX b0-b1 a0-a1

hogehoge(uint8_t a , uint16_t b)

最低単位は2レジスタ,16bitなので、8bit変数でも16bit使われる
レジスタ r8-r9 ・・・ r18-r19 r20-r21 r22-r23 r24-r25
変数 XX-XX ・・・ XX-XX XX-XX b0-b1 a0-XX

hogehoge(uint32_t a , uint16_t b)

4バイト変数は2レジスタペアを使う
レジスタ r8-r9 ・・・ r18-r19 r20-r21 r22-r23 r24-r25
変数 XX-XX ・・・ XX-XX b0-b1 a0-a1 a2-a3

hogehoge(uint32_t a,uint32_t b,uint32_t c,uint32_t d,uint32_t e)

r8-r25に収まりきれない場合はスタックを使うようです。
レジスタ r8-r9 r10-r13 r14-r17 r18-r21 r22-r25
変数  XX  d  c  b  a
SPは関数先頭でのスタックポインタ値
アドレス 内容
SP-1〜SP-2 戻りアドレス
SP-3〜SP-6 変数e

でも、ここまでやると関数もpush/popの嵐になるので素直にアドレス渡しが吉。例えば下記。

hogehoge(uint32_t *abcde)
uint32_t abcde[5]; 構造体(頭に&をつけてね)を使ってもいい

レジスタ r8-r9 ・・・ r18-r19 r20-r21 r22-r23 r24-r25
変数 XX-XX ・・・ XX-XX XX-XX XX-XX abcde

値の返し方

関数が終了した時点でのレジスタ値が渡される。割り当ては引数と同様。

char     hogehoge()  8bit←r24(r25ではない)
uint16_t hogehoge() 16bit←r24:r25
uint32_t hogehoge() 32bit←r22〜r25(下位が若い番号のレジスタ)
uint8_t *hogehoge() 16bit←r24:r25
  • C から char や uint8_t を返す時には今のところ r25 には r24 の符号拡張か 0 が入っています。

例:unsigned intを5桁の10進文字列に変換する

サンプルプログラムの簡易2進10進変換プログラムitoa_small.c(111)をアセンブラで書き換えてみます。

  • itoas.S (拡張子は大文字の.S)
.global itoas // itoasを外部から利用可能にする
.func itoas   // 関数名の宣言。この後に書かれるアセンブラ命令が関数の中身になる
//拡張子を.SにすればCプリプロセッサにも掛けられるので、C++方式のコメントが利用できる
/* C形式のコメントも使用可能 */
;アセンブラ形式のコメントも使用可能

// 引数用レジスタ.16bit引数が2個なのでこのように割り当てられる
#define strL  r24   //.def命令の代わりにマクロを使う
#define strH  r25
#define dataL r22
#define dataH r23

// ローカル変数用レジスタ。r18〜r25,X,Zレジスタが埋まっている場合は
// r0〜r17レジスタをpushで退避させた後使用する。関数終了時pop復帰
#define num   r20
#define tmp   r21
#define zf    r18

itoas:  //関数の開始
	clr  zf
	mov  r30,strL
	mov  r31,strH  ;Z=s
	
	ldi  num,'0'
5:	subi dataL,lo8(10000)	;dataL:dataH-10000
	sbci dataH,hi8(10000)	;
	brcs 5f			;0を下回ったらCarryが立つ。前方(下方)の5:へ
	inc  num
	ser  zf			;ゼロ以外の文字が出現したらzf=0xFFにする
	rjmp 5b			;後方(上方)の5:へ
5:	subi dataL,lo8(-10000)	;dataL:dataH+10000
	sbci dataH,hi8(-10000)	;引きすぎた10000を戻す操作
	sbrs zf,0	;zfのbit0が1なら、下の1命令(ゼロサプレスのためスペースを入れる)をスキップ
	ldi  num,' '	;zero-supress
	st   Z+,num

	ldi  num,'0'
4:	subi dataL,lo8(1000)
	sbci dataH,hi8(1000)
	brcs 4f
	inc  num
	ser  zf
	rjmp 4b
4:	subi dataL,lo8(-1000)
	sbci dataH,hi8(-1000)
	sbrs zf,0
	ldi  num,' '
	st   Z+,num

	ldi  num,'0'
3:	subi dataL,lo8(100)
	sbci dataH,hi8(100)
	brcs 3f
	inc  num
	ser  zf
	rjmp 3b
3:	subi dataL,lo8(-100)
	sbci dataH,hi8(-100)
	sbrs zf,0
	ldi  num,' '
	st   Z+,num

;;この時点でdataは最大99なので、1byte処理にする
	ldi  num,'0'
2:	subi dataL,10
	brcs 2f
	inc  num
	ser  zf
	rjmp 2b
2:	subi dataL,-10
	sbrs zf,0
	ldi  num,' '
	st   Z+,num

1:	subi dataL,(-'0')
	st   Z+,dataL
	st   Z,0
	ret
	;;関数の返り値も引数と同じ。
	;;返り値と同じ型を第一引数に割り当てた場合に割り当てられるのと
	;;同じレジスタの値が返される。この場合は char *型で16bitなので
	;;r24:r25が返される。r24:r25は変更していないので第一引数のsの値が
	;;そのまま返される
.endfunc

C言語と比べての違いは、キャリーフラグが使えるために、Cだと比較+引き算するところをアセンブラでは引き算だけで対応ができた点です。signed intなら結果を正負判定する手もあるのでしょうが・・・。

  • test.c
#include <avr/io.h>
char *itoas(char *s,uint16_t data);
int main(void)
{
    char s[8];
    itoas(s,12345);
    for(;;);
}

コンパイル・リンク時はmakefileで以下の設定を.

TARGET = test
ASRC = itoas.S
サイズ、実行ステップ比較
mega8 itoas.S itoas.c AVRLib(itoa)
speed(cycle) 63-278 59-299 262-957
size(bytes) 106 144 142
AT90S2313 itoas.S itoas.c AVRLib(itoa)
speed(cycle) 63-278 59-299 267-968
size(bytes) 106 144 150
  • 同じ106バイトで、10以外の基数に対応、表示幅指定が可能なxitoaをあのchanさんが公開した模様・・・うまいなあ。

http://elm-chan.org/docs/avrlib/xitoa.zip

 割り込みルーチンをアセンブラで書いてみる

関数の時は、メインルーチン側は関数が呼び出される前にr18-r25,r30-r31をあけた上で関数をコールしてくれますが、割り込みの場合はいつ呼び出されるかわかりません。よって、割り込みプログラム側ではすべてのレジスタが使われているかもしれないと考えるべきです。よって、自分が書き換える可能性があるレジスタはすべてpush命令などで保存してください。

割り込み相手もアセンブラの場合は、適宜使用レジスタを割り振ることで不必要なレジスタ退避を省略することができます。(例えばメイン側ではr10-r17とZレジスタを決して使わず、割り込み側ではr10-r17とZレジスタだけを使うなど)。

SREGを退避復帰するのも忘れないでください。これを怠るとメインプログラム側の条件分岐などがぐちゃぐちゃになってしまいます。

C言語で書く(132bytes)

このプログラムと同じ機能を持つプログラムを作ってみます。

ovf_c.c

#include <avr/io.h>
#include <avr/interrupt.h>

ISR(TIMER0_OVF_vect )
{
    PORTB++;
}
int main(void)
{
    PORTB = 0xAA;
    DDRB  = 0xFF;
    TCCR0 = 5;
    TIMSK=_BV(TOIE0);
    sei();
    for(;;);
}

割り込みだけアセンブラ(122bytes)

割り込みルーチン内でr0,r1を保存したりr1を0にしたりする処理がなくなる分小さく速いようです。

ovf0.S

#include<avr/io.h>   //必須らしい。<avr/signal.h>や<avr/interrupt.h>は不要らしい

.global SIG_OVERFLOW0 //いつもSIGNAL()内に定義する名前で
.func   SIG_OVERFLOW0
#define SREG_SAVE r8
#define pbvalue   r9

SIG_OVERFLOW0:
    push SREG_SAVE
    push pbvalue
    in   SREG_SAVE,_SFR_IO_ADDR(SREG)
    
    in   pbvalue,PORTB
    inc  pbvalue
    out  PORTB,pbvalue
    
    out  _SFR_IO_ADDR(SREG),SREG_SAVE
    pop  pbvalue
    pop  SREG_SAVE
    reti
.endfunc
.end
ovf.c
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void)
{
    PORTB = 0xAA;
    DDRB  = 0xFF;
    TCCR0 = 5;
    TIMSK=_BV(TOIE0);
    sei();
    for(;;);
}
Makefile
TARGET = ovf
ASRC = ofv0.S

C言語側のグローバル変数として以下のものを用意すれば、r8とr9はC言語側で使われないので、割り込みプログラムのpush/pop(8bytes)を省略できますね。1クロックでも早く割り込み処理したい場合には使えそうです。

register SREG_SAVE asm("r8"::);
register pbvalue   asm("r9"::); 

オールアセンブラで書いてみる(110bytes)

AVR-LibCの assembler.htmlにも例があります。110バイト。当たり前ですが、ターゲットをat90s1200やattiny12にしてもアセンブルできます。

AVRStudioなどのアセンブラを使うのに比べ、割り込みハンドラなどをavr-as側が作ってくれる分は楽です。

ovf00.S
#include<avr/io.h>   //必須らしい。<avr/signal.h>や<avr/interrupt.h>は不要らしい

.global SIG_OVERFLOW0 //いつもSIGNAL()内に定義する名前で
.func   SIG_OVERFLOW0
#define SREG_SAVE r8
#define pbvalue   r9

SIG_OVERFLOW0:
//メインプログラム側でSREG_SAVE,pbvalueが使われていないのが確実なので、
//push/pop省略できる。複雑なプログラムになってくればそうはいかないのでしょうが
//多重割り込みを許可しないなら割り込みがいくつあっても一度に割り込み内で使われる
//レジスタは1セットしかないので、共用可能です。
    in   SREG_SAVE,_SFR_IO_ADDR(SREG)
    in   pbvalue,PORTB
    inc  pbvalue
    out  PORTB,pbvalue
    out  _SFR_IO_ADDR(SREG),SREG_SAVE
    reti
.endfunc

.global main
.func   main
main:
	//io.h,ioxxxx.hに定義されているIOレジスタ名などが使えます
         .ifdef SPH
	  ldi	ZH, hi8(RAMEND)
	  out	_SFR_IO_ADDR(SPH), ZH  //2313などSPHがないデバイスがある
	.endif
        ldi	ZL, lo8(RAMEND)
	out	_SFR_IO_ADDR(SPL), ZL
        ldi	ZL, 0xAA
	out	PORTB, ZL
	ldi	ZL, 0xFF
	out	DDRB, ZL
	ldi	ZL, 0x05
	out	_SFR_IO_ADDR(TCCR0),ZL
	ldi	ZL, _BV(TOIE0)
	out	_SFR_IO_ADDR(TIMSK), ZL
	sei
1:
	rjmp	1b
.endfunc
.end
Makefile
TARGET = ovf00
SRC =
ASRC = $(TARGET).S

いつもは書き換えないSRCを変更し、ASRCに$(TARGET).Sを書く点に注意です。他にもリンクするアセンブラファイルがあればその後ろに続けます。

 疑似命令など

.で始まるコマンド。既に.func、.endfunc、.globalなどが登場しています。使えそうなものを並べてみました。

関数宣言

.global hogehoge

hogehogeというシンボルを外部に公開し、リンク対象にする。外部から参照する関数名などはこれで宣言。ローカル関数については不要。

.func   hogehoge
(関数の中身)
.endfunc

関数を定義する。C言語と異なり、自動的にreturnなどつけてくれませんので、末尾に注意。ret/reti等の他、rjmpなどで他の関数に飛ぶこともできる。

データの置き場所を指定するもの

.data    ; .data セクションへの切り替えを行います (初期化済みRAM上変数) 
.text    ; .text セクションへの切り替えを行います (プログラムコードとROM上定数) 

データアドレスを指定するもの

.balign 8,0x90

以下に置かれるデータは8の倍数のアドレスから始まり、空いたスペースは0x90で埋められる。例えば、256の約数である8の倍数のアドレスから8バイト単位のデータを並べてこれを扱う場合(例えば8x8のフォントデータなど)データの途中に上位アドレスが繰り上がることはなくなるので、プログラムが短く簡単になる。最初に先頭アドレスをセットした後は下位アドレスだけ動かせばいい。

.align

(ちょっとまってね)

.p2align 3,0x90

動作はbalignと同様だが、境界条件を2の指数として指定する。上記は.balign 8,0x90と同等

.equ 0x1000

アセンブラではお馴染みですが、アドレスはリンカに任せるべきと言うgccの発想から、あまり推奨されていないようです。

データを置く疑似命令

(.textの後ならFLASHメモリ、.dataの後ならSRAM上定数領域)

.byte 0x12,0x34  ; 1バイト定数を割り当てます 
.word 0x1234,0x5678 ; 2バイト定数割り当て
.long 0x1234ABCD    ; 4バイト定数割り当て
.ascii "abcde"   ; 終端文字がない文字列を表します。
                 ; .byte 'a','b','c','d','e'と同じ 
.asciz "abcde"   ; \0 終端文字を持つ文字列を表します(Cの文字列)
                 ; .byte 'a','b','c','d','e',0 と同じ 
.fill REPEAT , SIZE , VALUE

データサイズ SIZEバイトの、値VALUEを、REPEAT個だけ置く

.space 16,0xFF
.skip  16,0xFF

どちらも16バイトの0xFFを置く疑似命令

.float  value
.single value
.double value

浮動小数点数値/単精度/倍精度実数値データを置く

構造文−まあ見ての通り

.if EXPRESSION    ;EXPRESSIONが0以外で成立
.else
.elseif EXPRESSION
.endif

.ifdef SYMBOL  ; SYMBOLが定義されていれば成立
.ifndef SYMBOL ; SYMBOLが定義されていなければ成立
.ifnotdef SYMBOL  ; .ifndefと同じ

.ifc    STRING1,STRING2    ;2つの文字列が同じものなら成立
.ifnc  STRING1,STRING2  ; 2つの文字列が異なるものなら成立
.ifeqs "STRING1","STRING2" ;文字列をダブルクォートでくくる以外は.ifcと同じ
.ifnes "STRING1","STRING2" ;文字列をダブルクォートでくくる以外は.ifncと同じ

.ifeq  EXPRESSION   ; 引数が0なら成立
.ifne  EXPRESSION   ; 引数が0以外なら成立 .ifと同じ?
.ifge  EXPRESSION   ; 引数が0以上なら成立
.ifgt  EXPRESSION   ; 引数が0より大きいなら成立(0は含まず)
.ifle  EXPRESSION   ; 引数が0以下なら成立
.iflt  EXPRESSION   ; 引数が0より小さいなら成立(0は含まず)

繰り返し

.irp param,1,2,3
    out _SFR_IO_ADDR(PORTB),\param
.endr

.irpc param,123
    out _SFR_IO_ADDR(PORTB),\param
.endr

どちらも以下のように展開される。irpcは2番目のパラメータを1文字ずつparamに渡して展開する。

out _SFR_IO_ADDR(PORTB),1
out _SFR_IO_ADDR(PORTB),2
out _SFR_IO_ADDR(PORTB),3

マクロ

.macro Latch
    sbi  _SFR_IO_ADDR(PORTB),0
    cbi  _SFR_IO_ADDR(PORTB),0
.endm

Latchというマクロを定義。PB0をHi/Loにする。

.Macro Latch2 bit
    sbi  _SFR_IO_ADDR(PORTB),\bit
    cbi  _SFR_IO_ADDR(PORTB),\bit
.endm

引数をとる。Latch 1で、PB1をHi/Loする。

.Macro Latch2 port=PORTB,bit
    sbi  _SFR_IO_ADDR(\port),\bit
    cbi  _SFR_IO_ADDR(\port),\bit
.endm

2つの引数をとる。Latch2 PORTD,1でPD1をHi/Loする。Latch2 ,1とport指定を省略するとPORTBが使われる

マクロの再帰コールをうまく利用すると、繰り返し表現が実現できます。

.macro latch3 port=PORTB,from=0,to=7
    sbi  _SFR_IO_ADDR(\port),\from
    cbi  _SFR_IO_ADDR(\port),\from
    .if \to-\from
      latch3 \port,"(\from+1)",\to
    .endif
.endm

この定義で、latch3 PORTB は、下記のように展開されます。

sbi	_SFR_IO_ADDR(PORTB), 0
cbi	_SFR_IO_ADDR(PORTB), 0
sbi	_SFR_IO_ADDR(PORTB), 1
cbi	_SFR_IO_ADDR(PORTB), 1
sbi	_SFR_IO_ADDR(PORTB), 2
cbi	_SFR_IO_ADDR(PORTB), 2
sbi	_SFR_IO_ADDR(PORTB), 3
cbi	_SFR_IO_ADDR(PORTB), 3
sbi	_SFR_IO_ADDR(PORTB), 4
cbi	_SFR_IO_ADDR(PORTB), 4
sbi	_SFR_IO_ADDR(PORTB), 5
cbi	_SFR_IO_ADDR(PORTB), 5
sbi	_SFR_IO_ADDR(PORTB), 6
cbi	_SFR_IO_ADDR(PORTB), 6
sbi	_SFR_IO_ADDR(PORTB), 7
cbi	_SFR_IO_ADDR(PORTB), 7

引数が1つなら、irp/irpcの方がよさげ

.irpc bit,01234567
    sbi  _SFR_IO_ADDR(PORTB),\bit
    cbi  _SFR_IO_ADDR(PORTB),\bit
.endr

メッセージ系?

.err

これがあるとアセンブルはそこでエラー終了する。.ifなどと組み合わせて、アセンブル時ある条件でエラーを出すのに使える

.fail 54
.fail 534

指定された番号のエラーコードを出す。コード番号が500未満だとError扱いでアセンブル中止、500以上だとWarning扱いでアセンブル続行

.print message

アセンブル中メッセージを表示する

その他

.include  filename

ファイル filename をインクルードする。でもcppを通されない気がするので#includeを使った方がいいかな?