最終更新時間:2007年08月26日 13時12分53秒
avrgccでアセンブラを使う(インラインアセンブラでない方)
なぜアセンブラ?
- Cにあるいろんなオーバーヘッドを回避してコードを小さくする
- キャリーを駆使したシフト演算など、Cでは効率よくできない演算ができる
- そこにavr-asがあるから(w
アセンブラによる関数の書き方
参照
- AVR-LibCのFAQ.html-「Cコンパイラではどんなレジスタが使われるの? 」
- AVR-LibC assembler.html
- Electronic Lives Mfg.(ELM)のアセンブラ関数の書き方(avr-gcc)
使用可能レジスタの割り当て
- 関数内で使用されるレジスタ 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-1〜SP-2 | 戻りアドレス |
SP-3〜SP-6 | 変数e |
でも、ここまでやると関数もpush/popの嵐になるので素直にアドレス渡しが吉。例えば下記。
- hogehoge(uint32_t *abcde)
レジスタ | 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(429)をアセンブラで書き換えてみます。
- 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を使った方がいいかな?