ReadFile(ser_hdl, IOBuf, BUFFERSIZE-DataCount,
&DataLength, NULL);
last update 2006.07.15#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;
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を指定しておきます.
// 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の種類を判別します.
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 !!
}
}
}
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関数を作成しています.
//---------------------------------------------------------------------------
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の再設定などは必要ないかもしれませんが念のために行っています.
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を引数として指定するようにしています.
SerialThread = new TSerialThread((TThreadPriority) 3);
ここではThreadのpriorityも同じに設定できるように設定しています.なおSerialThreadのオブジェクトそのものはメインフォームのクラスヘッダの中で宣言しておく必要があります.
class TForm1 : public TForm
{
__published: // IDE 管理のコンポーネント
// Mainのオブジェクトおよびメンバー関数の宣言
.....
private: // ユーザー宣言
TSerialThread *SerialThread;
public: // ユーザー宣言
.....
};
上のリストではSerialThread以外の宣言はすべて省略しています.
if( SerialThread ){
SerialThread->Terminate();
}
void __fastcall TSerialThread::UpdateLine(String s)
{
Message = s;
Synchronize(UpdateDispLine);
}
//---------------------------------------------------------------------------
void __fastcall TSerialThread::UpdateDispLine(void)
{
Form1->eEdit->Text = Message;
}
上記のようにsynchronizeメソッドを用いてメイン画面の変更を行います.CRITICAL_SECTION CritSec_Serial; HANDLE hGet_event;と宣言しておいてメインフォームのOnCreate関数の中で
// Critical Section Initialize
InitializeCriticalSection(&CritSec_Serial);
hGet_event = CreateEvent(NULL, false, false, NULL);
というように初期化しておきます.
//---------------------------------------------------------------------------
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);
}
#includeCRITICAL_SECTIONを使用してからqueue cにデータをセットしてpushしてやればCMDQueueにコマンドデータが登録されます.こうしておいてからThreadにEventを送り,CRITICAL_SECTIONから抜けます.#include typedef struct { int cmd; int param1; int param2; } cmd_packet; cmd_packet c; std::queue CMDQueue;
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の有用な活用法の事例であると言えます.DllImport BOOL APIENTRY VbGetRingBufConst( LPWORD, BOOL, BOOL ); DllImport int APIENTRY AdGetRingBufConst( LPVOID, int, int );
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: // ユーザー宣言
.....
};
//
// 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で使用するようにしています.
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の使用方法についてはシリアルデータ取得方法の項を参照してください.
//---------------------------------------------------------------------------
__fastcall TAdcThread::~TAdcThread(void)
{
AdStopIrqMode();
Sleep(1000);
if( AdGetRingBufAdrs() != 0 ){
AdFreeRingBuf();
}
}
AdcThread = new TAdcThread((TThreadPriority) 4);
ここではAdcThreadのpriorityを通常より1ランクだけ高いものにしています.
AdcThread->Terminate();
#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;
}
.....
TIMECAPS tc;
timeGetDevCaps(&tc, sizeof(TIMECAPS));
minimum_interval = tc.wPeriodMin;
maximum_interval = rc.wPeriodMax;
マルチメディアタイマーはWindowsのシステムタイマーよりも格段に精度が高いので,リアルタイムソフトウェアを組む場合にはこちらを使用するのがよいでしょう.
大阪大学大学院医学系研究科 麻酔・集中治療医学講座 萩平 哲(はぎひら さとし)
565-0871 吹田市山田丘2-2
Email: hagihira@masui.med.osaka-u.ac.jp,
satoshi.hagihira@nifty.com