ATMEL AVRマイコンのプログラミングは、Z80でのアセンブラプログラミングの経験がある方なら、比較的簡単に修得できると思われます。アセンブラは初めて、という方でも基本さえわかればそう難しいものでもないので、AVRではC言語が使えないからといって悲しむことはありません(2000/10/29追記:最近AVRのC言語開発環境も整いつつあるようです)。ではこれからAVRマイコンによるアセンブラプログラミングの基本を見ていくことにしましょう。ここでもAT90S1200をメインに話を進めていきますが、その他のマイコンでも基本的な命令は同じです。2.ポート入出力
2−1.ポート出力3.タイマー・カウンタまずはマイコン制御の基本、ポート出力について見ていきましょう。ポート出力とはパラレルI/Oポート(AT90S1200ではポートBとポートD)にある決まった値を出力することです。第1回でポート出力の概要は説明しましたが、ここでは具体的なニーモニックも含めて説明していきます。
ではいきなりですが、ポートBの出力をすべて0にするプログラムを書きます。
.INCLUDE "1200def.inc" .DEF TEMP = R16 ;ポートBを全ビット出力に設定 LDI TEMP, 0xFF OUT DDRB, TEMP LDI TEMP, 0x00 OUT PORTB, TEMP
ではこのプログラムを順番に見ていきましょう。まず.INCLUDEや.DEFのように先頭が.(ピリオド)で始まっている命令はアセンブラ命令(もしくは疑似命令)と言うもので、AVRマイコンが実行する命令ではなく、アセンブラに対する指示を表しています。
.INCLUDE疑似命令は、他のソースコードをインクルード(取り込み)することを指示します。ここでインクルードされている1200def.incというファイルは、あらかじめATMEL社によって用意されているファイルで、ここにはAT90S1200で使用されるI/Oポートやレジスタがあらかじめ定義されています。この後に出てくるDDRBやPORTBといったものは、すべてこのファイル内で定義されているので、必ずインクルードしておいてください(なお、AT90S4414を使用される場合は4414def.inc、AT90S8515を使用される場合には8515def.incというように、それぞれのAVRマイコン専用のファイルが用意されています)。
次の行の.DEF疑似命令は、シンボルを定義します。この例ではレジスタ16をTEMPというシンボルで使えるようにしています(AVRのレジスタは0〜31までありますが、16から使い始めるのが良いようです。詳しくは第1回参照)。これは要するに、レジスタをどのような用途に使っているのかをわかりやすくするために、名前を付けているわけです。
その次の、;(セミコロン)で始まる行はコメント行です。先頭にセミコロンを付けた行はアセンブラによって無視されるので、どのような処理を行っているのかを書いておけば、後で読み返したときにわかりやすくなります。
で、その次の行からが、いよいよAVRマイコンが実行する命令です。命令は先頭にタブ1つ分(たいてい半角スペース8個分)空けて書き始めることになっています。ここでは2種類の命令が使われていますが、それをそれぞれ説明していきましょう。
まずLDIですが、これは「LoaD Immediate」の頭文字を取ったものです(私の憶測なので断定は出来ませんが…)。つまりイミディエイト(即値)をロードする命令です。これではなんのこっちゃわからんかもしれないので、もう少し説明すると、LDI 格納レジスタ, 代入する値
という書式をとります。このプログラムの場合だと、TEMP(つまりレジスタ16)に、0xFF(16進数でFF)を代入しているわけです。Z80のニーモニックを知っておられる方なら、すぐに理解できると思います(ただしレジスタ−レジスタ間のロードはMOV命令を使うので注意が必要です)。なお、16進数の表し方は先頭に0xを付けるほかに、$をつける書き方もあります。
次はOUT命令です。これは何となくわかると思いますが、特定のI/Oポートにデータを出力する命令で、OUT 出力アドレス, 転送元レジスタ
という書式です。出力ポートはここではDDRBとPORTBが使われていますが、これは先ほども少し述べたように1200def.incの中で、DDRBは$17、PORTBは$18とそれぞれ定義されています。この$17や$18というのはもちろん、I/Oアドレスを表しています。
各命令の説明が終わったので、具体的にポートBにデータを出力する流れをもう一度見ていくと、・DDRBで入力か出力かを設定(1にしたビットが出力となる)
となります。ただしOUT命令では、レジスタの内容しか出力できないので、一旦あるレジスタに出力したいデータを入れて、それをOUTで出力するという手順が必要になります。
・PORTBに実際に出力したいデータを代入
ここまでの説明で、大体わかってもらえたでしょうか。もしポートBの出力をすべて1にしたいならば、下から2行目の0x00を0xFFにすれば良いわけですし、他のデータを出力したいときも同様です。またポートBではなくポートDに出力したい場合は、DDRBをDDRDに、PORTBをPORTDにそれぞれ書き換えればOKです。
2−2.ポート入力では次に、パラレルI/Oポートからデータを入力する方法を見ていきます。まずポートBからデータを読み込むプログラムを見ていきましょう。
.INCLUDE "1200def.inc" .DEF TEMP = R16 ;ポートBを全ビット入力に設定 LDI TEMP, 0x00 OUT DDRB, TEMP ;内部プルアップはすべて有りに設定 LDI TEMP, 0xFF OUT PORTB, TEMP ;ポートBからデータ取り込み IN TEMP, PINB
最初の方はポート出力と同じなので特に説明することはありません。まずDDRBをすべて0にして、ポートBを全ビット入力に設定します。ここまでは先ほどと一緒です。
その次は内部プルアップの設定ですが、まずプルアップとは何なのか、説明しておきます。
ディジタル回路設計の経験のない人が、ポートに押しボタンスイッチをつなげ、といわれた場合、最初に頭に浮かぶのはこんな回路ではないかと思います。
ところがこれではうまくいかないのです。なぜかというと、スイッチが押されているときはVcc(電源の+側)につながっているので確かに1になるのですが、スイッチが押されていない時に、0にならないのです。0の状態というのは、GND(電源の−側)につながっている状態のことなので、この回路ではスイッチが押されていないときはどこにもつながっていないので、これでは0か1か不定になってしまいます。そこで、次のようにすればいいことがわかります。
これだと確かに1か0かはっきりするのですが、問題はこういう構造の押しボタンスイッチが少ないことです。無いことはないのですが、高価です。そこで一般に押しボタンスイッチをつなぐときは次のようにします。
この回路だと、スイッチが押されていないときは抵抗を介してVccにつながっているので1、スイッチが押されるとGNDへの抵抗はほぼ0Ωとなるので、GNDにつながっているのと同じ状態になり、0となります。
普通のマイコンでは、この抵抗(プルアップ抵抗といいます)は自分で外付けしなければならないのですが、AVRマイコンでは便利なことに、このプルアップ抵抗を内蔵しています。もちろん使わなくても良い場合もあるので、ONにするかOFFにするか、自分で設定するようになっています。設定の方法は、ポート出力のときは出力するデータを格納する場所だったPORTBを使用します。内部プルアップを利用するビットを1にすることで有効となります。
最後にデータの読み込みですが、これにはIN命令を使用します。書式は、IN 転送先レジスタ, 入力アドレス
となります。読み込むアドレスはPINBです。ここからデータを読み込むと、その時点でのポートBのデータを読み込んだことになります。
ポート入力をまとめると、・DDRBで入力か出力かを設定(0にしたビットが入力となる)
となります。
・PORTBで内部プルアップの設定(1でプルアップ有効)
・PINBからデータを読み込む
なお、ポートDについても同様で、DDRBをDDRD、POTRBをPORTD、PINBをPINDに書き換えればOKです。
なお、プルアップを有効にしてスイッチのもう一方をGNDにつないだ場合、スイッチを押さないときは1、押したときは0になるので注意して下さい(かくいう私もこのことをすっかり忘れていたために、うまく動かなくて首をひねったことがあります…)
タイマー・カウンタはマイコン制御には必須の機能です。逆に言えばタイマー・カウンタを使うためにマイコン制御をするといっても過言ではないでしょう(私の経験上…ですが)。タイマー・カウンタを使うには、まず割り込みについて知らないといけないので、はじめにAVRマイコンでの割り込みについて説明します。
3−1.割り込みについて詳細は第1回を参照してもらうとして、ここでは簡単に説明します。
タイマー・カウンタは、設定された時間毎に割り込みを発生させます。そして、タイマー・カウンタ割り込みが発生するとAVRマイコンは強制的に、次に実行するプログラムアドレスを$002番地に変更するわけです。このため、タイマー・カウンタ割り込みを使用する場合は、プログラムを、
.INCLUDE "1200def.inc" .DEF STACK = R16 ; リセットベクトルと割り込みベクトルの指定 RJMP RESET ; リセット($000) RJMP RESET ; 外部割り込み($001) RJMP TIM0_OVF ; タイマー0オーバーフロー($002) RJMP RESET ; アナログコンパレータ($003) RESET: ; 〜ここにメインプログラムを記述〜 TIM0_OVF: IN STACK, SREG ; 〜ここにタイマー・カウンタ割り込みプログラムを記述〜 OUT SREG, STACK RETI
という風に記述します。
ここで、RJMPは「Relative JuMP」の頭文字を取ったもので、相対ジャンプ命令を表します。書式は、
RJMP 飛び先(相対指定 -2048〜+2047)
となります。相対指定とは、絶対アドレスではなくて、今いるアドレスから前後にどれぐらい飛ぶかで指定するという意味です。ただ「RJMP 5」とか書いてもどこに飛ぶのかがぱっと見てわからないですし、いちいち飛びたいアドレスまで何バイトあるのか数えるのも大変ですから、普通はラベルを使います。
ラベルとは、RESET:やTIM0_OVF:のようなもので、左端から書き始め、最後に:(コロン)を付けます。もちろん名前は自分のわかりやすいように好きに付けてかまいません(ただし1文字目はアルファベットで、2文字目以降は数字も可)。ラベルを使うと、いちいち飛び先アドレスを計算しなくても、そのラベルのあるアドレスに飛ぶことが出来ます。
さて、このプログラムではRJMP命令が先頭に4つ並んでいます。第1回でも書きましたが、割り込み要因には4つあり、それぞれの割り込みに応じた処理プログラムへ飛ぶようになっています。ただしここではタイマー・カウンタ割り込みとリセット以外は使っていないので、外部割り込みとアナログコンパレータ割り込みは、リセット割り込みと同じ処理をしています。リセット割り込みはその名の通り、リセットがかかったときに発生するほか、電源投入時にも発生するので、まず電源が投入されると「RJMP RESET」が実行されて、RESET:へとジャンプするようになっています。
RESET:以降にはメインのプログラムを記述します。そして、タイマー・カウンタ割り込みは通常、このメインプログラムを実行中に発生します。タイマー・カウンタ割り込みが発生すると$002番地にアドレスが飛び、「RJMP TIM0_OVF」が実行されます。その結果次の処理はTIM0_OVF:に飛び、そこからタイマー・カウンタ割り込み処理が開始する事になるわけです。
まず、どの割り込み処理にも共通することですが、最初にSREGの中身を待避させます。SREGは現在のフラグの状態を格納したレジスタで、元のメインプログラムの、割り込みがかかった時点でのフラグの状態をちゃんと保存しておかないと、割り込み処理プログラムからもとのメインプログラムに戻ったときに、最悪の場合プログラムが暴走してしまいます。そのため、ここではSTACKという名前をつけたレジスタを用意し、IN命令で保存し、プログラムの最後にOUT命令で戻してやります。
そして、必ず割り込み処理の最後にはRETI命令を実行します。RETIとは「Interrupt RETurn」のことで、割り込み処理から復帰する命令です。この命令が実行されると、元のメインプログラムに戻り、先ほど処理が中断されたところから再び実行が開始されます。
3−2.使用方法例によって、プログラムから解説していきます。
.INCLUDE "1200def.inc" .DEF STACK = R16 ; リセットベクトルと割り込みベクトルの指定 RJMP RESET ; リセット RJMP RESET ; 外部割り込み RJMP TIM0_OVF ; タイマー0オーバーフロー RJMP RESET ; アナログコンパレータ RESET: ; タイマー・カウンタ割り込みの設定 ; タイマーカウンタ 1/128を指定 LDI TEMP, 0x80 OUT TCNT0, TEMP ; 内部クロック 1/1024を指定 LDI TEMP, ((1<<CS02) + (1<<CS00)) OUT TCCR0, TEMP ; タイマーオーバーフロー割り込みを指定 LDI TEMP, (1<<TOIE0) OUT TIMSK, TEMP SEI ; 〜ここにメインプログラムを記述〜 TIM0_OVF: IN STACK, SREG ; 〜ここにタイマー・カウンタ割り込みプログラムを記述〜 OUT SREG, STACK RETI
先ほどと変わったのは、メインプログラムの所だけです。まずAVRマイコンのタイマー・カウンタは、どういう条件で割り込みを発生させるのか、簡単におさらいしておきましょう。
AT90S1200が唯一持っているタイマー・カウンタ0には、TCNT0という8ビットのレジスタがあり、まずはここに数値を設定します。そして、TCCR0というレジスタで設定した周期でTCNT0のカウンタを1ずつ増やしていき、オーバーフローした時点で、つまり255+1になったときに割り込みが発生するようになっています。詳しくは第1回をもう一度見て下さい。そして、タイマー割り込みを有効にするために、TIMSKのbit1を1にし、最後にSEI命令で割り込みを有効にします(SEIとは「SEt Interrupt」です)。
このプログラムではまず、TCNT0に$80(128)を代入しています。割り込み周期は、1/(256-128)=1/128となります。次にTCCR0のbit0とbit2を1にすることで、内部クロックを1/1024にしています。この詳しい設定は第1回を参照。なお、内部クロックとは、AVRマイコンを動かしているクロックのことです。これは外部のクロックを使用するように設定することもできます。なお、「((1<<CS02) + (1<<CS00))」と書くのと、「0x05」と書くのとは同じ意味です。1200def.incの中で、
.equ CS02 =2 .equ CS01 =1 .equ CS00 =0
という風に定義されているので、「1<<CS02」とはbit2を1にすること、「1<<CS00」とはbit0を1にすることを意味しています。このようにわざわざややこしく書くのは、どのビットを1にするのか、一目でわかるようにするためです。なお.EQU疑似命令は、数字をシンボルで使えるようにするための疑似命令です。
次にTIMSKのbit0を1にしています。これも「1<<TOIE0」と「0x01」は同じ意味になります。そして最後にSEIが実行され、ここからタイマー割り込みが発生するようになります。
3−3.問題点タイマー・カウンタ割り込みの使用方法はだいたい理解していただけたと思いますが、ここで1つ問題が生じます。第1回でもちらっと述べましたが、例えば内部クロックをクロック源とした場合、
1,000,000(Hz) ÷ 1024(分周比) ÷ 256(TCNT0=0x00) ≒ 3.8(Hz)
が最長となるのですが、これでは短すぎる場合が多々あるのです。そのため、数秒毎に割り込みを発生させたい場合は多少テクニックが必要となります。
テクニックといっても少し考えればわかることなのですが、要するにあるレジスタを使用してタイマー・カウンタ割り込みがかかる毎にそのレジスタの値を1ずつ減らしていき、0になった時点で本来のタイマー・カウンタ割り込み処理プログラムを実行させれば良いわけです。こうすると、
1,000,000(Hz) ÷ 1024(分周比) ÷ 256(TCNT0=0x00) ÷ 256 ≒ 0.015(Hz)
つまり、最大約67秒毎に割り込みが発生するようになります。
それで、具体的にどういうプログラムにするかというと、
.INCLUDE "1200def.inc" .DEF STACK = R16 .DEF COUNT = R17 ; リセットベクトルと割り込みベクトルの指定 RJMP RESET ; リセット RJMP RESET ; 外部割り込み RJMP TIM0_OVF ; タイマー0オーバーフロー RJMP RESET ; アナログコンパレータ RESET: ; タイマー・カウンタ割り込みの設定 ; タイマーカウンタ 1/256を指定 LDI TEMP, 0x00 OUT TCNT0, TEMP ; 内部クロック 1/1024を指定 LDI TEMP, ((1<<CS02) + (1<<CS00)) OUT TCCR0, TEMP ; タイマーオーバーフロー割り込みを指定 LDI TEMP, (1<<TOIE0) OUT TIMSK, TEMP LDI COUNT, 0xFF SEI ; 〜ここにメインプログラムを記述〜 TIM0_OVF: IN STACK, SREG DEC COUNT CPI COUNT, 0x00 BREQ TIM_INT INT_EXIT: OUT SREG, STACK RETI TIM_INT: ; 〜ここにタイマー・カウンタ割り込みプログラムを記述〜 RJMP INT_EXIT
という感じになるでしょう。DEC命令は、レジスタから1を引く命令です。次のCPIは「ComPare Immediate」のことで、レジスタと定数を比較します。比較方法は引き算です。従ってレジスタと定数が一致する場合0となり、Zフラグが立ちます。そして、次のBREQ命令で条件分岐をします。BREQは「BRanch if EQual」のことで、Zフラグが立っていれば、飛び先アドレスにジャンプします。ここではCOUNTが0になれば、TIM_INTにジャンプすることになります。そしてそこで本来のタイマー・カウンタ割り込みプログラムを実行すれば良いわけです。
TCNT0、TCCR0、そしてCOUNTの値はちょうど良い割り込み周期となるように、適宜変更してください。
また、これ以上の長さが必要な場合は、レジスタを2つ以上使って、一方が0になればもう一方を減らし始めるといったやり方で長くすることができます。詳しいやり方は省略しますので考えてみて下さい。