Arduino Uno とPIC12F1840 間でi2c通信によるサーボ制御の実験

2021.03.14


1 概要 

 Arduino Unoをマスターにしてi2cを経由してスレーブ側のサーボを制御するサンプルを作製した。目的は8pinの安価なPICでi2c制御可能なデバイスモ ジュールを作ることとpicのpwm制御の習得。
 i2c(アイスクエアシー)はSCLとSDAという2本のバスを通じて他のモジュールとシリアル通信をする仕様で概ね基板内のモジュールを 想定 している。しかし実際には1m程度離れても難なく通信できている。今回スレーブ側はPIC12F1840(秋月電子で110円/個)を使った。既存のi2c接続16チャンネ ル サーボ&PWM駆動キット950円には負けるが、これのノウハウがあればサーボに限らずいろいろ応用が効く。

マスターとスレーブ
 i2cはマスターが常に権限を持っている。マスターからスレーブにデータを送信する。あるいはマスターがスレーブに
なにか送信要求をするという流れになる。

マスター →  スレーブ 方向の通信
  (1)マスターのUNOからサーボモータのDuty値をPICへアドレスを指定して送信
   指定アドレスのスレーブ側のPICは受信したDuty値によるパルスをサーボへ出力

 (2)マスターが指定アドレスのスレーブに現在のDuty値の問い合わせ

スレーブ → マスター 方向の通信
  マスターの問い合わせ要求に対して現在サーボに出力中のDuty値を返信

UNO===PIC==PIC===PIC===PIC

i2cのアドレス
  図のようにi2cのバスにスレーブは複数個接続できる。種類が異なることも可能である。例えば温度センサーや
i2c-ROM 、液晶パネルなどが考えられる。そこで各スレーブにはアドレスが割り振られるのだが7bitのアドレス方式と10bitのアドレス方式がある。10bit ば扱ったことがないので省く。7bitの場合は マスターがスレーブにデータを書き込む(送信する)場合はLSBに0を付加して8bitにし て i2cバスに流し、反応したスレーブとやりとりする。上の図では左側のPICのスレーブアドレスが5bとなっている。
 マスターは5bのスレーブにデータを転送するのでアドレスは (1011011+0) = (1011 0110)=B6という値をバスに流す。逆にマスターがスレーブからデータを読み出す場合は、LSBに1を付加するので(1011011+1)= (100110111)=B7というア ドレスをi2cバスに流して反応したスレーブとその後のやり取りを行う。
 故にスレーブ側は自分のスレーブアドレスを設定するレジスタに最初からLSBの(R/W)の1bit分を空けて8bitでセットする。上の 例では左から5B →B6,  5C →B8 ,  5D →BA,  5E →BC である。 MasterがArduinoの場合はi2cのライブラリが、書込みと読込みで自動的にLSBに(R/W)を付加してくれるので上図のアドレス のままでよい。


2 ハードウェア

 Master: Arduino Uno
               i2cのSCLとSDA pinはデジタルピンの列の左端にある。
       Unoより古いDuemilianoveを使う場合はAnalog4がSDA,Analog5がSCL
    
               なおSCL,SDAの線は10〜22kΩぐらいの抵抗で+5vプルアップするのだがArduinoを接続する場合はプルアップの必要なし。

 Slave:   PIC12F1840    ピン配置は 1:VDD, 2:RA5 PWM出力, 5:RA2 SDA, 6:RA1 SCL  8:VSS
  fig-4 スレーブ側の回路 これを横に接続していく。

Slave側回路

実際の画像

実際の写真

 緑と白の線がSCLとSDA スレーブ側はピンソケットからジャンパー線で繋いでいるが
L型ピンヘッダとL型ピンソケットにすればスッキリする


3 プログラム

(1)マスター側(Aruduino)

 スレーブ側のサーボを動かすデモプログラム  ポイントとなるところを赤太字にした
//////////////////////////////////////
//    i2cで動くサーボモータの制御
//      2021.03.13
///////////////////////////////////////
//   Master:  Arduino Uno           Digital Pinの左端にSCLとSDAのピンあり
//           (Arduino Duemilianove  Analog  4: I2C SD  Analog  5: I2C SCL)
//  接続図
// +----------+
//  | Aruduino |==i2c===(Servo1)===(Servo2)===(Servo3)===(Servo4)
//  +----------+  Address  #5b        #5c        #5d        #5e
//
#include <stdio.h>
#include <Wire.h>  //i2cを使うのに必要なライブラリ

#define  SV1_ADR  0x5b //スレーブ1のアドレス
#define  SV2_ADR  0x5c //スレーブ2のアドレス
#define  SV3_ADR  0x5d //スレーブ3のアドレス
#define  SV4_ADR  0x5e //スレーブ4のアドレス

#define  LEFT   0x12 //Duty Value LEFT
#define  CENTER 0x2f //Duty Value Center
#define  RIGHT  0x4b //Duty Value Right   
//************************************
// servo データ書き込み
//   入力 sv_adr サーボモータアドレス
//        t_data  デューティー値
//************************************
void writeData(byte sv_adr,byte t_data){
    Wire.beginTransmission(sv_adr); //スレーブのアドレスを発信
    Wire.write(t_data);             //データ転送
    Wire.endTransmission();         //通信の終了
    delay(1);
 } 
////////////////////////
// Set up
////////////////////////
void setup() {
    delay(1000);
    Wire.begin(); //i2c初期化
    delay(1000);
}
///////////////////////
// main loop
///////////////////////
void loop(){
  writeData(SV1_ADR,RIGHT); //スレーブ1サーボを右90°
  delay(500);
  writeData(SV2_ADR,RIGHT); //スレーブ2サーボを右90°
  delay(500); 
  writeData(SV3_ADR,RIGHT); //スレーブ3サーボを右90°
  delay(500); 
  writeData(SV4_ADR,RIGHT); //スレーブ4サーボを右90°
  delay(500); 
  writeData(SV1_ADR,CENTER); //スレーブ1サーボを中央0°
  delay(500);
  writeData(SV2_ADR,CENTER); //スレーブ2サーボを中央0°
  delay(500); 
  writeData(SV3_ADR,CENTER); //スレーブ3サーボを中央0°
  delay(500); 
  writeData(SV4_ADR,CENTER); //スレーブ4サーボを中央0°
  delay(500); 
  writeData(SV1_ADR,LEFT); //スレーブ1サーボを左90°
  delay(500);
  writeData(SV2_ADR,LEFT); //スレーブ2サーボを左90°
  delay(500); 
  writeData(SV3_ADR,LEFT); //スレーブ3サーボを左90°
  delay(500); 
  writeData(SV4_ADR,LEFT); //スレーブ4サーボを左90°
  delay(500);
  writeData(SV1_ADR,CENTER); //スレーブ1サーボを中央0°
  writeData(SV2_ADR,CENTER);  //スレーブ2サーボを中央0°
  writeData(SV3_ADR,CENTER);  //スレーブ3サーボを中央0°
  writeData(SV4_ADR,CENTER); //スレーブ4サーボを中央0°
 
  以下略

}



(2) スレーブ1  PIC12F1840のプログラム
  スレーブアドレスは「#define SLVADR 0xB6     //SLAVE ADDRESS」の0xB6を変更できる。(前述のとおり上位7bitがアドレスなのでLSBは0にしてセット。)
/*
 * i2cマスターからduty値を受け取りサーボモータを制御するi2cスレーブ側のシステム
 * スレーブCPU PIC12F1840  サーボモータSG-90(秋月電子より購入)
 *
 CPU ピン配置
                 |--u--|   
             VDD=|1   8|=VSS
        PWM(RA5)=|2   7|=(RA0)
           (RA4)=|3   6|=(RA1)SCL
       MCLR(RA3)=|4   5|=(RA2)SDA
                 ~~~~~~
*/
#include<stdio.h>
#include <xc.h>
#include <pic12f1840.h>

//config1
#pragma config FOSC=INTOSC  //Oscillator Selection 内部クロック使用
#pragma config WDTE=OFF     //Watchdog Timer off
#pragma config PWRTE=ON     //Power up timer on(スイッチを入れた直後電源が安定するまで待つ)
#pragma config MCLRE=OFF    //MCLR PIN off (ハードウェアリセットのピンの用途を無しにしてdigital pinとして使えるようにする)
#pragma config CP=OFF       //CODE PROTECT OFF プログラムの読み出しのプロテクトoff
#pragma config CPD=OFF      //Data Protect OFF データ領域の読み出しのプロテクトoff
#pragma config BOREN=ON     // Brownout on (もし電源が不安定のとき一時停止する)
#pragma config CLKOUTEN=OFF // CLOCK 信号の外部出力をOFF
#pragma config IESO=OFF     // 2段階クロックoff(立ち上がりで即安定する内部クロックを使いその後外部クロックに切り替える設定)
#pragma config FCMEN=OFF    //Fale-safe Clock Monitor off(外部クロックが壊れたとき内部クロックに切り替える設定)
//config2
#pragma config WRT=OFF      //Write Protection プログラム領域の書き込み禁止 off
#pragma config PLLEN=OFF    //クロック逓倍off (onにすると発生したクロックが4倍になる)
#pragma config STVREN=ON    //スタック領域をオーバーしたらリセットする(offは何もしない)
#pragma config BORV=HI      // Brounout電圧設定 (HI:電源電圧がちょっとさがるとリセット、LO:うんと下がるとリセット)
#pragma config LVP=OFF      //プログラム書き込み電圧(ON;低電圧書き込み有効 マイコンの電圧で書き込める off:低電圧書き込み無効 PICKIT3のときoffに)

#define _XTAL_FREQ 2000000  //内部clock2MHz for delay macro

#define SLVADR 0xB6         //SLAVE ADDRESS
//#define SLVADR 0xB8         //SLAVE ADDRESS
//#define SLVADR 0xBA         //SLAVE ADDRESS
//#define SLVADR 0xBC         //SLAVE ADDRESS

#define PWMPRD 159          //PWM period for PR2
#define D_LEFT 0x4B         // Duty 2.5% Servo Left
#define D_CENTER 0x2F       // Duty 7.25% Servo Center
#define D_RIGHT    0x12        //Duty 12%  Servo Right

void dutyset(char); //Duty値出力処理
void i_read(void);  //i2c 受信要求処理
void i_write(void); //i2c 送信要求処理
//void __interrupt() i2cslave(void); //割込処理
/****************************
 *  割込処理         
*****************************/
void __interrupt() i2cslave(void){
    char x;
    x=SSP1STAT & 0x2C;
    if(x==0x08){
        i_read();   //受信要求
    }else if(x==0x0C){
        i_write();  //送信要求
    }
}
/*****************************
 *      受信要求処理       
 *****************************/
void i_read(void){
    char x; //wrk
    char d_val; //duty値
    x=SSP1BUF; //最初は読み捨てる
   
    do{
        SSP1CON1bits.CKP=1; //stretch enable
        PIR1bits.SSP1IF=0; //割込フラグクリア
        if(SSP1STATbits.BF==0){
            if(SSP1STATbits.P==1){
                break; //stop検出
            }
        }else{
            //受信有り データ受信処理
            d_val=SSP1BUF;
            if(D_LEFT < d_val ){
                d_val = D_LEFT; //D_LEFTの値を超えたので修正
            }
            if(D_RIGHT > d_val){
                d_val = D_RIGHT; //D_RIGHTの値を下回ったので修正
            }
            dutyset(d_val); //duty値出力
        }
    }while(1);
}
/*****************************
 *      送信要求処理
 *  duty値を取り出してmasterに送信する       
 *****************************/
void i_write(void){
    char d_val; //duty値
    char x;
    do{
    x=CCP1CON & 0x30; //<5:4>抽出
    x= x >> 4;
    d_val=CCPR1L <<2;
    d_val=d_val | x;
    SSP1BUF=d_val; //バッファにduty値セット
    SSP1CON1bits.CKP=1; //stretch解除
    PIR1bits.SSP1IF=0; //割込フラグクリア
    }while((PIR1bits.SSP1IF==1)&&(SSP1STATbits.P==0));
}

/****************************************
;  d_valの値をCCPR1L:CCP1CON<5:4> に転送
*****************************************/ 
void dutyset(char d_val){
    char x; //wrk
    CCP1CON=CCP1CON & 0xCF; //4bit目と5bit目クリア
    x=d_val & 0x03; //下2桁をxに抽出
    x=x<<4; //4ビット左シフト
    CCP1CON=CCP1CON | x; // CCP1CONの5bit,4bitにd_valの2bit 1bitをセット
    CCPR1L=d_val;
    CCPR1L=CCPR1L >>2; //CCPR1Lにd_valの7〜2bitをセット
 }
/**********************************
        main
 **********************************/
int main(void){
    char d_val; //duty値
 
    OSCCON=0x60; //内部Clock 2MHz
   
    INTCONbits.GIE=0;   //割り込み禁止
    INTCONbits.PEIE=0;  //割り込み禁止
   
    //PORTAをディジタルI/Oにする
    PORTA=0;
    LATA=0;
    ANSELA=0;
    TRISA=0x06; //SCL,SDAのRA1,RA2は入力 RA5はPWMの出力

    //ここからI2Cの設定  
    SSP1STAT=0x80; //SMP ON
    SSP1CON1=0x36; //SSP1EN=ON,I2C slave mode 7bit address
    SSP1ADD=SLVADR; //SLAVE ADDR SET
    SSP1CON2bits.SEN=1; //stretch enabled
    SSP1CON1bits.CKP=1; //stretch enabled
    PIR1bits.SSP1IF=0; //割り込みフラグクリア
    //ここまでI2Cの設定

   

    //Servo 初期化
    APFCONbits.CCP1SEL=1;    //1:RA5をPWM出力PINに設定
    //CCP1CONの下位4bitに1100をセットしてPWMを有効にする
    CCP1CON=CCP1CON & 0xF0;
    CCP1CON=CCP1CON | 0x0C;

    PR2=PWMPRD;             //pwmの周期20msを設定
    T2CON=T2CON | 0x03;     //T2CONのb1,b0を11(プリスケーラ64)

    // Duty値設定
    d_val=D_CENTER; //センターの値を設定
    dutyset(d_val);
    //PWMスタート
    T2CONbits.TMR2ON=1;

    // ここに割り込み許可をしてからメインループへ
    INTCON=0;
    INTCONbits.INTE=1;  //外部割込み許可
    INTCONbits.PEIE=1;  //周辺割込許可
    INTCONbits.GIE=1;   //周辺割込許可  
    PIE1bits.SSP1IE=1;  //i2c 割り込み 許可 Enables the MSSP interrupt
    //loop
    while(1){
    }
    return 0;
}