API/MFC

Visual C++ 시리얼 통신(RS-232) 강좌 (1)

by MoA posted Jul 28, 2013
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 게시글 수정 내역 댓글로 가기 인쇄

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;

             // Overlapped 구조체의 이벤트를 생성

             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;

 

}

 

이렇게 시리얼 통신 클래스의 모든 구현이 끝났습니다아주 특수한 경우가 아니라면 제 경험상 거의 대부분 무리 없이 잘 돌아가는 클래스입니다.


1차 출처 : http://blog.naver.com/loginpark/120089678114

2차 출처 : http://roter.pe.kr/80