FirmGod

Modbus TCP Frame Builder & Parser

Build and decode Modbus TCP frames. The MBAP header field by field, the length nobody gets right first time, and splitting a socket read into frames.

Hex, any separator. Parsing happens in this page.

Examples
Length agreesProtocol id 0
FieldBytesValue
Transaction id00 011 — the server echoes this back
Protocol id00 000 — Modbus
Length00 066 — unit id plus 5-byte PDU
Unit idFF255 — the TCP device itself, as the specification suggests
Function code033 — Read Holding Registers
Start address00 000 (0x0000)
Quantity00 011

The same request on a serial line

The MBAP header is replaced by the unit id as a slave address and a CRC. Useful for checking a gateway against the device behind it.

FF 03 00 00 00 01 91 D4

What changes between RTU and TCP

The request itself does not change. The same function codes, the same addresses, the same data layout — that part is the PDU, and it crosses a gateway untouched. What changes is the envelope around it.

RTU frames a request with a one-byte slave address in front and a two-byte CRC behind, and separates frames with silence on the line. TCP already delivers bytes in order and already checks them, so both go away and a seven-byte MBAP header takes their place.

FieldSizeMeaning
Transaction id2 bytesEchoed by the server, so a client can match a reply to its request
Protocol id2 bytesAlways 0x0000 for Modbus
Length2 bytesThe bytes that follow: unit id plus PDU
Unit id1 byteThe slave address, when a gateway needs one
PDU1–253 bytesFunction code and data, identical to serial

The length field

This is the field that gets written wrong first. It counts the unit id and the PDU, and nothing else — not itself, not the protocol id, not the transaction id. Put another way it is the frame length minus six.

A request to read one holding register is twelve bytes and its length field reads 6:

00 01 | 00 00 | 00 06 | FF | 03 00 00 00 01

Asking for ten registers instead of one does not change it, because the quantity is inside the PDU. Theresponse does change, since it carries the register contents. A client that computes the length from the request it sent rather than from the bytes it is about to send will be right until the first write.

TCP is a stream, not a sequence of messages

This is the difference that causes the most field problems, and it is not really about Modbus. A socket delivers bytes. One read() can return half a frame, one frame, or three frames end to end, and nothing in the data marks where one stops except the length field.

The correct loop is: read until you have seven bytes, take the length from bytes four and five, then read until you have that many more. Anything else works on a desk against a device that replies slowly and one frame at a time, and fails as soon as the network coalesces two replies into one segment — which is exactly when traffic is heaviest and the failure hardest to reproduce.

The third tab above splits a capture the same way, so a buffer pulled from a log can be checked against what a reader should have made of it.

The unit id is not decoration

For a device that speaks Modbus TCP natively the field is usually ignored, and the specification suggests 0xFF. Behind a TCP-to-RTU gateway it is the serial slave address and the gateway forwards on it, so getting it wrong reaches the wrong device or no device.

That is also why so much example code sets it to 1 or 0 with no comment: against a native TCP device it genuinely does not matter, right up until the same code is pointed at a gateway.

Porting serial code

  • Drop the CRC. Appending one anyway is the most common porting bug. The server counts those two bytes as part of the PDU, the length no longer agrees, and the request is dropped without a useful error.
  • Move the slave address. It becomes the unit id, in the header rather than in front of the function code.
  • Keep the addresses. The 40001-style documentation offset works exactly as it does on serial: a register documented as 40108 is address 107 on the wire.
  • Match replies by transaction id rather than assuming the next frame answers the last request. Several requests may be outstanding at once, which serial never allowed.
  • Stop relying on inter-frame silence. There is none. The length field is the framing.

Frequently asked questions

What exactly does the length field count?

The unit id and the PDU — everything after the length field itself. It does not include the length field, the protocol id or the transaction id, which is why it is always the frame length minus six. A read of one register gives 6, and that figure does not change with the quantity requested, because the quantity lives inside the PDU.

What should the unit id be?

For a device that is natively Modbus TCP, the specification suggests 255, and most devices ignore the field entirely. Behind a TCP-to-RTU gateway it must be the serial slave address, because that is what the gateway forwards to. Many devices also accept 0 or 1, which is why so much example code uses those without explaining them.

Why does my client sometimes read half a frame?

Because TCP is a byte stream, not a message stream. One read can return part of a frame or several frames at once, and nothing marks the boundaries except the length field. Read seven bytes, take the length, then read exactly that many more. A client that treats each read as one message works against a slow device on a desk and fails the first time the network coalesces two replies.

Is there a CRC in Modbus TCP?

No. TCP already checksums and sequences the data, so the CRC-16 of RTU is dropped. Appending one anyway is a common mistake when porting serial code: the extra two bytes fall inside the PDU as far as the server is concerned, the length no longer matches, and the request is rejected without explanation.

What is the maximum frame size?

The PDU is capped at 253 bytes, which with the 7-byte MBAP header fixes the frame at 260. That cap comes from serial Modbus — 256 bytes minus an address and a CRC — and was kept so the same PDU crosses a gateway unchanged.