PICマイコン(PIC16F1827)のI2C通信を実装する

組み込みエンジニア

こんにちは、ENGかぴです。

PIC16F1827はI2C(MSSP)通信機能を持っておりLCD等の外部機器と通信することができます。MCCによるI2C通信の実装例とLCDを使用したストップウォッチのソフトを作り経過時間を表示して動作確認しました。

LCDはAQM1602XA-RN-GBW(秋月電子)を使用しています。

I2Cを実装する

PIC16F1827のI2C(MSSP)の設定はMCCを使って行っています。MCCの使い方については下記記事にまとめています。

MPLAB Code Configurator(MCC)の追加と使い方

今回はMCCを使用してMSSP1(I2C)及びTMR0の設定を行っています。

System Moduleの設定

最初に共通事項であるSystem Moduleの設定を行います。MPLAB X IDEを起動しMCCのアイコンをクリックしてMCCを有効にします。

System Moduleの設定(MCC)
System Moduleの設定(MCC)

内部クロックを使用するためINTOSCを選択しています。クロック周波数は任意でもよいですが4MHz_Hzを選択しています。クロックを高速にするほど消費電流が増えてしまいます。他にも設定項目はありますがクロックの設定のみとしています。

MSSP1(MSSP1)を設定する

MSSPはMaster Synchronous Serial Port の略称です。PICマイコンのI2C通信はMSSP機能に含まれています。Device Resources内のPeripherals欄のMSSP1を選択して追加します。

MSSP1の設定(MCC)
MSSP1の設定(MCC)

MSSP1を選択しSerial ProtocolをI2Cを選択するとModeやClockの設定画面が表示されます。PICマイコンがLCDに対してコマンドを送信するためMasterとなります。

Clock設定を高速にすることもできますが、高速にするほど電流が必要(抵抗値を低くする)になるためお勧めしていません。外部機器とPICマイコンの配線の長さによるインピーダンスから波形が鈍ってしまい通信不良の原因となります。

I2C通信が完了したときに割り込みを発生させることができますが特に処理する必要はないため割り込みは使用していません。

TMR0を設定する

TMR0はDevice Resources内のPeripherals欄のTimerを選択しTMR0をクリックして追加します。

TMR0の設定(MCC)
TMR0の設定(MCC)

Timer Clock内でクロックに関する設定を行います。Clock Sourceは内部クロックを使用しているためFOSC(FOSC/4)を指定します。PrescalerはClock Sourceに対してTMR0のカウントアップする分周比を指定します。

Timer PeriodはTMR0がオーバーフローするタイミングを設定します。1ms毎にオーバーフロー割り込みが発生しますがCallback Function Rateを0x0A(10)を指定しているため10ms毎にコールバック関数が呼び出されます。

void mainTimer(void); //タイマー0割り込みのコールバック関数
//コールバック関数の使用例
TMR0_SetInterruptHandler(mainTimer); //コールバック関数を指定

上記例ではmainTimer()が10ms毎にコールされます。

MCCが生成した関数の使用例

#include "mcc_generated_files/examples/i2c1_master_example.h" //追加

uint8_t initmoji[2][16] ={"PIC16F1827-I2C","         Ver1.00"};

I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, FUNC1_SET ); //コマンドを送信

for( i = 0; i < sizeof(initmoji[1]); i++ ){
    I2C1_Write1ByteRegister(SLAVE_ADRS,LINE1_ADRS,initmoji[1][i]); //1バイトずつ送信
}

MCCでI2Cの設定を行うとi2c1_master.cなどほかにexamplesフォルダが生成されます。フォルダー内のi2c1_master_example.cファイルに実装されている関数を使用するため#include “mcc_generated_files/examples/i2c1_master_example.h”を追加しています。

LCDは書き込み専用であるためWriteを使用します。LCDの初期化ではコマンド1バイト送信して設定するためI2C1_Write1ByteRegister()をします。

LCDの初期化と文字表示

引用:LCD資料(参考資料)ー秋月電子HP(AQM1602XA-RN-GBW)
引用:LCD資料(参考資料)ー秋月電子HP(AQM1602XA-RN-GBW)

LCDで文字を表示するために初期化を行います。秋月電子のHPに公開されているAQM1602XA-RN-GBWのLCD資料(参考資料)の例から変更している部分について説明します。

void LcdInit(void){

    //Function set
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, 0x38); //8ビットバス・2LINE表示
    __delay_us(40);
    //Function set
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, 0x39); //拡張コマンド
    __delay_us(40);
    //Internal OSC frequency
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, 0x14); //内部周波数調整
    __delay_us(40);
    //Contrast set
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, 0x77); //コントラスト1
    __delay_us(40);
    //Power/ICON/Contrast control
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, 0x54);//コントラスト2
    __delay_us(40);
    //Follower control  
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, 0x6C);//フォロワー制御
    __delay_ms(250);

    //Function set
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, 0x38); //拡張コマンドをオフ
    __delay_us(40);
    //Clear Display
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, CLR_DISP ); //ディスプレイをクリア
    __delay_us(40);
    //Display ON/OFF control
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, DISP_ONOFF_SET ); //ディスプレイON/OFF
    __delay_us(40);
}

電源がDC5Vを使用しているためコントラストは例の通りだと暗くなり(ビット表示が黒くなる)文字が見えなくなるため薄くなるように設定します。C0~C2を1としC4とC5が0になるようにビットをセットします。Contrast Setに0x77、Power/ICON/Contrast controlを0x54をセットしています。

Follower controlレジスタV0の増幅率の調整で使用します。

コントラストと関係で目安となる値は決まりますが実際に表示してみてC0~C5を調整するかFollower controlレジスタを調整するか検討するのが良いと思います。

ST7032のデータシートを確認するとI2C使用時のレイアウトではOPF1、OPF2は0とするためFonに1をセットすると内部フォロアが有効になります。

続けてdisptbl配列に表示したい文字を格納しているものとして使用例を示しています。

#define LINE1_ADRS 0x40

for( i = 0; i < sizeof(disptbl[0]); i++ ){
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD,LINE1_ADRS,disptbl[0][i]);
}
引用:ST7032のデータシート(2-line Interface protocol)
引用:ST7032のデータシート(2-line Interface protocol)

I2C1_Write1ByteRegister()関数でスレーブアドレスを送信した後にcontrol byteを指定しますが2-line interface protocolに従って書き込みアドレスと文字データを指定します。control byte以降に制御コードを書き込まないのでCoに0をセットしRSがHになるので0x40になります。R/WはW固定なのでLになります。

LCDのアドレスカウンタは書き込みごとに自動でインクリメントされるため表示する文字のサイズ分繰り返して文字を表示しています。

動作確認

PIC16F1827とLCDを接続して動作確認を行います。LCDにI2C通信してデータを書き込み表示します。

動作確認用の回路

PIC16F1827のI2C通信の動作確認回路
PIC16F1827のI2C通信の動作確認回路

I2C通信にはプルアップ抵抗が必要ですが変換基板に抵抗が実装されています。電源を投入するとLCDの初期化を行い1列目に「PIC16F1827-I2C」、2列目に「Ver1.00」を2秒間表示します。

2秒が経過するとストップウォッチアプリがスタートします。SW1を押すとカウント開始しもう一度SW1を押すと停止します。SW1を2秒間長押しするとカウントをクリアします。ピン設定は以下の通りです。

PIC16F1827のピン設定(MCC)
PIC16F1827のピン設定(MCC)

RB0はウィークプルアップしているため信号レベルが浮くことはありません。SW1を押すとLED3が点灯し押さない場合はLED3が消灯するようにします。PB0はINPUTでウィークプルアップ(WPU)設定しています。WPUを有効にするためにはRegistersのnWPUENをenabledにする必要があります。

Easy SetupでWPUにチェックするのみでは有効にならないので注意が必要です。Notificationsにワーニングで通知されます。

ストップウォッチを作る

TMR0を使ってスタートからストップまでの時刻を管理します。SW1を押すとtimcntの更新をスタートします。ShowDataSet()はtimcntをLCD表示用のテキストデータ(アスキーコード)に変換する関数です。

void ShowDataSet(void){
    uint8_t msdata;
    uint8_t sdata;
    uint8_t ms10;
    uint8_t ms1;
    uint8_t s10;
    uint8_t s1;
    
    sdata = (timcnt / 100 ) % 60;
    msdata = timcnt % 100;

    ms10 = msdata / 10 + 0x30;
    ms1 = msdata  % 10 + 0x30;
    s10 = sdata / 10 + 0x30;
    s1 = sdata % 10 + 0x30;
 
    disptbl1[1][11] = s10;
    disptbl1[1][12] = s1;
    disptbl1[1][13] = '.';
    disptbl1[1][14] = ms10;
    disptbl1[1][15] = ms1;
}

msdataはms単位(ミリ秒)の表示のためtimcntを100で割った余りの数を計算しています。小数点以下の10の位をテキスト表示するためmsdataを10で割った商に0x30(アスキーコードで0を示す)を加えてテキストデータに変換しています。

小数点以下の1の位はmsdataを10で割った余りであり0x30を加えてテキストデータに変換しています。

sdataはs単位(秒)の表示のためtimcntを100で割った商となりますが秒は60までの59秒までなので60で割った余りを計算しています。

sdataを10で割ると秒の10の位となり10で割った余りが1の位となるため、それぞれに0x30を加えてテキストデータに変換しています。

動作結果

ストップウォッチの動作確認
ストップウォッチの動作確認

電源を投入すると初期画面が2秒間表示されていることが確認できSW1を押すとカウントがスタートすることが確認できました。カウント中にSW1を押すとカウントがストップしSW1を長押しするとカウントがクリアされることも確認できました。

10.00秒を目指してSW1を繰り返して押すといった昔ながらの遊びを再現することができて満足感を得ました。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/examples/i2c1_master_example.h"

#define SLAVE_ADRS 0x3E
#define LINE1_ADRS 0x40
#define LINE2_TOP (0x40 +0x80)
#define FUNC1_SET 0x38
#define FUNC2_SET 0x39
#define INT_OSC 0x14
#define CONST_SET 0x77
#define PWR_ICON_SET 0x54 //0x56
#define FOLLOWER_SET 0x6C
#define CLR_DISP 0x01
#define DISP_ONOFF_SET 0x0C

#define TIME_OFF -1         //タイマーを使用しない場合
#define TIME_UP 0           //タイムアップ
#define TIME_BASE_MAX 10     //ベースタイマカウント値
#define LCD_TIM_MAX 1
#define TIME_FILTER_MAX 1
#define	CNT_INIT_MAX 10     //10ms×10 = 100ms
#define WATCH_CNT_MAX 200
#define DI_NUM 2            //DIフィルタのサンプリング数

typedef struct{
    uint8_t    wp;
    uint8_t    buf[ DI_NUM ];
    uint8_t    di;
}FILT_DATA;

/* 変数定義 */
int16_t cnt10ms;
uint8_t CntInit; //初期化時のみ使用
uint32_t timcnt;
int8_t timlcd;
int16_t timfilter; //DIフィルタ起動
int16_t timcntclear = TIME_OFF;
FILT_DATA DiData;
bool startwatch = false;
bool btn1hold;
uint8_t initmoji[2][16] ={"PIC16F1827-I2C","         Ver1.00"};
uint8_t disptbl1[2][16] ={"STOP WATCH    ","                "};

/* 関数プロトタイプ宣言 */
void LcdInit(void);
void DspLine2Top(void);
void DspClear(void);
void mainTimer(void);
void Tmr0Int(void);
void DispSet(void);
void ShowDataSet(void);
void DiFilter(void);
void DiFilterInit(void);
void mainApp(void);

/* Main application */
void main(void)
{
    SYSTEM_Initialize();
    TMR0_SetInterruptHandler(mainTimer);
    INTERRUPT_GlobalInterruptEnable();
    INTERRUPT_PeripheralInterruptEnable();
    DiFilterInit(); //メイン処理前にDIを確定する
    LcdInit();
    
    while (1)
    {
        mainApp();
        DiFilter();
    }
}
/* メイン関数 */
void mainApp(void){
    
    if( DiData.di == 0){
        if( btn1hold == false ){
            btn1hold = true;
            timcntclear = WATCH_CNT_MAX;
            
            if( startwatch){
                startwatch = false;
            } 
            else{
                startwatch = true;
            }
        }
    }
    else{
        btn1hold = false;
        timcntclear = TIME_OFF;
    }    
 
    if( timlcd == TIME_UP ){
        timlcd = LCD_TIM_MAX;
        DispSet(); //LCD表示部分をセット
    }
    
    if( timcntclear == TIME_UP ){
        timcntclear = TIME_OFF;
        startwatch = false;
        timcnt = 0;
    }
}
/* タイマ管理関数 */
void mainTimer(void){
  
    if( timlcd > TIME_UP ){
        --timlcd;
    }
    
    if( timfilter > TIME_UP ){
        --timfilter; //タイマを更新
    }
    
    if( timcntclear > TIME_UP ){
        --timcntclear;
    }

    if( startwatch && timcntclear == TIME_OFF ){
        ++timcnt;
    }
}
/* DIフィルタの初期化 */
void DiFilterInit(void){
	
    CntInit = CNT_INIT_MAX;
    while(CntInit > 0){  //0になるまでフィルタを実施
        DiFilter();         //DIフィルタ処理
	__delay_ms(10);     //10ms遅延させてDIフィルタ処理
        CntInit--;
	timfilter = TIME_UP;
    }    
}
/* モード切替部のDIフィルタ */
void DiFilter(void){
    uint8_t i;
    bool boo = true;

    if( timfilter == TIME_UP ){
        timfilter = TIME_FILTER_MAX;
        DiData.buf[ DiData.wp ] = IO_RB0_GetValue();

        for( i = 1; i < DI_NUM; i++){
            if( DiData.buf[i-1] != DiData.buf[i] ){
                boo = false;
                break;
            }
        }

        if( boo ){ //比較して一致したら値を採用
             DiData.di = DiData.buf[ 0 ]; 
        }

        if( ++DiData.wp >= DI_NUM){
                DiData.wp = 0;
        }
    }
}
/* LCDの初期化 */
void LcdInit(void){
    uint8_t i;
    
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, FUNC1_SET ); //8ビットバス・2LINE表示
    __delay_us(40);
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, FUNC2_SET ); //拡張コマンド
    __delay_us(40);
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, INT_OSC ); //内部周波数調整
    __delay_us(40);    
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, CONST_SET ); //コントラスト1
    __delay_us(40);    
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, PWR_ICON_SET );//コントラスト2
    __delay_us(40);   
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, FOLLOWER_SET );//フォロワー制御
    __delay_ms(250);
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, FUNC1_SET ); //拡張コマンドをオフ
    __delay_us(40);
    
    DspClear(); //LCD表示をクリア
    
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, DISP_ONOFF_SET );
    __delay_us(40);
    
    //for( i = 0; i < sizeof(initmoji[0]); i++ ){
    //    I2C1_Write1ByteRegister(SLAVE_ADRS,LINE1_ADRS,initmoji[0][i]);
    //}
      
    I2C1_WriteNBytes( SLAVE_ADRS,&initmoji[0][0], sizeof(initmoji[0])); //LINE1の表示(上のforと同じ)
    
    DspLine2Top(); //カーソルをLINE2の先頭にする

    for( i = 0; i < sizeof(initmoji[1]); i++ ){
        I2C1_Write1ByteRegister(SLAVE_ADRS,LINE1_ADRS,initmoji[1][i]); //LINE2の表示
    }        
    __delay_ms(2000);
    DspClear();
    
    for( i = 0; i < sizeof(disptbl1[0]); i++ ){
        I2C1_Write1ByteRegister(SLAVE_ADRS,LINE1_ADRS,disptbl1[0][i]); //LINE1を表示
    }     
}
/* LINE2へのカーソル移動処理*/
void DspLine2Top(void){
    
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, LINE2_TOP );
    __delay_us(40);    
}
/* ディスプレイをクリア */
void DspClear(void){
  
    I2C1_Write1ByteRegister(SLAVE_ADRS, 0x00, CLR_DISP );
    __delay_us(40);
}
/* LINE2の表示(ストップウォッチ) */
void DispSet(void){
    uint8_t i;
    
    DspLine2Top();
    ShowDataSet();
    
    for( i = 0; i < sizeof(disptbl1[1]); i++ ){
        I2C1_Write1ByteRegister(SLAVE_ADRS,LINE1_ADRS,disptbl1[1][i]); //LINE2の表示(ストップウォッチ)
    } 
}
/* ストップウォッチの表示 */
void ShowDataSet(void){
    uint8_t msdata;
    uint8_t sdata;
    uint32_t mdata;
    uint8_t ms10;
    uint8_t ms1;
    uint8_t s10;
    uint8_t s1;
    uint32_t m10;
    uint8_t m1;
    
    mdata = (timcnt / 100 ) / 60;
    sdata = (timcnt / 100 ) % 60;
    msdata = timcnt % 100;
    
    ms10 = msdata / 10 + 0x30;
    ms1 = msdata  % 10 + 0x30;
    s10 = sdata / 10 + 0x30;
    s1 = sdata % 10 + 0x30;
    m10 = mdata / 10 + 0x30;
    m1 = mdata % 10 + 0x30;
    
    disptbl1[1][8] = (uint8_t)m10;
    disptbl1[1][9] = m1;    
    disptbl1[1][10] = ':';
    disptbl1[1][11] = s10;
    disptbl1[1][12] = s1;
    disptbl1[1][13] = '.';
    disptbl1[1][14] = ms10;
    disptbl1[1][15] = ms1;
}
/**
 End of File
*/

本ソースコードはMPLAB X IDEにMCCのプラグインをインストールしていないと使用できません。

関連リンク

PICマイコンを使ってマイコンのレジスタの設定やMPLAB X IDEのプラグインであるMCCを使用して動作確認したことについてまとめています。

PICマイコン(PIC12F675)で実現できる機能と解説リンクまとめ

PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ

テックジム|ゼロからはじめるPython入門講座の申込

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました