Modbus TCP 프레임 구조
헤더 7바이트의 length로 메시지의 끝을 판별하고, transaction identifier로 요청과 응답을 대응시킨다.
헤더의 필요성
TCP는 연속된 바이트 스트림이므로 메시지의 경계를 구분하지 않는다. Modbus TCP는 메시지마다 앞에 MBAP 헤더 7바이트를 추가하여 길이와 요청·응답의 대응 관계를 표시한다. 헤더 뒤에는 기능 코드와 데이터가 오며, 직렬 통신에서 사용하던 CRC는 포함되지 않는다.
MBAP 헤더
| 필드 | 크기 | 의미 |
|---|---|---|
| transaction identifier | 2바이트 | 요청과 응답을 대응시키는 번호. 서버가 그대로 반환 |
| protocol identifier | 2바이트 | Modbus는 0 |
| length | 2바이트 | 이 필드 뒤에 남은 바이트 수 |
| unit identifier | 1바이트 | 게이트웨이 후단 직렬 장비의 주소 |
모든 필드는 상위 바이트를 먼저 전송하는 big-endian이다. 서버는 TCP 502번 포트에서 대기한다. TCP에 직접 연결된 장비에서는 unit identifier가 의미를 갖지 않으며, 구현 가이드는 0xFF를 권장한다.
03 요청과 응답 예제
holding register 세 개를 주소 0x006B부터 읽는 예이다.
00 01 transaction identifier
00 00 protocol identifier
00 06 length (뒤에 6바이트)
FF unit identifier
03 기능 코드
00 6B 시작 주소
00 03 레지스터 개수
00 01 00 00 00 09 FF 헤더 (length는 9)
03 06 기능 코드, 데이터 바이트 수
02 2B 00 00 00 64 값 555, 0, 100
수신 바이트 해석
import struct
rsp = bytes.fromhex("00 01 00 00 00 09 FF 03 06 02 2B 00 00 00 64")
# >는 big-endian, H는 2바이트, B는 1바이트다
tid, pid, length, unit = struct.unpack(">HHHB", rsp[:7])
# length는 unit identifier 1바이트도 세므로, 헤더 뒤로는 length - 1바이트가 온다
assert len(rsp) == 7 + length - 1
function, count = struct.unpack(">BB", rsp[7:9])
values = struct.unpack(f">{count // 2}H", rsp[9:])
print(tid, function, values) # 1 3 (555, 0, 100)