home / junkbox / avr-libc AVR libcを使ってみる

 AVRというか、ほぼArduinoに使われているCPU ATmega328Pの話題。 主にアセンブラとの関係。 ドキュメントがしっかりしている(英語だけど)ので、分からないことがあったらドキュメントを見ればたいてい解決する。

●最近のArduino統合環境を利用したコマンドラインビルド
 Version 1.6系あたりの話。 コンパイルやリンクがうまくいかないことが多々ある。 カイシャでハマって、家に帰ってきたらまた別の現象でハマるという・・・。 検索したらArduinoのフォーラムで結構みんな引っかかってるみたいなので、簡単な英語も書いておこう。 Googleさん見つけてくれるかしら?
avr-gcc: error: CreateProcess: No such file or directory
 PATH環境変数にクオートが入っているとこうなる。

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%
collect2.exe: error: ld returned 5 exit status
 これは2通りあって、コマンドプロンプトではうまくいくけれど、Cygwinではうまく行かない場合、TZ環境変数をunsetするとうまくいくようになる。 TZ変数が必要な場合は TZ=JST-9 のように設定する。

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でプログラムを組むこともできる。

●Arduino Unoとavrdude
 Duemilanove時代のIDEでは avrdude -c stk500v1 でボードにリセットがかかって自動的に書き込めたが、Uno時代のIDE付属のavrdudeでは
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。

●AVR-GCC
 -mmcu オプションでCPUを指定できる。 ATmega328Pならば-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関数に対応するアセンブラシンボルは、デフォルトでは先頭にアンダーバーが付かないようになっている。

●レジスタ
 コンパイラはR1が常にゼロである、と仮定したコードを吐く。 アセンブラソースを吐かせると、ソース冒頭で__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+

のように書かれている。 これはそのまま書く。 また、マクロではないので小文字でもよい。

●レジスタをグローバル変数として使う
 AVRはレジスタの数が多いため、レジスタを何本かグローバル変数として使用してもパフォーマンスへの影響が少なく、逆にアクセス頻度の多いグローバル変数をレジスタに確保することでパフォーマンスを改善することができる。 例えば、UART・SPIで速いビットレートを扱う場合、バッファポインタをレジスタに割り当てておくと、レジスタの復帰・退避やポインタの読み出し・保存などの処理がそっくりいらなくなるため、性能が大きく改善する場合がある。

 この場合、グローバル変数として割り当てたレジスタを使わないよう、コンパイラに指示する必要がある。 これは、

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は引数のサイズが大きい関数があると引数渡しに使われるため、注意が必要(どうなるかは自由研究で)。

 当然、プログラム全体で同じ設定でビルドしないとおかしなことになる。 特に割り込みハンドラの中でレジスタ変数を扱う場合、ライブラリも含めてリビルドしないと正しく動かないだろう。 あるいは、スタートアップ以外は(あるいはスタートアップも含めて)標準ライブラリを使わないという手もある。

●呼び出し規約
関数内で保存することなく使ってよいレジスタ: R18-R27, R30-R31 (X, Z)

リターン時に値を復帰する必要のあるレジスタ: 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
 Special Functino Register、いわゆる内蔵ペリフェラルのレジスタ。 AVRのSFRは基本的にメモリ空間にマッピングされているが、低位の64バイトはI/O空間にもマッピングされていて、どちらでもアクセスできる。 しかし、メモリ空間の最初の32バイトには汎用レジスタがマッピングされているのに対し、I/O空間にはそれがないのでアドレスが32バイトずれている。 つまり、メモリ空間で0x20〜0x5Fのレジスタが、I/O空間で0x00〜0x3Fに見える。 これらのレジスタはIN命令やOUT命令などでアクセスが可能だが、io.h ではレジスタ名がメモリ空間のアドレスで定義されいているので変換が必要になる。 このズレ(0x20)を示すマクロが__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)

とでも定義しておくとよい。

●割り込みベクタ
 Cスタートアップに既に割り込みベクタが書かれているが、.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しておくとよい。

●データをプログラムメモリに置く
 constと書いてもだめ。 なぜかはマニュアルに書いてある。 例えば、
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はプログラムメモリ上にあることになってしまう。 これではfooabのどちらかを正しく扱えない。 では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];}))

と定義されていて、式の中にブロックを作り、その中で静的変数を定義することで実現している。 この機能は定数式が要求されるところでは使えないという制限があり、静的変数の初期値もこの制限に当てはまるため、上の例でもstrstrtblstaticを付けるとエラーになる。

 strcpyputsなど、const char *を引数に取る関数は基本的にデータメモリ上のデータを扱うが、後ろに_Pの付いたstrcpy_Pputs_Pといった関数は、プログラムメモリ上のデータを扱うようになっている。

●標準ライブラリを使わない
 avr-libcの解説なのに・・・。 グローバル変数をレジスタに確保したような場合、ライブラリをビルドしなおす必要があるが、そもそもほとんどライブラリ関数を使っていない場合は、ライブラリを一切使わない、という選択肢もある。 ギリギリのパフォーマンスで動いている、小さな組み込みシステムなどはほとんどこのパターンに当てはまるだろう。 まぁ、そういう場合は全部アセンブラで書いた方がいいという話もあるが・・・。

 リンク時に-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の場合、ほとんどの整数乗算はインライン展開されるので、意外と労力は少なく済むはず。 除算は時間がかかる処理なので、ビット演算などで済むようにアルゴリズムを変更するのもよい。

●その他のTIPS
逆アセンブル
$ avr-objdump.exe -d a.out  # コードだけ
$ avr-objdump.exe -D a.out  # データも含めて
16ビット値のロード
    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のコードを共通のソースから生成している。

●どうでもいい話
 スタートアップがどうやってリンクされるか? -mmcu=オプションによって必要なスタートアップが決まり、コンパイラが.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 $