マイコン-UI間通信仕様
前回、マイコンにFT232RLというUSB-シリアル変換ICをつなぎ、UI(パソコン)とマイコン制御システムの間を物理的に接続しました。
さあ次はプログラム、といきたいところですが、その前にシステム全体の観点から、マイコンとUIの通信仕様を考察したいと思います。
よく言われるように、モジュール間は疎結合であるべきです。
今回、制御システムはUIモジュールと、なにかの制御モジュールを備えているわけで、
UIモジュールと制御モジュールはなるべく相互に依存しない作りにするべきです。
特に、制御モジュールがリアルタイムに動かなければならないのに対し、
UIモジュールは人間が相手ですので、リアルタイム性はそれほど必要ではありません。
例えば、センサが取得した値は、制御モジュールが制御に利用するのですが、
それをリアルタイムで(MPUが動く、数MHzという時間単位で)UIに表示するというのはあまり意味がありませんし、
人間からの入力を待ち受けている間、制御モジュールがストップするようなことがあってはいけません。
そこで今回は、
- 制御モジュールは、リアルタイムで制御を実行する。
- UIモジュールは、表示する値が必要なときや、人間からのなんらかの入力があった場合、制御モジュールにコマンドを送信する。
- 制御モジュールは、制御のリアルタイム性に影響を与えないように、UIモジュールからのコマンドを受け付け、またそのコマンドを適切に処理する。
具体的なコマンドの内容は対象となる制御内容によって異なってきます。
コマンドを設計する際、コマンドはアトミックにします。つまりコマンドは1つで完結し、1つのコマンドがそれに続く他のコマンドを期待するものであってはいけません。
通信は常に失敗すると考えるべきです。複数に分割されたコマンドは、制御モジュールを矛盾した状態にしてしまう可能性があります。
コマンドは、制御モジュールをクラスと考えた場合のパグリックメソッドに相当するものです。
以下は、昔作成した、電気炉のPID制御温度コントローラのコマンドの一部です。
コマンド | コマンドバイト数 | 返信 | 返信バイト数 | 説明 |
Q1 | 2 | 温度 | 2 | 現在の温度を問い合わせる。バイナリ16ビットで温度が返る。 |
R | 1 | "OK" or "NG" | 2 | 電気炉の加熱をスタートする。スタートできれば"OK"が返る。 すでに加熱中であった場合は"NG"が返る。 |
P | 5 | "OK" or "NG" | 2 | PID制御のゲインを設定する。ゲインは32ビット数値で指定する。コマンド"P"の後に、バイナリで4バイト分のデータをリトルエンディアン方式で送る。異常がなければ"OK"が返る。値が設定可能な範囲を超えていれば"NG"を返す。 |
基本的に、制御システムのコマンドというのは、固定長で設計できると思います。
可変長は終端文字の検出などいろいろ煩雑なので私はやりません。
では、次は、AVRマイコン(ここではATMega328P)のシリアル通信機能を見て行きましょう。
AVRのUSART機能
以下は、AVRのUSART機能を使用した、簡単なシリアル通信プログラムです。//UART0初期化処理 void usart0_init(void) { //USART 0 制御/状態レジスタ A の設定 UCSR0A = 0x00;//倍速不使用、複数プロッサ動作不使用 //USART 0 制御/状態レジスタ C の設定 //非同期動作、パリティなし 、ストップビット1 //データ長を8ビット(UCSZ02ビットは、UCSR0Bにある) UCSR0C = (1<<UCSZ01)|(1<<UCSZ00); //USARTボーレートレジスタの設定 //9600bpsとすると、 //0x33 - CPUが8MHzのとき //0x67 - CPUが16MHzのとき UBRR0L = 0x33; UBRR0H = 0x00; //USART 0 制御/状態レジスタ Bの設定 UCSR0B = (1<<RXEN0)|(1<<TXEN0);//受信ON、送信ON } //1バイトデータ送信 void usart0_Tx(const uint8_t tx_data) { while( !(UCSR0A & (1<<UDRE0)) ); UDR0 = tx_data; } //1バイトデータ受信 uint8_t usart0_Rx(void) { while( !(UCSR0A & (1<<RXC0)) ); return UDR0; }
使い方は、usart0_initで初期化し、usart0_Txでデータを送信し、またusart0_Rxで受信データを待ち受けます。
さて、実際にUIとこの通信プログラムで通信しようとすると、困ってしまいます。 それは受信プログラムの以下の箇所
while( !(UCSR0A & (1<<RXC0)) );while・・・そうです、この受信プログラムはUCSR0AのRXC0ビットが立つまで、ずっと待っています。 UCSR0AのRXC0ビットは受信データがある場合に立つ受信フラグです。 相手が何か送ってくるまで待つならこれでいいのですが、制御システムはそうはいきません。
こういったときに便利なのが割り込みです。
USARTでは、データ受信時に割り込みを起こすことができます。
割り込みが起こった際には、即座に受信データをバッファに退避させます。
次のデータ受信に備えるために、受信割り込みルーチンは、できるだけ短い時間で終了しなければなりません。さもなくばデータを取りこぼすでしょう。
まあフロー制御をした方がいいのかもしれませんが、その辺は通信速度とMPU速度の兼ね合いですね。
割り込みを使ったUSART受信プログラム
割り込みで受信するのは非常に簡単です。 割り込みルーチンを定義して、内部レジスタで受信割り込みを許可するだけです。 以下のようになります。void usart0_init(void) { //USART 0 制御/状態レジスタ A の設定 UCSR0A = 0x00;//倍速不使用、複数プロッサ動作不使用 //USART 0 制御/状態レジスタ C の設定 //非同期動作、パリティなし 、ストップビット1 //データ長を8ビット(UCSZ02ビットは、UCSR0Bにある) UCSR0C = (1<<UCSZ01)|(1<<UCSZ00); //USARTボーレートレジスタの設定 //9600bpsとすると、 //0x33 - CPUが8MHzのとき //0x67 - CPUが16MHzのとき UBRR0L = 0x33; UBRR0H = 0x00; //USART 0 制御/状態レジスタ Bの設定 UCSR0B = (1<<RXEN0)|(1<<TXEN0)|(1<<RXCIE0);//受信ON、送信ON、受信割り込みOK } //受信バッファのサイズ。コマンドやシステムなどから、通常時に溢れないように決める #define RX_BUFFER_SIZE 128 volatile uint8_t rx_buffer[RX_BUFFER_SIZE]; //受信バッファ volatile uint8_t rx_buffer_wrote_bytes; //受信バッファに書き込んだバイト数 ISR(USART_RX_vect) //USART 受信割り込みベクタの定義 { uint8_t r = UDR0; //受信データの取得 //受信データをバッファに保存。ただし、バッファがあふれた場合は無視する。 if( rx_buffer_wrote_bytes < RX_BUFFER_SIZE) { rx_buffer[ rx_buffer_wrote_bytes ] = r; rx_buffer_wrote++; } }上記にはありませんが、もちろん、受信バッファ内のデータをコマンドとして解析し、 解析後にバッファから受信データを削除するルーチンが必要です。 また、処理が追いつかずにバッファがいっぱいになった場合は以降のデータを無視していますが、 これも他の処理が必要になる場合があるかもしれません。
そこら辺の、コマンド解析部などはまた次回ということで・・