C#팁

C#으로 시리얼통신을 해보자! 데이터의 수신과 분석을 분리편. 아이프리드 평점: 10.0/10 (1명 참여) 조회: 3889

 
http://www.hoons.net/Board/cshaptip/Content/89080

전편에서 간단한 시리얼 통신 송수신 프로그램을 만들어보았습니다.
하지만 전편에서의 프로그램에서는 수신시 데이터를 일괄로 받아서 그냥 화면에 뿌리기만했기에
데이터가 정상적으로 받았는지 사라진 데이터가 없는지 등에 대한 확인이 되지 않습니다.
실제 개발에서는 이런 부분을 다 신경 써줘야 하지요.

일반적으로 시리얼 통신은 언제 어떻게 수신될지 알수 없습니다. 같은 10바이트의 데이터를 송신하더라도
어떨때는 10바이트가 동시에 들어오기도 하고 어떨때는 5바이트씩 나눠서 들어올때도 있고 아니면 2, 3, 5 이런식으로
나눠질때도 있고 하드웨어 상태에 따라서 달라져서 정확히 정해져 있는게 아닙니다. 그래서 수신 이벤트가 여러번 발생하는 경우가 있지요. 그러므로 수신 데이터를 처리할때 수신받은 데이터의 길이만으로 확인하지 않고 별도의 확인 데이터를 통해 수신 로직을 구성해야 합니다. 보통 STX(시작)과 ETX(끝) 바이트를 추가로 달며 이 사이의 데이터만 정상적인 데이터라고 인식하는 방법이 쓰입니다.

하지만 이를 처리하기 위해서는 수신과 처리에 대한 로직이 분리될 필요가 있는데 이유는 수신에서 데이터의 확인까지 처리하게 되면 시간이 오래 걸리는 데이터 확인 작업중에는 데이터를 수신하지 못하는 문제가 있기 때문이지요. 물론 데이터의 길이가 길지않다면 문제 되지 않습니다만 데이터의 길이가 길어지면 길어질수록 문제가 발생할 확률이 커집니다. 그래서 데이터 수신은 수신만 하는 아주 심플한 로직으로 구성되고 별도의 쓰레드에서 수신한 데이터를 처리하는 것을 권장합니다.

 

위 도표처럼 1. 데이터 수신부에서 데이터를 받아 2. 데이터 버퍼에 차례대로 데이터를 쌓으면 3. 일정 길이 이상이 되었을때 4. 데이터 처리부에서 데이터를 처리하고 5. 사용한 데이터는 삭제합니다. 이런 차례로 데이터를 처리하게 되는 것이죠.


그럼 간단히 구현해볼까요?
지난 글의 프로젝트에서 일부 수정해보았습니다. 기존에는 Send버튼을 누르면 한번만 전송되던데서 빠르게 연속적으로 데이터를 보내는 작업을 위해 타이머를 하나 만들고 자동으로 일정시간(500ms)마다 데이터를 전송하도록 하였습니다.


연결 버튼을 누른 이후에 자동전송 버튼을 누르면 상단의 텍스트박스에 들어간 데이터를 500ms 마다 계속해서 날리게 됩니다.


송신부에서 가장 유심히 보셔야 할 것은 패킷을 만드는 부분입니다.
private Int32 CreatePacket(String msg)
{
    /* 
        * Packet 구조
        * [STX:0x02(1byte)] [Data Length(1byte)] [DATA+0] [DATA+1] .... [DATA+n] [CheckSum(1byte)] [ETX:0x03(1byte)]
        * 
        * Check sum 계산
        * ([Data+0]+[Data+1]+[Data+2] .... +[Data+n]) & 0xFF
        */
    Int32 checkSum = 0;

    // 패킷 데이터 채우기
    SendDataList.Add(0x02);                        // STX
    SendDataList.Add((Byte)msg.Length);            // Data의 길이. 최대 255. 그 이상 할려면 Packet 구조 변경이 필요
    foreach (Byte ch in msg)
    {
        SendDataList.Add((Byte)ch);                // string 값 차례대로 넣기
        checkSum += (Byte)ch;                      // checkSum 계산
    }
    SendDataList.Add((Byte)(checkSum & 0xFF));     // CheckSum 1byte. 전체 중 마지막 8bit값만 취함
    SendDataList.Add(0x03);                        // ETX
            
    return SendDataList.Count;       // 길이 반환
}
CreatePacket 라는 함수에 msg 라는 텍스트 인자를 집어넣으면 전송할 패킷 규격에 맞게 데이터를 생성합니다.

Packet 구조는 개발자가 만들기 나름인데 저 같은 경우는 [STX][데이터길이][데이터]...[데이터][체크섬][ETX] 형태로 만들었습니다. 개발자 나름대로 규격을 만드는 것이기에 원하시는데로 입맛대로 변경하시면 됩니다. 데이터 길이 앞에 Command(명령) 바이트를 추가한다거나 데이터 길이를 두바이트로 한다거나 체크섬이 아닌 별도의 CRC 를 쓰신다거나 하실 수 있습니다.

다른것은 그렇다쳐도 CheckSum 은 무엇인가? 라는 의문이 생기실겁니다.
사실 CheckSum이란 용어 대신 CRC라는 것이 정확한 용어이며 CRC란 '순환 중복 검사'란 의미로 앞서 나온 데이터가 정확한 데이터인지 확인하는 기능입니다. 이 CRC 바이트를 만들어내는 방법에는 수 많은 방법이 있고 규격도 존재합니다. 하지만 좀 계산이 복잡한터라서 저 같은 경우는 그냥 모든 바이트를 더한후에 마지막 바이트만 가지고 확인하는 편입니다. 물론 규격이 필요한 외부 디바이스와 통신 할일이 있으면 구현하구요. 일단 편의를 위해서 데이터 전체를 Sum(합계)를 하고 마지막 1바이트를 확인하여 동일한 값이면 데이터가 무결하다고 판정하였습니다.
만약 수신한 CRC값이 계산한 CRC값과 다르다면 전송받은 데이터가 틀린것이므로 재요청하는 등의 로직이 필요해지게
됩니다.

CRC에 대한 자세한 내용은 https://ko.wikipedia.org/wiki/%EC%88%9C%ED%99%98_%EC%A4%91%EB%B3%B5_%EA%B2%80%EC%82%AC 를 참조하시기 바랍니다.


private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    // Serial Port StreamBuffer에 있는 데이터길이 만큼 데이터 수신 후 분석 버퍼에 채우기
    for (int i = 0; i < Port.BytesToRead; i++)
        RecvDataList.Add((Byte)Port.ReadByte());
    /*
    // Receive 버퍼 뒤에 수신 받은 데이터 채우기
    String msg = Port.ReadExisting();           // Byte 변환에 문제 있음
    if (!String.IsNullOrEmpty(msg))
        // Port.Encoding은 Port 생성시에 설정된 Encoding(UTF-8. line38 참조)
        RecvDataList.AddRange(Port.Encoding.GetBytes(msg));
    */
}
시리얼포트 수신부를 보겠습니다. Port.BytesToRead 는 시리얼포트 StreamBuffer에서 읽을 수 있는 버퍼의 길이 값이기 때문에 이 만큼 for문을 돌려서 한바이트씩 차례로 RecvDataList라는 List<Byte> 컬렉션에 쌓아둡니다. 물론 아래 주석처리 된 부분처럼 한번에 읽어서 변환해서 넣어도 됩니다만 ReadExisting 로 읽을시에 바이트 변환에 문제가 있어서 이런식으로 읽었습니다.

private void MorRecvDataTimer_Tick(object sender, EventArgs e)
{
    // 분석할 데이터가 4개 이하면 return
    if (RecvDataList.Count < 4) return;

    // STX 확인
    if (RecvDataList[0] != 0x02)
    {
        Strings = String.Format("[ERR] STX fail.");
        RecvDataList.Clear();
        return;
    }
    // 길이 확인
    Int32 len = RecvDataList[1];
    if (len > (RecvDataList.Count - 4)) return;
    // CheckSum 확인
    Int32 checkSum = 0;
    StringBuilder msg = new StringBuilder();
    for (int i = 0; i < len; i++)
    {
        msg.Append((Char)RecvDataList[i + 2]);
        checkSum += RecvDataList[i + 2];
    }
    if ((Byte)(checkSum & 0xFF) != RecvDataList[len + 2])
    {
        Strings = String.Format("[ERR] CheckSum fail.");
        RecvDataList.Clear();
        return;
    }
    // ETX 확인
    if(RecvDataList[len + 3] != 0x03)
    {
        Strings = String.Format("[ERR] ETX fail.");
        RecvDataList.Clear();
        return;
    }
    // 데이터 출력
    Strings = String.Format("[RECV] {0}", msg.ToString());
    // 분석한 데이터 삭제
    RecvDataList.RemoveRange(0, len + 4);
}

그리고 별도 타이머에서 100ms 마다 수신된 데이터를 확인 후에 처리하는 Tick 이벤트입니다. RecvDataList에 쌓인 데이터를 앞에서부터 분석해서 첫번째 바이트가 STX이고 그 다음 바이트가 데이터의 길이. 그리고 데이터들이 차례대로 들어와 있으며 그 데이터에 대한 무결성을 확인후에 ETX 를 확인하고 여기까지 모두 맞으면 화면에 출력합니다. 마지막으로 버퍼에서 사용한 데이터 만큼 삭제하구요.

이렇게 수신부와 처리부를 분리함으로서 수신 이벤트가 지연되거나 하는 문제를 방지할 수 있으며, 처리부는 일정한 시간마다 호출되어 쌓아둔 데이터를 처리하기만 하면 됩니다. 이렇게 데이터를 쌓아서 처리 하기 때문에 수신하는 데이터의 길이와 무관해지고 긴 데이터도 일률적으로 처리할 수 있게 되는것이죠.

사실 이렇게 로직을 분리하는 것은 별로 중요한게 아니고 가장 중요한것은 STX와 ETX등으로 이루어진 패킷의 분석이 중요한 것이겠지요? 이런 패킷의 구조는 기업마다 프로토콜이 다르므로 거기에 따라가면 됩니다. PLC의 경우는 MODBUS 같은걸 쓰게 되면 그 프로토콜을 따르시면 되는거구요. 일하면서 쓸려니 중간 중간 끊겨서 제대로 설명이 안된거 같아서 죄송합니다 =_=;;

게다가 샘플 예제가 데이터가 STX 수신 이후에 ETX가 들어오지 않으면 로직상 수신부가 먹통이 되는 문제가 있기도한데 이걸 처리할려면 타임아웃을 체크하는 부분도 추가해야 합니다만 귀차니즘과 일이 바빠서 생략하였습니다.....;; 직접 한번 해보시길 바랍니다.
태그 : 시리얼 통신
작성자 정보
아이프리드
Level 44
 [EXP.38/100]

메일:  비공개
글등록 +12 340 덧글등록 +3 1101
자기소개
전자과 공돌이지만 프로그램 개발이 더 적성에 맞는 직장인. 갈수록 전공 지식이 미흡해지는 것을 느끼지만 그래도 프로그램 개발이 좋음.
글 공유하기 |
  tweet facebook
2017-05-15 오후 3:12:24
나도한마디
사용자
이건내얼굴            [2017-05-16]
Level 9
 [EXP.9/14]
만세! 배워갑니다 ㅠㅠ
사용자
이건내얼굴            [2017-05-16]
Level 9
 [EXP.9/14]
textbox 스크롤 내리는 방법 찾다가 포기 먹었는데 해결해 주셨네요ㅜ 감사합니다!

textbox이름.SelectionStart = textbox이름.Text.Length;
textbox이름.ScrollToCaret();
사용자
효조            [2017-05-18]
Level 1
 [EXP.3/16]
지난번 글부터 공부하는데 많은 도움이 되었습니다.

감사합니다!!
사용자
prokorsh            [2017-08-10]
Level 20
 [EXP.14/70]
감동의 눈물이... 주군 옥체 기체후일향만강 하옵시고
원하시는 프로그램을 개발 하시면서 천세를 누리옵소서

감사합니다 항상 행복하세요 꾸벅 ^^
사용자
jihoonb91            [2017-11-23]
Level 3
 [EXP.9/16]
와 좋은 정보 감사합니다!!
태그로 엮인글
[C#.NET Q&A] TCP통신 배열이 포함된 구조체 전송 질문드립니다.  훈스훈스훈스
[C#.NET Q&A] 시리얼 통신 데이터 수신시 짤려서 들어오는데 질문 드립니다.[1]  데브로스
[C#.NET Q&A] 시리얼포트 연결 질문[3]  데브로스
[C#.NET Q&A] 다중 시리얼 통신 질문[4]  데브로스
[WPF Q&A] C# wpf 다중 시리얼 데이터 처리에 대한 문의입니다.  블링c
[C#.NET Q&A] timer 간격이 일정하지 않은 경우[3]  까리이브
[C#.NET Q&A] 2개 이상의 IP에 접속 데이터 교환[3]  hs
[C#.NET Q&A] C# 통신 비정상 헤더 구분에 대한 질문입니다.[4]+1  궁금해욥
[C#.NET Q&A] c#윈폼에서 폰으로 알림 전송하기 질문드립니다[1]+1  amp123
[C#.NET Q&A] 시리얼 통신 수신 딜레이 문제[3]+3  LSIP
글리스트
 ★현재글->   C#으로 시리얼통신을 해보자! 데이터의 수신과 분석을 분리편.[5] 파일첨부 아이프리드
C#에서 파일 입출력 한다고 ms사에서 나온 예제 코드 하는데 복호화 하는데 문제가 있습니[4]+2  두들기기
비동기 소켓 프로그래밍에 대해 질문이 있습니다... ㅠㅠ[1]  궁금해욥
딕션어리 vs 데이터 테이블(값이 자주 변경)[2]  레미콘
C#으로 시리얼통신을 해보자! 시리얼 통신의 원리부터 구현까지[17] 파일첨부 아이프리드
트리뷰 + - 버튼을 이미지로 바꿀 수 있나요 ?[1]+1  훈스훈스훈스
안녕하세요. PartialView에 대한 질문 드립니다.[3] 파일첨부 노찬이
두 가지 간단한 질문..[2]+1  븅멍뭉친구
OS X의 파일명의 한글 자모음이 분리되어 보이는 경우[4]  sa2랑
달력컨트롤 예시 입니다.[5] 파일첨부 방랑개죽
WinForm 투명 컨트롤 ~ 작은 경험 하나 공유합니다.[3]+1  Booh