【Arduino】UART シリアル通信(HardwareSerial / SoftwareSerial)
Arduinoでシリアル通信をするにあたって、調べたことを備忘録としてまとめておく。
- UARTとは
- HardwareSerial
- SoftwareSerial
- HardwareSerial実装
- SoftwareSerial実装
- SoftwareSerialメソッド
- 複数のSoftwareSerialを使う場合
- まとめ
UARTとは
UART(読み方:ユーアート)は、「Universal Asynchronous Receiver Transmitter」の略であり、直訳すると「汎用非同期式送受信機」となる。
2つのデバイス間でシリアル通信をするために、シリアル信号をパラレル信号に変換したり、その逆でパラレル信号をシリアル信号に変換したりする通信回路のことである。
機器同士の通信では配線経路の問題が発生するので、シリアル通信が主流である。しかしながら、パラレル通信のほうが1クロックで送受信できるデータが大きいので、機器内部ではパラレル通信が使われている場合がある。そのため、シリアル信号↔パラレル信号の変換が必要となることがある。
シリアル通信には、同期式と非同期式(調歩同期式)の2パターンがある。
非同期式(調歩同期式)シリアル通信のみに対応しているモジュールを「UART」という。
そして、同期式と非同期式(調歩同期式)のどちらにも対応しているモジュールを 「USART(Universal Synchronous and Asynchronous Receiver Transmitter)」という。
HardwareSerial
HardwareSerialは、MCUに内蔵されたUARTモジュールを使ってシリアル通信を行う。
メリット
- 高速通信が可能
- 64Byteのシリアルバッファに余裕がある限り、他のタスクで作業中であってもシリアル通信を受信できる
デメリット
- 使用できるピンが限定される(RX/TXのみ)
Arduino Uno と Arduino Mega 2560のMCUのブロック図をデータシートで確認する。
Arduino Unoの場合は、USARTが1つしかないが、Arduino Mega 2560の場合は、USARTが4つある。
SoftwareSerial
SoftwareSerialは、UARTモジュールを使わずに、GPIOのポートを使ってシリアル通信を行う。
メリット
- GPIOであればどのピンでも使用可能(ただし、RX-PINは、入力変化に対する割り込みをサポートしている必要あり。)
- 複数のSoftwareSerialを利用可能(ただし、複数データの同時受信はできず、受信は1ポートのみしか対応できない。)
デメリット
- ソフトウェアでデータの処理をするため、通信速度が速いと動作が不安定になる。(115200bpsまで対応可能だが、コード次第では9600bpsでしか対応できない場合もある。)
- 他のソフトウェアの処理の影響を受けてしまう可能性がある。(割り込み処理などで時間がかかり、通信エラーになる場合がある)
HardwareSerial実装
HardwareSerialは、Arduinoの標準ライブラリSerial
として追加されており、PD0(RXD)とPD1(TXD)でUSB接続を行い、PCとシリアル通信をすることができる。サンプルとして、PCから入力された文字をオウム返しで返すプログラムを紹介する。
void setup() { // Initialize serial and wait for port to open: Serial.begin(115200); // opens serial port, sets data rate to 115200 bps while (!Serial) { // wait for serial port to connect. Needed for native USB } Serial.println("Program Srart."); } void loop() { int incomingByte = 0; if (Serial.available()) { // reply only when you receive data incomingByte = Serial.read(); // read the incoming byte Serial.write(incomingByte); // send the incoming byte } }
詳細は、公式ドキュメントを参考にして欲しい。
SoftwareSerial実装
SoftwareSerialも、Arduinoの標準ライブラリに含まれているが、必要な場合のみ追加するようになっているので、#include <SoftwareSerial.h>
を最初に書く必要がある。
SoftwareSerialも、HardwareSerial同様に、Streamクラスを継承しており、使えるメソッドもほとんど同じである。
class HardwareSerial : public Stream class SoftwareSerial : public Stream
今回、SoftwareSerialを使うにあたり、標準ライブラリに追加されているSoftwareSerialではなく、以下のライブラリを用いた。
実装としてはほとんど同じであるが、以下の部分で違いが見られたので、こちらを使うことにした。
- 標準ライブラリのSoftwareSerialとHardwareSerialを同時に使おうとすると、通信エラーが発生して、SoftwareSerialでのデータ受信が正確にできなかった。
featherfly/SoftwareSerial
は複数のSoftwareSerialとHardwareSerialを同時に使うことができた。 - 標準ライブラリのSoftwareSerialでは、
flush()
が使えなくなっていた(// There is no tx buffering, simply return
とだけコメントされていて、メソッドの中身が空の状態であった)が、featherfly/SoftwareSerial
では通常通り使えた。
サンプルとして、HardwareSerial同様に、PCから入力された文字をオウム返しで返すプログラムを紹介する。SoftwareSerialの場合は、SoftwareSerialオブジェクトを最初に生成する必要がある。
#include <SoftwareSerial.h> SoftwareSerial mySerial(2, 3); // RX, TX void setup() { // Initialize serial and wait for port to open: mySerial.begin(9600); // opens serial port, sets data rate to 9600 bps mySerial.println("Program Srart."); } void loop() { int incomingByte = 0; if (mySerial.available()) { // reply only when you receive data incomingByte = mySerial.read(); // read the incoming byte mySerial.write(incomingByte); // send the incoming byte } }
SoftwareSerialメソッド
- featherfly/SoftwareSerial@^1.0
簡単ではあるが、コードの一部抜粋と合わせて、メソッドを紹介する。
SoftwareSerialコンストラクタ
受信するピン(receivePin/RX)と送信するピン(transmitPin/TX)を設定する。
SoftwareSerial::SoftwareSerial(uint8_t receivePin, uint8_t transmitPin, bool inverse_logic /* = false */) : _rx_delay_centering(0), _rx_delay_intrabit(0), _rx_delay_stopbit(0), _tx_delay(0), _buffer_overflow(false), _inverse_logic(inverse_logic) { setTX(transmitPin); setRX(receivePin); }
begin
シリアル通信のボーレート(通信速度)の設定を行い、listen()
で対象オブジェクトのポートで受信ができる状態にする。
ボーレートは、300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200 から選択可能であるが。
void SoftwareSerial::begin(long speed) { _rx_delay_centering = _rx_delay_intrabit = _rx_delay_stopbit = _tx_delay = 0; for (unsigned i=0; i<sizeof(table)/sizeof(table[0]); ++i) { long baud = pgm_read_dword(&table[i].baud); if (baud == speed) { _rx_delay_centering = pgm_read_word(&table[i].rx_delay_centering); _rx_delay_intrabit = pgm_read_word(&table[i].rx_delay_intrabit); _rx_delay_stopbit = pgm_read_word(&table[i].rx_delay_stopbit); _tx_delay = pgm_read_word(&table[i].tx_delay); break; } } // Set up RX interrupts, but only if we have a valid RX baud rate if (_rx_delay_stopbit) { if (digitalPinToPCICR(_receivePin)) { *digitalPinToPCICR(_receivePin) |= _BV(digitalPinToPCICRbit(_receivePin)); *digitalPinToPCMSK(_receivePin) |= _BV(digitalPinToPCMSKbit(_receivePin)); } tunedDelay(_tx_delay); // if we were low this establishes the end } #if _DEBUG pinMode(_DEBUG_PIN1, OUTPUT); pinMode(_DEBUG_PIN2, OUTPUT); #endif listen(); }
listen
対象オブジェクトをlistening
状態(データを受信できる状態)にする。複数オブジェクトを生成している場合、他のオブジェクトの受信データは破棄される。
bool SoftwareSerial::listen() { if (active_object != this) { _buffer_overflow = false; uint8_t oldSREG = SREG; cli(); _receive_buffer_head = _receive_buffer_tail = 0; active_object = this; SREG = oldSREG; return true; } return false; }
available
シリアル通信ポートの受信バッファにある、読み取り可能なデータのバイト数を返す。0の場合は、読み取りデータがないことを示す。
int SoftwareSerial::available() { if (!isListening()) return 0; return (_receive_buffer_tail + _SS_MAX_RX_BUFF - _receive_buffer_head) % _SS_MAX_RX_BUFF; }
read
シリアル通信ポートの受信バッファの読み取り位置から1バイトのデータを読み出す。
int SoftwareSerial::read() { if (!isListening()) return -1; // Empty buffer? if (_receive_buffer_head == _receive_buffer_tail) return -1; // Read from "head" uint8_t d = _receive_buffer[_receive_buffer_head]; // grab next byte _receive_buffer_head = (_receive_buffer_head + 1) % _SS_MAX_RX_BUFF; return d; }
flush
シリアル通信ポートの受信バッファの読み取り位置を先頭にする。
void SoftwareSerial::flush() { if (!isListening()) return; uint8_t oldSREG = SREG; cli(); _receive_buffer_head = _receive_buffer_tail = 0; SREG = oldSREG; }
peek
シリアル通信ポートの受信バッファにある先頭データを読み出す。readと異なり、バッファ中の読み込み位置は変更しない。
int SoftwareSerial::peek() { if (!isListening()) return -1; // Empty buffer? if (_receive_buffer_head == _receive_buffer_tail) return -1; // Read from "head" return _receive_buffer[_receive_buffer_head]; }
write
1バイトの数値データを送信する。
print/println
との違いは、ASCII文字としてではなく、数値として送信することである。
size_t SoftwareSerial::write(uint8_t b) { if (_tx_delay == 0) { setWriteError(); return 0; } uint8_t oldSREG = SREG; cli(); // turn off interrupts for a clean txmit // Write the start bit tx_pin_write(_inverse_logic ? HIGH : LOW); tunedDelay(_tx_delay + XMIT_START_ADJUSTMENT); // Write each of the 8 bits if (_inverse_logic) { for (byte mask = 0x01; mask; mask <<= 1) { if (b & mask) // choose bit tx_pin_write(LOW); // send 1 else tx_pin_write(HIGH); // send 0 tunedDelay(_tx_delay); } tx_pin_write(LOW); // restore pin to natural state } else { for (byte mask = 0x01; mask; mask <<= 1) { if (b & mask) // choose bit tx_pin_write(HIGH); // send 1 else tx_pin_write(LOW); // send 0 tunedDelay(_tx_delay); } tx_pin_write(HIGH); // restore pin to natural state } SREG = oldSREG; // turn interrupts back on tunedDelay(_tx_delay); return 1; }
複数のSoftwareSerialを使う場合
SoftwareSerialは複数同時にデータ受信状態になることができない。そのため、SoftwareSerialを複数利用する場合、データを受信する前に、listen()
で使用するシリアルポートを切り替える必要がある。
#include <SoftwareSerial.h> // software serial #1: RX = digital pin 5, TX = digital pin 6 SoftwareSerial portOne(5,6); // software serial #2: RX = digital pin 7, TX = digital pin 8 SoftwareSerial portTwo(7,8); void setup() { // Open serial communications and wait for port to open: Serial.begin(9600); // wait for serial port to connect. Needed for Leonardo only while (!Serial) {} // Start each software serial port portOne.begin(9600); portTwo.begin(9600); } void loop() { // By default, the last intialized port is listening. // when you want to listen on a port, explicitly select it: portOne.listen(); Serial.println("Data from port one:"); // while there is data coming in, read it // and send to the hardware serial port: while (portOne.available() > 0) { char inByte = portOne.read(); Serial.write(inByte); } // blank line to separate data from the two ports: Serial.println(); // Now listen on the second port portTwo.listen(); // while there is data coming in, read it // and send to the hardware serial port: Serial.println("Data from port two:"); while (portTwo.available() > 0) { char inByte = portTwo.read(); Serial.write(inByte); } // blank line to separate data from the two ports: Serial.println(); }
まとめ
UARTの基礎を復習した上で、HardwareSerial / SoftwareSerialのライブラリのコードを読むことで動作の流れを確認することができた。
シリアル通信はよく使うので、様々な場面で使っていきたい。