そこそこ使いやすい感じなのを学習しながら書いてみた。
使いかた
マスターの場合
割込みを利用はしてますが、APIとしてはブロックする同期なものしか用意してません。
uint8_t data[7]; uint8_t ret; i2c_set_bitrate(100); // Set target slave address i2c_master_init(0x60); ret = i2c_master_write((uint8_t*)"\x04", 1); if (ret) goto error; ret = i2c_master_read(data, 8); if (ret) goto error; i2c_master_stop(); error: i2c_master_stop();
スレーブの場合
スレーブの場合、特定のメモリ領域を登録すると、そこに対して read も write もできる、という感じの設計になっています。
// Slave memory map (must be smaller than 254 (0xfe) bytes) uint8_t data[9]; // Enter to slave receive mode with data and size. // After this operation, data will be changed automatically by TWI interrupt. i2c_slave_init(0x65, data, 10); // Access (set or get) to I2C data block data[0] = 0x10;
// Or more readable code with struct struct { uint8_t foo_flag1; uint8_t foo_flag2; uint16_t bar_value1; uint16_t bar_value2; uint16_t bar_value3; uint16_t bar_value4; } data; i2c_slave_init(0x65, &data, 10);
割込みでいつのまにかデータが変化する感じなので、マルチバイトデータの読み書きではデータが化ける可能性があります。
マルチバイトデータの読み書きを行う場合、
- 書く場合
- i2c_state を見て、I2C_STATE_IDLE であることを確認してから
- 読む場合
- i2c_state を見て、I2C_STATE_IDLE であることを確認してから
- cli してから
が必要だと思います。通信中のデータはコピーして別で持っておけばいいんですが、もったいないし、そんなに問題にならなそうだしお手軽なのでこんな感じです。
実装にあたって
I2C は仕様書の日本語訳で公開されている。本家?はi2c-bus.org かな。
ポイントとしては
- 送信・受信の切替えにあたっては必ず再度アドレスの送信が必要
- Repeated START というのは単に STOP + START 相当のことを START だけでできるようにしているに過ぎない (バスを連続して占有し続けられるというだけ)
- NACK は ACK を返さない状態のことを言ってる
- エラーと区別はない
- 転送終了とか、受信終了とかの意味付けされてるけど、応答がないのでもう何もしない、って感じ
- START コンディション、STOP コンディション、クロックは常にマスターが生成する
マルチマスターまわりとゼネラルコール(マルチキャスト)まわりはいまいち理解できてない (今のところ必要性を感じてない)。
I2C 上のプロトコル
I2C 自体は任意長のバイトの送受信しか定義してない (言及はあるけど) ので「特定のアドレスのデータを読みだしたい」みたいな場合は、その上にプロトコルを乗せる必要がある。デファクトスタンダードっぽいのは
- アドレスを1バイト送信する
- Repeated START (たぶん STOP + START でも同じだけど…)
- データをnバイト受信する
というもののようだ。ステートフルなので、読み出される側は読まれようとしているアドレスを記憶しておく必要があり、1バイト読まれるごとにインクリメントする必要がある。送信と受信は別々にアドレスを指定する必要があるので、この場合2度アドレス送信が行われる。
AVR での実装
ポイントは
- 割込みがかかったら何か必要なことをして TWINT を 1 にする、というのをくりかえす
- TWINT をクリアしないと無限に割込みが入り続けるし、SCL がローのままになるのでバスが解放されず一切I2Cできなくなる
- すべきこと (何をしたら次どういう状態になるか) はデータシートにモードごとに書いてある
- データシートにある "TWINT Flag is set" な状態というは TWINT が 0 の状態のことで、TWINT Flag を clear するというのは TWINT に 1 を書くということらしい。論理逆なの?
- TWINT Flag が set されている間 SCL はローになる
- ソフトウェア処理に時間がかかった場合自動でクロックストレッチングされる
- MR, MT, SR, ST ({Master,Slave} {Receiver,Transmitter}) の定義を先に読んどかないと意味不明
基本がわかればあとはそんなに難しくなく、試行錯誤したらできる感じだった。ただ、割込みの中で余計なことをすると、うまく次の割込みが入らなくなるということがあったりするので、動作を観測するのが難しい。LED チカらせてデバッグするしかないことがある。
備考
Linux の I2C まわりを調べていて出てくる SMBus とかいうのはPCの電源管理とかで使われるプロトコルで、基礎プロトコルとしてI2Cを利用している。I2C レイヤーの上に SMBus というレイヤーがあるイメージ。