View all lessons
Part 1
Fundamentals
Part 2
Modbus RTU
- Lesson 5RS485 explained: A, B, common and termination20 min
- Lesson 6Baud rate and parity: why 8E1 is the default16 min
- Lesson 7The Modbus RTU frame byte by byte, plus CRC18 min
- Lesson 8Multiple Modbus devices on one RS485 bus16 min
- Lesson 9Read your first Modbus device with mbpoll22 min
- Lesson 10Modbus RTU troubleshooting: symptom to cause20 min
Part 3
Modbus TCP
Part 4
Advanced
- Lesson 16Writing to a Modbus device without breaking it18 min
- Lesson 17Word order and floats: same bytes, other value22 min
- Lesson 18Calculate your poll interval and bus load18 min
- Lesson 19Modbus security: the protocol will not help16 min
- Lesson 20Modbus integration: PLC, Home Assistant, cloud20 min
- Lesson 21Modbus commissioning checklist and cheat sheet18 min
Port 502, the MBAP header and the unit id
The Modbus TCP MBAP header is seven bytes and replaces address and CRC. Learn what each field does, why port 502 is fixed and when the unit id matters.
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.
| Bytes | Field | What it says |
|---|---|---|
00 01 | Transaction id | 1, client picks it, server echoes it |
00 00 | Protocol id | 0, always, this is Modbus |
00 06 | Length | 6, so 1 unit id plus 5 PDU bytes follow |
FF | Unit id | 255, device sits directly on the network |
03 | Function code | FC03, read holding registers |
00 6B | Start address | PDU address 107, datasheet number 40108 |
00 03 | Quantity | Three 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.
| Offset | Field | Size | Who fills it in |
|---|---|---|---|
| 0 | Transaction identifier | 2 bytes | Client chooses, server copies back |
| 2 | Protocol identifier | 2 bytes | Always 0x0000, server copies back |
| 4 | Length | 2 bytes | Each side calculates its own |
| 6 | Unit identifier | 1 byte | Client sets, server copies back |
No CRC: Ethernet and TCP already handle the integrity check.
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.
| Bytes | Field | What it says |
|---|---|---|
00 01 | Transaction id | Copied from the request |
00 00 | Protocol id | Copied from the request |
00 09 | Length | 1 unit id plus 8 PDU, server calculated |
FF | Unit id | Copied from the request |
03 | Function code | FC03 unchanged, so no error |
06 | Byte count | 6, three registers times two bytes |
02 2B | Register 108 | 555 |
00 00 | Register 109 | 0 |
00 64 | Register 110 | 100 |
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)
PDU (function code and data)
Length counts 1 unit id byte plus 5 PDU bytes, which is 6 together. That is the value the header carries.
| Field | Bytes | Value | Meaning |
|---|---|---|---|
| Transaction id | 00 01 | 1 | A number you choose freely. The response repeats it, so you can pair request and answer. |
| Protocol id | 00 00 | 0 | Always 0 for Modbus. |
| Length | 00 06 | 6 | The number of bytes that follow, including the unit id. |
| Unit id | 01 | 1 | The real server address behind a gateway. For a device directly on the network, 255 or 0. |
| Function code | 03 | 3 (Read Holding Registers) | Which operation you are asking for. |
| Start address | 00 00 | 0 | The first register, counted from zero on the wire. |
| Quantity | 00 0A | 10 | How many registers or coils you read in one request. |
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.
| Situation | Unit id |
|---|---|
| Device directly on the TCP/IP network | 0xFF recommended, 0x00 also accepted |
| Device behind a gateway to a serial line | The 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
-
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
0xFFunless the datasheet names a value. -
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.
-
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. -
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
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
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
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
Switch to FC16
See how much the PDU grows, because now you send values as well as an address and a quantity.
- 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
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
0xFFon 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
Want to see how it works?
The ModbusCloud Gateway reads the devices from this course without you programming a single register.