##1.はじめに
I2C(アイ・スクエアド・シーと読むのが正しいらしい)はフィリップスが提唱した通信プロトコルで、センサとマイコンのインターフェースなどに用いられます。使える経路長は基板内通信よりは長いが2-3mまでが無難な範囲です(ストレッチする技術もあるようですが)。
クロック(SCL)と、データ(SDA)のわずか2本の信号線で双方向にデータのやり取りができるため、多くのデバイスで使われています(こう書くと、本当に2本しか接続しなくて「動かない!」というトラブルがときどき見受けられますが、信号線のリターンとしてGNDの接続は必須です。念のため)。
信号線本数が少なく、お手軽なI2Cですが、マスタ側からのみ出力されるクロック(SCL)はともかく、マスタ、スレーブの両方から出力されるデータ(SDA)は、信号の流れが煩雑に変わるため、解析が難しいという問題があります。マスタorスレーブのどちらが出している信号なのかがわからないので、何が原因かわからないようなことになるわけです。
今回は、MATLABを用いて、信号の解析をおこなってみます。MATLABを使わない方でもわかるように書いてみました。
なお、ここでは、7bitアドレス(スレーブのIDが7bit)、マスタが1つの場合についての説明のみで、10bitアドレスやマルチマスタの場合については説明しません。
##2.I2Cの特徴と制御
最初にI2Cの特徴をまとめました。
1)マスタ(マイコンなど)とスレーブ(センサなど)を接続します。
接続信号はSCL(クロック)とSDA(データ)です。
スレーブのデバイスは複数用いることができます。
マスタは通常1つですが、特殊な使い方としてマルチマスタもあります。
2)データのやり取りをおこなっていないときは、SCL(クロック)、SDA(データ)ともにHighになっています。
SCL&SDAは10kΩ程度の抵抗でHighに吊って(引っ張り上げられて)います。
マスタもスレーブもLowに引き下げる機能しか持ちません。ですので、Highに吊る抵抗がないと、信号をHighに戻すことができず、機能しなくなります。
(下図のプルアップ抵抗の値:10kΩは一例であり、条件により変える場合があります)
3)通常のデータ通信では、SCL(クロック)がHighの間(と、その前後の決められた時間)は、SDA(データ)が変化すること(H→L、L→H)は許されていません(次の項目に例外を示します)。
4)データシーケンスの開始時(スタートコンディション)、終了時(ストップコンディション)には意図的にSCL(クロック)がHighの期間にSDL(データ)が変化して、開始・終了を示します。
5)SCL(クロック)はマスタ側が常に出力します。
これに対しSDA(データ)に関してはマスタ、スレーブの双方が状況に応じて出力します。
6)最小シーケンス単位は1単位が9ビットからなり、前の8ビットは通常のデータ、最後の1ビットは応答(アクノリッジ)に用いられます。
7)スタートコンディションの直後の9ビットは、スレーブデバイスの指定に用いられます。
9ビットのうち最初の7ビットがアドレス、次の1ビットは、この後のシーケンスでスレーブデバイスに書き込みたいのか(Write:ビットはLow0)、スレーブデバイスから読み出したいのか(Read: ビットはHigh)を指定します。
最後の1ビットはACK(アクノリッジ)となっています。
8)アドレス指定の9ビットの後は、9ビットずつでデータとなります。9ビットのうちの最初の8ビットがデータ、最後の1ビットがACKです。
##3.MATLABによるI2Cの信号解析
一例として、温度、湿度、圧力センサを備えたBME280とArduinoをつないで温度情報を取り込む場合を見てみましょう。
まず、オシロスコープから取り込んだSCL(クロック)、SDA(データ)の波形ファイルをMATLABにLoadします。
SCL=load('SCLbme280T.dat');%クロック読み込み
SDA=load('SDAbme280T.dat');%データ読み込み
これらのデータを表示すると、
上図のようになっています。
このままですと、振幅が多値ですので、2値に変換します。電圧が2.5Vあたりを、しきい値とするとよさそうです。
2値化には、logicalを用います。
Vth=2.5;%しきい値
LSCL=logical(SCL>Vth);% 2値化されたSCL
LSDA=logical(SDA>Vth);% 2値化されたSDA
###3-1.シーケンスを切り出す
I2Cのシーケンスは、スタートコンディションからストップコンディションまで1つの区切りとなります。
これらのコンディションはI2Cのプロトコルの中でも特殊な部分であり、SCLがHighの期間中にSDAが変化します(通常はSCLがHighの間はSDAは変化してはいけません)。
まず、スタートコンディションを抽出します。
スタートコンディションはSCLがHighのときに、SDAが立ち下がるところです。これをプログラムに書くと、
SDA_Edge=diff(LSDA);%diff 隣接データ間の差分を取る
%SDA_Edgeが+1のところが立ち上がり、-1のところが立ち下がりとなる。
%0のところはデータが変化しないところ。
%SDA_Edgeは差分なのでLSDAよりデータ数が1つ少なくなる。
SDA_EdgeN=logical(SDA_Edge<0);%SDAの立下りエッジ検出
ST_Cond=(SDA_EdgeN & LSCL(2:end));%SDAの立下りで、かつSCLがHighの点
ST_Cond_Cnt=find(ST_Cond==1);%スタートコンディションのポイント
同様にしてストップコンディションを抽出します。
ストップコンディションはSCLがHighのときに、SDAの立ち上がるところです。これをプログラムに書くと、
SDA_EdgeP=logical(SDA_Edge>0);%SDAの立上りエッジ検出
SP_Cond=(SDA_EdgeP & LSCL(2:end));%SDAの立上り かつ SCLがHigh
SP_Cond_Cnt=find(SP_Cond==1);%スタートコンディション位置
こうして、抽出したスタートコンディション、ストップコンディションをSCL,SDAのグラフに書き加えます。
###3-2.SDA(データ)から必要なデータを拾う
I2Cのプロトコルにおいては、SCL(クロック)がHighの間にSDA(データ)が取り込まれます。ここではSCL(クロック)の立ち上がりで取り込まれる、と見ることにします。
SCLの立ち上がり検出は次のようになります
SCL_Edge=diff(LSCL);%LSCLの前後に隣接するデータ間の差分を取る
SCL_EdgeP=logical(SCL_Edge>0);%立ち上がり部分はSCL_Edgeの+1
SCL_EdgeP_Cnt=find(SCL_EdgeP>0);%立ち上がり位置
SCLの立ち上がり部分のSDAの値を全てピックアップしてみます。
SDA_Value=LSDA(SCL_EdgeP);%SDAのうちの意味のある部分のみを抜き出す
%SCL,SDA,スタートコンディション、ストップコンディションのグラフを描く
%SCLの立ち上がりのSDAにマークをつける
hold on;
plot(SCL_EdgeP_Cnt,SDA_Value*0.5-0.1,'ko');
hold off;
SDAの線上の○の部分が必要なデータとなります(見にくくてすみません)。
###3-3.データの分類をする
上でピックアップしたSDAの値には、いくつかの種類があります。
1)まずスタートコンディションから1つめのシーケンスは、スレーブのアドレスが出力されます。
1つめのシーケンスの9ビットのデータのうち、最初の7ビットはスレーブデバイスのアドレス(ID)で、マスタが出力します(スレーブデバイスを指名します)。I2Cにおいては、複数のスレーブデバイスが存在し、この7ビットのアドレスに当てはまるスレーブデバイスとの通信を行ないます。
アドレスの7ビットに続く1ビットもマスタが担当し、スレーブにデータを送りたいのか(Write=Low)、スレーブからデータを受け取りたいのか(Read=High)を出します。
さらにWrite/Readの1ビットの次の1ビット(9ビットのうちの最終ビット)はアクノリッジ(Ack)で、「了解しました」の意味でスレーブがLowを返します。
(スレーブが動作していない場合は、Ackを返すことができないのでHighのままとなります:No-Ack)。
以上のアドレス部分の解析図は下のようになります。
プログラムは下記のようになり複雑のように見えますが、スタートコンディションを基準に9ビット取り出して、アドレス、Read/Write、アクノリッジに分類しているだけにすぎません。
for s=1:length(ST_Cond_Cnt)
u=find(SCL_EdgeP_Cnt > ST_Cond_Cnt(s) & SCL_EdgeP_Cnt < SP_Cond_Cnt(s));
Num_byte=(length(u)-1)/9;
for w=0:Num_byte-1
ww=w*9;
S=0;
if w==0
for x=1:7
S=S+SDA_Value(u(ww+x))*2^(7-x);
text(double(SCL_EdgeP_Cnt(u(ww+x))),double(SDA_Value(u(ww+x)))*0.05-0.3,'A','Color','k');
end
Adrs=dec2hex(S,2);
text(double(SCL_EdgeP_Cnt(u(ww+3))),-0.4,Adrs);
RW(s)=SDA_Value(u(ww+x+1));
if RW(s)==0
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.3,'W','Color','r');
else
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.3,'R','Color','r');
end
ACK=SDA_Value(u(ww+x+2));
if ACK==false
text(double(SCL_EdgeP_Cnt(u(ww+x+2))),0.05-0.2,'S','Color','c');
else
text(double(SCL_EdgeP_Cnt(u(ww+x+2))),0.05-0.2,'NA','Color','c');
end
end
end
end
2)スタートコンディションから2つめ以降のシーケンスは、データが出力されます。
2つめ以降のシーケンスの9ビットのデータのうち、最初の8ビットはデータが出力されます。
直前のアドレス7ビットの後がWriteの場合は、引き続きマスタからスレーブにデータが送られます(スレーブデバイスの設定など)。Readの場合は、スレーブからマスタにデータが返されます(測定データなど)。
8ビットの後の1ビット(アクノリッジ)は「データを受信したほう」が出力します。Writeの後の場合はスレーブが、Readの後の場合はマスタがAckをLowにします。
マスタがAckを出力することになっている場合でも、ストップコンディションの直前では、AckをLowにしません(NoAck)。
以上のデータ部分の解析図は下のようになります。
プログラムは下記のようになり複雑のように見えますが、スタートコンディションの後、10ビット目から9ビットずつ取り出して、データ、アクノリッジに分類しているだけです。誰が出力したアクノリッジかは、直前のRead/Writeを見て判定しています。
for s=1:length(ST_Cond_Cnt)
u=find(SCL_EdgeP_Cnt > ST_Cond_Cnt(s) & SCL_EdgeP_Cnt < SP_Cond_Cnt(s));
Num_byte=(length(u)-1)/9;
for w=0:Num_byte-1
ww=w*9;
S=0;
if w>0
for x=1:8
S=S+SDA_Value(u(ww+x))*2^(8-x);
text(double(SCL_EdgeP_Cnt(u(ww+x))),double(SDA_Value(u(ww+x)))*0.05-0.3,'D','Color','k');
end
Dat=dec2hex(S,2);
text(double(SCL_EdgeP_Cnt(u(ww+4))),-0.4,Dat);
ACK=SDA_Value(u(ww+x+1));
if ACK==false
if RW(s)==0
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.2,'S','Color','r');
else
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.2,'M','Color','b');
end
else
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.2,'NA','Color','g');
end
end
end
end
##4.まとめ
今回の流れをすべてプロットすると下図のようになります。
プログラムをまとめますと、
%SCL(クロック)、SDA(データ)読み込み
SCL=load('SCLbme280T.dat');
SDA=load('SDAbme280T.dat');
%元波形プロット
figure(2);
t=1:length(SCL);
subplot(2,1,1);
plot(t,SCL,'r');
title('SCL(クロック)');
xlabel('point');
ylabel('[Volt]');
subplot(2,1,2);
plot(t,SDA,'b');
title('SDA(データ)');
xlabel('point');
ylabel('[Volt]');
%2値化
Vth=2.5;%しきい値
LSCL=logical(SCL>Vth);
LSDA=logical(SDA>Vth);
%2値化波形プロット
figure(3);
t=1:length(LSCL);
subplot(2,1,1);
plot(t,LSCL,'r');
ylim([-0.2 1.2]);
title('SCL(クロック)');
xlabel('point');
yticks([0 1]);
subplot(2,1,2);
plot(t,LSDA,'b');
ylim([-0.2 1.2]);
title('SDA(データ)');
xlabel('point');
yticks([0 1]);
%解析
t=1:length(LSCL);
%Start condition 抽出
SDA_Edge=diff(LSDA);
SDA_EdgeN=logical(SDA_Edge<0);%SDAの立下りエッジ検出
ST_Bit=(LSCL(2:end)&SDA_EdgeN);%SCLがHighで、かつ、SDAの立下り
ST_Cond_Cnt=find(ST_Bit==1);%スタートコンディション位置
%Stop Condition 抽出
SDA_EdgeP=logical(SDA_Edge>0);%SDAの立上りエッジ検出
SP_Bit=(LSCL(2:end)&SDA_EdgeP);%SCLがHighで、かつ、SDAの立上り
SP_Cond_Cnt=find(SP_Bit==1);%スタートコンディション位置
%Start & Stop Condition 表示
figure(4);
plot(t,LSCL*0.5+0.45,'r',t,LSDA*0.5-0.1,'b');
lgd = legend('SCL','SDA');
lgd.AutoUpdate = 'off';
hold on;
for s=1:length(ST_Cond_Cnt)
plot([ST_Cond_Cnt(s) ST_Cond_Cnt(s)],[-0.5 1.1],'g--');
end
for s=1:length(SP_Cond_Cnt)
plot([SP_Cond_Cnt(s) SP_Cond_Cnt(s)],[-0.5 1.1],'m--');
end
%SCLの立ち上がりエッジ 抽出
SCL_Edge=diff(LSCL);
SCL_EdgeP=logical(SCL_Edge>0);
SCL_EdgeP_Cnt=find(SCL_EdgeP>0);
%SDAの値を抽出
SDA_Value=LSDA(SCL_EdgeP_Cnt);
ylim([-0.5 1.1]);
yticks([]);
%アドレス&データ部分解析
for s=1:length(ST_Cond_Cnt)
u=find(SCL_EdgeP_Cnt > ST_Cond_Cnt(s) & SCL_EdgeP_Cnt < SP_Cond_Cnt(s));
Num_byte=(length(u)-1)/9;
for w=0:Num_byte-1
ww=w*9;
S=0;
if w==0
for x=1:7
%アドレス解析
S=S+SDA_Value(u(ww+x))*2^(7-x);
text(double(SCL_EdgeP_Cnt(u(ww+x))),double(SDA_Value(u(ww+x)))*0.05-0.3,'A','Color','k');
end
Adrs=['0x',dec2hex(S,2)];
text(double(SCL_EdgeP_Cnt(u(ww+2))),-0.4,Adrs);
RW(s)=SDA_Value(u(ww+x+1));
if RW(s)==0
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.3,'W','Color','r');
else
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.3,'R','Color','r');
end
ACK=SDA_Value(u(ww+x+2));
if ACK==false
text(double(SCL_EdgeP_Cnt(u(ww+x+2))),0.05-0.2,'S','Color','r');
else
text(double(SCL_EdgeP_Cnt(u(ww+x+2))),0.05-0.2,'NA','Color','g');
end
else
for x=1:8
%データ解析
S=S+SDA_Value(u(ww+x))*2^(8-x);
text(double(SCL_EdgeP_Cnt(u(ww+x))),double(SDA_Value(u(ww+x)))*0.05-0.3,'D','Color','k');
end
Dat=['0x',dec2hex(S,2)];
text(double(SCL_EdgeP_Cnt(u(ww+3))),-0.4,Dat);
ACK=SDA_Value(u(ww+x+1));
if ACK==false
if RW(s)==0
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.2,'S','Color','r');
else
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.2,'M','Color','b');
end
else
text(double(SCL_EdgeP_Cnt(u(ww+x+1))),0.05-0.2,'NA','Color','g');
end
end
end
end
xlabel('point');
yticks([]);
となります。
ちなみに、ZeroplusのロジックアナライザLAP-CのI2Cプロトコルでデータ取込み&表示した結果は下記のようになりました。
おつかれさまでした。