목차

[RS232] Windows 98/NT 를 위한 통신 프로그램 강좌(2)

[출처] http://cpueblo.com/board/bbs/board.php?bo_table=programming&wr_id=431

Windows 98/NT 를 위한 통신 프로그램 강좌(2)

직렬 상태

직렬 통신 포트의 상태를 알아내는 방법은 두 가지다.
첫 번째 방법은 이벤트 마스크를 사용하여 이벤트가 발생한 것을 알아내는 방법이다.
SetCommMask 함수는 이벤트 마스크를 설정하고, WaitCommEvent는 지정한 이벤트가 발생할 때까지 기다린다.
이 함수들은 16비트 윈도우에서의 SetCommEventMast와 EnableCommNotification과 비슷한데 단지 윈32 함수는 WM_COMMNOTIFY메시지를 보내주지 않는다는 점이 다르다.
사실상 WM_COMMNOTIFY메시지는 윈32에서는 없다. 두 번째 방법은 거의 차이점이 없다.

통신 이벤트

통신 이벤트는 통신 포트를 사용하는 중에 계속 발생한다. 이 이벤트를 알아내는 방법은 2단계가 있다.

  1. SetCommMask로 알고자 하는 이벤트를 설정한다.
  2. WaitCommEvent 로 상태를 체크한다. 상태 체크는 중첩/비중첩 작업 어디서든 사용할 수 있다.

여기서 이벤트라고 하는 것은 통신포트 이벤트만을 말한다. 동기통신을 위한 이벤트 객체에 대한 것이 아니다.
여기에 SetCommMask함수의 예제가 있다

DWORD dwStoredFlags;
 
dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING | EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY;
 
if (!SetCommMask(hComm, dwStoredFlags))
{
     // 통신 마스크 설정 오류
     return false;
}

각 이벤트의 설명은 다음과 같다

이벤트 설명
EV_BREAK 입력에서 브레이크가 검색되었다
EV_CTS CTS(Clear-to-send)신호가 변경 상태. CTS선에 대한 실제적 인 상태를 검사하기 위하여는 GetCommModemStatus를 호출하 여야 한다
EV_DSR DSR(Data-set-ready)신호가 변경 상태.DSR선에 대한 실제적 인 상태를 검사하기 위하여는 GetCommModemStatus를 호출하 여야 한다
EV_ERR 오류가 발생하였다. 라인 상태 오류는 CE_FRAME, CD_OVERRUN, CE_RXPARITY이다. 오류의 종류를 알려면 GetCommModemStatus가 호출되어야 한다.
EV_RING 전화벨이 울렸다는 신호가 왔다
EV_RLSD RLSD(reveive-line-signal-detect 수신선 신호 검색됨)상태 가 변경되었다.
RLSD선에 대한 실제적인 상태를 검사하기 위하여는 GetCommModemStatus를 호출하여야 한다.
이것은 CD(carrier detect 접속된 상태)를 볼 때 공통으로 사용한다.
EV_RXCHAR 새로운 문자가 수신되어 입력 버퍼에 놓여있다.
EV_RXFLAG 이벤트 문자가 수신되어 입력 버퍼에 놓여 있다.
이벤트 문자는 DCB구조체에서 EvtChar로 지정하는데 차후 설명한다.
EV_TXEMPTY 마지막 문자가 송신되어 송신 버퍼가 비었다.
만약 하드웨 어적인 버퍼가 사용된다면 이것은 하드웨어에 전송이 끝났 다는 것만을 알려준다.
하드웨어의 버퍼가 송신이 끝나 비 워졌다는 것을 알 수 있는 방법은 없다.

이벤트 마스크를 설정한 후엔 WaitCommEvent 펑션은 이벤트가 발생했다는 것을 검색하게 한다.
만약 포트가 비중첩 작업을 위해 열렸다면 WaitCommEvent 펑션은 OVERLAPPED구조체를 포함하지 않는다.
지정한 이벤트 중의 하나가 발생할 때까지 프로그램은 진행되지 않고 붙잡힌다. 만약 이벤트가 전혀 발생하지 않는다면 그 쓰레드 역시 붙들린다.
여기에 단편적인 예제가 있다. 포트는 비중첩 작업으로 열렸고 EV_RING 이벤트를 기다리는 것을 보여준다.

DWORD dwCommEvent;
 
if (!SetCommMask(hComm, EV_RING))
{
    // 통신 마스크 세트하는 중 오류
    return FALSE;
}
 
if (!WaitCommEvent(hComm, &dwCommEvent, NULL))
{
    // 이벤트를 기다리는 중에 오류가 발생
    return FALSE;
}
else
{
    // 이벤트가 발생하였다.
    return TRUE;
}


:!:주의 사항

 마이크로소프트 소프트 개발자 기술자료집에는 윈도우 95에서 EV_RING을 사용하는데 문제점이 있다는 설명을 해준다.
 위의 코드는 윈도우 95에서는 절대 복귀되지 않고 무한정 붙들려 있는 상태가 된다. 왜냐하면 시스템에서 EV_RING 이벤트를 검사할 수 없다.
 윈도우 NT에서는 철두철미하게 EV_RING이벤트에 대하여 알려준다. 이 버그에 대한 정보는 윈32 SDK 기술자료집에 좀더 많은 정보가 있으니 참고합니다.

위의 코드에서 만일 이벤트가 영원히 발생하지 않는다면 영원히 붙들려있는 상태가 된다. 더 나은 솔루션은 포트를 열 때 중첩 작업방식으로 열어서 다음과 같은 요령으로 처리하는 것이다

#define STATUS_CHECK_TIMEOUT      500   // 1000분의 1초
DWORD      dwRes;
DWORD      dwCommEvent;
DWORD      dwStoredFlags;
BOOL      fWaitingonStat = FALSE;
OVERLAPPED osStatus = {0};
 
dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING |\\
EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
if (!SetCommMask(comHandle, dwStoredFlags))
// 통신 마스크 설정중 오류; 중단함
return 0;
 
osStatus.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (osStatus.hEvent == NULL)
// 이벤트를 생성하는데 실패; 중단함
return 0;
 
for ( ; ; ) {
    // 상태 채크가 이미 되지 않았다면 이벤트를 체크한다.
    if (!fWaitingOnStat) {
        if (!WaitCommEvent(hComm, &dwCommEvent, &osStatus)) {
            if (GetLastError() == ERROR_IO_PENDING)
                bWaitingonStatusHandle = TRUE;
            else
                // WaitCommEvent에서 오류 발생; 중단함
                break;
        }
        else
            // WaitCommEvent는 즉시 복귀된다.
            // Deal with status event as appropriate.
            ReportStatusEvent(dwCommEvent);
    }
 
    // 중첩 작업에서 채크.
    if (fWaitingOnStat) {
        // 이벤트가 발생할 때까지 잠깐 기다린다.
        dwRes            =           WaitForSingleObject(osStatus.hEvent,
            STATUS_CHECK_TIMEOUT);
        switch(dwRes)
        {
            // 이벤트가 발생.
        case WAIT_OBJECT_0:
            if   (!GetOverlappedResult(hComm,  &osStatus,   &dwOvRes,
                FALSE))
                // 중첩 작업에서 오류가 발생하였다;
                // GetLastError호출로 무슨일인지 확인하고
                // 심각한 상황이면 중단한다
                else
                // 상태 이벤트는 WaitCommEvent를 호출할 당시 지정한
                // 이벤트 플래그에 저장되어 있다.
                // 이벤트를 사용한다.
                ReportStatusEvent(dwCommEvent);
 
            // Set fWaitingOnStat flag to indicate that a new
            // WaitCommEvent is to be issued.
            fWaitingonStat = FALSE;
            break;
 
        case WAIT_TIMEOUT:
            // 작업이 아직까지 완료되지 않았다.fWaitingOnStatusHandle
            // flag isn't changed since I'll loop back around and
            // I don't want to issue another WaitCommEvent until
            // the first one finishes.
            //
            // This is a good time to do some background work.
            DoBackgroundWork();
            break;
 
        default:
            // Error in the WaitForSingleObject; abort
            // This indicates a problem with the OVERLAPPED
            // structure's
            // event handle.
            CloseHandle(osStatus.hEvent);
            return 0;
        }
    }
}
 
CloseHandle(osStatus.hEvent);

위의 코드는 중첩 읽기와 유사하다.
SetCommMask 와 WaitCommEvent는 두 가지 관심거리가 있다.

첫째, 통신 포트가 비중첩 작업으로 열었을 때는 WaitCommEvent는 이벤트가 발생할 때까지 무한정 기다린다.
만약 별도의 쓰레드가 SetCommMask에 새로운 마스크를 설정하고자 한다면 그 쓰레드도 무한정 붙잡힌다. 그 이유는 첫번째 쓰레드가 WaitCommEvent를 호출하여 멈춘 상태에 있기 때문이다.
SetCommMask를 호출한 쓰레드는 WaitCommEvent펑션이 복귀될 때까지 붙잡혀 있게된다. 비중첩 입출력으로 포트를 열어두면 이러한 면이 있다.
만일 어떤 쓰레드가 직렬 통신 펑션을 호출하였을 경우 다른 두 번째 쓰레드가 통신 펑션을 호출하면 두 번째 쓰레드는 첫 번째 쓰레드가 종료될 때까지 붙잡혀 있게 된다.

둘째, 중첩 작업에서 포트를 열었을 경우이다. 만약 SetCommMask가 새로운 이벤트 마스크를 설정하면 (예를 들면 이벤트 마스크를 NULL로 했을때)어떠한 계류중인 WaitCommEvent도 성공적으로 완료될 수 있다. .

통보(Caveat)

포트에 하나의 바이트가 도착하였다는 것을 그 쓰레드에게 통보하기 위해 EV_RXCHAR 플래그를 사용한다.
이 이벤트는 ReadFile 펑션과 짝을 이루어 사용된다. 데이터가 수신될 때까지 기다리지 않고 수신된 이후에만 읽어 들이기 작업하기 가능하도록되어있다.
이것은 비중첩 작업에서 매우 유용한데 왜냐면 데이터가 들어왔는지 폴링(1초이하의 시간 간격으로 데이터가 들어왔는지 계속 읽어 보는 행위)이 필요하지 않다.
프로그램은 EV_RXCHAR이벤트로 데이터가 수신되었다는 사실을 통보받는다.

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;
if (!SetCommMask(hComm, EV_RXCHAR))
// 이벤트 마스크 세트에 오류.
 
for ( ; ; ) {
    if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
        if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
            // 한 바이트가 읽어졌다.; 처리요망
            else
            // ReadFile 호출에 문제 발생
            break;
    }
    else
        // WaitCommEvent에서 문제
        break;
}

위의 코드는 EV_RXCHAR 이벤트가 발생할 때까지 기다린다. 발생할 땐 ReadFile을 호출하여 한 바이트를 수신한다.
루프(반복 원위치 되는 구간)는 다시 시작되고 EV_RXCHAR 이벤트가 발생할 때까지 기다린다.
이 코드는 한두 바이트가 도작할 때 빨리 잇달아서 처리하기 좋다. EV_RXCHAR이벤트가 발생 하였다는 원인으로 바이트를 수신한다.
WaitCommEvent을 호출하기 전에 이미 수신된 바이트가 있어도 좋다.
새로운 문자가 수신되면 EV_RXCHAR가 발생하여 이미 수신된 바이트를 읽어 들이고, 새로이 수신된 바이트는 내부적으로 EV_RXCHAR플래그가 세트되어 ReadFile을 호출하여 다시 읽혀질 수 있다.

위 코드의 문제는 3개나 그 이상의 바이트가 연속하여 빨리 도착하였을 경우이다.
첫 번째 바이트는 EV_RXCHAR 이벤트를 발생한다.
두 번째 바이트는 내부적으로 EV_RXCHAR 플래그가 세트되는 원인이 되어 다음번에 WaitCommEvent 를 호출할 때 EV_RXCHAR 이벤트가 발생하도록 한다.
이 시점에서 세 번째 바이트가 도착하였을 때 EV_RXCHAR 플래그를 내부적으로 세트하려하지만 실패한다. 왜냐하면 이미 두 번째 바이트가 도착했을 때 발생했던 것이라 세 번째 바이트가 도착한 사실은 통보되지 않는다.
결국 이 코드에서 첫 바이트는 문제없이 읽는다. 그런 다음 WaitCommEvent이 호출될 것이고 두 번째 바이트에 의해서 EV_RXCHAR이벤트를 받는다.
두 번째 바이트가 읽혀지고 WaitCommEvent가 호출되면 세 번째 바이트는 시스템의 내부 수신 버퍼에 놓여진 상태로 기다린다. 코드와 시스템은 이제 동시성을 갖지 않는다.
네 번째 바이트가 성공적으로 도착하면 EV_RXCHAR이벤트가 발생하고, 세 번째 바이트였던 하나의 바이트가 읽혀지고 그렇게 계속된다.

이 문제의 손쉬운 해법은 읽기 작업의 요구 숫자를 늘이는 방법이다.
하나의 바이트를 요구하는 것보다 둘이나 열 개 또는 그이상의 숫자를 정한다. 이 생각에도 여전히 문제는 있는데 둘이나 그 이상의 남은 바이트가 있게된다.
그래서 만약 2바이트를 읽기로 했을 땐 4바이트가 연달아 도착했을 때 문제가 되고, 열 바이트를 요구하는 때에는 20바이트가 도착하였을 때 문제가 된 다.

실제 해법은 남은 바이트가 없을 때까지 읽는 방법이다.
다음은 이 문제를 해결하여 0바이트가 읽혀질 때까지 반복하여 읽는 소스이다.
또다른 가능한 방법은 ClearCommError를 사용하여 버퍼에 남아있는 바이트의 수를 파악하여 한번에 읽어들이는 작업을 하는 것이다.
이 방법은 매우 복잡한 버퍼 관리를 요구하지만 엄청나게 많은 데이터가 수신된 것을 읽어들이는 횟수를 한번으로 줄여준다.

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;
if (!SetCommMask(hComm, EV_RXCHAR))
// Error setting communications event mask
 
for ( ; ; ) {
    if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
        do {
            if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
                // A byte has been read; process it.
                else
                // An error occurred in the ReadFile call.
                break;
        } while (dwRead);
    }
    else
        // Error in WaitCommEvent
        break;
}

위의 코드는 시간초과(time-outs)를 지정하지 않고서는 올바르게 동작하지 않는다.
통신 시간 초과는 나중에 설명되고 ReadFile 작업에서 바이트가 도착할 때를 기다리지 않는 영향을 미치게 된다. “통신 시간 초과” 절을 참고하기 바란다.

위의 코드는 바이트가 연달아 도착하여도 EV_RXFLAG가 발생하지 않을 것이다. 다시 강조하건대 가장 좋은 해법은 한 바이트도 남지 않을 때까지 반복하여서 읽는 것이다.

위의 통보 방법은 문자 이벤트가 아닌 다른 이벤트에서도 적용된다. 만약 다른 이벤트가 발생하고 연이어 계속 발생한다면 몇몇은 잃어버리게 될 것이다.
만약 CTS라인의 전압이 고위(high), 저위(low)로 반복되어 이벤트가 발생한다. 만약 CTS라인이 아주 빨리 변경된다면 WaitCommEvent는 실제적인 EV_CTS이벤트를 검사하는 것을 보장해 주지 않는다.
이러한 이유로 인해 WaitCommEvent는 라인 상태를 추적하는데 사용할 수 없다. 라인 상태는 “모뎀 상태들” 절에서 커버한다.

오류 핸들링과 통신 상태들

SetCommMask를 호출하여 이벤트 플래그를 지정하는 때에 EV_ERR도 지정할 수 있다.
EV_ERR 이벤트는 통신 포트에 오류 컨디션이 존재한다는 것을 나타낸다.
EV_ERR 이벤트를 지정하지 않은 포트에서도 오류가 발생할 수도 있는데 오류 컨디션이 삭제될 때까지 모든 입출력 작업은 억제된다.
ClearCommError는 오류 검사와 오류 컨디션을 청소하기 위해 호출되는 펑션이다.

ClearCommError는 물론 통신이 왜 멈추게 되었는지 멈춘 이유도 제공한다. 또한 보내기와 받기 버퍼에 몇 개의 바이트가 대기 중인지도 나타낸다.
오류나 흐름제어에 의하여 통신이 멈출 수 있다. 흐름제어는 뒤에 이 문서에서 설명한다.

여기 ClearCommError 호출하는 법을 보여주는 예시가 있다.

COMSTAT comStat;
DWORD   dwErrors;
BOOL    fOOP, fOVERRUN, fPTO, fRXOVER, fRXPARITY, fTXFULL;
BOOL    fBREAK, fDNS, fFRAME, fIOE, fMODE;
// Get and clear current errors on the port.
if (!ClearCommError(hComm, &dwErrors, &comStat))
// Report error in ClearCommError.
return;
 
// Get error flags.
fDNS = dwErrors & CE_DNS;
fIOE = dwErrors & CE_IOE;
fOOP = dwErrors & CE_OOP;
fPTO = dwErrors & CE_PTO;
fMODE = dwErrors & CE_MODE;
fBREAK = dwErrors & CE_BREAK;
fFRAME = dwErrors & CE_FRAME;
fRXOVER = dwErrors & CE_RXOVER;
fTXFULL = dwErrors & CE_TXFULL;
fOVERRUN = dwErrors & CE_OVERRUN;
fRXPARITY = dwErrors & CE_RXPARITY;
 
// COMSTAT structure contains information regarding
// communications status.
if (comStat.fCtsHold)
// Tx waiting for CTS signal
 
if (comStat.fDsrHold)
// Tx waiting for DSR signal
 
if (comStat.fRlsdHold)
// Tx waiting for RLSD signal
 
if (comStat.fXoffHold)
// Tx waiting, XOFF char rec'd
 
if (comStat.fXoffSent)
// Tx waiting, XOFF char sent
 
if (comStat.fEof)
// EOF character received
 
if (comStat.fTxim)
// Character waiting for Tx; char queued with TransmitCommChar
 
if (comStat.cbInQue)
// comStat.cbInQue bytes have been received, but not read
 
if (comStat.cbOutQue)
// comStat.cbOutQue bytes are awaiting transfer

모뎀 상태

EV_CTS, EV_DSR, EV_RING, and EV_RLSD플래그를 포함하여 SetCommMask를 호출할 수 있다.
이 플래그들은 직렬 포트의 라인에 전압이 변경된 것을 나타낸다. 변화가 발생하여도 실질적인 이들의 라인의 상태를 나타내는 것은 없다.
GetCommModemStatus펑션은 실질적인 이들 라인의 상태를 비트 마스크로 0이면 저위(low)이고 1이면 고위(high)로 각 라인의 상태를 나타내 준다.

기억할 것은 RLSD (Receive Line Signal Detect)는 일반적으로 CD (Carrier Detect)라인을 참조한다.

:!:주의사항 : EV_RING플래그는 윈도우95에서는 앞서 설명한 바와같이 동작하지않는다. 그러나 GetCommModemStatus 펑션은 RING라인을 검색한다.

이들 라인이 변경되면 흐름제어 이벤트의 원인이 된다. ClearCommError펑션은 흐름제어에 의하여 송수신이 억제된 상태를 알려준다.
만약 필요하다면 쓰레드는 ClearCommError펑션을 호출하여 이벤트가 발생한 상황을 검사한다. 흐름제어는 “흐름제어” 절에서 자세히 설명한다.

GetCommModemStatus 호출법 예시

DWORD dwModemStatus;
BOOL  fCTS, fDSR, fRING, fRLSD;
 
if (!GetCommModemStatus(hComm, &dwModemStatus))
// Error in GetCommModemStatus;
return;
 
fCTS = MS_CTS_ON & dwModemStatus;
fDSR = MS_DSR_ON & dwModemStatus;
fRING = MS_RING_ON & dwModemStatus;
fRLSD = MS_RLSD_ON & dwModemStatus;
 
// Do something with the flags.

확장된 펑션들

장치 제어기(통신 포트를 제어하는 소프트웨어, 예를 들면 Windows폴더 안의 System 폴더안에 Serial.Vxd 파일과 같은 것)는 제어 라인(직렬 통신 전선 상에서 흐름제어를 위해 사용하는 전선 가닥)의 상태를 필요에 따라 자동적 으로 변경한다.
일반적으로 라인들의 상태를 제어하는 것은 제어기에 의한 것이다. 만약 RS-232 표준과는 다른 방식으로 포트의 제어라인을 사용하고자 않다면 표준 직렬 장치 제어기는 동작하지 않을 것이다.
만약 표준 직렬 통신 장치제어기가 장치를 제어하지 못한다면 별도 제작된 장치 제어기가 필요하다.

그러한 일이 닥치면 응용프로그램은 직접 스스로 흐름 제어를 하여야 한다.
응용프로그램은 RTS와 DTR라인의 상태를 직접제어해야 하는 책임이 있다.
EscapeCommFunction는 직접적으로 특별한 작업을 할 수 있다. EscapeCommFunction는 브레이크를 설정하거나 지우는 것과 같은 컨디션을 할 수 있다.
이에 대하여 더 많은 정보를 원한다면 Win32 SDK 문서를 참조한다.