316.PICでI2C通信(PIC16F873)


2002/12/12 【ソフトウエア編TOPに戻る】

今回はPIC16F873を使ってI2C通信の基礎実験をしてみましょう。I2CとはInter Integrated Circuitの略でアイスクエアシーと読みます。IICと表記される場合もあります。力弥は未だにアイツーシーなどと言ってしまいますが、これはダメです。小泉今日子のことをkyon2などと表記した時代がありましたが、裏でなにか流行でもあったのでしょうか。個人的にはわざわざアイスクエアシーなどと呼ばせずに、IICで良いじゃないかと思ったりしますが...多分、皆さんも同じ思いではないでしょうか?

そんな話はどうでも良いのですが、I2Cはひとつのマスター(主)役のデバイスに対して複数のスレーブ(従)役のデバイスをぶら下げることによって、近距離間の1対多チャンネル通信が手軽に行えるのが魅力です。手軽といってもアセンブラで通信手順を記述するのは力弥にとって壁が高かったのですが、最近導入したPICのCコンパイラ(CCS)ではI2Cが組み込み関数としてサポートされているので、随分敷居が低くなりました。

I2C通信を行うためには、デバイスにSSP(Synchronous Serial Port)モジュールがハードウエア的に搭載されている必要がありますが、CCSの組み込み関数を利用するとSSPが搭載されていないPIC(PIC16F84Aなど)でもソフトウエアの力でマスター側としては動作させることができます。ただし、SSPが搭載されていないPICをスレーブ側として動作させることは出来ません。

PIC16F873には、このI2C通信を行うためのSSPが搭載されていますので、マスター側としても、スレーブ側としても、どちらでも動作させることができます。今回はアセンブラではなく、CCSのCコンパイラを使ってI2C通信の実験をしてみます。


316−1.I2C通信とは

 

I2C通信に関する規格やタイミングの詳細な解説については、後閑さんの「電子工作の実験室」に分かり易く紹介されていますし、PIC16F873のハードウエアマニュアルなどにも細かく記載がありますので、ここでは省略いたしますが、以下にその概略だけを簡単にご紹介します。

I2C通信は、主に同一基板内などの近距離に配置されたデバイス間でデータのやり取りを行うための通信方式です。例えばCPUとEEPROMやA/DコンバータやLCD表示デバイスなどとの間での通信です。デバイス間はSDA(serial data)とSCL(serial clock)の2本の信号線だけをバスとして共有して通信を行います。通信速度は100Kbps、400Kbps、3.4Mbpsなどがあり、PIC16F873では100Kbps400Kbpsをサポートしています。ひとつのデバイスをマスターにして、それにスレーブとなるデバイスを複数バス接続することによって1対多チャンネル間の通信を行うことができます。このとき、通信の主導権は全てマスターが握り、各スレーブからマスターに対して通信要求を出したり、各スレーブどうしが通信を行うことなどはできません。

各デバイス間の接続は以下のようなネットワークとなります。

マスターを複数接続するマルチマスター機能というものもありますが、ここではマスターを1個とします。スレーブは複数接続することができます。スレーブになる各デバイスにはアドレス情報を持たせ、マスター側から送られてきたアドレス情報が自分のアドレスと一致したときだけ、データの送受信が出来る仕組みになっています。アドレスには7ビットアドレスモード10ビットアドレスモードの2種類があります。通常は処理が簡単な7ビットアドレスモードで十分だと思いますが、このモードで最大127個までのスレーブを接続することができる計算になります。ただし、I2Cの通信方式は+5VにプルアップされたSDASCLの各信号線を、それぞれのデバイスがオープンドレイン等によるワイヤードオア接続で信号の操作と共有を行っている関係から、電気的にはあまり強い環境ではありません。なので、沢山のデバイスを接続したり、通信距離を長くしたりといったことには不向きなので、ハードウエアの構築には細心の注意が必要でしょう。

今回、実験を行うためのプラットフォームとして、自作のI2Cの実験基板を利用します。

テストボード上には4個のPIC16F873が実装できます。各PICのSDA(RC4ポート)SCL(RC3ポート)はバス接続されており、それぞれRB0RB1の2ビット分に、動作をモニタリングするためのLEDを接続しています。

そして、RB4,5,6の3ビットを使って、PICの実装場所(ID番号)を検出するための配線を行っています。詳しくは13.I2Cテストボードのページを参照して下さい。

ちなみにRB4〜6の配線は、PICの各ソケットごとに以下のようになっています。

  RB6 RB5 RB4
ソケット① 0 0 0
ソケット② 0 0 1
ソケット③ 0 1 0
ソケット④ 0 1 1

0:GND、1:+5V ソケット番号は左から①、②、③、④の順です。

【先頭に戻る】


316−2.CCSのCコンパイラにおけるI2Cの組み込み関数と通信手順

 

CCSのCコンパイラに用意されているI2C用の組み込み関数には以下のものがあります。基本的には、これだけのプリプロセッサと関数だけで通信のプログラムを組みます。

ここでも詳細の説明はCCSのマニュアルもしくは後閑さんの「電子工作の実験室」を参照して下さい。ここでは概要と重要事項だけをご紹介します。

 

プリプロセッサ/関数 説 明
#USE I2C(Mode,SDA=pin,SCL=pin,Option) I2Cを利用するためのプリプロセッサ。マスター/スレーブやSDA/SCLに利用するピンの指定、通信速度などを指定する。
I2C_START( ); マスターモードのPICが、スタートコンディションを出力して通信を開始することを通知するための関数。
I2C_WRITE(byte); マスターもしくはスレーブのPICが、相手に対してデータを送信するための関数。byteが送信データを示す引数で、1バイト(8ビット)のデータとなる。
data = I2C_READ( ); マスターもしくはスレーブのPICが、相手からデータを受信するための関数。1バイトのデータを関数の戻り値として受け取る。I2C_READ(0);とすると、相手に対して受信のACK(受け取ったという返事)を返さない。
ready = I2C_POLL( ); マスターもしくはスレーブのPICが、相手からデータを受け取っていれば戻り値として1(TRUE)を返し、受け取っていなければ(データバッファが空ならば)0(FALSE)を返す。

I2Cの通信手順には、大きくわけて以下の2個のパターンがあります。

 ■1.マスター送信 → スレーブ受信

 ■2.マスター受信 ← スレーブ送信

以下に、7ビットアドレスモードでの、上記の組み込み関数を使った場合の通信手順を簡単にご説明します。

 

■1.マスター送信 → スレーブ受信

マスター側からスレーブ側に対してデータを送信する場合の通信手順です。

先ずマスター側からスタートコンディションを投げて通信の開始を知らせます。続いて投げる最初の1バイト目には、通信したいスレーブのアドレスaddを乗せます。ここで乗せるアドレス情報は、以下のようなビット割りになっているので注意して下さい。

7 6 5 4 3 2 1 0
add6 add5 add4 add3 add2 add1 add0 R/W

最下位の0ビット目は、これから行う通信がマスター側から見て送信なのか受信なのかを指定するためのビットです。マスターが送信の場合は0、マスターが受信の場合は1にセットします。

そして、実際のスレーブ指定のアドレスは1ビット目から指定します。なので、マスター送信の場合、i2c_write( )関数に渡すアドレスデータは、見かけ上0から始まる偶数アドレス(0、2、4、6、8、A、C、E...)として指定することになります。スレーブ側は送られてきたアドレスデータから、自分は次に受信モードで動くのだと判断します。実際にスレーブ側が自分で持つアドレスデータも、この1個飛びの偶数アドレスで設定します。

さて、送られてきたアドレスデータが自分のアドレスと同じだった場合、スレーブはマスターから送られてくるデータを受信することができます。受信は、マスター側のi2c_write( )関数1個に対して、スレーブ側のi2c_read( )関数1個のペアで1バイトずつ行います。このペアをずらっと並べれば、連続して複数バイトのデータを受信することができます。

マスターは所定のデータを送信し終わったらストップコンデョションを投げて、通信の終了を知らせます。

 

■2.マスター受信 ← スレーブ送信

スレーブ側からマスター側に対してデータを送信する場合の通信手順です。

マスターが受信の場合にも、マスター側からスタートコンデョションを投げて通信の開始を知らせます。続いて投げる1バイト目のデータには、通信したいスレーブのアドレスaddに1を足したデータを乗せます。これは上記の説明の通り、アドレスデータの0ビット目を1にセットすることによって、指定のスレーブを送信モードにするためです。

次に、指定されたアドレスを持つスレーブが、あらかじめi2c_write( )関数で準備しておいたデータをマスター側に送信します。ここでも、送信はマスター側のi2c_read( )関数1個に対して、スレーブ側のi2c_write( )関数1個のペアで1バイトずつ行います。

I2Cの通信手順上、マスターが受信する最後のi2c_read( )関数には、引数としてを指定します。(i2c_read(0))。これは、受信後にスレーブ側に返すACK信号(受信できたことを知らせる信号)を返さないことによって、もう受信が終わったよと教えているのです。そして、最後にストップコンディションを投げて通信の終了を知らせます。

 

以上がCCSのCコンパイラを使った通信プログラムのおおまかな動きです。スレーブ側に持たせるアドレスの設定と、マスターが通信相手を選ぶためのアドレス指定、そして、そのデータに含まれる送受信方向の指定、といったことが理解できれば、あとは難しいことはありません。

 

【2003.01.11】

「難しいことはありません」なんて書いておきながら、イマイチ癖が掴みきれないところがあります。それは、まさに上記のサンプルのような複数バイトを同時に送受信する場合の動作です。実際にi2c_start( );i2c_stop( );の間で複数のデータ送信を実行してループ命令で繰り返してみると、スレーブ側で受信する1バイト目のデータ(上記ではdata1)に、受信したスレーブアドレスが格納されたり、ちゃんと意図したデータが格納されたりという症状が交互に起きます。これが、1バイトだけであれば全く安定して意図したデータだけを送受信させることができます。色んなパターンを組んで試してみているのですが、どうも原因がわからず、その癖を掴みきれないのです。

どなたか、分かる方がいらしたらお教えください...m(_ _;m

また、原因が掴めて安定した動作が出来るようになったら、この場でご報告します。

【2007.09.12】
岐阜にお住いの山田茂雄様より、以下の対応策についてご連絡を頂きました。今日現在、力弥の作業環境で検証できる状況になく、試せないでいますが、同様のお悩みを持つ方は是非お試し下さい。

マスターから、アドレスデータを送信したすぐ後に

i2c_read()
をすることで不安定になるようです。
これを防ぐために、スレーブ側で
 int state;
 state = i2c_isr_state();
とし、もし"state"が0ならば"i2c_read()"を実行しない、とすることで、解決できました。
(アドレスを受け取りデータ送信待ちが0、データを受け取るごとに値増)


 

【先頭に戻る】


316−3.実際のプログラム例

 

それでは、実際に通信を行わせるプログラムの例をご紹介しましょう。ここでは、基本的な以下のパターンをご紹介したいと思います。

 

■1.マスター送信 → スレーブ受信 (スレーブは割り込み無し/有り)

■2.マスター受信 ← スレーブ送信 (スレーブは割り込み無し/有り)

■3.マスター送受信←→スレーブ送受信 (スレーブは割り込み有り)

■4.スレーブアドレスをハードウエアから検出して自動設定する方法

 

それぞれの事例では、マスター1個に対してスレーブ3個の通信を行っています。スレーブのアドレスは、それぞれ0x00、0x02、0x04です。0x00のアドレスは特定のスレーブではなく「全てのスレーブを対象としたデータ送信」という意味合いで動作させるモードもあり、本来は空けておくべきアドレスだと思いますが、今回は勘弁してください。

なお、以下のプログラムについて、各種イニシャル処理部分やハードウエアを動作させるための組み込み関数などは、後閑さんの「電子工作の実験室」CCSのマニュアルを参照して頂くということで、詳しい解説は省略させて頂き、要点の解説のみとさせてください。(手抜きですみません...)


■1.マスター送信 → スレーブ受信 (スレーブは割り込み無し/有り)

 

マスターからスレーブに対して1バイトのデータを投げるプログラムです。

 

【マスター側】 ここからダウンロードできます。i2cmas1.c

//////////////////////////////////////////////////
// I2Cマスターテストプログラム                                   //
// PIC16F873                                RIKIYA 2002.11.29 //
//                                                           i2cmas1.C //
// 1:3通信用のマスター側プログラム                           //
// 各スレーブに順次0x0fと0x00のデータ                      //
// を送信する。                                                       //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                        // clock 10MHz
#use fast_io(B)                                            // 固定入力モード
#use i2c(MASTER,SDA=PIN_C4,SCL=PIN_C3,FAST,FORCE_HW)

void i2c_led(int add);

//メイン関数/////////////////////////////////////////

main(){
int count = 0;

      set_tris_b(0xf0);                                     //RB 7-4:IN 3-0:OUT
      set_tris_c(0x00);                                    //RC 7-0:OUT
      output_float(PIN_C3);                             //I2C pin float
      output_float(PIN_C4);                             //I2C pin float

// 正常起動確認
      output_b(0x0f);                                     //PBに0x0fを出力
      delay_ms(1000);                                    //1000mS待機
      output_b(0x00);                                    //PBに0x00を出力

// 各スレーブに順次アクセス
      while(1){
            switch(count){
                  case 0:
                        i2c_led(0x00);
                        break;
                  case 1:
                        i2c_led(0x02);
                        break;
                  case 2:
                        i2c_led(0x04);
                        break;
            }
      count++;
      if(count > 2){ count = 0; }
      }
}

//各スレーブ制御/////////////////////////////////////
//スレーブのアドレスを引数addで渡す

void i2c_led(int add){

      i2c_start();                                         //スタートコンディション
      i2c_write(add);                                    //アドレスaddに送信
      i2c_write(0x0f);                                   //送信データ
      i2c_stop();                                         //ストップコンディション
      output_b(0x0f);                                   //自分のPBに0x0fを出力

      delay_ms(250);                                   //250mS待機

      i2c_start();                                        //スタートコンディション
      i2c_write(add);                                   //アドレスaddに送信
      i2c_write(0x00);                                  //送信データ
      i2c_stop();                                         //ストップコンディション
      output_b(0x00);                                  //自分のPBに0x00を出力

      delay_ms(250);                                   //250mS待機
}

プリプロセッサ部分で重要なのが以下の部分です。

#use i2c(MASTER,SDA=PIN_C4,SCL=PIN_C3,FAST,FORCE_HW)

ここでは、マスターモードで動きますという指定をしています。

メイン関数は、i2c_led( )という関数を順番に繰り返し実行する処理をしています。i2c_led( )引数としてスレーブアドレスを受け取り、指定されたアドレスのスレーブにデータを送信する動作をさせています。今回は、それぞれ引数として0x00,0x02,0x04を順番に渡しています。

i2c_led( )関数内では、スタートコンディション→アドレス指定→データ送信→ストップコンディションという一連の動きをして0x0fというデータをスレーブに投げています。250mS待機した後、再び同じ処理で今度は0x00というデータを投げ、再び250mS待機してから関数を抜けます。

 

【スレーブ側割り込み無し】 ここからダウンロードできます。i2cslv11.c

//////////////////////////////////////////////////
// I2Cスレーブテストプログラム                                   //
// PIC16F873                                 RIKIYA 2002.11.29 //
//                                                            i2cslv11.C //
// ID = 0x00                                                            //
// 1:3通信スレーブ側プログラム                                 //
// マスター側から受信したデータをPBに出力する          //
// 割り込みなし                                                       //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                         // clock 10MHz
#use fast_io(B)                                             // 固定入力モード
#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

//メイン関数/////////////////////////////////////////

main(){

      set_tris_b(0xf0);                                      //RB 7-4:IN 3-0:OUT
      set_tris_c(0x1f);                                      //RC 7-5:OUT 4-0:IN
      output_float(PIN_C3);                              //I2C pin float
      output_float(PIN_C4);                              //I2C pin float
      output_b(0x01);                                      //正常起動確認用

      while(1){
            output_b(i2c_read());                         //受信データをPBに出力
      }
}

プリプロセッサ部分で重要なのが以下の部分です。

#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

ここでは、スレーブとして動き、自分のアドレスは0x00ですという指定をしています。

メイン関数では初期化をした後、

      while(1){
            output_b(i2c_read());                         //受信データをPBに出力
      }

でマスターから送られてくるデータを、ポートBに出力させる処理を繰り返しています。i2c_read( )関数は、マスターが指定するアドレスが自分のアドレスと一致して、データが送られてくるまでひたすら待っています。なので、その間他の仕事は何も出来ません。今回の例では、マスター側でi2c_led( )関数を1回実行した中で、スレーブ側のi2c_read( )関数は2回実行されることになります。

 

【スレーブ側割り込み有り】 ここからダウンロードできます。i2cslv12.c

//////////////////////////////////////////////////
// I2Cスレーブテストプログラム                                  //
// PIC16F873                                RIKIYA 2002.11.29 //
//                                                           i2cslv12.C //
// ID = 0x02                                                           //
// 1:3通信スレーブ側プログラム                                //
// マスター側から受信したデータをPBに出力する         //
// 割り込みあり                                                      //
/////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                         // clock 10MHz
#use fast_io(B)                                             // 固定入力モード
#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x02,FAST,FORCE_HW)

//SSP割り込み処理関数////////////////////////////////
#INT_SSP
void ssp_interupt (){
      output_b(i2c_read());                              //受信データをPBに出力
}

//メイン関数/////////////////////////////////////////
void main (){

      set_tris_b(0xf0);                                   //RB 7-4:IN 3-0:OUT
      set_tris_c(0x1f);                                   //RC 7-5:OUT 4-0:IN
      output_float(PIN_C3);                           //I2C pin float
      output_float(PIN_C4);                           //I2C pin float
      output_b(0x01);                                  //正常起動確認

      enable_interrupts(GLOBAL);                 //全ての割り込みを許可
      enable_interrupts(INT_SSP);                 //SSP割り込みを許可

      while (1) {}                                        //無限ループ
}

プリプロセッサ部分で重要なのが以下の部分です。

#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x02,FAST,FORCE_HW)

ここでは、スレーブとして動き、自分のアドレスは0x02ですという指定をしています。

このプログラムでは、i2c_read( )関数の実行を割り込み関数ssp_interupt( )内で行っています。メイン関数側では所定の初期化処理にあわせて、割り込み許可を行うための処理を追加しています。そして無限ループに入って割り込みが発生するのを待っています。実際に利用する場合には、この無限ループ部分に好きな処理プログラムを記述することによって、マスター側からデータが送信されてくるのをひたすら待つのではなく、普段は別の仕事をさせておくことが出来ます。

I2Cのスレーブ側は、いつどんなタイミングでマスター側から通信要求がくるのかわかりません。なので、このように割り込み処理によって受信できるプログラムにしておいたほうが有利です。

 

【プログラムの実行】

先ず、3個目のスレーブ用のプログラムを準備しましょう。上記i2cslv11.cのプログラムをコピーして、i2cslv13.cという名前のプログラムを準備します。i2cslv13.cでは、#use i2c(  )プリプロセッサ内でアドレス指定の部分を、ADDRESS=0x04に書き換えれば準備OKです。

左のPICから順に①、②、③、④と数えたとき、それぞれのPICに以下のプログラムをコンパイルしてダウンロードさせます。

①:i2cslv11.c (i2cslv11.hex)

②:i2cslv12.c (i2cslv12.hex)

③:i2cslv13.c (i2cslv13.hex)

④:i2cmas1.c (i2cmas1.hex)

PICを実装してから電源を入れると、各LEDが通信によって以下のように点灯します。点灯のパターンは0.25秒ごとに切り替わります。(:点灯 ○:消灯)

No 動 作
0 ●● 起動後1秒間の正常起動確認用の表示。
1 ●● ○○ ○○ ●● 0x00のスレーブに0xffのデータを渡します。
2 ○○ ○○ ○○ ○○ 0x00のスレーブに0x00のデータを渡します。
3 ○○ ●● ○○ ●● 0x02のスレーブに0xffのデータを渡します。
4 ○○ ○○ ○○ ○○ 0x02のスレーブに0x00のデータを渡します。
5 ○○ ○○ ●● ●● 0x04のスレーブに0xffのデータを渡します。
6 ○○ ○○ ○○ ○○ 0x04のスレーブに0x00のデータを渡します。No.1

【先頭に戻る】


■2.マスター受信 ← スレーブ送信 (スレーブは割り込み無し/有り)

 

マスターがスレーブから1バイトのデータを受け取るプログラムです。

 

【マスター側】 ここからダウンロードできます。i2cmas2.c

//////////////////////////////////////////////////
// I2Cマスターテストプログラム                                   //
// PIC16F873                                RIKIYA 2002.11.29 //
//                                                           i2cmas2.C //
// 1:3通信用マスター側プログラム                              //
// 各スレーブから順次データを受信しPBに表示する      //
//////////////////////////////////////////////////

#include <16f873.h>

  ”プリプロセッサ部分からメイン関数まで、i2cmas1.cと全く同じです。”

//各スレーブ制御/////////////////////////////////////
//スレーブのアドレスを引数addで渡す
void i2c_led(int add){

      i2c_start();                                       //スタートコンディション
      i2c_write(add | 0x01);                         //アドレスaddから受信
      output_b(i2c_read(0));                        //受信データを自分のPBに出力
      i2c_stop();                                       //ストップコンディション

      delay_ms(250);                                 //250mS待機
      output_b(0x00);                                //自分のPBに0x00を出力
      delay_ms(250);                                 //250mS待機
}

i2c_led( )関数以外の部分は、前項でご紹介したi2cmas1.cのプログラムと全く一緒です。今回のプログラムではi2c_led( )関数内の処理だけを変更しています。

i2c_led( )関数内では、スタートコンディション→アドレス指定→データ受信(ポートBへの出力)→ストップコンディションという一連の動きをしています。250mS待機した後、勝手にポートBへの出力をクリアして、再び250mS待機してから関数を抜けます。

このとき重要なのが、最初のアドレス指定部分で i2c_write(add | 0x01); としていることです。 強制的に0ビット目を1にして、マスター側が受信動作であるとスレーブ側に教えています。i2c_write(add + 1);としても一緒です。

もうひとつ重要なのが、output_b(i2c_read(0));です。i2c_read( )関数の引数をにしてあげないと、プログラム動作はフリーズしてしまいます。

 

【スレーブ側割り込み無し】 ここからダウンロードできます。i2cslv21.c

//////////////////////////////////////////////////
// I2Cスレーブテストプログラム                                  //
// PIC16F873                                RIKIYA 2002.12.07 //
//                                                           i2cslv21.C //
// ID = 0x00                                                            //
// 1:3通信スレーブ側プログラム                                 //
// マスターに対してデータを送信する                          //
// 割り込みなし                                                       //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                     // clock 10MHz
#use fast_io(B)                                         // 固定入力モード
#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

//メイン関数/////////////////////////////////////////
void main (){
int data = 0;
      set_tris_b(0xf0);                                 //RB 7-4:IN 3-0:OUT
      set_tris_c(0x1f);                                 //RC 7-5:OUT 4-0:IN
      output_float(PIN_C3);                         //I2C pin float
      output_float(PIN_C4);                         //I2C pin float
      output_b(0x01);                                 //正常起動確認

// ハードウエア配線から出力データを作る
      data = (input_b() >> 4) & 0x0f;

      while (1){
            i2c_write(data + 1);                     //マスターにdata+1を送信
            output_b(0x0f);                          //自分のPBに0x0fを出力
            delay_ms(250);                          //250mS待機
            output_b(0x00);                         //自分のPBに0x00を出力
            delay_ms(250);                          //250mS待機
      }
}

プリプロセッサ部分で重要なのが以下の部分です。

#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

ここでは、スレーブとして動き、自分のアドレスは0x00ですという指定をしています。

メイン関数では初期化をした後、data = (input_b() >> 4) & 0x07; で、ハードウエアから変数dataに格納するためのデータを作っています。テストボードの配線をもう一度確認して頂きたいのですが、RB4,5,6の各ビットには、実装ソケットの位置を検出させるための配線パターンを用意していました。今回はその3ビット分を取り込み、下位に4ビットシフトさせて、上位5ビット分にマスクをかけて、0,1,2,3のソケット番号を取り出す処理をしています。

次のwhile(1){  }のループで、i2c_write( )関数によるデータの送信→自分のポートBに0x0fの出力→250mSの待機→自分のポートBに0x00の出力→250mSの待機 の一連の処理を繰り返しています。

i2c_write( )関数の引数(送信データ)としてdata + 1としているのは、dataだけだと0番目のソケット番号の場合に送信データが0になってしまい、正常にデータ送信ができたかどうかが判別しづらいためです。

ここで注意ですが、PIC16F873などのようにSSPモジュールを実装しているPICの場合、i2c_write( )関数の実行は通信相手のタイミングとは無関係に行われるようです。つまり、送信バッファへのデータ書き込みは好きなタイミングで行われ、相手側からの読み出し処理が行われる前に、次の処理に進んでしまうようです。実際、上のプログラムを実行すると、マスター側の読み込みのタイミングとは無関係に、指定の待機時間の間隔でLEDの点灯が繰り返されます。

 

【スレーブ側割り込み有り】 ここからダウンロードできます。i2cslv22.c

//////////////////////////////////////////////////
// I2Cスレーブテストプログラム                                  //
// PIC16F873                                RIKIYA 2002.12.07 //
//                                                           i2cslv22.C //
// ID = 0x02                                                            //
// 1:3通信スレーブ側プログラム                                 //
// マスターに対してデータを送信する                          //
// 割り込みあり                                                       //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                         // clock 10MHz
#use fast_io(B)                                             // 固定入力モード
#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x02,FAST,FORCE_HW)

int DATA = 0;                                              // 送信データの宣言

//SSP割り込み処理関数////////////////////////////////
#INT_SSP
void ssp_interupt (){

      i2c_write(DATA + 1);                             //マスターにDATA+1を送信
      output_b(0x0f);                                    //自分のPBに0x0fを出力
      delay_ms(250);                                    //250mS待機
      output_b(0x00);                                   //自分のPBに0x00を出力
}

//メイン関数/////////////////////////////////////////
void main (){

      set_tris_b(0xf0);                                  //RB 7-4:IN 3-0:OUT 
      set_tris_c(0x1f);                                  //RC 7-5:OUT 4-0:IN
      output_float(PIN_C3);                          //I2C pin float
      output_float(PIN_C4);                          //I2C pin float
      output_b(0x01);                                  //正常起動確認

// ハードウエア配線から出力データを作る
      DATA = (input_b() >> 4) & 0x07;

      enable_interrupts(GLOBAL);                //全ての割り込みを許可
      enable_interrupts(INT_SSP);                //SSP割り込みを許可

      while (1) {}                                       //無限ループ
}

プリプロセッサ部分で重要なのが以下の部分です。

#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x02,FAST,FORCE_HW)

ここでは、スレーブとして動き、自分のアドレスは0x02ですという指定をしています。

今度は、i2c_write( )関数に関する部分を、割り込み処理で行っています。こうすると、マスター側からアドレス指定されてデータ送信の要求が来るまでは、i2c_write( )関数は実行されません。従って、マスター側のタイミングに同期した動作となります。もちろん割り込み処理なので、普段は別の仕事に専念できます。

I2Cのスレーブ側は、いつどんなタイミングでマスター側から通信要求がくるのかわかりません。なので、このように割り込み処理によって受信できるプログラムにしておいたほうが有利です。

 

【プログラムの実行】

先ず、3個目のスレーブ用のプログラムを準備しましょう。上記i2cslv22.cのプログラムをコピーして、i2cslv23.cという名前のプログラムを準備します。i2cslv23.cでは、#use i2c(  )プリプロセッサ内でアドレス指定の部分を、ADDRESS=0x04に書き換えれば準備OKです。

左のPICから順に①、②、③、④と数えたとき、それぞれのPICに以下のプログラムをコンパイルしてダウンロードさせます。

①:i2cslv21.c (i2cslv21.hex)

②:i2cslv22.c (i2cslv22.hex)

③:i2cslv23.c (i2cslv23.hex)

④:i2cmas2.c (i2cmas2.hex)

PICを実装してから電源を入れると、各LEDが通信によって以下のように点灯します。点灯のパターンは0.25秒ごとに切り替わります。(:点灯 ○:消灯)

No 動 作
0 ○○ ●● 起動後1秒間の正常起動確認用の表示。
1 ○○ ○○ ○○ 0x00のスレーブから0x01のデータを受信します。
2 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。
3 ○○ ●● ○○ 0x02のスレーブから0x02のデータを受信します。
4 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。
5 ○○ ○○ ●● ●● 0x04のスレーブから0x03のデータを受信します。
6 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。

上の例では、割り込み処理を使っていない①のPICが勝手な周期でLEDの点滅を繰り返すため、動作的に分かりづらくなっています。①のPICにADDRESS=0x00とした割り込み版のプログラムをダウンロードさせると、以下のように動作して、分かり易くなりますので試してみてください。

No 動 作
0 ●● 起動後1秒間の正常起動確認用の表示。
1 ●● ○○ ○○ 0x00のスレーブから0x01のデータを受信します。
2 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。
3 ○○ ●● ○○ 0x02のスレーブから0x02のデータを受信します。
4 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。
5 ○○ ○○ ●● ●● 0x04のスレーブから0x03のデータを受信します。
6 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。

【先頭に戻る】


■3.マスター送受信←→スレーブ送受信 (スレーブは割り込み有り)

 

マスターとスレーブ間で、お互いにデータの受け渡しを行うためのプログラムです。

 

【マスター側】 ここからダウンロードできます。i2cmas3.c

//////////////////////////////////////////////////
// I2Cマスターテストプログラム                                   //
// PIC16F873                                RIKIYA 2002.12.08 //
//                                                           i2cmas3.C //
// 1:3通信用マスター側プログラム                               //
// 各スレーブと順次データを送受信し                          //
// 受信データをPBに表示する。                                  //
//////////////////////////////////////////////////

#include <16f873.h>

  ”プリプロセッサ部分からメイン関数まで、i2cmas1.cと全く同じです。”


//各スレーブ制御/////////////////////////////////////
//スレーブのアドレスを引数addで渡す
void i2c_led(int add){

      i2c_start();                              //スタートコンディション
      i2c_write(add);                         //アドレスaddに送信
      i2c_write((add/2)+1);                //(add/2)+1を送信
      i2c_start();                             //再スタートコンディション
      i2c_write(add | 0x01);               //アドレスaddから受信
      output_b(i2c_read(0));              //受信データを自分のPBに出力
      i2c_stop();                             //ストップコンディション

      delay_ms(250);                       //250mS待機
      output_b(0x00);                      //自分のPBに0x00を出力
      delay_ms(250);                       //250mS待機
}

i2c_led( )関数以外の部分は、前項でご紹介したi2cmas1.cのプログラムと全く一緒です。今回のプログラムではi2c_led( )関数内の処理だけを変更しています。

i2c_led( )関数内では、先ずスレーブ側に対してデータ送信の処理を行い、続いてデータ受信の処理を行っています。送信動作から受信動作に切り替える時、i2c_stop( )関数を実行せずにいきなりi2c_start( )関数を実行しています。これは再スタートコンディションという処理で、送受信モードやスレーブアドレスを変更する場合に使います。

その他の処理は、前項までのプログラム例の流用です。

 

【スレーブ側割り込み有り】 ここからダウンロードできます。i2cslv31.c

//////////////////////////////////////////////////
// I2Cスレーブテストプログラム                                  //
// PIC16F873                                RIKIYA 2002.12.08 //
//                                                           i2cslv31.C //
// ID = 0x00                                                            //
// 1:3通信スレーブ側プログラム                                 //
// マスター側とデータを送受信し                                //
// 受信データをPBに表示する。                                 //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                         // clock 10MHz
#use fast_io(B)                                             // 固定入力モード
#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

int DATA = 0;                                              // 送信データの宣言

//SSP割り込み処理関数////////////////////////////////
#INT_SSP
void ssp_interupt (){

      if(i2c_poll()){                                         //データ受信判定
            output_b(i2c_read());                        //マスターからの受信
      }
      else{
            i2c_write(DATA + 1);                       //マスターにDATAを送信
            delay_ms(250);                               //250mS待機
            output_b(0x00);                              //自分のPBに0x00を出力
      }
}

//メイン関数/////////////////////////////////////////
void main (){

      set_tris_b(0xf0);                                  //RB 7-4:IN 3-0:OUT
      set_tris_c(0x1f);                                  //RC 7-5:OUT 4-0:IN
      output_float(PIN_C3);                          //I2C pin float
      output_float(PIN_C4);                          //I2C pin float
      output_b(0x01);                                  //正常起動確認

// ハードウエア配線から出力データを作る
      DATA = (input_b() >> 4) & 0x07;

      enable_interrupts(GLOBAL);                //全ての割り込みを許可
      enable_interrupts(INT_SSP);                //SSP割り込みを許可

      while (1) {}                                       //無限ループ
}

プリプロセッサ部分で重要なのが以下の部分です。

#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

ここでは、スレーブとして動き、自分のアドレスは0x00ですという指定をしています。

今回のプログラムでネックとなるのが、I2Cの送受信で発生するひとつの割り込み処理内で、今は受信で割り込みが発生したのか、送信で割り込みが発生したのか、の場合に応じた処理をしなければならない点です。つまり、送信か受信かを判別させるための処理が必要になります

そこで登場するのが、i2c_poll( )関数です。この関数は、受信バッファにデータが入っているときを返します。スレーブ側ではデータを受信したときか、データ送信の要求があったときに割り込みが発生しますので、割り込み処理の最初に受信バッファを調べて、データが入っていれば受信、入っていなければ送信要求という判断を行うことができます

なので、割り込み関数内でi2c_poll( )関数の戻り値を使った、以下のようなif文で受信と送信の両方に対応することができます。

      if(i2c_poll()){                                         //データ受信判定
            output_b(i2c_read());                        //マスターからの受信
      }
      else{
            i2c_write(DATA + 1);                       //マスターにDATAを送信
            delay_ms(250);                               //250mS待機
            output_b(0x00);                              //自分のPBに0x00を出力
      }

 

【プログラムの実行】

先ず、3個分のスレーブ用のプログラムを準備しましょう。上記i2cslv31.cのプログラムをコピーして、i2cslv32.ci2cslv33.cという名前のプログラムを準備します。そしてそれぞれの#use i2c(  )プリプロセッサ内でアドレス指定の部分を、ADDRESS=0x020x04に書き換えれば準備OKです。

左のPICから順に①、②、③、④と数えたとき、それぞれのPICに以下のプログラムをコンパイルしてダウンロードさせます。

①:i2cslv31.c (i2cslv31.hex)

②:i2cslv32.c (i2cslv32.hex)

③:i2cslv33.c (i2cslv33.hex)

④:i2cmas3.c (i2cmas3.hex)

PICを実装してから電源を入れると、各LEDが通信によって以下のように点灯します。点灯のパターンは0.25秒ごとに切り替わります。(:点灯 ○:消灯)

No 動 作
0 ●● 起動後1秒間の正常起動確認用の表示。
1 ○○ ○○ 0x00のスレーブと0x01のデータを送受信します。
2 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。
3 ○○ ○○ 0x02のスレーブと0x02のデータを送受信します。
4 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。
5 ○○ ○○ ●● ●● 0x04のスレーブと0x03のデータを送受信します。
6 ○○ ○○ ○○ ○○ それぞれ所定の時間で表示をクリアします。

マスター、スレーブのそれぞれのPICは、通信相手からもらったデータをLED表示させます。

【先頭に戻る】


■4.スレーブアドレスをハードウエアから検出して自動設定する方法

 

さて、今まで実験してきて、作業が面倒で何とかならんか?と思うことがひとつありませんか?

それは、各スレーブのプログラム管理とコンパイル、ダウンロード作業です。今までご紹介してきたスレーブ側のプログラムは、基本的には動作がみんな同じでした。違うのはスレーブアドレスの指定だけです。

実験ではスレーブが3個だったので、まだ許せますが、スレーブが数十個といったシステムを組みたいといった場合、アドレスだけを書き換えたプログラムを全て管理するのは非常に面倒です。更に話は「面倒」だけでは済まず、間違えによる動作不良などにもつながりかねません。

同じ動作をする全てのスレーブは、ひとつのプログラムにしたいなあ、というのがこれからご紹介する方法です。つまり、ハードウエア的にPICのソケット番号を検出して、それをもとにプログラム上でスレーブアドレスを自動的に設定してしまえばアドレスだけが違うプログラムを、いくつも用意する必要がないということです。

PICの内部では、スレーブアドレスを保持するためにSSPADDというレジスタがあります。通常は#use i2cのプリプロセッサで指定されたアドレスが書き込まれますが、このレジスタの情報を後から好きなアドレスデータに書き直すということをします。

はじめにもご紹介していますが、今回使っているテストボード上には、PICのソケットに応じて以下のような配線をしています。今回は、この配線を利用してスレーブアドレスのデータを生成します。

  RB6 RB5 RB4
ソケット① 0 0 0
ソケット② 0 0 1
ソケット③ 0 1 0
ソケット④ 0 1 1

0:GND、1:+5V ソケット番号は左から①、②、③、④の順。

以下にプログラムは、前項でご紹介したi2cslv31.cを基本として、アドレス自動設定部分だけを追加したものです。追記部分をで表示しています。

 

【スレーブ側アドレス自動設定】 ここからダウンロードできます。i2cslv34.c

//////////////////////////////////////////////////
// I2Cスレーブテストプログラム                                   //
// PIC16F873                                 RIKIYA 2002.12.09 //
//                                                           i2cslv34.C //
// ID = 自動設定                                                     //
// 1:3通信スレーブ側プログラム                                 //
// マスター側とデータを送受信し                                //
// 受信データをPBに表示する。                                 //
// スレーブアドレスは、PB4〜7ビット                           //
// から読み込むデータで自動設定する                        //
//////////////////////////////////////////////////

#include <16f873.h>
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#use delay(clock = 10000000)                        // clock 10MHz
#use fast_io(B)                                            // 固定入力モード
#use i2c(SLAVE,SDA=PIN_C4,SCL=PIN_C3,ADDRESS=0x00,FAST,FORCE_HW)

void add_set(int add);                                   // プロトタイプ宣言

int DATA = 0;                                             // 送信データの宣言

//SSP割り込み処理関数////////////////////////////////
#INT_SSP
void ssp_interupt (){

      if(i2c_poll()){                                         //データ受信判定
            output_b(i2c_read());                        //マスターからの受信
      }
      else{
            i2c_write(DATA + 1);                       //マスターにDATAを送信
            delay_ms(250);                               //250mS待機
            output_b(0x00);                              //自分のPBに0x00を出力
      }
}

//メイン関数/////////////////////////////////////////
void main (){

      set_tris_b(0xf0);                                  //RB 7-4:IN 3-0:OUT
      set_tris_c(0x1f);                                  //RC 7-5:OUT 4-0:IN
      output_float(PIN_C3);                          //I2C pin input
      output_float(PIN_C4);                          //I2C pin input
      output_b(0x01);                                  //正常起動確認

// ハードウエア配線から出力データを作る
      DATA = (input_b() >> 4) & 0x07;
// スレーブアドレス設定
      add_set(DATA * 2);


      enable_interrupts(GLOBAL);               //全ての割り込みを許可
      enable_interrupts(INT_SSP);               //SSP割り込みを許可

      while (1) {} //無限ループ
}

//アドレス自動設定////////////////////////////////////
void add_set(int add){
      int *sspadd = (int *)147; // 変数*sspaddにSSPADDのアドレス147をセット
      *sspadd = add;             // SSPADDレジスタにアドレスをセット
}

プリプロセッサ#use i2c( )ADDRESSの指定は0x00としていますが、あとから書き換えるので何でも結構です。

add_set( )関数が、スレーブアドレスをSPPADDレジスタに直接書き込みに行く処理を行います。

ところで、ソケットごとの配線からスレーブアドレスのデータを生成する処理として、上記のプログラムでは以下のような処理をしています。

      DATA = (input_b() >> 4) & 0x07;
      add_set(DATA * 2);

この処理を行うと、DATA変数にはソケットによって0x00,0x01,0x02のデータが格納されます。実際に使えるスレーブアドレスは、0x00,0x02,0x04...と、ひとつ飛びの数値になるため、add_set( )関数に渡す引数としてDATA * 2という計算を用いています。

これは、マスター側に送信するためのデータをアドレス生成に流用したためで、ちょっとスマートではありません。例えば以下のようにしてあげても、同じ効果を得ることができます。

 int add;

      add = (input_b() >> 3) & 0x0e;

      add_set(add);

もしくはハードウエアの配線を、RB4,5,6ではなくRB1,2,3にしてしまえば、

      add_set(input_b() & 0x0e);

でもいけるのでは?と思います。(試していません...)

 

ところで、#use i2cのプリプロセッサは、プログラム中のどこに記述しても有効です。なので、PICのポートの初期化が済んで、ハードウエアの配線からDATA変数の値が確定した後でも実行できます。そうすると、わざわざadd_set( )関数など作らず、直接#use i2cでアドレスを指定してしまえば良いとも思ったのですが、#use i2cの中で ADDRESS = DATA などと変数を用いた指定はコンパイラでエラーとなりダメでした。

しかし、#use i2cのプリプロセッサがソース上のどこに配置されても有効というのは、使い方によっては結構柔軟な処理をさせることが出来そうな気もします。

 

【プログラムの実行】

さて、3個のスレーブに上記の同じプログラムをダウンロードさせると、■3.マスター送受信←→スレーブ送受信 (スレーブは割り込み有り)と同じ動作をしてくれます。

今回は各ソケットごとのアドレス配線を固定としましたが、ディップスイッチなどに置き換えれば柔軟なネットワーク構築の助けとなります。是非試してみてください。

【先頭に戻る】


今回は基本的な通信のパターンについてなるべく多くの事例をご紹介したかったため、またダラダラとしたページになってしまいました。実は現時点では、力弥もここにご紹介した程度の実験しかやっていません。なので、I2C通信の実行結果については、まだクセなども掴みきっておらず霞の中といった感じです。

今後、PICは分散処理の要として利用していきたいと考えていますので、様々な機能とI2C通信を組み合わせた実験をしていきたいと思います。

【ソフトウエア編TOPに戻る】


変更履歴 2003.01.11 複数バイト連続送受信時の不安定動作についての告白を追記。


【表紙に戻る】