AVRのプログラミング



ここでは各種プログラミング方法について項目別に例を挙げたいと思います。ここではAT90S2313,AT90S8515,AT908535などを使用したいと思います。早くmegaAVRのnew coreはいってこないかなあ。また主にAVR-GCCも使用していきます。GCCを使う場合は例から見たほうがいいかも知れません。

仕様の説明についてはDataSheetの私的和訳要約です(笑)。

  お品書き 

● AVRのメモリ構造と命令セット
● 割り込みプログラム

● EEPROMやプログラムメモリの使用
● 8515で拡張RAMをつなぐ
● シリアル通信
● AD変換
● AVR-GCCのインラインアセンブラ


   メモリ構造と命令セットについて

●AVRのメモリー構造

AVRのメモリ構造はProgramMemoryとDataMemoryが分かれているHarvard architectureとなっています。これにより命令のPreFetchと計算結果の書き込みをパイプライン化しています(16+8=24bitのバスを持っていることになります)。ProgramFlash,Register,I/O,SRAMの構造は次のようになっています。at90s2313の例です。

Program Memory

Program Flash
(1K*16)

$000

 

 

 

 

 

 

 

$3FF

Data Memory

32Gen.Purpose
Working Registors

$00

$1F

64 I/O Registers

$20

 

$5F

SRAM
(128*8)

$60

 

 

$DF


CPUレジスタ(32本)、I/O(64byte)、SRAM(128byte)が同じ空間に配置されています。ロードストア命令時にはこれらのレジスタにリニアにアクセス可能です。プログラムメモリは16bit境界でアドレッシングされています。

●命令セット(アセンブラ)

・General Purpose Registerについて
32本のレジスタはほぼ同等の機能を持っています。リトルエンディアンです。
(1) 即値計算、代入(LDI)、ビット操作(SBR,CBR)はR16-R31のみ。即値コードに値を含む。アドレス直接ロードストア(LDS,STS)、bit skip(SBRC,SBRS)はすべてのレジスタで可能。
(2) 16bit即値計算はR24-R31のみ。ただし値は0-63まで。
(3) インデックスポインタはR26-R31を使ったX,Y,Zの3本。
(4) X,Y,Zを使ったロードストアはPostIncrement,PreDecriment可能(LD命令)。Y,Zのみdisplacementが使える(LDD命令)。ProgramMemoryロード(LPM)はZ使用のみ可能。
(5) 掛け算命令(MUL,MULS,MULSU,FMUL,FMULS,FMULSU)の結果はR0:R1に入る。FMUL系はDSPで使われる小数演算。

・I/O命令について
(1) IN,OUT命令によるI/OアクセスはI/Oアドレスで行う。I/O命令アドレスの$00はDataMemoryアクセス(ロードストア)時の$20と同じ。
(2) I/Oアドレスでのbit操作命令(CBI,SBI)、スキップ命令(SBIC,SBIS)はI/O$00-$1Fまでのみ可能。STATUSレジスタ(I/O$3F)は専用命令を持つ。

・STATUSについて
(1) STATUS(I/O$3F)レジスタのbit操作は専用命令がある(基本はBSET,BCLR)。
(2) STATUSによる分岐命令は前後64命令まで(基本はBRBC,BRBS)。

・比較分岐命令について
(1) 16bit以上に比較分岐に便利なように Z flag が伝播する引き算命令(CPC,SBC,SBCI)がある。おもにキャリーつき引き算で Z flag が伝播します。
(2) 比較のみ行う命令(CP,CPC,CPI)がありさらに比較スキップ(CPSE)がある。
(3) 値が0あるいはマイナスであることを調べる命令(TST)がある。
(4) ジャンプ(RJMP,JMP,IJMP)、コール命令(RCALL,CALL,ICALL)はR系が前後2K, normal系は64Kword, I系はZインデックス。


   割り込みプログラム

割り込みプログラムを書いてみます。今回はタイマー割り込みを使用したいと思います。

●AVRの割り込みシステムについて

AVRは割り込みベクタ形式で割り込みがかかります。ベクタの配置はAT90S2313の場合次のようになっています。AddressはAVRの場合2バイト境界となっています。通常はrjmp命令で埋めるようです。この順番は割り込みのpriority順となっています。at90s2313の例です

VectorNo.
ProgramAddress Source InterruptDifinition

1

$000
RESET Hardware Pin,Power-on Reset and Watchdog Reset

2

$001
INT0 External Interrupt Request 0

3

$003
INT1 External Interrupt Request 1

4

$004
TIMER1 CAPT1 Timer/Counter1 Capture Event

5

$005
TIMER1 COMP1 Timer/Counter1 Compare Match

6

$006
TIMER1 OVF0 Timer/Counter1 Overflow

7

$007
TIMER0 OVF0 Timer/Counter0 Overflow

8

$008
UART,RX UART,RX Complete

9

$009
UART,UDRE UART Data Register Empty

10

$010
UART,TX UART,TX Complete

11

$011
ANA_COMP Analog Comparator

割り込みは開始(ベクタのRJMP命令実行)までに4clock、割り込み終了後(RETI実行後)4clockかかります(スタックの復帰退避などのため)。もし同時に割り込みが起こった場合は優先順に従って次々と割り込み処理されていきます。

●割り込みに関するレジスタ

またそれらに付随して、割り込みマスク、割り込みフラグがあります。括弧内はI/Oアドレスです。
・割り込みマスクは1をセットすると割り込み可能になります。0でマスク状態です。
・割り込みフラグは割り込みが起こったときにセットされ、割り込みハンドリング時に自動でクリアされます。割り込みをマスクしていた場合にはフラグは立ったままとなります。

・SREG ($3F) : Status Register

7

6

5

4

3

2

1

0

I

T

H

S

V

N

Z

C
bit7-I : Global Interrupt Enable
すべての割り込みを可能にするにはこのビットを1にする必要があります。割り込みがかかるとクリアされ、割り込みルーチンの終わりでRETI命令がかかると同時に再びセットされます。CEI,SEI命令でクリア,セットできます。

・GIMSK ($3B) : General Interrupt Mask Register

7

6

5

4

3

2

1

0

INT1

INT0

-

-

-

-

-

-
bit7-INT1 : External Interrupt Request 1 Enable
外部割込みINT1の割り込みマスクです。
bit6-INT0 : External Interrupt Request 0 Enable
外部割込みINT0の割り込みマスクです。
なお外部端子割り込みは出力端子に設定していても割り込みはかかります。

・GIFR ($3A) : General Interrupt FLAG Register

7

6

5

4

3

2

1

0

INTF1

INTF0

-

-

-

-

-

-
bit7-INTF1 : External Interrupt Flag 1
bit6-INTF0 : External Interrupt Flag 0

外部割込みがかかったときに1になります。ただしレベル割り込みはフラグしません。

・MCUCR ($35) : MCU Control Register

7

6

5

4

3

2

1

0

-

-

SE

SM

ISC11

ISC10

ISC01

ISC00
bit3-ISC11,bit2-ISC10 : Interrupt Sense Control 1
bit1-ISC01,bit0-ISC00 : Interrupt Sense Control 0

外部端子割り込みの条件を設定できます。

ISC11,ISC01

ISC10,ISC00
効果

0

0
Low levelの間割り込みがかかります

0

1
予約

1

0
falling edgeで割り込みがかかります

1

1
rising edgeで割り込みがかかります
このビットを変更するときはGIMSKをdisableにしておかないとビットを変更したときに割り込みがかかってしまうそうです。エッジやレベルはCPUクロックより長くないと保証されないそうです。

・TIMSK ($39) : Timer/Counter Interrupt Mask Register

7

6

5

4

3

2

1

0

TOIE1

OCIE1A

-

-

TICIE1

-

TOIE0

-
bit7-TOIE1 : Timer/Counter1 Overflow Interrupt Enable
タイマー1のオーバーフロー割り込みマスクです。
bit6-OCIE1A : Timer/Counter1 Output Compare Match Interrupt Enable
タイマー1のOCR1AH($2B),OCR1AL($2A)の値によるCompare/Match時の割り込みマスクです。
bit3-TICIE1 : Timer/Counter1 Input Capture Interrupt Enable
外部端子(IPCピン)のLoのタイミングでカウンタをICR1H($25),ICR1L($24)に転送するときにかかる割り込み(InputCapture)のマスクです。
bit1-TOIE0 : Timer/Counter0 Overflow Interrupt Enable
タイマー0のオーバーフロー割り込みマスクです。

・TIFR ($38) : Timer/Counter Interrupt FlLAG Register

7

6

5

4

3

2

1

0

TOV1

OCF1A

-

-

ICF1

-

TOV0

-
bit7-TOV1 : Timer/Counter1 Overflow Flag
タイマー1のオーバーフローフラグです。PWMモード時はカウント値$0000でカウント方向が変わるときにセットされます。
bit6-OCF1A : Output Compare Flag
タイマー1のCompare/Matchが起こったときのフラグです。
bit3-ICF1 : Input Capture Flag
Input Captureイベントが起こったときのフラグです。
bit1-TOV0 : Timer/Counter0 Overflow Flag
タイマー0のオーバーフローフラグです。

●AVR-GCCで割り込みを掛ける

実際にAVR-GCCで割り込みプログラミングしてみます。回路はAVR-startのページのものを使用しました。

仕様は「タイマー割り込みを利用してLEDを点滅させる」です。at90s2313のTMR0でタイマー割り込みをかけてみました。

・プログラム

プログラム例です。ファイルはこれです。出力アセンブラリストはこれです。
avr-gcc -mmcu=at90s2313 inttimer.c -o inttimer.elf
avr-objcopy -O ihex inttimer.elf inttimer.hex
でromイメージができます。同じようにLEDが点滅すれば成功です。

/**** interrupt function ****/
SIGNAL(SIG_OVERFLOW0)
{
・・・・
}

のように割り込みルーチンを記述します。
sig-avr.hに割り込みルーチンのSIGNAL定義があります。
interrupt.hにsei()命令(global interrupt enable flag set)があります。
プログラムではカウンタクロック源を内部CL/1024にセットし割り込みタイマー割り込みマスクをはずし(1をセットし)、グローバル割り込みを許可にするといった手順です。

・印象

できたコードが長い!!豪快にpush-popしてます。(アセンブラなら短いのに...)
PICと比べて・・・レジスタが多い分コンテクストの退避復帰が大変。けど割り込みシーケンスやCPU速度を考慮すれば実行時間はあまり変わらない?ただしアセンブラでは互角ではないでしょうか。

・追伸

大変なミスをしてました。最適化オプションをつけてませんでした。
avr-gcc -Os -mmcu=at90s2313 inttimer.c -S
としてコードサイズで最適化してみるとこのようになりました。かなり最適化できてるようです。(すげー。)


   EEPROMとプログラムメモリの使用

AVRのEEPROM使用とプログラムメモリのconst dataとしての使用について書きます。

●EEPROMに関するレジスタ

EEPROMに対してはI/Oレジスタを通じでアクセスします。at90s8535の例ですがat90s2313の拡張版のようなものです(アドレス等は同じです)。

・EEARH,EEARL ($1F,$1E) : EEPROM Address Register
EEARH ($1F)

7

6

5

4

3

2

1

0

-

-

-

-

-

-

-

EEAR9
EEARL ($1E)

7

6

5

4

3

2

1

0

EEAR7

EEAR6

EEAR5

EEAR4

EEAR3

EEAR2

EEAR1

EEAR0
EEPROMにアクセスするときにアドレスをセットします。

・EEDR ($1D) : EEPROM Data Register

7

6

5

4

3

2

1

0

MSB

 

 

 

 

 

 

LSB
EEPROMにアクセスするときのデータをセットします。書き込み中は変更してはいけません。また読み出したデータが入ります。

・EECR ($1C) : EEPROM Control Register

7

6

5

4

3

2

1

0

-

-

-

-

EERIE

EEMWE

EEWE

EERE
bit3-EERIE : EEPROM Ready Interrupt Enable
このビットを1にするとEEWEがクリアされているときに割り込みがかかります。
bit2-EEMWE : EEPROM Master Write Enable
このビットを1にしてEEWEをセットすると、EEPROMへの書き込みプロセスが始まります。4クロックでハードウェアクリアされます。
bit1-EEWE : EEPROM Write Enable
EEMWEがセットされ、かつ、このビットに1をセットすると書き込みを開始します。このときCPUは2clock停止します。書き込み時間はVcc=5Vの時約2.5msだそうです。書き込み終了でクリアされます。書き込みシーケンスは次のようになります。
1. EEWEが0になるまで待つ
2. EEARHとEEARLにアドレスを書き込む(必要あらば)
3. EEDRにデータを書き込む(必要あらば)
4. EEMWEを1にセットする
5. 4から4クロック以内にEEWEを1にする
割り込み処理内とメインルーチンの双方でEEPROMアクセスするときはレジスタの書き換えをお互いにしてしまったり、4clock内の処理ができないことがあるので書き込みに失敗するそうです。割り込み禁止にするなどの処理が必要です。
bit0-EERE : EEPROM Read Enable
EEARH,EEARLにアドレスをセットした後EEREをセットするとデータがEEDRに1clockで読み出されますのでポーリングする必要はありません。EEREがセットされるとCPUは4clock停止します。ただし書き込みシーケンスと同時に実行はでないので、読み込み前はEEWEが0になっている必要があります。

・Errata
書き込み中にリセットがかかったとき(BODなども含む)0x00番地にデータが書き込まれてしまいます。0x00番地は使わないようにとのことです。

●プログラムメモリのconstデータとしての使用

アセンブラではLPMを使います。

●AVR-GCCでプログラムメモリconstおよびEEPROMを使う

AVR-GCCにはEEPROMアクセスのための関数があります。それを利用しました。LCDでチェックしたので今回はat90s8535を使用しました。LCDとLEDの配線はlcd.hにあります。

・プログラムの例

例はこれです。またLCDライブラリとMakefileをまとめたものはこれです

・PROGMEM プログラム

プログラムメモリconstを使うためにprogmem.hをincludeします。グローバルとして定義するときはPROGMEM修飾をつけます。

/*program memory constants*/
PROGMEM char hello[] = "Hello AVR world.";

読み出すときは

char c;
c = PRG_RDB(&hello[i]);

のようにPRG_RDB(address)というマクロを使用します。またプログラム中にconst値を埋め込むときは、

/*pointer access*/
char *s;
s = PSTR("EEPROM test");	/*get pointer for inline progmem const value*/
c = PRG_RDB(s++);

のようにポインタを取得して読み込みます。

・EEPROM プログラム

eepromライブラリを使うためにeeprom.hをincludeします。命令は

eeprom_is_ready() : 読み書きの準備ができているとき1そうでないとき0を返す。
unsigned char eeprom_rb(unsigned int addr) : バイト読み込み。
unsigned int eeprom_rw(unsigned int addr) : ワード読み込み(little endian)。
void eeprom_wb(unsigned int addr,unsigned char val) : バイト書き込み。
void eeprom_read_block(void *buf,unsigned int addr,size_t n) : ブロック読み込み。

があります。まず読み書き準備ができているか確認して、読み書きします。


   AT90S8515でSRAMを拡張する

8515など一部のAVRはデータSRAMを拡張することができます。このような1chipマイコンで拡張RAMをつけることに意義があるかは別として、外部ペリフェラルなどもメモリマップドIOとして接続できるようになります。8515は8051の代替品として位置付けられているからでしょうか。

増設されたメモリは内蔵RAMの後ろにくっつき、間接アクセス(ポインタ)などで普通の命令でアクセスできるようになります。

●拡張SRAMに関するレジスタ

拡張RAMを有効にするためには最初にレジスタの設定をしなくてはいけません。といっても1つだけです。

・MCUCR ($35) : MCU Control Register

7

6

5

4

3

2

1

0

SRE

SRW

SE

SM

ISC11

ISC10

ISC01

ISC00
bit7-SER : External SRAM Enable
これを1にすると外部拡張SRAMが有効になります。このときAD0-7(PortA),A8-15(PortC),~WR,~RD(PortD)の設定も自動でオーバーライドされバスになります。
bit6-SRW : External SRAM Wait State
これを1にすると外部SRAMに対して1ウェイトが入ります。通常は3サイクルですが、ウェイトを入れると4サイクルになります。

●拡張SRAMの接続

インテルの8086のようにアドレスバスとデータバスがマルチプレクスされていますので、トランスペアレントラッチで分離します。8086とおなじように74HC573を使用しました。回路図はこれです。SRAMは62256-70互換品(32KByte)を使用しました。こっちは田舎なので980円もしました。(そんなばかなっ。)

これがその写真です。一番下のAVRと同じくらい自己主張してるやつがSRAM(しつこいですが980円)です。AVRとの間にラッチがはさっまっています。シリアルポートはほとんどRS485状態なのでステレオジャックにしてしまいました。SRAMはMOSEL VITELICとかのやつでスタンバイ時20uAです。かつ35ns品まであるそうな。

●AVR-GCCで拡張SRAMを認識させる

拡張SRAMをつないだらそれを使用できなくてはいけません。アセンブラでしたら単にMCUCRのフラグを立てるだけですが、Cコンパイラの場合は自動でメモリを割り振るので認識させないといけません。それには以下の3つの過程が必要です。これらはローダで使用されます。

(1) メモリの大きさを認識させる
まずメモリの大きさですが、/avr/lib/ldscriptsにローダスクリプトがありますのでavr85xx.xをカレントディレクトリに移動させ、たとえばex_avr85xx.xという名前に変更してこれを使用します。さいしょにMEMORYの指定がありますのでこれを

MEMORY
{
  text   (rx)   : ORIGIN = 0,    LENGTH = 8K
  data   (rw!x) : ORIGIN = 0x800060, LENGTH = 32K
  eeprom (rw!x) : ORIGIN = 0,    LENGTH = 512
}

のように大きくします(変更はこれだけ)。それをロードさせるようにします。avr-gccプリプロセッサから読み込むときは(これが推奨されているようです) -Wl,--script,ex_avr85xx.x オプションを加えます。

(2) スタートアップでSRAMを有効にする
スタートアップコード内(gcrt1.S)でMCU Control RegisterやWDTのレジスタ設定を行っていますが、これらは .weak __init_mcucr__ で定義されていますのでプログラムのロード時にこれをオーバーライドできます。最後にスタートアップコード(crt8515.o)とオブジェクトファイルをまとめるときに(*.oから*.elfにまとめるときに)ローダオプションで指定しました。なぜか直接書き込んでもうまくいきませんでした。avr-gccプリプロセッサからまとめるときは -Wl,--defsym,__init_mcucr__=0xC0 としてローダへ渡します。0x80にするとNO-Waitになります。

(3) スタックポインタの場所を拡張メモリの最後に置く
さらにスタックポインタも設定します。そのままでもグローバルとして領域を取ることはできますが、これをしないとローカル変数として大きな領域を取ることができません。ただしスタックが外部RAMになるとどうしてもアクセスが遅くなります。(2)と同じく __stack が .weak として宣言されているのでオーバーライドします。たとえば -Wl,--defsym,__stack=0x7fff のようにメモリの最後に指定しています。(この場合メモリの最後は0x825fのほうでしょうか?誰か教えてください)

●プログラム例

これらをまとめたものをここにおいておきます。ローダスクリプトとMakefileです。Makefile内で
#linker flags
LDFLAGS = -Wl,-Map=$(TARG).map,--cref -Wl,--script,ex_avr85xx.x -Wl,--defsym,__init_mcucr__=0xC0 -Wl,--defsym,__stack=0x7fff
としています。プログラムはint型で8192個の配列を確保してチェックしています。

・追伸(2000/08/24)
よく考えると、せっかくバスがあるからLCDもそっちにぶら下げればよかったです。ぼけてました。


   シリアル通信(UART)

シリアル通信の方法はPICと同じように簡単です。IOのアドレスが各種AVR間で同じなので可搬性がよくて助かっています。どうでもいいからまず動作確認したい場合は以下の命令だけでOKです。IOの入出力なんかもオーバーライドされます。

	/* enable RxD/TxD */
	outp(BV(RXEN)|BV(TXEN),UCR);
	/*9600bps at 8Mhz*/
	outp(51,UBRR);
	/*put char*/
	outp('#',UDR);

これを焼いて走らせてtera termなんかでモニタしたら取り合えず#が画面に映るはずです。(BVはビット数を数値に直す命令です。たとえばBV(7)は0x80になります。)

●UARTに関するレジスタ

4つのレジスタがあります。

・UCR($0A) : UART Control Register

7

6

5

4

3

2

1

0

RXCIE

TXCIE

UDRIE

RXEN

TXEN

CHR9

RXB8

TXB8
bit7-RXCIE : RX Complete Interrupt Enable
これを1にしてデータを受信すると受信後に受信終了割り込みベクタから割り込みがかかります。
bit6-TXCIE : TX Complete Interrupt Enable
これを1にしてデータを送信すると送信後(データ送信シフト後)に送信終了割り込みベクタから割り込みがかかります。
bit5-UDRIE : UART Data Register Empty Interrupt Enable
これを1にするとデータ送信レジスタからシリアル出力のシフトレジスタにデータが移った段階での割り込みがかかります。
bit4-RXEN : Reciever Enable
これを1にすると受信可能状態になり、IOピンがオーバーライドされシリアル受信ピンになります。
bit3-TXEN : Transmitter Enable
これを1にすると送信可能状態になり、IOピンがオーバーライドされシリアル送信ピンになります。
bit2-CHR9 : 9-bit Characters
これを1にすると9bit送受信になります。そのデータはこのレジスタ内のRXB8,TXB8で処理します。
bit1-RXB8 : Receive Data Bit 8
9bit通信時の9bit目の受信データです。
bit0-TXB8 : Transmit Data Bit 8
9bit通信時の9bit目の送信データです。

・UBRR($09) : UART BAUD Rate Register

7

6

5

4

3

2

1

0

MSB

 

 

 

 

 

 

LSB
送受信速度設定用レジスタです。簡単に表にするとCK=8MHzの時は
Baud 4800 9600 19200 38400 57600 115200
UBRR 103 51 25 12 8 3
のようになります。(ただし、115200bpsはあやしげでした。)

・UDR($0C) : UART I/O Data Register

7

6

5

4

3

2

1

0

MSB

 

 

 

 

 

 

LSB
送受信用のI/Oレジスタです。送受信ともにこのレジスタを使います。つまり送受信で2重になっていることになります。

・USR($0B) : UART Status Register

7

6

5

4

3

2

1

0

RXC

TXC

UDRE

FE

OR

-

-

-
bit7-RXC : UART Receive Complete
1キャラクタを受信するとこのフラグが1になります。UDRよりデータを読み込むと自動で0に戻ります。フレーミングエラーが起こってもフラグは立ちます。受信割り込み使用時も割り込みフラグと連動しているため一度UDRを読んでこのフラグを0に戻さないと次の割り込みがかかりません。
bit6-TXC : UART Transmit Conplete
送信シフトレジスタが空でUDRに書き込みがないときに1になります。これは半2重通信の時に役に立ちます。TXCIEを併用している場合は割り込み時に自動でクリアされます。また書き込んで0にすることもできます。
bit5-UDRE : UART Data Register Empty
UDRにデータが送信シフトレジスタに移ったときに1になります。すなわちこのレジスタが1のときUDRにデータを書き込むことができます。UDRIEを併用しているときは必ずUDRにデータを書き込まないとずっと割り込みがかかりっぱなしになってしまいます。
bit4-FE : Framing Error
直前のキャラクタのSTOP BITが0の時(つまりエラーのとき)このフラグが1になります。次のデータが正常だった場合再びこのフラグが0に戻ります。
bit3-OR : Overrun
受信シフト終了後に一つ前のデータがUDRに残っていたときこのフラグが立ち、そのときUDRに入りきれなかったデータは失われ以前のデータがそのまま残ります。UDRのデータを読み込むと0に戻ります。

●GCCによる実装

一つ前の増設SRAMのプログラムにポーリングによる送受信例があります。単純に受信したデータをエコーするだけです。LCDに表示するのも受信データをLCD_data(char c)で表示するだけですので簡単にできると思います。普通のターミナルソフト(tera termなど)を意識して、リターンコードだけエコー時に行送りも追加するようにしています。tera termはシリアルポートも使えるのでので便利です。Linuxだったらkermitでしょうか。
割り込み駆動の例はAVR-libcの中にprintfを送信するものが入っています。


   AD変換

ADCのついているAT90S8535でテストします。このチップには8入力マルチプレクサつきの10bitADCがついています。またフリーランモードで1クロック早く変換することもできるようです。もともと消費電力が小さいので大して効果はありませんが、変換後の割り込みを利用して変換中にCPUをスリープさせ、ノイズを減らすこともできます。

●AD変換に関するレジスタ

AD変換に関してもI/Oレジスタを通してアクセスします。コントロール1本、チャンネル設定1本、16bitデータレジスタ1本のシンプルな構成です。

・ADCSR($06) : ADC Control and Status Register

7

6

5

4

3

2

1

0

ADEN

ADSC

ADFR

ADIF

ADIE

ADPS2

ADPS1

ADPS0

bit7-ADEN : ADC Enable
1を書き込むとAD変換が可能になります。変換中に0にするとその変換を終えてからADCがoffになります。最初にADCをonにしたときは同時にADSCも1にしてADC初期化のためのダミー変換(26cycle)を行うといいでしょう。最初の変換は初期化サイクルとなります。
bit6-ADSC : ADC Start Conversion
シングルモード(14cycle)の時は1を書き込むと変換開始し変換開始後自動で0になります。
bit5-ADFR : ADC Free Run Select
1にすると連続して変換します。このとき13cycleで最速の変換になります。これを1にしてADSCを1にするとADSCはずっと1のままになります。
bit4-ADIF : ADC Interrupt Flag
変換後にこの割り込みフラグが1になります。ADIEも1にしていたときは割り込みベクタに飛んだ時に自動でクリアされます。これをポーリングすることで変換終了を知ることができます。
bit3-ADIE : ADC Interrupt Enable
これを1にしてSREG($31)のbit7のグローバル割り込み許可を1にすると割り込みが可能になります。割り込みベクターは16bit境界プログラムメモリの$00Eです。
bit2..0-ADPS2..ADPS0 : ADC prescaler Select Bits
ADCに与える基本クロックをCPUクロックから与える時のプリスケーラです。最大200KHzまで与えることができます。2^x乗でプリスケールされます。ADPS2=1,ADPS1=1,ADPS=0の時は2^6=64となり1/64のクロックが与えられます。

・ADMUX($07) : ADC Multiplexer Select Register

7

6

5

4

3

2

1

0

-

-

-

-

-

MUX2

MUX1

MUX0

bit2..0-MUX2..MUX0 : Analog Channel Select Bits
マルチプレクサのチャンネルを設定します。フリーランモードの時など変換中にチャンネルを変更すると次の変換時に変更が有効になります。

・ADCH,ADCL ($05,$04) : ADC Data Register
ADCH ($05)

7

6

5

4

3

2

1

0

-

-

-

-

-

-

ADC9

ADC8
ADCL ($04)

7

6

5

4

3

2

1

0

ADC7

ADC6

ADC5

ADC4

ADC3

ADC2

ADC1

ADC0
変換後のAD値が入ります。値は16bitですので上書き防止のために先にADCLからしか読めないようになっています。ADCLをよんだときADCHがロックされ安全に16bit値を読むことができます。ただしこのときデータが入ってきたらデータは失われます。

●AD変換のためのポートの準備と回路の準備

AD変換するためのポートは基本的に入力ポートにしてプルアップをoffにしておきましょう。DDRA($1A)の対応するビットを0にして入力にし、PORTA($1B)の対応するビットを0にしてプルアップをoffにします。
回路上では、AVCC,AGND,AREFをつなぎます。AREFの電圧の時最大値になります。

●GCCでADCを使用する例

AT90S8535でやってみました。探してみたのですが、変換を行う関数はlibcには入っていないようです。
初期化はたとえば

	/*for ADC*/
	outp(0,DDRA);	/*all input pins*/
	outp(0,PORTA);	/*disable pull up*/
	outp(BV(ADEN)|BV(ADSC)|0x06,ADCSR);	/*clock 8MHz/64 = 125kHz*/

として行います。このときADCそのものの初期化シーケンスを行うためにADSCも立てています。例としてAD変換するルーチンは

/*AD conversion routine*/
int AD_get(char ch)
{
	/*set channel*/
	ch &= 0x07;
	outp(ch,ADMUX);
	
	/*start ad conversion*/
	cbi(ADCSR,ADIF);
	sbi(ADCSR,ADSC);
	loop_until_bit_is_set(ADCSR,ADIF);	/*bit treat special function*/
	return __inw(ADCL);	/*read ADCL first function from iomacros.h*/
}

のようになります。loop_until_bit_is_setは特定のビットが1になるのを待つ命令です。あと、__inwは下のバイトから読むためのルーチンです。16bitカウンタの読み込みなどにもつかえます。どちらもAVR-libcの中に入っています。これらの特殊命令を使うとコードサイズやスピードが小さくなるようです。

わりと変換特性はいいようです。そのままで10bit値で最下位が静止します。ただし同時に派手にI/OをばたばたさせるとAREFが揺らぐのか最下位ビットがゆれるようです。

●スリープモードを使用する例

スリープモードを使用するとよりノイズの少ない変換が可能となります。

プログラム例はこれです。nopやsleepはインラインアセンブラで入れています。

まず初期化の時にMCUCRを操作し、スリープを許可します。スリープを使用するときはまずADCSRレジスタのADIEを1にして割り込みを許可しておきます。その後SREGのIフラグを1にしてグローバル割り込みを許可しておきます。

スリープモードに入る前には、以前の割り込み時にクリアされているはずですが、一応ADCSRのADIFをクリアしておきました。その後おもむろにスリープします。このときADCSRのADSCbitを1にする必要はありません。sleep命令時に自動でスタートします。よってスリープ時には、ADCSRのビットを
ADEN = 1
ADSC = 0
ADIF = 0
ADIE = 1

の状態でsleep命令を実行します。変換後にスリープから起きて割り込みルーチンに飛びます。ただしerrataに出ているように割り込みにはいる前にsleepに続く2,3命令が実行されてしまうようですので、今回の場合にはなくてもいいのですが、ここにはnopを入れておきました。ただし、他の割り込みが入るとsleepから起きてしまうので割り込みを複数使用するときにはsleepは使えないでしょう。

でも、今回は回路的にもともと安定していたので、変換値にあまり効果がありませんでしたが、待ち命令を入れる必要がないのでコードがすっきりして短くなる効果はあります。


   AVR-GCCのインラインアセンブラ
2000/08以降コール規約が変更されています。以前の記事はこちらです。

Harald KippさんのAvr-gcc Inline Assembler Cook Bookを訳しました。これもあわせてどうぞごらんください。もしここは変ですというご意見がありましたらメールください。

AVR-GCCのインラインアセンブラの書き方について、述べます。AVR固有のものとGCC標準のものが混ざっていますが、ご容赦ください。2001/03ではまだ正式リリースされていませんがGCCはVer3を基準に書きます。

まずは、GCCのinfoファイルをご覧になるのが本筋と思います。あと一つ前のページのtipsの中にインラインアセンブラの書き方についてのあるMLでの記事がありますのでそれとあわせてみるといいのではと思います。またstring-avr.hの中に少しインラインアセンブラが入っているようですのでそれも参考になると思います。すべてを述べるためにinfoの和訳でもいいのですが、それだとmaroly自身が後から見たときわからなくなるので表と例題で書いていこうと思います。

例題は順次増やしていこうと思います。

●はじめに

gccのインラインアセンブラ内では、実際の値が何でどこに配置されているのかを推測する必要がないようになっています。しかしインラインアセンブラ内での変数の使われ方を制御する必要があります。よってオペランドの制御子と修飾子を知っておく必要があります。インラインアセンブラと単にアセンブラファイルとリンクする方法とは区別します。ここではインラインアセンブラのみ扱います。

アセンブラとリンクさせるときはcall convensionが重要になりますがこれはML上では実際にダミー関数をつくって調べるとよいということになっているようです(実際、アセンブラ出力を見ればすぐわかる)。その内容の概略は前のページの時系列情報の中のリンクに少しレポートしています。

●例題1−ただ値をインクリメントする関数

単なるお試しプログラムです。ISO99-C関連でアンダースコアを2つつけることにするそうです。

unsigned int IncFunc(unsigned int x)
{
	__asm__ __volatile__("\n\t"
		"adiw %0,1\n\t"
		:"=&w"(x) /*outputs*/
		:"0"(x) /*inputs*/
		:"memory" /*write to phisical memory if remain in register*/
	);
	return x;
}

\n\tは改行後タブ位置に移動します。命令はadiwだけでただ値をインクリメントします。最初のコロンのあとにインラインアセンブラから出力する値、次のコロンはインラインアセンブラへ引き渡すCの値、最後のmemoryはもしレジスタに値が残ったままだったらメモリに書き出す意味です。記号の意味は下をご覧ください。inputsのところの0は"=&w"で示した最初の値をさします。こうすることで入力値を出力値のレジスタを同じにすることができます。adiw %0,1の「%0」はoutputsとinputsのレジスタの通し番号です。この例では%0,%1が使えます。%1のほうは上記で説明したとおり通し番号0と同じものになります。

●例題2−CPUループによるウェイト

CPUループを使うときは最適化オプションで速度が変わってしまうことがあります。最適化オプションによらずいつも同じ速度でウェイトを掛けたいときはそこをインラインアセンブラにするのが確実だと思います。volatileをかけるのはインラインアセンブラそのものが最適化で削除されるのを防ぐためです。

void CPU_wait(unsigned int time){
	register unsigned char lpcnt;
	__asm__ __volatile__("\n"
		"CPU_wait_entry:\n\t"
		"ldi %0,200\n"
		"CPU_wait_lp:\n\t"
		"nop\n\t"
		"dec %0\n\t"
		"brne CPU_wait_lp\n\t"
		"sbiw %1,1\n\t"
		"brne CPU_wait_entry\n\t"
		:"=&a"(lpcnt)
		:"w"(time)
	);
	return;
}

この例は0.1ms単位で設定しています。やはりCでコーディングするよりもHEXファイルは小さいです。内部ループ用のレジスタをregister修飾でlpcntを作っています。インラインアセンブラ内でラベルを使用しています。

●オペランド制御子

レジスタが完全に等価でない場合のレジスタの型(クラス)の定義です。ほとんどAVR特化のほうでいいと思いますが、gcc一般のものも挙げます。gcc一般のほうはアセンブラで受け入れてくれるものは一部でしょう。

これらは普通はコンパイラ内部で、あるopcodeに対して使用可能なoprandを制御するための定義に使用されるものです(gcc/config/avr/avr.mdを参照すると載っています)。つまりこのファイルで定義されているものがoprandとして使用可能です。

・AVR特化のオペランド制御子
制御子 意味

"l"
Register from r0 to r15.

"a"
Register from r16 to r23.

"d"
Register from r16 to r31.

"w"
Register from r24 to r31. These register can be used in 'adiw' command.

"e"
Pointer register (r26 - r31).

"b"
Base pointer register (r28 - r31).

"q"
Stack pointer register (SPH:SPL).

"t"
Temporary register r0.

"x"
Register pair X (r27:r26).

"y"
Register pair Y (r29:r28).

"z"
Register pair Z (r31:r30).

"I"
Constant greater than -1, less than 64.

"J"
Constant greater than -64, less than 1.

"K"
Constant integer 2.

"L"
Constant integer 0.

"M"
Constant that fits in 8bits.

"N"
Constant integer -1.

"O"
Constant integer 8, 16, or 24.

"P"
Constant integer 1.

"G"
A floating point constant 0.0

・gcc一般のオペランド制御子
意味の方はかなり要約してるのですこしずれているかも知れませんが大体の意味です。IA32(i386)や68000系でも使うものです。もともとGCCはレジスタが等価なRISCとターゲットとしてるようです。その点AVRは向いてるかも。
制御子 意味

"m"
種類を問わずメモリオペランドを可能にする。

"o"
オフセット可能なメモリオペランド。

"V"
オフセットできないメモリオペランド。

">"
preまたはpostデクリメントができるメモリオペランド。

"<"
preまたはpostインクリメントできるメモリオペランド。

"r"
汎用レジスタとして使えるレジスタオペランド。

"i"
シンボルも使える即値整数をオペランドとして可能にする。もちろんシンボル値はアセンブラ時に値がわかっていないといけない。

"n"
(アセンブラが)すでにわかっている即値を数字としてオペランドに可能にする。多くのアセンブラはこっちしか使えないそうなので、"i"より"n"を使うべきです。

"I"-"P"
マシン依存の即値。たとえばディスプレースメントは通常範囲があるのでそれ以外を受け付けないようにする。AVRでは上記のようになっている。

"E"
フロートのそくちを可能にする。ホストマシンとターゲットマシンのformatが同じでないとちゃんと動かない。

"G","H"
マシン依存のフロート即値。AVRでは"G"が一応定義されている。

"s"
整数の即値だけれども、ある範囲を除外するときに使うもの。

"g"
どんなレジスタ、即値、メモリも受け入れる。ただしレジスタが一般レジスタでないときは除く。

"X"
そんなオペランドでも受け付ける。一般レジスタでなくても。

"0"-"9"
特定の数字のみ受け入れる。r0とかで物理的なレジスタ指定できる。CISCマイコンでは特に必要性がある。

"p"
有効な範囲のみ受け入れるアドレスオペランド。

●レジスタ制御子の修飾子

制御子の修飾をします。特に書き込み可能にしたりする機能があります。基本はread onlyになっているためです。

修飾子 意味

"="
write-onlyにする。前の値は捨てられて新しい値が入る。修飾の最初につける。

"+"
命令でread-write可能にする。修飾の最初につける。

"&"
レジスタ番号が重ならないようにする修飾子です。つまりインラインアセンブラ内では重なる可能性があるということです。その結果input valueが上書きされてしまうのを防ぎます。

"%"
命令に続く2つのオペランドを交換可能にすることで、制御子のフィット性を高める。

"#"
これに続く文字はオペランド制御を無視する。

"*"
これに続く文字はレジスタ選択からはずされる。

●アセンブラでのシンボル

リンク時にアンダースコア"_"を使わないで済む方法として
int foo asm("myfoo") = 2;
とすることができる。アセンブラ内ではアンダースコアなしのmyfooを使える。

関数の場合は
extern func() asm("FUNC");
func(x,y)
int x,y;
・・・・
とする。

●C変数のレジスタ化(Cでのregister修飾)

・グローバル変数の場合
すべてのプログラムを通してその変数のためにレジスタが保存される。

・ローカル変数の場合
必要ないとコンパイラが判断した時点で破棄される。しかしインラインアセンブラには役に立つ。つまりインラインアセンブラ内でダイレクトにレジスタ変数に書き込むことができる。


戻る