http://blog.naver.com/hoi_yeon?Redirect=Log&logNo=10012385864
Serial Port 제어
UNIX나 Window등 Workstation 이상 급의 컴퓨터에서는 가상 디바이스 개념이 적용되어 하드웨어를 직접 제어할 수 없도록 되었습니다. 그 대신에 디바이스 드라이버를 통해 하드웨어를 제어 할 수 있도록 되어 있습니다. Windows나 UINX에서는 특별히 Serial Port나 Parallel Port등의 기본적이고 자주 쓰이는 장치는 일반 File과 같이 Open, Close하여 Read, Write할 수 있도록 하였습니다.
COM1, COM2, LPT1, LPT2 등의 장치 명으로 된 것들이 그러한 것들입니다.
1) COM Port Open
Windows에서 Serial Port인 COM Port를 스트림으로 Open 하려고 할 경우는 CreateFile()함수를 사용합니다.
HANDLE CreateFile(
LPCTSTR lpFileName, // 디바이스 파일 이름
DWORD dwDesiredAccess, // 파일 억세스 모드
DWORD dwShareMode, // 공유 모드
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 보안 정보
DWORD dwCreationDisposition, // 생성 방법
DWORD dwFlagsAndAttributes, // 파일 플래그와 속성
HANDLE hTemplateFile
);
CreateFile로 Open된 시리얼 포트는 이 함수에서 리턴된 파일 핸들을 이용하여 콘트롤 하게됩니다. 이 파일 핸들을 이용해서 데이터를 쓰거나 읽는 것은 직접 UART Buffer에서 데이터를 읽는 것과 동일한 역할을 하게되는 것입니다.
lpFileName 은 Open하고자 하는 디바이스 파일의 이름을 말합니다. 만약 COM1을 Open하고자 하는 경우 여기에 “COM1"이란 String을 주면 됩니다. 만약 COM1~COM4 이상의 Port를 사용하려고 하는 경우는 Windows System에서 MAX_PATH로 제한하고 있는 파일 길이를 초과 하게되므로 사용할 수가 없습니다. 이런 경우에는 CreateFile의 Wide Version을 이용하여 확장하면 됩니다. 이 경우에도 특별히 달라지는 것은 없고 단지 Device File Name 앞에 "\\.\"를 덧붙여 주면 됩니다. 즉 COM10을 Open하고자 하는 경우라면 ”COM10" 대신에"\\.\COM10"이라고 써주기만 하면 됩니다.
dwDesiredAccess는 Device File을 어떤 모드로 Access할 지를 정해주는 부분입니다. 여기서는 3가지 모드를 조합하여 설정해 줄 수 있는데
0 : 디바이스의 특성만을 물어보고 데이터를 Read/Write하지 않을 경우.
GENERIC_READ :디바이스에서 데이터를 읽고 파일 포인터를 움직일 수 있도록 한다.
GENERIC_WRITE : 디바이스에서 데이터를 쓰고 파일 포인터를 움직일 수 있도록 한다.
COM Port의 경우 우리는 데이터를 읽고 쓰는 것이 모두 가능해야 하므로 GENERIC_READ|GENERIC_WRITE로 Open해야 합니다..
dwShareMode는 파일을 어떤 특성으로(읽기 전용, 읽기/쓰기) 열 것인가를 설정하는 부분인데, Device File의 경우는 공유될 수 없으므로 그냥 0으로 Setting합니다.
lpSecurityAttributes는 파일의 보안을 설정해 주는 부분인데 마찬가지로 디바이스 파일에서는 사용하지 않으므로 NULL로 Setting합니다.
dwCreationDisposition은 파일을 생성할 때 새로이 만들 것인가 아니면 기존의 것에 덧붙일 것인가 아니면 이미 존재하는 경우에만 생성할 것인가를 정해주는 부분입니다. 디바이스 파일은 이미 존재하고 있지 않다면 생성할 수 있는 것이 아니므로 이 부분은 항상 OPEN_EXISTING으로 Setting되어야만 합니다. 그렇지 않으면 오류를 일으키게 되므로 주의하여야 합니다.
dwFlagsAndAttributes는 파일의 속성을 정해주는 부분인데 디바이스 파일의 경우는
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED로 설정해 주어야 합니다.
FILE_ATTRIBUTE_NORMAL : 다른 특성을 가지고 있지 않은 일반 파일임을 의미합니다.
FILE_FLAG_OVERLAPPED : 일반 File의 경우는 데이터를 File에 Write할 때 Caching을 하게되어 있으나 디바이스 파일의 경우는 Write할 때 현재 버퍼에 들어있는 내용을 다 전송하고 나서 전송하여만 합니다. 그러므로 이 Flag 를 Set하여 주어서 전송하기 전에 시스템에서 전송하고자 하는 장치를 초기화하고 읽기나 쓰기가 끝난 경우 메시지를 띄워 프로그램에 알려 주도록 해야 합니다.
이런 목적에서 이 Flag를 Setting할 경우엔 항상 데이터를 쓰고 읽는 함수에서 OVERAPPED Structure를 이용하여 Overapped Read/Write를 해주어야만 합니다.
hTemplateFile은 GENERIC_READ로 Access Mode를 Setting 하였을 때 템플릿 파일을 제공하도록 하는 것으로 템플릿 파일은 Win NT에서 만 제공되므로 Win95나 Win98에서 사용하고자 하는 경우에는 항상 NULL로 Setting하여야만 합니다.
이상과 같이 Setting해주고 File을 Open하면 성공시에는 Device 파일을 제어하기 위한 Handle이 리턴 되고 실패하는 경우는 INVALID_HANDLE_VALUE가 Return 됩니다.
2) COM Monitor Event 설정
COM Port를 Open하고 나면 이제 COM Port에서 발생하는 이벤트 중 어떤 이벤트를 사용할 지를 결정해 주어야만 합니다. 이것을 해주는 함수는 SetCommMask()입니다.
BOOL SetCommMask(
HANDLE hFile, // Device 파일의 핸들
DWORD dwEvtMask // 가능한 이벤트 중 사용할 것만 마스크 시킨다.
);
hFile은 위의 CreateFile에서 리턴된 파일 핸들 입니다.
dwEvtMask는 사용가능 한 이벤트 중 어느 것을 사용할 지 설정해주는 정보이며 EV_BREAK, EV_CTS, EV_DSR, EV_ERR, EV_RING, EV_RLSD, EV_RXCHAR, EV_RXFLAG, EV_TXEMPTY 같은 값들을 조합하여 만들어 줄 수 있습니다. 본 예제에서는 단순히 데이터가 들어오는 지만 체크하므로 EV_RXCHAR만 사용하지만 보다 정교한 제어를 원하는 경우엔 필요로 하는 플래그를 추가하여 처리 할 수 있습니다.
3) 입출력 버퍼 크기 설정
다음으로 해 줄 일은 입출력 큐의 크기를 정해주는 것입니다. 입출력 큐의 크기를 정해주는 함수는 SetupComm() 입니다.
BOOL SetupComm(
HANDLE hFile, // Device 파일의 핸들
DWORD dwInQueue, // 입력 buffer의 크기
DWORD dwOutQueue // 출력 buffer의 크기
);
입출력 버퍼의 크기는 주고받는 데이터의 량에 따라 결정되겠지만 일반적으로 4096 (80*25*4) 정도의 크기로 설정해 주는 것이 좋습니다. 80*25는 VT100 단말기의 모니터 사이즈입니다.
4) 입출력 버퍼의 초기화
처음 버퍼를 생성하고 난 후에는 시리얼 포트에 대기하고 있던 모든 입출력 데이터를 제거하고 전송이나 수신하기 위해 대기하고 있던 것들을 없애 주어야 합니다. 이 일을 해주는 함수가 PurgeComm()입니다.
BOOL PurgeComm(
HANDLE hFile, // Device 파일의 핸들
DWORD dwFlags // 수행 작업
);
dwFlags는 초기화 시 수행할 작업들을 설정해 주는 것으로 다음과 같습니다.
PURGE_TXABORT : Overapped 전송 작업을 취소 합니다.
PURGE_RXABORT : Overapped 전송 작업을 취소 합니다.
PURGE_TXCLEAR : 출력 버퍼를 클리어
PURGE_RXCLEAR : 입력 버퍼를 클리어
5) 타임아웃 값 설정
이제 읽기 쓰기 Overapped I/O를 위한 Time Out 값들을 설정해 주어야할 단계입니다. MFC에서 Serial Port의 Timeout 값을 설정해 주기 위해 사용하는 structure는 COMMTIMEOUTS입니다.
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;
DWORD ReadTotalTimeoutMultiplier;
DWORD ReadTotalTimeoutConstant;
DWORD WriteTotalTimeoutMultiplier;
DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
ReadIntervalTimeout : 데이터가 들어올 때 두 바이트의 입력이 이루어지는 시간으로 이 이상의 시간만큼 다음 바이트의 입력이 없을 경우 입력 값을 리턴하고 RX_CHAR Message를 발생시킵니다.
ReadTotalTimeoutMultiplier*ReadTotalTimeoutConstant : 읽기 동작에 걸리는 총 시간으로 이 값이 0이면 사용하지 않는다는 뜻입니다.
WriteTotalTimeoutMultiplier*WriteTotalTimeoutConstant : 쓰기 동작에 걸리는 총 시간으로 이 값이 0이면 사용하지 않는 다는 뜻입니다.
위의 값들을 설정한 후 SetCommTimeouts() 함수를 호출하여 Timeout값을 설정합니다.
BOOL SetCommTimeouts(
HANDLE hFile, // Device 파일의 핸들
LPCOMMTIMEOUTS lpCommTimeouts // COMMTIMEOUTS
);
즉, ReadIntervalTimeout만큼 기다리다 다음 데이터가 없으면 읽은 데이터를 리턴하고, ReadTotalTimeoutMultiplier * ReadTotalTimeoutConstant의 시간만큼 기다려도 읽기로한 데이터 사이즈만큼 읽지못하거나 WriteTotalTimeoutMultiplier * WriteTotalTimeoutConstant 시간 안에 전송을 못할 경우 에러 이벤트를 발생시키도록 설정하는 것입니다.
6) 포트 설정
다음은 포트의 전송 속도, 데이터 비트 수, 패리티, 스톱 비트 수, XON/XOFF Flow Control
설정을 해줄 차례입니다. MFC에서는 이런 설정을 위해 DCB라는 structure를 제공합니다.
typedef struct _DCB {
DWORD DCBlength; // sizeof(DCB)
DWORD BaudRate; // baud rate
DWORD fBinary: 1; // binary mode, EOF을 check 하지 않음
DWORD fParity: 1; // Parity를 체크함
DWORD fOutxCtsFlow:1; // CTS output flow control을 함
DWORD fOutxDsrFlow:1; // DSR output flow control을 함
DWORD fDtrControl:2; // DTR flow control type
DWORD fDsrSensitivity:1; // DSR 감지
DWORD fTXContinueOnXoff:1; // XOFF 라도 전송을 계속함
DWORD fOutX: 1; // XON/XOFF 전송 흐름 제어
DWORD fInX: 1; // XON/XOFF 입력 흐름 제어
DWORD fErrorChar: 1; // 오류 수정
DWORD fNull: 1; // 입력 받은 데이터 중 NULL을 없앰
DWORD fRtsControl:2; // RTS flow control
DWORD fAbortOnError:1; // Error 발생 시 입출력 취소
DWORD fDummy2:17; // reserved
WORD wReserved; // 현재 사용 안함
WORD XonLim; // 전송 XON 제한 치
WORD XoffLim; // 전송 XOFF 제한 치
BYTE ByteSize; // 한 Byte당 비트 수
BYTE Parity; // 0-4=no,odd,even,mark,space
BYTE StopBits; // 0,1,2 = 1, 1.5, 2
char XonChar; // XON으로 사용할 문자
char XoffChar; // XOFF로 사용할 문자
char ErrorChar; // 오류 시 대체할 문자
char EofChar; // EOF 문자
char EvtChar; // 수신 이벤트 문자
WORD wReserved1; // reserved
} DCB;
위의 값들을 모두 일일이 설정해도 되지만 편의를 위해 GetCommState() 함수를 이용할 수도 있습니다. 이 함수는 시스템에서 이미 사용 중인 설정을 읽어옵니다.
BOOL GetCommState(
HANDLE hFile, // Device 파일의 핸들
LPDCB lpDCB // DCB의 Pointer
);
이렇게 읽어온 설정 중 원하는 설정을 바꾸어 주고 SetCommState() 함수를 이용해서 다시 설정해 줍니다.
BOOL SetCommState(
HANDLE hFile, // Device 파일의 핸들
LPDCB lpDCB // DCB의 Pointer
);
7) 포트 감시 쓰레드 생성과 데이터 수신
포트로 데이터가 들어오는 지를 알기 위해서는 계속해서 포트에서 데이터를 읽는 방법이 있을 수 있습니다. 그렇지만 프로그램 전체에서 이런 식으로 무한 루프를 이용해서 입력을 감시하는 것은 좋지 못한 방법일 뿐 아니라 메시지를 기반으로 움직이는 Windows의 철학에도 맞지 않는 방법입니다. 이와 별도로 윈도우에서는 WaitCommEvent() 함수를 제공합니다. 이 함수를 실행하면 프로세스는 아무 것도 하고 있지 않다가 COM Port에서 위에서 설정한 Event가 발생할 경우 리턴됩니다.
BOOL WaitCommEvent(
HANDLE hFile, // Device 파일의 핸들
LPDWORD lpEvtMask, // 리턴 된 이벤트를 저장할 포인터
LPOVERLAPPED lpOverlapped, // Overapped structure에로의 포인터
);
이런 방법을 써도 간단한 프로그램에서는 가능하겠지만 실제 프로그램에서는 입력이 없을 경우에는 자신의 일을 수행해야 하므로 위와 같이 입력을 마냥 기다릴 수는 없습니다. 그러므로 입력 이벤트를 감시하기 위한 Thread를 생성하여 입력의 감시는 이 쓰레드에 전담시키고 원래의 프로그램은 자신의 일을 계속하도록 해 주어야만 합니다. 윈도우에서 쓰레드를 생성시키는 함수는 CreateThread()입니다.
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 쓰레드의 보안 특성을 설정
DWORD dwStackSize, // 쓰레드에서 사용할 스택의 초기 크기를 설정
LPTHREAD_START_ROUTINE lpStartAddress,// 쓰레드로 사용할 함수의 포인터
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
lpThreadAttributes : 생성된 쓰레드가 부모의 특성을 물려받을 지를 결정. 이 경우 우리는 NULL로 설정하여 특성을 물려받지 않아도 지장이 없습니다.
dwStackSize : 생성되는 쓰레드에서 사용할 스택의 크기. 이것을 0으로 설정하여 주 Process의 크기에 따라 자동으로 설정하게 해 두면 쓰레드가 종료될 경우에 자동으로 해지도 됩니다. 예제에서는 0으로 설정 이러한 특징을 이용하였다. 만약 여기서 설정한 스택 사이즈 만큼의 memory를 할당 받지 못하면 쓰레드가 생성되지 않습니다.
lpStartAddress : 쓰레드로 수행할 함수의 포인터를 설정합니다.
lpParameter : 쓰레드에 전달할 파라메터가 있을 경우 사용, 우리의 경우 부모 Process의 Window Handle을 이를 이용하여 전달함으로써 부모 프로세스에 메시지를 전달하는데 사용합니다.
dwCreationFlags : 이 값을 CREATE_SUSPENDED로 해두면 ResumeThread()를 이용하여 활성화시키기 전에는 쓰레드가 정지해 있습니다. 우리는 쓰레드가 즉각 작동해야하므로 0으로 Setting 합니다.
lpThreadId : 쓰레드를 식별하기 위한 핸들의 포인터입니다.
이렇게 쓰레드를 생성시키고 Serial Port를 감시하도록 한 뒤에는 원 프로그램은 계속 일을 수행할 수 있습니다. 이제 이 쓰레드가 Serial Port에서 데이터가 들어왔다는 것을 원 프로그램에 전달하는 방법에 대해서 알아보도록 하겠습니다.
쓰레드를 생성시킨 프로그램과 쓰레드 간에 통신을 위해서는 Message를 이용하여야 합니다. 즉, 쓰레드 생성 시에 원 프로그램의 윈도우 핸들을 저장해두었다가 이 윈도우 핸들을 이용해서 사용자 정의 Message를 원 프로그램에 보내고 원 프로그램은 이 Message의 Callback 함수를 만들어 두고 여기서 Serial Port에서 데이터를 읽도록 하는 것입니다. Buffer를 생성시키고 lpBuffer에 이 포인터를 전달하고 읽기를 원하는 크기를 설정해주고 ( 이 크기는 꼭 실제로 읽을 크기와 다르더라도 괜찮습니다 ) ReaFile()을 실행시키면 Serial Port의 Buffer속에 있던 데이터를 읽어서 Buffer에 저장해 줍니다.
[ 쓰레드에서 하는 일 ]
먼저 WaitCommEvent함수를 실행 시켜서 Serial Port에서 Event가 발생할 때까지 대기 합니다.
WaitCommEvent(m_hComm,&dwEvent,NULL); //Serial Port Event 대기
Event가 발생하면 WaitCommEvent함수가 리턴됩니다. 그러면 리턴된 Event가 무엇인지에 따라 처리를 해 줍니다. 가령 입력이 있어서 EV_RXCHAR가 발생했다면
if ((dwEvent & EV_RXCHAR) == EV_RXCHAR) // Event가 데이터 입력이면
{
do
{
포트에서 데이터를 읽어서 버퍼에 저장합니다.
} while (dwRead);
그리고 나서 부모 프로세스에 메시지를 보내어 데이터를 읽어가도록 합니다.
::PostMessage(hCommWnd,WM_USER+1,0, 0);
}
[부모 프로그램에서 하는 일]
먼저 위에서 얘기한 WM_USER+1등의 사용자 정의 메시지에 따른 콜백 함수를 만들어 줍니다.
BEGIN_MESSAGE_MAP(CAccuMView, CFormView)
//{{AFX_MSG_MAP(CAccuMView)
:
ON_MESSAGE(WM_USER+1,OnCommRead) // Callback 함수로 설정
:
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
// Callback 함수에서 데이터를 읽어서 다양한 처리를 해주면 됩니다.
LONG CComuView::OnCommRead(UINT wParam, LONG lParam )
// WM_USER+1의 Callback 함수
{
:
버퍼에서 데이터를 읽고 처리한다.
:
}
데이터를 포트에서 읽어오는 함수는 ReadFile()입니다.
BOOL ReadFile(
HANDLE hFile, // Device 파일의 핸들
LPVOID lpBuffer, // 데이터를 저장한 버퍼의 포인터
DWORD nNumberOfBytesToRead, // 읽을 바이트 수
LPDWORD lpNumberOfBytesRead, // 읽은 바이트 수를 저장할 변수의 포인터
LPOVERLAPPED lpOverlapped // Overapped structure에로의 포인터
);
8) 데이터를 주고 받기 위한 자료 구조
위에서 Thread와 부모 프로그램간에 데이터를 주고 받기 위한 버퍼가 있어야 했다. 이 경우 Thread 측에서는 데이터를 입력만 시키고 모 프로그램에서는 데이터를 읽기만 하므로 Queue를 사용하는 것이 좋습니다. 즉, Thread에서는 큐에 데이터를 넣고 이벤트를 발생 시키고 부모 프로그램에서는 큐에서 데이터를 읽어오는 것입니다.
[ 쓰레드에서 하는 일 ]
먼저 Serial Port에서 데이터를 읽어옵니다.
dwRead = pComm->ReadComm( buff, 2048);
읽은 데이터를 한 Byte씩 Queue에 집어 넣습니다.
if (BUFF_SIZE - pComm->m_QueueRead.GetSize() > (int)dwRead)
{
for ( WORD i = 0; i < dwRead; i++)
pComm->m_QueueRead.PutByte(buff[i]);
}
else AfxMessageBox("m_QueueRead FULL!");
[부모 프로그램에서 하는 일]
현재 Queue에 들어 있는 데이터의 크기를 얻어옵니다.
int size= (m_ComuPort.m_QueueRead).GetSize();
Queue가 빌때까지 데이터를 읽어옵니다.
for( int i=0; i< size; i++ )
{
(m_ComuPort.m_QueueRead).GetByte(&aByte);
if( aByte!= NULL ) buff[i]= aByte;
else { i--; size--; }
}
9) 데이터 송신
데이터를 송신하기 위해서는 WriteFile()을 이용 합니다.
BOOL WriteFile(
HANDLE hFile, // Device 파일의 핸들
LPCVOID lpBuffer, // 데이터를 저장할 버퍼의 포인터
DWORD nNumberOfBytesToWrite, // 보낼 바이트 수
LPDWORD lpNumberOfBytesWritten,// 보낸 바이트 수
LPOVERLAPPED lpOverlapped // Overapped structure에로의 포인터
);
Buffer를 생성시키고 lpBuffer에 이 포인터를 전달하고 쓰기를 원하는 크기를 설정해주고 WriteFile()을 실행시키면 Buffer속에 있던 데이터를 Serial Port를 통해 출력해 줍니다.
이상과 같은 절차를 이용한 CCommThread를 만들고 이를 이용하여 뒤의 예제와 같이 Serial통신을 하게됩니다.
[출처] Serial Port 제어 |작성자 바부