PICマイコン(PIC16F1827)のシリアル通信の応用

組み込みエンジニア

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

PIC16F1827のシリアル通信機能(EUSART)を応用して外部機器から電文を送信しPIC16F1827を操作し電文でレスポンスさせて動作確認を行いました。電文を使ったシリアル通信の送信と受信の考え方についてまとめました。

本記事は下記記事の応用編となります。EUSARTの設定やTMR0の設定は同様です。

PICマイコン(PIC16F1827)のシリアル通信を実装する

外部機器にはArudino UNOを使用しています。

送受信に使用する電文

PIC16F1827を外部機器からの電文で操作します。電文の構成は任意で構成することができますが外部機器とPIC16F1827で交換する電文の構成は同じ構成にすると管理しやすくなります。

電文の構成

電文の構成
電文の構成

電文の構成は1バイト目をヘッダーとしデータの先頭であることを通知します。2バイト目には3バイト以降にセットするデータのサイズを示すデータ長をセットします。電文の最終バイトにはデータ長以降のデータの総和を計算したチェックサムを付加します。

ヘッダーの部分の文字コードが’P’の場合はPIC16F1287への電文とし’A’であればArudino UNOへ送出する電文として区別します。

チェックサムはデータ部分の総和をとり1バイト分のデータをセットします。1バイトの大きさ(255)を超えても下位の1バイトをセットします。チェックサムをuint8_tで宣言しておくとオーバーフローして1バイトデータとなります。

例ではデータ1からデータ4としていますが任意のデータとすることで様々なパターンに対応する処理を作ることができます。

電文を受信する

while(EUSART_is_rx_ready()) //受信データがあるか
{
    picRcv.buf[picRcv.wp] = EUSART_Read(); //データをリード
    if( ++picRcv.wp >= sizeof(picRcv.buf)){ //次に保管する位置を更新
        picRcv.wp = 0;
    }
}

EUSARTで受信したデータを一時保管します。While()で受信データを確認しているのはメイン処理に来るたびに受信しているデータ分だけ確実に取得するためです。if()で確認するよりもデータの一時保管が早くなります。

データの受信と送信のシーケンス

void eUsartmain(void){

    switch( ComMode ){
        case COM_RX_WAIT:
            //受信待ちで一時保管したデータのチェックを行う  
            break;
        case COM_RCVDATA: 
            //受け入れた電文に対して処理を行う
            break;
        case COM_TX:
            //返信電文を送信する
            break;        
    }
}

受信と送信をモードによるシーケンス処理で管理します。以下の3つのモードを準備しています。

モード説明
COM_RX_WAIT受信待ちで一時保管したデータをヘッダーやデータ長から電文を判断する。
データのチェックサムによって問題なければ電文を受け付ける
COM_RCVDATA受け付けた電文を解析して処理を行う。
COM_TX準備したデータを送信する。
送受信を管理するモードの説明

COM_RX_WAITで受信を待ちチェックサムを得て電文を受け付けるとモードをCOM_RCVDATAに遷移させます。COM_RCVDATAで電文を解析して処理を行いデータを返信する場合はモードをCOM_TXに遷移させます。

返信しない場合はモードをCOM_RCVDATAに戻して受信待ちにします。

シーケンスとして管理しており電文を受け付けたら返信(送信)することを目的としているため受信と送信を同時に行うことはできません。

受信データのチェック

一時保管したデータが電文の構成であるかを確認するためにCOM_RX_WAITモードで受信データのチェックを行います。

if( picRcv.buf[ rp ] != HEADER ){ //ヘッダーが一致するか
    ReadPointerAdd(); //読み込み位置の更新
}
else{
    if( rxsz >= 2 ){ //データ長分だけ獲得しているか
        //データ長を確定する             
        if( rxsz >= allsz){ //確定したサイズ以上か                
            for( i=0; i < allsz; i++ ){
                Rcvdata[i] = picRcv.buf[picRcv.rp]; //一時保管したデータを移す
                ReadPointerAdd(); //読み込み位置の更新
            }
                       
            sumchk = CalcSum(&Rcvdata[2], datsz); //チェックサムの計算
            if( sumchk == Rcvdata[allsz-1]){ //チェックサムの確認
                ComMode = COM_RCVDATA; //モードを遷移
            }
        }
    }               
}

最初にヘッダーを確認します。PIC16F1827であればArudino UNOから受信するためヘッダーは’P'(0x50)になります。ヘッダーが一致した場合は2バイト目のデータ長が受信できているかの確認を行います。

2バイト目のデータ長によって電文全体のサイズが確定できるため受信データを電文保管用のバッファ(Rcvdata[])に移し替えます。Rcvdata[]のチェックサムを計算しチェックサムが一致するか確認します。チェックサムに問題がなければ電文として受け入れるためモードを進めて処理を行います。

データの送信

if(EUSART_is_tx_ready()){ //送信ビジーでないか
    if ( Txdata.cnt < sizeof(Txdata.data)){
        EUSART_Write(Txdata.data[Txdata.cnt]); //データを送信
        ++Txdata.cnt; //送信データカウントを更新
    }
    else{ //全データの送信が完了
        timTxWait = TIME_OFF;
        ComMode = COM_RX_WAIT; //受信待ちに遷移
    }
}  
                
if( timTxWait == TIME_UP ){ //送信タイムアウトか
    timTxWait = TIME_OFF;
    ComMode = COM_RX_WAIT; //受信待ちに遷移
}

送信がビジー(待機状態)でなければデータを送信します。送信データを配列として準備し送信データカウントを更新しながら配列の先頭から順に送信します。送信データの準備についてはソースコード全体のTxDataSet()を参考にしてください。

送信する際にタイムアウトを設けていますが何らかの要因で送信がうまくいかなかった場合にタイムアウトすることで受信モードに切り替えることができます。

送信タイムアウトは電文のサイズによって適切な時限をセットする必要があります。

送信タイムアウトに引っかかってしまう場合は異常状態とみなしてシリアル通信機能をリスタートするなど対策に使用できます。

動作確認

PIC16F1827とArduino UNOを接続してシリアル通信の動作確認を行います。通信条件はボーレートが19200bps、パリティなしとします。

動作確認用の回路

PIC16F1827とArduino UNOによるシリアル通信の確認回路
PIC16F1827とArduino UNOによるシリアル通信の確認回路

Arduino UNOではソフトウェアシリアルのライブラリを使用してPIC16F1827とシリアル通信を行います。

SW1とSW2でArudino UNOから送出する電文を切り替えて送信します。SW1を押すとPIC16F1827はLED3を点灯してArudino UNOに「OK-1」を含む電文で返信します。SW2を押すとPIC16F1827はLED3を消灯して「OK-2」を含む電文で返信します。

LED1はPIC16F827からシリアル通信でデータを取得すると点灯/消灯します。LED2は動作確認用として250ms毎に点灯/消灯を切り替えています。

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

シリアル通信に使用するピン以外MCLRを除いてDOピンとして設定しています。

動作結果

シリアルモニタによる動作確認の結果
シリアルモニタによる動作確認の結果

SW1とSW2を押すとArudino UNOから電文が送出されLED3が点灯/消灯することが確認できました。

返信電文はArduinoのシリアルモニタを使用して動作確認を行いました。「OK-1」を含む電文はSW1を押したときの文字列であり「OK-2」はSW2を押したときの文字列であり、PIC16F1827から電文による返信を受けていることが分かりました。

ソースコード全体

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

PIC16F1827のソースコード:

#include "mcc_generated_files/mcc.h"

#define TIME_OFF -1     //タイマーを使用しない場合
#define TIME_UP 0       //タイムアップ
#define TIM_RX_WAIT 20  //200ms
#define TIM_TX_WAIT 10  //100ms
#define TIM_LED_WAIT 25 //250ms
#define RING_SZ 64
#define SZ_MASK 0x3F
#define HEADER_A 'A'
#define HEADER_P 'P'
#define OFFSET_SZ 3
#define TX_SZ 7

typedef struct{
    uint8_t wp;
    uint8_t rp;
    uint8_t buf[RING_SZ];
}RING_MNG;

typedef struct{
    uint8_t cnt;
    uint8_t data[TX_SZ];
}TX_MNG;

typedef enum{	//モード
	COM_RX_WAIT = 0,
	COM_RCVDATA,
	COM_TX,
	COM_MAX
}COM_MODE_NO;

/* 変数定義 */
int8_t      timRxWait = TIME_OFF;
int8_t      timTxWait = TIME_OFF;
int8_t      timLedWait;
RING_MNG    picRcv;
COM_MODE_NO ComMode;
uint8_t     Rcvdata[RING_SZ];
TX_MNG      Txdata;

/* 関数プロトタイプ宣言 */
void eUsartmain(void);
uint8_t CalcSum(uint8_t *buf, uint8_t sz); 
void mainTimer(void); //タイマー0割り込みでの処理
void ReadPointerAdd(void);
bool RcvChk(void);
void TxDataSet(uint8_t no);
void mainApp(void);

/* Main application */
void main(void)
{
    SYSTEM_Initialize();
    TMR0_SetInterruptHandler(mainTimer);
    INTERRUPT_GlobalInterruptEnable();
    INTERRUPT_PeripheralInterruptEnable();

    while (1)
    {   
        eUsartmain();
        mainApp();
    }
}
/* タイマ管理関数 */
void mainTimer(void){
 
    if( timRxWait > TIME_UP ){
        --timRxWait; //タイマを更新
    }
    
    if( timTxWait > TIME_UP ){
        --timTxWait; //タイマを更新
    }
    
    if( timLedWait > TIME_UP){
        --timLedWait;
    }
}
/* メイン関数 */
void mainApp(void){
    
    while(EUSART_is_rx_ready())
    {
        picRcv.buf[picRcv.wp] = EUSART_Read(); //データをリード
        IO_RA7_Toggle();
        if( ++picRcv.wp >= sizeof(picRcv.buf)){ //次に保管する位置を更新
            picRcv.wp = 0;
        }
    }
    
    if( timLedWait == TIME_UP){
        timLedWait = TIM_LED_WAIT;
        IO_RA3_Toggle();
    }
}
/* シリアル通信メイン処理 */
void eUsartmain(void){
    int8_t  rxsz;
    uint8_t datsz;
    uint8_t allsz;
    uint8_t rp = picRcv.rp;  
    uint8_t i;
    uint8_t sumchk;
    uint8_t dat[2];
    
    if( timRxWait == TIME_UP){ //受信タイムアウトか
    	timRxWait = TIME_OFF;
	ReadPointerAdd(); 
        ComMode = COM_RX_WAIT;
    }
    
    switch( ComMode ){
        case COM_RX_WAIT:
            rxsz = picRcv.wp - picRcv.rp; //受信データ数の算出
    
            if( rxsz < 0 ){
                rxsz = rxsz + sizeof(picRcv.buf);
            }
            
            if( rxsz == 0 ){
		timRxWait = TIME_OFF;
	    }
            else{
                if( timRxWait == TIME_OFF ){
		    timRxWait = TIM_RX_WAIT; //受信タイムアウトをセット
		}
                
                if( picRcv.buf[ rp ] != HEADER_P  ){ //ヘッダーが一致するか
                    ReadPointerAdd(); //読み込み位置の更新
                }
                else{
                    if( rxsz >= 2 ){ //データ長分だけ獲得しているか
                        for( i = 0; i < 2; i++){//データサイズ算出のため仮おき
                            dat[i] = picRcv.buf[ rp ];
                            if(++rp >= sizeof(picRcv.buf) ) rp = 0;
                        }
                        
                        if( dat[1] > sizeof(picRcv.buf) - OFFSET_SZ ){
                            allsz = sizeof(picRcv.buf) - OFFSET_SZ;
                        }
                        else{
                            datsz = dat[1];
                            allsz = dat[1] + OFFSET_SZ; //header,datalengh,sumを含む
                        }
                        
                        if( rxsz >= allsz){ //確定したサイズ以上か
                            timRxWait = TIME_OFF;
                        
                            for(i=0; i < sizeof(Rcvdata); i++){
                                Rcvdata[i] = 0; //バッファのクリア
                            }
                        
                            for( i=0; i < allsz; i++ ){
                                Rcvdata[i] = picRcv.buf[picRcv.rp]; //一時保管したデータを移す
                                ReadPointerAdd(); //読み込み位置の更新
                            }
                       
                            sumchk = CalcSum(&Rcvdata[2], datsz); //チェックサムの計算
                            if( sumchk == Rcvdata[allsz-1]){ //チェックサムの確認
                                ComMode = COM_RCVDATA; //モードを遷移
                            }
                        }
		    }               
                }
            }
            break;
        case COM_RCVDATA: 
            if( RcvChk() ){
                ComMode = COM_TX;
                timTxWait = TIM_TX_WAIT;
            }
            else{
                ComMode = COM_RX_WAIT;
            }
            break;
        case COM_TX:
            if(EUSART_is_tx_ready()){ //送信ビジーでないか
                if ( Txdata.cnt < sizeof(Txdata.data)){
                    EUSART_Write(Txdata.data[Txdata.cnt]); //データを送信
                    ++Txdata.cnt; //送信データカウントを更新
                }
                else{ //全データの送信が完了
                    timTxWait = TIME_OFF;
                    ComMode = COM_RX_WAIT; //受信待ちに遷移
                }
            }  
                
            if( timTxWait == TIME_UP ){ //送信タイムアウトか
                timTxWait = TIME_OFF;
                ComMode = COM_RX_WAIT; //受信待ちに遷移
            }
            break;        
    }
}
/* 読み込み位置の更新 */
void ReadPointerAdd(void){
    
    if(++picRcv.rp >= sizeof(picRcv.buf) ){
        picRcv.rp = 0;
    }
}
/* チェックサムの計算 */
uint8_t CalcSum(uint8_t *buf, uint8_t sz ){
    uint8_t ret = 0;

    for(uint8_t i=0; i < sz; i++ ){
        ret += *buf;
        buf++; 
    }
    return ret;
}
/* 電文の処理 */
bool RcvChk(void){
    bool ret = false;
    
    if( Rcvdata[2] == 0x30 && Rcvdata[3] == 0x31 && 
        Rcvdata[4] == 0x32 && Rcvdata[5] == 0x33 ){
        IO_RB6_SetHigh();
        TxDataSet(1);
        ret = true;
    }
    else if( Rcvdata[2] == 0x33 && Rcvdata[3] == 0x32 &&
             Rcvdata[4] == 0x31 && Rcvdata[5] == 0x30 ){
        IO_RB6_SetLow();
        TxDataSet(2);
        ret = true;
    }
    return ret;
}
/* 送信データ生成 */
void TxDataSet(uint8_t no){
    
    Txdata.cnt = 0;
    Txdata.data[0] = HEADER_A;
    Txdata.data[1] = 4;
    Txdata.data[2] = 'O';
    Txdata.data[3] = 'K';
    Txdata.data[4] = '-';
    Txdata.data[5] = 0x30 + no;
    Txdata.data[6] = CalcSum(&Txdata.data[2], 4);
}
/**
 End of File
*/

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

Arduino UNOのスケッチ例:

#include <SoftwareSerial.h>

#define BASE_TIME 10
#define TIME_FILTER_MAX 1    //ベースタイマカウント値
#define TIM_RX_WAIT 5   //50ms
#define DI_NUM 4        //DIフィルタのサンプリング数
#define DI_MAX 2
#define TIME_OFF -1     //タイマーを使用しない場合
#define TIME_UP 0       //タイムアップ
#define PIN_DI1 7
#define PIN_DI2 8 
#define RING_SZ 64
#define HEADER_A 'A'
#define HEADER_P 'P'
#define OFFSET_SZ 3
#define CNT_INIT_MAX 10     //10ms×10 = 100ms

SoftwareSerial mySerial(10, 11); // RX, TX

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

typedef struct{
    uint8_t wp;
    uint8_t rp;
    uint8_t buf[RING_SZ];
}RING_MNG;

/* 変数定義 */
uint8_t buf[]={HEADER_P,0x04,0x30,0x31,0x32,0x33,0x00}; //電文送出パターン1
uint8_t buf2[]={HEADER_P,0x04,0x33,0x32,0x31,0x30,0x00}; //電文送出パターン2
uint32_t basemillis;
int8_t  timfilter;
int8_t  timRxWait = TIME_OFF;
bool btn1hold;
bool btn2hold;
FILT_DATA DiData;
RING_MNG ArudinoRcv;
uint8_t Rcvdata[RING_SZ];
uint8_t CntInit;  //初期化時のみ使用

/* 関数プロトタイプ宣言 */
uint8_t CalcSum(uint8_t *buf, uint8_t sz );
void  mainTimer(void);
void DiFilter(void);
void Rcvmain(void);
void ReadPointerAdd(void);

void setup() {
    Serial.begin(19200);
    mySerial.begin(19200);
    pinMode(PIN_DI1,INPUT_PULLUP);
    pinMode(PIN_DI2,INPUT_PULLUP);
    DiFilterInit(); //メイン処理前にDIを確定する
}

void loop() {

    mainTimer();
    DiFilter();
    Rcvmain();
  
    while( Serial.available()){
        ArudinoRcv.buf[ArudinoRcv.wp] = Serial.read(); //データリード
            
        if( ++ArudinoRcv.wp >= sizeof(ArudinoRcv.buf)){
            ArudinoRcv.wp = 0;
        }
    }

    if( DiData.di[0] == 0){
        if( btn1hold == false ){
            btn1hold = true;
            buf[6] = CalcSum(&buf[2],4); //sumの計算
            mySerial.write(&buf[0],7); //データ送信 
        }
    }
    else{
        btn1hold = false;
    }
    
    if( DiData.di[1] == 0){
        if( btn2hold == false){
            btn2hold = true;
            buf2[6] = CalcSum(&buf2[2],4); //sumの計算
            mySerial.write(&buf2[0],7);
        }
    }
    else{
        btn2hold = false;
    }
}
/* タイマ管理 */
void mainTimer(){

    if( millis() - basemillis > BASE_TIME ){
        basemillis = millis();

        if( timfilter > TIME_UP ){
            --timfilter; //タイマを更新
        }

        if( timRxWait > TIME_UP ){
            --timRxWait;
        }
    }
}
/* チェックサムの計算 */
uint8_t CalcSum(uint8_t *buf, uint8_t sz ){
    uint8_t ret = 0;

    for(uint8_t i=0; i < sz; i++ ){
        ret += *buf;
        buf++; 
    }
    return ret;
}
/* DIフィルタの初期化 */
void DiFilterInit(void){
  
    CntInit = CNT_INIT_MAX;
    while(CntInit > 0){  //0になるまでフィルタを実施
        timfilter = TIME_UP;
        DiFilter();         //DIフィルタ処理
        delay(10);     //10ms遅延させてDIフィルタ処理
        CntInit--;
    }    
}
/* DIフィルタ */
void DiFilter(void){
    uint8_t i;
    uint8_t j;
    bool boo[DI_MAX]={true,true};

    if( timfilter == TIME_UP ){
        timfilter = TIME_FILTER_MAX;

        DiData.buf[0][ DiData.wp ] = digitalRead(PIN_DI1);
        DiData.buf[1][ DiData.wp ] = digitalRead(PIN_DI2);

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

            if( boo[ i ]){ //比較して一致したら値を採用
                DiData.di[i] = DiData.buf[i][0]; 
            }
        }
        
        if( ++DiData.wp >= DI_NUM){
            DiData.wp = 0;
        }
    }
}
/* 受信データの処理 */
void Rcvmain(void){
    int8_t  rxsz;
    uint8_t sz;
    uint8_t datsz;
    uint8_t allsz;
    uint8_t rp = ArudinoRcv.rp;  
    uint8_t i;
    uint8_t sumchk;
    uint8_t dat[2];
    
    if( timRxWait == TIME_UP){
        timRxWait = TIME_OFF;
        ReadPointerAdd();    
    }
    
    rxsz = ArudinoRcv.wp - ArudinoRcv.rp; //受信データ数の算出
    
    if( rxsz < 0 ){
        rxsz = rxsz + sizeof(ArudinoRcv.buf);
    }
            
    if( rxsz == 0 ){
        timRxWait = TIME_OFF;
    }
    else{
        if( timRxWait == TIME_OFF ){
            timRxWait = TIM_RX_WAIT;
        }
                
        if(  ArudinoRcv.buf[ rp ] != HEADER_A ){ //ヘッダーの確認
            ReadPointerAdd(); 
        }
        else{
            if( rxsz >= 2 ){
                for( i = 0; i < 2; i++){//データサイズ算出のため仮おき
                    dat[i] = ArudinoRcv.buf[ rp ];
                    if(++rp >= sizeof(ArudinoRcv.buf) ) rp = 0;
                }
                                    
                if( dat[1] > sizeof(ArudinoRcv.buf) - OFFSET_SZ ){
                    allsz = sizeof(ArudinoRcv.buf) - OFFSET_SZ;
                }
                else{
                    datsz = dat[1];
                    allsz = dat[1] + OFFSET_SZ; //header,datalengh,sumを含む
                }

                if( rxsz >= allsz){
                    timRxWait = TIME_OFF;
                            
                    for(i=0; i < sizeof(Rcvdata); i++){
                        Rcvdata[i] = 0;
                    }
                            
                    for( i=0; i < allsz; i++ ){
                        Rcvdata[i] = ArudinoRcv.buf[ArudinoRcv.rp];
                        ReadPointerAdd();
                    }
    
                    sumchk = CalcSum(&Rcvdata[2], datsz); 
                    if( sumchk == Rcvdata[allsz-1]){ //チェックサムが一致するか
                        Serial.write(&Rcvdata[2],4); //文字列部分をシリアルモニタに表示
                        Serial.println("");
                    }
                }
            }
        }
    }
}
/* 読み込み位置の更新 */
void ReadPointerAdd(void){
    
    if(++ArudinoRcv.rp >= sizeof(ArudinoRcv.buf) ){
        ArudinoRcv.rp = 0;
    }
}

関連リンク

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

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

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

テックジム-将来のためにプログラミングを学ぶ

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

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