개요 및 목표
이전에 PLC와 PC의 시리얼 통신과 이더넷 통신을 할 때 XGT전용 프로토콜을 사용하였다.
생각해 보니 XGT전용 프로토콜을 사용해서 프로그램을 만들게 되면 나중에 LS사의 PLC 말고 다른 PLC를 사용하게 되면 다시 통신 프로그램을 만들어야한다.
그래서 범용성이 높은 Modbus TCP/IP 통신을 하면 나중에 LS의 PLC말고 다른 PLC를 사용할 때 편리할 것 같아서 이번에는 Modbus TCP/IP 프로토콜을 사용하여 PLC와 PC의 통신을 해보려고 한다.
본문에는 파이썬으로 하는 간단한 통신 프로그램만 나와있지만 PLC - Web 방식으로 모니터링 프로그램을 만들 것이다.
Modbus란?
PLC와 통신에 사용할 목적으로 만들어졌으며 산업 자동화 분야에서 널리 사용되는 통신 프로토콜이다.
모디콘(Modicon)이라는 회사에서 개발되었으며 주로 PLC, 센서, HMI 같은 장비들 간의 통신에 사용된다.
간단한 구조로 인해 다양한 장비와 시스템에 비교적 쉽게 구현될 수 있어서 많이 사용된다.
Modbus의 특징
- 무료로 공개된 프로토콜
- 마스터-슬레이브 구조
- 다양한 전송 모드 (RTU,ASCII,TCP/IP)
- 데이터 모델을 사용(Coils, Discrete Inputs, Holding Registers, Input Registers)
Modbus 프로토콜
Modbus TCP/IP 통신 아키텍처
클라이언트가 서버에 데이터를 요청하는 구조이다.
Modbus 프레임
Modbus 프레임은 크게 ADU와 PDU 두 가지로 나뉜다.
ADU (Application Data Unit)
Additonal address | 통신에서 해당 메시지가 송신되어야 할 대상 장치 (ex 슬레이브)의 주소를 나타낸다. Modbus TCP에서는 유닛ID로 불린다. |
Function code | 마스터 장치가 슬레이브에게 수행하길 원하는 작업을 지정한다. (ex 코일 읽기, 레지스터 쓰기 등) |
Data | 명령에 필요한 추가 데이터 (ex. 레지스터의 값을 읽거나 쓸 때 필요한 정보)를 담는다. |
Error check | 프레임의 무결성을 확인하기 위한 오류 검사 코드를 포함한다. Modbus RTU에서는 CRC를 사용하지만 Modbus TCP에서는 이 필드가 필요 없으므로 헤더의 다른 부분으로 대체된다. |
PDU (Protocol Data Unit)
Fucntion code + Data : PDU는 Modbus 프레임의 핵심 부분으로 ADU 내에서 Fuction code와 Data 필드를 포함한다. PDU는 프로토콜의 독립적인 부분이다.
Modbus TCP/IP 프레임
Modbus TCP/IP는 이더넷 네트워크를 통해 통신하고 크게 두 부분으로 구성된다.
MBAP Header | PDU | ||||
Transaction ID | Protocol ID | Length | Unit ID | Fuction code | Data |
MBAP Header (Modbus Application Protocol Header)
Modbus TCP/IP ADU의 시작 부분이고 Transaction ID, Protocol ID, Length, Unit ID를 포함한다.
Transaction ID | 2 Bytes | 통신 세션 내에서 메시지의 식별을 위해 사용된다. 마스터가 메시지를 보낼 때 설정한 값이며 슬레이브는 응답 메시지에서 같은 값을 사용한다. 즉 마스터가 요청과 응답을 매칭할 수 있도록 하고 보낼 때 마다 1씩 증가한다. |
Protocol ID | 2 Bytes | 프로토콜의 ID를 나타내며 Modbus TCP/IP 통신에서는 항상 0x0000으로 고정되어 있다. |
Length | 2 Bytes | Unit ID 부터 Data 까지 메시지의 길이를 byte 단위로 나타낸다. |
Unit ID | 1 Bytes | Modbus 서버 내의 특정 슬레이브 장치를 식별하는데 사용된다. |
Function code
function code 구분 | 기능 | 모드버스 표기 |
01 | 출력 비트 읽기 | Read Coils |
02 | 입력 비트 읽기 | Read Discrete inputs |
03 | 출력 워드 읽기 | Read Holding Registers |
04 | 입력 워드 읽기 | Write Input Register |
05 | 출력 비트 쓰기 | Write Single Coil |
15 | 출력 비트 연속 쓰기 | Wirte Multiple Coils |
16 | 출력 워드 연속 쓰기 | Write Multiple Registers |
Data
요청이나 응답 메시지에서 실제로 전송되는 정보이다.
function code에 따라서 내용이 결정된다.
통신 프로그램
PLC 및 세팅
이전에 XGT 전용 프로토콜을 사용하여 통신할때는 이더넷 모듈 - 드라이버 설정 - 서버 모드가 XGT 서버를 사용하였는데 이번에는 Modbus를 사용하므로 서버 모드를 모드버스 서버로 바꾸었다.
그리고 아래의 모드버스 설정을 눌러 워드 읽기 영역 시작 주소를 D00000로 바꾸었다.
PLC 프로그램 (XG5000)
D00000 ~ D00006에 임의의 값들을 넣어두었다.
Python 코드
import socket
import struct
HOST = '192.168.0.100' # PLC의 IP 주소
PORT = 502 # Modbus TCP 표준 포트
# Modbus TCP 요청 메시지 구성
transaction_id = 0 # 트랜잭션 ID (임의로 설정 가능)
protocol_id = 0 # 프로토콜 ID (Modbus TCP의 경우 0)
length = 6 # 길이 (유닛 ID, 함수 코드, 시작 주소, 레지스터 수 의 바이트 합)
unit_id = 255 # 유닛 ID (Modbus 장치 ID, 1 바이트)
function_code = 4 # 함수 코드 (4는 입력 레지스터 읽기, 1 바이트)
start_address = 0 # 시작 주소 (0부터 시작 , 2 바이트)
register_count = 7 # 읽을 레지스터 수 (7개의 레지스터 읽기 , 2 바이트)
# '>HHHBBHH' 형식 문자열을 사용하여 Modbus 요청 메시지 패킹
request = struct.pack('>HHHBBHH', transaction_id, protocol_id, length, unit_id, function_code, start_address, register_count)
# TCP 소켓 생성 및 연결
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
client_socket.connect((HOST, PORT)) # PLC에 연결
client_socket.sendall(request) # 요청 메시지 전송
# 응답 메시지 받기
response = client_socket.recv(1024) # PLC로부터의 응답 받기
# Modbus TCP 응답 메시지 분석
# '>HHHBBB' 형식 문자열을 사용하여 응답 메시지의 헤더 부분 언패킹
response_header = struct.unpack('>HHHBBB', response[:9])
# 나머지 부분을 언패킹하여 레지스터 값들 추출
register_values = struct.unpack('>' + 'H' * register_count, response[9:])
# 결과 출력
print("receive : ", response) # 받은 전체 응답 메시지
print("header : ", response_header) # 응답 메시지의 헤더 부분
print("Register values :", register_values) # 추출된 레지스터 값들
코드 분석
Modbus TCP 요청 메시지
구분 | 길이 | 설명 |
transaction_id = 0 | 2 Bytes | |
protocol_id = 0 | 2 Bytes | |
length = 6 | 2 Bytes | unit_id, fucntion_code, 레지스터 수의 바이트 합이다. 1+1+2+2가 되어 6이 된다. |
unit_id = 255 | 1 Byte | Modbus 장치의 ID로 1부터 255까지 설정이 가능하다. |
function_code = 4 | 1 Byte | D영역에 있는 내부 레지스터 값을 읽어와야 하므로 Read Input Registers |
start_address = 0 | 2 Bytes | D00000번 부터 읽어와야 하므로 0으로 설정 |
register_count = 7 | 2 Bytes | 레지스터 7개를 읽어야 하므로 7로 설정 |
struct
Python에서 struct는 바이트 문자열과 Python의 native타입 사이의 변환을 위해 사용된다.
네트워크 프로토콜, 파일 포맷 등의 바이트 단위로 데이터를 처리해야 하는 곳에서 많이 쓰이고 데이터의 포맷을 지정하는 형식 문자열을 사용하여 바이너리 데이터를 구조화하고 패킹하거나 언패킹한다.
struct 공식문서
https://docs.python.org/3/library/struct.html
XGT 전용 프로토콜을 사용하여 이더넷 통신을 할때 느꼈지만 바이트를 처리하기가 상당히 힘들었다.
받은 메시지를 hex 처리하여 하나하나 분석하거나 가공하였는데 struct를 사용하니 매우 편리하였다.
형식
- struct.pack(format, v1, v2, ...)
- struct.unpack(format, buffer)
여러가지 형식이 있지만 그 중에서 이번에 사용된 형식이다.
바이트 순서
데이터 형식
요청 코드
request = struct.pack('>HHHBBHH', transaction_id, protocol_id, length, unit_id, function_code, start_address, register_count)
- > : 바이트 오더를 빅 엔디안으로 지정한다.
- HHH : unsigned short (2 bytes) 형식으로 transaction_id, protocol_id, length를 나타낸다.
- BB : unsigned char (1 byte) 형식으로 unit_id, function_code를 나타낸다.
- HH : unsigned short (2 bytes) 형식으로 start_address, register_count를 나타낸다.
왜 바이트 오더를 >(빅 엔디안)으로 지정하는가?
PLC - Python 이더넷 통신 2에서 직접 경험하여 적어두었는데 한번 더 상기시켜 보면
TCP/IP 네트워크 프로토콜은 데이터를 전송할 때 빅 엔디안 방식을 따르기 때문에 MSB 부터 저장한다.
예를 들어 16진수 값 0x3456은 빅 엔디안 방식으로 바이트 배열 [0x34, 0x56]으로 전송된다.
하지만 대부분의 컴퓨터는 리틀 엔디안 프로세서를 사용하므로 LSB 부터 저장한다.
그래서 16진수 값 0x3456은 리틀 엔디안 메모리에서는 [0x56, 0x34]로 저장된다.
이 차이 때문에 데이터를 전송하기 전에 바이트 순서를 네트워크 바이트 순서로 변환하는 과정이 필요하다.
응답 코드
struct.unpack('>' + 'H' * register_count, response[9:])
레지스터 값들을 언패킹한다.
- 'H' * register_count : register_count의 값에 따라 H 문자열을 반복한다.
- > : 빅 엔디안 바이트 순서 지정
- 'H' * register_count : register_count 개수만큼의 2 bytes unsigned short 값을 나타낸다.
- response[9:] : 처음 9 비트 이후 부분을 사용한다. MBAP header, Function code, byte count 이후 실제 데이터 부분
결과 및 분석
receive : b'\x00\x00\x00\x00\x00\x11\xff\x04\x0e\x00\x9f\x00\xd2\x00\xdd\x01\xb0\x02\x01\x00\xc8\x01,'
처음 receive 를 통해 받은 데이터는 받은 전체 응답 메시지의 바이너리 표현이다.
구조 | 항목 | 내용 |
MABP Header | Transaction Identifier | 00 00 |
Protocol Identifier | 00 00 | |
Length (17 bytes) | 00 11 | |
Unit Identifier (초기에 255로 설정하였다.) | FF | |
PDU | Function Code ( 초기에 Read Input Registers(04)로 설정하였다.) | 04 |
Byte Count (14 bytes) | 0E | |
7개의 레지스터 값 |
00 9F 00 D2 00 DD 01 B0 02 01 00 C8 01 2C |
header : (0, 0, 17, 255, 4, 14)
위에서 받은 전체 응답 메시지에서 9개까지만 자른 것을 언패킹 하였다.
- 0 : 00 00
- 0 : 00 00
- 17 : 00 11을 10진수로 변환한 값
- 255 : FF를 10진수로 변환한 값이며 요청에서 보낸 값과 동일하다.
- 4 : 04 요청에서 보낸 값과 동일하다.
- 14 : 메시지의 길이를 나타낸다.
register : (159, 210, 221, 432, 513, 200, 300)
위에서 받은 전체 응답 메시지에서 9개 이후로 자른 것을 언패킹 하였다.
- 159 : 00 9F의 변환 값
- 210 : 00 D2의 변환 값
- 221 : 00 DD의 변환 값
- 432 : 01 B0의 변환 값
- 513 : 02 01의 변환 값
- 200 : 00 C8의 변환 값
- 300 : 01 2C의 변환 값
PLC에 D00000 부터 D000006 까지 들어있는 값들이 맞게 날아 왔다.
느낀점
이전에 XGT 전용 프로토콜을 사용하여 통신을 할 때는 struct를 몰라서 한참 걸렸는데 이번에는 struct를 사용하여 패킹과 언패킹을 하니 훨씬 수월하게 프로그램을 만든 것 같다.
XGT 전용 프로토콜은 참고 자료들이 많지 않았는데 Modbus는 확실히 많이 사용해서 그런지 자료들이 많아서 만들기 쉬웠다.
이번 프로젝트의 최종 목표는 파이썬으로 만드는 GUI 프로그램이 아닌 웹에서 PLC를 제어하고 모니터링 하는 프로그램을 만드는 것이다.
아래의 링크로 가면 웹사이트에서 PLC를 제어하고 모니터링하는 프로그램을 볼 수 있다.
https://fortex66.tistory.com/14
PLC - Web Server (Modbus TCP/IP 프로토콜을 사용한 웹 통신 프로그램)
개요 및 구성 https://fortex66.tistory.com/13 이전 글에서 파이썬에서 Modbus TCP/IP 프로토콜을 사용한 PLC와 PC의 통신 프로그램을 만들었다. 파이썬을 사용한 GUI 프로그램으로 만드는 것보다 누구나 쉽게
fortex66.tistory.com
참고문헌
https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf