I/Oとはinput(入力)とoutput(出力)の頭文字を取ったもので入出力という意味となります。ここでいうI/Oとはマイコンから論理信号を入出力させることです。論理信号?ナニソレ?って方は、そこまで難しく考える必要はありません。マイコンに興味をもっておられる方なら、「コンピュータは1と0の世界で出来ている」みたいな謳い文句を訊いたことがあるのではないのでしょうか?実はその通りで、コンピュータは1と0で処理をしています。電気が流れている(1)、電気が流れていない(0)で計算等の処理を行っており、このように0と1で表す手法を2進法と呼ばれています。では論理信号とはなんなのかというと、先に述べた「電気が流れている(1)、電気が流れていない(0)」のことです。論理信号で表す場合は1を「HIGH」、0を「LOW」と表現する場合があります。つまるところ、ONなのかOFFなのか、ただそれだけです。
使用するマイコンはMega88なので、まずはMega88のデータシートを見てみましょう。検索エンジンで探せばすぐ出てくるはずです。本来は英語で書いてあるのですが、有志の方々によって日本語訳されているデータシートもありますので、そちらも参考にしてみると良いかと思います。
まず、AVRのI/Oを使うには、3つのレジスタを理解する必要があります。ここで、レジスタという謎のワードが出てきましたがこれも難しく考える必要がありません。レジスタとはただのメモリです。マイコンはこのレジスタ(メモリ)に書いてある値を読み取って動作をします。各レジスタにはそれぞれ役割が割り当てられていて、あるレジスタは受け取ったデータを保持していたり、別のレジスタでは1を書き込むとタイマーをスタートさせることが出来たり……とこんな感じです。
閑話休題、話を本筋に戻しましょう。理解する必要があるレジスタとは、
の3つのレジスタです。小文字のxは、B・C・Dが入ります。
では、各レジスタを細かく見ていきましょう。
DDRxレジスタは、「Port x Data Direction Register(ポートx方向レジスタ)」の略で、入出力の方向を決めるレジスタです。もう少しわかり易く言うと、入力設定にするのかい、出力設定にするのかい、どっちなんだいっ!ということです。出力設定にする場合は1を、入力設定にする場合は0を書き込みます。
PORTxレジスタは、「Port x Data Register(ポートx出力レジスタ)」の略です。
PORTxレジスタは特殊で、DDRxレジスタの値により機能が変化します。DDRxが1(HIGH)のとき出力設定となり、1を書き込むとHIGHに、0を書きこむとLOWになります。DDRxが0(LOW)の場合は、内部プルアップ抵抗の有効/無効の切り替えとなり、有効にする場合は1に、無効にする場合は0に設定します。
PINxレジスタは、「Port x Input Addres(ポートx入力レジスタ)」の略で、入力のデータを得られるレジスタです。目的のピンがLOWになっている場合0を、其れ以外の時は1を得ることができます。
では、実際にC言語のプログラムを見てみましょう。
#include "avr/io.h" #define F_CPU 1000000UL #include "util/delay.h" int main( void ){ //初期化 DDRD = 0b00000010; //1bit目を1,その他を0に PORTD = 0b00000000; //全ビットを0に //点滅 while( 1 ){ PORTD = 0b00000010; //1bit目をHIGHに _delay_ms( 500 ); //500msec wait PORTD = 0b00000000; //1bit目をLOWに _delay_ms( 500 ); //500msec wait } return 0; }
上記のプログラムはポートDの1ピンを500msecでON/OFFするプログラムです。プログラム自体はとても簡素で理解しやすいかと思います。ですが簡単に解説をしましょう。
まず1行目ではio.hをインクルードしています。詳しい話は割愛させていただきますが、これによりPORTDやDDRDといったレジスタ名をプログラム上で使うことができます。2行目のdefineは、F_CPUの値を定義しています。F_CPUとは、マイコンのクロック周波数を定義するものであり、これを正確に定義しないと、3行目でインクルードしているdelay.hで使えるdelayの関数群のwait時間が狂ってしまいます。
main関数内では最初に初期化を行い、次に点滅処理を行っています。まず、DDRDの1bit目をHIGHにして、出力設定にしています。次に、保険でPORTDをすべてLOWにしています。点滅処理では、PORTDの1bit目をHIGHにしてポートDの1ピンをHIGHにしています。その後500ミリ秒waitを挟み、次にPORTDをすべてLOWにすることで、ポートDの1ピンをLOWにしています。もう一度500ミリ秒waitをいれてからループの先頭に戻ります。
ポートDの1ピンを1kΩ以上の抵抗(感覚的にそのくらいですが、実際繋げる時はその時の電圧とLEDの最大定格電流を考えて抵抗値を求めて下さい)を挟んでLEDのアノードに繋ぎ、LEDのカソードをGNDに接続してあげると、LEDが点滅するかと思います。
では次に、入力を使ったプログラムを書いてみましょう。
#include "avr/io.h" #define F_CPU 1000000UL #include "util/delay.h" int main( void ){ //初期化 DDRD = 0b00000010; //1bit目を1,その他を0に PORTD = 0b00000100; //2bit目をHIGHに //押されたら光るよ while( 1 ){ if( ( PIND >> 2 ) & 1 ){ //もし、ポートDの2ピンがHIGHなら PORTD &= 0b11111101; //PORTDと0b11111101の論理積をとる }else{ //もし、ポートDの2ピンがLOWなら PORTD |= 0b00000010; //PORTDと0b00000010の論理和をとる } } return 0; }
上記のプログラムは、入力ピンの変化によって出力ピンを変化させるプログラムです。こちらは先程のプログラムに比べて理解しづらいかと思います。なので焦らずに一個一個解説していきましょう。
まず、ヘッダのインクルードは先と変わらないので大丈夫でしょう。問題はメイン関数です。最初のDDRDは先のプログラムと一緒ですが、PORTDの設定から少し毛色が違ってきます。先のプログラムでは全ビットを0にしていたのですが、今回は2bit目を1にしています。これは、内部プルアップ抵抗をONにするためです。先程説明しましたがDDRxを0に設定したとき、PORTxはプルアップ抵抗のON/OFFの設定になります。
さて、次のif文内を見てみると、なにやら良く分からないプログラムが出てきました。
( PIND >> 2 ) & 1
これは、PINDの値を2bit右にシフトして、0bit目のビットマスクをとっています。言葉で説明しても「???」となってしますので、順を追って解説しましょう。
…の前に、今後の説明で01001010(2)のような表記が出てきます。これは、括弧内の進数で表わされる数値となります。01001010(2)は2進数の数値であり、10進数に直すと74(10)となります。今後の説明でバンバン使いますので、覚えておいてください。
まず、最初の2bit右シフトですが、仮にPINDの値が10100101(2)の場合、右に2bitシフトすると
10100101| ↓ 00101001|01
となり、0bit目の位置に元の2bit目の値がきます。次に1と論理和をとると
00101001 &)00000001 ---------- 00000001
となり、1を得ることができます。では、次にPINDの値が10100001(2)となった場合を考えてみると、
10100001| ↓ 00101000|01
となり、0bit目の位置に元の2bit目の値がきます。次に1と論理和をとると
00101000 &)00000001 ---------- 00000000
となり、0を得ることができます。つまり先程のプログラムは2bit目の値で1(真)/0(偽)になるということです。
では、次の行を見てみましょう。
PORTD &= 0b11111101;
これもわかりづらいですね。ですが、これも先に説明したビットマスクと同じです。これは1bit目の値を強制的に0にし、他ビットの値を反映させるために、1bit目以外のビットを1にしています。例として00110011(2)という値に論理積をとってみると、
00110011 &)11111101 ---------- 00110001
となり、見事に1bit目だけを0にし、残りはそのままに出来ました。これはどのような意図があるかというと、最初の点滅プログラムだと全体を0にしても問題が無かったのですが、今回のプログラムの場合は、プルアップ抵抗をONにするために2bit目を1に設定してあるため、全体を0にするとプルアップ抵抗の設定もOFFになってしまいます。これを避けるために0にしたいビットだけを0にし、其れ以外はそのままにする方法をとっています。
では、次に進みましょう。else内にある、
PORTD |= 0b00000010;
は、どのような意味があるかわかりますか?
これは先程の「0にしたいビットを0にし、他ビットはそのまま反映させる方法」の逆バージョンで、「1にしたいビットを1にし、他ビットはそのまま反映させる方法」です。これは11001100(2)という値を使って説明しましょう。
11001100 |)00000010 ---------- 11001110
このように、1bit目を1にして、其れ以外はそのまま残しているのがわかりますね。これらのビット演算を用いた任意のビットを変更する方法は、AVRのような8bitマイコンでは必ず使うことになると思います。
なんとなく理解できたでしょうか?
このプログラムは、ポートDの2ピンをスイッチと繋げGNDに落とし、ポートDの1ピンを先程のように抵抗とLEDを繋いであげると、スイッチを押している間だけLEDが点灯する、といった動作になると思います。
余談ですが、よくcbi()やsbi()、_BV()といったマクロをみかけるのですが、そのマクロの意味と動作が理解できているなら良いのですが、そうでないなら自力で書いていったほうが良いかと思います。勉強にもなりますし、ビット演算に対する理解も深まるかと。
このページを読み返してみると、なんの脈絡もなく2進表記が出てきていますね(汗)
ビット演算というとこがあるのでそこで詳しく読んで頂戴!