PIC12F1840  i2cの設定とPWMの設定 (XC8編)

2020.10.10
2021.02.18


1.概要

  i2cとPWMが使えて安価な8pinのPICをスレーブに使えば、ArduinoやRaspberryPi等のマイコンから制御可能なモジュールをいろ いろ作れると思いました。
 この条件にあうPICはPIC12F1840-I/P(秋月電子価格 税込110円)またはPIC12F1822-I/P(同 110円)があ ります。サーボモータやDCモータ等ちょっとした
小物をi2cでマスターに繋ぐ役割を果たすにはどちらでも差はないと思います。実験ではi2cのマスター側にも12F1840を使い、スレーブ側 にLCDと温度センサADT7840、12F1840を経由したサーボモータの3つを接続しました。

 2.仕様概要
 3.PIC12F1840のコンフィギュレーション等の設定
 4.PIC12F1840 i2c Master の設定
 5.PIC12F1840 i2c Master側の送受信プログラム
  6.i2c接続のLCDに表示(マスター側 プログラム)
  7.温度センサーADT7410との通信(マスター側 プログラム)
  8.サーボモータのDUTY値の送受信(マスター側 プログラム)
  9.マスター側プログラム全体
  10.PIC12F1840 スレーブ側のi2cプログラム
  11.PWM制御の設定
  12 スレーブ側プログラム全体

2.仕様概要(温度表示以外なんの役にもたたない仕様です)

 ハードウェア
    i2cマスター PIC12F1840-I/P
    i2cスレーブ LCD  AQM1602A (16文字✕2行),温度センサADT7410,PIC12F1840-I/P+サーボモータsg-90
  動作
  マスター側
   温度センサから温度を読み取りLCDの2行目に表示する
      温度は摂氏とし、10進数で表示。
   温度の1桁目が偶数ならスレーブ側のサーボモータを右いっぱいに回すPWM値を送信、
   温度の2桁目が偶数ならスレーブ側のサーボモータを右いっぱいに回すPWM値を送信、
   PWMの送信値はLCD 1行目左端に16進数で表示する。また約4秒毎にスレーブ側のPWM値を受信してLCDに表示する

  スレーブ側PIC12F1840
      電源投入時はサーボモータの角度を中央にする。
      受信割り込みによりマスターから受信したPWM値をサーボモータに出力する
   マスターからの送信要求に対して現在のサーボモータのPWM値を送信する


回路図



写真
実際のハード 単3電池2本はサーボ用、CPUとADT7410、LCDは左上の方から別途給電
LCDの1行目TX:0Fはマスターからスレーブに送ったDUTY値、RXは逆に受信したDUTY値
2行目の4桁のs数字はADT7410モジュールから受け取った数値、右側はそれを変換した温度

3.PIC12F1840のコンフィギュレーション等の設定

 クロックは内部発振器を使う。

#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 D_LEFT 0x4E         // Duty 2.5% Servo Left
#define D_CENTER 0x2F       // Duty 7.25% Servo Center
#define D_RIGHT    0x0F        //Duty 12%  Servo Right

#define     LCDADR      0x7C    //i2c LCD AQM1602XAのアドレス+R/wビット0
#define     ADTADR_W    0x90    //ADT7410スレーブアドレス書き込みモード
#define     ADTADR_R    0x91    //ADT7410スレーブアドレス読み込みモード
#define     SERVOADR_W  0xB6    //サーボモータモジュール書き込みアドレス
#define     SERVOADR_R  0xB7    //サーボモータモジュール読み込みアドレス

OSCCONレジスタに値を設定してCPUのクロックを2MHzに設定する。(スレーブ側ではサーボモータSG-90をPWM制御するの に、これ以上高い周波数は使えない。)

    OSCCON=0x60; //内部Clock 2MHz


4.PIC12F1840 i2c Master の設定

(1)ポートの設定

 マスター側PICもスレーブ側PICもi2cのポートとなるSCL(RA1),SDA(RA2)はデジタルピンの入力に設定す る

    //PORTAをディジタルI/Oにする
    PORTA=0;
    LATA=0;
    ANSELA=0;
    TRISA=0x06; //SCL,SDAのRA1,RA2は入力 RA5はPWMの出力

(2)マスターの設定

   SSP1STATSMPビットをONにすることでI2Cマスターモードになる。

(3)I2Cポート(SCL,SDA)enableの設定

 SSP1CON1レジスタのSSP1EN をONにする

(4)I2Cマスターモードのクロックの設定など

 I2Cのクロックはマスター側のSSP1CON1<3:0>とSSP1ADDの値で決まる。
 
 まずSSP1CON1レジスタの<3:0>にb'1000'をセットすると
 CPUのクロックFosc[Hz]とi2cのバスクロックSCL[Hz]の関係式は次のようになる
        Fosc/(4*(SSP1ADD+1))=SCL[Hz]

  上の式からSSP1ADD=Fosc/(4*SCL) - 1
 
  Fosc=2MHz,SCL=100KHz=0.1MHz を代入するとSSP1ADD=4をセット すればよい。



 SSP1CON2をクリアする

  (2)から(4)のコード

    //ここからI2Cの設定  
    SSP1STAT=0x80; //SMP ON
    SSP1CON1=0x28; //SSP1EN=ON,I2C master mode
    SSP1CON2=0;
    SSP1ADD=0x04; //2MHz/((04H+1)*4)=100kHz  100Kbpsの設定
    PIR1bits.SSP1IF=0; //割り込みフラグクリア
    //ここまでI2Cの設定

i2c関連のレジスタは下記のとおり。データシートの240pにも記載されている。

INTCON
PIE1
PIE2
PIR1
PIR2
SSP1ADD
SSP1BUF
SSP1CON1
SSP1CON2
SSP1CON3
SSP1MSK
SSP1STAT
TRISA


5.PIC12F1840 i2c Master側の送受信プログラム

 Fig2はi2c通信の7bitアドレス方式のデータの流れである。
 上段がマスターからスレーブへのデータ書き込みである。
マスターが開始条件を送信したのち7bitのスレーブアドレスにWrite=0のフラグを付加して8bitにして送信する。
バス上のアドレスが一致したスレーブからACKが返信された後マスター側は8bit毎にデータを送る。
送信データが尽きたら終了条件を送信する。

 下段はマスターがスレーブからデータを受信する場合である。
マスターが開始条件を送信したのち7bitのスレーブアドレスにRead=1のフラグを付加して8bitにして送信する。
バス上のアドレスが一致したスレーブからAckが返信される。その後、スレーブが8ビットのデータをが送られるのでマスターは受信の都度 ACKを返す。

 i2cモードで動作している場合はデータの送受信はSSP1BUFを介して自動で行われる。送受信の完了はそれぞれ関連するフ ラグをチェックすることで判定できる。

  i2c データフォーム

i2c master


/*=========================================================================
;   I2C  制御モジュール群
;=========================================================================*/
/***********************************************************************
; 開始条件送信
; (1)通信状態ではないときSDA,SCLともにHIGH状態(22KΩの抵抗でこの2本をプルアップ
; (2) SCLがHのままSDAがH --> L に変化したときが開始状態
************************************************************************/
void i2cstart(void){
    do{
        SSP1CON1bits.WCOL=0;    //SSPCONの衝突フラグクリア
        SSP1CON2bits.SEN=1;     //開始条件送信
    }while(SSP1CON1bits.WCOL==1);   //衝突有り? 有りならループ継続

    while(SSP1CON2bits.SEN==1){
        //SEN==0でスタート処理終了
    }
}
/**********************************************************************
; 受信処理の再開始条件送信
**********************************************************************/
void i2crestart(void){
    PIR1bits.SSP1IF=0;      //割込みフラグクリア
    SSP1CON2bits.RSEN=1;    //再開始条件送信
    while(SSP1CON2bits.RSEN==1){
        //RSEN==0で再開始条件出力処理終了
    }
}
/**********************************************************************
   データ送信   
   入力 W: 送信データ
**********************************************************************/
void i2csend(unsigned char t_data){
    PIR1bits.SSP1IF=0;  //割込フラグクリア
    SSP1BUF=t_data;     //送信データセット
    while(PIR1bits.SSP1IF==0){
        //SSP1IF==1 送信完了になるまで待つ
    }
    while(SSP1CON2bits.ACKSTAT==1){
        //ACK受信まで待つ
    }
}
/*********************************************************************
;  1バイトデータ受信      出力 受信データ
*********************************************************************/
unsigned char i2crcv(void){
    PIR1bits.SSP1IF=0;  //割込フラグクリア
    SSP1CON2bits.RCEN=1;    //受信許可
    while(SSP1CON2bits.RCEN==1){
        //RCEN==0 受信完了まで待つ
    }
    SSP1CON2bits.ACKDT=0;   //ACKをセット
    SSP1CON2bits.ACKEN=1;   //ACK送信
    while(SSP1CON2bits.ACKEN==1){
        //ACKEN==0 ACK送信完了まで待つ
    }
    return SSP1BUF; //データ出力
}
/**********************************************************************
;   最終バイト受信   (最終バイトを受信して終了の時はCPUからNACKを送信)
;    出力  受信データ
;*********************************************************************/
unsigned char i2crend(void){
    PIR1bits.SSP1IF=0;  //割込フラグクリア
    SSP1CON2bits.RCEN=1;    //受信許可
    while(SSP1CON2bits.RCEN==1){
        //RCEN==0 受信完了まで待つ
    }
    SSP1CON2bits.ACKDT=1;   //NACKをセット
    SSP1CON2bits.ACKEN=1;   //NACK送信
    while(SSP1CON2bits.ACKEN==1){
        //ACKEN==0 NACK送信完了まで待つ
    }
    return SSP1BUF; //データ出力
}
/**********************************************************************
; 終了条件送信
;*********************************************************************/
void i2cend(){
    PIR1bits.SSP1IF=0;  //割り込みフラグクリア
    SSP1CON2bits.PEN=1; //ストップ出力
    while(SSP1CON2bits.PEN==1){
        //PEN==0 出力完了まで待つ
    }
    PIR1bits.SSP1IF=0;  //割り込みフラグクリア
}


6.i2c接続のLCDに表示(マスター側 プログラム)

 LCDに文字が表示できるとデバッグがとても捗る。
秋月電子で購入したAQM1602A (16文字2行)のLCDを接続してみた。
初期設定とデータの表示位置を添付のdatasheetsでおさえておけばできる。このLCDはコントラストもプログラム制御で便利なのだが
設定は2つのレジスタにセットするのでちょっとわかりにくかった。
 尚、他のi2c接続のLCDはそれぞれデータの書式に作法があるので、単純に差し替えても表示できるとは限らない。

まず定数のワーキングレジスタの定義
#define     LCDADR      0x7C    //i2c LCD AQM1602XAのアドレス+R/wビット
                 // Arduinoで使う場合はたぶん3EH

プログラム
 以下のプログラムは LCD初期化、LCDへのコマンド送信、LCDへのデータ送信、その他のLCDへの送信、でメイン処理で各モジュールをCALLで呼んで使うものである。
/*=============================================================
 *    LCD 制御モジュール群
 * ===========================================================*/ 
/****************************
 LCD 初期化
 *****************************/
void lcd_init(void){
    __delay_ms(50); //50ms wait(datasheets:40ms以上待つ)
    i2cstart();     //開始条件送信
    i2csend(LCDADR);    //LCDのスレーブアドレス+W 送信
    //Function set #1
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0
    i2csend(0x38);  //Instruction送信  Function Set(instruction table #0)
    __delay_us(30); //30μs待つ
    //Function set #2
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0
    i2csend(0x39);  //Instruction送信 Function Set(instruction table #1)
    __delay_us(30); //30μs待つ
   
    //Internal OSC Frequency
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x16);  //Instruction 送信
    __delay_us(30); //30μs待つ   

    //Contrast set
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x73);  //Instruction 送信
    __delay_us(30); //30μs待つ       

    //Power /Con /Contrast
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x54);  //Instruction 送信
    __delay_us(30); //30μs待つ          

    //Follower Control
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x6C);  //Instruction 送信
    __delay_ms(200); //200ms待 つ            

    //Function set
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0      
    i2csend(0x38);  //Function Set(instruction table #0に戻す)
    __delay_us(30); //30μs待つ

    //Clear Display
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x01);  //Instruction送信

    //Display ON/OFF control
    i2csend(0x00); //control byte 送信 00H ;Co=OFF RS=0
    i2csend(0x0c);
    __delay_us(30); //30μs待つ
    i2cend();   //I2C終了条件送信処理   
}
/**************************************
 LCD COMMAND 実行 入力(コマンド1バイト)
 **************************************/
void lcd_exe(unsigned char lcdwbf){
    i2cstart();     //i2cスタート条件送信
    i2csend(LCDADR);    //LCD スレーブアドレス+W 送信
    i2csend(0x00);      //control byte 送信 00H:Co=OFF RS=0
    i2csend(lcdwbf);    //Instruction(=コマンド)送信
    i2cend();   //I2C終了条件送信処理 
}
/************************************
 LCD DATA WRITE   入力(データ1バイト)
 ***********************************/
void lcd_write(unsigned char lcdwbf){
    i2cstart();     //i2cスタート条件送信
    i2csend(LCDADR);    //LCD スレーブアドレス+W 送信
    i2csend(0x40);      //control byte 送信 00H:Co=OFF RS=1
    i2csend(lcdwbf);    //data送信
    i2cend();   //I2C終了条件送信処理 
}
/************************************
 LCD Off
 ***********************************/
void lcd_off(void){
    lcd_exe(0x08);
}
/***********************************
 LCD ON
 * 表示ON カーソルはoff (カーソルonの場合は0EHを設定)
 **********************************/
void lcd_on(void){
    lcd_exe(0x0c);
}
/**********************************
 LCD CLEAR
 **********************************/
void lcd_clear(void){
    lcd_exe(0x01);
    __delay_us(600);    //600μ wait
    __delay_ms(1);      //1ms wait
}
/********************************
 カーソルの位置を1行目の先頭にセット
 ********************************/
void lcd_l1(void){
    lcd_exe(0x80); //MSB=1 +DDRAMADDRESS(000 0000)
}
/********************************
 カーソルの位置を2行目の先頭にセット
 ********************************/
void lcd_l2(void){
    lcd_exe(0xC0); //MSB=1 +DDRAMADDRESS(100 0000)
}
/********************************
 カーソルの位置を右シフト(使ってないけど)
 ********************************/
void lcd_right(void){
    lcd_exe(0x14);
    __delay_us(50); //50μS wait
}
/**********************************
   16進数--> LCD 表示
    例 3AH  --> 3A とカレントのカーソル位置から表示
 **********************************/
void hex2lcd( char hex2wrk){
    char x; //wrk
    //上位4bitの処理B
    x = hex2wrk >> 4; //上位4bitを下位に
    x = x & 0x0f;
    if (x > 9){
        x=0x40 + (x-9);
    }else{
        x = 0x30 + x;
    }
    lcd_write(x);
    //下位4bitの処理
    x=hex2wrk & 0x0f; //下位4bitを抽出
    if (x > 9){
        x=0x40 + (x-9);
    }else{
        x=0x30+x;
    }
    lcd_write(x);   
}


7 温度センサーADT7410との通信(マスター側 プログラム)

 秋月電子でADT7410使用のi2c接続温度センサーモジュール(税込み500円)を購入。
 上記LCDの2行目に読み取ったデータと10進数の温度(摂氏)にした値を表示する

定数とワークの定義
#define     ADTADR_W    0x90    //ADT7410スレーブアドレス書き込みモード
#define     ADTADR_R    0x91    //ADT7410スレーブアドレス読み込みモード

温度センサーに対する初期設定は特にない。i2c通信がLCDとできていることを前提にする。回路はこのページに最初の方に接続端子として
描いてある。
mainプログラムのループの中で温度センサーから温度を読み取りLCDのに表示するところと、温度読み取りのためのモジュール、読み取り後の表 示のための計算モジュールを下に示す。

/********************************
  ADT7410から温度を読み込む
 ********************************/
void adt_read(char *temph, char *templ){
    i2cstart();         //開始条件送信
    i2csend(ADTADR_W);  //ADTアドレス(書込用)
    i2csend(0x00);      //温度レジスタHigh 0番地

    i2crestart();       //開始条件再送
    i2csend(ADTADR_R);  //ADTアドレス(読込用)
   
    *temph=i2crcv();     //ADTより温度上位バイト受信
    *templ=i2crcv();     //ADTより温度下位バイト受信
    
    i2cend();
}
/*************************************************************
; 温度計算
;  (TEMPH,TEMPL)から10進数の温度(ondo4,ondo3,ondo2,ondo1,ondo0)に変換
 *      ondo4は符号(-)  *ondo3 十進2桁目 *ondo2 十進1桁目 *ondo1 小数1位 *ondo0 小数2位
; (1)*ondo4にスペース20H,*ondo3,*ondo2はそれぞれ0クリア
; (2)TEMPHのMSBが1なら *ondo4'-'を入れる
; (3)TEMPHのMSBが1なら (TEMPH,TEMPL)の0と1をひっくり返して+1する
; (4)(TEMPH,TEMPL)を2倍(左シフト)してT_WRKH,T_WRKLに入れる
; (5)T_WRKHを10進数にして十の位を*ondo3に、一の位を*ondo0に入れる
; (6)T_WRKlの上位4bitを十進数小数部1位、2位にセットする
; (7) *ondo3がスペースかつ*ondo3〜*ondo0が0なら- 0.00なので*ondo4の'-'をスペースに置き換える
; (8)*ondo3〜*ondo0の数値を文字コードにする。ただし*ondo3がスペースのときは*ondo2〜*ondo0を文字コードにする。
*************************************************************/
void t_cal(char temph,char templ,char *ondo4,char *ondo3,char *ondo2,char *ondo1,char *ondo0){
    //(1)の処理
    *ondo4=0x20;    //十進温度の符号 とりあえずスペースを入れる
    *ondo3=0;       //十進温度十の位 とりあえず0
    *ondo2=0;       //十進温度一の位 とりあえず0
    //(2),(3)の処理
    if((temph & 0x80) ==0x80){   //temphのMSBが1なら氷点下
        *ondo4='-';
        temph=~temph;       //ビット反転
        templ=~templ;       //ビット反転
        templ=templ+1;
        if(STATUSbits.C==1){
            temph=temph+1;
        }
    }
    //(4)の処理
    if((templ & 0x80 ) ==0){
        templ=templ << 1;
        temph=temph << 1;
    }else{
        templ=templ<<1;
        temph=temph<<1;
        temph=temph | 0x01;
    }

    //(5)の処理
    while(temph>=10){
        *ondo3=*ondo3+1;
        temph=temph-10;
    }
    *ondo2=temph;
    //(6)の処理
    switch(templ & 0xF0){
        case 0x00:
            *ondo1=0;
            *ondo0=0;
            break;
        case 0x10:
            *ondo1=0;
            *ondo0=6;
            break;
        case 0x20:
            *ondo1=1;
            *ondo0=3;
            break;
        case 0x30:
            *ondo1=1;
            *ondo0=9;
            break;
        case 0x40:
            *ondo1=2;
            *ondo0=5;
            break;           
         case 0x50:
            *ondo1=3;
            *ondo0=1;
            break;
        case 0x60:
            *ondo1=3;
            *ondo0=8;
            break;
        case 0x70:
            *ondo1=4;
            *ondo0=4;
            break;
        case 0x80:
            *ondo1=5;
            *ondo0=0;
            break;
        case 0x90:
            *ondo1=5;
            *ondo0=6;
            break;
        case 0xA0:
            *ondo1=6;
            *ondo0=3;
            break;
        case 0xB0:
            *ondo1=6;
            *ondo0=9;
            break;
        case 0xC0:
            *ondo1=7;
            *ondo0=5;
            break;           
         case 0xD0:
            *ondo1=8;
            *ondo0=1;
            break;
        case 0xE0:
            *ondo1=8;
            *ondo0=8;
            break;
        case 0xF0:
            *ondo1=9;
            *ondo0=4;
            break;
        default:
            *ondo1=0;
            *ondo0=0;
    }
    //(7)の処理   - 0.00 は-を消す
    if ((*ondo3==0x20)&& (*ondo2 + *ondo1 + *ondo0 == 0)){
        *ondo4=0x20;
    }
    //(8)の処理
    if(*ondo3 != 0x20){
        *ondo3=*ondo3+0x30; //文字に変換
    }
    *ondo2=*ondo2+0x30;//文字に変換
    *ondo1=*ondo1+0x30;//文字に変換
    *ondo0=*ondo0+0x30;//文字に変換
   
   
    //(8)の処理
    if(*ondo3==0x30){
        *ondo3=0x20; //ondo1が'0'ならスペースに置き換える
    }
    //(9)の処理
    if((*ondo3==0x20) && (*ondo2 == 0x30)){
        *ondo4=0x20; // 0℃のときは*ondo2,*ondo1ともにスペースにする '-'は表示しない
    }
}


8.サーボモータのDUTY値の送受信(マスター側 プログラム)


  温度(摂氏の10進表示)の1桁目が奇数なら、スレーブ側のサーボを制御するPICに右いっぱいにサーボを動かすためのDuty値を送信し奇数の場合は左 いっぱいにサーボを動かすDUTY値を送信する。(という意味のない仕様)。また送信値はLCDBの1行目に表示し、さらにスレーブ側から現在の サーボの DUTY値を受け取り表示する。
 スレーブ側CPUへの送信はアドレスを指定したあと、サーボモータのDUTY値1byteを送信するだけである。受信はスレーブ側 CPUがサーボモータに出力しているDuty値を1バイト受け取るだけである。
定数とワークの定義
#define     SERVOADR_W  0xB6    //サーボモータモジュール書き込みアドレス
#define     SERVOADR_R  0xB7    //サーボモータモジュール読み込みアドレス
#define D_LEFT 0x4E         // Duty 2.5% Servo Left
#define D_CENTER 0x2F       // Duty 7.25% Servo Center
#define D_RIGHT    0x0F        //Duty 12%  Servo Right


/************************************************
; DUTY値送信
;***********************************************/
void send_duty(char d_val){
    i2crestart();   //開始条件送信
    i2csend(SERVOADR_W);    //サーボモータ書込用アドレス
    i2csend(d_val);
    i2cend();
}
/************************************************
; DUTY値受信
;************************************************/
char receive_duty(void){
    char rcv_wrk;  //受信用ワーク
    i2crestart();           //開始条件送信
    i2csend(SERVOADR_R);    //サーボモータ読込用アドレス
    rcv_wrk=i2crend();
    i2cend();
    return rcv_wrk;   
}
/*======================================
        main
========================================*/
int main(void){
    char d_val;      //Duty 5%〜10% 初期値 7.25%
    char temph; //adtから受信した温度上位バイト
    char templ; //adtから受信した温度下位バイト
    char ondo4;      //10進温度の + - の符号
    char ondo3;      //10進温度の2桁目
    char ondo2;      //10進温度の1桁目
    char ondo1;      //10進温度の小数1桁目
    char ondo0;      //10進温度の小数2桁目   
   
   
    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=0x28; //SSP1EN=ON,I2C master mode
    SSP1CON2=0;
    SSP1ADD=0x04; //2MHz/((04H+1)*4)=100kHz  100Kbpsの設定
    PIR1bits.SSP1IF=0; //割り込みフラグクリア
    //ここまでI2Cの設定
   
    lcd_init(); //LCD初期化
    lcd_on();   //LCD ON
   
    //main loop
    while(1){
        adt_read(&temph, &templ); //温度取得 --> temph,templ
        lcd_exe(0xc0); //2行目0文字目に位置設定
        hex2lcd(temph); //温度上位バイトをそのまま16進数でLDDに表示
        hex2lcd(templ); //温度上位バイトをそのまま16進数でLDDに表示
       
        t_cal(temph,templ,&ondo4,&ondo3,&ondo2,&ondo1,&ondo0);    //温度の10進数--->ondo2,ondo1,ondo0
        lcd_exe(0xc8);  //2行目8文字をセット
        lcd_write(ondo4);
        lcd_write(ondo3);
        lcd_write(ondo2);
        lcd_write('.'); //小数点
        lcd_write(ondo1);
        lcd_write(ondo0);
        lcd_write(0xf2);//℃の丸
        lcd_write('C');     //℃のC
       
        if((ondo2 & 0x01) ==0){
            d_val=D_RIGHT;  //温度が偶数ならサーボを右にきる
        }else{
            d_val=D_LEFT;   //温度が奇数ならサーボを右にきる
        }
        send_duty(d_val);   //サーボモータduty値を送信
        //送信したduty値の表示
        lcd_exe(0x80);  //表示位置セット 1行目1文字目
        lcd_write('T');
        lcd_write('X');
        lcd_write(':');
        hex2lcd(d_val); //d_valを16進数で表示
       
        __delay_ms(500); //500ms wait
        __delay_ms(500); //500ms wait
        __delay_ms(500); //500ms wait
        __delay_ms(500); //500ms wait       
       
        //serbo motor モジュールからDuty値受信
        d_val=receive_duty(); //duty値-->rcv_wrk
        lcd_exe(0x88);  //表示位置セット 1行目8文字目
        lcd_write('R');
        lcd_write('X');
        lcd_write(':');
        hex2lcd(d_val); //d_valを16進数で表示       
        }
}


9.マスター側プログラム全体

/* 2021.02.18 作成
; CPU ピン配置 PIC12F1840
;                 |--u--|   
;             VDD=|1   8|=VSS
;        PWM(RA5)=|2   7|=(RA0)
;           (RA4)=|3   6|=(RA1)SCL
;           (RA3)=|4   5|=(RA2)SDA
;                  ~~~~~~
; クロック2MHz
; 摂氏の温度(10進数)が偶数ならスレーブ(0B6HのCPU)のサーボを右に、奇数ならサーボを左にする
; スレーブアドレス0B6HのマイコンからDuty値を受信してLCD1行目8文字目に表示
*/
#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 D_LEFT 0x4E         // Duty 2.5% Servo Left
#define D_CENTER 0x2F       // Duty 7.25% Servo Center
#define D_RIGHT    0x0F        //Duty 12%  Servo Right

#define     LCDADR      0x7C    //i2c LCD AQM1602XAのアドレス+R/wビット0
#define     ADTADR_W    0x90    //ADT7410スレーブアドレス書き込みモード
#define     ADTADR_R    0x91    //ADT7410スレーブアドレス読み込みモード
#define     SERVOADR_W  0xB6    //サーボモータモジュール書き込みアドレス
#define     SERVOADR_R  0xB7    //サーボモータモジュール読み込みアドレス

/****************************
 *  割込処理         
*****************************/
void __interrupt() isr(void){
    //今回は割り込み処理は使わない
}
/*=========================================================================
;   I2C  制御モジュール群
;=========================================================================*/
/***********************************************************************
; 開始条件送信
; (1)通信状態ではないときSDA,SCLともにHIGH状態(22KΩの抵抗でこの2本をプルアップ
; (2) SCLがHのままSDAがH --> L に変化したときが開始状態
************************************************************************/
void i2cstart(void){
    do{
        SSP1CON1bits.WCOL=0;    //SSPCONの衝突フラグクリア
        SSP1CON2bits.SEN=1;     //開始条件送信
    }while(SSP1CON1bits.WCOL==1);   //衝突有り? 有りならループ継続

    while(SSP1CON2bits.SEN==1){
        //SEN==0でスタート処理終了
    }
}
/**********************************************************************
; 受信処理の再開始条件送信
**********************************************************************/
void i2crestart(void){
    PIR1bits.SSP1IF=0;      //割込みフラグクリア
    SSP1CON2bits.RSEN=1;    //再開始条件送信
    while(SSP1CON2bits.RSEN==1){
        //RSEN==0で再開始条件出力処理終了
    }
}
/**********************************************************************
   データ送信   
   入力 W: 送信データ
**********************************************************************/
void i2csend(char t_data){
    PIR1bits.SSP1IF=0;  //割込フラグクリア
    SSP1BUF=t_data;     //送信データセット
    while(PIR1bits.SSP1IF==0){
        //SSP1IF==1 送信完了になるまで待つ
    }
    while(SSP1CON2bits.ACKSTAT==1){
        //ACK受信まで待つ
    }
}
/*********************************************************************
;  1バイトデータ受信      出力 受信データ
*********************************************************************/
char i2crcv(void){
    PIR1bits.SSP1IF=0;  //割込フラグクリア
    SSP1CON2bits.RCEN=1;    //受信許可
    while(SSP1CON2bits.RCEN==1){
        //RCEN==0 受信完了まで待つ
    }
    SSP1CON2bits.ACKDT=0;   //ACKをセット
    SSP1CON2bits.ACKEN=1;   //ACK送信
    while(SSP1CON2bits.ACKEN==1){
        //ACKEN==0 ACK送信完了まで待つ
    }
    return SSP1BUF; //データ出力
}
/**********************************************************************
;   最終バイト受信   (最終バイトを受信して終了の時はCPUからNACKを送信)
;    出力  受信データ
;*********************************************************************/
char i2crend(void){
    PIR1bits.SSP1IF=0;  //割込フラグクリア
    SSP1CON2bits.RCEN=1;    //受信許可
    while(SSP1CON2bits.RCEN==1){
        //RCEN==0 受信完了まで待つ
    }
    SSP1CON2bits.ACKDT=1;   //NACKをセット
    SSP1CON2bits.ACKEN=1;   //NACK送信
    while(SSP1CON2bits.ACKEN==1){
        //ACKEN==0 NACK送信完了まで待つ
    }
    return SSP1BUF; //データ出力
}
/**********************************************************************
; 終了条件送信
;*********************************************************************/
void i2cend(){
    PIR1bits.SSP1IF=0;  //割り込みフラグクリア
    SSP1CON2bits.PEN=1; //ストップ出力
    while(SSP1CON2bits.PEN==1){
        //PEN==0 出力完了まで待つ
    }
    PIR1bits.SSP1IF=0;  //割り込みフラグクリア
}
/*=============================================================
 *    LCD 制御モジュール群
 * ===========================================================*/ 
/****************************
 LCD 初期化
 *****************************/
void lcd_init(void){
    __delay_ms(50); //50ms wait(datasheets:40ms以上待つ)
    i2cstart();     //開始条件送信
    i2csend(LCDADR);    //LCDのスレーブアドレス+W 送信
    //Function set #1
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0
    i2csend(0x38);  //Instruction送信  Function Set(instruction table #0)
    __delay_us(30); //30μs待つ
    //Function set #2
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0
    i2csend(0x39);  //Instruction送信 Function Set(instruction table #1)
    __delay_us(30); //30μs待つ
   
    //Internal OSC Frequency
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x16);  //Instruction 送信
    __delay_us(30); //30μs待つ   

    //Contrast set
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x73);  //Instruction 送信
    __delay_us(30); //30μs待つ       

    //Power /Con /Contrast
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x54);  //Instruction 送信
    __delay_us(30); //30μs待つ          

    //Follower Control
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x6C);  //Instruction 送信
    __delay_ms(200); //200ms待 つ            

    //Function set
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0      
    i2csend(0x38);  //Function Set(instruction table #0に戻す)
    __delay_us(30); //30μs待つ

    //Clear Display
    i2csend(0x80);  //control byte 送信 80H ;Co=ON RS=0  
    i2csend(0x01);  //Instruction送信

    //Display ON/OFF control
    i2csend(0x00); //control byte 送信 00H ;Co=OFF RS=0
    i2csend(0x0c);
    __delay_us(30); //30μs待つ
    i2cend();   //I2C終了条件送信処理   
}
/**************************************
 LCD COMMAND 実行 入力(コマンド1バイト)
 **************************************/
void lcd_exe(char lcdwbf){
    i2cstart();     //i2cスタート条件送信
    i2csend(LCDADR);    //LCD スレーブアドレス+W 送信
    i2csend(0x00);      //control byte 送信 00H:Co=OFF RS=0
    i2csend(lcdwbf);    //Instruction(=コマンド)送信
    i2cend();   //I2C終了条件送信処理 
}
/************************************
 LCD DATA WRITE   入力(データ1バイト)
 ***********************************/
void lcd_write(char lcdwbf){
    i2cstart();     //i2cスタート条件送信
    i2csend(LCDADR);    //LCD スレーブアドレス+W 送信
    i2csend(0x40);      //control byte 送信 00H:Co=OFF RS=1
    i2csend(lcdwbf);    //data送信
    i2cend();   //I2C終了条件送信処理 
}
/************************************
 LCD Off
 ***********************************/
void lcd_off(void){
    lcd_exe(0x08);
}
/***********************************
 LCD ON
 * 表示ON カーソルはoff (カーソルonの場合は0EHを設定)
 **********************************/
void lcd_on(void){
    lcd_exe(0x0c);
}
/**********************************
 LCD CLEAR
 **********************************/
void lcd_clear(void){
    lcd_exe(0x01);
    __delay_us(600);    //600μ wait
    __delay_ms(1);      //1ms wait
}
/********************************
 カーソルの位置を1行目の先頭にセット
 ********************************/
void lcd_l1(void){
    lcd_exe(0x80); //MSB=1 +DDRAMADDRESS(000 0000)
}
/********************************
 カーソルの位置を2行目の先頭にセット
 ********************************/
void lcd_l2(void){
    lcd_exe(0xC0); //MSB=1 +DDRAMADDRESS(100 0000)
}
/********************************
 カーソルの位置を右シフト(使ってないけど)
 ********************************/
void lcd_right(void){
    lcd_exe(0x14);
    __delay_us(50); //50μS wait
}
/**********************************
   16進数--> LCD 表示
    例 3AH  --> 3A とカレントのカーソル位置から表示
 **********************************/
void hex2lcd( char hex2wrk){
    char x; //wrk
    //上位4bitの処理B
    x = hex2wrk >> 4; //上位4bitを下位に
    x = x & 0x0f;
    if (x > 9){
        x=0x40 + (x-9);
    }else{
        x = 0x30 + x;
    }
    lcd_write(x);
    //下位4bitの処理
    x=hex2wrk & 0x0f; //下位4bitを抽出
    if (x > 9){
        x=0x40 + (x-9);
    }else{
        x=0x30+x;
    }
    lcd_write(x);   
}
/********************************
  ADT7410から温度を読み込む
 ********************************/
void adt_read(char *temph, char *templ){
    i2cstart();         //開始条件送信
    i2csend(ADTADR_W);  //ADTアドレス(書込用)
    i2csend(0x00);      //温度レジスタHigh 0番地

    i2crestart();       //開始条件再送
    i2csend(ADTADR_R);  //ADTアドレス(読込用)
   
    *temph=i2crcv();     //ADTより温度上位バイト受信
    *templ=i2crcv();     //ADTより温度下位バイト受信
    
    i2cend();
}
/*************************************************************
; 温度計算
;  (TEMPH,TEMPL)から10進数の温度(ondo4,ondo3,ondo2,ondo1,ondo0)に変換
 *      ondo4は符号(-)  *ondo3 十進2桁目 *ondo2 十進1桁目 *ondo1 小数1位 *ondo0 小数2位
; (1)*ondo4にスペース20H,*ondo3,*ondo2はそれぞれ0クリア
; (2)TEMPHのMSBが1なら *ondo4'-'を入れる
; (3)TEMPHのMSBが1なら (TEMPH,TEMPL)の0と1をひっくり返して+1する
; (4)(TEMPH,TEMPL)を2倍(左シフト)してT_WRKH,T_WRKLに入れる
; (5)T_WRKHを10進数にして十の位を*ondo3に、一の位を*ondo0に入れる
; (6)T_WRKlの上位4bitを十進数小数部1位、2位にセットする
; (7) *ondo3がスペースかつ*ondo3〜*ondo0が0なら- 0.00なので*ondo4の'-'をスペースに置き換える
; (8)*ondo3〜*ondo0の数値を文字コードにする。ただし*ondo3がスペースのときは*ondo2〜*ondo0を文字コードにする。
*************************************************************/
void t_cal(char temph,char templ,char *ondo4,char *ondo3,char *ondo2,char *ondo1,char *ondo0){
    //(1)の処理
    *ondo4=0x20;    //十進温度の符号 とりあえずスペースを入れる
    *ondo3=0;       //十進温度十の位 とりあえず0
    *ondo2=0;       //十進温度一の位 とりあえず0
    //(2),(3)の処理
    if((temph & 0x80) ==0x80){   //temphのMSBが1なら氷点下
        *ondo4='-';
        temph=~temph;       //ビット反転
        templ=~templ;       //ビット反転
        templ=templ+1;
        if(STATUSbits.C==1){
            temph=temph+1;
        }
    }
    //(4)の処理
    if((templ & 0x80 ) ==0){
        templ=templ << 1;
        temph=temph << 1;
    }else{
        templ=templ<<1;
        temph=temph<<1;
        temph=temph | 0x01;
    }

    //(5)の処理
    while(temph>=10){
        *ondo3=*ondo3+1;
        temph=temph-10;
    }
    *ondo2=temph;
    //(6)の処理
    switch(templ & 0xF0){
        case 0x00:
            *ondo1=0;
            *ondo0=0;
            break;
        case 0x10:
            *ondo1=0;
            *ondo0=6;
            break;
        case 0x20:
            *ondo1=1;
            *ondo0=3;
            break;
        case 0x30:
            *ondo1=1;
            *ondo0=9;
            break;
        case 0x40:
            *ondo1=2;
            *ondo0=5;
            break;           
         case 0x50:
            *ondo1=3;
            *ondo0=1;
            break;
        case 0x60:
            *ondo1=3;
            *ondo0=8;
            break;
        case 0x70:
            *ondo1=4;
            *ondo0=4;
            break;
        case 0x80:
            *ondo1=5;
            *ondo0=0;
            break;
        case 0x90:
            *ondo1=5;
            *ondo0=6;
            break;
        case 0xA0:
            *ondo1=6;
            *ondo0=3;
            break;
        case 0xB0:
            *ondo1=6;
            *ondo0=9;
            break;
        case 0xC0:
            *ondo1=7;
            *ondo0=5;
            break;           
         case 0xD0:
            *ondo1=8;
            *ondo0=1;
            break;
        case 0xE0:
            *ondo1=8;
            *ondo0=8;
            break;
        case 0xF0:
            *ondo1=9;
            *ondo0=4;
            break;
        default:
            *ondo1=0;
            *ondo0=0;
    }
    //(7)の処理   - 0.00 は-を消す
    if ((*ondo3==0x20)&& (*ondo2 + *ondo1 + *ondo0 == 0)){
        *ondo4=0x20;
    }
    //(8)の処理
    if(*ondo3 != 0x20){
        *ondo3=*ondo3+0x30; //文字に変換
    }
    *ondo2=*ondo2+0x30;//文字に変換
    *ondo1=*ondo1+0x30;//文字に変換
    *ondo0=*ondo0+0x30;//文字に変換
   
   
    //(8)の処理
    if(*ondo3==0x30){
        *ondo3=0x20; //ondo1が'0'ならスペースに置き換える
    }
    //(9)の処理
    if((*ondo3==0x20) && (*ondo2 == 0x30)){
        *ondo4=0x20; // 0℃のときは*ondo2,*ondo1ともにスペースにする '-'は表示しない
    }
}
/************************************************
; DUTY値送信
;***********************************************/
void send_duty(char d_val){
    i2crestart();   //開始条件送信
    i2csend(SERVOADR_W);    //サーボモータ書込用アドレス
    i2csend(d_val);
    i2cend();
}
/************************************************
; DUTY値受信
;************************************************/
char receive_duty(void){
    char rcv_wrk;  //受信用ワーク
    i2crestart();           //開始条件送信
    i2csend(SERVOADR_R);    //サーボモータ読込用アドレス
    rcv_wrk=i2crend();
    i2cend();
    return rcv_wrk;   
}
/*======================================
        main
========================================*/
int main(void){
    char d_val;      //Duty 5%〜10% 初期値 7.25%
    char temph; //adtから受信した温度上位バイト
    char templ; //adtから受信した温度下位バイト
    char ondo4;      //10進温度の + - の符号
    char ondo3;      //10進温度の2桁目
    char ondo2;      //10進温度の1桁目
    char ondo1;      //10進温度の小数1桁目
    char ondo0;      //10進温度の小数2桁目   
   
   
    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=0x28; //SSP1EN=ON,I2C master mode
    SSP1CON2=0;
    SSP1ADD=0x04; //2MHz/((04H+1)*4)=100kHz  100Kbpsの設定
    PIR1bits.SSP1IF=0; //割り込みフラグクリア
    //ここまでI2Cの設定
   
    lcd_init(); //LCD初期化
    lcd_on();   //LCD ON
   
    //main loop
    while(1){
        adt_read(&temph, &templ); //温度取得 --> temph,templ
        lcd_exe(0xc0); //2行目0文字目に位置設定
        hex2lcd(temph); //温度上位バイトをそのまま16進数でLDDに表示
        hex2lcd(templ); //温度上位バイトをそのまま16進数でLDDに表示
       
        t_cal(temph,templ,&ondo4,&ondo3,&ondo2,&ondo1,&ondo0);    //温度の10進数--->ondo2,ondo1,ondo0
        lcd_exe(0xc8);  //2行目8文字をセット
        lcd_write(ondo4);
        lcd_write(ondo3);
        lcd_write(ondo2);
        lcd_write('.'); //小数点
        lcd_write(ondo1);
        lcd_write(ondo0);
        lcd_write(0xf2);//℃の丸
        lcd_write('C');     //℃のC
       
        if((ondo2 & 0x01) ==0){
            d_val=D_RIGHT;  //温度が偶数ならサーボを右にきる
        }else{
            d_val=D_LEFT;   //温度が奇数ならサーボを右にきる
        }
        send_duty(d_val);   //サーボモータduty値を送信
        //送信したduty値の表示
        lcd_exe(0x80);  //表示位置セット 1行目1文字目
        lcd_write('T');
        lcd_write('X');
        lcd_write(':');
        hex2lcd(d_val); //d_valを16進数で表示
       
        __delay_ms(500); //500ms wait
        __delay_ms(500); //500ms wait
        __delay_ms(500); //500ms wait
        __delay_ms(500); //500ms wait       
       
        //serbo motor モジュールからDuty値受信
        d_val=receive_duty(); //duty値-->rcv_wrk
        lcd_exe(0x88);  //表示位置セット 1行目8文字目
        lcd_write('R');
        lcd_write('X');
        lcd_write(':');
        hex2lcd(d_val); //d_valを16進数で表示       
        }
}


10. PIC12F1840 スレーブ側のi2cプログラム

(1)スレーブ側初期設定 

  コンフィギュレーションはマスターと同じ。CPUのクロックはSG-90を動かす関係から2MHzに設定
  3.PIC12F1840 のコンフィギュレーション等の設定
  
/*
 * i2cマスターからduty値を受け取りサーボモータを制御するi2cスレーブ側のシステム
 * スレーブCPU PIC12F1840  サーボモータSG-90(秋月電子より購入)
 *     2021.02.12 MPASMからXC8に移植
 *
 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 PWMPRD 159          //PWM period for PR2
#define D_LEFT 0x4E         // Duty 2.5% Servo Left
#define D_CENTER 0x2F       // Duty 7.25% Servo Center
#define D_RIGHT    0x0F        //Duty 12%  Servo Right

void dutyset(char); //Duty値出力処理
void i_read(void);  //i2c 受信要求処理
void i_write(void); //i2c 送信要求処理

(2)スレーブ側i2c送受信処理(割込処理)

i2c スレーブ側フローチャート


/****************************
 *  割込処理         
*****************************/
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値を取り出して送信する       
 *****************************/
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をセット
 }



11 PWMの設定

 ここではサーボモータSG-90用を制御するための設定を行う。

Pwm Period
 
 SG-90のPWM Period(PWM周期)は20ms (f=50Hz) 
  Pulse width(=Duty cycle)は0.5ms〜2.4ms
 0.5msのとき-90°(上から見て右回り限界)
 2.4msのとき 90°(上から見て左回り限界)

設定手順
     pic12f1840のdatasheetの188pにPWM設定に関するレジスタの一覧表がある。
(1)CPUのクロックの確認
  コンフィギュレーション(マスター用と同じ)から内部発振器を使っていることがわかる。
         3.PIC12F1840 のコンフィギュレーション等の設定
  クロック周波数はOSCCONレジスタ<6:3>で定義される。  (datasheet 53p)
    OSCCONに60Hをセットして2MHzになっている

(2)PWMの出力ピンの決定
    APFCONレジスタのCCP1SELレジスタ(datasheet 99p)   1:RA5   0:RA2
    1をセットしてRA5を出力ピンに設定

(3)PWMモード設定
   CCP1CONレジスタ(datasheet189p)のCCP1Mに1100をセット
  
(4)PWM Period(PWM周期)の設定
 
  PWM Periodを20msに設定したい。
 PWM Periodは次式で決まる

  PWM Period [sec] = (PR2+1)*4*Tosc * Prescaler

  ToscはこのCPUのクロック  Tosc=1/f=1/(2*10^6)=0.5μs=0.0005ms
  PrescalerはT2CONレジスタのT2CKPSビットの設定によって決まる
 T2CKPS=11をセット prescalerは64
 あとはPR2の値を求めればよい。


 PR2=PWM Period /(4*Tosc*Prescaler)-1
   
     =20[ms]/(4*0.0005[ms]*64) - 1 =  155 (小数点は四捨五入)

(4)Pulse widh の値の算出

 Pulse width(Duty値)はCCPR1Lレジスタの 8bit とCCPICONレジスタ2ビットの全10bitで構成される。
 CCPR1Lレジスタが上位8bit,CCPICON<5:4>の2bitが下位になる。
 この10bitをCCPR1L:CCP1CON<5:4> とすると

 PulseWidth = CCPR1L:CCP1CON<5:4> * Tosc * Prescaler

  という関係になる。
 
 PulseWidth = PWM Period つまりDuty比100%となるCCPR1L:CCP1CON<5:4>の値は

 CCPR1L:CCP1CON<5:4> = PulseWidth/(Tosc*Prescaler)

  =20[ms]/(0.0005[ms]*64)=625

 サーボモータsg-90は0.5[ms]〜2.4[ms]のPulseWidthでこれはPWM Period=20[ms]の2.5%〜12%である。
 -90° 0.5[ms]のCCPR1L:CCP1CON<5:4>の値は625*0.025=15=0FH
  90° 2.4[ms]のCCPR1L:CCP1CON<5:4>の値は625*0.12=75=4BH
 になる。

(5)PWM制御スタート
 (4)のCCPR1L:CCP1CON<5:4>の値が設定できたら T2CONレジスタのTMR2をONにするとPWMがスタートする
 

12 スレーブ側プログラム全体

/*
 * i2cマスターからduty値を受け取りサーボモータを制御するi2cスレーブ側のシステム
 * スレーブCPU PIC12F1840  サーボモータSG-90(秋月電子より購入)
 *     2021.02.12 MPASMからXC8に移植 2021.02.18 最終版
 *
 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 PWMPRD 159          //PWM period for PR2
#define D_LEFT 0x4E         // Duty 2.5% Servo Left
#define D_CENTER 0x2F       // Duty 7.25% Servo Center
#define D_RIGHT    0x0F        //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値を取り出して送信する       
 *****************************/
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;
}