AVRというか、ほぼArduinoに使われているCPU ATmega328Pの話題。 主にアセンブラとの関係。 ドキュメントがしっかりしている(英語だけど)ので、分からないことがあったらドキュメントを見ればたいてい解決する。
This occurs if PATH environment variable is quoted.
NG: path "c:\Program Files\Arduino\hardware\tools\avr\bin";%path% OK: path c:\Program Files\Arduino\hardware\tools\avr\bin;%path%
If it works fine on command prompt, but not on Cygwin, unset TZ environment variable, or set like as "TZ=JST-9".
コマンドプロンプトでも動かない場合や、TZ環境変数をunsetしてもうまくいかない場合、うちの場合 (Win7 Pro SP1 x86) はc:\Program Files\Arduino\hardware\tools\avr\avr\bin\ld.exe のプロパティを開いて、互換性タブで Windows 7 互換で実行するようにするととりあえずうまくいった。 あるいは、1.0系統合環境のld.exeを持ってくる。
If it doesn't works on command prompt, or without TZ environment variable, do one of the following.
a: set ld.exe to compatible with Windows 7.
b: use ld.exe from Arduino IDE 1.0 series.
int main(void) { for (;;) ; }
戻り型がintなのにreturnがなくて変だけど、戻り型をvoidにすると警告が出るので。 実際には無限ループするので、returnはなくても大丈夫。 気になるなら
int main(void) __attribute__((noreturn));
でも加えておくとよい。
これを例えば、avr-gcc -W -Wall -O -mmcu=atmega328p do-nothing.cとかやるとa.out
ができるので、avr-objcopy -O ihex -R .eeprom a.out a.hexなどとすればa.hex
ができる。
ちなみに、この例ではベクタ・スタートアップルーチンも含めて200バイト以下に収まっている。
そして(きちんと設定した)avrdudeでArduinoに送りつければちゃんと動く。
普通、自分で起こしたキカイのスタートアップくらいは自分でアセンブラで書かないといけないものだが、AVRではCPUさえ決まってしまえばペリフェラルやメモリマップがほぼ決まってしまうため、これだけできちんと動くものができる。
ペリフェラルもCから叩けるので、やろうと思えば完全にCでプログラムを組むこともできる。
avrdude.exe: stk500_getsync(): not in sync: resp=0x00
と言われてうまくいかない。 一応、-c stk500v1 を指定してavrdudeを起動し、その直後にタイミングよくArduinoのリセットボタンを押せば書き込める。
でもIDEも確かavrdude使ってるはずだよなぁ、と、IDEの「ファイル - 環境設定」で「より詳細な情報を表示する - 書き込み」にチェックを入れると、
C:\〜/avrdude -CC:\〜/avrdude.conf -v -v -v -v -patmega328p -carduino -P\\.\COM7 -b115200 -D -Uflash:w:〜link.cpp.hex:i
マジデスカ。 本家のavrdude-6.0.1-mingw32に含まれるavrdude.confにもprogrammerとしてarduinoが含まれるので、どうも本家正式サポートのようだ。
ちなみに、Duemilanoveの場合はボーレートが57600bpsになる。プログラマはarduinoでよいようだ(統合環境が吐き出すコマンドラインで確認した)。 Unoの場合はブートローダーがoptibootに変わっていて、ボーレートが115200bps。
ということで、近頃のavrdudeでarduinoに書き込む場合は -c arduino としないと書き込めない。 Duemilanoveは -b 57600、Unoは -b 115200。
-mmcu=atmega328p
。
このオプションで__AVR_ATmega328P__
というマクロが定義され(たぶん)、avr/io.h をincludeすると、自動的にATmega328P用の定義がincludeされるようになっている。
拡張子が .S (大文字のS)のファイルを指定すると、Cプリプロセッサを適用した後、AVRのアセンブラを呼び出す。
したがって、拡張子が .S のアセンブラソースには、Cコメント・C++コメントや、Cプリプロセッサマクロ・プリプロセッサ指令が使える。
このとき__ASSEMBLER__
というプリプロセッサマクロが定義されるので、アセンブラではエラーになるような関数プロトタイプなどは#ifndef __ASSEMBLER__
〜#endif
で囲っておけば、アセンブラとCでヘッダファイルを共通にできる。
そして、avr-libcのio.h系のヘッダも全部そうなっている。
C関数に対応するアセンブラシンボルは、デフォルトでは先頭にアンダーバーが付かないようになっている。
__zero_reg__ = 1
というのを吐く。
predefinedされたものでも、ヘッダからincludeされたものでもない点に注意。
つまり、アセンブラソースから使う場合は自分で定義してやる必要がある。
GNUのアセンブラはレジスタが使えるところに数値を書くと、それを勝手にレジスタ番号と解釈する仕様になっているので、これで__zero_reg__
と書いたところはR1が使用される。
R1は乗算結果の上位8ビットを出力するレジスタとして暗黙的に使用されるので注意が必要である。 インラインアセンブラや、Cからアセンブラを呼び出すような場合、乗算命令を使用したあとはすみやかに(少なくとも他の関数に制御が渡る前に)R1を0に戻さなければならない。 アセンブラでR1が0であると仮定した割り込みハンドラを書いていて、割り込みハンドラ先頭でR1を0に設定していない場合、乗算命令からR1を0に戻すまでは割り込み禁止にする必要がある。 Cに吐かせた割り込みハンドラは(ISR_NAKEDを指定しない限り)ハンドラ先頭でR1を0に設定しているので、このような配慮は必要ない。
ADIW・MOVW命令などではレジスタペアを扱う。 AtmelのAVR Instruction Setマニュアルには、
adiw r25:24, 1 movw r17:16, r1:r0 sbiw r25:r24, 1
などと書かれている(書き方が一定しないが、オリジナルにそう書いてある)が、GNUのアセンブラではレジスタペアはすべて下位側のレジスタで指定することになっているので、実際には、
adiw r24, 1 movw r16, r0 sbiw r24, 1
と書く。 r26からr31は別名XL・XH・YL・YH・ZL・ZHとも呼ばれ、レジスタペアを組んで16ビットのポインタとして使われる。 これらは avr/common.h で、
#define XL r26 #define XH r27 #define YL r28 #define YH r29 #define ZL r30 #define ZH r31
のように定義されている。
したがって、movw XL, YL
で X←Y の意味になる。
マクロなので大文字と小文字が区別される。
つまり、xh とは書けない。
X・Y・ZレジスタペアはLD命令などで使う。 AtmelのAVR Instruction Setマニュアルには、
ld r0, Y+
のように書かれている。 これはそのまま書く。 また、マクロではないので小文字でもよい。
この場合、グローバル変数として割り当てたレジスタを使わないよう、コンパイラに指示する必要がある。 これは、
register unsigned char gptr asm("r2");
とすることでできる。 逆に、この宣言があればC側からはgptrでレジスタR2を参照することができる。 アセンブラでもプリプロセッサが使えることを利用して、
#ifdef __ASSEMBLER__ #define gptr r2 #else register unsigned char gptr asm("r2"); #endif
というヘッダを用意しておけば、Cからも、アセンブラからもgptrでR2を参照できる。 修正するときは両方を一度に修正しないとおかしなことになるので注意。 r2をさらに他のマクロで置き換えた方がよい。
グローバル変数として使用できるレジスタはr2からr7が安全。 r8からr15は引数のサイズが大きい関数があると引数渡しに使われるため、注意が必要(どうなるかは自由研究で)。
当然、プログラム全体で同じ設定でビルドしないとおかしなことになる。 特に割り込みハンドラの中でレジスタ変数を扱う場合、ライブラリも含めてリビルドしないと正しく動かないだろう。 あるいは、スタートアップ以外は(あるいはスタートアップも含めて)標準ライブラリを使わないという手もある。
リターン時に値を復帰する必要のあるレジスタ: R2-R17, R28-R29 (Y)
これらのレジスタが引数を渡すのに使われた場合も保存する必要がある。
呼び出し側から見ると、R16以降で値が保存されるのはR16・R17・Yだけである。
引数: C言語上で最初に書かれた引数から順に、R25:R24, R23:R22, ... R9:R8
8ビットの引数の場合は偶数番号のレジスタが使われる。
戻り値の場合と違って、この場合の奇数番号レジスタの値は不定のようだ。
レジスタで足りなくなった場合はスタックで渡される(けど、滅多になかろう)。
戻り値: 8ビットの場合R24(符号拡張あるいはゼロ拡張して上位8ビットをR25に入れる、と書いてあるがそうなっていないように見える)、
16ビットの場合R25:R24、32ビットまでR22-R25、64ビットまでr18-r25。
※R0はテンポラリ、R1は常にゼロ。
avr-gcc -O2でコンパイルするとこうなる。
; int bar(int x); ; char foo(char x) { return bar(x); } foo: clr r25 sbrc r24,7 com r25 call bar ret
8ビット引数(R24)を16ビット引数に渡すと、符号拡張が行われることがわかる。 16ビットの戻り値を8ビット戻り値とする場合、R24だけが採用されている。 もし、barの戻り値が0x00F0だった場合、先のルールなら符号拡張して0xFFF0にしなければならないはず。
; char fuga(char x); ; int hoge(int x) { return fuga(x); } hoge: call fuga mov r18,r24 clr r19 sbrc r18,7 com r19 movw r24,r18 ret
16ビット引数(R25:R24)はそのまま8ビット引数(R24)として渡されている。 8ビットの戻り値R24はR19:R18に符号拡張されてから、movwでR25:R24にコピーされて戻り値となっている。 つまり、R25は信頼されてない。 なんで一旦R19:R18に結果を作るのかは謎。
__SFR_OFFSET
で、メモリ空間からI/O空間に変換するためのマクロが_SFR_IO_ADDR
。
_SFR_MEM_ADDR
というのもあり、こちらは何も変換しない。
I/O空間でアクセスできるレジスタは、avr/io.h の中で
#define PINB _SFR_IO8(0x03)
のように定義されている。
ここでマクロの引数 0x03 はI/Oアドレスである。
_SFR_IO8
は、avr/sfr_defs.h で定義されていて、Cの場合は、
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr)) #define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
となっているので、PINBなどのマクロはCでは普通の変数のように扱えて、常にメモリ空間をアクセスする。 一方、アセンブラソースからincludeした場合は、
#define _SFR_IO8(io_addr) ((io_addr) + __SFR_OFFSET)
となっているので、これはI/O空間からメモリ空間への変換を定義している。
つまり、アセンブラからPINBなどのマクロを使うと、メモリ空間でのレジスタアドレスを返す。
これを_SFR_IO_ADDR
に渡せばもう一度I/O空間のアドレスに戻してくれる。
結局、I/O空間でアクセスできるレジスタを使いたい場合、アセンブラからは
in r16, _SFR_IO_ADDR(PINB)
のように書けばよい。
I/O空間でアクセスできないレジスタに対しては
#define UDR0 _SFR_MEM8(0xC6) // アセンブラの場合 #define _SFR_MEM8(mem_addr) (mem_addr) // Cの場合 #define _SFR_MEM8(mem_addr) _MMIO_BYTE(mem_addr)
となっているので、
lds r16, _SFR_MEM_ADDR(UDR0)
と書けばよい。 アドレス変換マクロの名前が長いと思った人は、自分で
#define IO(x) _SFR_IO_ADDR(x) #define MEM(x) (x)
とでも定義しておくとよい。
.weak
で宣言されている。
同じ名前で他にシンボルを定義すると、そちらの方が勝ち残る。
つまり、割り込みが必要なければ何も定義しなければいいし、割り込みルーチンを独自に作りたければ同じ名前で定義してしまえばよい。
ベクタの名前はリセットが__init
、あとは__vector_1
から順に10進数で名づけられている。
そして、__vector_1
などは.set
で__bad_interrupt
と定義されていて、__bad_interrupt
は__vector_default
へジャンプするコードになっている。
この__vector_default
も.weak
で宣言されているので、同じ名前で割り込みハンドラを定義すれば、デフォルトハンドラを置き換えることができる。
オリジナルの__vector_default
の値は__vectors
と定義されていて、これはベクタテーブルの先頭に打たれたラベル、つまりリセットベクタがあるアドレス(つまり0x0000)なので、放っておくとリセットがかかる。
割り込みベクタの番号をいちいち調べるのが面倒なのと、デバイスによって番号と機能の対応が違うので、実際にはマクロを使って名前を作り出す。 Cならば普通はこんな風に書く。
ISR(USART_RX_vect) { ... }
ISRマクロで定義した関数は、まずr0を保存、次いでr0を使ってステータスレジスタを保存、ゼロ固定のレジスタ(通常はr1)を保存して0クリアして関数本体を実行する。 エピローグコードはこの逆になり、リターン命令はretiになる。 試してないが、多分内部で使ったレジスタは保存してくれるのだろう。
USART_RX_vect
などは avr/io.h および avr/sfr_defs.h で以下のような定義になっている。
#define USART_RX_vect _VECTOR(18) // avr/io.h配下 #define _VECTOR(N) __vector_ ## N // avr/sfr_defs.h
つまり、USART_RX_vect
と書くと、プリプロセッサはこれを__vector_18
と展開することになる。
これはプリプロセッサを使えばアセンブラでも同じで、アセンブラシンボルの先頭にアンダーバーを付け加えない約束なので、アセンブラでもこの名前が使える。
#include <avr/io.h> .globl USART_RX_vect USART_RX_vect: reti
これで適当にリンクすれば、勝手にこのハンドラが生き残る。
同様に、デフォルトベクタは
#define BADISR_vect __vector_default // avr/interrupt.h
となっているので、CならばISR(BADISR_vect)
、アセンブラならばBADISR_vect
をラベルとして割り込みルーチンを定義すればよい。
さらに、EMPTY_INTERRUPT
というC向けのマクロもあって、名前の通り空っぽ(retiだけの)割り込みハンドラを生成するので、不意に割り込みがかかってリセットがかかるのはマズイという場合は、コードのどこかに
#include <avr/interrupt.h> // Cの場合 EMPTY_INTERRUPT(BADISR_vect) // アセンブラの場合 .globl BADISR_vect BADISR_vect: reti
と書いておけばよい。
なお、avr/interrupt.h が avr/io.h をinlcudeし、avr/io.h は avr/sfr_defs.h をincludeするようになっているのと、avr/interrupt.h にはこの他にも割り込み禁止・許可などのマクロも含まれているので、割り込みハンドラでは avr/interrupt.h をincludeしておくとよい。
int foo(const int *p); extern int a[10]; extern const int b[10]; void bar(void) { foo(a); foo(b); }
foo
の呼び出しがふたつあるが、これはどちらも問題ない。
a
はもちろんデータメモリ上にある。
もし、constでプログラムメモリが仮定されたら、b
はプログラムメモリ上にあることになってしまう。
これではfoo
はa
とb
のどちらかを正しく扱えない。
ではconstと宣言されたデータがどうなるかというと、データメモリ上に領域が取られ、初期化時にプログラムメモリからデータメモリへデータがコピーされる。
つまり、プログラムメモリとデータメモリの両方の領域を食う。
これを、プログラムメモリ上だけで済ませるには以下のようにする。
const int b[10] PROGMEM = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
PROGMEM
はGCCのアトリビュートを使って以下のようになっている。
#define __ATTR_PROGMEM__ __attribute__((__progmem__)) #define PROGMEM __ATTR_PROGMEM__
これでデータはプログラムメモリ上に置かれるのだが、今のところAVR GCCはプログラムメモリ上のデータの読み方を知らない(超重要)。
放っておくとb
の置いてあるプログラムメモリ上のアドレスを使って、データメモリを読みに行ってしまう(とりあえず「gcc version 4.3.2 (WinAVR 20081205)」では)。
このデータを読むためには、明示的にアドレスを取ってプログラムメモリを読むコードを書かなければならない。
#include <avr/pgmspace.h> int foo(const prog_int16_t *p) { return pgm_read_word(&(p[5])); // もちろん pgm_read_word(p+5) でもよい。 }
これでLPM命令に展開される。
今のところ、アトリビュートの違いで警告が出たりはしないので、間違ったデータを渡さないように注意する必要がある。
prog_int16_t
などはavr/pgmspace.hの中で、
typedef int16_t prog_int16_t PROGMEM;
のように定義されている。
グローバル変数の場合、
const char *st[] PROGMEM = { // ポインタはプログラムメモリ上だが、 "abcdefg", // 文字列はデータメモリ上。 "hijklmn" };
のようにするのだが、これでは不十分で、
const char str1[] PROGMEM = "abcdefg"; const char str2[] PROGMEM = "hijklmn"; PGM_P *st[] PROGMEM = { str1, str2 };
などとする必要がある。
関数の中ならば
void hoge(PGM_P s); void fuga(PGM_P *tbl); void foo() { PGM_P str = PSTR("opqrstu"); PGM_P strtbl[] = { PSTR("abc"), PSTR("def"), NULL }; hoge(str); fuga(strtbl); puts_P(PSTR("vwxyzzz")); }
などと書ける。
PSTR
はGNU Cコンパイラの拡張機能を使って
# define PSTR(s) (__extension__({static char __c[] PROGMEM = (s); &__c[0];}))
と定義されていて、式の中にブロックを作り、その中で静的変数を定義することで実現している。
この機能は定数式が要求されるところでは使えないという制限があり、静的変数の初期値もこの制限に当てはまるため、上の例でもstr
やstrtbl
にstatic
を付けるとエラーになる。
strcpy
・puts
など、const char *
を引数に取る関数は基本的にデータメモリ上のデータを扱うが、後ろに_Pの付いたstrcpy_P
・puts_P
といった関数は、プログラムメモリ上のデータを扱うようになっている。
リンク時に-nostdlibを付ければ、標準ライブラリはリンクされなくなる。 スタートアップ・定数の初期化・BSSセクションのクリア・割り込みベクタなどは全部面倒を見る必要がある。 実行はリセットベクタから始まるので、ベクタ先頭に初期化ルーチンへのジャンプを書く(CPUによって使うべきジャンプ命令が違うので注意)。 AVR-libcのスタートアップコードを見ると、スタートアップはR1のクリア、スタックの設定と、ステータスレジスタもゼロにクリアしている。
定数の初期化はプログラムメモリからSRAMへのコピーを書くことになる。
これはlibgccに含まれていて、プログラムメモリ上にある__data_load_start
から__data_load_end
までのデータを、__data_start
にコピーしている(実際の終了判定は転送先が__data_end
になるまで、だが)。
これらのシンボルはリンカスクリプト(例えば lib/ldscript/avr5.x )で定義されていて、__data_load_start
はプログラムメモリ上の初期値テーブルの先頭アドレス、__data_start
はデータメモリ上の実データアドレスになっている。
これが面倒な場合、初期値テーブルはすべてプログラムメモリ上に作成し、データメモリ上の変数には一切初期値を指定せず、プログラム中で明示的に初期化すればよい。
値を変更する必要がなければもちろんpgm_read_word
などで直接アクセスすればよい。
同様に、BSSセクションのクリアは__bss_start
から__bss_end
までを0にクリアしている。
これも自分でちゃんと変数を初期化してから使えば必要ない。
データセクション・BSSセクションとも、終了アドレスは転送すべき最後のバイトの次のアドレスを示している。
-nostdlibが意味を持つのはリンク時だけなので、整数除算・浮動小数点演算などを使ったプログラムをコンパイルすると、普通にライブラリ関数を呼ぶコードが生成される。 これらは自分で相当品を書くか、明示的に関数呼び出しで書き直して、自分で演算関数を書けばよい。 乗算命令のあるAVRの場合、ほとんどの整数乗算はインライン展開されるので、意外と労力は少なく済むはず。 除算は時間がかかる処理なので、ビット演算などで済むようにアルゴリズムを変更するのもよい。
$ avr-objdump.exe -d a.out # コードだけ $ avr-objdump.exe -D a.out # データも含めて
ldi r16, lo8(LABEL1) ldi r17, hi8(LABEL1)
GCCでは一般的に、touch foo.h; cpp -dM foo.hで調べられる(man gccより)。gccではなくcppなので注意。 avr-cppでこれをやると・・・。
$ touch foo.h; avr-cpp -dM -mmcu=atmega328p foo.h ・・・ #define __AVR_MEGA__ 1 #define __AVR_2_BYTE_PC__ 1 #define __AVR_ENHANCED__ 1 #define __AVR_ARCH__ 5 #define __AVR_HAVE_MUL__ 1 #define __AVR 1 #define __AVR_HAVE_LPMX__ 1 #define AVR 1 #define __AVR__ 1 #define __AVR_HAVE_JMP_CALL__ 1 #define __AVR_HAVE_MOVW__ 1 #define __AVR_ATmega328P__ 1
のようなマクロが定義されている。 avr-libcもこれらのマクロを使って、多くのCPUのコードを共通のソースから生成している。
.global __do_data_copy
のようにグローバルシンボルを宣言するコードを吐く。
吐いているのは.global
だけで、実体もないし、参照もされていない。
メモリ初期化ルーチンはこのシンボルを元にlibgccからリンクされる
(GCCのバージョンとAVRのアーキテクチャによってはavr-libcからリンクされる、かもしれない)。
参照されていないのにどうやって実行されるのか? 実はメモリ初期化ルーチンが配置されるサブセクションは決まっていて、その直前のサブセクションがエピローグなしで終わっている(ジャンプもリターンもない)ので、リンクされると勝手に制御が流れてきて実行されるようになっている。 ちなみに、このシンボルはリンク時に見つからなくてもエラーにならない(参照されてないから)。 リンクされなければメモリ初期化のサブセクションは飛ばされて、そのまま次のサブセクションへ進む。
avr-libcのスタートアップはほぼひとつのソースからマクロ定義で各AVR用のものが生成される。
どのスタートアップが使用されるかはやはり-mmcu=で決まり、コンパイラドライバが正しいスタートアップのモジュール名をリンカに渡している。
スタートアップが初期化のためのアドレスを知る手段は先に説明した通りで、リンカスクリプト内にシンボル定義が書いてある。
また、データセグメントにAT
を指定して、初期化データがテキストセグメントの直後に吐き出されるようになっている。
間違ってるかもしれないけど、大体こんな感じだった。
Copyright (C) 2012-2015 akamoz.jp
$Id: avr-libc.htm,v 1.5 2015/11/10 14:26:07 you Exp $