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