AVR Libc Home Page AVRs AVR Libc Development Pages
Main Page User Manual Library Reference FAQ Alphabetical Index Example Projects

Original Page is here.

avr-libc and assembler programs

Introduction

AVRマイクロコントローラー用にアセンブラでコードを書きたい理由はいくつかあります。とりわけ、以下のようなものが挙がります。

通常、最初はより簡単に、コンパイラが持つインラインアセンブラの仕組みを使うのが一般的でしょう。

avr-libcは、AVRマイクロコントローラーはC(もしくはC++)を用いてプログラムサポートすることを目的としたものではありますが、直接アセンブラを使用する道もあります。
この利益は以下のようなものです。

Invoking the compiler

このドキュメントで説明しているような目標のために、アセンブラやリンカは直接手動で使うのではなく、Cコンパイラフロントエンド ( avr-gcc) を介してアセンブラやリンカを起動する方法で利用します。この方法は以下のような利点があります:

アセンブラに与えられるファイル名が、拡張子「.S」(sの大文字)である場合は、Cプリプロセサの起動は自動的に行われます。
これはOSのファイルシステムが、拡張子の大文字小文字を区別しないシステムである場合も有効です。
実際の拡張子が大文字Sであることの判定はコマンドラインでのファイル指定文字列に寄ります。故に、ファイルシステムが小文字のsの拡張子のファイル名を提供してきたとしても問題ありません。

明示的にアセンブラファイルにCプリプロセサを適用させるために、 -x assembler-with-cpp というオプションも用意されています。

Example program

以下の例題は、AT90S1200用に書かれた100kHz矩形波ジェネレータのソースです。
10.7MHzクロックで、ピンPD6を矩形波出力とするよう想定されています。
#include <avr/io.h>             ; Note [1]

work    =       16              ; Note [2]
tmp     =       17

inttmp  =       19

intsav  =       0

SQUARE  =       PD6             ; Note [3]

                                ; Note [4]:
tmconst= 10700000 / 200000      ; 矩形波エッジ切替周期(クロック単位):100kHz矩形波=毎秒200,000回の切替
fuzz=   8                       ; # 割込がかかってからTCNT0がセットされるまでのクロック数

        .section .text

        .global main                            ; Note [5]
main:
        rcall   ioinit
1:
        rjmp    1b                              ; Note [6]

        .global TIMER0_OVF_vect                 ; Note [7]
TIMER0_OVF_vect:
        ldi     inttmp, 256 - tmconst + fuzz
        out     _SFR_IO_ADDR(TCNT0), inttmp     ; Note [8]

        in      intsav, _SFR_IO_ADDR(SREG)      ; Note [9]

        sbic    _SFR_IO_ADDR(PORTD), SQUARE
        rjmp    1f
        sbi     _SFR_IO_ADDR(PORTD), SQUARE
        rjmp    2f
1:      cbi     _SFR_IO_ADDR(PORTD), SQUARE
2:

        out     _SFR_IO_ADDR(SREG), intsav
        reti

ioinit:
        sbi     _SFR_IO_ADDR(DDRD), SQUARE

        ldi     work, _BV(TOIE0)
        out     _SFR_IO_ADDR(TIMSK), work

        ldi     work, _BV(CS00)         ; tmr0:  CK/1
        out     _SFR_IO_ADDR(TCCR0), work

        ldi     work, 256 - tmconst
        out     _SFR_IO_ADDR(TCNT0), work

        sei

        ret

        .global __vector_default                ; Note [10]
__vector_default:
        reti

        .end
Note [1]
Cプログラムと同様、この指定でプロセッサ毎に特異的な、IOポート定義などを含んだファイルを読み込みます。
どんなインクルードファイルでもアセンブラファイル内に読み込めるわけではありません。
Note [2]
シンボル名へレジスタを割り当てています。Cプリプロセサのマクロで同じ事を行うこともできます。
 #define work 16 
Note [3]
PD6は矩形波出力するピンのビット番号です。この表現式の右辺はCプリプロセサマクロで、アセンブラに渡される前にその値(この場合は6)に置き換えられます。
Note [4]
アセンブラは表現式の整数操作をホスト定義の整数サイズ(32bitかそれ以上)で行います。
これはC-type-intを表現式整数演算の計算に使うCコンパイラとは対照的です。
100kHz出力を得るためには、PD6ポート出力を毎秒20万回切り替えなければなりません。
ここでは、要求に応える周波数で正確なタイミングを取るために、プリスケールオプションを使用しないtimer0を使っています。
これはタイミングに関して困った問題があります。
タイマーオーバーフロー割込でタイマが0に戻ったことを検知して、その後再びゼロになるまでの時間を5nsec(1/20万秒)にするような値をtimer0の値をセットすることで5usecのタイミングを取っているのですが、タイマーはその間にもカウントを続けています。そのため、割込受け入れからTCNT0のセットまでの時間を勘定に入れる必要があります。
割込がかかるまでに4サイクル、割り込みベクタのジャンプに2サイクル、TCNT0の値設定に2命令・2サイクルです。
合計8サイクル、これが定数fuzzに入れている数字(8)の意味です。
Note [5]
外部関数は.global として宣言されなければなりません。mainはアプリケーションに入口です。
リセット後、初期化ルーチン(ここではcrts1200.o ) 実行が終わったら、そこからmainにジャンプします。
Note [6]
メインループは単純な自己(ラベル 1: )への後方参照ジャンプ(rjmp 1b)で実現します。
(,メインルーチンはrjmp 1bを実行し続けるだけですが、)矩形波生成そのものは完璧にタイマ0オーバーフロー割込サービスで実現されています。
できれば、メインループ側ではアイドルモードスリープに入るコードを仕込んでもいいのですが、これほど高頻度に割込がかかる(割り込み外の実行時間が少ない)アプリケーションでは、どのみち電力節約量はたかが知れています。
※よくあるパターンとして、アイドルモードスリープに入るコードを繰り返す無限ループコードを組み、アイドルモードに入る→タイマ割込で起こされる→割込終わりメインルーチンに戻ったらアイドルモードスリープするコードを実行 という形を取ると割込ルーチン実行以外の時間の消費電力が節約されます。

Note [7]
割込関数名はC言語の割り込みルーチン記述で使われるAVRの割込名を使えます。
リンカはこれら割込名を適切な割り込みベクタスロットに当てはめます。
これらの割込ルーチンとして使用する関数名は .global として宣言されなければならないことに注意してください。
これは、 <avr/io.h> がインクルードされている場合のみ有効です。
アセンブラやリンカが正しい割込名かどうかをチェックすることはないので、念入りにチェックしてください。
(生成オブジェクトファイルをavr-objdumpやavr-nmでチェックすると、__vector_N (Nは小さな整数) といった形の名前が見えるはずです。
Note [8]
特殊機能レジスタ(SFR,special function registers)の説明で書いたように、実際のIOポートアドレスはマクロ _SFR_IO_ADDR で得なければなりません。
※in/out命令は、アドレス0x20をゼロとしたIOレジスタアドレスでなされるが、各IOレジスタ定義が持つアドレス値は、メモリマップ通りのアドレスなので、in/out命令に使うアドレスは、IOアドレスを取り出すマクロ _SFR_IO_ADDR() を通さなければならない。TCNT0は0x52、_SFR_IO_ADDR(TCNT0)は0x32。
(lds/stsやld/st命令によるメモリアクセスもできますが、) これらメモリマップドアクセスはIOアドレスによるin/out命令より遅いですし、どのみちAT90S1200 はRAMを持っていないので、メモリマップドアクセス命令が無く、不可能です・・・)

今回のTCNT0操作は大変時間制約が厳しいので、SREG退避する前にTCNT0設定処理を行っています。(※できれば割り込みルーチンの先頭が望ましい)
通常、割込時は、(割込から抜けるときに)SREGのどのビットも変更されない状態にして終了しなければなりません。
※このケースでは、intsav(r0レジスタ)にSREGを退避させ、割込終了前に書き戻しています。
Note [9]
割り込みルーチンは(割込の前後で)CPUの状態をぶちこわしてはいけません。最低でもSREGの各フラグビットの退避復帰は必須です。
(Note that this serves as an example here only since actually, all the following instructions would not modify SREG either, but that's not commonly the case.)
また、割り込みルーチン内部で使用するレジスタが、外部で使用するレジスタと衝突することがあってもいけません。今回のRAMがないAT90S1200のケースでは、これは割り込みルーチン内で使うレジスタのセットと、外部のセットを完全にわけることでしか実現できません。どこかにレジスタ内容を "save" することができないからです。
割り込みルーチンがC言語で作られたモジュールとリンクされる場合は、Cコンパイラでのレジスタ使用ガイドラインに充分注意してください。
割込内部で変更されるレジスタは(変更前に)必ずセーブされなければなりません。通常はpush命令によりスタックに保存することになります。
Note [10]
割り込みの項で説明したように、汎用の "全部引き受け(catch-all)" 割り込みベクタが、__vector_default という名前で組み込めます。
これは 明示的に.global で宣言され、reti 命令で終わらなければなりません(reti命令だけでいい)。
(0番地へのジャンプで代用されることもあります ※想定外の割込がかかったら0番地から再実行する、リセットに近い状態になる)

疑似命令とオペランド

利用可能なアセンブラの疑似命令は GNU assembler (gas) マニュアルに記述されています。マニュアルは現行のbinutilsの一部としてオンラインで公開されています。
http://sources.redhat.com/binutils/.

gas はUNIX起源であるため、偽締め入れはアセンブラ文法全般は他のアセンブラとはやや異なっています。
数値定数はCの流儀に従っています(16進数は 0xを先頭につけるなど)。表現式もCに似た文法となっています。

一般的な疑似命令には以下のものが含まれます。

gasで(強制的にコードのアドレスを指定する) .org 疑似命令は利用可能ですが、リロケータブルオブジェクトファイルを扱い、ROM/RAM上の最終アドレスはリンカが決定する環境であるアセンブラにとっては使いにくい疑似命令です。

アーキテクチャに依存しない標準的な演算子と共に、いくつかAVR独特の演算子が利用可能です。
これらは残念ながら公式ドキュメントで触れられていません。最も使える演算子は以下のものでしょう。

Example:

	ldi	r24, lo8(pm(somefunc))
	ldi	r25, hi8(pm(somefunc))
	call	something

これは関数「somefunc」のアドレスを関数「something」の第一引数(r24:r25)に渡して呼び出すものです。