7839

雑草魂エンジニアブログ

【Arduino】UART シリアル通信(HardwareSerial / SoftwareSerial)

Arduinoでシリアル通信をするにあたって、調べたことを備忘録としてまとめておく。

UARTとは

UART(読み方:ユーアート)は、「Universal Asynchronous Receiver Transmitter」の略であり、直訳すると「汎用非同期式送受信機」となる。

2つのデバイス間でシリアル通信をするために、シリアル信号をパラレル信号に変換したり、その逆でパラレル信号をシリアル信号に変換したりする通信回路のことである。

f:id:serip39:20220206134534j:plain

機器同士の通信では配線経路の問題が発生するので、シリアル通信が主流である。しかしながら、パラレル通信のほうが1クロックで送受信できるデータが大きいので、機器内部ではパラレル通信が使われている場合がある。そのため、シリアル信号↔パラレル信号の変換が必要となることがある。

シリアル通信には、同期式と非同期式(調歩同期式)の2パターンがある。

f:id:serip39:20220206134846j:plain

非同期式(調歩同期式)シリアル通信のみに対応しているモジュールを「UART」という。

そして、同期式と非同期式(調歩同期式)のどちらにも対応しているモジュールを 「USART(Universal Synchronous and Asynchronous Receiver Transmitter)」という。

HardwareSerial

HardwareSerialは、MCUに内蔵されたUARTモジュールを使ってシリアル通信を行う。

メリット

  • 高速通信が可能
  • 64Byteのシリアルバッファに余裕がある限り、他のタスクで作業中であってもシリアル通信を受信できる

デメリット

  • 使用できるピンが限定される(RX/TXのみ)

f:id:serip39:20220205160429j:plain

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のライブラリのコードを読むことで動作の流れを確認することができた。

シリアル通信はよく使うので、様々な場面で使っていきたい。