2019年3月23日土曜日

いろんなArduinoのシリアルを関数の引数として使う方法


背景

Arduinoとはプログラムを書き込んで電子工作に利用できる開発ボードの総称です。
開発ボードの回路図や、開発環境であるArduinoIDEなど、多くの設計がオープンソースとして公開されています。
そのためArduinoIDEを利用して公式・非公式含めて様々な種類のボードを開発できます。

シリアル通信とは、情報を1ビットずつ送信する通信形式の名前です。
Arduino Unoなどの場合、Serialという定義済みの変数を利用することでシリアル通信を行なえます。

シリアル通信の変数は、型を指定すれば下記のように関数の引数として渡せます。
#include <HardwareSerial.h>

void printHello(HardwareSerial *serial) {
  serial->print("hello");
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  printHello(&Serial); // print hello
  delay(1000);
}

Unoなどの代表的なArduinoでは、上記のプログラムのようにHardwareSerialをシリアルの型とすればをビルドして動かせます。
しかし、USB通信機能を備えるLeonardoやARM系のArduinoは、Serialに別の型が使われているためビルドに失敗します。

adafruitのTravisCIスクリプトで検査対象となっているボード全てに対応するシリアル通信プログラムを書くのに時間がかかったため、備忘録を兼ねて方法を共有します。

使ったもの

Arduino IDE

Arduino IDEとは、Arduino向けのプログラムの開発ができるソフトウェアです。
下記のページからダウンロードできます。

Download the Arduino IDE

Arduino IDEで利用できるボード情報

Arduino IDEの設定にjsonを追加してボードマネージャーからダウンロードすることで、標準設定のArduino以外のボードをArduino IDEでビルド出来るようになります。

使ったボードの読み込み先はそれぞれこちらです。

esp8266/Arduino
ESP8266が使えるようになります。

espressif/arduino-esp32
ESP32が使えるようになります。

arduino/ArduinoCore-sam
ArduinoのSAM系(Due)が使えるようになります。

arduino/ArduinoCore-samd
ArduinoのSAMD系(Zeroなど)が使えるようになります。

Adafruit boards
Adafruitのボード全般が使えるようになります。

一覧

解説するボードとシリアルの種類を表にまとめます。
ボード プログアム用シリアル 自由なシリアル SoftwareSerial
変数名 変数名
Arduino Uno HardwareSerial Serial - - 利用可能
ESP8266 HardwareSerial Serial - - 利用可能
ESP32 HardwareSerial Serial HardwareSerial Serial1, Serial2 利用できない
Arduino Mega HardwareSerial Serial HardwareSerial Serial1, Serial2, Serial3 利用可能
Arduino Leonardo Serial_ (USBAPI.h) Serial HardwareSerial Serial1 利用可能
Arduino Due Serial_ (USB/USBAPI.h), UARTClass (HardwareSerialとして使える) SerialUSB,
Serial
USARTClass (HardwareSerialとして使える) Serial1, Serial2, Serial3 利用できない
Arduino Zero Serial_ (USB/USBAPI.h), Uart (HardwareSerialとして使える) SerialUSB, Serial Uart (HardwareSerialとして使える) Serial1 利用できない
Adafruit M0 Serial_ (USB/USBAPI.h) Serial Uart (HardwareSerialとして使える) Serial1 利用できない
Adafruit M4 Serial_ (USB/USBAPI.h) Serial Uart (HardwareSerialとして使える) Serial1, SERCOMで定義できるUart 利用できない

それぞれ解説します。

Streamで受け取れば、printを使う関数を記述可能

Streamというクラスを介してserialを受け取れば、大体のserialを引数として受け取れます。
#include <HardwareSerial.h>
#include <SoftwareSerial.h>

void streamHello(Stream *serial) { serial->println("hello"); }

void setup() {
Serial.begin(115200);
softSerial.begin(115200);
}

void loop() {
streamHello(&Serial);
streamHello(&softSerial);
delay(1000);
}


twitterで教えていただきました。

Arduino Uno(atmega328p系)

Arduino Unoはプログラミングポートを兼ねたHardwareSerialを1つ持っています。
SoftwareSerialを利用できます。
PassUno.ino
#include <HardwareSerial.h>
#include <SoftwareSerial.h>

SoftwareSerial softSerial(8, 9);

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void softHello(SoftwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  Serial.begin(115200);
  softSerial.begin(115200);
}

void loop() {
  hardHello(&Serial);
  softHello(&softSerial);
  delay(1000);
}

ESP8266

ESP8266はプログラミングポートを兼ねたHardwareSerialを1つ持っています。
SoftwareSerialを利用できます。
つまり、Unoと同じコードが動きます。
PassESP32.ino
#include <HardwareSerial.h>
#include <SoftwareSerial.h>

SoftwareSerial softSerial(8, 9);

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void softHello(SoftwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  Serial.begin(115200);
  softSerial.begin(115200);
}

void loop() {
  hardHello(&Serial);
  softHello(&softSerial);
  delay(1000);
}

ESP32

ESP32はプログラミングポートを兼ねたHardwareSerialを1つ、自由に使えるHardwareSerialを2つ持っています。
SoftwareSerialは利用できません。
下記のコードはビルドはできますが、ESP32のSerial1は利用できないピンが割り当てられているため、Serial1を利用する場合はピンの配置を変える必要があります。
PassESP32.ino
#include <HardwareSerial.h>

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200);
  Serial2.begin(115200);
}

void loop() {
  hardHello(&Serial);
  hardHello(&Serial1);
  hardHello(&Serial2);
  delay(1000);
}

Arduino Mega(atmega2560系)

Arduino Megaはプログラミングポートを兼ねたHardwareSerialを1つ、自由に使えるHardwareSerialを3つ持っています。
SoftwareSerialを利用できます。
自分が調査した中では、SoftwareSerialとHardwareSerialを合わせると、最も多くのシリアルを利用でるボードです。
PassMega.ino
#include <HardwareSerial.h>
#include <SoftwareSerial.h>

SoftwareSerial softSerial(10, 11);

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void softHello(SoftwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200);
  Serial2.begin(115200);
  Serial3.begin(115200);
  softSerial.begin(115200);
}

void loop() {
  hardHello(&Serial);
  hardHello(&Serial1);
  hardHello(&Serial2);
  hardHello(&Serial3);
  softHello(&softSerial);
  delay(1000);
}

Arduino Leonardo(atmega32u4系)

Arduino LeonardoはUSB接続用のUSBAPIを1つ、自由に使えるHardwareSerialを1つ持っています。
SoftwareSerialを利用できます。
PassLeonardo.ino
#include <HardwareSerial.h>
#include <SoftwareSerial.h>
#include <USBAPI.h>

SoftwareSerial softSerial(8, 9);

void usbHello(Serial_ *serial) {
  serial->println("hello");
}

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void softHello(SoftwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200);
  softSerial.begin(115200);
}

void loop() {
  usbHello(&Serial);
  hardHello(&Serial1);
  softHello(&softSerial);
  delay(1000);
}

Arduino Due(Arduino SAM系)

Arduino DueはNativeポートとしてUSBAPIを1つ、プログラミングポートを兼ねたUARTClassを1つ、自由に使えるUSARTClassを3つ持っています。
UARTClassとUSARTClassはHardwareSerialとして利用できます。
SoftwareSerialは利用できません。
PassDue.ino
#include <HardwareSerial.h>
#include <USB/USBAPI.h>

void usbHello(Serial_ *serial) {
  serial->println("hello");
}

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  SerialUSB.begin(115200);
  Serial.begin(115200);
  Serial1.begin(115200);
  Serial2.begin(115200);
  Serial3.begin(115200);
}

void loop() {
  usbHello(&SerialUSB);
  hardHello(&Serial);
  hardHello(&Serial1);
  hardHello(&Serial2);
  hardHello(&Serial3);
  delay(1000);
}

Arduino Zero(Arduino SAMD系)

Arduino ZeroはNativeポートとしてUSBAPIを1つ、プログラミングポートとして使えるUartを1つ、自由に使えるUartを1つ持っています。
UartはHardwareSerialとして利用できます。
SoftwareSerialは利用できません。
PassZero.ino
#include <HardwareSerial.h>
#include <USB/USBAPI.h>

void usbHello(Serial_ *serial) {
  serial->println("hello");
}

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  SerialUSB.begin(115200); // native port
  Serial.begin(115200); // programming port
  Serial1.begin(115200); // 0: RX, 1: TX
}

void loop() {
  usbHello(&SerialUSB);
  hardHello(&Serial);
  hardHello(&Serial1);
  delay(1000);
}

Adafruit M0(Adafruit SAMD系)

Adafruitが出しているM0系のボードは、USBAPIを1つ、自由に使えるUartを1つ持っています。
UartはHardwareSerialとして利用できます。
SoftwareSerialは利用できません。
PassAdafruitM0.ino
#include <HardwareSerial.h>
#include <USB/USBAPI.h>

void usbHello(Serial_ *serial) {
  serial->println("hello");
}

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200);
}

void loop() {
  usbHello(&Serial);
  hardHello(&Serial1);
  delay(1000);
}

Adafruit M4(Adafruit SAMD系)

AdafruitのM4系ボードは、USBAPIを1つ、自由に使えるUartを1つ、定義によってUartやI2Cなどに利用できるポートを3つくらい持っています。
UartはHardwareSerialとして利用できます。
Uartの定義方法は、Adafruitの解説ページを参考にしてください。
Creating a new Serial | Using ATSAMD21 SERCOM for more SPI, I2C and Serial ports | Adafruit Learning System
SoftwareSerialは利用できません。
PassAdafruitM4.ino
#include <HardwareSerial.h>
#include <Uart.h>
#include <USB/USBAPI.h>

void usbHello(Serial_ *serial) {
  serial->println("hello");
}

void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

// https://learn.adafruit.com/using-atsamd21-sercom-to-add-more-spi-i2c-serial-ports/creating-a-new-serial
Uart uartSerial(&sercom1, 11, 10, SERCOM_RX_PAD_0, UART_TX_PAD_2);

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200);
  uartSerial.begin(115200);
}

void loop() {
  usbHello(&Serial);
  hardHello(&Serial1);
  hardHello(&uartSerial);
  delay(1000);
}

全部の環境に向けてビルドできるプログラム

Arduinoはc言語やc++の記述をプログラムで利用できます。
c言語にはコンパイル前にプログラムの内容を処理するマクロという機能があり、これを利用することで環境ごとにプログラムの内容を変えられます。
全部の環境に向けてビルドできるプログラムをマクロを利用して作ってみました。
PassAll.ino
#include <HardwareSerial.h>

#if !defined(__arm__) && !defined(ESP32)
#include <SoftwareSerial.h>

void softHello(SoftwareSerial *serial) {
  serial->println("hello");
}

#ifdef ARDUINO_MEGA
SoftwareSerial softSerial(10, 11);
#else
SoftwareSerial softSerial(8, 9);
#endif
#endif

// define hardHello before including SoftwareSerial cause error
void hardHello(HardwareSerial *serial) {
  serial->println("hello");
}

#if ADAFRUIT_METRO_M4_EXPRESS
#include <Uart.h>

Uart uartSerial(&sercom1, 11, 10, SERCOM_RX_PAD_0, UART_TX_PAD_2);
#endif

#ifdef USBCON
#ifdef __arm__
#include <USB/USBAPI.h>
#else
#include <USBAPI.h>
#endif

void usbHello(Serial_ *serial) {
  serial->println("hello");
}
#endif

// define flag to detect using SerialUSB
#if defined(__arm__) && !defined(ADAFRUIT_FEATHER_M0) && !defined(ADAFRUIT_METRO_M4_EXPRESS)
// adafruit boards don't use SerialUSB
#define USING_SERIAL_USB
#endif

void setup() {
#if !defined(__arm__) && !defined(ESP32)
  softSerial.begin(115200);
#endif

#ifdef USING_SERIAL_USB
  usbHello(&SerialUSB);
#endif

  Serial.begin(115200);

#if defined(USBCON) && !defined(USING_SERIAL_USB)
  Serial1.begin(115200);
#endif
#ifdef ESP32
  Serial1.begin(115200);
  Serial2.begin(115200);
#endif
#if defined(ARDUINO_MEGA) || defined(ARDUINO_DUE)
  Serial1.begin(115200);
  Serial2.begin(115200);
  Serial3.begin(115200);
#endif

#if ADAFRUIT_METRO_M4_EXPRESS
  uartSerial.begin(115200);
#endif
}

void loop() {
#if !defined(__arm__) && !defined(ESP32)
  softHello(&softSerial);
#endif

#ifdef USING_SERIAL_USB
  usbHello(&SerialUSB);
#endif

#if defined(USBCON) && !defined(USING_SERIAL_USB)
  usbHello(&Serial);
#else
  hardHello(&Serial);
#endif

#if defined(USBCON) && !defined(USING_SERIAL_USB)
  hardHello(&Serial1);
#endif
#ifdef ESP32
  hardHello(&Serial1);
  hardHello(&Serial2);
#endif
#if defined(ARDUINO_MEGA) || defined(ARDUINO_DUE)
  hardHello(&Serial1);
  hardHello(&Serial2);
  hardHello(&Serial3);
#endif

#if ADAFRUIT_METRO_M4_EXPRESS
  hardHello(&uartSerial);
#endif
  delay(1000);
}

マクロで切り替えている内容はこちらです。

SoftwareSerial関係
  • ESP32とSAMとSAMDのボード(ARM系のボード)ではSoftwareSerialを利用しない
  • Arduino MegaのSoftwareSerialはRXポートとして10を利用する
#if !defined(__arm__) && !defined(ESP32)
#include <SoftwareSerial.h>

void softHello(SoftwareSerial *serial) {
  serial->println("hello");
}

#ifdef ARDUINO_MEGA
SoftwareSerial softSerial(10, 11);
#else
SoftwareSerial softSerial(8, 9);
#endif
#endif

void setup() {
#if !defined(__arm__) && !defined(ESP32)
  softHello(&softSerial);
#endif
}

void loop() {
#if !defined(__arm__) && !defined(ESP32)
  softHello(&softSerial);
#endif
}


USBAPI関係
  • USBCONが定義されている場合、USBAPIを利用する
  • SAMとSAMDのボード(ARM系のボード)ではSerial_をUSB/USBAPI.hでincludeする
  • ArduinoのSAMとSAMDボード(ARM系のボード)ではSerialUSBを利用する
  • AdafruitのSAMDボードではSerialUSBではなくSerialを利用する
    (このプログラムはAdafruitのFeather M0とMetro M4 Expressだけ除外する処理にしています。Adafruitの他のSAMDのボード情報はAdafruitのsamdのboards.txtに載っています。)
#ifdef USBCON
#ifdef __arm__
#include <USB/USBAPI.h>
#else
#include <USBAPI.h>
#endif

void usbHello(Serial_ *serial) {
  serial->println("hello");
}
#endif

// define flag to detect using SerialUSB
#if defined(__arm__) && !defined(ADAFRUIT_FEATHER_M0) && !defined(ADAFRUIT_METRO_M4_EXPRESS)
// adafruit boards don't use SerialUSB
#define USING_SERIAL_USB
#endif

void setup() {
#ifdef USING_SERIAL_USB
  usbHello(&SerialUSB);
#endif

  Serial.begin(115200);

#if defined(USBCON) && !defined(USING_SERIAL_USB)
  Serial1.begin(115200);
#endif
}

void loop() {
#ifdef USING_SERIAL_USB
  usbHello(&SerialUSB);
#endif

#if defined(USBCON) && !defined(USING_SERIAL_USB)
  usbHello(&Serial);
#else
  hardHello(&Serial);
#endif

#if defined(USBCON) && !defined(USING_SERIAL_USB)
  hardHello(&Serial1);
#endif
}

複数のHardwareSerial関係
  • Mega、DueはSerial1、Serial2、Serial3を利用する
  • ESP32はSerial1とSerial2を利用する
  • USBAPIをSerialUSBではなくSerialとして利用するボードは、Serial1をHardwareSerialとして利用する
void setup() {
#ifdef ESP32
  Serial1.begin(115200);
  Serial2.begin(115200);
#endif
#if defined(ARDUINO_MEGA) || defined(ARDUINO_DUE)
  Serial1.begin(115200);
  Serial2.begin(115200);
  Serial3.begin(115200);
#endif
}

void loop() {
#ifdef ESP32
  hardHello(&Serial1);
  hardHello(&Serial2);
#endif
#if defined(ARDUINO_MEGA) || defined(ARDUINO_DUE)
  hardHello(&Serial1);
  hardHello(&Serial2);
  hardHello(&Serial3);
#endif
}

Adafruit M4の機能
  • Adafruit M4はUartでポートを定義して利用する
#if ADAFRUIT_METRO_M4_EXPRESS
#include <Uart.h>

Uart uartSerial(&sercom1, 11, 10, SERCOM_RX_PAD_0, UART_TX_PAD_2);
#endif

void setup() {
#if ADAFRUIT_METRO_M4_EXPRESS
  uartSerial.begin(115200);
#endif

void loop() {

#if ADAFRUIT_METRO_M4_EXPRESS
  hardHello(&uartSerial);
#endif
}

まとめ

USB通線専用ポートを持つボードのUSBAPIがSerialだったりSerialUSBだったりして切り替えに手間取りましたが、Arduinoで定義されている複数のシリアルを関数の引数として使う方法が分かりました。
動作確認に利用したプログラムは下記のリポジトリで公開します。

asukiaaa/TravisPracticeArduinoLib/examples

「この記述はこうにると、より簡潔になるよ」など、改善案などありましたら、コメントやメッセージをもらえるとありがたいです。

「複数種類のシリアルをサポートしたいけどマクロを多用してまで同じ記述を増やしたくないよ」という方は、シリアルをラップするライブラリを公開してますので、良かったらそちらをお使いください。

asukiaaa/SomeSerial
ArduinoでHardwareSerialもSoftwareSerialもサポートする関数やクラスを作る方法

この情報が何かの参考になれば嬉しいです。

参考

Serial - Arduino Reference
Adapting Sketches to M0 - Adafruit
ArduinoCore-samd/boards.txt

変更履歴

2021.12.31
Streamクラスを使えば大体のシリアルクラスを扱えるという情報共有があったので、更新しました。

0 件のコメント :