AVRマイクロコントローラー用にアセンブラでコードを書きたい理由はいくつかあります。とりわけ、以下のようなものが挙がります。
- RAMを持たないためにC言語サポートがないデバイス用のコードを書きたい
- 非常に厳密なタイミングを要するアプリケーションを書きたい
- Cでは表現できない特別な裏技を要するもの
通常、最初はより簡単に、コンパイラが持つインラインアセンブラの仕組みを使うのが一般的でしょう。
avr-libcは、AVRマイクロコントローラーはC(もしくはC++)を用いてプログラムサポートすることを目的としたものではありますが、直接アセンブラを使用する道もあります。
この利益は以下のようなものです。
- Cプリプロセサが使えるので、Cプログラムと同様のシンボル名(IOレジスタ名など)、Cの識別子が使える柔軟なマクロが利用できること。
(アセンブラのマクロコンセプトは、基本的にアセンブラ命令の置き換えに用いられるものである)
- 割り込みベクタ構築を自動的に割り当てられるランタイム骨格が使える。
RAMを使えるデバイスでは、RAM上変数の初期化 も利用できる。
このドキュメントで説明しているような目標のために、アセンブラやリンカは直接手動で使うのではなく、Cコンパイラフロントエンド
( avr-gcc) を介してアセンブラやリンカを起動する方法で利用します。この方法は以下のような利点があります:
- 実際に使用する言語(ここではアセンブラ)を気にせず、基本的にavrgccという1つのプログラムさえ起動すればいい。
- Cプリプロセサの起動は自動的で、適切なオプションにより、必要なインクルードファイルを読み込み追加機能を組み込める。
- リンカの起動も自動的で、適切なオプションで追加のライブラリやスタートアップコード(crt
crt
XXX.o
) やリンカスクリプトなどを読み込める
アセンブラに与えられるファイル名が、拡張子「.S」(sの大文字)である場合は、Cプリプロセサの起動は自動的に行われます。
これはOSのファイルシステムが、拡張子の大文字小文字を区別しないシステムである場合も有効です。
実際の拡張子が大文字Sであることの判定はコマンドラインでのファイル指定文字列に寄ります。故に、ファイルシステムが小文字のsの拡張子のファイル名を提供してきたとしても問題ありません。
明示的にアセンブラファイルにCプリプロセサを適用させるために、
-x assembler-with-cpp
というオプションも用意されています。
以下の例題は、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プリプロセサのマクロで同じ事を行うこともできます。
- 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に似た文法となっています。
一般的な疑似命令には以下のものが含まれます。
- .ascii 文字列終端マークがない文字列を提供する
※ .ascii "abc" →'a' 'b' 'c'
.asciz 文字列終端マークとして \0 を使用するCの文字列を提供する
※ .asciz "abc" →'a' 'b' 'c' '\0'
- .data データセクションに切り替える指定 (初期化されたRAM変数領域)
- .text テキストセクションに切り替える指定
(プログラムコード、FLASHROM上定数など)
- .set シンボルに定数を割り当てる ( .equ と同等)
- .global (or .globl) リンカから見えるパブリックなシンボルを定義する。関数エントリポイントやグローバル変数など
.extern 外部で定義されたシンボルを宣言する。 gasは全ての未定義シンボルを外部定義として扱うので、コメントとしての意味しかないですが・・・
gasで(強制的にコードのアドレスを指定する)
.org 疑似命令は利用可能ですが、リロケータブルオブジェクトファイルを扱い、ROM/RAM上の最終アドレスはリンカが決定する環境であるアセンブラにとっては使いにくい疑似命令です。
アーキテクチャに依存しない標準的な演算子と共に、いくつかAVR独特の演算子が利用可能です。
これらは残念ながら公式ドキュメントで触れられていません。最も使える演算子は以下のものでしょう。
lo8
16bit整数表現式から下位8bitを取り出す
hi8
16bit整数表現式から上位8bitを取り出す
pm
RAM上のアドレス(※1命令2番地)を、ROM上のアドレス(※1命令1番地)に変換する。2で割るのと同等。
関数のアドレス(RAM上アドレス)を、ICALL/IJMP命令になどに使えるROMアドレスの形に変換するのに用いられる。
Example:
ldi r24, lo8(pm(somefunc))
ldi r25, hi8(pm(somefunc))
call something
これは関数「somefunc」のアドレスを関数「something」の第一引数(r24:r25)に渡して呼び出すものです。