Visual C++ 시리얼 통신(RS-232) 강좌 (1)
먼저 강좌를 하기 전에 몇 가지만 말씀 드리겠습니다. 일단 처음으로 강좌라는 것을 시도해보네요. 저는 10년(?)째 프로그래머의 길을 걷고 있는 사람입니다. 전 10년 전에 C언어를 처음 접했습니다. 무작정 VC++6.0 깔아놓고 그 두꺼운 비주얼 c++ 바이블을 놓고 밤새 일일이 코딩을 하던 안 좋은 기억이 생각나네요. 초보 때는 무식하게 프로그램을 개발했었지만 요즘엔 그 동안 했던 많은 소스들이 있기에 거의 copy & paste 로 프로그램의 80% 이상을 한다는… (많은 고수 분들이 그럴 것이라 생각되지만.. 나만 그런가^^;)
그런데 제가 프로그램을 하면서 제일 많이 느낀 것은 “절대 실무에서 돈되는 테크닉은 공짜로는 거의 안 가르쳐 준다.(학원을 안 다녀봐서 모르겠지만 학원은 가르쳐 주나?)” 이것 입니다. 그렇다고 그런 것을 욕할 필요는 없지요. 자기 밥줄 끊으면서 남에게 자기 기술을 오픈 할 아무 이유도 없으니까요.
그래서~ 제가 이 얇고 넓은(OTL) 지식으로 충분히 상용으로 쓸 수 있는 지금도 우리회사에서 쓰고 있는 시리얼통신에 관한 강좌를 하고자 합니다. 일단 시리얼통신이 무엇인지는 알고 계시다는 가정하에 소스코드와 함께 설명을 드리겠습니다.
그럼 이제 슬슬 한번 해~~봅시다. [모든 코드는 직접 타이핑 해 보시길 바랍니다.]
먼저 시리얼 통신을 전담하는 클래스를 만들겠습니다. 먼저 CCmdTarget을 부모 클래스로 하는 클래스를 하나 생성합니다. 전 클래스 이름을 CPYH_Comm 이라고 하겠습니다.
그런 다음 생성자 함수에서 Serial Port, Baudrate, Parity bit, Data bit, Stop bit를 인자로 받게끔 하겠습니다.
헤더파일에 생성자 함수는 다음과 같이 되겠고
class CPYH_Comm : public CCmdTarget
{
             DECLARE_DYNCREATE(CPYH_Comm)
                           CPYH_Comm() {};           // protected constructor used by dynamic creation
                           ~CPYH_Comm();
…
…
Cpp 파일의 생성자 함수는 다음과 같이 하면 되겠죠?
CPYH_Comm::CPYH_Comm(CString port,CString baudrate,CString parity,CString databit,CString stopbit)
{
             m_sComPort = port;                                                     // Comport
             m_sBaudRate = baudrate;                                            // Baud Rate
             m_sParity = parity;                                                      // Parity Bit
             m_sDataBit = databit;                                                   // Data Bit
             m_sStopBit = stopbit;                                                   // Stop Bit
             m_bFlowChk = 1;                                                        // Flow Check
             m_bIsOpenned = FALSE;                                             // 통신포트가 열려 있는지
             m_nLength = 0;                                                           // 받는 데이터의 길이
             memset(m_sInBuf,0,MAXBUF*2);                                  // Receive Buffer 초기화
             m_pEvent = new CEvent(FALSE,TRUE);                        // 쓰레드에서 사용할 이벤트
}
위와 같이 인자로 받은 파라메타로 멤버변수의 값을 설정합니다. 생성자 함수에서 각 파라메타를 설정합니다. 변수는 벌써 헤더파일에 선언이 되어 있어야 함은 물론 이구요.
자 그럼 이제 통신 포트를 실제로 여는 함수를 만들어 보겠습니다.
함수는 BOOL CPYH_Comm::Create(HWND hWnd) 이렇게 선언을 하겠습니다. 여기서 hWnd 는 나중에 클래스 객체를 사용하는 클래스에 메시지를 전달하기 위하여 클래스의 핸들을 인자로 받습니다. 그럼 함수의 구현부로 들어가 보지요.
BOOL CPYH_Comm::Create(HWND hWnd)
{
             m_hWnd = hWnd;                                  // 메시지를 보낼때 사용
             m_hComDev = CreateFile(m_sComPort, GENERIC_READ | GENERIC_WRITE,
                                        0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,
NULL); // 시리얼 포트 오픈
            
             if (m_hComDev!=INVALID_HANDLE_VALUE)    // 포트가 정상적으로 열리면 
                           m_bIsOpenned = TRUE;                 // 성공
else // 아니면
return FALSE; // 실패하고 빠져나감
             ResetSerial();                                            // 시리얼 포트를 설정값대로 초기화
m_OLW.Offset = 0; // Write OVERLAPPED structure 초기화
             m_OLW.OffsetHigh = 0;
             m_OLR.Offset = 0;                                      // Read OVERLAPPED structure 초기화
             m_OLR.OffsetHigh = 0;
             m_OLR.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
             if (m_OLR.hEvent == NULL) {
                           CloseHandle(m_OLR.hEvent);
                           return FALSE;
             }
             m_OLW.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
             if (m_OLW.hEvent == NULL) {
                           CloseHandle(m_OLW.hEvent);
                           return FALSE;
}
// 시리얼 데이터를 받기위한 스레드 생성
AfxBeginThread(CommThread,(LPVOID)this);
// DTR (Data Terminal Ready) signal 을 보냄
             EscapeCommFunction(m_hComDev, SETDTR);   // MSDN 참조
             return TRUE;
}
위 내용을 종합해서 설명하면 시리얼포트를 열고 정상적으로 통신 포트가 열리면 시리얼포트를 설정값에 따라 초기화 한 다음 데이터를 보내고 받을 overlapped i/o를 초기화하고 각각의 이벤트를 생성하는 것입니다. 그런 다음 통신포트로 들어오는 데이터를 받기 위해 쓰레드를 생성하고 DTR 시그널을 set 하는 것이 이 Create 함수의 동작입니다.
위 함수 내용 안에 있는 ResetSerial() 함수를 만들어 보겠습니다. 말 그대로 시리얼 포트를 초기화하는 것입니다.
그럼 함수를 구현해 볼까요?
void CPYH_Comm::ResetSerial()
{
             DCB                    dcb;
             DWORD   DErr;
             COMMTIMEOUTS CommTimeOuts;
            
             if (!m_bIsOpenned)
                                        return;
            
             // 통신포트의 모든 에러를 리셋
             ClearCommError(m_hComDev,&DErr,NULL);
             // 통신포트의Input/Output Buffer 사이즈를 설정
             SetupComm(m_hComDev,InBufSize,OutBufSize);
             // 모든 Rx/Tx 동작을 제한하고 또한 Buffer의 내용을 버림
            PurgeComm(m_hComDev,PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
             // set up for overlapped I/O (MSDN 참조)
             // 통신 선로상에서 한바이트가 전송되고 또한 바이트가 전송되기까지의 시간
             CommTimeOuts.ReadIntervalTimeout = MAXDWORD ;
             // Read doperation 에서 TimeOut을 사용하지 않음
             CommTimeOuts.ReadTotalTimeoutMultiplier = 0 ;
             CommTimeOuts.ReadTotalTimeoutConstant = 0 ;
                          
             // CBR_9600 is approximately 1byte/ms. For our purposes, allow
             // double the expected time per character for a fudge factor.
             CommTimeOuts.WriteTotalTimeoutMultiplier = 0;
             CommTimeOuts.WriteTotalTimeoutConstant = 1000;
             // 통신포트의TimeOut을설정
             SetCommTimeouts(m_hComDev, &CommTimeOuts);       
             memset(&dcb,0,sizeof(DCB));
             dcb.DCBlength = sizeof(DCB);
             // 통신포트의 DCB를 얻음
             GetCommState(m_hComDev, &dcb);
             // DCB를 설정(DCB: 시리얼통신의 제어 파라메터, MSDN 참조)
             dcb.fBinary = TRUE;
             dcb.fParity = TRUE;
             if (m_sBaudRate == "300")
                           dcb.BaudRate = CBR_300;
             else if (m_sBaudRate == "600")
                           dcb.BaudRate = CBR_600;
             else if (m_sBaudRate == "1200")
                           dcb.BaudRate = CBR_1200;
             else if (m_sBaudRate == "2400")
                           dcb.BaudRate = CBR_2400;
             else if (m_sBaudRate == "4800")
                           dcb.BaudRate = CBR_4800;
             else if (m_sBaudRate == "9600")
                           dcb.BaudRate = CBR_9600;
             else if (m_sBaudRate == "14400")
                           dcb.BaudRate = CBR_14400;
             else if (m_sBaudRate == "19200")
                           dcb.BaudRate = CBR_19200;
             else if (m_sBaudRate == "28800")
                           dcb.BaudRate = CBR_38400;
             else if (m_sBaudRate == "33600")
                           dcb.BaudRate = CBR_38400;
             else if (m_sBaudRate == "38400")
                           dcb.BaudRate = CBR_38400;
             else if (m_sBaudRate == "56000")
                           dcb.BaudRate = CBR_56000;
             else if (m_sBaudRate == "57600")
                           dcb.BaudRate = CBR_57600;
             else if (m_sBaudRate == "115200")
                           dcb.BaudRate = CBR_115200;
             else if (m_sBaudRate == "128000")
                           dcb.BaudRate = CBR_128000;
             else if (m_sBaudRate == "256000")
                           dcb.BaudRate = CBR_256000;
             else if (m_sBaudRate == "PCI_9600")
                          dcb.BaudRate = 1075;
             else if (m_sBaudRate == "PCI_19200")
                           dcb.BaudRate = 2212;
             else if (m_sBaudRate == "PCI_38400")
                           dcb.BaudRate = 4300;
             else if (m_sBaudRate == "PCI_57600")
                           dcb.BaudRate = 6450;
             else if (m_sBaudRate == "PCI_500K")
                           dcb.BaudRate = 56000;
            
             if (m_sParity == "None")
                           dcb.Parity = NOPARITY;
             else if (m_sParity == "Even")
                           dcb.Parity = EVENPARITY;
             else if (m_sParity == "Odd")
                           dcb.Parity = ODDPARITY;
             if (m_sDataBit == "7 Bit")
                           dcb.ByteSize = 7;
             else if (m_sDataBit == "8 Bit")
                           dcb.ByteSize = 8;              
             if (m_sStopBit == "1 Bit")
                           dcb.StopBits = ONESTOPBIT;
             else if (m_sStopBit == "1.5 Bit")
                           dcb.StopBits = ONE5STOPBITS;
             else if (m_sStopBit == "2 Bit")
                           dcb.StopBits = TWOSTOPBITS;
             dcb.fRtsControl = RTS_CONTROL_ENABLE;
             dcb.fDtrControl = DTR_CONTROL_ENABLE;
            dcb.fOutxDsrFlow = FALSE;
             if (m_bFlowChk) {
                           dcb.fOutX = FALSE;
                           dcb.fInX = FALSE;
                           dcb.XonLim = 2048;
                           dcb.XoffLim = 1024;
             }
             else {
                           dcb.fOutxCtsFlow = TRUE;
                           dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
             }
             // 설정된 DCB로 통신포트의 제어 파라메터를 설정
             SetCommState(m_hComDev, &dcb);
             // Input Buffer에 데이터가 들어왔을 때 이벤트가 발생하도록 설정
             SetCommMask(m_hComDev,EV_RXCHAR);
}
자~ 이렇게 해서 통신포트를 하나 열고 그 포트를 초기화 했습니다. 그럼 이제 데이터를 보내는 함수와 받는 함수를 만들어야 겠지요. 그럼 먼저 데이터를 보내는 함수를 만들어 봅시다.
함수는 다음과 같습니다.
BOOL CPYH_Comm::Send(LPCTSTR outbuf, int len)
{
             BOOL                  bRet=TRUE;
             DWORD                ErrorFlags;
             COMSTAT            ComStat;
             DWORD                BytesWritten;
             DWORD                BytesSent=0;
             // 통신 포트의 모든 에러를 리셋
             ClearCommError(m_hComDev, &ErrorFlags, &ComStat);
             // overlapped I/O를 통하여 outbuf의 내용을 len길이 만큼 전송
             if (!WriteFile(m_hComDev, outbuf, len, &BytesWritten, &m_OLW)) {
                           if (GetLastError() == ERROR_IO_PENDING){
                                        if (WaitForSingleObject(m_OLW.hEvent,1000) != WAIT_OBJECT_0)
                                                     bRet=FALSE;
                                        else
                                                     GetOverlappedResult(m_hComDev, &m_OLW, &BytesWritten, FALSE);
                           }
                           else /* I/O error */
                                        bRet=FALSE; /* ignore error */
             }
             // 다시 한번 통신포트의 모든 에러를 리셋
             ClearCommError(m_hComDev, &ErrorFlags, &ComStat);
            
             return bRet;
}
이번에는 통신 포트를 통하여 들어온 데이터를 받는 함수를 구현해 보겠습니다. 함수는 다음과 같습니다. Inbuf에 len 만큼 길이의 데이터를 받는 것입니다. 별도의 설명은 드리지 않겠습니다.
int CPYH_Comm::Receive(LPSTR inbuf, int len)
{
             CSingleLock lockObj((CSyncObject*) m_pEvent,FALSE);
             // argument value is not valid
             if (len == 0)
                           return -1;
             else if  (len > MAXBUF)
                           return -1;
             if (m_nLength == 0) {
                           inbuf[0] = ' ';
                           return 0;
             }
             // 정상적이라면 본루틴으로 들어와 실제 들어온 데이터의 길이 만큼 Input Buffer에서 데이터를 읽음
             else if (m_nLength <= len) {
                           lockObj.Lock();
                           memcpy(inbuf,m_sInBuf,m_nLength);
                           memset(m_sInBuf,0,MAXBUF*2);
                           int tmp = m_nLength;
                           m_nLength = 0;
                           lockObj.Unlock();
                           return tmp;
             }
             else {
                           lockObj.Lock();
                           memcpy(inbuf,m_sInBuf,len);
                           memmove(m_sInBuf,m_sInBuf+len,MAXBUF*2-len);
                           m_nLength -= len;
                           lockObj.Unlock();
                           return len;
             }
}
이제 거의 끝나가는 것 같군요. Resetserial 함수에서 생성한 쓰레드는 뭘하기 위한걸까요. 눈치빠른 분들은 아셨겠지만 통신 포트를 통하여 데이터가 들어오면 EV_RXCHAR 이벤트가 발생하도록 설정하였습니다. 따라서 우리가 생성한 쓰레드에서는 그 이벤트를 기다리다가EV_RXCHAR 이벤트가 발생하면 overlapped i/o 동작을 통하여 input buffer 에 들어오는 데이터를 복사하고 Create 함수에서 인자로 받은 핸들에 데이터가 들어왔다고 메시지를 보내 줍니다. 그러면 우리는 그 메시지를 전달받아 Receive 함수를 사용하여 데이터를 받으면 되는 것이죠. 이 쓰레드의 함수는 다음과 같습니다. 역시 별도의 설명은 하지 않겠습니다.
UINT CommThread(LPVOID lpData)
{
             extern short          g_nRemoteStatus;
             DWORD                ErrorFlags;
             COMSTAT            ComStat;
             DWORD                EvtMask ;
             char                    buf[MAXBUF];
             DWORD                Length;
             int                       size;
             int                       insize = 0;
            
             // 통신클래스의 객체포인터를 얻음
             CPYH_Comm* Comm = (CPYH_Comm*)lpData;
            
             // 통신포트가 열려 있다면
             while (Comm->m_bIsOpenned) {
                           EvtMask = 0;
                           Length = 0;
                           insize = 0;
                           memset(buf,' ',MAXBUF);
                           // 이벤트를 기다림
                           WaitCommEvent(Comm->m_hComDev,&EvtMask, NULL);
                           ClearCommError(Comm->m_hComDev, &ErrorFlags, &ComStat);
                           // EV_RXCHAR에서 이벤트가 발생하면
                           if ((EvtMask & EV_RXCHAR) && ComStat.cbInQue) {
                                        if (ComStat.cbInQue > MAXBUF)
                                                     size = MAXBUF;
                                        else
                                                     size = ComStat.cbInQue;
                                        do {
                                                     ClearCommError(Comm->m_hComDev, &ErrorFlags, &ComStat);
                                                     // overlapped I/O를 통해 데이터를 읽음
                                                     if (!ReadFile(Comm->m_hComDev,buf+insize,size,&Length,&(Comm->m_OLR))) {
                                                     // 에러
                                                                  TRACE("Error in ReadFilen");
                                                                  if (GetLastError() == ERROR_IO_PENDING)       {
                                                                                if (WaitForSingleObject(Comm->m_OLR.hEvent, 1000) != WAIT_OBJECT_0)
                                                                                             Length = 0;
                                                                                else
                                                                                             GetOverlappedResult(Comm->m_hComDev,&(Comm->m_OLR),&Length,FALSE);
                                                                  }
                                                                  else
                                                                                Length = 0;
                                                     }
                                                     insize += Length;
                                        } while ((Length!=0) && (insize<size));
                                        ClearCommError(Comm->m_hComDev, &ErrorFlags, &ComStat);
                                        if (Comm->m_nLength + insize > MAXBUF*2)
                                                     insize = (Comm->m_nLength + insize) - MAXBUF*2;
                                        // 이벤트 발생을 잠시 중단하고 input buffer에 데이터를 복사
                                        Comm->m_pEvent->ResetEvent();
                                        memcpy(Comm->m_sInBuf+Comm->m_nLength,buf,insize);
                                        Comm->m_nLength += insize;
                                        // 복사가 끝나면 다시 이벤트를 활성화 시키고
                                        Comm->m_pEvent->SetEvent();
                                        LPARAM temp=(LPARAM)Comm;
                                        // 데이터가 들어왔다는 메시지를 발생
                                        SendMessage(Comm->m_hWnd,WM_MYRECEIVE,Comm->m_nLength,temp);
                           }
             }
             PurgeComm(Comm->m_hComDev, PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
             LPARAM temp=(LPARAM)Comm;
             // 쓰레드가 종료될 때 종료 메시지를 보냄
             SendMessage(Comm->m_hWnd,WM_MYCLOSE,0,temp);
             return 0;
}
자~ 마지막으로 통신 포트를 닫는 함수인 Close 함수, overlapped i/o를 close 하는 HandleClose함수,
통신 포트의 버퍼를 클리어하는 Clear 함수만 구현하면 끝이 납니다.
별도의 설명이 없어도 이해 하실 줄 믿습니다. 그렇죠? 쉽네~머 시리얼이 별건가?
void CPYH_Comm::Close()
{
             if (!m_bIsOpenned)
                           return;
             m_bIsOpenned = FALSE;
             SetCommMask(m_hComDev, 0);
             EscapeCommFunction(m_hComDev, CLRDTR);
             PurgeComm(m_hComDev,PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
             Sleep(500);
}
void CPYH_Comm::HandleClose()
{
             CloseHandle(m_hComDev);
             CloseHandle(m_OLR.hEvent);
             CloseHandle(m_OLW.hEvent);
}
void CPYH_Comm::Clear()
{
             PurgeComm(m_hComDev, PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
             memset(m_sInBuf,0,MAXBUF*2);
             m_nLength = 0;
}
이렇게 시리얼 통신 클래스의 모든 구현이 끝났습니다. 아주 특수한 경우가 아니라면 제 경험상 거의 대부분 무리 없이 잘 돌아가는 클래스입니다.
2차 출처 : http://roter.pe.kr/80
