Arduinoのシリアル通信機能・詳細
MIDI INライブラリの作成過程でわかったことを整理しました。
USART
昨今は見かけることも少なくなりましたが、一昔前までは大抵のPCに、「RS-232」あるいは「Serial」といったラベルのついた9ピンのD-Subコネクタから成る、シリアル通信用のポートが少なくとも一つは付いていました。
この9つあるピンのうち、多くの場合送信/受信に使用されていたのは各1ピンずつだけでした。Arduinoには、USARTと呼ばれるこの通信規格に準じたピンと、それを簡単に扱うための機能が内蔵されています。この機能を用いると、以下のような通信を外部機器と行う仕組みが割合と簡単に実装できるようになります。
- シリアル・ポートを介したPCとの通信
- MIDI機器との通信
- その他、シリアル通信が可能な機器(業務用途の機器にはいまだに多い)
これらの例はいずれもUSART/UARTという共通のプロトコル(0/1の列が持つ意味合い)を使用しており、プログラムを記述する者は相手の機器に関係なく、ほぼ同じ要領で通信を行うことができます。ただし、接続する相手によっては0あるいは1と見做す電圧が異なりますので、その部分は回路で補います。具体的には、前述のD-Sub型コネクタは大抵、1を+15V、0を-15Vの電圧で表します。Arduinoが出力する0Vを-15Vに、あるいは5Vを15Vの電圧に変換するには、何らかの電子回路を加える必要があります。
ここではArduino Diecimilaに対して、Serial.println(),Serial.read()などの関数を使用して、入出の結果などをArduino環境でモニターする場合を考えてみましょう。
Arduinoの頭脳に当たるAVRシリーズのマイコン「ATMEGA168」が関知する限り、このマイコンが送受信に用いているのはUSBポートではなく、あくまでUSARTという古い規約に準じた信号です。
ArduinoはこのUSARTの信号を、同じくボード上に実装されているFT232というUSART→USB変換ICに一旦渡した上で、そこを通じてPCとの通信を行います。(ATMEGA同様、FT232もチップ表面に品番が刻印されているのが読み取れるかと思います)
翻って電子楽器の同士の通信に用いるMIDIでは0Vあるいは5Vを0、1とするUSARTが「まま」用いられているため、(通信速度などの細かな制限はありますが)電気的な特性を揃える回路さえ正しく間に挿めばArduinoと直結することが可能です。
前者のFT232チップを介した通信では通信の「内容」を変換し、後者のMIDIの例では通信に用いる「信号電圧」を変更しているという差異にご注意下さい。
ソフトウェアによるシリアル通信
さて、ここまではArduinoでシリアル通信を行う2つの手段のうち、ハードウェアの機能を用いた方法に関するお話です。詳細は後述しますが、マイコンに内蔵された機能を使用する場合、送信であれば特定変数に値をセットし、受信であれば受信完了時に通知を受けるよう設定するだけ、というお手軽さです。しかし時に不便になるのが、上記のように簡単な手続きにて通信の行えるポートが、Arduinoには送受信一つずつしか備わっていないことです。
どうしても一台のArduinoに複数のポートでUSART通信を行いたい場合、ArduinoのCPUは十分に高速ですので、汎用のI/Oピン(1~13)をプログラム側で制御し、正しいタイミングで読み書きを行うことで通信することは不可能ではありません。ただしこの場合、ソフトウェアでポーリング(通知を受けるのを待つのではなく、自ら状態を見に行く)操作を行う必要があるため、処理に時間を要します。この処理を行うためのライブラリが一応はArduino環境に付属していますので、処理を一から書き上げるよりは楽に準備できることと思われますが、ソフトウェア制御のシリアル通信についてはまた別の機会に取り上げたいと思います。
シリアル通信用ライブラリの内側
シリアル通信関連のクラスは、基本的に以下のコードに記されています。 arduino-0011\hardware\cores\arduino\HardwareSerial.(cpp|h) しかし、ファイルの中をご覧になれば判るようArduino標準のSerialクラスは、実際の処理のほとんどを以下のファイルに記された関数に丸投げしています。 arduino-0011\hardware\cores\arduino\wiring_serial.c
たとえば、Serial.begin()は、beginSerial()関数を呼び出しているだけです。
では、通信をするのに先立って実行される、beginSerial()関数の中身を見てみましょう。
ここでは、Arduino Diecimilaに関係のない行は省略しています。
void beginSerial(long baud) { UBRR0H = ((F_CPU / 16 + baud / 2) / baud - 1) >> 8; UBRR0L = ((F_CPU / 16 + baud / 2) / baud - 1); // enable rx and tx sbi(UCSR0B, RXEN0); sbi(UCSR0B, TXEN0); // enable interrupt on complete reception of a byte sbi(UCSR0B, RXCIE0); }
ここでは4つの操作が行われています。
- Baud rateの設定
- USARTの受信を有効にする
- USARTの送信を有効にする
- USART受信時完了時の割込みを有効にする
UBRR0H, UBRR0Lはそれぞれ、USARTの通信速度を設定するレジスタです。
このレジスタは数字で指定するのではなく、CPUの動作クロック数の分周比で指定します。
たとえばArduinoの場合、ハードウェアに変更を加えない限りCPUは16MHzで動作しています。
この状態で31,250bpsのMIDIデータをやり取りする場合、USARTで1ビットの通信を行う間に、ArduinoのCPUは512クロック進むことになります。
beginSerial()関数はこのレジスタの設定を自動化してくれますが、任意の比率を設定するのに、どのレジスタを指定するか関心のある方は、ATMEGA168のデータシートを参照して下さい。
残る3つの設定は、sbi()関数(実際はwiring_private.hで定義されたマクロ)で機能を有効にするビットを立てているだけです。
さて、このwiring_serial.cの中では、受信データの扱いについてもう一つ工夫がなされています。USARTでは通常、データは1バイト単位でやりとりされます。AVRマイコンが受信完了したバイトは、UDR0レジスタの値を読み出すことができますが、このレジスタは一つしかないため、もし万が一プログラムがデータを読みに行く前に次の1バイトの受信が完了してしまうと、以前のデータを取りこぼしてしまいます。
そこで、受信データの扱いが多少容易になるよう、以下の工夫がなされています。
#define RX_BUFFER_SIZE 128 unsigned char rx_buffer[RX_BUFFER_SIZE]; int rx_buffer_head = 0; int rx_buffer_tail = 0; (略) SIGNAL(SIG_USART_RECV) { unsigned char c = UDR0; int i = (rx_buffer_head + 1) % RX_BUFFER_SIZE; if (i != rx_buffer_tail) { rx_buffer[rx_buffer_head] = c; rx_buffer_head = i; } }
まず、128バイトのリングバッファと、バッファの読み出し位置と書き込み位置を指すポインタを用意します。
SIGNAL(SIG_USART_RECV)マクロで定義された関数は、1バイトの受信完了時に実行される割込み関数です。(割込みに関する詳細はこちら)
この中で受信データUDR0の内容をリングバッファにコピーしています。
Arduino側でこの仕組みを用意することにより、通常1バイト受信ごとに読み出さなければ破棄されるデータを、128バイトまで蓄えることができるのです。
ついでに見ておくと、バッファ内の未読バイト数を返す関数Serial.available()は、以下のように書き込み位置を指すポインタと、読み込み位置を指すポインタの距離を求めることにより実現しています。
int serialAvailable() { return (RX_BUFFER_SIZE + rx_buffer_head - rx_buffer_tail) % RX_BUFFER_SIZE; }