トワイライト(TWELITE)とArduino間でシリアル通信する

組み込みエンジニア

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

ZigBeeモジュールであるトワイライト(TWELITE)はシリアル通信機能があり、センサーのデータ値を表示させることができます。Arduino Unoとシリアル通信を行いArduinoのシリアルモニターに表示して確認しました。

トワイライトの相手のマイコンはArduino UNOを使用します。Arduino開発環境であるArduino IDEやシリアル通信の方法については下記リンクでまとめています。

Arduino IDEをインストールしてシリアル通信を行う

トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。

トワイライト(TWELITE)のソフト開発と無線通信でできること

以下Arduino UNOをArduinoと表記します。シリアル通信の方法だけでなく電文の作り方や受信データの扱い方についてまとめています。

Serial機能を実装する

トワイライトの開発環境であるMWSTAGE上ではSerial通信機能は自動でスタートするため特に設定する項目はありません。今回はシリアル通信のデータの受信に関するAPIであるSerialParserについて使用していきます。下記リンクに使用方法の説明があります。

モノワイヤレス社ーSerialParser-serparser

SerialPaserを実装する

今回はserparser_local<N>の使い方を基に実装していきます。既存バッファしないのは受信と送信を非同期に動作させたい場合に分けておいたほうが良い場合があるからです。

void setup() {

    SerialParser.begin(PARSER::BINARY,128); //バイナリ形式を選択 128バイト分の領域を確保
    Serial << "start" << mwx::crlf << mwx::flush;
}

書式種別はアスキー形式が推奨されていますが、今回はバイナリー方式で通信させます。アスキー形式にするとバイナリデータを2バイト分のデータで表現することになるので通信量が増えてしまうからです。

通信量が気にならない場合はアスキー形式のほうが文字列として表現されるため使用しやすいこともあるので用途に応じて使い分けるとよいでしょう。

例)0x65(バイナリ形式)はアスキー形式にすると0x36 0x35のように2バイトのバイナリ形式のデータとして送信されます。

void loop() {

    while(Serial.available()){
        if(SerialParser.parse(Serial.read())){
            auto&& b = SerialParser.get_buf();
            Serial << "TWE-rcv-OK" << mwx::crlf << mwx::flush;
        }
    }
}

メインループ内にSerial.available()を実装し受信データを取得します。while文をif文にしても良いのですが、複数バイト受信していた時に効率よくデータを受信するためwhile文としています。

SerialParserのAPI内にリードした受信データを入れることでAPI内部でヘッダの照合とデータ長に対するチェックサムの計算を行いチェックサムが一致したらデータ部のデータが一時変数bに格納されるようになっています。ヘッダからチェックサムまでの値に問題があるとデータが破棄されます。

バイナリ形式で受信するためにはバイナリ形式の定義に合うようにデータを作成する必要があります。

SerialPaserの動作を確認する

シリアル通信確認の回路図
シリアル通信確認の回路図

トワイライトとArduinoを回路図のように配線してシリアル通信の確認を行います。トワイライトとArduinoは電源系統が異なるので配線するときは注意が必要です。

トワイライトはDC3.3VでありArduinoはDC5Vです。ArduinoのTXをそのままトワイライトに入力すると過電圧となり故障の原因になります。DC5Vが直接印加されないように抵抗で分圧してDC3V程度に減圧しています。

ArduinoはHレベルを2.7V程度まで認識できるためトワイライトのTXはそのままArduinoのRXに入力することができます。SW1をONするとLOWレベルとなりArduinoからトワイライトに向けてシリアル通信データを送信します。Arduinoのソースコードは以下の通りです。

#define PIN_DI1 7

bool flg= false;
uint8_t buf[]={0xA5,0x5A,0x80,0x08,0x00,0xA0,0x13,0x01,0xFF,0x12,0x34,0x56,0x3D};
//TWELITEのバイナリ形式の例に倣ってbufに値を入れている

void setup() {

    Serial.begin(115200);
    pinMode(PIN_DI1,INPUT_PULLUP);
}

void loop() {

    if( !digitalRead(PIN_DI1)){
        if(flg ){
            flg = false;
      
            for(uint8_t i = 0; i < sizeof(buf); i++ ){
                Serial.write(buf[i]); //バイナリで送信するときはwrite
            }
            Serial.println(); //改行を入れたい
        }
    }else{
        flg = true;
    }

    while(Serial.available()){ //TWELITEからの受信待ち
        Serial.write(Serial.read());
    }
}

トワイライトのバイナリ形式の定義の例に従って変数を準備してSerial.write()で送信するソフトです。Serial.available()でトワイライトからの返信を待ちます。

シリアルモニタで通信を確認
シリアルモニタで通信を確認

シリアルモニタを見ると文字化けして見えるのがバイナリデータです。トワイライトはSerial.Parserが電文を受け取ると「TWE-rcv-OK」を返信するようにしておりシリアルモニタ上でも確認できたことからArduino対トワイライト間の通信ができていることが分かります。

Arduino UNOのシリアルピンはシリアルモニタと兼用しているためトワイライトのSerialPaserの条件を満たす文字をシリアルモニタに出力して確認しようとすると無限に通信を繰り返してしまうため注意が必要です。

シリアル通信の電文の送信と受信(応用編)

トワイライトとArduinoの通信をTWELITEのバイナリ形式の例に倣って送信/受信を確認しましたが、応用としてバイナリ形式の電文を作りトワイライトとArduinoの送受信を行う方法について考えてみます。

Arduino側とトワイライト側で同様の処理を行っているものはArduino側のプログラムで説明しています。トワイライトのプログラムはソースコード全体を参照してください。

バイナリ形式のデータの作り方

トワイライトのバイナリ形式の電文
トワイライトのバイナリ形式の電文

トワイライトのバイナリ形式の電文の詳細はモノワイヤレス社のHPの通りです。ヘッダ部分は固定値となりますが、データ長やデータ部はユーザーがセットする任意の値となります。チェックサムはデータ部のバイトをXORした値であるため計算して電文として付加する必要があります。

void SerialTxSet(uint8_t cmd){
    uint8_t *adrs;
    uint8_t allsz = 0;

    txdata.header[0] = HEADER1; //0xA5
    txdata.header[1] = HEADER2; //0x5A
    txdata.header[2] = HEADER3; //0x80
    txdata.sz = sizeof(buf)+1; //txdata.buf[0]分で+1
    txdata.buf[0] = cmd;
  
    for(uint8_t i=0; i < txdata.sz; i++){
        txdata.buf[i+1] = buf[i]; //buf[0]分をオフセット
    }
  
    txdata.sum = CalcSum(&txdata.buf[0],txdata.sz);
    allsz = sizeof(txdata.header)+ sizeof(txdata.sz)+ txdata.sz;

    adrs =&txdata.header[0];
    for(uint8_t i = 0; i < allsz; i++ ){
        Serial.write(*adrs);
        adrs++;
    }
    Serial.write(txdata.sum);
}

バイナリ形式の送信電文の例を示しています。txdataという送信用の変数を宣言し定義に合うように値をセットしています。txdata.sumにデータ部のチェックサムを計算してセットしています。データをセットした後はSerial.write()でバイナリデータを送信します。チェックサムは以下の関数で計算しています。

uint8_t CalcSum(uint8_t *buf, uint8_t sz=0){
    uint8_t ret = 0;

    for(uint8_t i=0; i < sz; i++ ){
        ret = ret ^ (*buf);
        buf++; 
    }
    return ret;
}

データ部分の先頭アドレスとサイズを引数として指定しfor文でサイズ分だけXOR(演算子は^)しています。引数のポインタを更新しながら繰り返しているのでtxdata.buf[0]の値を先頭に1バイトずつずらしながらからサイズ分だけのXORをとることになります。

トワイライト側の受信と送信の処理

トワイライトがArduinoからデータを受信するとSerialParserによって電文に問題なければヘッダー部分を除いたデータ部分が取得できます。取得したデータをもとに返信データを作成してArduinoへ送信します。

void RcvData(uint8_t *b ){

    memcpy(&rcvdata.buf[0],b,sizeof(rcvdata));

    switch (rcvdata.buf[0]){
    case 0:
        SerialTxSet(0);
        break;
    case 1:
        SerialTxSet(1);
        break;
    default:
        break;
    }
}

SerialParserによって取得したデータ部分に関する処理としてRcvData()関数を実装しています。データの先頭の値によって返信データを区別するため分岐をいれています。

トワイライトも送信電文の関数としてArduinoと同様にSerialTxSet()を実装しますが、cmdによって返信する内容を変更するようにします。

for(uint8_t i=0; i < txdata.sz; i++){
    if(cmd == 0){
        txdata.buf[i+1] = sbuf[i]; //txdata.buf[0]分をオフセット
    }
    else if (cmd == 1){
        txdata.buf[i+1] = sbuf1[i]; //txdata.buf[0]分をオフセット
    }
    else{
        txdata.buf[i+1] = 0xff;
    }
}

cmdが0の場合はsbuf[]の内容をセットし1の場合はsbuf1[]の内容をセットするようにしています。それ以外の考え方はArduinoのプログラム例と同じです。

基本的にモジュール間の通信フォーマットを統一した方がよいでしょう。通信ポートが複数ある場合区別しやすくなります。

Arduino側の受信処理

リングポインタの説明
リングポインタの説明

Arduinoは受信したデータを処理せずに一旦すべて受信し通信フォーマットの定義に従っているかの確認を行います。

while( Serial.available()){
    rxarduino.buf[rxarduino.wp] = Serial.read();

    if( ++rxarduino.wp >= RING_SZ ){ //次にデータを入れる場所を更新
        rxarduino.wp = 0;
    }
}

受信したデータの確認はRxDataChk()という関数を作成して処理を行います。

rxsz = rxarduino.wp - rxarduino.rp; //受信データ数の算出

if( rxsz < 0 ){
    rxsz = rxsz + RING_SZ;
}

リングポインタの位置関係から受信したデータ数を確認します。データ数が負の値になったときはwpがリングの最大値に達したため0に戻っているためリングの最大数を加算することで受信データ数が分かります。次にヘッダの確認とデータ長部分から全体の電文のデータ数を確認します。

if( chkdat[0]== HEADER1 && chkdat[1]== HEADER2 && chkdat[2]== HEADER3 ){
    sz = chkdat[3] & 0x3F; //szが63を超えないようにする(データ長がおかしい場合の対策)
    allsz = RXCHK_SZ + sz + 1; //+1はSUMで受信データすべての長さを算出

    if( rxsz >= allsz ){
        //電文のデータ数分だけデータを移し替える.
        //移し替えたデータ数分rpを更新する.
        if( rxdata.sum == rxdata.buf[sz]){ //SUMのチェック
            //受け入れたデータに関する処理
        }
    }
}
else{
  //rpを更新(不要なデータなので読み飛ばし)
}

ヘッダが一致しない場合は不要なデータであるrpを更新しています。ヘッダが一致した場合は電文のデータ長分データを移し替えながらrpを更新します。データの健全性の確認のためチェックサムを計算し受信データのものと一致するかのチェックを行います。

チェックサムが一致するとデータに問題がないため電文を受け入れて該当と処理を行います。

上記のような処理を行うことでMWSTAGEのSerialParserに近い動作になります。受信タイムアウトなども追加することでヘッダが一致するもののデータ数がそろわない場合の処理が実装できます。

動作確認

シリアルモニタでの動作確認
シリアルモニタでの動作確認

Arduinoのシリアルモニタでデータを確認するとトワイライトからの返信が表示できています。トワイライトとArduinoの双方でバイナリ形式の電文が交換できています。

Arduinoの処理の分岐でDOポート(DO8とDO9)をコントロールするようにしています。DOの電圧の変化を確認したところ処理を分けられていることが確認できました。抵抗とLEDを接続してLEDを点灯消灯させて動作確認してもよいと思います。

ソースコード全体

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

トワイライトのソースコード:

#include <TWELITE>
#include <NWK_SIMPLE>

#define BINARY_USE
#define BUF_SZ 64
#define HEADER1 0xA5
#define HEADER2 0x5A
#define HEADER3 0x80

struct  COM_TYP{
    uint8_t buf[BUF_SZ];
};

struct TXCOM_TYP{
    uint8_t header[3];
    uint8_t sz;
    uint8_t buf[BUF_SZ];
    uint8_t sum;
};

// application use
COM_TYP rcvdata;
TXCOM_TYP txdata;
uint8_t sbuf[]="TWE-CMD0-OK";
uint8_t sbuf1[]="TWE-CMD1-OK";

/*** Local function prototypes */
void RcvData(uint8_t *b);
uint8_t CalcSum(uint8_t *buf, uint8_t sz);
void SerialTxSet(uint8_t cmd);

/*** the setup procedure (called on boot) */
void setup() {

    #ifdef BINARY_USE
        SerialParser.begin(PARSER::BINARY,128);
    #else
        SerialParser.begin(PARSER::ASCII,128);
    #endif

    Serial << "start" << mwx::crlf << mwx::flush;
}

/*** the loop procedure (called every event) */
void loop() {

    while(Serial.available()){
        if(SerialParser.parse(Serial.read())){
            auto&& b = SerialParser.get_buf();

            #ifdef BINARY_USE
                RcvData(&b[0]);
            #else
                Serial << b[i];
            #endif    
        }
    }
}
/* RX function add */
void RcvData(uint8_t *b ){

    memcpy(&rcvdata.buf[0],b,sizeof(rcvdata));

    switch (rcvdata.buf[0]){
    case 0:
        SerialTxSet(0);
        break;
    case 1:
        SerialTxSet(1);
        break;
    default:
        break;
    }
}
/* TX function add */
void SerialTxSet(uint8_t cmd){
    uint8_t *adrs;
    uint8_t allsz = 0;

    txdata.header[0] = HEADER1;
    txdata.header[1] = HEADER2;
    txdata.header[2] = HEADER3;

    if(cmd == 0){
        txdata.sz = sizeof(sbuf)+1; //bud[0]はCMD
    }
    else if (cmd == 1){
        txdata.sz = sizeof(sbuf1)+1; //bud[0]はCMD
    }
    else{
        txdata.sz = 4;
    }

    txdata.buf[0] = cmd;
  
    for(uint8_t i=0; i < txdata.sz; i++){
        if(cmd == 0){
            txdata.buf[i+1] = sbuf[i]; //txdata.buf[0]分をオフセット
        }
        else if (cmd == 1){
            txdata.buf[i+1] = sbuf1[i]; //txdata.buf[0]分をオフセット
        }
        else{
            txdata.buf[i+1] = 0xff;
        }
    }
    txdata.sum = CalcSum(&txdata.buf[0],txdata.sz);
    allsz = sizeof(txdata.header)+ sizeof(txdata.sz)+ txdata.sz;
    
    adrs =&txdata.header[0];
    for(uint8_t i = 0; i < allsz; i++ ){
        Serial << (*adrs);
        adrs++;
    }
    Serial.write(txdata.sum);
}
/* ChkSum function add */
uint8_t CalcSum(uint8_t *buf, uint8_t sz=0){
    uint8_t ret = 0;

    for(uint8_t i=0; i < sz; i++ ){
        ret = ret ^ (*buf);
        buf++; 
    }
    return ret;
}

Arduinoのソースコード:

#define PIN_DI1 7
#define PIN_DO1 8
#define PIN_DO2 9 
#define DI_WAIT_CNT 1000
#define BINARY_USE
#define HEADER1 0xA5
#define HEADER2 0x5A
#define HEADER3 0x80
#define BUF_SZ 64
#define RING_SZ 256
#define RXCHK_SZ 4

struct DI_TYP{
    uint8_t nowst;
    uint8_t oldst;
    uint16_t cnt;
    bool  start;
};

struct COM_TYP{
    uint8_t header[3];
    uint8_t sz;
    uint8_t buf[BUF_SZ];
    uint8_t sum;
};

struct RING_MNG{
    uint8_t wp;
    uint8_t rp;
    uint8_t buf[RING_SZ];
};
// application use
bool comflg= false;
DI_TYP distatus;
COM_TYP txdata;
COM_TYP rxdata;  //チェック後の受信データ
RING_MNG rxarduino;
uint8_t reqcmd;
uint8_t buf[]={0xA0,0x13,0x01,0xFF,0x12,0x34,0x56};

/*** Local function prototypes */
void SerialTxSet(uint8_t cmd);
uint8_t CalcSum(uint8_t *buf, uint8_t sz);
void RxDataChk();

void setup() {

    Serial.begin(115200);
    pinMode(PIN_DI1,INPUT_PULLUP);
    pinMode(PIN_DO1,OUTPUT);
    pinMode(PIN_DO2,OUTPUT);

    distatus.nowst = digitalRead(PIN_DI1);
    distatus.oldst =!distatus.nowst;
    distatus.cnt = 0;
    distatus.start = false;
}

void loop() {

    distatus.nowst = digitalRead(PIN_DI1); //現在のDIの値

    if( distatus.nowst == distatus.oldst ){ //前回の値と同じか
        if(++distatus.cnt >= DI_WAIT_CNT ){ //規定回数以上一致したか
            distatus.start = !distatus.nowst;
            if(distatus.start == false ){
                comflg = true;
            }
            distatus.cnt = 0;
        }
    }
    else{
        distatus.cnt = 0;
    }

    distatus.oldst = distatus.nowst; //比較のため今回の値を旧値として保存

    if( distatus.start ){
        if(comflg ){
            comflg = false;

        #ifdef BINARY_USE
            SerialTxSet(reqcmd);
            Serial.println();

            if(++reqcmd >= 2 ){
                reqcmd = 0;
            }
        #else
            Serial.println(":00A011X");
        #endif
        }
    }

    while( Serial.available()){
        rxarduino.buf[rxarduino.wp] = Serial.read();

        if( ++rxarduino.wp >= RING_SZ ){ //次にデータを入れる場所を更新
            rxarduino.wp = 0;
        }
    }
    RxDataChk();
}
/* TX function add */
void SerialTxSet(uint8_t cmd){
    uint8_t *adrs;
    uint8_t allsz = 0;

    txdata.header[0] = HEADER1; //0xA5
    txdata.header[1] = HEADER2; //0x5A
    txdata.header[2] = HEADER3; //0x80
    txdata.sz = sizeof(buf)+1; //txdata.buf[0]分で+1
    txdata.buf[0] = cmd;
  
    for(uint8_t i=0; i < txdata.sz; i++){
         txdata.buf[i+1] = buf[i]; //buf[0]分をオフセット
    }
  
    txdata.sum = CalcSum(&txdata.buf[0],txdata.sz);
    allsz = sizeof(txdata.header)+ sizeof(txdata.sz)+ txdata.sz;

    adrs =&txdata.header[0];
    for(uint8_t i = 0; i < allsz; i++ ){
        Serial.write(*adrs);
        adrs++;
    }
    Serial.write(txdata.sum);
}
/* ChkSum function add */
uint8_t CalcSum(uint8_t *buf, uint8_t sz=0){
    uint8_t ret = 0;

    for(uint8_t i=0; i < sz; i++ ){
        ret = ret ^ (*buf);
        buf++; 
    }
    return ret;
}
/* RX function add */
void RxDataChk(){
    int rxsz;
    uint8_t sz;
    uint8_t allsz;
    uint8_t *adrs;
    uint8_t rp = rxarduino.rp;
    uint8_t chkdat[RXCHK_SZ];

    rxsz = rxarduino.wp - rxarduino.rp; //受信データ数の算出
    if( rxsz < 0 ){
        rxsz = rxsz + RING_SZ;
    }

    if(rxsz >= RXCHK_SZ){
        for(uint8_t i=0; i< RXCHK_SZ; i++ ){  //ヘッダーのチェックのため一時保存
            chkdat[i] = rxarduino.buf[rp];
            if(++rp > RING_SZ ){
                rp = 0;
            }
        }
        if( chkdat[0]== HEADER1 && chkdat[1]== HEADER2 && chkdat[2]== HEADER3 ){
            sz = chkdat[3] & 0x3F;//szが63を超えないようにする(データ長がおかしい場合の対策)
            allsz = RXCHK_SZ + sz + 1; //+1はSUMで受信データすべての長さを算出

            if( rxsz >= allsz ){ 
                adrs = &rxdata.header[0];
                for(uint8_t i=0; i< allsz; i++ ){
                     *adrs = rxarduino.buf[rxarduino.rp];
                      adrs++;
                      if(++rxarduino.rp >= RING_SZ ){
                          rxarduino.rp = 0;
                      }
                }
                rxdata.sum = CalcSum(&rxdata.buf[0],sz);

                if( rxdata.sum == rxdata.buf[sz]){ //SUMのチェック
                    if( rxdata.buf[0] == 0 ){
                        digitalWrite(PIN_DO1, HIGH);
                        digitalWrite(PIN_DO2, LOW);
                        for(uint8_t i=1; i<sz-1; i++ ){ //buf[0]は対象外
                            Serial.write(rxdata.buf[i]);
                        }
                       Serial.println();
                    }
                    else if(rxdata.buf[0] == 1 ){
                        digitalWrite(PIN_DO1, LOW);
                        digitalWrite(PIN_DO2, HIGH);
                        for(uint8_t i=1; i<sz-1; i++ ){ //buf[0]は対象外
                            Serial.write(rxdata.buf[i]);
                        }
                        Serial.println();          
                    }
                    else{
                        Serial.println("NG");   
                        digitalWrite(PIN_DO1, HIGH);
                        digitalWrite(PIN_DO2, HIGH);            
                    }
                }
            }
        }
        else{
            if(++rxarduino.rp >= RING_SZ ){
                rxarduino.rp = 0;
            }
        }
    }
}

関連リンク

Arduinoを使った開発環境を作り方やライブラリを使って各種モジュールを動作させています。Arduinoを使って動作確認したことについてリンクをまとめています。また無線モジュールであるトワイライトを使ってソフト開発したことについてまとめています。

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

トワイライト(TWELITE)のソフト開発と無線通信でできること

GEEKJOB-未経験からITエンジニアに【オンライン無料体験】

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

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