본문 바로가기

C#

[C#] 시리얼 통신 데이터 주기(RS232)

1. 서론

rs232 시리얼 통신을 프로그래머나 전자 쪽 혹은 기계 쪽을 다루는 일이 있다.

보통은 누가 하는지 모르겠으나 일이 들어와서 간단하게 만들어보았다.

필자는 리시브 데이터는 필요 없고 일방적으로 보내기만 하기 때문에 리시브하는 기능은 구현하지 않았다.

 

또한, 필자는 C# 콘솔 프로그램으로 만들 것이 아니라 유니티로 옮겨와 게임과 연동해야 하기 때문에

메인에서 키 입력 부분은 더 좋은 방식으로 만들기 바란다.

대충 함수만 구현했다는 소리

2. 본론

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Ports;

namespace Csharp_TEST
{
    //프로토콜 종류
    public enum SerialProtocol
    {
        STX = 0,
        BaseMoterSpeed,
        BaseMoterDirection,
        HulahoopMoterSpeed,
        HulahoopMoterDirection,
        LED,
        FanMoterSpeed,
        ExternalPower,
        KegelMoterSpeed,
        MovementMode,
        CheckSum,
        ETX,
        End
    }

    //프로토콜 패킷
    struct protocolPacket
    {
        public int DataLength;
        public byte[] protoData;
    }

    //데이터 담을 클래스
    static class Data
    {
        public static string BaseProtocolData { get; private set; } = "A/000/0/000/0/000/000/000/000/0/0000/Z";
        public static Dictionary<SerialProtocol, protocolPacket> m_SerialProtocol;
    }
    
    //시리얼포트 초기화데이터
    static class Port
    {
        public static string _strPortName = "COM7";
        public static int _iBaudRate = 9600;
        public static int _iDataBits = 8;
        public static Parity _eParity = Parity.None;
        public static StopBits _eStopBits = StopBits.Two;
        public static Handshake _eHandShake = Handshake.None;
    }
    
    //메인
    class Program
    {
        static void Main(string[] args)
        {
            
            ProtocolData _ProtocolData = new ProtocolData();
            _ProtocolData.Awake();

            SerialPortRun _SerialPortRun = new SerialPortRun();

            ConsoleKeyInfo key = new ConsoleKeyInfo();
            while (key.Key != ConsoleKey.A)
            {
                key = Console.ReadKey(true);

                if (key.Key == ConsoleKey.S)
                {
                    _SerialPortRun.Run();
                }
                else if(key.Key == ConsoleKey.D)
                {
                    string s = "000";

                    _SerialPortRun.Change(s, SerialProtocol.BaseMoterSpeed);
                    _SerialPortRun.Run();
                }
                    
            }
        }
    }
    
    //프로토콜 분별
    class ProtocolData
    {
        public void Awake()
        {
            Data.m_SerialProtocol = new Dictionary<SerialProtocol, protocolPacket>();

            for (int i = 0; i < (int)SerialProtocol.End; ++i)
            {
                protocolPacket prodata = new protocolPacket();
                prodata.DataLength = Data.BaseProtocolData.Split('/')[i].Length;
                prodata.protoData = Encoding.UTF8.GetBytes(Data.BaseProtocolData.Split('/')[i]);
                Data.m_SerialProtocol.Add((SerialProtocol)Enum.ToObject(typeof(SerialProtocol), i) ,prodata );
            }
        }
    }
    
    //시리얼포트 초기화
    class SerialPortRun
    {
        SerialPort m_SerialPort;
        //생성자
        public SerialPortRun()
        {
            m_SerialPort = new SerialPort();
            m_SerialPort.Close();

            m_SerialPort.PortName  = Port._strPortName;
            m_SerialPort.BaudRate  = Port._iBaudRate;
            m_SerialPort.Parity    = Port._eParity;
            m_SerialPort.DataBits  = Port._iDataBits;
            m_SerialPort.StopBits  = Port._eStopBits;
            m_SerialPort.Handshake = Port._eHandShake;
            m_SerialPort.DataBits = 8;

            // Set the read/write timeouts
            m_SerialPort.WriteTimeout = 5000;

            m_SerialPort.Open();
        }
        
        //데이터보내기
        public void Run()
        {
            if (m_SerialPort.IsOpen)
            {
                byte[] SendData = new byte[27];
                int length = 0;
                
                for (int i = 0; i < Data.m_SerialProtocol.Count; ++i)
                {
                    Array.Copy(Data.m_SerialProtocol[(SerialProtocol)Enum.ToObject(typeof(SerialProtocol), i)].protoData
                        , 0, SendData, length, Data.m_SerialProtocol[(SerialProtocol)Enum.ToObject(typeof(SerialProtocol), i)].DataLength);
                    length += Data.m_SerialProtocol[(SerialProtocol)Enum.ToObject(typeof(SerialProtocol), i)].DataLength;
                }
                m_SerialPort.Write(SendData, 0, SendData.Length);
            }
        }

		//데이터 변경
        public void Change(string _data, SerialProtocol _serialProtocol)
        {
            //데이터 무결성 검사

            //키 여부 검사
            if (!Data.m_SerialProtocol.ContainsKey(_serialProtocol))
                return;

            //데이터 잘 들어왔는지 검사
            if (_data.Length != Data.m_SerialProtocol[_serialProtocol].DataLength)
                return;

            //데이터 변경
            Array.Copy(Encoding.UTF8.GetBytes(_data), 0, Data.m_SerialProtocol[_serialProtocol].protoData, 0 ,_data.Length);
        }
    }
}

메인부터 보자. 남에 코드 보는 데엔 시작 지점을 파악하는 게 제일 빠르고 차근차근 쫓아가면 쉽다.

 

메인에서 ProtocolData 클래스를 생성하고 Awake() 함수를 호출한다. 기능은 Dictionary <SerialPort, ProtocolPacket> 변수에 담을 것들을 static 클래스인 Data클래스에서 값을 가져온다.

 

필자가 받은 프로토콜 형식이 저따위라 저따위로 담았다. 매우 화나는 상황.

프로토콜 형식이 어떤 데이터는 3바이트 어떤 데이터는 1바이트 어떤 데이터는 4바이트....

필자는 시리얼 통신을 처음 해본다. 대부분 시리얼 프로토콜이 저런 식인 건지...

 

아무튼, 시리얼 프로토콜을 순회하면서 ProtocolPacket 구조체에 데이터를 담고 딕셔너리 변수에 넣어준다.

그럼 기본 베이스가 완성되었다.

 

다음 SerialPortRun 클래스에서는 생성자 Run()와 Change() 함수 둘 뿐이다.

생성자에서는 using System IO.Ports; 의  SerialPort 클래스를 생성하고 static클래스인 Port 클래스의 데이터를 그대로 옮겨준다. 필자는 static클래스에 직접 담아두었으나, 유저나 기계 쪽 만지는 사람들이 쉽게 변경하기 위해 파일 입출력이나 config파일로 바꿔줄 예정이다.

 

딕셔너리에 값을 넣어 두었으니 그대로 Run()을 호출하면 바로 딕셔너리에 담긴 데이터를 순회하여 시리얼 포트에 write 될 것이다.

 

Change()에서는 키값 SerialProtocol과 string _Data를 파라미터 값으로 받게 하였는데, byte [] 형식이니 byte []로 받으면 편하지 않느냐 뭐하러 Encoding.UTF8.GetBytes 까지 써가며 했냐? 하고 의문이 들 수 있다.

 

순전히 필자의 실력이 드러나는 부분이다..... 하..;;;

 

byte [] 형식으로 정수 값을 받으니 DEC데이터로 들어가지 않고, 그냥 정수 형태로 박아버린다.

이 부분을 어떻게 수정해야 할지 몰라서 그냥 스트링 형식으로 받고 인코딩해버렸다.

아시는 분은 댓글로 좀 부탁드립니다.

 

변경한 값을 Array.Copy를 통해 해당 딕셔너리 키값의 벨류로 바꿔주면 다음 Run()만 호출하면 데이터가 넘어간다.

 

저기 코드엔 없으나 프로그램이 종료하거나 더 이상 시리얼 통신이 필요 없을 땐 Close()를 호출해주면 끝이다.

3. 결론

작업할 수 있는 시간이 하루도 채 되지 않아 코드가 많이 난잡하다.

수정하고 보안해서 올려야 했지만... 이번 프로젝트에 그렇게 큰 비중을 차지하지 않아 마무리 지었다.

 

궁금한 사항이나 문제점 등 말씀해주시면 감사하겠습니다.

 

 

** 2019.09.17 추가사항

enum을 object타입으로 바꾸면 힙 할당 때문에 GC가 남습니다..

다음 글에 유니티에 맞물리는 글에서는 GC가 일어나지 않도록 수정해서 올리겠습니다.

그냥 참고정도로만 해주시면 될 거 같습니다.

'C#' 카테고리의 다른 글

[C#]Singleton패턴 상속으로 사용하기 feat.Interface  (0) 2020.02.08
[C#] 벡터의 내적  (0) 2019.10.11