こんにちは、ENGかぴです。
Seeeduino XIAOの標準ライブラリであるSerial1を使用すると外部機器とシリアル通信ができます。Serialはシリアルモニタ専用であるためArduino UNOできなかったシリアルモニタと外部機器へのシリアル通信を区別できます。外部機器にはトワイライト(TWELITE)を使用します。
トワイライトとシリアル通信をしていますが、受信データを受け入れてチェックする方法などは他のマイコンが対象でも応用できます。
Seeeduino XIAOで動作確認したことについてリンクをまとめています。
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
Seeeduino XIAOでシリアル通信する方法
Seeeduino XIAOではシリアルモニタ用と外部機器に出力する用途のシリアル通信が別々に実装されています。そのためArduino UNOできなかったシリアルモニタと外部機器へのシリアル通信を区別できます。
シリアル | 用途 |
---|---|
Serial(SerialUSB) | シリアルモニタ専用:ピンから出力しない |
Serial1 | 外部機器へのシリアル通信 6ピン:TX 7ピン:RX |
SerialはSerialUSBと表記してもSerialと同じ動作になるためどちらの表記でも問題ありません。Serialと表記した方が他のArduino環境でも使用できるためSerialUSBは使わない方が良いと思います。
標準ライブラリを使用する方法
Seeeduino XIAOでシリアル通信を使用する際はSerial.begin()を発行する必要があります。
void setup() {
Serial.begin(115200); //シリアルモニタ用
Serial1.begin(115200); //外部機器との通信用
//while(!Serial ){ } //ポートを開くまでの待ち
}
引数にシリアル通信のボーレートを入力します。ボーレートは任意でも良いのですが、マイコンのクロック周波数によって誤差が出るので注意が必要です。
Arduino IDEやVsCodeで選択できるボーレートから選択すると多くの場合問題になりませんが、ボーレートを早くしすぎると配線の長さや周辺回路の部品などの影響で通信できなくなることがあるので外部機器のクロックを含めて検討する必要があります。
Serial1.begin()を発行した後ポートが開く(初期設定待ち)までループさせて確実に設定が完了するまで待機させることもありますが、初期化時に他で遅延させている場合やbegin()の発行から直ちにシリアル通信をしない場合は実装する必要はありません。
SerialとSerial1の使い分け
Serialはシリアルモニタに表示したい場合に使用します。Serial1で受信したデータをシリアルモニタに表示したい場合など使い分けることで効率よくデバッグできるようになります。
void loop() {
//受信データをモニタする例
while( Serial1.available()){ //Serial1に受信データがあるか
data = Serial1.read(); //Serial1データを読み出し
Serial.write(data); //シリアルモニタにSerial1から読み出したデータをモニタ表示
}
}
外部機器からシリアル通信のデータを受信するとSerial1.available()の条件を満たすためSerial1.read()によってSerial1のデータを読み出します。受信したデータをSerial.write()によってシリアルモニタに表示しています。
Serialでよく使うAPI
Serialでよく使うAPIをまとめました。Serialで表記していますがSerial1でも同様に使用できます。
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が表示されます。
Seeeduino XIAOとトワイライトの通信
Seeeduino XIAOとトワイライトをシリアル通信で接続してSeeeduino XIAOの電文でトワイライトの動作を切り替えます。トワイライトは電文を受け取るとCMDに応じた処理を行いOKを返信します。Seeeduino XIAOはOKの返信でCMDのカウントを更新します。
- 初期値は0でLEDを消灯する(CMD0)
- ブザーを鳴らす。(CMD1)
- ブザーを止める。(CMD2)
- LEDを消灯する。(CMD3)
1から4を繰り返してトワイライトの動作を切り替えます。ブザーは鳴り続けると耳障りに感じるため100ms間鳴らした後900msは鳴らないようにします。
トワイライトのシリアル通信はSerialParserのバイナリ形式を使います。
トワイライト(TWELITE)とArduino間でシリアル通信する
トワイライトのSerialParserの電文構成に従ってSeeeduino XIAOで電文を生成して送出することで双方のシリアル通信によるデータの送受信が可能となります。詳細は参考記事にまとめていますが、ポイントを整理すると電文の構成は次の通りになります。
この電文構成に従ってSeeeduino XIAOの電文を以下の例のように生成します。
struct COM_TYP{
uint8_t header[3];
uint8_t sz;
uint8_t buf[BUF_SZ];
uint8_t sum;
};
uint8_t buf[]={'C','M','D', 0x00};
COM_TYP txdata;
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);
for(uint8_t i=0; i < txdata.sz; i++){
txdata.buf[i] = buf[i]; //buf[0]分をオフセット
}
txdata.buf[3] = cmd;
txdata.sum = CalcSum(&txdata.buf[0],txdata.sz); //データ部のSUM値を計算
allsz = sizeof(txdata.header) + sizeof(txdata.sz) + txdata.sz; //すべてのデータ数
Serial1.write(&txdata.header[0],allsz); //下記のコメントアウト部分の書き方でもよい
Serial1.write(txdata.sum); //SUM値を最後に送出
//adrs =&txdata.header[0];
//for(uint8_t i = 0; i < allsz; i++ ){
// Serial1.write(*adrs);
// adrs++;
//}
//Serial1.write(txdata.sum);
}
トワイライトは電文を受信するとCMD番号に対する動作を実行しSeeeduino XIAOにOKを返信します。トワイライトの処理はソースコード全体を確認してください。トワイライトからの返信データに問題がなければCMDのカウントを進めてボタンが押されるのを待ちます。
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){
//サイズ部分まで一旦受信してチェックする
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 ){ //確定したデータ数を受信したかを確認
//rxdata.bufに受信データを格納する
rxdata.sum = CalcSum(&rxdata.buf[0],sz); //SUMを計算
if( rxdata.sum == rxdata.buf[sz]){//SUMのチェック
if( rxdata.buf[0] == 'O'&& rxdata.buf[1] == 'K' ){
//トワイライトからのOKを確認
if(++reqcmd >= MODE_MAX ){ //コマンド番号を更新
reqcmd = 0;
}
}
}
else{
//SUMチェックが不一致なのでデータを破棄する
}
}
}
else{
//ヘッダーが不一致なのでデータを破棄する
}
}
}
トワイライトからの返信電文のチェックを行っています。ヘッダー部分(固定している箇所)で全体のデータ数が分かるため一旦ヘッダー部分を受信して全体のデータ数を確認しています。
電文を受け付けた後はSUMチェックを行い正常であれば正式な電文として処理します。電文を受け付けてもSUMチェックが一致しない場合はエラーとして電文を破棄します。
動作確認
Seeeduino XIAOのDIにはスイッチを実装しています。スイッチを押すとSeeeduino XIAOからトワイライトに電文を送出します。トワイライトは受信した電文に応じてLED1を点灯消灯したりブザーを鳴らしたりします。
トワイライトはSeeeduino XIAOに対して電文を受け入れるとOKの文字を返信します。Seeeduino XIAOはOKを受信するとCMD番号のカウントを更新してボタンが押されるのを待ちます。
ボタンを繰り返し押すことでLED1が点灯しブザーが1秒ごとになることが確認来ました。CMD番号が切り替わっていることが確認できたのでシリアル通信は成功していることを確認できました。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
Seeeduino XIAOのスケッチ例:
#include <TimerTC3.h>
#define PIN_DI1 0
#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
#define DI_FILT_MAX 4
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define FILT_MIN 1
#define MODE_MAX 4
struct DIFILT_TYP{
uint8_t wp;
uint8_t buf[DI_FILT_MAX];
uint8_t di1;
};
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;
DIFILT_TYP difilt;
COM_TYP txdata;
COM_TYP rxdata; //チェック後の受信データ
RING_MNG rxarduino;
uint8_t reqcmd;
uint8_t buf[]={'C','M','D', 0x00};
int8_t timdifilt = TIME_OFF;
int8_t cnt10ms;
bool btnflg1;
/*** Local function prototypes */
void SerialTxSet(uint8_t cmd);
uint8_t CalcSum(uint8_t *buf, uint8_t sz);
void RxDataChk();
void TimerCnt();
void mainTimer();
void DiFilter();
void setup() {
Serial.begin(115200);
Serial1.begin(115200);
pinMode(PIN_DI1,INPUT_PULLUP);
TimerTc3.initialize(1000);
TimerTc3.attachInterrupt(TimerCnt);
timdifilt = FILT_MIN;
for( uint8_t i=0; i < 10; i++ ){
mainTimer();
DiFilter();
delay(10);
}
}
void loop() {
mainTimer();
DiFilter();
if(difilt.di1 == 0){
if(btnflg1){
btnflg1 = false;
SerialTxSet(reqcmd);
}
}
else{
btnflg1 = true;
}
while( Serial1.available()){
rxarduino.buf[rxarduino.wp] = Serial1.read();
Serial.write(&rxarduino.buf[rxarduino.wp],15);
if( ++rxarduino.wp >= RING_SZ ){ //次にデータを入れる場所を更新
rxarduino.wp = 0;
}
}
RxDataChk();
}
/* callback function add */
void TimerCnt(){
++cnt10ms;
}
/* Timer Management function add */
void mainTimer(){
if( cnt10ms >= BASE_CNT ){
cnt10ms -=BASE_CNT; //10msごとにここに遷移する
if( timdifilt > TIME_UP ){
timdifilt--;
}
}
}
/* DiFilter function add */
void DiFilter(){
if( timdifilt == TIME_UP ){
difilt.buf[difilt.wp] = digitalRead(PIN_DI1);
if( difilt.buf[0] == difilt.buf[1] &&
difilt.buf[1] == difilt.buf[2] &&
difilt.buf[2] == difilt.buf[3] ){ //4回一致を確認
difilt.di1 = difilt.buf[0];
}
if( ++difilt.wp >= DI_FILT_MAX ){
difilt.wp = 0;
}
timdifilt = FILT_MIN;
}
}
/* 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);
for(uint8_t i=0; i < txdata.sz; i++){
txdata.buf[i] = buf[i]; //buf[0]分をオフセット
}
txdata.buf[3] = cmd;
txdata.sum = CalcSum(&txdata.buf[0],txdata.sz);
allsz = sizeof(txdata.header) + sizeof(txdata.sz) + txdata.sz;
adrs =&txdata.header[0];
Serial1.write(&txdata.header[0],allsz); //下記のコメントアウトの書き方でもよい
Serial1.write(txdata.sum);
//for(uint8_t i = 0; i < allsz; i++ ){
// Serial1.write(*adrs);
// adrs++;
//}
//Serial1.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] == 'O'&& rxdata.buf[1] == 'K' ){
if(++reqcmd >= MODE_MAX ){
reqcmd = 0;
}
}
}
else{
if(++rxarduino.rp >= RING_SZ ){
rxarduino.rp = 0;
}
}
}
}
else{
if(++rxarduino.rp >= RING_SZ ){
rxarduino.rp = 0;
}
}
}
}
トワイライトのソフト例:
#include <TWELITE>
#include <NWK_SIMPLE>
#define BUF_SZ 64
#define HEADER1 0xA5
#define HEADER2 0x5A
#define HEADER3 0x80
#define TIME_UP 0
#define TIME_OFF -1
#define BUZ_ON_MAX 100
#define BUZ_OFF_MAX 900
const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18; //LED
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[]={'O','K'};
uint8_t rxchk[]={'C','M','D'};
int16_t timbuzon = TIME_OFF;
int16_t timbuzoff = TIME_OFF;
bool buzflg = false;
/*** Local function prototypes */
void RcvData(uint8_t *b);
uint8_t CalcSum(uint8_t *buf, uint8_t sz);
void SerialTxSet();
/*** the setup procedure (called on boot) */
void setup() {
SerialParser.begin(PARSER::BINARY,128);
pinMode(PIN_DO1,OUTPUT_INIT_LOW);
Timer1.setup();
Timer1.begin(1000,false,false);
// Serial << "--- serial ---" << mwx::crlf;
}
/*** the loop procedure (called every event) */
void loop() {
if(TickTimer.available()){
if(timbuzon > TIME_UP){
--timbuzon;
}
if(timbuzoff > TIME_UP){
--timbuzoff;
}
}
if(buzflg) {
if( timbuzon == TIME_UP){
timbuzon = TIME_OFF;
timbuzoff = BUZ_OFF_MAX;
Timer1.begin(1000,false,false);
Timer1.change_duty( 0 );
}
if(timbuzoff == TIME_UP){
timbuzoff = TIME_OFF;
timbuzon = BUZ_ON_MAX;
Timer1.begin(1000,false,true);
Timer1.change_duty( 500 );
}
}
while(Serial.available()){
if(SerialParser.parse(Serial.read())){
auto&& b = SerialParser.get_buf();
RcvData(&b[0]);
}
}
}
/* RX function add */
void RcvData(uint8_t *b ){
memcpy(&rcvdata.buf[0],b,sizeof(rcvdata));
if( rcvdata.buf[0] == rxchk[0] &&
rcvdata.buf[1] == rxchk[1] &&
rcvdata.buf[2] == rxchk[2] ){
switch (rcvdata.buf[3]){
case 0:
digitalWrite(PIN_DO1,HIGH);
break;
case 1:
buzflg = true;
timbuzon = BUZ_ON_MAX;
Timer1.begin(1000,false,true);
Timer1.change_duty( 500 );
break;
case 2:
buzflg = false;
Timer1.begin(1000,false,false);
timbuzon = TIME_OFF;
timbuzoff = TIME_OFF;
break;
case 3:
digitalWrite(PIN_DO1,LOW);
break;
}
SerialTxSet();
}
}
/* TX function add */
void SerialTxSet(){
uint8_t *adrs;
uint8_t allsz = 0;
txdata.header[0] = HEADER1;
txdata.header[1] = HEADER2;
txdata.header[2] = HEADER3;
txdata.sz = sizeof(sbuf);
for(uint8_t i=0; i < txdata.sz; i++){
txdata.buf[i] = sbuf[i];
}
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のライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。
トワイライト(TWELITE)のソフト開発と無線通信でできること
広告
マイベスト3年連続1位を獲得した実績を持つ実践型のプログラミングスクール
最後まで、読んでいただきありがとうございました。
Seeeduino XIAOにおいてSerial1が外部機器に対して動作することに気づくのが遅れてしまいました。SerialとSerial1を区別してデバッグできることはソフト開発面で効率が良いと改めて感じました。