I2CインターフェースをArduinoで動かす

【思考停止】完全ノーリスク自動販売ツールの情報メール全7回(プレゼントあり)

トイレ向けIoTの導入サービスを始めています。

I2Cインターフェースを容易に開発する目的で、新たなプロジェクトを開始しています。こちらのブログを参照ください。

===
I2CインターフェースをArduinoで動かす
2017.10.11 Rev.1.2
目次
0.はじめに
1.本稿の対象読者
2.本稿で扱うI2Cインターフェース
3.ArduinoのI2C向け関数

========
0.はじめに

世の中にはI2Cインターフェースを持つセンサー類(以下I2Cセンサーと称す)が多く出回っており実験的にちょっと試してみたい、という場面が多々あると思う。

そして今やITの専門家でなくとも簡単に扱えるArduinoというマイコンボードが多く出回っておりI2Cインターフェースをサポートしている。しかしながら、Arduinoのスケッチ(プログラムのこと)でI2Cインターフェースを動かそうとするとそれほど簡単ではないと思う方も多いだろう。

主な原因として、Arduinoの関数の仕様と、実際のI2Cインターフェースの対応がとりづらいことにある。例えばWire.requestFrom(0x40,8)と書いたときに、SCLとSDAという信号がどんな動きをするのかがピンと来ないのだ。I2Cセンサーの説明書にはSCLとSDAのタイミングチャートしか書かれていないのに、だ。

自分と同じ境遇の人たちもいると想像されるので自分のために書いた備忘録に手を入れてここに公開することにした。

========
1.本稿の対象読者

簡単に言えば「信号のタイミングチャートは理解できるがソフトウェアの専門家ではない」という技術者向けである。もう少し具体的には「I2Cセンサーの説明書からSCLとSDAをどう扱えばいいかは理解できるが、Arduinoでどうプログラミングしたらいいかがわからない、もしくは行き詰まった」という人を対象にする。
========
2.本稿で扱うI2Cインターフェース

本稿で参照している規格は
「I2Cバス仕様およびユーザーマニュアルRev.5.0J-2012年10月9日」
の日本語翻訳11月2日、である。以下「I2Cマニュアル」と記す。
また、本稿で扱うI2Cインターフェースは、Arduinoが1個のマスターデバイスである場合に限定する。新しく入手したセンサーを評価のためや自分の実験のためにちょっとだけ使いたい、など多くの場合はこれで十分であろう。

ここでまずI2Cインターフェースの動きをおさらいしておく。初めに信号のタイミングチャートを図で表現する代わりに以下の記法を採用する。

S -> Start(masterがslaveに対して出力する。以下M>Sと記し逆はM<Sと記す)
P -> Stop(M>S)
ACKS -> Acknowledge by slave(M<S)
ACKM -> Acknowledge by master(M>S)
NACKM -> Not acknowledge by master(M>S)

SAW -> slave address, write mode(M>S, slave address 7bitを左1bitシフトしてLSB=0とした8bit)
SAR -> slave address, read mode(M>S, slave address 7bitを左1bitシフトしてLSB=1とした8bit)
WD -> write dataの8bit(M>S)
RD -> read dataの8bit(M<S)

以下に記述する動作は次に限定する
1) 全てのトランザクションはmasterが出すSで始まりPで終わる。
2) データ転送とそれに続くacknowledgeは以下の組合せに限られる。

SAW/SAR/WD –> ACKS
RD –> ACKM or NACKM

====
ここでn byte writeを表現すると(「I2Cマニュアル」図11)

S -> スタート
SAW ACKS -> slave address7bitをwrite modeで送り、slaveからackを待つ
WD ACKS -> write data8bitを送り、slaveからackを待つ

WD ACKS -> write data8bitを送り、slaveからackを待つ
P ストップ

====
次にn byte readを表現すると(「I2Cマニュアル」図12)

S -> スタート
SAR ACKS -> slave address7bitをread modeで送り、slaveからackを待つ
RD ACKM -> slaveからread data8bitを待ち、受け取ったらmasterからackを出す

RD NACKM -> read data8bitを待ち、受け取ったらmasterがデータ終了のnackを出す
P ストップ

====
slave device内のレジスタアドレスを指定して読み書きするためにはこれらをrepeated startで組み合わせた動作が定義されている。これを使ってn byte readするには(「I2Cマニュアル」図13)

S -> スタート
SAW ACKS -> slave address7bitをwrite modeで送り、slaveからackを待つ
WD ACKS -> readするregisterのaddress8bitを送り、slaveからackを待つ
S -> repeatedスタート
SAR ACKS -> slave address7bitをread modeで送り、slaveからackを待つ
RD ACKM -> slaveからread data8bitを待ち、受け取ったらmasterがackを出す

RD NACKM -> slaveからread data8bitを待ち、受け取ったらmasterがデータ終了のnackを出す
P -> ストップ

となる。

※n byte writeやread/writeの混在も仕様上は可能だが使用頻度が低いと思われるのでここでは割愛する。
========
3.ArduinoのI2C向け関数

この項の目標は2.のシーケンスと3.の関数とを関連付けることである。

本稿で参照している書籍とサイトは
「Arduinoをはじめよう第2版」
https://www.arduino.cc/en/Reference/Wire
である。

これらによれば、ArduinoのWireライブラリでmasterとして使える関数は

Wire.begin()
Wire.requestFrom(address,quantity)
Wire.beginTransmission(address)
Wire.endTransmission()
Wire.write(value)
Wire.available()
Wire.read()

であり、これらの使い方を見ていくことにする。
(注:上記の”address”は、バス上で左1bitシフトする前の7bitアドレス、つまり0〜127である。2017.10.11)

========
ではサイトに出ているサンプルスケッチから見ていく。
以下は”Wire Master Writer”である。

#include <Wire.h>

void setup() {
Wire.begin(); // join i2c bus (address optional for master)
}

byte x = 0;

void loop() {
Wire.beginTransmission(8); // transmit to device #8
Wire.write(“x is “); // sends five bytes
Wire.write(x); // sends one byte
Wire.endTransmission(); // stop transmitting

x++;
delay(500);
}
これを2.の記述で対応を取ると
Wire.begin() –> ライブラリの初期化。信号には影響なし。
Wire.beginTransmission(8) –> S SAW ACKS
Wire.write(“x is “) –> WD ACKSの繰り返し
Wire.write(x) –> WD ACKS
Wire.endTransmission() –> P
ということがわかる。

同様に”Wire Master Reader”によれば

#include <Wire.h>

void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Serial.begin(9600); // start serial for output
}

void loop() {
Wire.requestFrom(8, 6); // request 6 bytes from slave device #8

while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}

delay(500);
}

これを2.の記述と対応を取ると
Wire.requestFrom(8,6) –> S SAW ACKS
Wire.available() –> RD。内部のバッファに8bit揃うのを待つ
Wire.read() –> 内部のバッファから読みだしてACKM or NACKMを出す
ということがわかる。

========
では最後に、レジスタアドレスを指定して読み書きするにはどうしたらいいか?

ここでサイトの仕様をよく読むと
Wire.endTransmission(true)はstopを出してバス解放するのに対して
Wire.endTransmission(false)はconnectionをkeepしたまま、repeated startを出す
とある。例えばレジスタアドレス指定して複数バイト読みたい場合、

S -> スタート
SAW ACKS -> slave address7bitをwrite modeで送り、slaveからackを待つ
WD ACKS -> readするregisterのaddress8bitを送り、slaveからackを待つ
S -> repeatedスタート
SAR ACKS -> slave address7bitをread modeで送り、slaveからackを待つ
RD ACKM -> slaveからread data8bitを待つ

RD NACKM -> slaveからread data8bitを待ち、受け取ったらmasterがデータ終了のnackを出す
P -> ストップ

の場合は、

Wire.beginTransmission(slave device address) –> S SAW ACKS
Wire.write(register address) –> WD ACKS
Wire.endTransmission(false) –> S ※ここでrepeated startが出る
Wire.requestFrom(slave device address,quantity) –> SAR ACKS
Wire.available() –> RDが8bit揃うのを待つ
Wire.read() –> ACKMを出す
…(quantity分繰り返し)
Wire.available() –> RDが8bit揃うのを待つ
Wire.read() –> NACKMを出す
Wire.endTransmission(true) –> stopが出る
のシーケンスを組めばよいことがわかる。

ここまで(2016.1.31)

YouTubeにチャンネルを開設しました。

https://blog.ermine.co.jp/