PR

Raspberry Pi PicoでSerialライブラリを使用する

組み込みエンジニア
本記事はプロモーションが含まれています。

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

Raspberry Pi PicoでSerialライブラリを使用すると外部機器とシリアル通信ができます。Arduino環境のシリアルモニターにSerialクラスを使用し、外部機器にSerial1クラスを使用するなど使い分けができます。外部機器には下記記事で使用したPIC16F1827を使用します。

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

Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。Grove Buttonを2つ使用して通信パターンを切り替えて、PIC16F1827(以下PICマイコンとする)と通信します。

Picoでシリアル通信する方法

Picoはシリアルモニター用と外部機器用のシリアル通信が別々に実装されています。それぞれのSerialのクラスとピンは下記の通りです。

シリアルのクラス用途
Serial(SerialUSB)シリアルモニタ専用:ピンから出力しない
Serial1外部機器へのシリアル通信 GP0ピン:TX GP1ピン:RX
PicoのSerialクラス

SerialはSerialUSBと表記してもSerialと同じ動作になるためどちらの表記でも問題ありません。Serial1クラスはデフォルトのピン番号です。

Serialライブラリを使用する方法

SerialとSerial1は同様の使用方法で使用することができます。以下ではSerialクラスのメンバー関数及びSerial1クラスのメンバー関数を赤文字で表記します。SerialはArduino環境では標準ライブラリなのでインクルードの必要はありません。

void setup() {

    Serial.begin(115200); //シリアルモニタ用
    //while(!Serial ){ } //ポートを開くまでの待ち
    Serial1.begin(19200); //外部機器との通信用
}

begin()関数でシリアル通信のボーレートを指定します。引数にシリアル通信のボーレートを指定します。

Arduino IDEで選択できるボーレートから選択すると多くの場合問題になりませんが、ボーレートを早くしすぎると配線の長さや周辺回路の部品などの影響で通信できなくなることがあるので外部機器のクロックを含めて検討する必要があります。

ボーレート以外の設定は特に指定しない場合はデータ長は8ビット・パリティなしの条件で動作します。

begin()関数の後で初期化待ちで待機させることもありますが、直ちにシリアル通信をしない場合は実装する必要はありません。

Serial1のボーレートは外部機器のPICマイコンに合わせるため19200bpsを指定しています。

PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!

SerialとSerial1の使い分け

Serialはデバッグ用の文字列表示に使用します。Serial1で受信したデータをSerialでシリアルモニターに表示すると効率よくデバッグできるようになります。

void loop() {
    //受信データをモニタする例
    while( Serial1.available()){ //Serial1に受信データがあるか
        data = Serial1.read(); //Serial1データを読み出し
        Serial.write(data); //シリアルモニタにSerial1から読み出したデータをモニタ表示
    }
}

Serial1クラスのavailable()関数で受信したデータ数が取得できるため、Serial1クラスのread()関数で受信データを読み出します。このときSerialクラスのwrite()関数で受信したデータをシリアルモニターに表示します。

Serialクラスでよく使うメンバー関数

Serialクラスでよく使うメンバー関数をまとめました。Serialで表記していますがSerial1でも同様に使用できます。

メンバー関数説明使用例
write()バイナリデータを送信するSerial.write(0x30)
write(引数1,引数2)引数1に対象バッファのアドレス
引数2に送信するバイト数
Serial.write(&buf,10)
read()受信データを1バイトずつ取り出すavailable()とセットで使う
buf = Serial.read()
available()受信したデータ数を確認し、データがある場合
は0よりも大きな値となる。
if(Serial.available() ){ }
print()文字列として送信するSerial.print(“Test”)
println()文字列に改行コードを付加して送信するSerial.println(“Test”)
Serialクラスでよく使うコマンド

Serial.write(0x30)の場合は、バイナリデータの0x30が送信されます。0x30をアスキーコード(テキストデータ)に置き換えると0になるためシリアルモニターに0が表示されます。

Serial.print(0x30)の場合は10進数の48が表示されます。16進数で表示する場合はSerial.print(0x30,Hex)のように指定すると、30が表示されます。

PR:(即戦力のスキルを身に着ける:DMM WEBCAMP 学習コース(はじめてのプログラミングコース))

電文を受信する

while( Serial1.available()){
    picoRcv.buf[picoRcv.wp] = Serial1.read(); //データリード
    
    if( ++picoRcv.wp >= sizeof(picoRcv.buf)){
        picoRcv.wp = 0;
    }
}

Serial1のavailable()関数で受信したデータ数を確認します。受信したデータが存在するとavailable()関数の戻り値が1以上になるため、while()で受信データを取得します。

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

read()関数で受信データを読み込んで、一時保存用の変数に格納します。格納する配列の位置の更新を行って次の受信データを読み込みます。

PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル

受信データのチェック

一時保管した受信データが電文の構成(送受信に使用する電文を参照)と一致するかチェックします。

if( picoRcv.buf[ rp ] != HEADER_A){ //ヘッダーが一致するか
    ReadPointerAdd(); //読み込み位置の更新
}
else{
    if( rxsz >= 2 ){ //データ長分だけ獲得しているか
        //データ長を確定する             
        if( rxsz >= allsz){ //確定したサイズ以上か                
            for( i=0; i < allsz; i++ ){
                Rcvdata[i] = picoRcv.buf[picoRcv.rp]; //一時保管したデータを移す
                ReadPointerAdd(); //読み込み位置の更新
            }
                       
            sumchk = CalcSum(&Rcvdata[2], datsz); 
            if( sumchk == Rcvdata[allsz-1]){ //チェックサムが一致するか
                Serial.write(&Rcvdata[2],datsz); //文字列部分をシリアルモニタに表示
                Serial.println("");
            }
        }
    }               
}

PICマイコンから受信したデータが電文の構成であるかを確認するため、ヘッダー(’A’)を確認します(1行目)。ヘッダーが一致した場合は2バイト目のデータ長が受信できているかの確認(5行目)を行います。

2バイト目のデータ長によって電文全体のサイズが確定できるため受信データを電文保管用のバッファ(Rcvdata[])に移し替えます。(8~11行目)

Rcvdata[]のチェックサムを計算し、受信データのチェックサムと一致するか確認します。(13~14行目)チェックサムが一致すると受信データをシリアルモニターに表示します(15~16行目)。

スポンサーリンク

データの送信

uint8_t buf[]={HEADER_P,0x04,0x30,0x31,0x32,0x33,0x00}; //電文送出パターン1

buf[6] = CalcSum(&buf[2],4); //sumの計算
Serial1.write(&buf[0],7); //データ送信 

送信するデータの配列を準備して送信します。buf[]配列の最後尾のデータはSUM値を計算してセットします。

write()関数でbuf[]配列のデータを送信します。第1引数に配列のアドレスを指定します。第2引数に送信するデータ数を指定します。

送受信に使用する電文

PICマイコンのソースコードは下記記事のソースコードを流用するため電文を同一の構成にします。

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

このソースコードはArduino UNOを想定した作りになっていますが、自作の電文なので同一の電文構成であれば使用することできます。

電文の構成
電文の構成

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

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

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

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

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

動作確認

動作確認用の回路図
動作確認用の回路図

Picoを拡張基板のGrove Shield for Pi Picoに挿入します。GP26はデフォルトでA0ピンですがGrove Button(P)のDIとし、SW1のボタンを押すとPICマイコンに電文パターン1を送信します。PICマイコンは電文を受信するとLED2を点灯します。

GP16はGrove Button(P)のDIとし、SW2のボタンを押すとPICマイコンに電文パターン2を送信します。PICマイコンは電文を受信するとLED2を消灯します。

SW1及びSW2を押した時のシリアルモニターとPICマイコンの状態を確認しました。

シリアルモニターの結果
シリアルモニターの結果

電源をONすると「Raspberry Pi Pico」「Arduino IDE」「Serial Test」をシリアルモニターに表示します。SW1ボタンを押して電文を送信するとPICマイコンから電文の応答を受信しシリアルモニターに「OK-1」の文字列が表示されました。同様にSW2ボタンを押すと「OK-2」の文字列が表示されました。

シリアル通信によるLED点灯/消灯の確認
シリアル通信によるLED点灯/消灯の確認

SW1を押すとPicoの電文でLED2(赤色)が点灯し、SW2を押すとLED2(赤色)が消灯することが確認できました。

スポンサーリンク

ソースコード全体

ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。

#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 LED_ONOFF 50
#define PIN_DI1 A0
#define PIN_DI2 16
#define PIN_DO1 25
#define RING_SZ 64
#define HEADER_A 'A'
#define HEADER_P 'P'
#define OFFSET_SZ 3
#define CNT_INIT_MAX 10     //10ms×10 = 100ms

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;
int8_t timled = LED_ONOFF;
int8_t ledcnt;
bool btn1hold;
bool btn2hold;
FILT_DATA DiData;
RING_MNG picoRcv;
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(115200);
    Serial1.begin(19200);
    pinMode(PIN_DI1,INPUT_PULLUP);
    pinMode(PIN_DI2,INPUT_PULLUP);
    pinMode( PIN_DO1, OUTPUT);
    DiFilterInit(); //メイン処理前にDIを確定する

    delay(3000);
    Serial.println("Raspberry PI Pico");
    Serial.println("Arduino IDE");
    Serial.println("Serial Test");
}

void loop() {

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

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

  if( timled == TIME_UP ){
    timled = LED_ONOFF;
    ++ledcnt;

    if( ledcnt % 2 ){
      digitalWrite(PIN_DO1, HIGH);
    }
    else{
      digitalWrite(PIN_DO1, LOW);
    }
  }
}
/* タイマ管理 */
void mainTimer(){

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

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

        if( timRxWait > TIME_UP ){
            --timRxWait;
        }

        if( timled > TIME_UP ){
        timled--;
        }
    }
}
/* チェックサムの計算 */
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 = picoRcv.rp;  
    uint8_t i;
    uint8_t sumchk;
    uint8_t dat[2];
    
    if( timRxWait == TIME_UP){
        timRxWait = TIME_OFF;
        ReadPointerAdd();    
    }
    
    rxsz = picoRcv.wp - picoRcv.rp; //受信データ数の算出
    
    if( rxsz < 0 ){
        rxsz = rxsz + sizeof(picoRcv.buf);
    }
            
    if( rxsz == 0 ){
        timRxWait = TIME_OFF;
    }
    else{
        if( timRxWait == TIME_OFF ){
            timRxWait = TIM_RX_WAIT;
        }
                
        if(  picoRcv.buf[ rp ] != HEADER_A ){ //ヘッダーの確認
            ReadPointerAdd(); 
        }
        else{
            if( rxsz >= 2 ){
                for( i = 0; i < 2; i++){//データサイズ算出のため仮おき
                    dat[i] = picoRcv.buf[ rp ];
                    if(++rp >= sizeof(picoRcv.buf) ) rp = 0;
                }
                                    
                if( dat[1] > sizeof(picoRcv.buf) - OFFSET_SZ ){
                    allsz = sizeof(picoRcv.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] = picoRcv.buf[picoRcv.rp];
                        ReadPointerAdd();
                    }
    
                    sumchk = CalcSum(&Rcvdata[2], datsz); 
                    if( sumchk == Rcvdata[allsz-1]){ //チェックサムが一致するか
                        Serial.write(&Rcvdata[2],datsz); //文字列部分をシリアルモニタに表示
                        Serial.println("");
                    }
                }
            }
        }
    }
}
/* 読み込み位置の更新 */
void ReadPointerAdd(void){
    
    if(++picoRcv.rp >= sizeof(picoRcv.buf) ){
        picoRcv.rp = 0;
    }
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

広告
マイベスト3年連続1位を獲得した実績を持つ実践型のプログラミングスクール

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

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