こんにちは、ENGかぴです。
Arudinoの標準ライブラリであるSerialを使用すると外部機器とシリアル通信ができます。ESP32-WROOM-32Eは複数のシリアル通信ポートを持っているためシリアルモニタと外部機器と接続するシリアル通信を区別することができます。
ESP32-WROOM-32Eのシリアル通信を使って電文を外部機器であるPICマイコンに送信しPICマイコンを操作します。PICマイコンはPIC16F1827を使用し下記記事と同じ条件とします。
ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。また、AE-SHT35(秋月電子)を使用しています。
ESP32-WROOM-32Eを使って動作確認したことをまとめています。
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
シリアル通信を実装する
ESP32-WROOM-32EにおいてArduino標準ライブラリに搭載されているシリアル通信は3ポート分準備されています。2024年6月中旬でのデフォルトピンは以下の通りです。
シリアルポート | GPIO番号(シルク印刷) |
---|---|
Serial | 送信:1ピン(TX) 受信:3ピン(RX) |
Serial1 | 送信:27ピン(27) 受信:26ピン(26) |
Serial2 | 送信:25ピン(25) 受信:4ピン(4) |
今回はシリアルモニタ用にSerialと外部機器と通信するためSerial2を使用します。ライブラリの更新によってデフォルトピンが変更になることがあるので注意が必要です。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
標準ライブラリを使用する方法
void setup() {
Serial.begin(115200);
while(!Serial); //ポートを開くまでの待ち
Serial1.begin(19200);//デフォルトピンを使用する場合
Serial1.begin(119200,SERIAL_8N1,18,19);//ピンを指定して使用する場合
Serial2.begin(19200);//デフォルトピンを使用する場合
Serial2.begin(19200,SERIAL_8N1,16,17);//ピンを指定して使用する場合
while(!Serial2); //ポートを開くまでの待ち
}
シリアル通信の起動はbegin()で行います。第1引数に通信速度であるボーレート値を指定します。
引数にシリアル通信のボーレートを入力します。ボーレートは任意でも良いのですが、マイコンのクロック周波数によって誤差が出るので注意が必要です。
ピンを指定して初期化する場合は第2引数以降を指定します。第2引数はデータ長及びパリティを指定します。デフォルトはSERIAL_8N1(データビット8、パリティなし、ストップビット1)です。第3引数にRX(受信)に使用するピンを指定します。第4引数にTX(送信)に使用するピンを指定します。
Arduino IDEで選択できるボーレートから選択すると多くの場合問題になりませんが、ボーレートを早くしすぎると配線の長さや周辺回路の部品などの影響で通信できなくなることがあるので外部機器のクロックを含めて検討する必要があります。
begin()を発行した後ポートが開く(初期設定待ち)までループさせて確実に設定が完了するまで待機させることもありますが、初期化時に他で遅延させている場合やbegin()の発行から直ちにシリアル通信をしない場合は実装しなくても特に問題になることはありません。
【クリエイターズファクトリー】卒業がない!挫折する心配なし!Webスクール説明会申し込み
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.print(0x10)はバイナリデータでは16であるためモニタ上では16が表示されます。Serial.write(0x30)の場合は0x30がアスキーコード(テキストデータ)では0にあたるためモニタには0が表示されます。
広告
送受信に使用する電文
ESP32-WROOM-32Eから送出した電文でPIC16F1827を操作します。電文の構成は任意で構成することができますが双方で使用する電文の構成を同じ構成にすると管理しやすくなります。
電文の構成
電文の構成は1バイト目をヘッダーとしデータの先頭であることを通知します。2バイト目には3バイト以降にセットするデータのサイズを示すデータ長をセットします。電文の最終バイトにはデータ長以降のデータの総和を計算したチェックサムを付加します。
チェックサムはデータ部分の総和をとり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(Serial2のRX2、TX2のデフォルトピンがライブラリの変更により変わっているためbegin()関数でデータビット及びピンを指定して初期化を行っています。)を使用して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);//デフォルトピン変更の為
Serial2.begin(19200,SERIAL_8N1,16,17);//ピン番号を指定して初期化
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;
}
Serial2のRX2、TX2のデフォルトピンがライブラリの更新により記事作成時より変わっているためbegin()関数でデータビット及びピンを指定して初期化を行っています。
PIC16F1827のソースコードは下記記事のものを使用しています。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
最後まで、読んでいただきありがとうございました。