I2C通信まとめ

はじめに

マイコン工作でよく使う通信方式と言えば,概ね

  • UART
  • I2C
  • SPI

を挙げることに異を唱える人はいないだろう.
(カメラとか作る人はLVDSとかその辺も入ってくるけど)
このうち,UARTはどちらかと言うと基板内ではなく基板間
(というか大抵FT232RL通してパソコンと接続)
で使うことが多いので,基板内通信で使うものと言えばSPIとI2Cが主流である.

このうち,SPIは単純なシステム故にわかりやすい.
送受信がそれぞれ別の信号線を用いているうえに,アドレスでデバイスを指定する必要が無い.
バイスが増えると増えただけnCSを必要とする以外は使いやすいだろう.

問題はI2Cである.
I2Cもマイコン用の通信規格である以上,そこまで複雑なことはさせていない.
しかし,SPIと異なり色々と「初心者がハマる」ポイントがいくつかある.
しかも悪いことに,標準的に使われる規格である以上,
ArduinoWireライブラリやmbedのライブラリを駆使しつつサンプルコードをコピペして動かすだけの
資料がそろってしまっているのである.
単にサクッと動かしたいだけならそれはありがたいのだが,
ライブラリのないデバイスをデータシート頼りで動かそうとするとさすがに中身を知らないと
バグの取りようがない.

そこで,一念発起して一通り今まで理解したことを書いておくことにする.

I2Cでの通信

基本的な通信の流れとして

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレス(7ビット+リードライトを示すビットで8ビット)を送信する
  3. 【スレーブ→マスタ】該当するアドレスを持つデバイスはマスタに向けてACKを返す
  4. 【双方向】送受信する
    • 送信の場合
      1. 【マスタ→スレーブ】データを1バイト送信する
      2. 【スレーブ→マスタ】ACKを返す
      3. 上二つを送りたいデータ数分だけ繰り返す
    • 受信の場合
      1. 【スレーブ→マスタ】マスタのクロックに合わせてデータを送信する
      2. 【マスタ→スレーブ】ACKを返す
      3. 上二つを受け取りたいデータ数-1だけ繰り返す
      4. 【スレーブ→マスタ】最後のデータを受け取る
      5. 【マスタ→スレーブ】NACKを返す
  5. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

このようになる.

送信

概ね前の章で書いたことがI2Cの送受信のやり方だが,
送信だけなら難しいことはほぼ何も無い.

  • スタートコンディション
  • アドレス送信
  • データ送信
  • ストップコンディション

でおしまいである.あとはアドレス送信とデータ送信の間にスレーブからACKが返ってくるので,
それを待つことさえ忘れなければ良い.

受信

問題は受信である.ここで変に勘付いて「どうせ送信と逆でしょ」と

  • スタートコンディション
  • アドレス送信
  • データ受信
  • ストップコンディション

という流れを考え,
「データ受信したらマスタからスレーブにACKを送っておけばOK」
と思い込むとハマる.
結論から言えばその考えは80%は間違ってなかっただろう.
「最後の1バイトを受信したら,ストップコンディションの前にACKではなくNACKを返す必要がある」
という点には十分な注意が必要である.

ACKと言う信号は,一般には通信応答の意味を示す.これは
ACKを返す=データを正しく受信した
ACKを返せない(NACKを返す)=データが正しく受信できていない
という意味になる.マスタからスレーブに送信する場合, ACKの意味は上記で間違いない.

しかしI2Cの場合,マスタが受信する際に送るACKは
「データをさらに要求するかどうか?」
という意味に変化し,正しく受信できたか?という意味にはならない.
ここを勘違いしたままでいると,
(そしてArduinoライブラリなど,このあたりの処理が隠されたライブラリを使っていると),
最初の1回は上手くいくのだがそれ以降通信がタイムアウトしてしまう.

受信におけるACKとは,
ACKを返す=さらにデータを要求する
ACKを返せない(NACKを送る)=データの要求を終了する
と言う意味である,ということを念頭に置かないと処理がおかしくなってしまうのである.

レジスタを使った通信

さて,一般的なセンサデバイスと通信する場合,計測値の受信や設定値の送信は
バイス内のレジスタに対する読み書きという形で表現されることが多い.
これをI2Cの流れに沿って考えると,

**レジスタに書き込む

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレスを書き込みモードで送信する
  3. 【スレーブ→マスタ】該当するアドレスを持つデバイスはマスタに向けてACKを返す
  4. 【マスタ→スレーブ】書き込み対象のレジスタアドレスを送る
  5. 【マスタ→スレーブ】レジスタに書き込むデータを送る
  6. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

これで書き込みは実現できる.もちろん,レジスタアドレスやデータを送る際には,
スレーブからACKが返ってくる.

次に,レジスタから読み込みたい場合はどうすればよいだろう?
レジスタアドレスの指定はマスタ→スレーブなのだから書き込みモードだが,
読み込むのはスレーブ→マスタなのだから読み込みモードのはず・・・ ではデバイスにはどちらのモードで接続をかけるのが正解なのだろうか?

まず,余計なことを考えずに「送信と受信」で考えると以下のようになる.

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレスを書き込みモードで送信する
  3. 【マスタ→スレーブ】読み込み対象のレジスタアドレスを送る
  4. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)
  5. 【マスタ→スレーブ】改めてスタートコンディションを送る(というか示す)
  6. 【マスタ→スレーブ】アドレスを読み込みモードで送信する
  7. 【スレーブ→マスタ】データを送信する
  8. 【マスタ→スレーブ】最後だけNACKを返す
  9. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

「送信するものは送信し,受信するものは受信する」で処理を分けた.
モノにもよるがこれでうまくいってくれるデバイスは多い.
実際には,このうち4.を省略することが可能であるし,
一般にはそのようにすることが多いようだ.その場合このようになる.

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレスを書き込みモードで送信する
  3. 【マスタ→スレーブ】読み込み対象のレジスタアドレスを送る
  4. 【マスタ→スレーブ】改めてスタートコンディションを送る(というか示す)
  5. 【マスタ→スレーブ】アドレスを読み込みモードで送信する
  6. 【スレーブ→マスタ】データを送信する
  7. 【マスタ→スレーブ】最後だけNACKを返す
  8. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

この場合,4.のスタートコンディションは別名「リスタートコンディション」と呼ばれる.
しかし,実態は見ての通りスタートコンディションを送りなおしているだけであって,
リスタートコンディションという特別なコンディションは存在しない

まとめ

そんなわけで,今までハマったポイントをまとめると以下のようになる.

  • 送信は単純.各データを送った際に1バイトごとにスレーブからACKが来る
  • 受信の際はACKの意味が「さらなるデータを要求するかどうか」に変化していることに注意
  • そのため,受信を最後はACKではなくNACKを送ってデータをもらい終わったことを示さなければならない
  • レジスタし指定して受信する際は「スタート,スタート,ストップ」コンディションを送る
  • 2回目のスタートコンディションをリスタートコンディションと言うが,特別なコンディションではない

これを覚えておけば,多少なりともI2Cの挙動が理解しやすくなるし,
また上手く通信ができない場合も原因究明に役立つことだろう.

それでは,また.