본문 바로가기
실무 코드 보관소/C#

[LS PLC 통신 2-1] C# 실전: 이더넷 헤드 프레임 만들기

by 코드아재 2021. 12. 7.
반응형
2026년 1월 20일 리뉴얼 업데이트

LS PLC(XGT 시리즈)와 외부 장치 간 통신을 구현하기 위해서는 단순히 소켓을 열고 데이터를 보내는 것만으로는 정상적인 통신이 이루어지지 않습니다. LS PLC의 데이터 전송은 헤더(Header) + 프레임(Frame) 구조로 이루어져 있으며, 이 두 요소를 정확한 형식으로 조합해야만 PLC가 해당 데이터를 정상적인 명령으로 인식하고 처리할 수 있습니다.

특히 읽기(Read)와 쓰기(Write) 명령은 프레임 부분만 달라질 뿐, 공통적으로 동일한 헤더 구조를 사용합니다.따라서 통신 구현의 첫 단계는 읽기·쓰기 명령 이전에 헤더 구조를 정확히 이해하고 생성하는 것입니다.

이 글에서는 LS PLC XGT 통신에서 반드시 포함되어야 하는 전용 서비스 헤더 구조를 먼저 살펴보고, 이를 기반으로 이후 글에서 읽기 / 쓰기 명령 프레임을 단계적으로 만들어 보도록 하겠습니다.

반응형

1. LS PLC XGT 전용 서비스 헤더 구조

LS PLC와의 통신은 [헤더(Header) + 명령어 프레임(Instruction Frame)]의 결합으로 이루어집니다.
헤더는 통신 대상과 데이터의 길이를 정의하는 핵심 부분입니다.

헤더 상세 명세 (20바이트)

바이트 인덱스 항목 (Item) 크기(Byte) 설정값 상세 설명
0 ~ 7 Company ID 8 LSIS-XGT 고정 문자열 (ASCII 코드값)
8 ~ 9 Reserved 2 0x0000 예약 영역 (고정값)
10 ~ 11 PLC Info 2 0x0000 Client(PC) 접속 시 고정값
12 CPU Info 1 0xA0 XGK / XGB 모델 기준 고정
13 Source of Frame 1 0x33 PC(Client)에서 전송 시 고정
14 ~ 15 Invoke ID 2 0x0001 프레임 식별 번호 (임의 설정 가능)
16 ~ 17 Length 2 가변 헤더 이후 데이터 프레임의 바이트 수
18 FEnet Position 1 0x00 통신 모듈 슬롯 번호 (CPU 직결 시 0)
19 BCC (Checksum) 1 계산값 중요: 헤더 0 ~ 18번 바이트 합계의 하위 1바이트

2. C# 기반 헤더 및 BCC 체크 코드

C#에서 byte 배열을 다루어 헤더를 생성하는 공통 함수 예시입니다.
구조체(Struct)와 마샬링(Marshalling) 기술을 활용하여, 복잡한 바이트 연산 없이도 직관적으로 헤더를 생성하고 BCC 체크섬을 계산하여 전체 프레임을 완성하는 과정을 보여줍니다.

[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct _LS_FENET_HEADER
{
    // --- 헤더 멤버 구성 (20바이트 고정) ---
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public char[] CompanyID;        // [0~7]   Company ID ("LSIS-XGT", ASCII, 고정)
    public ushort Reserved;         // [8~9]   Reserved (0x0000 고정)
    public ushort PLC_Info;         // [10~11] PLC Info (Client PC 접속 시 0x0000 고정)
    public byte CPU_Info;           // [12]    CPU Info (0xA0 : XGK / XGB)
    public byte Source_of_Frame;    // [13]    Source of Frame (0x33 : PC → PLC)
    public ushort InVoke_ID;        // [14~15] Invoke ID (요청/응답 식별자, 임의 설정)
    public ushort Length;           // [16~17] Length (헤더 이후 바디 데이터 길이, Byte)
    public byte FEnet_Position;     // [18]    FEnet Position (CPU 직결 시 0x00)
    public byte Reserved_BCC;       // [19]    BCC (0~18 Byte 합계의 하위 1 Byte)

    // 데이터 준비부터 전송용 배열 생성까지 한 번에 처리
    public byte[] GetSendBuffer(List<byte> body)
    {
        // 1. 헤더 기본 정보 설정
        this.CompanyID = "LSIS-XGT".ToCharArray();
        this.Reserved = 0x00;
        this.PLC_Info = 0x00;
        this.CPU_Info = 0xA0;        // XGK 기준
        this.Source_of_Frame = 0x33; // 클라이언트 -> 서버
        this.InVoke_ID = 0x01;
        this.Length = (ushort)body.Count; // 바디(데이터)의 길이를 헤더에 기록
        this.FEnet_Position = 0x00;
        this.Reserved_BCC = 0x00;    // 계산 전 초기화

        // 2. 구조체를 바이트 배열로 변환 (마샬링)
        byte[] headerBytes = new byte[Marshal.SizeOf(this)];
        unsafe
        {
            fixed (byte* ptr = headerBytes)
            {
                Marshal.StructureToPtr(this, (IntPtr)ptr, false);
            }
        }

        // 3. BCC 체크섬 계산 (헤더의 0번~18번 바이트까지 합산)
        int checkSum = 0;
        for (int i = 0; i < headerBytes.Length - 1; i++)
        {
            checkSum += headerBytes[i];
        }
        headerBytes[headerBytes.Length - 1] = (byte)(checkSum & 0xFF);

        // 4. 최종 패킷 조립 (헤더 20바이트 + 바디 n바이트)
        byte[] fullPacket = new byte[headerBytes.Length + body.Count];
        Array.Copy(headerBytes, 0, fullPacket, 0, headerBytes.Length);
        Array.Copy(body.ToArray(), 0, fullPacket, headerBytes.Length, body.Count);

        return fullPacket;
    }
}

마무리 및 요약

위의 구현 방식은 LS PLC 통신의 표준을 C#의 객체지향적 특징을 살려 정리한 것입니다.

  • 정교한 구조체 설계: LayoutKind.Sequential과 Pack = 1을 사용하여 메모리 상의 데이터 순서가 PLC 규격과 1:1로 일치하도록 보장했습니다.
  • 효율적인 마샬링: unsafe 블록과 fixed 포인터를 사용하여 구조체 데이터를 바이트 배열로 빠르게 변환함으로써 성능 손실을 최소화했습니다.
  • 유연한 데이터 결합: GetSendBuff 함수 하나로 헤더 생성, BCC 계산, 그리고 실제 명령어 프레임 결합까지 처리할 수 있어 코드의 재사용성이 높습니다.

이제 이 공통 헤더 구조체를 사용해서, 개별 변수 읽기 및 쓰기 명령어 프레임을 조립하는 단계로 나아갈 수 있습니다.