-
Serial통신 데이터 수신 루틴 구현Software/C# 2024. 7. 12. 11:32728x90
C# 시리얼 포트 System.IO.Ports
.NET 기본 제공 API인 System.IO.Ports는 문제가 많은 것으로 유명하다
문제가 많기 때문에 코드 구현을 효율적으로 하지 않으면 통신간 문제가 발생할 확률이 높다.
가장 큰 문제점은 통신량이 많아져서 수신버퍼에 처리되지 않은 데이터가 많이 쌓이면 Receive Error 메세지와 함께 내부에서 데이터가 깨질 확률이 상승한다는 점이다.
C#의 고질적인 문제인 UI접근 속도 때문에 모든 수신 데이터를 UI처리 시킨다면 수신버퍼에 데이터가 금새 쌓이게 되고 깨진 데이터를 받아보게 될 것이다.
따라서 UI접근을 최소화 시키고, 모든 데이터를 다 UI처리까지 할 필요가 없는 경우가 대부분이므로 수신버퍼를 빠르게 비우기 위해 데이터를 과감하게 버리는 구조로 통신을 진행시켜야 데이터의 무결성이 어느정도 확보된다.
수신 접근법 String vs byte
Serialport sdk는 두 가지의 방법으로 버퍼의 데이터를 읽어올 수 있다.
하나는 String 형식으로 수신버퍼를 아스키 형식으로 불러오는 방법이 있고, 다른 하나는 byte형식으로 수신버퍼를 바이너리 형태로 읽어오는 방법이 있다.
스트링 형식으로 받아올 때의 이점은 체계에서 주로 사용하는 SOF나 EOF로 프레임을 분리하기 용이하다는 점이다. Split 메소드는 대부분 String이 가지고 있기 때문이다. byte 어레이에서도 이 작업을 충분히 구현할 수 있지만 비용이 배로 들기 때문에 추천하지 않는다. 스트링 수신은 속도가 비교적 빠를 수 있지만 데이터가 일부 깨져서 읽어질 수 있는 여지가 있다. 이건 내부 SDK의 문제로 .Readline() 메소드나 .ReadinBuffer()등 String 읽기 메소드의 고질적인 문제로 알려져 있다.
데이터의 무결성이 중요한 작업 (점검장비 등)에서는 byte 읽기를 선호하는 이유가 이것이다. byte단위로 읽어오는 작업은 데이터의 깨짐 정도가 훨씬 덜하고 안전하다. byte 수신의 단점으로는 메세지를 수신 시에 필연적으로 프레임 내부에 데이터 길이를 표현하는 정보가 들어있어야한다는 점이다. 이것도 물론 길이 정보 없이 수신을 할 수는 있지만 비용이 매우 커지게 되므로 되도록 프레임에 길이정보를 넣어서 수신하는 것이 좋다.
점검장비를 주로 구현하는 나에게 있어서는 데이터 무결성이 속도보다 우위에 있다. 따라서 byte읽기 형식으로 수신부를 구현하는게 좋다.
Event 수신용 클래스
public class SerialDataReceivedEventArgs : EventArgs { public byte[] Data { get; private set; } public string name { get; set; } public SerialDataReceivedEventArgs(byte[] data,string _name) { Data = data; name = _name; } }
먼저 Event를 수신 했을 경우데이터를 읽어올 구조체 형식의 클래스를 생성한다. 이벤트 작업을 다뤄야하므로 EventArgs를 상속받는다.
// 메시지 수신 이벤트 public event EventHandler<SerialDataReceivedEventArgs> MessageReceived; private void OnMessageReceived(SerialDataReceivedEventArgs e) => MessageReceived?.Invoke(this, e);
메세지 이벤트를 람다형식으로 등록하고 수신 시 Invoke로 실행하여 Thread 권한을 UI 생성자에게 넘겨주는 형식으로 구현한다. Invoke 시키지 않으면 크로스 쓰레드 에러가 발생하므로 주의하여야 한다.
Port Open
public bool OpenSerialPort(string _portname, string port, int baudRate,int _threshold, Parity parity, StopBits stopBits, Handshake handshake) { m_PortName = _portname; serialPort.PortName = port; serialPort.BaudRate = baudRate; serialPort.Parity = Parity.None; serialPort.ReceivedBytesThreshold = _threshold; serialPort.StopBits = StopBits.One; serialPort.Handshake = handshake; try { serialPort.Open(); MessageBox.Show("Open SerialPort"); StartProcessDataThread(); StartReadSerialDataThread(); return true; } catch(Exception ex) { string message = $"{ex.Message}"; MessageBox.Show(ex.Message); return false; } }
Port 오픈 메소드로 포트를 여는 데, 여기서 주의할 점은 메세지를 Port.ReceviedEvent에 추가해서 이벤트 형식으로 직접 받는 게 아니라 쓰레드가 무한 루프를 돌며 버퍼에서 메세지를 읽어서 그대로 위에 생성한 Messgebox 구조체에 입력한다는 점이다. 메세지 박스에 올려진 메세지는 메세지를 처리할 클래스에서 무한루프를 돌며 메세지가 있으면 수신하는 방식으로 구현한다.
여기서 도는 쓰레드인 ReadSerialDataThread는 무한루프를 돌며 메세지 큐에 데이터를 쌓아주기만 하는 역할을 하고, ProcessdataThread는 해당 작업요건에 따라 바이트 메세지를 검사하고, 길이를 맞추고, 체크섬체크까지하여 통과한 메세지들을 Messagebox에 올려주는 작업까지를 담당한다.
메세지 처리 클래스
m_JSerialPort.MessageReceived += SerialManager_MessageReceived; private void SerialManager_MessageReceived(object sender, M823.Helpers.SerialDataReceivedEventArgs e) { if(e.Data == null) { return; } ProcessMsg(e.Data, e.name); }
이제 실제로 쓰는 곳에서 포트를 오픈 시킨다음 메세지 도착 이벤트에 메세지 처리루틴을 연결한다.
메세지 처리 루틴에서는 루프를 돌며 앞선 두 쓰레드들이 안전하게 Messagebox 클래스에 데이터를 넣어 줄 때까지 대기하다가 메세지가 들어있으면 해당 메세지를 처리하고 종료한다.
728x90'Software > C#' 카테고리의 다른 글
C# Form Interop.Excel Nuget 패키지 추가 및 환경설정 (0) 2024.09.09 C# Form Textbox 변수 값 입력 이벤트 (0) 2024.05.23 C# Form UI 접근과 Invoke (0) 2024.05.23 Form 공용 기능을 위한 BaseForm (0) 2024.05.23 C# Form 자식 폼 붙이기 (0) 2024.05.23