제임스딘딘의
Tech & Life

개발자의 기록 노트/C#

[C#] C#.net에서의 시리얼통신 기초

제임스-딘딘 2017. 4. 18. 23:15

C#.net에서의 시리얼통신 기초


C#은 시리얼 통신에 대한 모든것을 개발자가 구현할 필요 없이 매우 쉽고 간단하게 사용할 수 있는 객체를 지원한다. 
그것은 System.IO.Port namespace에 포함되어있는 System.IO.Ports.SerialPort 인데, Visual Basic 6.0 에서 지원하던 Comm 컨트롤과 매우 유사해 사용은 간단했다.

참고로 이 글은 .net framework 3.5 기준으로 작성됐다.






객체 생성

SerialPort 객체를 Form에 끌어넣어주면 된다.
SerialPort 객체는 Device Components 에 있다. 
아래 그림을 참고하자.


또한, 아래와 같이 namespace 추가가 되었는지 코드를 확인해보고, 안되어있다면 추가하도록 한다.

using System.IO.Ports;


초기화

사용하고자 하는 포트의 번호와, BaudRate(나는 보통 '보 레이트' 라고 읽는다)를 설정하는 것이 Serial통신의 시작이다.

SerialPort.PortName 속성은 COM1, COM2 등 몇번 COM port를 사용할지 설정하면 된다.
SerialPort.BaudRate 속성은 통신 속도를 설정한다.
일반적인 경우 이 두가지만 설정하면 된다.

아래 코드를 참고하자.

SerialPort SP = new SerialPort();

SP.PortName = "COM1";
SP.BaudRate = (int)38400;
SP.DataBits = (int)8;
SP.Parity = Parity.None;
SP.StopBits = StopBits.One;
SP.ReadTimeout = (int)500;
SP.WriteTimeout = (int)500;

Baud rate, Stop bits, Data bits등 시리얼 통신을 위한 여러 설정들은 위와 같은 방법으로 정의할 수 있다.
위 예에서 적용된 설정들은 이 설정들을 적용하지 않았을 경우 default값으로 적용되는 값들이다.

한가지 팁을 드리자면, 실제 사용 가능한 시리얼 포트가 몇번인지 확인하는 방법은 아래와 같다.
SerialPort.GetPortNames() 라는 method가 있는데, 이를 사용하면 사용가능한 모든 port의 이름목록을 얻어 올 수 있다.
이후 foreach문을 사용하면 모든 사용 가능한(물리적으로) 시리얼 포트를 찾을 수 있다.

foreach (string comport in SerialPort.GetPortNames())
{
    //각각의 'comport'는 사용가능한 포트이름이다.
    //이것을 리스트뷰나 콤보박스에 추가해서 디스플레이 해주는 등의 처리 코드를 넣는다.
}

Parity로 사용할 수 있는 값
  • EVEN
  • MARK
  • NONE
  • ODD
  • SPACE

Stop Bits로 사용할 수 있는 값
  • None
  • One
  • OnePointFive
  • Two
 

Port 열고 닫기


여기까지 설정이 끝났다면 단순히 Open() 메소드를 사용하여 적용된 설정과 함께 포트를 열 수 있다.
2개의 method만 기억하면 된다. Open( )과 Close( )가 그것이다.
SerialPort.Open(), SerialPort.Close() method를 이용해 포트를 열고 닫아주면 된다.
또한 필요한 Exception에 대한 핸들링도 주로 이곳에서 하게 된다.

코드를 작성하다 보면 종종 Open( )이 잘 됐는지, 혹은 아직 Open안된 상태인지 확인을 해야 할 때가 있다.
이를 판단하기위한 속성값이 있는데, SerialPort.IsOpen 이다.
이 IsOpen 속성을 이용하여 포트가 정상적으로 열렸는지 확인 할 수 있다.

SP.Open();

if (SP.IsOpen)
{   
   // 포트 오픈 성공
}
else
{
   // 포트 오픈 실패.
}


데이터 전송 및 수신

데이터 전송은 간단하다. 
'txtSend' 라는 텍스트 박스가 있다고 가정하자.
이 txtSend 에 입력되어있는 값을 전송하려면 다음과 같은 코드 한줄이면 된다.

SP.Write(txtSend.Text);

혹은 아래와 같이 Line단위로 처리하는 method도 사용가능하다.

SP.WriteLine("It’s a test.");
SP.ReadLine();

Read작업의 경우 ReadByte, ReadChar, ReadExisting, ReadLine 중 필요한 메소드를 골라서 사용하면 된다.
Win32에서 Read작업의 경우 쓰레드를 만들고 이벤트를 감시하여 해당 이벤트(시리얼로 데이터가 들어오는)가 발생하면 특정 루틴으로 넘겨주는 방법을 사용했는데, 닷넷에서도 마찬가지의 방법을 사용한다.
SerialPort가 데이터를 받으면 다음과 같은 event handler 가 호출된다.

private void SP_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)

하지만 SerialPort로 받아온 데이터를 읽어올 때 주의사항이 있다.
DataReceived 이벤트 핸들러에서 main thread의 resource에 곧바로 접근했다가는 thread exception을 접하게 된다.
이것을 해결하는 방법을 간단히 다루는것이 이 C#을 이용한 시리얼 통신에 대한 글의 목표이다.


그리고 SerialPort.ReadExisting() 를 호출하면 SerialPort 객체의 버퍼에 남아있는 데이터들을 모두 읽어오게 된다.
하지만 이 데이터들을 main form 의 ListBox 에 넣으려고 하는 순간 exception이 발생했다.
MSDN 을 검색해 보면 다음과 같은 설명이 나와있다.
굵은 부분이 핵심이다.

The DataReceived event is raised on a secondary thread when data is received from the SerialPort object. 
Because this event is raised on a secondary thread, and not the main thread, attempting to modify some elements in the main thread, such as UI elements, could raise a threading exception. 
If it is necessary to modify elements in the main Form or Control, post change requests back using Invoke, which will do the work on the proper thread.


요약하면 SerialPort객체를 통해 호출되는 DataReceived 이벤트 핸들러는 secondary thread이고, main form 은 main thread이다. 
즉, 서로 다른 thread 이다. 
그런데 Serial Port 의 thread 에서 main form 의 thread 의 객체를 modify 하려고 하기 때문에 발생하는 문제이다.

이 문제를 해결하기 위해서 secondary thread인 SerialPort_DataReceived() event는 main thread의 event handler를 invoke 하도록 했다.
main thread에서 invoke되는 event handler에서는 SerialPort 를 통해 받은 데이터를 이용해 main form을 수정한다.

private void SP_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
    this.Invoke(new EventHandler(SerialReceived));
}


즉, SerialPort 가 데이터를 받아올 때마다 main form 에서 SerialReceived() event handler가 호출되며, 이 event handler에서 main form 의 객체를 조작하게 된다.
이로써 thread exception 이 일어나는 문제가 해결된다.

private void SerialReceived(object s, EventArgs e)
{
    String strReceive = SP.ReadExisting();
    listReceive.Items.Add(strReceive);
}


데이터 패킷화


public static void SetPacket(uint uCommand, uint uData , ref byte[] btBuf, ref uint uLen)
{
BitConverter.GetBytes(STARTCODE).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(SESSIONNO_UNKNOWN).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

const uint DATALENGTH = sizeof(uint) + sizeof(uint);     //CMD + DATA length
//btBuf.SetValue(DATALENGTH, sizeof(uint) + uLen);
BitConverter.GetBytes(DATALENGTH).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(uCommand).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(uData).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(ENDCODE).CopyTo(btBuf, uLen);
uLen += sizeof(uint);
}


부록

아래 그림은 시리얼통신용으로 사용하는 D-SUB 커넥터를 캐드로 그린것이다.
자주 사용하는 TXD, RXD, GND 를 표시해놓은 그림이다. 
종종 시리얼통신을 사용할 때마다 핀 배열이 헷갈릴 때가 있는데, 그때마다 찾아보는 그림이다.