In this lesson

What this lesson covers

  • The seven bytes of the MBAP header and who fills in each field
  • Recalculating the Length field, and why there is no CRC
  • Which unit id fits a native device and which one behind a gateway

Read first: Reading exception codes and choosing a function code, The Modbus RTU frame byte by byte, plus CRC

The MBAP header is the seven bytes Modbus TCP puts in front of a request, in place of the address byte and the CRC that RTU uses. The request itself does not change: same function code, same register address, same quantity. After this lesson you can name every field, recalculate the Length yourself, and decide which unit id belongs in the frame for the device in front of you.

The same request, now over Ethernet

A complete Modbus TCP request, twelve bytes on the wire: 00 01 00 00 00 06 FF 03 00 6B 00 03.

BytesFieldWhat it says
00 01Transaction id1, client picks it, server echoes it
00 00Protocol id0, always, this is Modbus
00 06Length6, so 1 unit id plus 5 PDU bytes follow
FFUnit id255, device sits directly on the network
03Function codeFC03, read holding registers
00 6BStart addressPDU address 107, datasheet number 40108
00 03QuantityThree registers

The same read over RS485 is eight bytes: 01 03 00 6B 00 03 74 17. Line the two up and the middle is identical, 03 00 6B 00 03. That is the PDU, and the Modbus RTU frame byte by byte carries the same one. Only the envelope changed: the address byte and the two CRC bytes are gone, seven header bytes in their place. See Modbus TCP explained for the transport as a whole.

The seven bytes of the MBAP header

MBAP is short for Modbus Application Protocol, and its header is always seven bytes in the same order.

OffsetFieldSizeWho fills it in
0Transaction identifier2 bytesClient chooses, server copies back
2Protocol identifier2 bytesAlways 0x0000, server copies back
4Length2 bytesEach side calculates its own
6Unit identifier1 byteClient sets, server copies back
MBAP header, 7 bytesPDU, function code and dataTID2 BPID2 BLEN2 BUID1 BFC1 BDATAN BCRC-16
TIDTransaction id, 2 bytes
PIDProtocol id, 2 bytes, always 0
LENLength, 2 bytes
UIDUnit id, 1 byte
FCFunction code
DATAData

No CRC: Ethernet and TCP already handle the integrity check.

Seven header bytes, then exactly the same PDU as on RS485.

Six of those seven bytes come straight back in the response. Length is the only one the server works out for itself, because its value depends on what it is about to send. Worth knowing while sniffing: a protocol id other than zero is discarded without an answer, so you see nothing rather than an error.

Why there is a transaction identifier

Because on TCP more than one request from the same client can be in flight at once. The client puts a number in the first two bytes, the server copies it into the response, and the client uses it to pair answer with question. A PLC or a building automation server that keeps several requests open on one connection leans on that field for every answer it gets back.

On RS485 the problem does not exist. A client sends one request, waits for the answer or the timeout, then sends the next, so there is nothing to pair and the RTU frame has no such field. A plain counter is a fine value, as long as it is unique among the open requests on that connection.

Checking the Length field yourself

Length counts the bytes after it: one unit id byte plus the PDU. In the request above the PDU is 03 00 6B 00 03, so 1 plus 5 is 6 and the field reads 00 06. The complete frame is therefore always Length plus six.

The response, fifteen bytes: 00 01 00 00 00 09 FF 03 06 02 2B 00 00 00 64.

BytesFieldWhat it says
00 01Transaction idCopied from the request
00 00Protocol idCopied from the request
00 09Length1 unit id plus 8 PDU, server calculated
FFUnit idCopied from the request
03Function codeFC03 unchanged, so no error
06Byte count6, three registers times two bytes
02 2BRegister 108555
00 00Register 1090
00 64Register 110100

Same header, different Length. Change one thing: if the server refuses, it answers 00 01 00 00 00 03 FF 83 02. Length is 3, because the PDU shrank to 83 (FC03 with the high bit set) plus exception code 02. A short frame with a function code above 0x80 is an exception.

Complete frame (ADU)

MBAP header (7 bytes)

00010000000601

PDU (function code and data)

030000000A

Length counts 1 unit id byte plus 5 PDU bytes, which is 6 together. That is the value the header carries.

Transaction id00 011A number you choose freely. The response repeats it, so you can pair request and answer.
Protocol id00 000Always 0 for Modbus.
Length00 066The number of bytes that follow, including the unit id.
Unit id011The real server address behind a gateway. For a device directly on the network, 255 or 0.
Function code033 (Read Holding Registers)Which operation you are asking for.
Start address00 000The first register, counted from zero on the wire.
Quantity00 0A10How many registers or coils you read in one request.
Change the function code and the quantity, and watch what happens to the Length field.

Why there is no CRC

Because two layers underneath have already done that work. An Ethernet frame carries a CRC-32 over its contents, and TCP adds a checksum of its own plus retransmission of anything that arrives damaged. A 16 bit Modbus checksum on top would cost bandwidth without catching what those layers miss.

There is a second job the CRC never did anyway: marking where a message ends. On RS485 that is 3.5 character times of silence. TCP is a byte stream without message boundaries, so a Modbus message can be split across segments or share one with the next. Length tells the receiver where to cut.

What to put in the unit id field

Two situations, two answers, and swapping them produces a timeout that looks like a network fault.

SituationUnit id
Device directly on the TCP/IP network0xFF recommended, 0x00 also accepted
Device behind a gateway to a serial lineThe real server address, 1 to 247, where 0 is broadcast

The IP address already identifies a device on the network, so the unit id has nothing left to do there. 0xFF is recommended for a practical reason: if that IP address is later reassigned to a gateway, a meaningful server address would send your request on to a device you never meant to reach.

Manufacturers do not all follow that advice. SMA expects 126 for its SunSpec profile, a Fronius GEN24 inverter uses 1 while its smart meter answers on 200, and SolarEdge over Ethernet uses 1. Check the datasheet.

Why port 502 is fixed

Modbus/TCP was registered with IANA in 1996 and given system port 502. The registry entry for that port carries the service name mbap, with modbus.org as the assignee. The specification is stricter than a recommendation: a device must listen on 502 by default, and if it also offers another port for Modbus, 502 has to stay available alongside it.

In the field that rule gets broken. SolarEdge inverters with SetApp default to port 1502, while the older LCD models default to 502. So when a connection is refused, check the datasheet before you suspect the firewall. Which transport to run at all is compared in Modbus RTU versus Modbus TCP.

Common mistakes

  1. Treating the unit id as an RS485 address on a native device. People copy the 1 from an old RTU configuration into a device that sits straight on Ethernet. Some answer anyway, others stay silent. Use 0xFF unless the datasheet names a value.

  2. Reading Length as the total frame length. Length counts only what follows it, so a 12 byte frame shows 6. Compare those two numbers and you conclude the frame is truncated, then hunt a network problem that is not there.

  3. Expecting a CRC at the end. Point an RTU decoder at a TCP frame and it reads the last two data bytes as a checksum, then reports a CRC error. In the response above that is 00 64, register value 100.

  4. Assuming the port is always 502. A refused connection on 502 while the device listens on 1502 looks exactly like a blocked firewall rule.

Get hands-on

Build the frame from this lesson in the builder above and check that the numbers add up. It all runs in the page, so you need no hardware.

  1. 1

    Set up the request

    Set the function code to FC03, the unit id to 255, the start address to 107 and the quantity to 3.

  2. 2

    Compare it with the reference frame

    Hold your frame against 00 01 00 00 00 06 FF 03 00 6B 00 03. Only the transaction id may differ, because you choose that value yourself.

  3. 3

    Change the quantity to 10

    Watch what happens to Length, and what does not. The request stays five PDU bytes long, so Length stays 6: it describes the request, never the response.

  4. 4

    Switch to FC16

    See how much the PDU grows, because now you send values as well as an address and a quantity.

  5. 5

    Work out the response on paper

    The answer to step 2 is 7 bytes of MBAP, plus 1 function code, plus 1 byte count, plus 6 data bytes: 15 bytes, with Length 9.

  6. 6

    Check it against a capture later

    Read one register from a simulator or a real device and the same twelve bytes appear on the wire. The Wireshark lesson later in this chapter puts that capture next to what you built.

Expected result: a frame identical to the reference except for the transaction id, and a response length you calculated before seeing it.

Summary

  • The MBAP header is seven bytes, transaction id (2), protocol id (2), Length (2) and unit id (1), replacing the address byte and CRC of an RTU frame.
  • Length counts one unit id byte plus the PDU, so the complete frame is always six bytes longer than the Length value.
  • Six of the seven header bytes are copied unchanged into the response, and only Length is recalculated by the server.
  • Modbus TCP needs no checksum of its own: Ethernet carries a CRC-32, TCP retransmits damaged data, and Length marks the message boundary.
  • Use 0xFF on a device directly on the network and the real server address, 1 to 247, behind a gateway.

Check yourself

Four questions about this lesson. Every answer comes with an explanation.

Question 1 of 4
In the frame 00 01 00 00 00 06 FF 03 00 6B 00 03, what does 00 06 mean?
You read an energy meter that sits behind an RS485 to Ethernet gateway. You fill in unit id 255 and get exception 0B back. What does that mean?
Why does Modbus TCP have no CRC while Modbus RTU does?
Two requests from the same client are in flight at the same time. How does the client tell which response belongs to which request?

Stuck on a project?

Describe what you see and where it goes wrong. We will look at it and tell you what we would do.

Further reading in the knowledge base