今回はI2C(アイ・スクエアド・シー)(ウィキペディア)です。とりあえず動作確認を行うレベルの内容です。実際に使う場合、例外処理(タイムアウトなど)が必要になり複雑になってしまうので、そこら辺の複雑な部分は割愛し、単純に使うだけの説明を行います。
I2Cを使って以下の制御を行います。
- マスタから各スレーブへ発光ダイオードの制御命令(点灯・消灯)の送信(スレーブは受信)
- 各スレーブに取り付けたスイッチの状態をマスタへ送信(マスタは受信)
<I2Cについて>
- 複数の機器を接続し通信を行うことができます
- 信号線は2本(シリアルデータ(SDA)、シリアルクロック(SCL))
- マスタとスレーブの関係を持っています。すべての管理をマスタが行い、スレーブはマスタの指示で動作します
- 回路とプログラムの作りによりますが、起動中に新たな機器の接続や切断も行うことができます
- スレーブからマスタへ要求や指示を送ることはできない
- データは1バイト送信し、受信側は受信した際、応答伝文(ACK)を返信する(例:もし送信したいのが、10バイトのデータでデータのやり取りだけを考えた場合、10回送信し、その1バイトずつに対して応答を返すので、10回の応答を行う)
<今回の作りについて>
- 起動中の追加・切断は対応していません
- スレーブから起動しなくてはいけません
- 1バイトの送受信
1.今回使用するPICについて
今回使用するPICは「PIC12F1822」です。
2.回路
2.1 電子回路図
2.2 電子回路の写真
2.3 回路説明
2.3.1 I2Cの回路説明
I2Cで使用するSCLとSDAにはプルアップ抵抗が必要です。
2.3.2 マスタの回路説明
- 電源:5[V]
- RA1:SCL
- RA2:SDA
- RA4:発光ダイオード:スレーブ1のスイッチをオンすると点灯
- RA5:発光ダイオード:スレーブ2のスイッチをオンすると点灯
2.3.3 スレーブ1の回路説明
- 電源:5[V]
- RA1:SCL
- RA2:SDA
- RA4:発光ダイオード:マスタからの指示で点灯・消灯
- RA5:スイッチ:オンするとマスタのRA4に接続している発光ダイオードが点灯
2.3.4 スレーブ2の回路説明
- 電源:5[V]
- RA1:SCL
- RA2:SDA
- RA4:発光ダイオード:マスタからの指示で点灯・消灯
- RA5:スイッチ:オンするとマスタのRA5に接続している発光ダイオードが点灯
2.3.5 電源
電源はニッケル水素充電池を4本直列に接続し使用しています。
3.プログラム
3.1 スレーブPICのアドレス
I2Cの場合、スレーブには、そのネットワーク上で一意のアドレスを設定する必要があります。今回は下記のような番号にしました。
スレーブ1:1
スレーブ2:2
3.2 マスタのプログラム説明
3.2.1 各種初期設定
OSCCON = 0b01110000 ; // 内部クロック8MHz ANSELA = 0b00000000 ; // すべてデジタルI/Oに割当てる TRISA = 0b00001110 ; // RA1,RA2,RA3を入力とする PORTA = 0b00000000 ; // ピン状態初期化
OSCCON = 0b01110000;
ANSELA = 0b00000000;
I2Cで使用するRA1とRA2を入力に設定します。(RA3は入力専用)
TRISA = 0b00001110;
PORTA = 0b00000000;
3.2.2 I2Cの設定
SSP1STAT= 0b10000000 ; // 100kHz SSP1CON1= 0b00101000 ; // SDA/SCLピンをI2C、マスター SSP1ADD = 0x4F; // 通信速度クロック値を設定
I2C、マスタモード、baudレート100[kHz]に設定します。
SSP1STAT= 0b10000000;
SSP1CON1= 0b00101000;
SSP1ADD = 0x4F;
設定・計算方法は「PIC12F1822」のデータシートを確認してください。使いたい速度やPICの設定により組み合わせが変わります。
3.2.3 スレーブの起動待ち
for(i=0; i<100000; i++) { // 処理無し(一定時間待ち) }
今回のプログラムでは、スレーブが先に起動していなくてはいけません。電源を入れると同時に起動する作りになっているので、forループで待つようにしています。
3.2.4 マスタからスレーブへの送信処理
// スレーブへ送信 // スタート出力 SSP1CON2 |= I2C_CON2_SEN; // スタート出力待ち while( (SSP1CON2 & I2C_CON2_SEN) != 0 ) { // スタート出力待ち(出力後自動的にOFF) } // 送信するスレーブのアドレスを設定 // 0bit目が0で送信モード SSP1BUF = id << 1; // 転送待ち while( (SSP1STAT & I2C_STAT_BF) != 0 ) { // データの送信待ち(送信完了後自動的にOFF) } // ACK待ち while( (SSP1CON2 & I2C_CON2_ACKSTAT) != 0 ) { // スレーブからのACK待ち(ACK応答を受けると自動的にOFF) } // レディ待ち while( (SSP1STAT & I2C_STAT_RW) != 0 ) { // レディ状態になると自動的にOFF }
スタート条件の生成
SSP1CON2 |= 0x01;
スタート条件が完了するのを待つ
while( (SSP1CON2 & 0x01) != 0 ) {}
送信先スレーブのアドレスを設定(スレーブ1への送信:アドレスは「1」)し、マスタ -> スレーブのモードにする。
アドレス設定時のSSP1BUFは
0ビット目:送受信の設定(0がマスタからスレーブへの送信)
1~7ビット:アドレス
となります。そこで、アドレスを1ビット左にシフトします。すると、アドレスが1~7ビット目に変わり、シフトしたことで0ビット目が0になります。
SSP1BUF = 1 << 1;
登録したアドレスがスレーブに転送されるのを待ちます。
while( (SSP1STAT & 0x01) != 0 ) {}
スレーブからのACK応答を待ちます。
while( (SSP1CON2 & 0x40) != 0 ) {}
次のデータを送信可能になるのを待ちます。
while( (SSP1STAT & 0x04) != 0 ) {}
ここまででマスタからどのスレーブへデータを送信するかの設定が完了です。
// 送信するデータを設定 if( id == 1 ) SSP1BUF = led1; // LED指示 else SSP1BUF = led2; // LED指示 // 転送待ち while( (SSP1STAT & I2C_STAT_BF) != 0 ) { // データの送信待ち(送信完了後自動的にOFF) } // ACK待ち while( (SSP1CON2 & I2C_CON2_ACKSTAT) != 0 ) { // スレーブからのACK待ち(ACK応答を受けると自動的にOFF) } // レディ待ち while( (SSP1STAT & I2C_STAT_RW) != 0 ) { // レディ状態になると自動的にOFF } // ストップ出力 SSP1CON2 |= I2C_CON2_PEN; // ストップ出力待ち while( (SSP1CON2 & I2C_CON2_PEN) != 0 ) { // ストップ出力待ち(出力後自動的にOFF) }
続いてデータを送信します。SSP1BUFに送信したいデータを1バイト(8ビット)ずつ書き込み転送します。
SSP1BUFに送信したいデータを代入(代入することが送信するということになります)
データの転送が完了するのを待ちます。
while( (SSP1STAT & 0x01) != 0 ) {}
スレーブからのACK応答を待ちます。
while( (SSP1CON2 & 0x40) != 0 ) {}
次のデータを送信可能になるのを待ちます。
while( (SSP1STAT & 0x04) != 0 )
送信終了(ストップ)をスレーブに知らせます。
SSP1CON2 |= 0x04;
今回は1バイトの転送で良いので、ここで終了させます。もし、もっと送信したい場合はSSP1BUFにデータを書き込むところから繰り返してください。
ストップ命令の出力を待ちます。
while( (SSP1CON2 & 0x04) != 0 ) {}
以上でマスタからスレーブへの送信は完了です。
3.2.5 マスタのスレーブからの受信処理
// スタート出力 SSP1CON2 |= I2C_CON2_SEN; // スタート出力待ち while( (SSP1CON2 & I2C_CON2_SEN) != 0 ) { // スタート出力待ち(出力後自動的にOFF) } // 取得するスレーブのアドレスを設定 // 0bit目が1で受信モード SSP1BUF = (id << 1) | 0x01; // 転送待ち while( (SSP1STAT & I2C_STAT_BF) != 0 ) { // データの送信待ち(送信完了後自動的にOFF) } // ACK待ち while( (SSP1CON2 & I2C_CON2_ACKSTAT) != 0 ) { // スレーブからのACK待ち(ACK応答を受けると自動的にOFF) } // レディ待ち while( (SSP1STAT & I2C_STAT_RW) != 0 ) { // レディ状態になると自動的にOFF } SSP1CON2 |= I2C_CON2_ACKDT; // NACK SSP1CON2 |= I2C_CON2_RCEN; // 受信を許可 // 受信待ち while( (SSP1STAT & I2C_STAT_BF) == 0 ) { // データの受信待ち(送信完了後自動的にOFF) } SSP1CON2 |= I2C_CON2_ACKEN; // ACKデータを返す
スタート条件の生成
SSP1CON2 |= 0x01;
スタート条件が完了するのを待つ
while( (SSP1CON2 & 0x01) != 0 ) {}
受信元スレーブのアドレスを設定(スレーブ1からの受信:アドレスは「1」)し、マスタ <- スレーブのモードにする。
アドレス設定時のSSP1BUFは
0ビット目:送受信の設定(1がマスタがスレーブから受信)
1~7ビット:アドレス
となります。そこで、アドレスを1ビット左にシフトし、0x01で論理和をとります。すると、アドレスが1~7ビット目に変わり、シフトしたことで0ビット目が0になりますが、0x01で論理和をとるのでアドレス部はそのままで0ビット目が1になります。
SSP1BUF = (1 << 1) | 0x01;
データの転送が完了するのを待ちます。
while( (SSP1STAT & 0x01) != 0 ) {}
スレーブからのACK応答を待ちます。
while( (SSP1CON2 & 0x40) != 0 ) {}
次のデータを送信可能になるのを待ちます。
while( (SSP1STAT & 0x04) != 0 )
受信モードを「肯定応答しない」に設定する。
SSP1CON2 |= 0x20;
マスタの受信モードを有効にする。
SSP1CON2 |= 0x08;
スレーブからの受信を待ちます。
while( (SSP1STAT & 0x01) == 0 ) {}
マスタからスレーブにACKを返す。
SSP1CON2 |= 0x10;
if( id == 1 ) { io1 = SSP1BUF; } else { io2 = SSP1BUF; } // ACK送信待ち while( (SSP1CON2 & I2C_CON2_ACKEN) != 0) { // 送信後自動OFF } // レディ待ち while( (SSP1STAT & I2C_STAT_RW) != 0 ) { // レディ状態になると自動的にOFF } // ストップ出力 SSP1CON2 |= I2C_CON2_PEN; // ストップ出力待ち while( (SSP1CON2 & I2C_CON2_PEN) != 0 ) { // ストップ出力待ち(出力後自動的にOFF) } SSP1CON2 &= ~I2C_CON2_RCEN;
受信したデータを取り出す。受信したデータはSSP1BUFに入っています。SSP1BUFを他の変数に代入するとSSP1BUFはクリアされます。
変数 = SSP1BUF;
先ほど送信した、マスタからスレーブへ送信したACKの処理が完了するのを待ちます。
while( (SSP1CON2 & 0x10) != 0) {}
次のデータを送信可能になるのを待ちます。
while( (SSP1STAT & 0x04) != 0 )
今回は1バイトの転送で良いので、ここで終了させます。
SSP1CON2 |= 0x04;
ストップ命令の出力を待ちます。
while( (SSP1CON2 & 0x04) != 0 ) {}
SSP1CON2 &= ~0x08;
3.3 スレーブのプログラム説明
スレーブは2個用意しています。プログラムの違いはアドレスを「1」にするか「2」にするかの違いです。
3.3.1 各種初期設定
OPTION_REG = 0b00000000 ;// 7bit目が0でプルアップ抵抗が有効 OSCCON = 0b01110000 ; // 内部クロック8MHz ANSELA = 0b00000000 ; // すべてデジタルI/Oに割当てる TRISA = 0b00101110 ; // RA1,RA2,RA3,R5を入力とする PORTA = 0b00000000 ; // ピン状態初期化 WPUA = 0b00100000 ; // RA5をプルアップ抵抗
プルアップを有効にする
OPTION_REG = 0b00000000;
OSCCON = 0b01110000;
ANSELA = 0b00000000;
I2Cで使用するRA1とRA2を入力に設定します。スイッチの状態を認識するためにRA5を入力、発光ダイオードの点灯のためにRA4を出力に設定します。(RA3は入力専用)
TRISA = 0b00101110 ; // RA1,RA2,RA3,R5を入力とする
PORTA = 0b00000000;
スイッチに繋がっているRA5のプルアップを有効にします。
WPUA = 0b00100000;
3.3.2 I2Cの設定
SSP1STAT= 0b10000000 ; // 100kHz SSP1CON1= 0b00100110 ; // SDA/SCLピンをI2C、スレーブ SSP1CON2 |= 0x01; // クロックストレッチを許可 SSP1ADD = (id << 1) & 0xFE; // スレーブのアドレスを設定 SSP1MSK = 0xFE;
I2C、スレーブモード、baudレート100[kHz]に設定します。
SSP1STAT= 0b10000000;
SSP1CON1= 0b00100110;
クロックストレッチを有効にします。クロックストレッチを有効にすると、スレーブからの応答を待ってから次の転送を行うようになします。スレーブでデータの送受信完了後、SSP1CON1のCKPビットをセットしSCLラインを解放しなければいけません。
SSP1CON2 |= 0x01;
スレーブのアドレスを設定します。スレーブ1のアドレスは1です。スレーブ2の場合、アドレスを2にしてください。(右辺の1番左の数字)
SSP1ADD = (1 << 1) & 0xFE;
SSP1MSK = 0xFE;
設定・計算方法は「PIC12F1822」のデータシートを確認してください。使いたい速度やPICの設定により組み合わせが変わります。
3.3.3 割り込み処理の設定
SSP1IF = 0; // I2Cの割り込みフラグをクリア SSP1IE = 1; // I2Cの割り込みを許可 PEIE = 1; // 周辺装置の割り込みを許可 GIE = 1; // 全割り込みを許可
I2Cの割り込みフラグをクリアします。
SSP1IF = 0;
I2Cの割り込みを許可します。
SSP1IE = 1;
周辺装置の割り込みを許可します。
PEIE = 1;
全割り込みを許可します。
GIE = 1;
3.3.4 割り込み処理
void interrupt I2C( void ) { if( SSP1IF ) { if( (SSP1STAT & I2C_STAT_RW) == 0) { // データ or アドレス if( (SSP1STAT & I2C_STAT_DA) == 0) { // アドレスを受信 buf = SSP1BUF; // 空読みする } else { // データを受信 led = SSP1BUF; // データ読出し } SSP1CON1 |= I2C_CON1_CKP; // 通信の再開 } else { // I2Cの送信 if( (SSP1STAT & I2C_STAT_BF) != 0 ) { // アドレス受信後の割り込み buf = SSP1BUF; // アドレスデータを空読み SSP1BUF = send; // 送信データをセット(初回データ) SSP1CON1 |= I2C_CON1_CKP; // 通信の再開 } else { // データの送信後のACK受け取りによる割り込み if( (SSP1CON2 & I2C_CON2_ACKSTAT) == 0) { // ACK応答なら次のデータをセットする SSP1BUF = 0; // 送信データのセット(2byte以降) SSP1CON1 |= I2C_CON1_CKP; // 通信の再開 } else { // NACKで応答された時は送信終了(処理無し) } } } SSP1IF = 0; } }
スレーブでのI2Cの送受信は割り込み処理内で行っています。受信したデータをグローバル変数に代入しメイン処理で使用しています。送信したいデータはグローバル変数に入れておき、送信時に処理を行います。I2Cの割り込みフラグは「SSP1IF」です。
3.3.5 割り込み処理(受信処理):49 ~ 63行目
受信か送信かの判定には以下の条件式を使用します。条件が成り立つ(真)場合、受信処理になります。
if( (SSP1STAT & 0x04) == 0)
受信した内容がアドレスなのかデータなのかを確認します。条件が成り立つ(真)場合、内容はアドレスになります。
if( (SSP1STAT & 0x20) == 0)
- 受信内容がアドレスの場合
SSP1BUFからアドレスを読み込みます。特にその他の処理は不要です。割り込みが発生した段階で自分自身への通信です。SSP1BUFからデータを読み込むと自動的にクリアされます。 - 受信内容がデータの場合
SSP1BUFからデータを読み込みます。必要に応じて読み込んだデータを処理します。
読み込みが完了したら、SSP1CON1のCKPビットをセットし、通信を再開させます。
SSP1CON1 |= 0x10;
3.3.6 割り込み処理(送信処理):64 ~ 88行目
スレーブでの送信タイミングは、マスタに管理されているため、自発的に行うことは出来ません。マスタから送信するように指示が来ると割り込みが発生するので、その際、送信するようにします。
先ほど受信時に判定した処理を利用し送信判定を行います。条件が成り立つ(真)場合が受信なので、その反対で条件が成り立たない(偽)場合、送信処理ということになります。(else内に処理を記述)
if( (SSP1STAT & 0x04) == 0) {}
else
{
送信処理を記述
}
バッファの空き状態を確認します。
if( (SSP1STAT & 0x01) != 0 )
もしバッファが空でない場合は受信したデータが存在しているか、送信中のデータが存在していることになります。通信の開始時はマスタからスレーブへの送信指示が来ているので、バッファにはアドレスが入っており、条件は「真」になります。その後、スレーブからマスタへの送信が正常に完了すれば「偽」になるので、次のデータを送る処理に入るようにします。ただし、送信中のデータがある場合も「真」となってしまうので、注意が必要です。
- 条件が「真」の場合(初回送信処理)
マスタから送られてきたアドレスが入っているのでSSP1BUFから読み込みます。
buf = SSP1BUF;SSP1BUFに、マスタに送信するデータをセットします。(sendは送信するデータ(1バイト))
SSP1BUF = send;送信データのセットが完了したので、SSP1CON1のCKPビットをセットし、通信を再開させます。
SSP1CON1 |= 0x10; - 条件が「偽」の場合(2回目以降の送信処理)
マスタから次の送信を要求しているのかを確認します。条件が「真」の場合、要求が来ているので次のデータを送信します。条件が「偽」の場合、送信処理の完了なので特に処理は必要ありません。
if( (SSP1CON2 & 0x40) == 0)もし次のデータを送信する場合は以下の処理を行います。
SSP1BUF = データ;
送信データのセットが完了したので、SSP1CON1のCKPビットをセットし、通信を再開させます。
SSP1CON1 |= 0x10;
3.3.7 割り込み処理(送受信処理後):90行目
I2Cの割り込みフラグを下すのを忘れないようにしてください。
SSP1IF = 0;
3.4 補足:2バイト以上の型の送信について
もし2バイトのデータを送りたい場合、その2バイトのデータを分解して送信しなくてはいけません。
- 始めに上位8ビットを送信(右に8ビットシフトを行い、00FFで論理積を行い、1バイトの型に入れ送信)
- 続いて下位8ビットを送信(00FFで論理積を行い、1バイトの型に入れ送信)
- 受信した上位8ビットを左に8ビットシフトし、下位8ビットのデータと論理和をとる
以上の手順で2バイトの型のデータを送信することができます。4バイトでも8バイトでも同様です。
3.5 データシートのフロー(サンプル)
データシートに記載されている流れを簡単な図にしてみました。本サイトの流れと違うところがあるので注意してください。あくまでデータシートに掲載されている例です。
3.5.1 マスタのサンプルフロー
3.5.2 スレーブのサンプルフロー
3.6 タイムアウト処理
今回のプログラムでは全て正常に動作することを前提に作成しています。while文を使い条件が成り立つまで永久に待つようになっているので、条件がそろわないと、何も仕事をしなくなってしまします。そこで本来は無応答を避けるためにタイムアウト処理やイレギュラーなことが発生した場合の再初期化処理などが必要になります。例えば、while文の中に一定時間経過したら抜けるようにするなどのような処理です。
実際に使う場合はしっかりとそういった処理を追加するようにしてください。
3.5 ソースコード
プログラムの全文を掲載します。
<注意>
本サイトの注意事項を確認してください。
ソースコードや回路図などを使用する場合、上記注意とともに、自己責任でお願いします。
4.動作確認
- PICの電源を入れます
- マスタはプログラムの最初に一定時間待つようにしているので、少し待ってください
- スレーブ1とスレーブ2の発光ダイオードが交互の点滅します
- スレーブ1のスイッチを押している間、マスタのRA4の発光ダイオードが点灯します
- スレーブ2のスイッチを押している間、マスタのRA5の発光ダイオードが点灯します
<更新履歴>
日付 | 内容 |
2015年6月2日 | 新規作成 |
2015年6月15日 | 確認動画の追加 |
2016年10月6日 | フォーマット変更(内容の変更はなし) |
Very informative blog post.Really thank you! Keep writing. bedeabdcdded
ピンバック: 部屋の明るさを計測してブラウザから確認してみる(その1) – 絶対IT企業に転職するマンの独学プログラミング
すごいな
さらにわかりやすい
ありがとうございます
初めまして
私は79歳の老人です。
I2cをやりたくて、あなた様のサイトに出会いました。
アセンブラは少しできます。
C言語は初めてなので、幼稚な質問が行くかもわかりませんがよろしくお付き合い願えますか?
おかげさまで成功しました。
これでI2Cを勉強できます。
当方パソコンが壊れ、MPLABXも入れ直したので、v4.2を使っています。ありがとうございました。
大変参考になりました。ありがとうございます。
xc8 v2.10ではinterruptは__interruptと変更になっているようです。