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




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