ESP32-WROOM-32Eでシリアル通信を実装する

組み込みエンジニア

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

Arudinoの標準ライブラリであるSerialを使用すると外部機器とシリアル通信ができます。ESP32-WROOM-32Eは複数のシリアル通信ポートを持っているためシリアルモニタと外部機器と接続するシリアル通信を区別することができます。

ESP32-WROOM-32Eのシリアル通信を使って電文を外部機器であるPICマイコンに送信しPICマイコンを操作します。PICマイコンはPIC16F1827を使用し下記記事と同じ条件とします。

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

ESP32-WROOM-32Eの開発環境はArduino IDEを使用しています。またESP32-WROOM-32E開発ボード(秋月電子)及びAE-SHT35(秋月電子)を使用しています。

シリアル通信を実装する

ESP32-WROOM-32EにおいてArduino標準ライブラリに搭載されているシリアル通信は3ポート分準備されています。

シリアルポートGPIO番号(シルク印刷)
Serial送信:1ピン(TX) 受信:3ピン(RX)
Serial1送信:10ピン(D3) 受信:9ピン(D2)
Serial2送信:17ピン(17) 受信:16ピン(16)
Arduinoライブラリで搭載されているシリアルポート

今回はシリアルモニタ用にSerialと外部機器と通信するためSerial2を使用します。

2021年8月現在ではコンパイルエラーにはなりませんがSerial1を指定するとモジュールのソフトが暴走して繰り返しリセットを繰り返してしまうため使用できません。

標準ライブラリを使用する方法

void setup() {

  Serial.begin(115200);
  while(!Serial); //ポートを開くまでの待ち
  //Serial1.begin(19200); 実装すると暴走するためコメントアウト
  Serial2.begin(19200);
  while(!Serial2); //ポートを開くまでの待ち
}

シリアル通信の起動はbegin()で行います。第1引数に通信速度であるボーレート値を指定します。

引数にシリアル通信のボーレートを入力します。ボーレートは任意でも良いのですが、マイコンのクロック周波数によって誤差が出るので注意が必要です。

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

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

begin()を発行した後ポートが開く(初期設定待ち)までループさせて確実に設定が完了するまで待機させることもありますが、初期化時に他で遅延させている場合やbegin()の発行から直ちにシリアル通信をしない場合は実装しなくても特に問題になることはありません。

SerialとSerial2の使い分け

Serialはシリアルモニタに表示したい場合に使用します。Serial2で受信したデータをシリアルモニタに表示したい場合など使い分けることで効率よくデバッグできるようになります。

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

外部機器からシリアル通信のデータを受信するとSerial2.available()の条件を満たすためSerial2.read()によってSerial2のデータを読み出します。受信したデータをSerial.write()によってシリアルモニタに表示しています。

Serialでよく使うAPI

Serialでよく使うAPIをまとめました。Serialで表記していますがSerial2でも同様に使用できます。

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

Serial.print(0x10)はバイナリデータでは16であるためモニタ上では16が表示されます。Serial.write(0x30)の場合は0x30がアスキーコード(テキストデータ)では0にあたるためモニタには0が表示されます。

送受信に使用する電文

ESP32-WROOM-32Eから送出した電文でPIC16F1827を操作します。電文の構成は任意で構成することができますが双方で使用する電文の構成を同じ構成にすると管理しやすくなります。

電文の構成

電文の構成
電文の構成

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

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

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

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

電文を受信する

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

Serial2でデータを受信するとavailable()が条件を満たし内部の処理が行われます。read()で受信したデータを読み込みesp32Rcvに格納します。esp32Rcv.wpを更新して次に格納する場所を指定しながらデータを一時保管していきます。

While()で受信データを確認しているのはメイン処理に来るたびに受信しているデータ分だけ確実に取得するためです。if()で確認するよりもタイミングによっては受信データの格納が早くなることがあります。

受信データのチェック

受信データが電文の構成に合致しているかをチェックします。

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

最初にヘッダーを確認します。ESP32-WROOM-32EはPIC16F1827からヘッダーとして’A’(0x41)を受信します。ヘッダーが一致した場合は2バイト目のデータ長が受信できているかの確認を行います。

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

動作確認

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

シリアル通信の動作確認回路
シリアル通信の動作確認回路

ESP32-WROOM-32EはSerial2を使用してPIC16F1827と通信を行います。SW1を押すとESP32-WROOM-32Eから電文を送出します。SW1を押すごとに電文を切り替えて送信します。

PIC16F1827はESP32-WROOM-32Eから受信した電文に応答してLED1を点灯/消灯します。LED1の点灯/消灯に連動して点灯させた場合は「OK-1」、消灯させた場合は「OK-2」を含む電文をESP32-WROOM-32Eに返信します。

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

SW1を押すとLED1が点灯/消灯することを確認しました。またLED1の点灯/消灯に連動してシリアルモニタに返信データが表示されることが確認できました。

ソースコード全体

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

ESP32-WROOM-32Eのスケッチ例:

#define HEADER_A 'A'
#define HEADER_P 'P'
#define RING_SZ 64
#define DIFILT_MAX 4
#define TIME_UP 0
#define TIME_OFF -1
#define TIM_DIFILT 1
#define TIM_RX_WAIT 5
#define OFFSET_SZ 3
#define PIN_DI_SW 5

typedef struct DIFILT{
  uint8_t wp;
  uint8_t buf[DIFILT_MAX];
  uint8_t di1;
};

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

/* 変数宣言 */
DIFILT diData;
RING_MNG esp32Rcv;
uint8_t Rcvdata[RING_SZ];
hw_timer_t *timer = NULL;
int16_t timDifilter = TIME_OFF;
int8_t  timRxWait = TIME_OFF;
uint8_t buf[]={HEADER_P,0x04,0x30,0x31,0x32,0x33,0x00}; //電文送出パターン1
uint8_t buf2[]={HEADER_P,0x04,0x33,0x32,0x31,0x30,0x00}; //電文送出パターン2
bool btn1hold;
bool btncnt;

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

void setup() {
  uint8_t i;

  pinMode(PIN_DI_SW,INPUT_PULLUP);
  
  Serial.begin(115200);
  Serial2.begin(19200);

  timer = timerBegin(0,80,true); //クロック80MHzを80で分周するので1usが最小分解能
  timerAttachInterrupt(timer, &onTimer,true);
  timerAlarmWrite(timer,10000,true);  //コールバックするタイミングを指定 10msごとにonTimer()がコール
  timerAlarmEnable(timer); //timer通知を開始

  timDifilter = TIM_DIFILT;
  while( i < 10){
    DiFilter();
    delay(10);
    i++;
  }
}

void loop() {

  mainApp();
  DiFilter();
  Rcvmain();
}
/* タイマ割り込みによってコールする関数 */
void IRAM_ATTR onTimer(void){ //IRAMセクションに割り当てるとRAM領域となり高速となる
  mainTimer();
}

/* タイマ管理 */
void mainTimer(void){

  if( timDifilter > TIME_UP ){
    --timDifilter;
  }
}
/* メイン関数 */
void mainApp(void){

  if( diData.di1 == 0){
    if( btn1hold == false ){
      btn1hold = true;

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

  while( Serial2.available()){
    esp32Rcv.buf[esp32Rcv.wp] = Serial2.read(); //データリード
            
    if( ++esp32Rcv.wp >= sizeof(esp32Rcv.buf)){
      esp32Rcv.wp = 0;
    }
  }
}
/* DIフィルタ */
void DiFilter(void){
  bool boo = true;
  uint8_t i;

  if( timDifilter == TIME_UP ){
    timDifilter = TIM_DIFILT;

    diData.buf[diData.wp] = digitalRead(PIN_DI_SW);

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

    if(boo){ //データがすべて一致なので採用する
      diData.di1 = diData.buf[0];
    }
    if( ++diData.wp >= sizeof(diData.buf)){
      diData.wp = 0;
    }
  }
}
/* 受信データの処理 */
void Rcvmain(void){
  int8_t  rxsz;
  uint8_t sz;
  uint8_t datsz;
  uint8_t allsz;
  uint8_t rp = esp32Rcv.rp;  
  uint8_t i;
  uint8_t sumchk;
  uint8_t dat[2];
    
  if( timRxWait == TIME_UP){
    timRxWait = TIME_OFF;
    ReadPointerAdd();    
  }
    
  rxsz = esp32Rcv.wp - esp32Rcv.rp; //受信データ数の算出
    
  if( rxsz < 0 ){
    rxsz = rxsz + sizeof(esp32Rcv.buf);
  }          
  if( rxsz == 0 ){
    timRxWait = TIME_OFF;
  }
  else{
    if( timRxWait == TIME_OFF ){
      timRxWait = TIM_RX_WAIT;
    }

    if( esp32Rcv.buf[ rp ] != HEADER_A ){ //ヘッダーの確認
      ReadPointerAdd(); 
    }
    else{
      if( rxsz >= 2 ){
        for( i = 0; i < 2; i++){//データサイズ算出のため仮おき
          dat[i] = esp32Rcv.buf[ rp ];
          if(++rp >= sizeof(esp32Rcv.buf) ) rp = 0;
        }
                                    
        if( dat[1] > sizeof(esp32Rcv.buf) - OFFSET_SZ ){
          allsz = sizeof(esp32Rcv.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] = esp32Rcv.buf[esp32Rcv.rp];
            ReadPointerAdd();
          }

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

PIC16F1827のソースコードは下記記事のものを使用しています。

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

関連リンク

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

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

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

CODEGYM Monthly コーチング主体のプログラミングスクール (無料相談)

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

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