JHB의 프로그래밍 삽질기

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

PROGRAMMING/Win/MFC

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

roter 2010.04.29 19:04

본문 수정 및 배포 허가를 받았으며, 본 강좌는 데브피아에도 게제되 있음을 알려드립니다.

퍼가실때는 꼭 출처를 밝혀 주시기 바랍니다.

 

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;

 

}

 

이번에는 통신 포트를 통하여 들어온 데이터를 받는 함수를 구현해 보겠습니다. 함수는 다음과 같습니다. Inbuflen 만큼 길이의 데이터를 받는 것입니다. 별도의 설명은 드리지 않겠습니다.

 

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] = '\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,'\0',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 ReadFile\n");

                                                                  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;

 

}

 

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


저작자 표시 비영리 변경 금지
신고
11 Comments
  • 프로필사진 오르골소리 2010.04.30 10:34 신고 CEvent 안되는 컴파일러의 경우 afxmt.h 선언
  • 프로필사진 BlogIcon 하늘의은총 2011.06.08 02:08 신고 Oncreate() 지정된 파일을 찾을 수 없습니다. 라는 에러는 뭘까요.ㅠㅠ
  • 프로필사진 BlogIcon roter 2011.06.08 17:37 신고 안녕하세요 하늘의은총님 저의 블로그에 방문해주셔서 감사합니다 :)
    질문의 내용이 너무 광범위 해서요, 구체적으로 말씀해주시면 답을 찾아 노력해보겠습니다 :)
    질문이 길어지시면 방명록에 남기셔도 괜찮으시구요.
    감사합니다 :D
  • 프로필사진 시리얼어렵다ㅠㅠ 2011.07.21 15:55 신고 시리얼 구문은 작성하였습니다.
    이후에 제가 사용할 클래스에서

    CPYH_Comm* Serial_Comm;
    void CSerial::OnBnClickStart
    {
    Serial_Comm = new CPYH_Comm(MotorComport, MotorBaudrate, "None","8 Bit","1 Bit");
    if(Serial_Comm->Create(GetSafeHwnd()) != 0)
    {
    블라블라
    }
    }
    한 후에 버튼을 한개 생성해서 잠시 멈추는 구문을 만들었습니다.
    void CSerial::OnBnClickedBtstop()
    {
    if (Serial_Comm)
    {
    timeKillEvent(m_nTimerID_Recv);
    timeKillEvent(m_nTimerID_Send);

    Serial_Comm->Close();
    Serial_Comm = NULL;
    }
    }

    이런 식으로 하였는데, 문제는
    Start를 누르면 포트가 생성이 잘 됩니다. 하지만 Stop을 누른 후에
    다시 Start를 누르면 포트가 생성되지 않습니다.
    m_hComDev에서 0xfffffff 를 출력하는데, 왜그럴까요ㅠㅠ
  • 프로필사진 BlogIcon roter 2011.07.25 10:40 신고 안녕하세요 시리얼어렵다님~ 저의 블로그에 방문해 주셔서 감사합니다.
    시리얼을 close할 때는 그냥 close만 하면 되는 것이 아니고 위의 설명을 보면 아시겠지만 close 프로세스를 총 세번에 걸쳐서 해주셔야 합니다.
    Close(), HandleClose(), Clear() 이렇게 세개 입니다.
    세개 모두 호출 후 다시 Start하시면 잘 될거에요.
    해결 되시길 바랍니다 :)
    좋은 하루 보내세요.
  • 프로필사진 감사합니다 2011.09.08 09:47 신고 초보라 어렵지만 자꾸 보고있습니다
    감사합니다
  • 프로필사진 BlogIcon roter 2011.09.08 16:21 신고 저도 해당 소스 보면서 열심히 구현했던 기억이 나네요 ^^
    화이팅입니다!
  • 프로필사진 탱구 2013.04.01 10:40 신고 좋은글 담아갑니다.
  • 프로필사진 BlogIcon roter 2013.04.06 15:44 신고 감사합니다 :)
  • 프로필사진 용국 2014.11.07 15:48 신고 MFC에선 구현이 잘되는데
    콘솔에선 실행오류가 자꾸 뜨네요
    이것저것 오류를 해결하고 나중에보니 m_bisOpenned 변수가 포트가 열린 후에 Create함수를 빠져나가면 자꾸 0이 되어버립니다.
    이유가뭔지 알수있을까요?
  • 프로필사진 BlogIcon roter 2015.01.13 22:11 신고 안녕하세요~ 코멘트를 이제야 확인했네요..
    말씀하신 내용 만으로는 문제가 무엇인지 파악하기가 힘드네요.
    지금쯤이면 해결하셨으리라 봅니다..
    늦게 봐서 죄송..(_ _)
댓글쓰기 폼