Arduino のマイコンを直接制御して I2C を実行する方法はわりと簡単です。
ここでは Master Write の流れを説明します。
「→ マイコン」 をマイコンへの入力、「← マイコン」をマイコンからの応答として、以下のようになります。
- → マイコン: Start Condition 取得を要求
- ← マイコン: 取得完了
- → マイコン: Slave address + W の入力
- → マイコン: Data 送信を指示
- ← マイコン: 送信完了
- ← マイコン: ACK or NACK
- → マイコン: 送信する Data の入力
- → マイコン: Data 送信を指示
- ← マイコン: 送信完了
- ← マイコン: ACK or NACK
- ...
- → マイコン: Stop Condition を指示
→ マイコン: Start Condition 取得を要求
Start Condition 取得の要求は以下の通り、レジスタのページ TWCR の TWINT, TWSTA, TWEN の 3つの Bit を 1 にすることで行います。
byte i2c_start(){
TWCR = _BV(TWINT)|_BV(TWSTA)|_BV(TWEN);
return i2c_get_status_code(CONST_TIMEOUT);
}
TW は Two Wire の略で、I2C のことを意味しています。
TWINT は Interrupt の略で、ここに 1 を書き込むことで要求した I2C の処理を開始し、開始後は処理の完了を示す Frag にもなります。
TWSTA は Start の略で、Start Condition 取得を要求したい場合はここに 1 を書き込みます。
TWEN は Enable の略で、これに 1 を書き込むことでマイコンの I2C 機能を有効にします。
つまりこれは、要求として TWEN (I2C の有効) と TWSTA (Start Condition取得) をセットして、TWINT でそれら要求の処理開始を指示した、という内容になっています。
← マイコン: 取得完了
Start Condition の取得完了を待ちます。完了したかどうかは TWINT で確認できます。
byte i2c_get_status_code(int timeout){ byte status_code; int cnt_timeout = 0; while((TWCR & _BV(TWINT)) == 0){ if(cnt_timeout == timeout){ return SC_TIMEOUT; } cnt_timeout++; delayMicroseconds(100); } status_code = i2c_status_code(); if(status_code != SC_SUCCESS){ i2c_stop(); } return status_code; }
TWINIT & _BV(TWINT) は TWINT が 0 の間は 0、TWINT が 1 になると 0 以上の値になります。具体的には TWINT は Bit 7 なので、TWINT が 1 になると TWINT & _BV(TWINT) = B1000_0000 = 0x80 になります。
TWINT の動作なのですが、データシートには下記のような内容が記載されています (意訳)。
I2C の処理が終わるとマイコンによって値が 1 に変更されます。
ここに 1 を書き込むと新たな I2C の処理を開始します。
処理の開始のために 1 を書き込んで完了の合図も 1 だったら値は常に 1 になるので TWINT を見ても処理中なのか完了したのか判別できないんじゃないかと思ったのですが、実際にこの Bit の値を見張ってみたところ、処理開始の 1 を書き込むと TWINT の値は 0 になっていました。よって、 TWINT = 0 であれば処理中、 TWINT = 1 であれば処理完了と判別できます。
Wireライブラリで無限ループに陥るのはここです。Start Condition の取得要求に対してマイコンは SCL/SDA が空いていたら取る、空いていなければ待つ、という動作を行います。待っている間は処理中という扱いなので TWINT は 0 のままです。で、これが放っておくと永遠に待ちます。SCL/SDA の Pull-up を忘れていた場合、マイコンはそれを SCL/SDA が空いていないと判断し、いつまで経っても空くわけがないそれが空くのを延々と待ち続けるというわけです。
そこで、この関数では待ち時間を cnt_timeout でカウントし、別途設定したタイムアウト時間に達するとタイムアウト処理を行うようにしています。
タイムアウトせずに処理が完了した場合は Status Code を見て結果を確認します。
byte i2c_status_code(){ switch(TWSR & 0xF8){ case 0x08: //Stat condition return SC_SUCCESS; case 0x10: //Repeted Start condition return SC_SUCCESS; case 0x18: //Slave address + W ACK return SC_SUCCESS; case 0x20: //Slave address + W NACK return SC_NACK; case 0x28: //Data send ACK return SC_SUCCESS; case 0x30: //Data send NACK return SC_NACK; case 0x40: //Slave address + R ACK return SC_SUCCESS; case 0x48: //Slave address + R NACK return SC_NACK; case 0x50: //Data read ACK return SC_SUCCESS; case 0x58: //Data read NACK return SC_SUCCESS; default: return SC_TIMEOUT; } }
I2C処理が完了すると、マイコンは TWINT を 1 に変更して完了を知らせ、同時にレジスタページ TWSR の Bit 7-3 にその結果を Status code で出力します。Status code の細かい内容については割愛します。この関数では面倒なので全てを SC_SUCCESS (成功 or ACK)、SC_NACK (失敗 or NACK)、SC_TIMEOUT (タイムアウト) の 3つのいずれかに変換しています。
Start Condition 取得要求に対して、Start Condition が取れない場合は取れるまで待つので失敗はありません。成功の Status code には通常の Start Condition 取得と Repeated Start Condition 取得の二種類の code があるのですが、どちらもこの関数で SC_SUCCESS に変換しています。
→ マイコン: Slave address + W の入力
Start Condition が無事に取得できたら、次は Slave Address + R/W を送ります。下記は Slave Address + W の場合の記述です。
status_code = i2c_send_data(slave_adr<<1); byte i2c_send_data(byte data){ TWDR = data; TWCR = _BV(TWINT)|_BV(TWEN); return i2c_get_status_code(CONST_TIMEOUT); }
7-bit Slave Address を左シフトさせて Slave Addres + W にしたものをレジスタページ TWDR に書き込みます。
→ マイコン: Data 送信を指示
レジスタページ TWCR の TWEN = 1 をセットした状態で TWINT = 1 を書き込んで処理を開始するとマイコンは TWDR に格納されているデータ を送信します。
← マイコン: 送信完了
処理開始後は i2c_get_status_code の中で TWINT を見張りながら完了を待ちます。(Start Condition の処理待ちと同じ関数)
← マイコン: ACK or NACK
完了を確認したら Status code を取得して ACK (SC_SUCCESS) / NACK (SC_NAC) を確認します。 (Start Condition の処理待ちと同じ関数)
→ マイコン: 送信する Data の入力
Slave Address + W の送信に対して ACK が返ってきた場合は、続いてデータ送信の準備をします。送信するデータはレジスタページ TWDR に書き込みます。(Slave Address 送信と同じ関数)
→ マイコン: Data 送信を指示
レジスタページ TWCR の TWEN = 1 をセットした状態で TWINT = 1 を書き込んで処理を開始するとマイコンは TWDR に格納されているデータ を送信します。(Slave Addres 送信と同じ関数)
← マイコン: 送信完了
TWINT を見張りながら完了を待ちます。(Start Condition の処理待ちと同じ関数)
← マイコン: ACK or NACK
完了を確認したら Status code を取得して ACK (SC_SUCCESS) / NACK (SC_NAC) を確認します。 (Start Condition の処理待ちと同じ関数)
...
続けてデータを送信する場合は送信する Data の入力以降を繰り返します。
→ マイコン: Stop Condition を指示
送信が完了したら、Stop Condition を取って SCL/SDA を解放します。
void i2c_stop(){
TWCR = _BV(TWINT)|_BV(TWSTO)|_BV(TWEN);
}
TWSTO は Stop の略で、Stop Condition を要求する場合はここに 1 を書き込みます。
Stop Condition には処理待ちも失敗も無いので TWINT の見張りはしていません。