GPS620은 NMEA 프로토콜을 따른다.

NMEA 0183 프로토콜은 The National Marine Electronics Association 에서 정한 지리 정보의 전송과 관련된 규약으로 물리, 데이터링크, 어플리케이션 레벨의 규약의 집합체다. 물리, 데이터 링크 계층의 규약은 일반적인 RS232 시리얼 통신을 따르기 때문에 본 블로그의 시리얼 통신 관련 포스트를 보면 될 것 같다. 

 

어플리케이션 레벨의 규약은 여러가지가 있는데 그 중에 알아야 할 사항은 사실 몇가지 되지 않는다.. $GPGGA, $GPMRC 등 여러가지 머릿말을 달고 GPS수신기로부터 시리얼 통신을 통해 컴퓨터로 수신되는 정보들이 있지만 실상 알아야 할 정보는 아래에 있는 $GPGGA정보면 충분하다. 실제 GPGGA 정보를 가지고 각 값들이 의미하는 바를 알아보도록 하겠다.

 

 $GPGGA,142243.731,3739.0081,N,12704.0193,E,0,00,,,M,,,,0000*05

 

1.     전송 캐릭터 셋 : 시리얼 통신을 통해 전송되는 바이트 열은 모두 ASCII코드다.

 

2.     중요 정보 문자열 : $GPGGA, … <CR><LF>

$GPGGA로 시작해서 <CR><LF>로 종료되는 이 문자열 안에 실제로 GPS장치가 수신한 GPS정보가 들어있다. 각 정보는 쉼표 ‘,’ 로 구분되며 위도 경도 값은 ,로 구분되는 각 문자열 중에 35번째에 들어가 있다. (Index2, 4)

 

3.     GPGGA값에 들어가 있는 위경도 값의 형식 (Degree, Minute)

$GPGGA 문자열에 들어가 있는 위경도 값은 도,분 값으로 표현되어 있다. 위도의 경우 앞의 2개가 도, 나머지 값이 분이다. 경도의 경우 앞의 3개가 도, 나머지 값이 분이다. 예를 들어 위도 3834.0753의 경우 38340.0753*60 초다. 그리고 경도의 경우 12705.124의 경우 12750.124*60 초 이다. 그리고 각 위경도 값 다음에 쉼표 ‘,’ 가 나오고 이 위경도 값이 북위(N), 남위(S), 인지 동경(E), 서경(W) 인지 아스키 코드가 한 개 들어가 있다.

 

4.     위도 1초 당 30.828미터, 그리고 경도 1초 당 24.697미터의 거리 차이가 있다.

 

5.     GPGGA 문자열의 7번째 (Index 6) 의 값이 0인 경우 수신한 문자열의 위경 도 값이 신뢰 할 수 없음을 뜻한다. 1, 2 ,3 .. 각 숫자에 따라 위경도 값이 수정된 방식을 뜻하나 사실상 0이 "신뢰할 수 없음" 이라는 것만 알아도 상관 없다.

 

6.     구글맵을 이용한 테스트 방법

도분초 위경도로 위치 검색

37°33'58.35"N,126°58'40.30"E 을 검색창에 입력하면 해당 위치를 구글맵으로 표시해줌

 

Posted by 굿쟌
,

비동기 OVERLAPPED 읽기 수행하기

 

일단 비동기 read부터 알아보자. 역시나 백문이 불여일견이니 코드부터 보자

 

DWORD CSerialz::ReadData(BYTE *pBuff, DWORD nToRead)
{
 DWORD dwRead;
 DWORD dwError;

 //COMSTAT은 현재 읽기, 쓰기 버퍼등 통신에 관련된 상태를 담는 구조체
 DWORD dwErrorFlags;
 COMSTAT comstat;

 

//장치를 사용 할 수 있도록 오류플래그를 받아오고 오류 상태를 해제한다.

//그리고 장치 상태를 얻어온다.
ClearCommError(m_hIDComDev, &dwErrorFlags, &comstat);
 
 // 읽기버퍼에 들어와있지만 아직 읽지 못하고 남아있는 바이트 수
 dwRead = comstat.cbInQue;

 

//읽기 버퍼에 아직 읽지 못하고 남아있는 바이트가 남아 있는 경우
 if(dwRead > 0)
 {
    //읽기 버퍼를 nToRead만큼 읽어 들여온다. 그리고 읽기 성공한 바이트 수를 

    //dwRead에 다시 저장 비동기 함수라 바로 FALSE 반환
    if(!ReadFile(m_hIDComDev, pBuff, nToRead, &dwRead, &m_OverlappedRead))
   {
       //정상적으로 읽기 작업이 작업 큐에 저장
       if(GetLastError() == ERROR_IO_PENDING)
      { 
          // timeouts에 정해준 시간만큼 기다려준다.

          //COMMTIMEOUTS에 읽기는 기다리지 않도록 설정한 상태
          while (! GetOverlappedResult(m_hIDComDev,

                                                    &m_OverlappedRead,

                                                    &dwRead,

                                                    TRUE))
          {
               //읽을 때까지 기다렸는데 미리 설정한 타임아웃 이내에 읽지 못한 경우 
               dwError = GetLastError();
               if (dwError != ERROR_IO_INCOMPLETE)
               {

                   //장치의 에러 상태를 해제한다.
                   ClearCommError(m_hIDComDev, &dwErrorFlags, &comstat);
                   break;

                }
           }
       }

       //작업큐에 읽기 작업이 정상적으로 저장되지 못한 경우
       else
       {
            dwRead = 0;

             //에러상태를 해제한다.
            ClearCommError(m_hIDComDev, &dwErrorFlags, &comstat);
        }
     }
 }

 

 //실제로 읽은 바이트 수 반환
 return(dwRead); 

 

뭐 딱히 별다른 설명이 필요하지 않다. 읽는 것 자체는 Overlapped write와 크게 다르지 않다. 다르다고 할 만한게 있다면 ClearCommError의 사용 정도가 있겠다. ClearCommError 함수는 파일디스크립터 (핸들) 이 가리키는 IO Device를 사용 할 수 있도록 에러 상황을 받아오고서는 에러 상태를 해제한다. 그리고 IO Device관련된 정보를 받아오는 기능을 한다.

 

이제 우리는 읽기 버퍼로부터 바이트를 읽어 올 수 있게 되었다. 그렇다면 이제 남아있는 문제는 "어떻게 읽기 버퍼로 읽을 수 있는 바이트가 저장되는 것을 알아 차릴 수 있는 가?"이다. 방법은 쓰레드를 이용해 읽기 버퍼에 데이터가 들어올 때 발생하는 이벤트를 감시하고 있는 것인데 쓰레드로 돌릴 함수는 아래와 같다.

 

//COM 포트에서 이벤트가 일어나는지 감시하는 스레드 함수 DWORD CSerialz::THWatchEvPort(CSerialz *pComm)
{
   DWORD  dwEvent;
   OVERLAPPED os;
   BOOL  bOk = TRUE;  
   BYTE  buff[4096]; 
   DWORD  dwBufferCounter;   

 

   // Overap Structure 설정
   memset(&os, 0, sizeof(OVERLAPPED));
   if(!(os.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) bOk = FALSE;

 

   // m_hIDComDev COM 포트의 읽기 버퍼에 데이터가 들어왔을 때 발생하는

   //이벤트및 몇가지 에러 상황을 감지하도록 설정
   if(! SetCommMask(pComm->m_hIDComDev, 

                             EV_RXCHAR  |  EV_BREAK  |  EV_ERR )) bOk = FALSE;

 

   // OVERLAPPED 구조체 설정및 COM포트의 감지 이벤트 설정 중에 문제가

   //발생했으면 return FALSE
   if(!bOk) { 

      AfxMessageBox(_T("Error while Receive Waiting") + pComm->szPort); 

      return FALSE; 

   }

 

   // port가 열려 있는 동안.....포트가 PortClose에 의해 닫히면 m_bPortOnOff

   // 가 FALSE가 되어 종료.
   while(pComm->m_bPortOnOff)  
   {         
      dwEvent = 0;
    
       //이미 이벤트가 발생해 있는 상태라면 TRUE 반환, 이벤트 미발생 상태로

       //펜딩되면 FALSE 반환. OVERLAPPED의 이벤트는 함수 호출시 자동 RESET   
       BOOL result = WaitCommEvent(pComm->m_hIDComDev, &dwEvent, &os);  
  
       //WaitCommEvent함수 호출시 이벤트 발생된 상태로 바로 이벤트 감지
       if(result)
       {
          //DoNothing
        }
        else
       {
           //WaitCommEvent가 PENDING된 경우
           if(GetLastError() == ERROR_IO_PENDING)
          {

              //이벤트가 발생하면 다음단계로 진행
              WaitForSingleObject(os.hEvent,INFINITE);
           }  

          //에러로 인한 이벤트 발생시 스레드 종료
          else
          {
                break;
           }
      }     

      

      //발생 이벤트가 읽기 버퍼에 데이터 수신으로 인한 이벤트인 경우
      if((dwEvent & EV_RXCHAR) == EV_RXCHAR)
      {   // 포트에서 읽을 수 있는 만큼 읽는다.
          do

          {

                //읽기 버퍼에서 4096 (전체 버퍼 싸이즈) 만큼 읽어 들여와서 읽기

                //성공한 바이트 수를 dwBufferConter에 저장한다.
                dwBufferCounter = pComm->ReadData(buff, 4096);    // 데이터 읽기. 
                pComm->FrmTHRcvData(buff, (BYTE)dwBufferCounter);  //data 사용   
           }while (dwBufferCounter); // 읽을 수 있는 data가 없을 때까지 읽어온다.   
       }
       else
      {
          //에러 상황으로 인해 이벤트가 발생한 경우는 그냥 이 스레드를 종료시킴
          break;
       }
    }

 //OVERLAPPED 구조체의 이벤트 핸들을 닫고 스레드 포인터를 NULL로 한다.
 CloseHandle(os.hEvent);
 pComm->m_ThrdEvPort = NULL;

 return(TRUE);
}

 

주석만으로 전체 코드 구성을 이해 하는데 충분하다고 생각한다. 위의 함수를 CreateThread함수나 AfxBeginThread를 이용하여 돌리면, 읽기 버퍼에 데이터가 들어오는 경우 읽기 버퍼에서 더이상 읽을 버퍼가 없을 때까지 읽어들여 온다.

 

이상 RS232를 이용한 시리얼 통신 코드는 링크를 통해 볼 수 있다.(비공개)

 

Posted by 굿쟌
,

OVERLAPPED 구조체를 이용한 비동기 쓰기

 

시리얼 COM 포트 통신을 이용해 데이터를 읽고 쓰는 방법에 대해 알아보자. 이미 이전포스트에서 주욱 설명해 오고 있는것과 마찬가지로 비동기(Overlapped) 읽기, 쓰기에 대해 다룰 것인데 먼저 쓰기부터 알아보려 한다. 왜냐하면 읽기가 좀더 어렵기 때문이다. ㅎㅎ

 

쓰기는 내가 데이터를 모두 가지고 있고 어디부터 어디까지 언제 쓸 것인지를 선택 할 수 있다. 그러나 쓰기에 비해 읽기는 언제 상대방이 나에게 데이터를 전송할지 알 수 없다. 따라서 상대방이 데이터를 전송하여 내가 읽을만한 데이터가 읽기 버퍼에 들어오는 이벤트를 감지하는 쓰레드를 만들어 돌려야 하기때문에 상대적인 난이도가 쓰기에 비해 높다.

 

어찌됐든, 쓰기는 아래와 같은 WriteFile함수를 이용한다.

 

 BOOL WINAPI WriteFile(
  __in         HANDLE hFile,
  __in         LPCVOID lpBuffer,
  __in         DWORD nNumberOfBytesToWrite,
  __out_opt    LPDWORD lpNumberOfBytesWritten,
  __inout_opt  LPOVERLAPPED lpOverlapped
);

 

쓰기가 성공하면 TRUE를 반환하고, 지금 설명하고 있는 비동기 쓰기의 경우에는 무조건 FALSE를 반환한다. 그러나 GetLastError()를 통해 얻어낸 에러가 ERROR_IO_PENDING이면 함수가 실패 한 것은 아니다.

 

인자를 설명 하자면 처음부터, 파일디스크립터 (핸들), 쓸 데이터가 들어있는 버퍼, 몇바이트를 쓸지, 실제로 쓴 바이트 수, OVERLAPPED 구조체의 주소 이다.

 

이전전 포스트에서 설명한 OVERLAPPED가 여기서 쓰이는 것이다.

백문이 불여일견이니 1바이트를 시리얼 통신으로 쓰는 함수를 살펴보자

 

 

BOOL CSerialz::WriteCommByte(unsigned char ucByte)
{
 BOOL bWriteStat;
 DWORD dwBytesWritten;

 

//1바이트를 쓰고 다 쓰면 m_OverlappedWirte.hEvent를 set시킨다. 

bWriteStat = WriteFile(m_hIDComDev,

                              (LPSTR) &ucByte,

                              1,

                              &dwBytesWritten,

                              &m_OverlappedWrite);  

 

//overlapped write는 FALSE를 바로 반환하고 LASTERROR가 ERROR_IO_PENDING

if(!bWriteStat && (GetLastError() == ERROR_IO_PENDING))
{
      if(WaitForSingleObject(m_OverlappedWrite.hEvent, 1000))
          dwBytesWritten = 0;
      else
     {
          GetOverlappedResult(m_hIDComDev,

                                        &m_OverlappedWrite,

                                        &dwBytesWritten,

                                        FALSE);

       

           //사실 이 부분은 잘못된 예이다. 시리얼 통신을 비롯한 모든

           //통신 디바이스 에서는 overlapped.offset이 0으로 설정되어야 한다. 빼도좋다.
           m_OverlappedWrite.Offset += dwBytesWritten;   

      }
 }

 

//의미있는 반환값을 가지려면 쓰기성공한 바이트 수가 0 이상인지 체크해야 한다.

 return(TRUE);

 

 

위 코드에서 쓰인 GetOverlappedResult함수는 write 명령어를 수행할 때 넣었던 OVERLAPPED 구조체를 인수로 하여 해당 write명령어의 결과를 얻을 수 있게 해주는 함수이다.

 

 BOOL WINAPI GetOverlappedResult(
  _In_   HANDLE hFile,
  _In_   LPOVERLAPPED lpOverlapped,
  _Out_  LPDWORD lpNumberOfBytesTransferred,
  _In_   BOOL bWait
);

 

순서별로 인수는 COM 포트 파일핸들, OVERLAPPED 구조체 포인터, 더블워드(4바이트) 값으로 실제로 쓰기 된 바이트 수를 받을 변수에 대한 포인터, 그리고 write가 종료되기까지 기다릴지 말지에 대한 결정을 위한 BOOL 변수이다.

 

마지막 변수 bWait는 아래와 같은 규칙으로 쓰인다.

 

 

1. bWait == TRUE  && OVERLAPPED.internal == STATUS_PENDING

 

   bWait가 TRUE인 경우에 미리 COMMTIMEOUTS에 설정한 타임아웃 값

   만큼 기다린다. 기다리는 도중 완료되면 TRUE 반환 TIMEOUT되면 FALSE 반환

   GetLastError() 함수는 ERROR_IO_INCOMPLETE 반환

 

2. bWait == FALSE : && OVERLAPPED.internal == STATUS_PENDING

 

   bWait가 FALSE인 경우에 완료되길 기다리지 않고 바로 쓰기한 바이트를 넣고 반환된다.

   아직 쓰기작업이 끝나지 않았다면 FALSE를 반환하고 GetLastError() 함수는

   ERROR_IO_INCOMPLETE 반환

 

저 쓰기 구조를 요약해 보자면 아래와 같다.  

 

1. Overlapped로 비동기 write명령을 호출한다.

2. write명령어의 반환이 FALSE이고 GetLastError()의 반환이 ERROR_IO_PENDING이다.

3. write명령어에 쓰인 OVERLAPPED의 이벤트가 SET되길 1000ms 동안 기다린다.

   (참고로 wrtie명령어를 호출 할 때 OVERLAPPED의 이벤트 핸들은 자동으로 RESET됨)

4. write작업이 완료되고 이벤트가 SET되어 다음 단계로 넘어간다.

5. GetOverlappedResult를 이용하여 실제로 쓰기된 바이트 수를 얻는다.

6. 불필요한 통신 IO 이용 파일 핸들에서 OVERLAPPED의 offset 재조정

 

마지막으로 6번이 중요한데 위 코드에서 빨간색으로 색을 칠한 구문은 지난번 포스트에서도 언급한바와 같이 파일핸들이 열고 있는 IO Device가 통신 디바이스 일때는 사실상 쓸 필요도 없거니와 써서도 안된다.

 

어쨋든 요런식으로 1byte를 시리얼 통신으로 쓸 수 있으며, 이 1바이트 쓰기를 반복하여 d아래와 같이 원하는 데이터를 전송 할 수가 있다.

 

// 버퍼로부터 설정된 데이터를 사이즈만큼 쓴다.

int CSerialz::SendData(char *buffer, int size) 

{

    // port가 제대로 설정되어 있지 않을 때 - 쓰기 실패
    if(!m_bPortOnOff || m_hIDComDev == NULL) return(0); 

 

    DWORD dwBytesWritten = 0;

    for(int i=0; i<size; i++)
    {  

        // 여러 바이트를 쓰기 위하여 한 바이트씩 쓰는 함수를 중복 호출
        WriteCommByte(buffer[i]); // 쓰기 위한 함수 호출

 

        //요부분에 writeCommByte의 TRUE FALSE를 분간하는 코드가 있음좋다.
        dwBytesWritten++;
    }

    return((int)dwBytesWritten);  // 쓰기에 성공한 바이트 수 리턴

코드는 지금 공부하고 있는 코드를 복사해서 올리다 보니깐 좀 틀린 부분이 있긴 하지만

일단은 틀린 부분과 추가해야 할 사항들을 추가 하기위해 틀린 코드도 그대로 올리고 있으니 참고하여 코드를 봐야 한다.

 

 

Posted by 굿쟌
,