シリアル通信でコンピューターがフリーズする問題と対処法

ArduinoなどのマイコンからUSBシリアル変換チップ経由でMacやPC(以下まとめてPC)とシリアル通信すると、適当に書いたPC側のシリアル通信プログラムがフリーズすることがある。これへの対処法が分かったので書いておく。

シリアル通信とバッファ

マイコン、PCに限らず、大抵のシリアル通信をする計算機はバッファを持つ。プログラム側はバッファへの読み込み書き込みを行うことでシリアル通信を実現している。マイコンやPCのOSは、相手側から来た信号をバイトに変換してバッファに入れていく。また、バッファに書き込まれたバイトを信号に変換して相手側に送信する。プログラム側ではこの「バッファ」を読み込んだり書き込んだりすることしか出来ないのがポイント。

フリーズの対処法

先に対処法。簡潔に言うとバッファを空にするとフリーズしなくなる。
まず、フリーズしてたプログラムから。この例はopenFrameworksで書いた場合で、バージョンは0.8.4。マイコン側から値が0x7eをファーストバイトとして、その後に3byteデータが来るのをパースするものである。

フリーズしていたプログラム

ofSerial serial;

void ofApp::setup(){
	serial.setup("シリアルポート", 9600);
}

void ofApp::update(){
	if (serial.isInitialized()) {
		if (serial.available() > 3){
			if(serial.readByte() == 0x7e) {
				int a = serial.readByte();
				int b = serial.readByte();
				int c = serial.readByte();
			}
		}
	}
}

なにも考えずに書いてたプログラム。4バイト以上バッファに溜まった時、0x7eという値が読み込めたら色々処理をするのだが、これマイコン側から値が全然来ない場合は動くしフリーズもしないのだが、頻繁に来る(例えばoFのデフォルトのフレームレートである60fps以上)とバッファがどんどん増えていき、フリーズする。0x7e以外の値が来た時にたった1バイトしかバッファの読み出しを行わないので。

フリーズしないプログラム1

ofSerial serial;

void ofApp::setup(){
	serial.setup("シリアルポート", 9600);
}

void ofApp::update(){
	if (serial.isInitialized()) {
		if (serial.available() > 3){
			if(serial.readByte() == 0x7e) {
				int a = serial.readByte();
				int b = serial.readByte();
				int c = serial.readByte();
			}
			serial.flush();
		}
	}
}

とりあえずのフリーズを防ぐプログラム。毎フレームごとに問答無用でserial.flush()でバッファを空にすることで、とりあえずフリーズはしなくなるが、バッファの中身が消えてしまう、良くないプログラム。

フリーズしないプログラム2

ofSerial serial;

void ofApp::setup(){
	serial.setup("シリアルポート", 9600);
}

void ofApp::update(){
	if (serial.isInitialized()) {
		while (serial.available() > 4){
			if(serial.readByte() == 0x7e) {
				int a = serial.readByte();
				int b = serial.readByte();
				int c = serial.readByte();
			}
		}
	}
}

if文ではなくwhile文を使うとバッファをきちんと読み込めるようになる。例えば16バイトデータが来てた場合などはserial.flush()している場合は最初のデータ以外は消えるが、今回は全て読み出せる。
しかしこれも完璧ではなく、

if(serial.readByte() == 0x7e) 

の部分で、0x7e以外を読みだした時にきちんと保存し前フレームに読み出せたデータと繋げてデータとしてなってないか、までやるとようやく全てのデータを捨てずに読み込むことになる。そこまでする必要があるかどうかは、アプリケーション次第。

フリーズの原因

さて原因はと言うと、PCのシリアルバッファのオーバーフロー……ぽい。ここは詳しく検証していないので断言できないものの、バッファが溢れるとなにかと悪いことが起きる印象。また、値が遅延しているなと思ったら、バッファにかなり昔に書き込まれた情報を読み出している可能性が高い。つまり、バッファに入るデータの増加に読み出しが追いついてない状態。
ofSerial::available()でバッファにあるデータのバイト数が分かるので、それをコンソールに出力したらひたすら増加していったので、ここを直したら直ったという次第だったので、変だなと思ったら、ofSerial::available()を出力するのオススメです。

あとがき

知っている人には当たり前すぎてそんなところで引っかかってたの?!的な話だろうな〜とは思うものの、シリアル通信についてちゃんと学んだことが無かったのでまとめておきました。一度シリアル関係でプログラムがフリーズすると、プログラムがシリアルポートを占有してしまうらしく、PCの再起動しかシリアルポートを解放する手段が無くなってしまってやっかいだしね。

参考資料

XBeeで作るワイヤレスセンサーネットワーク (Make: PROJECTS)

XBeeで作るワイヤレスセンサーネットワーク (Make: PROJECTS)