/ クレア工房 / 電子工作 / AVRマイコン / AVR-GCC 導入編

AVR-GCC 導入編

$Date: 2019/06/02 04:55:42 $ (at UTC)

ここでは NetBSD (主に amd64 と i386) に avr-gcc をインストールし、 使い始めるために必要な手がかりになると思われる情報を説明しています。

記事中、 クロス開発環境の prefix として /usr/cross を指定していますが、 prefix にするパスは好みで選んでも差し支えありません。


AVR-GCC をインストールする

この記事は古くなりました。下記リンクの記事を参照してください。

過去に使っていたダウンロードスクリプト:

過去に使っていたパッチ:

コンパイルとインストールに使用するシェルスクリプトは次の通りです。


AVR-GCC を動かしてみる

gcc と binutils のインストールができたら、 簡単なソースコードを用意して試してみましょう。

$ vi add.c
$ cat add.c
int add(int a, int b)
{
  return a+b;
}

これをアセンブリ言語にコンパイルします。

$ avr-gcc -mmcu=at90s2313 -O2 -S add.c

生成されたアセンブリコードを覗いてみます。

        .file   "add.c"
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
        .text
.global add
        .type   add, @function
add:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
        add r22,r24
        adc r23,r25
        mov r24,r22
        mov r25,r23
        ret
        .size   add, .-add
        .ident  "GCC: (GNU) 4.7.2"

乗算を試してみます。

$ vi mul.c
$ cat mul.c
int mul16x16(int a, int b)
{
  return a*b;
}

ごく単純な加算ではライブラリ呼び出しを生成しませんでしたが、 見掛け上単純であっても乗算を使っている場合にはコンパイル結果の様子が変わってきます。 -mmcu=at90s2313 と指定してコンパイルすると、 avr-gcc は libgcc にあるライブラリ関数の呼び出しを生成します。

$ avr-gcc -mmcu=at90s2313 -O2 -S mul.c
$ cat mul.s
        .file   "mul.c"
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
        .text
.global mul16x16
        .type   mul16x16, @function
mul16x16:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
        rcall __mulhi3
        ret
        .size   mul16x16, .-mul16x16
        .ident  "GCC: (GNU) 4.7.2"

-mmcu=atmega88 と指定してコンパイルすると、 まったく同じソースコードでも乗算命令を使用するコードが得られます。

$ avr-gcc -mmcu=atmega88 -O2 -S mul.c
$ cat mul.s
        .file   "mul.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
        .text
.global mul16x16
        .type   mul16x16, @function
mul16x16:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
        mul r24,r22
        movw r18,r0
        mul r24,r23
        add r19,r0
        mul r25,r22
        add r19,r0
        clr r1
        movw r24,r18
        ret
        .size   mul16x16, .-mul16x16
        .ident  "GCC: (GNU) 4.7.2"

avr-gcc では、ターゲットとして使用する MCU によって ABI が異なります。 このため、コンパイラの -mmcu=name フラグでターゲットの種類を指定します。 この指示は、通常であれば Makefile の CFLAGS あたりに埋め込みます。

ソースコードは、 コンパイラに渡されたフラグを見て適切なターゲットに適応するように (できるだけ汎用に使えるよう) 記述します。 avr-libc のインクルードファイルも同様の作法で使用するように作成されています。 avr-gcc は指定されたターゲットによって (libc や libgcc の収容された) ライブラリ用ディレクトリを切り替えてリンクします。

以上でコンパイラがソースコードを食べてアセンブリ言語のコードを書き出せることを確認しました。 あとはアセンブリをバイナリに落とし込み、 足りないコードをライブラリから取り出してリンクし、 書き込み器に渡してハードウェアに書き込んでやれば目的を達成できます。

次の節では、手段の準備から実装の準備に駒を進めます。


Hello, World

PC向けのプログラムであれば参考書が山ほどありますが、 AVR の場合は printf("hello, world"); の呪文を唱えておしまいというわけにはいきません。 マイコンの世界でこれに相当するのは LED を点滅させる操作でしょう。 タイマと割り込みを使って LED を点滅させる操作には、 マイコンのエッセンスが詰め込まれています。

まず最初にアセンブリ言語での記述例を AT90S1200 を材料にして挙げ、 その次に C での記述例を ATtiny2313 を材料にして挙げます。 いずれも次の回路を想定しています。

ピン配列など、詳しくはデータシートを参照してください。 AT90S1200 であれば doc0838.pdf, AT90S2313 であれば doc0839.pdf に記載されています。

上位デバイスでは割り込みベクタの数が違いますが、 LED チカチカに必要はお作法はほとんど同じです。

アセンブリ言語でプログラムを書く

AT90S1200 を使っています。 見どころは次のとおりです。

avr-as 向けのアセンブリコードは普通に読むことができます。 i386 port や x86_64 port で見られたような、 読みにくい「AT&T スタイルの文法」に悩まされることはありません。 ディスティネーション・オペランドは正しく左側に配置されています。 レジスタ名はそのまま記述することができます。

ヘッダの内容は示しませんが、 データシートそのまんま #define しているだけですから、 適当に補完してください。

/*
 * AT90S1200 test program
 */

#include "at90s1200.h"

	.org	0
vectors:
	rjmp	init		; entry point
	rjmp	int0intr	; external int0
	rjmp	tim0intr	; timer0 overflow
	rjmp	ac0intr		; analog compare

int0intr:
	reti			; not used

tim0intr:
	in	r0, SREG	; push SREG
	lsl	r3
	brne	tim0ledout
	inc	r3
tim0ledout:
	out	PORTB, r3
	out	SREG, r0	; pop SREG
	reti

ac0intr:
	reti			; not used

init:
	; setup registers
	clr	r1		; r1 is zero register
	clr	r2		; r2 is timer register
	ldi	r16, 0x01
	mov	r3, r16		; r3 = LED pattern

	; setup PORTB
	ldi	r16, ACSR_ACD	; powerdown AC
	out	ACSR, r16	; AC control
	out	PORTB, r1	; port B data is zero
	ldi	r16, 0xFF	; b0 thru b7 is output
	out	DDRB, r16

	; setup timer0
	ldi	r16, TCCR0_CS_CK1024
	out	TCCR0, r16
	out	TCNT0, r1	; clear timer count
	ldi	r16, TIFR_TOV0
	out	TIFR, r16	; clear timer interrupt flag
	ldi	r16, TIMSK_TOIE0
	out	TIMSK, r16	; enable timer interrupt
	sei			; enable interrupt
main:
	sleep			; wait for interrupt
	rjmp	main

AVR-AS でアセンブル, AVR-LD でリンク, AVR-OBJCOPY で HEX ファイルへ

アセンブラ avr-as にはマクロ展開機能がありませんので、 適当なプリプロセッサを併用する形でソースコードを作成します。 例として C プリプロセッサ avr-cpp を通してからアセンブルします。

アセンブルした結果として得られるオブジェクトはリンカ avr-ld にまとめさせます。

リンカが出力する ELF ファイルには書き込み機が対応していませんので、 avr-objcopy を通して HEX ファイルに変換します。

一連の作業は、通常 Makefile を書いて対処します。

$ avr-cpp -mmcu=at90s1200 -o main.s main.S
$ avr-as -mmcu=at90s1200 -o main.o main.s
$ avr-ld -o test.elf main.o
$ avr-objdump -d test.elf > test.dump
$ avr-objcopy -I elf32-avr -O ihex test.elf test.hex

最終的に HEX ファイルを得ることができます。

:100000000CC002C002C008C018950FB6330C09F42A
:10001000339438BA0FBE189518951124222401E0A4
:10002000302E00E808B918BA0FEF07BB05E003BF90
:1000300012BE02E008BF02E009BF78948895FECFA7
:00000001FF

ここまでたどり着けば、一般的な書き込み器でデバイスに書き込むことができます。

C言語でプログラムを書く

次にC言語での実装例を示します。 使うコンパイラは先にインストールした avr-gcc です。 AT90S1200 は avr-gcc + avr-libc で扱うのには小さすぎるため、 ATtiny2313 をターゲットとして使っています。 回路と機能はほぼ同等です。

LED の点滅に必要な時間待ちは、 先にあげたアセンブリ言語による例と同様に割り込みを使用して実装しています。 ISR マクロで宣言されている関数が割り込みハンドラです。 ISR マクロに指定できる割り込みの名前は /usr/cross/avr/include/io*.h を眺めて確認すると良いでしょう。

定数値などはメーカが配布しているデータシート (www.atmel.com で配布されている doc0839.pdf) を参照してください。 先述のアセンブリ言語による実装例とは異なり、 ここでは avr-libc が提供しているヘッダとマクロを素直に使用しています。

/*
 * ATtiny2313 LED blinker
 */

#include <avr/io.h>
#include <avr/interrupt.h>

unsigned char phase;

ISR(TIMER0_OVF_vect)
{
  phase <<= 1;
  if (phase == 0)
    phase = 1;
  PORTB = phase;
}

int
main()
{
  phase = 1;
  ACSR = _BV(ACD); /* powerdown the analog comparator */

  /*
   * setup port B
   */
  PORTB = 0x55;
  DDRB = 0xFF; /* all output */

  /*
   * setup port D (parking)
   */
  PORTD = 0xFF; /* enable internal pullup */
  DDRD = 0x00; /* all input */

  /*
   * setup timer0
   */
  TCNT0 = 0; /* rewind the counter */
  TIFR  = _BV(TOV0); /* clear timer0 overflow flag */
  TIMSK = _BV(TOIE0); /* enable timer0 overflow interrupt */
  TCCR0A = _BV(CS02)|_BV(CS00); /* timer0 start running */

  sei(); /* set global interrupt enable */
  for (;;)
  {
    __asm__("nop");
  }

  return 0; /* shut up the compiler. */
}

とりあえずでも Makefile は書いておいた方がいいでしょう。 以下は従前例示していた NetBSD make のかわりに GNU make でも通るように修正したものです。

# Makefile for test1-attiny2313

NAME?=test1
MCU?=attiny2313
PROG=${NAME}-${MCU}
CFLAGS=-Os -mmcu=${MCU} -Wall

SRCS=main.c
OBJS=${SRCS:%.c=%.o}

TOOLDIR=/usr/cross
CC=${TOOLDIR}/bin/avr-gcc
OBJDUMP=${TOOLDIR}/bin/avr-objdump
OBJCOPY=${TOOLDIR}/bin/avr-objcopy

.SUFFIXES: .c .o .S .s

.c.o:
	${CC} ${CFLAGS} -c -o $@ $<

.c.s:
	${CC} ${CFLAGS} -S -o $@ $<

.S.o:
	${CC} ${CFLAGS} -c -o $@ $<

.s.o:
	${CC} ${CFLAGS} -c -o $@ $<

all: ${PROG}.elf ${PROG}.hex

${PROG}.elf: ${OBJS}
	${CC} ${CFLAGS} -o $@ ${OBJS}
	${OBJDUMP} -d $@ > ${PROG}.dump

${PROG}.hex: ${PROG}.elf
	${OBJCOPY} -I elf32-avr -O ihex ${PROG}.elf ${PROG}.hex

clean:
	rm -f *.s *.o *.dump *.hex *.elf

edit:
	avrdude -P usb -c avrisp2 -p ${MCU} -t

write:
	avrdude -P usb -c avrisp2 -p ${MCU} -U flash:w:${PROG}.hex

AVR-GCC でコンパイル

make すると次のコマンドが実行されます。

/usr/cross/bin/avr-gcc -Os -mmcu=attiny2313 -Wall -c -o main.o main.c
/usr/cross/bin/avr-gcc -Os -mmcu=attiny2313 -Wall -o test1-attiny2313.elf main.o
/usr/cross/bin/avr-objdump -d test1-attiny2313.elf > test1-attiny2313.dump
/usr/cross/bin/avr-objcopy -I elf32-avr -O ihex test1-attiny2313.elf test1-attiny2313.hex

リンク済みのオブジェクトを avr-objdump で逆アセンブルして、 どのようなコードにコンパイル・リンクされているのかをざっと眺めておくと良いと思います。

さて、HEX ファイルができました。

:1000000012C01FC01EC01DC01CC01BC01BC019C019
:1000100018C017C016C015C014C013C012C011C03C
:1000200010C00FC00EC011241FBECFEDCDBF10E019
:10003000A0E6B0E001C01D92A136B107E1F719D0EA
:100040002CC0DECF1F920F920FB60F9211248F9308
:1000500080916000880F09F001C081E0809360000A
:100060008091600088BB8F910F900FBE0F901F9002
:10007000189581E08093600080E888B985E588BBA9
:100080008FEF87BB82BB11BA12BE82E088BF89BFE7
:0E00900085E080BF78940000FECFF894FFCF8B
:00000001FF

これだけサイズが大きいと、 メモリ2倍の AT90S2313, ATtiny2313 が AT90S1200 とあまり変わらないように見えてしまいます。 ちょっと心許ない気がしなくもありませんが、 C で書くことによる開発効率は無視できません。 遠慮なく大容量のデバイスを投入するのが、 アマチュア的で良い方法かも知れませんね。


HEX ファイルを書き込む

現在のところ、 もっともおすすめできる「書き込み環境の確保」手段は、 市販されている AVR ライタ、 たとえばATMEL社純正の AVRISP mkII を買ってきて使うことです。 3000円程度とコストはかかりますが、 大幅に入門ハードルを引き下げられます。 製作にかかる費用や人的労力を考えますと、 AVR ライタの製作はあまりおすすめできる入門方法ではありません。

NetBSDの特殊事情として、 FTDI FT232シリーズのsynchronous bitbang modeや MPSSE modeが素直には使えないというものがあります。 これはlibusbが/dev/ugen.*しか参照しないことが原因です。 ucom(4)がattachしてしまうとugen(4)がattachしなくなり、 libusbが目的のデバイスを探索・認識できないのです。 NetBSDに限っては、基本的に、USBシリアルポートはUSBシリアルポートとしてしか使えません。

Windows PCでよく使われるArduino bootloader書き込み方法を流用したければ Windows PCの助けを借りてUSBシリアルモジュールのVID/PIDを書き換える方法があります。 VID/PIDの書き換えはモジュールの汎用性がなくなってしまう問題があるのと、 書き込む値を誤るとWindows PCですら救済できなくなるリスクがあるため、 個人的にはおすすめしません。

書き込むときだけuftdi(4)を取り除いたカーネルで起動するのも面倒ですので、 bootstrapとしてはserial port bitbangをお勧めしておきます。

Serial-port bitbanging

AVRISP mkIIなど専用のライタを持っていない場合、 速度は遅いですがserial-port bitbanging (serbb)の利用を検討します。 AE-FT232HLなどのUSBシリアルモジュールとブレッドボードと (ヒューズの都合で必要ならさらに水晶振動子と積セラ等少々の部品)で 書き込みが可能です。

AE-FT232HLなどのUSBシリアルモジュールを次のように配線します。

デフォルトのavrdude.confには含まれていませんので ホームディレクトリに.avrdudercを作成して次のように書き込んでおきます。

programmer
    id = "ft232serbb";
    desc = "serial-port bitbanging on FT232";
    baudrate = 9600;
    type = "serbb";
    reset = ~3; # TxD/AD0
    sck   = ~7; # RTS/AD2
    mosi  = ~4; # DTR/AD4
    miso  = ~8; # CTS/AD3
;

ヒューズビットの都合でライタに水晶振動子を外付けする場合、 3.3Vの電源電圧で利用できる速度は 5V時の約半分の速度までであることに留意してください。 ATtiny2313であれば8MHzが丁度よいでしょう。

この書き込み方法を用いる場合、やたらと速度が遅いので ATtiny2313などの小容量のもの専用ということにして、 AVR910系ライタを制作するための踏み台として使うのが良いと思います。

ファームウェアをこの方法で開発する場合、 ISPに使うピンをAE-FT232HLに直結しますから ISPに使うピンはマイコンの動作中においてHi-Zもしくは入力ピンでなければなりません。 RESET#が解除されてファームウェアが走り出したときに USBシリアルモジュールを壊してしまわないように注意してください。 I/Oピンの自由度を高めるためには、 RESET#をアサートしている間だけISP端子が接続されるよう 74HC4066と少々のゲートを挟み込むなどの工夫が必要です。 あるいは、1kΩ程度の保護抵抗をターゲットAVRと USBシリアルモジュールの間に挿入してください。

なお、ここではUSBシリアルモジュールの例としてAE-FT232HLを挙げましたが、 AE-FT232HLを採用した理由はAE-FT232HLがハイスピードUSB動作のためにUSBハブの トランザクショントランスレータを使用しないことができ、 USBハブを利用した場合のトラブル原因を減らすことができるからです。 また、Windows環境で重宝するであろう MPSSEを搭載していることも魅力的なデバイスです。 FT232RL等のフルスピードUSBデバイスを使う場合は適宜読み替えてください。

avrdude を使う

NetBSD では AVRISP mkII 付属の CD-ROM は使用しません。 avrdude をコンパイルしてインストールしておきます。 avrdude の稼動を確認するには、 -t オプションを渡すと便利です。 確認用のターゲットには、秋月で扱いのある AE-ATMEGA 基板が手ごろです。

次の例は avrdude で AVRISP mkII を操作します。 AVRISP mkII のような USB ライタを扱うには、通常、root になる必要があります。

# avrdude -P usb -c avrisp2 -p atmega88 -t

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.03s

avrdude: Device signature = 0x1e930a
avrdude> q
>>> q

avrdude: safemode: Fuses OK

avrdude done.  Thank you.

回路設計について

上記サンプルのプログラムでは、ISP に使う端子に LED をぶら下げているため、 負荷が重すぎて ISP できません。 ISP で使う端子はあまり重たい負荷にならないように配慮するなどして、 実際の応用では ISP できるように設計しておくと、 ソフトウェア的な試行錯誤が楽ちんになります。


Copyright © 2006, 2009, 2010, 2012, 2013, 2016, 2018 clare. All rights reserved. 本ページに掲げたサンプルプログラムは、 筆者の著作に帰すべき部分についてpublic domainに準じます。 筆者は運用結果にいかなる責任も負いません。