How to collect data from monitors


Borland C++Builderを使用してリアルタイムデータ処理を行うソフトウェアを構築するために


注意

私の知識不足で,これまでこのページに記載していたC++のソースコードの一部が正しく表示されておりませんでした.この度改訂して訂正致しました.お詫びいたします.

注意2

ソースコードのうち以下のReadFile関数の第2変数はIOBufが正しく,以前の&IOBufは間違いでした今回の改訂で訂正致しました.
        ReadFile(ser_hdl, IOBuf, BUFFERSIZE-DataCount,
        &DataLength, NULL);
last update 2006.07.15

目次

  1. はじめに
  2. デジタルデータ取得の方法
  3. デジタルデータ取得ソフトウェアの構築方法
  4. アナログデータ取得の方法
  5. アナログデータ取得ソフトウェアの構築方法
  6. タイマーに関して
  7. おわりに

1.はじめに

臨床研究をする際には各種の生体情報を記録する必要がありますが,これらを手動で記録したりモニターのプリントアウトを利用したりするのでは非常に煩雑ですし,記録ミスなど間違いの元となります.これらを自動的にコンピュータに取り込めれば記録の保管や後のデータ処理が非常に楽になります.学会発表や論文に使用するグラフもグラフ描画ソフトウェアを使用すれば美しく仕上げることができます.従ってモニターのデータをコンピュータへ自動で取り込むことができるなら理想的なことです.
ここではモニターからの情報をコンピュータに取り込むソフトウェアの構築法の実際について解説します.ここで紹介するソフトウェアはいずれもWindows95/98/NT/2000/XPで動作するものです.個人的には2004年からMacintoshを使用するようになりましたが現在のところMacintoshのソフトウェアは開発していないためここにはありません.開発環境が整えばMacintoshのソフトウェアも開発していく予定です.
多くのモニターはデジタルもしくはアナログの外部出力を持っていますから,理論的にはデータを取り込むことは可能です.ただし,実際にはコンピュータとモニターの接続方法(ケーブル接続の方法)や通信プロトコルなどの知識が必要になります.これらの情報はメーカーから入手しなければなりません.
ここに解説している実際のプログラムコードはBorland C++Builder Ver.5 Pro(Patch1)で私が開発した種々のソフトウェアで実際に実装しているものです.コードそのものはC++Builder Ver.3やVer.4でもそのまま利用できると思います.
これまでは,background I/Oを使用しておらずWin95/98/98SE/MEでは動作するもののWinNT/2000/XPではうまく動作しませんでした.今回WinNT/2000/XPのOSにも対応したコードに改定しました.

2.デジタルデータ取得の方法

2-1.Serial connection (RS232C)

現在のところモニター機器のデジタル出力の多くはシリアル(RS232C,RS422など)です.この場合に必要な情報は(1)通信パラメータ,(2)ケーブルの結線方法,(3)通信プロトコルなどです.

2-1-1.通信パラメータ

シリアル接続の通信パラメータにはbaud rate(300,600,1200,2400,4800,9600,19200,38400,57600,115200bps), bitサイズ(8bit, 7bit), stop bit (1,1.5,2), parity bit (none, even, odd)およびハンドシェイクの方法(no flow control, X-on/Xoff, RTS/CTS, DTR/DTR)が必要です.

2-1-2.ケーブルの結線方法

シリアル接続で使用されるコネクタは通常DSUB-9pinもしくはDSUB-25pinでWindowsマシンの場合9pinはケーブル側がメス,25pinはケーブル側がオスになっています.
ケーブル接続には一般的に"クロス"と呼ばれるコンピュータ同士を接続する際に使用するケーブルと"ストレート"と呼ばれるコンピュータとモデムを接続する際に使用するケーブルがあります.通常はどちらかを使用すればよいのですが,ケーブル接続には厳密な規格はありませんので,各モニターによってまちまちであるのが現状です.従ってメーカーからの情報が必須であることもあります.
最もよく用いられているクロスケーブルではRxD-TxD, TxD-RxD, GND-GNDの3本を接続すれば無手順での通信が行えます.さらに通信制御する場合にはCTS-RTS, RTS-CTSの接続やDTR-DSR, DSR-DTRといった接続方法が取られますが,先に書きましたようにこの制御ラインの接続にはきっちりした規格がありません(どのように使っても構わない)のでメーカーの情報を元に接続しなければなりません.尤も多くのモニターは無手順の垂れ流し式データ出力を行っていますのでデータラインの3本のみの接続で動作することも多いと考えられます.ただこの場合にはデータの取りこぼしに注意しなければなりません.

2-1-3.通信プロトコル

通信プロトコルは完全にソフトウェアレベルの問題で各社まかせですからこれもモニターメーカーからの情報が必要です.いくつかのモニターについては業者から得た情報がありますので,これを利用してデータ取得ソフトを作成しました.
現在以下のモニターのデータが取得可能です.(4,5,10については単体のソフトは作成していません.)
いくつかのソフトウェアは私のソフトウェアライブラリからdownload可能です.

統合型モニター
1. BP-508 (日本コーリン).....血圧,心拍数,SpO2など.(波形データはアナログで取得可能)
ガスモニター
2. CAPNOMAC (DATEX).....PETCO2, 麻酔ガス
パルスオキシメータ
3. N6000 (ネルコア).....SpO2, PETCO2
4. N200 (ネルコア)
5. N550, N595 (Tyco health care)
6. Masimo (マシモ)
7. BIOX-3700 (Ohmeda)
脳波モニター
8. BISモニター (A-1050,A-2000).....各種脳波パラメータ,波形データ
その他のモニター
9. 連続血液ガス測定装置(Paratrend7; バイエルン).....血液ガスデータ(測定値,温度換算後の値 etc.) 現在プローブが入手できなくなり使用不能になっています.
10. 血液凝固モニター(SonoClot; サイエンコ).....ClotSignal
11. DDGアナライザー(DDG2001; 日本光電).....血中ICG濃度
12. 連続心拍出量モニター(Vigilance; Baxter).....CO, SvO2
13. 混合静脈血酸素飽和度モニター(Oximetrix3; Baxter)..... SvO2

3. デジタルデータ取得ソフトウェアの構築方法

Borland C++Builderでシリアルポートを介してデータ取得を行うにはWin32APIをコールする必要があります.また,画面操作と平行してデータ取得を行うためにはThreadオブジェクトを作成するのが望ましいと考えられます.
ここでは(1)シリアルコントロールを行うためのWin32APIの操作方法と(2)Threadオブジェクトの操作に関して解説します.

3-1. シリアルコントロール関係のWin32APIの操作方法

Windowsでは外部デバイスのopen,closeにはCreatFile, CloseHandle関数を用います.また,種々の設定やコントロールのためにいくつかの構造体を用います.私が使用しているのは_DCB,_COMSTAT, _COMMTIMEOUTS, OVERLAPPEDの4種です.またファイルハンドル(HANDLE)もデバイスのopen, closeに必要です.構造体へのデータの受け渡しを助ける関数にはGetCommState関数,GetCommTimeouts関数, BuildCommDCB関数,SetCommState関数,SetCommTimeouts関数,SetCommMask関数などがあります.さらに実際のデータの入出力を担うのはReadFile関数とWriteFile関数です.エラーの後始末を行うのはPurgeComm関数です.これらの使い方について以下に解説します.
少し工夫をすればSerial ControlをThreadの派生Classとして定義してしまえば,複数のシリアルポートを比較的簡単にコントロールできるようにもなりますがここでは割愛しています.これまでにComtrol社のボードを用いて8チャンネルまでコントロールした経験があります.

3-1-1.初期設定

最初にWin32APIのコントロールとデータ入出力に必要なデータ領域を確保します.
#define BUFFERSIZE 1024
#define LineMax 256
#define TIMEOUT_MUL 50

HANDLE ser_hdl;
struct _DCB dcb;
struct _COMSTAT comstat;
struct _COMMTIMEOUTS timeout;
DWORD SerialEvent;
OVERLAPPED o_send, o_receive, o_event;

unsigned long ByteRead;
unsigned long ByteWrite;
unsigned long DataLength;
unsigned long ErrorMask;
char IOBuf[BUFFERSIZE];
char RingBuf[BUFFERSIZE];
char DataBuf[BUFFERSIZE];
char OutBuf[256];
int WritePoint,ReadPoint;
unsigned long DataCount;
char ev_chr;

ser_hdlはファイルハンドルです.dcbはデバイスコントロールブロック,comstatはシリアルポートステータスブロック,timeoutは通信タイムアウトブロックを示します.
void __fastcall COMsetup(void)
{
    o_send.Internal = 0;
    o_send.InternalHigh = 0;
    o_send.Offset = 0;
    o_send.OffsetHigh = 0;
    o_send.hEvent = CreateEvent(NULL, true, false, NULL);

    o_receive.Internal = 0;
    o_receive.InternalHigh = 0;
    o_receive.Offset = 0;
    o_receive.OffsetHigh = 0;
    o_receive.hEvent = CreateEvent(NULL, true, false, NULL);

    o_event.Internal = 0;
    o_event.InternalHigh = 0;
    o_event.Offset = 0;
    o_event.OffsetHigh = 0;
    o_event.hEvent = CreateEvent(NULL, true, false, NULL);

//  Initialize COM port
    ser_hdl = CreateFile("COM1",
    GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,
    NULL);
    GetCommState(ser_hdl, &dcb);
    GetCommTimeouts(ser_hdl, &timeout);
    BuildCommDCB("baud=9600 parity=N data=8 stop=1",&dcb);
//    BuildCommDCBAndTimeouts("baud=9600 parity=N data=8 stop=1",&dcb,
//    &timeout);

    dcb.fRtsControl = RTS_CONTROL_DISABLE;
    dcb.fOutxDsrFlow = FALSE;
    dcb.fDsrSensitivity = FALSE;
    dcb.fAbortOnError = FALSE;
    dcb.EvtChar = ev_chr;

    timeout.WriteTotalTimeoutConstant = 0;
    timeout.WriteTotalTimeoutMultiplier = TIMEOUT_MUL;
    timeout.ReadIntervalTimeout = MAXDWORD;
    timeout.ReadTotalTimeoutConstant = 0;
    timeout.ReadTotalTimeoutMultiplier = TIMEOUT_MUL;

    SetCommState(ser_hdl,&dcb);
    SetCommTimeouts(ser_hdl,&timeout);

    ReadPoint = 0; WritePoint = 0; DataCount = 0;
}

//---------------------------------------------------------------------------
__fastcall TSerialThread::TSerialThread(TThreadPriority StartPriority) : TThread(true)
{
    Priority = StartPriority;
    FreeOnTerminate = true;
    Resume();
}
//---------------------------------------------------------------------------
__fastcall TSerialThread::~TSerialThread(void)
{
    if( ser_hdl != NULL ){
        COMclose();
        ser_hdl = NULL;
    }
}
//---------------------------------------------------------------------------
最初にbackground I/OのためのOVERLAPPED構造体を定義します.ここで注意すべきことはOVERLAPPEDで用いるイベントをmanual resetにしなければならないことです.次に,CreateFile関数を使用してシリアルポートをオープンします.第1引数はポート名です."COM1", "COM2"という名前を使用します.多チャンネルのシリアルポートのついたボードを使用して10個以上のポートを使用する場合には10番目以降のポートに対しては"\\\\.\\COM10"といった特殊な名前を用いる必要がありますので注意してください.それ以外のパラメータは上記の通りにして下さい.OVERLAPPEDモードでは第6変数にFILE_FLAG_OVERLAPPEDを指定しておきます.
次にGetCommState関数とGetCommTimeouts関数を用いて現在のパラメータをdcbとtimeoutに読み込みます.さらにBuildCommDCB関数を用いて通信パラメータをdcbに設定します.通信パラメータは通信速度(baud)の値,パリティ(parity)の値(N=none, E=even, O=odd),データビット(data)の数(6,7,8),ストップビット(stop)の値(1,1.5,2)を1つの文字列で指定します.上記の例では9600bps,non parity,8bit,1stopというものです.なお上記の例ではコメントアウトしていますがBuildCommDCBAndTimeouts関数を用いてdcbとtimeout構造体に一度にデータを設定する方法もあります.
その次はハンドシェークの設定です.dcb.fRtsControlはRTS/CTSのハードウェアハンドシェークを行うかどうかを指定します.RTS/CTS制御をしない場合にはRTS_CONTROL_DISABLEを,RTS/CTS制御をする場合にはRTS_CONTROL_HANDSHAKEを指定します.また,DSR/TDR制御をする場合にはdcb.fOutxDsrFlowと,dcb.fDsrSensitivityをTRUEに,しない場合にはFALSEに設定します.通信エラーが起こった時にAbortするようにしたい場合にはdcb.fAbortOnErrorをTRUEに,しない場合にはFALSEに設定します.
ハードウェアハンドシェイクではなくXon/Xoffによるソフトウェアハンドシェイクを行う場合にはdcb.fInXおよびdcb.fOutXをtrueに設定しdcb.XonCharとdcb.XoffCharを設定しておく必要があります.この場合にはハードウェアハンドシェイクはdisableしておきます.
特定のデータが受信された場合にEventを発生させたい場合にはそのデータ(charコード)をdcb.EvtCharに設定します.これはASCIIデータを受信する場合に改行コードなどを基準にデータを取り込みたい場合に有用です.
あとはtimeout関係の設定を行って構造体に設定したデータをシステムにSetCommState関数とSetCommTimeouts関数を使用してシステムに通知します.最後にアプリケーション側で用意しているRing Bufferのコントロール変数を初期化します.
なおここには後述するThreadオブジェクトのコンストラクタデストラクタのリストも含まれています.

3-1-2. ASCIIデータの受信

データの読み込みにはReadFile関数を用います.OVERLAP構造体を用いたbackground読み込みのためにRead_BackGround関数を作成しています.以下にはThreadオブジェクトのExecute関数も示しています.
//  Get and Fill RingBuf from System Buffer
void __fastcall Read_Background(DWORD cnt)
{
    DWORD   endtime;

    if(!ReadFile(ser_hdl, IOBuf, cnt, &ByteRead, &o_receive) ){
        ByteRead = 0;
        if( GetLastError() == ERROR_IO_PENDING ){
            endtime = GetTickCount() + 200;
            while( !GetOverlappedResult(ser_hdl, &o_receive, &ByteRead, FALSE) ){
                if( GetTickCount() > endtime ){
                    break;
                }
            }
        }else{
            GetLastError();
        }
    }
    ResetEvent(o_receive.hEvent);
    return;
}
//  Get and Fill RingBuf from System Buffer
void __fastcall COMread_line(void)
{
    unsigned int i;

    ClearCommError(ser_hdl, &ErrorMask, &comstat);
    if(comstat.cbInQue == 0){
        return;
    }
    if( comstat.cbInQue > BUFFERSIZE-DataCount ){
        Read_BackGround(BUFFERSIZE-DataCount);
    }else{
        Read_BackGround(comstat.cbInQue);
    }
    if(DataLength == 0){
    	return;
    }
    for(i=0;i<DataLength;i++){
	RingBuf[WritePoint++] = IOBuf[i];
        DataCount++;
        if( WritePoint >= BUFFERSIZE){
            WritePoint = 0;
        }
    }
}

//  Get 1 line from RingBuf if exist then return TRUE
//                                   else return FALSE
int __fastcall get_COM_line(char *buf)
{
    unsigned long i;
    int j;

    COMread_line();
    for(i=0,j=ReadPoint;i<DataCount;i++){
        if( (*buf = RingBuf[j]) == ev_chr ){
            *buf = 0;
    	    break;
        }
//  ignore control code ( <0x20 )
        if( *buf >= 0x20 ){
    	    buf++;
        }else{
            *buf = 0;
        }
    	if(++j == BUFFERSIZE){
    		j=0;
    	}
    }
    if( i >= DataCount ){
    	return(FALSE);
    }
    if( ++j == BUFFERSIZE ){
    	j = 0;
    }
    ReadPoint = j;
    DataCount -= (i+1);
    return(TRUE);
}
//---------------------------------------------------------------------------
void __fastcall TSerialThread::Execute(void)
{
    ev_chr = 0x0a;

    COMsetup();
    if( ser_hdl == NULL ){
        return;
    }
    for(;;){
        SetCommMask(ser_hdl, EV_RXFLAG);
        WaitCommEvent(ser_hdl, &SerialEvent, &o_event);
	for(;;){
	    if( WaitForSingleObject(o_event.hEvent,200) == WAIT_OBJECT_0 ){
                ClearCommError(ser_hdl, &ErrorMask, &comstat);
                ResetEvent(o_event.hEvent);
//	if Serial communication error occured, then call PurgeComm
                if( ErrorMask|CE_IOE ){
                    PurgeComm(ser_hdl,PURGE_REABORT);
                }
                break;
            }	
            if(Terminated){
                return;
            }
        }
        for(;;){
            if( get_COM_line(DataBuf) == FALSE ){
                break;
            }
//    Add DataBuf processing routines here !!
        }
    }
}
TSerialThreadのExecute関数ではdcb.EvtCharを設定した場合のEvent driven方式の場合の記述をしています.この方法はASCII文字列を行ごとに受信する場合に有用です.多くの場合改行コード(0x0a)がEvtCharに設定されると思います.WaitCommEvent関数をOVERLAPPED構造体を用いてコールしており,実際のEventはWaitForSingleEventでトラップしています.この例では手を抜いて200msecごとにTerminateをチェックして終了を確認するようにしていますが,通信から抜ける際にもEventで行いたい場合にはEvent Handle2つの配列を用意します.そして,ひとつにはo_event.hEventを設定しもうひとつに終了用Eventを設定しWaitForMultipleObjectsで2つのEventを待つようにすれば完璧にEvent drivenになります.この場合WaitForMultipleObjectsの返す値がWAIT_OBJECT_0かWAIT_OBJECT_0+1かでEventの種類を判別します.
Eventが検知された場合にはWaitForSingleObjectの返り値がWAIT_OBJECT_0であることを確認し,o_event.hEventをResetしてデータの読み込みに移ります.
データの読み込みは以下の関数を用いて行います.COMread_line関数はClearCommError関数を用いてシステムバッファに読み込まれているデータのbyte数を調べ0でなければアプリケーションの一時バッファ(IOBuf)に読み込んだ後RingBufにセットし直します.get_COM_line関数はCOMread_line関数をコールした後,データに1行分のデータが読み込まれていれば,そのデータを指定された文字配列に読み込みTRUEを返します.1行分のデータがなければそのままFALSEを返します.ClearCommError関数は通信エラーを解除するのに用いられる他に上記のようにバッファ内のデータ数を得るためにも用いられます.ここの例ではASCII文字列を想定しておりSetCommMask関数でEventタイプをデータ読み込み時にEventが発生するように指定した後WaitCommEventでEventが発生するまで待機するようにプログラムされています.これによって余分なCPU timeの消費を防ぐことができます.

3-1-3. Binaryデータの受信

バイナリデータを扱う場合にはこのようなEventを用いた読み込みを行うことが困難ですので,Event drivenではなくpollingによってデータを読む必要があります.また,データがASCIIでも区切り文字がないような場合にはバイナリデータと同様の方法で行う必要があります.これらの場合にはSleep関数を使用してCPU timeの消費を抑えながらClearCommError関数で読み込まれたデータ数を監視しながら適宜データを読み込む処理を行います.以下の例では100msec毎にバッファチェックを行うものです.Sleep関数の引数はmsec単位です.
void __fastcall TSerialThread::Execute(void)
{
    int i;

    COMsetup();
    if( ser_hdl == NULL ){
        return;
    }
    for(;;){
        for(;;){
            if(Terminated){
                return;
            }
            COMread_line();
            if( DataCount == 0 ){
                Sleep(100);
                continue;
            }
//    Add DataBuf processing routines here !!
        }
    }
}

3-1-4. データの送信

モニターからのデータ取得は通常垂れ流しのデータを受けるのみの場合が多いのですが,中には受け取ったコマンドに対してレスポンスを返すものも存在します.例えば日本光電のDDGアナライザーなどはこれに当たります.BISモニターのA2000のようにバイナリパケットでのコマンドハンドシェイクが必要なモニターも存在します.次の例はDDG2001のデータ取得を行うプログラムの一部です.
void __fastcall Write_Background(DWORD cnt, char *outbuf)
{
    DWORD   endtime;

    if(!WriteFile(ser_hdl, outbuf, cnt, &ByteWrite, &o_send) ){
        ByteWrite = 0;
        if( GetLastError() == ERROR_IO_PENDING ){
            endtime = GetTickCount() + 200;
            while( !GetOverlappedResult(ser_hdl, &o_send, &ByteWrite, FALSE) ){
                if( GetTickCount() > endtime ){
                    break;
                }
            }
        }else{
            GetLastError();
        }
    }
    ResetEvent(o_send.hEvent);
    return;
}

void __fastcall TSerialThread::Execute(void)
{
    COMsetup();
    if( ser_hdl == NULL ){
        return;
    }
    for(;;){
        if(Terminated){
            break;
        }
        if( get_COM_line(DataBuf) ){
            if( strcmp(DataBuf, "!D;SEND REQ,") == 0 ){
                Write_background(9, &Init_Str);
            }else{
                UpdateLine((String) DataBuf);
                if( get_token(DataBuf, tok)>=2 ){
                    if( (strcmp(tok[0],"#D;") == 0)&&(strcmp(tok[1],"0") == 0)){
                        return;
                    }
                }
            }
        }
    }
    return;
}
このようにデータの送信にはWriteFile関数を使用します.ここでもbackground I/Oを行うためにWrite_background関数を作成しています.

3-1-5. 通信回線のClose

通信が終了したら回線を閉じます.回線を閉じるにはCloseHandle関数を用います.
//---------------------------------------------------------------------------
void __fastcall COMclose(void)
{
    SetCommMask(ser_hdl, 0);
    PurgeComm(ser_hdl, PURGE_RXCLEAR);
    PurgeComm(ser_hdl, PURGE_TXCLEAR);
    PurgeComm(ser_hdl, PURGE_RXABORT);
    PurgeComm(ser_hdl, PURGE_TXABORT);
    GetCommState(ser_hdl, &dcb);

    dcb.fOutxCtsFlow = FALSE;
    dcb.fRtsControl = RTS_CONTROL_DISABLE;
    dcb.fAbortOnError = TRUE;
    dcb.fOutxDsrFlow = FALSE;

    SetCommState(ser_hdl, &dcb);
    CloseHandle(o_receive.hEvent);
    CloseHandle(o_send.hEvent);
    CloseHandle(o_event.hEvent);
    CloseHandle(ser_hdl);
}
まずEvent drivenを解除するためにSetCommMask関数を使用します.また各種エラーを解除するためにPurgeComm関数を呼び,念の為にdcbの設定をハンドシェークなしにして最後にCloseHandle関数で回線を閉じて終了します.dcbの再設定などは必要ないかもしれませんが念のために行っています.

3-1-6. ToDo

ここではモニターからのデータ取得を主眼にしているため,データ送信に関しては特に別Threadで処理を行ったりしていません.またエラー処理に関してもきっちり行っていないため場合によっては不都合が生じる可能性もあります.ただ,現実問題としてここに書いたルーチンで問題が生じたことはありません.これらの点については今後の検討課題としておきます.

3-2. Thread廻りの設定

3-2-1. Threadのヘッダ

まずTThreadからTSerialThreadを派生させるために以下のような設定をヘッダーファイルに加えます.ここでは表処理の画面に取得したデータを表示させたりするための関数が追加されています.VCLの関数をThreadからcallする場合関数によりますがSynchronizeメソッドが必要になることに注意しておいて下さい.Canvas操作などの場合にはCanvasのTryLock(), Unlock()関数を用いてワーク用バッファを制御した上でSynchronizeメソッドを使用しなければWindowsNT/2000/XPではうまく動作しないようです.
class TSerialThread : public TThread
{
public:
    __fastcall TSerialThread(TThreadPriority StartPriority);
    __fastcall ~TSerialThread(void);
private:
    String Message;
    void __fastcall Execute(void);
    void __fastcall UpdateLine(String s);
    void __fastcall UpdateDispLine(void);
};
TSerialThreadのコンストラクタデストラクタおよびExecute関数については先にサンプルとして出しています.TSerialThreadクラスではThreadのpriorityを引数として指定するようにしています.

3-2-2. Threadの開始方法

Threadを開始するにはC++のnewメソッドで行います.
    SerialThread = new TSerialThread((TThreadPriority) 3);
ここではThreadのpriorityも同じに設定できるように設定しています.なおSerialThreadのオブジェクトそのものはメインフォームのクラスヘッダの中で宣言しておく必要があります.
class TForm1 : public TForm
{
__published:	// IDE 管理のコンポーネント
//  Mainのオブジェクトおよびメンバー関数の宣言
    .....
private:	// ユーザー宣言
    TSerialThread *SerialThread;
public:		// ユーザー宣言
    .....
};
上のリストではSerialThread以外の宣言はすべて省略しています.

3-2-3. Threadの終了

Threadを終了させるにはTerminateメソッドを使用します.コンストラクタでFreeOnTerminateプロパティをtrueにしていますからExecute関数が終了すればThreadは自動的に開放されます.Execute関数の中のTerminateプロパティのチェックルーチンでプロパティが変わったことが検知され終了します.
    if( SerialThread ){
        SerialThread->Terminate();
    }

3-2-4. Threadから画面にデータを表示される方法

先に書きましたようにここではUpdateLine関数と UpdateDispLine関数を追加しています.
void __fastcall TSerialThread::UpdateLine(String s)
{
    Message = s;
    Synchronize(UpdateDispLine);
}
//---------------------------------------------------------------------------
void __fastcall TSerialThread::UpdateDispLine(void)
{
    Form1->eEdit->Text = Message;
}
上記のようにsynchronizeメソッドを用いてメイン画面の変更を行います.

3-2-5. Threadとメインプログラムの間でデータの受け渡しをする方法

通常Threadではデータの取得のみを請け負い,取得されたデータの処理は別Threadもしくはメインプログラム側で行うことが多いと思います.このような場合2つのThread間で安全にデータの受け渡しができるように工夫する必要があります.このような場合にはEventとCRITICAL_SECTIONを利用します.これらを使用する場合には予めこれらのメモリー領域を確保しておく必要があります.
CRITICAL_SECTION CritSec_Serial;
HANDLE hGet_event;
と宣言しておいてメインフォームのOnCreate関数の中で
//  Critical Section Initialize
    InitializeCriticalSection(&CritSec_Serial);
    hGet_event = CreateEvent(NULL, false, false, NULL);
というように初期化しておきます.

3-2-7. Critical Sectionを用いたデータ領域の共有

さて取得されたデータを演算スレッド(CalcThread)に送ってここで処理する場合を考えます.この場合SerialThreadと同様の方法でCalcThreadを定義し生成します.CalcThreadのExecute関数は以下のような構造にしておきます.
//---------------------------------------------------------------------------
void __fastcall TCalcThread::Execute(void)
{
    for(;;){
	WaitForSingleObject(hGet_event, INFINITE);
        if( Terminate ){
            return;
        }
        EnterCriticalSection(&CritSec_Serial);
//        Data 取得ルーチンをここに書く
        LeaveCriticalSection(&CritSec_Serial);
//        演算ルーチンをここに書く
    }
}
一方SerialThreadの方でも共有バッファにデータを書くところの前後をEnterCriticalSection(&CritSec_Serial)とLeaveCriticalSection(&CritSec_Serial)で挟んでおき,データが共有バッファにセットできたところで SetEvent(hGet_event)でCalcThreadにEventを送ります.これでThread間で安全にデータの受け渡しが行えます.なおEventやCRITICAL_SECTIONはメインプログラムの終了時にはその領域を開放しなければなりません.これは以下のようにメインフォームのFormDestroy関数に書いておきます.
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
//  Close Event Handle
    CloseHandle(hGet_event); 
//  Delete Critical Section
    DeleteCriticalSection(&CritSec_Serial); 
}

3-2-8. Standard Template Libraryのqueueを用いたデータの受渡し

メインプログラムの複数の箇所からパラメータを一つのThreadに渡しながら処理をさせたいような場合にはSTLのqueueを使用するのが便利です.例えば以下のようなコマンド構造体を作成してqueueで使用します.
#include 
#include 

typedef struct {
  int cmd;
  int param1;
  int param2;
} cmd_packet;

cmd_packet c;

std::queue CMDQueue;
CRITICAL_SECTIONを使用してからqueue cにデータをセットしてpushしてやればCMDQueueにコマンドデータが登録されます.こうしておいてからThreadにEventを送り,CRITICAL_SECTIONから抜けます.
   EnterCriticalSection(&CritSec_DataProc);
   c.cmd = 1;
   c.param1 = 112;
   c.param2 = 300;
   CMDQueue.push(c);
   SetEvent(hCMD_event);
   LeaveCriticalSection(&CritSec_DataProc);
Thread側はEventを受け取ったらCRITICAL_SECTIONに入ってCMDQueueにコマンドが入っていることをempty()関数で確認してからfront()関数でコマンドを取り出しpop()でデータをqueueから取り除きます.cmd_code,cmd_param1,cmd_param2はいずれもDataThreadのprivate変数としておきます.
//---------------------------------------------------------------------------
void __fastcall TDataThread::Execute(void)
{
    for(;;){
	WaitForSingleObject(hCMD_event, INFINITE);
        if( Terminate ){
            return;
        }
        EnterCriticalSection(&CritSec_DataProc);
        if( CMDQueue.empty() == false ){
            cmd_code = CMDQueue.front().cmd;
            cmd_param1 = CMDQueue.front().param1;
            cmd_param2 = CMDQueue.front().param2;
            CMDQueue.pop();
            LeaveCriticalSection(&CritSec_DataProc);
//          実際の処理をここに書く
         }else{
            LeaveCriticalSection(&CritSec_DataProc);
         }
    }
}
このようにすればメインルーチンの複数の箇所からThreadをパラメータ付きで呼ぶことができるようになります.また1箇所からであってもパラメータをThreadに渡して種々の処理をさせることも可能になります.STLの有用な活用法の事例であると言えます.

3-2-9. Threadオブジェクトを使用する場合の注意点

Threadを使用すれば並列処理を簡単に行えますが,Threadの中でAnsiStringを使用するとメモリーリークが生じる危険性があります.これはどのルーチンがメモリー開放の責務を担うか明確にできないところがあるからです.

4.アナログデータの取得方法

4-1.アナログ接続に必要な情報

アナログ接続の場合に必要な情報は出力電圧の範囲とスケール(出力が現実の値とどのように対応しているか)です.これがあればA/D converterを使用してデータを取得することが可能になります.

4-2.A/D converter使用時の注意点

Windowsマシンで使用可能なA/D converterは数多くあります.ただベッドサイドでの使用を考えればコンピュータはNote型の方が便利ですからPCMCIAのカード型のものか,Parallel/Serialポートに接続するもの,USBポートに接続するものが選択枝になってきます.C++Builderで利用できるものとしてRatoc社製のRex5054Bを選択して使用しています.Visual C++に対応したA/D converterの方が多くC++Builderに対応しているものは残念ながらわずかです.これは12bit 4chのbi-polar式のものです.A/D converterにはsingle-endedのものとbi-polarのものがありますが,基本的にはbi-polarの方がノイズには強くなります.ただチャンネル数はその分少なくなります.

4-3.A/D converter選択時の注意点

通常のA/D converterでは多チャンネルでもconverterそのものは1個しか付いていないことが多いので,このようなタイプを用いる時にはチャンネル間の時間のずれが問題となることがあります.特にミリ秒単位の計測を行う時には大きな問題になります.見た目には同時にデータを取得しているように見えますが,現実にはチャンネルを切り替えてサンプリングする間の時間に差が生じます.例えば100Hzで2チャンネルの波形をサンプリングしていたとするとA/D converterそのものは200Hzで2チャンネルを交互にサンプリングすることになります.この場合1/200(sec)=50(msec)の時間差が生じます.これが問題にならないような事象の場合にはよいのですが,これに近いオーダーの時間計測を行うなら問題になります.
このような時間差が問題になる場合にはA/D converterそのものも複数搭載されているA/D converterを使用する必要があります.こういった製品も存在します.購入前に確認するようにして下さい.
また,取得した波形データの周波数解析を目的とする場合には2nのサンプリング周波数が設定できた方がbetterです.これはfast Fourier Transform (FFT)を行う場合には2n個のサンプルを使用するからです.

4-4. REX5054B/UをC++Builderで使用する

C++Builderで使えるPCMCIAタイプのカードは限られています.幸いにもRatoc社のREX5054B/UはC++Builderで使えるだけでなくサンプリングクロックを8MHzまたは10MHzに設定できるためFFTに都合のよい2nのサンプリング周波数(128Hz,256Hz,512Hz...)が設定できます.
ここではREX5054B/UをC++Builderから使用する方法について解説します.なお,Win95/98/Me用とWinNT/2000/XP用のライブラリは別ですのでそれぞれのOSにあわせてライブラリとヘッダーファイルを使用して下さい.当初WinNT/2000/XP用のライブラリにはWin95/98/Me用ライブラリに存在する一部の関数が実装されていませんでした.しかしその後の改訂で必要な関数のほとんどは実装されました.できるだけ最新のライブラリを御使用下さい.

4-4-1. C++Builderに必要なファイル

C++BuilderからREX5054関連の関数を使用するには,adlib32b.h(ヘッダファイル)とAdlib32b.lib(インポートライブラリ)が必要です.2001年版のドライバーセットに含まれているadlib32b.hには旧版に含まれていた以下の2行が無くなっています.ライブラリそのものには含まれているようですので加えておいて下さい.
DllImport BOOL APIENTRY 	VbGetRingBufConst( LPWORD, BOOL, BOOL );
DllImport int APIENTRY 		AdGetRingBufConst( LPVOID, int, int );

4-4-2. Import libraryのプロジェクトへの取り込み

Ratocのドライバーに添付されているReadme.TXTには直接プロジェクトファイルに追加記入するように書かれていましたが,それよりもC++Builderのメニューバーの「表示(V)」からプロジェクトマネージャーを選択し,この中でマウスの右クリックを行い「追加」からadlib32b.libを追加するのが正しい方法だと考えられます.これによってimport libraryをプロジェクトに追加することができます.これでREX5054をコントロールするための関数群を利用する準備が整いました.

4-4-3. REX5054のコントロールに必要なパラメータ

まずREX5054を使用するために定義するパラメータを以下に示します.これらはドライバーに附属のサンプルソースから流用したものです.
BOOL        Adcfound;
WORD	    MyIOAdrs;				// card IO base address
WORD  	    MyIrqNo;				// card IRQ number
WORD        MyCardType;                         // card type 1=>REX5054U、2=>REX5054B
WORD        SampFreq;				// sampling rate
WORD        Channels;				// number of cannel
DWORD       SampLen;				// sampling number per 1channel
DWORD       AdcCount;				// total data count
int         AdStopFlag = 1;			// AD converter stop flag

WORD Sequence[4] = {0, 1, 2, 3 };
また,ヘッダファイルにThreadの定義を追加します.
//---------------------------------------------------------------------------
class TAdcThread : public TThread
{
public:
    __fastcall TAdcThread(TThreadPriority StartPriority);
    __fastcall ~TAdcThread(void);
private:
    void __fastcall Execute(void);
};
そしてメインフォームにAdcThreadのオブジェクトを追加します.
class TForm1 : public TForm
{
__published:	// IDE 管理のコンポーネント
//  Mainのオブジェクトおよびメンバー関数の宣言
    .....
private:	// ユーザー宣言
    TAdcThread *AdcThread;
public:		// ユーザー宣言
    .....
};

4-4-4. REX5054の存在確認と初期化

まず最初はREX5054が実装されているかどうかの検出と検出された場合の初期化です.
//
//  check the existence of Rex5054B
//
BOOL CheckCard( HWND hDlg )
{
    WORD 	SlotNo;

// detect REX5054 card and get resource
    Adcfound = false;
    for(SlotNo=0; SlotNo<5; SlotNo++){
        if( AdGetCardResource( hDlg, SlotNo, &MyCardType, &MyIOAdrs,
        &MyIrqNo ) == 0 ){
            Adcfound = true;
            return(true);
        }
    }
    // NoCardInSlot
    return(false);
}
//---------------------------------------------------------------------------
//
//  Setting up Rex5054B
//
int __fastcall SetupADC( HWND hDlg )
{
    if( AdSetParamAuto(hDlg,INTPOSTMSG_MODE) != 0 ){
        MessageBox(hDlg, "Mode Error", "Executing Message",
        MB_OK | MB_ICONSTOP );
        return(false);
    }
    Channels = N_channel;        //  number of channels to use;
    if( AdSetChannel(Channels, (LPWORD) Sequence) != Channels ){
        MessageBox(hDlg, "Channel Error", "Executing Message",
        MB_OK | MB_ICONSTOP );
        return(false);
    }
//  Sampling Rate 512Hz = 8.192MHz / 16000
    if( AdSetInternalClock(INTERNAL_8MHZ, (WORD) 100, (WORD) 160 ) != 0 ){
        MessageBox(hDlg, "Sampling Rate Error", "Executing Message",
        MB_OK | MB_ICONSTOP );
        return(false);
    }
    if( AdGetRingBufAdrs() != 0 ){
        AdFreeRingBuf();
    }
    if( AdAllocRingBuf(hDlg, 10000, 0) != 0 ){
        MessageBox(hDlg, "Ring buffer cannot be allocated", "Executing Error",
        MB_OK | MB_ICONSTOP );
        return(false);
    }
    AdStopFlag = 0;
//  Continuously get AD data
    AdStartIrqRingMode( hDlg, (WORD) 0);
    return(true);
}
//---------------------------------------------------------------------------
//
//  Adc Thread
//
__fastcall TAdcThread::TAdcThread(TThreadPriority StartPriority) : TThread(true)
{
    FreeOnTerminate = true;
    Priority = StartPriority;
    Resume();
}
//---------------------------------------------------------------------------
CheckCard関数はREX5054の実装状況を調べる関数です.実装されていればtrueが返されます.通常はメインフォームの作成時にOnCreate関数内でコールすることになると思います.SetupADCはREX5054の各種設定を行う関数で,ここではポストメッセージ割り込みモードをRing Bufferで使用するようにしています.
まず,AdSetParamAuto関数でINTPOSTMSG_MODEを選択します.続いてAdSetChannel関数でチャンネル数とチャンネルの順序を設定します.REX5054Bの場合には最高4チャンネルです.チャンネルの順序は配列Sequenceに格納しています.
次はサンプリングレートの設定です.AdSetInternalClock関数で指定します.第1引数はINTERNAL_8MHZ, INTERNAL_10MHZのどちらかが選択できます.ここではFFTを睨んで512Hzサンプリングの設定を行っています.AdSetInternalClock関数の第2,第3引数は分周する値を積の形で(1 byte以内になるように)設定します.512Hzの場合には,8.192x1000000/160/100=512となります.
次はRing Bufferの確保です.AdAllocRingBuf関数を使用して確保しますが,その前にもし既に確保されていたら一度AdFreeRingBuf関数で領域を開放してから所定のサイズのバッファを改めて確保するようにしています.
最後にAdStartIrqRingMode関数でデータ取得を開始します.この関数の第2引数に0を指定するとAdStopIrqMode関数で取得が止められるまで連続的にデータ取得を行います.ただしメインループ側でバッファがオーバーフローしないうちにデータを読み込めるように考慮しておく必要があります.

4-4-5. REX5054からのデータ取得

シリアルコントロールの場合と同じくデータ取得にはThreadを使用します.SetupADC関数でデータの取り込みを開始したらすぐに,取り込みスレッドからpollingを行いAdGetRingBufConst関数で一定量のデータが蓄積されたらドライバーのRing Bufferからアプリケーション側にデータを読み込むようにします.
void __fastcall TAdcThread::Execute(void)
{
    if( SetupADC(Form1->Handle) == false ){
        MessageBox(Form1->Handle, "A/D Error", "Stop", MB_OK);
        return;
    }
    while(!Terminated){
        if( (count = AdGetRingBufConst(wave_buf, (DWORD) wave_wp,
            (DWORD) 256)) == -1 ){
            AdStopFlag = 1;
            Form1->StopSub3();
            MessageBox(Form1->Handle, "A/D converter Overrun Error", "Stop", MB_OK);
            return;
        }
        if( count == 0 ){
//  polling ADC every 100msec
            Sleep(100);
        }else{
//  Wave Data save to file
            EnterCriticalSection(&CritSec_AD);
            wave_wp += count;
            wave_count += count;
            wave_wp = (wave_wp>=BUF_MAX)?0:wave_wp;
//  OverRun...
            if( wave_count > BUF_MAX ){
                AdStopFlag = 2;
                LeaveCriticalSection(&CritSec_AD);
                Form1->StopSub3();
                MessageBox(Form1->Handle, "A/D converter Overrun Error", "Stop", MB_OK);
                return;
            }
            LeaveCriticalSection(&CritSec_AD);
            SetEvent(hAD_event);
        }
    }
}
このルーチンでは100msec毎にpollingしながら256bytes (0.5sec分のデータ)が溜まる毎にアプリケーションのリングバッファにデータが読み込まれ,データ処理ルーチンにデータが準備できたことを知らせるEventが送られるようになっています.EventおよびCRITICAL_SECTIONの使用方法についてはシリアルデータ取得方法の項を参照してください.

4-4-6. データ取り込みの停止

ADconverterを止める場合にはAdStopIrqMode関数を使用します.実際には以下のようにAdcThreadのデストラクタにこれを書きます.その後AdFreeRingBuf関数を使用して取得したRing Bufferの領域を開放します.以下のリストでは一応の安全性を考慮して1秒の待機時間を取ったあと開放するようにプログラムしています.
//---------------------------------------------------------------------------
__fastcall TAdcThread::~TAdcThread(void)
{
    AdStopIrqMode();
    Sleep(1000);
    if( AdGetRingBufAdrs() != 0 ){
        AdFreeRingBuf();
    }
}

4-4-7. メインルーチン側の処理

データ取得を開始するところで以下のようにしてAdcThreadを生成します.
    AdcThread = new TAdcThread((TThreadPriority) 4);
ここではAdcThreadのpriorityを通常より1ランクだけ高いものにしています.
また,Threadを終了させるには以下のようにTerminateメソッドを使用します.なお演算スレッドのようにWaitForSingleObjectでEventを待っているスレッドでは終了させるためにEventを送ってExecuteから抜けるようにする必要があります.
    AdcThread->Terminate();

4-4-8. その他の取り込みモードに関して

一定時間のみのサンプリングでよい場合にはREX5054に装備されているその他のデータサンプリング法を使用してもよいですが,麻酔中の波形データを連続的に取得する場合にはここに挙げたような方法が最も適していると考えられます.ここではその他の方法については触れません.REX5054のマニュアルを参考にして下さい.基本的な部分はここに解説した方法と同様にコーディングできると思います.

5.タイマーに関して

リアルタイムを扱うソフトウェアではタイマーの精度が重要ですが,Windowsのタイマー関数は非常に精度が悪くミリ秒単位を扱うことは困難です.特にWindows 95/98系ではタイマーの最小値は約55 msecでこれよりも短い間隔に設定することはできません.
そこで,この問題点を解決する方法としてマルチメディアのタイマーを使う方法を挙げておきます.既に,この方法を用いたマルチメディアタイマーコンポーネントを作っておられる方もありますので,そういったコンポーネントを使用してもよいのですがここでは最も簡単にこれらの関数群を使用する方法を解説しておきます.

5-1. マルチメディアタイマー関係の関数の使用法

まず,WIN32APIのマルチメディア関係の関数を使用する場合にはmmsystem.hをインクルードしておく必要があります.それから以下のようにいくつかの作業領域を確保しておきます.
#include <mmsystem.h>

unsigned int timerID, Interval;
次にTimeProc callback関数を定義します.
void CALLBACK timeproc(unsigned int timerID, unsigned int msg, 
         unsigned long user, unsigned long dw1, unsigned long dw2)
{
//	write process routine here !!
}
これで準備は完了です.後は以下ような関数をコールしてタイマーをスタートさせます.ここでは50msecのPERIODICの設定です.
....
    Interval = 50;
    timeBeginPeriod(Interval);
    timerID = timeSetEvent(Interval, Interval, timeproc, NULL, TIME_PERIODIC);
    if(!timerID){
        timeEndPeriod(Interval);
        ShowMessage("Error");
    }
.....
もしも1回だけの場合にはtimeSetEvent関数のTIME_PERIODICの代わりにTIME_ONESHOTを使用しますが,計測の多くの場合にはTIME_PERIODICを使用することになると思います.最後はタイマーを止める処理です.
.....
    if(timerID){
        timeKillEvent(timerID);
        timeEndPeriod(Interval);
        timeID = 0;
    }
.....

5-2. マルチメディアタイマー使用上の注意

あまり小さなIntervalを設定するとシステムに負荷がかかりすぎて問題が生じる可能性があります.設定できるIntervalの最小値と最大値はtimeGetDevCaps関数を使用して取得しておく方がよいでしょう.
TIMECAPS tc;

    timeGetDevCaps(&tc, sizeof(TIMECAPS));
    minimum_interval = tc.wPeriodMin;
    maximum_interval = rc.wPeriodMax;

マルチメディアタイマーはWindowsのシステムタイマーよりも格段に精度が高いので,リアルタイムソフトウェアを組む場合にはこちらを使用するのがよいでしょう.

6.おわりに

ここに解説しましたC++Builderのコードは私が配布しているいくつかのソフトウェア(Bsa, GetDDG etc.)の一部で実際に動作しているものです.Threadオブジェクトの操作は慣れないと難しいかもしれませんが,リアルタイム処理を行うソフトウェアを構築する際には必須のものであると思います.もちろんシリアル通信の簡単なものはTimerオブジェクトでも実現できますが,ThreadオブジェクトでEvent drivenのコードを書いた方が簡潔であると思われます.
将来的にはシリアル接続はTCP/IPなどのNetwork接続に置き換えられていくかもしれませんが,まだまだ当分はここに解説したようなデジタルやアナログのデータ通信が主流であると思われます.何かの参考になれば幸いです.
ご意見やご希望がありましたらEmailで連絡をいただけないでしょうか?

大阪大学大学院医学系研究科 麻酔・集中治療医学講座 萩平 哲(はぎひら さとし)
565-0871 吹田市山田丘2-2
Email: hagihira@masui.med.osaka-u.ac.jp, satoshi.hagihira@nifty.com


最終改訂 2006.07.15 (Ver 0.50)