30 March 2008

シリアルポートからのデータ受信 (WindowsおよびWindows CE)

シリアルポートに接続したセンサーが吐き出すデータを、WindowsパソコンやWindows Mobile端末で受信するための通信制御部分。

双方のOSで共通のAPIを利用しているので、同じプログラムを流用できます。

(Visual Studio 2003 と eMbedded Visual C++ 4.0 の双方で確認済み。)

CSerialCom クラスの宣言部
#define COM_BUFFERSIZE 2048 class CSerialCom { public: CSerialCom(void); ~CSerialCom(void); HANDLE m_hComm; int Open(LPCTSTR lpsPortName, DWORD _BaudRate=9600, BYTE _ByteSize=8, BYTE _Parity=NOPARITY, BYTE _StopBits=ONESTOPBIT, DWORD _RtsControl=RTS_CONTROL_DISABLE); void Close(void); int ReadLine(BYTE Delimiter=0x0a); int ReadLineEx(BYTE Delimiter=0x0a); int WriteRaw(int nSize); BYTE m_LineBuffer[COM_BUFFERSIZE+1]; BYTE m_LineBufferEx[COM_BUFFERSIZE+1]; BYTE m_WriteBuffer[COM_BUFFERSIZE+1]; protected: int ReadRaw(void); BYTE m_DataBuffer[COM_BUFFERSIZE]; int nLineStart; int nLineSize; int nSizePrevSaved; int nSizeSaved; };
クラスの初期化 (コンストラクタ、デストラクタ)
CSerialCom::CSerialCom(void) { // クラス内変数の初期化 m_hComm = 0; // ファイルハンドルが割り当てられていない時、ゼロ nLineStart = 0; // 次の切り出しデータの先頭ポインタを初期化 nLineSize = 0; // 読み込み済みRawデータのサイズを初期化 nSizeSaved = 0; nSizePrevSaved = 0; // 1行分のデータを複数回で読み取る場合、バッファにすでに格納されているデータサイズの初期化 } CSerialCom::~CSerialCom(void) { // ポートが開きっぱなしの場合、閉じる if(m_hComm) Close(); }
シリアルポートを開く
/******** Open() シリアルポートを開く 入力) ポート名(Windowsの時は "\\\\.\\COM1"、CEの時は "COM1:"):lpsPortName 速度:_BaudRate 出力)ファイルハンドル:m_hComm 戻り値) 正常時:1 エラー時:0 ********/ int CSerialCom::Open(LPCTSTR lpsPortName, DWORD _BaudRate, BYTE _ByteSize, BYTE _Parity, BYTE _StopBits, DWORD _RtsControl) { DCB _Dcb; COMMTIMEOUTS _Timeouts; if(m_hComm) return 0; // ファイルハンドルが既に割り当てられている時、エラー // COMポートを開く m_hComm = ::CreateFile(lpsPortName, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(m_hComm == INVALID_HANDLE_VALUE) { // COMポートが開けない時 m_hComm = 0; return 0; } // COMポートのタイムアウト時間の設定 if(!::GetCommTimeouts(m_hComm, &_Timeouts)) { Close(); return 0; } _Timeouts.ReadTotalTimeoutMultiplier = 0; _Timeouts.ReadTotalTimeoutConstant = 5000; // 5秒 if(!::SetCommTimeouts(m_hComm, &_Timeouts)) { Close(); return 0; } // COMポートの設定 ::ZeroMemory(&_Dcb, sizeof(_Dcb)); _Dcb.BaudRate = _BaudRate; _Dcb.ByteSize = _ByteSize; _Dcb.Parity = _Parity; _Dcb.StopBits = _StopBits; _Dcb.fRtsControl = _RtsControl; if(!::SetCommState(m_hComm, &_Dcb)) { Close(); return 0; } // 入出力バッファを全てクリア ::PurgeComm(m_hComm, PURGE_RXCLEAR); ::PurgeComm(m_hComm, PURGE_TXCLEAR); ::PurgeComm(m_hComm, PURGE_RXABORT); ::PurgeComm(m_hComm, PURGE_TXABORT); return 1; }
シリアルポートを閉じる
/******** Close() シリアルポートを閉じる 入力)ファイルハンドル:m_hComm ********/ void CSerialCom::Close(void) { if(m_hComm == 0) return; // ファイルハンドルが割り当てられていない時 // 入出力バッファを全てクリア ::PurgeComm(m_hComm, PURGE_RXCLEAR); ::PurgeComm(m_hComm, PURGE_TXCLEAR); ::PurgeComm(m_hComm, PURGE_RXABORT); ::PurgeComm(m_hComm, PURGE_TXABORT); // ファイルハンドルを閉じる ::CloseHandle(m_hComm); m_hComm = 0; }
シリアルポートからデータを読み込む
/******** ReadRaw() シリアルポートからデータを読み込む 入力)  シリアルポートのハンドル:m_hComm 出力)  データ格納先:BYTE m_DataBuffer[COM_BUFFERSIZE] 戻り値) 読込完了時:データサイズ(0~データのバイト数) エラー時:-1 ********/ int CSerialCom::ReadRaw() { DWORD _ErrorMask; COMSTAT _Stat; DWORD _nReadedSize = 0; if(m_hComm == 0) return -1; // シリアルポートが開かれていない場合、エラー // シリアルポートのデータバッファに溜まっているデータのサイズ(_Stat.cbInQue)を得る if(!::ClearCommError(m_hComm, &_ErrorMask, &_Stat)) { // 読み込みエラー return -1; } if(_Stat.cbInQue <= 0) { // バッファ上のデータがゼロの時 return 0; } // シリアルポートからデータを読み込む if(!::ReadFile(m_hComm, &m_DataBuffer, _Stat.cbInQue > COM_BUFFERSIZE ? COM_BUFFERSIZE : _Stat.cbInQue, &_nReadedSize, NULL)) { // 読み込みエラーの時 // エラー処理をここに書く return -1; } return _nReadedSize; }
生データを読み込み、デリミタで区切られたデータに切り出す
/******** ReadLine(BYTE Delimiter) シリアルポートからデータを読み込み、デリミタで区切られたデータに分割、1行分のデータを取り出す (バッファに前のデータが残っている場合は、そこから切り出す。バッファにデータが無い場合は シリアルポートから読み出す) デリミタ文字も切り出し後のデータとして出力される 切り出しデータは文字列だけではないので、データの末尾にNULLは付けられない形での出力 ※ シリアルポートから読み出したデータが、1行分無い(途中で切れている)場合は、   そこまでの出力となる。1行、完全なデータになるまで読み込むのは ReadLineEx関数を使う。 入力) デリミタ(データを区切る1バイト):BYTE Delimiter 内部使用しているクラス変数) Rawデータバッファ(ReadRaw関数から受け取る):BYTE m_DataBuffer[COM_BUFFERSIZE] Rawバッファに格納されているデータサイズ:int nLineSize 次に切り出すデータの先頭ポインタ:int nLineStart 依存)  ReadRaw() 関数を呼び出して、シリアルポートからまとめてデータを読み込む 出力)  切り出したデータ:BYTE m_LineBuffer[COM_BUFFERSIZE+1] 戻り値) 読込完了時:データサイズ(0~データのバイト数) エラー時:-1 ********/ int CSerialCom::ReadLine(BYTE Delimiter) { int _nWaitCounter = 0; // 受信失敗をリトライする回数 int nSize; // 切り出し後のデータサイズ int i; DWORD nDummy = 0; if(nLineSize == 0) { // バッファに残りのデータが無い場合、シリアルポートからデータを読み込む nLineStart = 0; // 次解析データの先頭を、バッファ先頭へ for(;;) { nLineSize = ReadRaw(); // シリアルポートからデータ読み込み if(nLineSize > 0) break; // データが読み込めた場合、ループ抜ける if(nLineSize < 0) return -1; // エラーのとき、リターンする(エラー) // データが読み込めなかった場合(データサイズがゼロの時) ::Sleep(10); // 10msec 待つ // データが受信できていない場合でも、一定時間おきに上位関数に制御を一旦返す // ことによって、ユーザによる処理割り込みを可能にする処理のため if(_nWaitCounter++ > 50) return 0; // 0.5秒待って読み込めないときは、リターン(データサイズゼロ) } } // デリミタ(1バイト)の位置を探す for(i=nLineStart; i<nLineSize; i++) { if(m_DataBuffer[i] == Delimiter) break; } if(i>=nLineSize) i--; // バッファの最後まで到達した時、データ長さまで戻す nSize = i-nLineStart+1; // デリミタで区切られたデータ1行のバイト数計算 ::memcpy(&m_LineBuffer, m_DataBuffer+nLineStart, nSize); // 1行分のデータをコピー if(i >= nLineSize-1) nLineSize = 0; // バッファ読み終わりの場合、バッファサイズをゼロにして、次のシリアルポート読み込みに備える nLineStart = i+1; // 次解析のデータの先頭を、今回切り出したデータの一つ後ろにずらす return nSize; // データのバイト数を返す } /******** ReadLineEx(BYTE Delimiter) シリアルポートからデータを読み込み、デリミタで区切られたデータに分割、1行分のデータを取り出す (バッファに前のデータが残っている場合は、そこから切り出す。バッファにデータが無い場合は シリアルポートから読み出す) デリミタ文字も切り出し後のデータとして出力される 切り出しデータは文字列だけではないので、データの末尾にNULLは付けられない形での出力 ※ データが1行分になるまで、複数回にわたりシリアルポートからデータを受信する。 入力) デリミタ(データを区切る1バイト):BYTE Delimiter 内部使用しているクラス変数) 切り出し前のデータ(ReadLine関数から受け取る):BYTE m_LineBuffer[COM_BUFFERSIZE+1] 切り出し先バッファm_LineBufferExに一時格納されているデータサイズ:int nSizePrevSaved 依存)  ReadLine() 関数を呼び出して、1行分データ(シリアル通信単位考慮せず)を読み込んでいる 出力)  切り出したデータ:BYTE m_LineBufferEx[COM_BUFFERSIZE+1] 戻り値) 読込完了時:データサイズ(0~データのバイト数) エラー時:-1 ********/ int CSerialCom::ReadLineEx(BYTE Delimiter) { int nSize; nSizeSaved = 0; // 保存済みデータクリア(データはm_LineBufferEx) if(nSizePrevSaved >= 0) { // 前回、サイズ超過で結合付加だった文字列から処理開始 // 保存済みデータに、前回結合付加文字列をコピー ::memcpy(m_LineBufferEx, m_LineBuffer, nSizePrevSaved); nSizeSaved = nSizePrevSaved; nSizePrevSaved = 0; } for(;;) { nSize = this->ReadLine(); // m_LineBufferに読み込む if(nSize <= 0) return nSize; // エラー、または読み込みサイズゼロはいったん帰る if(nSize + nSizeSaved < COM_BUFFERSIZE) { // 保存済みデータ + 今回データがバッファサイズ以下のとき、結合 ::memcpy(m_LineBufferEx + nSizeSaved, m_LineBuffer, nSize); if(m_LineBufferEx[nSizeSaved + nSize - 1] == Delimiter) { // データ終了デリミタが検出されたとき return nSizeSaved + nSize; // データ1行分読み込み完了 } nSizeSaved += nSize; // 保存済みデータのサイズ更新 } else { nSizePrevSaved = nSize; // 次回、処理開始するデータサイズ(データはm_LineBuffer) return nSizeSaved; // 今回は、すでにたまっているデータを返す(データはm_LineBufferEx) } } }

あとは、必要に応じて適当に処理をくっつけてゆけばよし

製作時間、2時間。 自由に使ってもらってもいいですが、責任持ちませんよ…


GPS受信機(BT-359W)からデータを受信したときの例

$PSRFTXT,Version:GSW3.2.4_3.1.00.12-SDK003P1.00a $PSRFTXT,Version2:F-GPS-03-0702021 $PSRFTXT,WAAS Disable $PSRFTXT,TOW: 25510 $PSRFTXT,WK: 1473 $PSRFTXT,POS: -3883590 3281983 3308624 $PSRFTXT,CLK: 94790 $PSRFTXT,CHNL: 12 $PSRFTXT,Baud rate: 38400 $GPGSA,A,3,21,15,24,18,,,,,,,,,9.8,3.7,9.1*37 $GPGSV,2,1,08,15,61,018,18,24,50,215,20,26,50,035,14,09,50,204,*7E $GPGSV,2,2,08,21,44,283,43,18,31,312,36,10,31,115,,28,13,059,*70 $GPRMC,070753.000,A,3427.5640,N,13603.0389,E,1.10,226.76,300308,,*09 $GPVTG,226.76,T,,M,1.10,N,2.0,K*65 $GPGGA,070754.000,3427.5642,N,13603.0384,E,1,04,3.7,106.4,M,34.3,M,,0000*56 $GPGLL,3427.5642,N,13603.0384,E,070754.000,A*39 $GPRMC,070754.000,A,3427.5642,N,13603.0384,E,0.91,259.78,300308,,*0F …

(緯度経度は書き換えていますよ)