To make an Arduino or ESP32 based PLC work as a Modbus TCP master (client), open a TCP connection to the slave on port 502 with an EthernetClient, and send the requests with a ModbusTCPMaster object from the Industrial Shields Modbus library, for example master.readHoldingRegisters(client, unitId, address, quantity). The answer arrives later through master.available(). It works on the M-Duino family and on the ESP32 PLC, which have Ethernet.
This guide covers what Modbus TCP is, the frame with its MBAP header, the differences with Modbus RTU, the network setup, a tested master sketch, the slave side, the function reference and the most common errors.
What is Modbus TCP?
Modbus TCP (also called Modbus TCP/IP) carries Modbus messages over Ethernet inside a TCP connection. Modicon created Modbus in 1979 as a serial protocol for its PLCs. The TCP version arrived in 1999 and is now part of the open specification of the Modbus Organization.

Modbus TCP keeps the request and response model of Modbus. The client (master) is the only device that starts a transaction. The server (slave) answers with the requested data or executes the requested action. A slave is any device that holds data: an I/O module, a meter, a drive, another PLC. A typical client is a PLC, a SCADA or an HMI. Some devices are client and server at the same time, such as a gateway.
In other words, Modbus TCP combines a physical network (Ethernet), a network standard (TCP/IP) and a standard way to represent data (Modbus as the application protocol): it is a Modbus message wrapped in a TCP/IP packet.
Modbus TCP frame: MBAP header and PDU

A Modbus TCP frame (ADU) is a 7-byte MBAP header followed by the same PDU as Modbus RTU: function code and data. There is no CRC, because TCP already checks the data.
| Field | Size | Content |
|---|---|---|
| Transaction identifier | 2 bytes | Set by the client, copied in the answer to pair request and response |
| Protocol identifier | 2 bytes | Always 0 for Modbus |
| Length | 2 bytes | Number of bytes that follow (unit identifier and PDU) |
| Unit identifier | 1 byte | Address of the slave behind a gateway. Direct TCP devices often ignore it |
| Function code | 1 byte | Same codes as Modbus RTU (01 to 06, 15, 16...) |
| Data | Up to 252 bytes | Addresses, quantities and values |
For example, the request to read 4 holding registers from address 0 of unit 1, with transaction 1, is 00 01 00 00 00 06 01 03 00 00 00 04. The Modbus RTU frame and function codes guide details the PDU of each function code, which is identical in Modbus TCP.
Modbus TCP vs Modbus RTU
| Modbus TCP | Modbus RTU | |
|---|---|---|
| Physical layer | Ethernet | RS-485 or RS-232 |
| Addressing | IP address and port (502), plus unit identifier | Slave address 1 to 247 |
| Error check | TCP checksums | CRC-16 in each frame |
| Frame separation | Length field in the MBAP header | Silence of 3.5 characters |
| Masters on the network | Several clients can talk to one server | One master per bus |
| Typical use | PLC to PLC, SCADA, HMI, gateways | Field devices: meters, drives, transmitters |
Many installations use both: the PLC reads its field devices on Modbus RTU and shares the data with the SCADA on Modbus TCP. See Modbus RTU vs Modbus TCP vs OPC UA to choose between them.
Which Industrial Shields PLCs support Modbus TCP?
| PLC family | Ethernet | Modbus TCP master and slave | Tools > Board |
|---|---|---|---|
| M-Duino | Yes (W5500) | Yes | Industrial Shields AVR Boards > M-Duino family |
| ESP32 PLC | Yes | Yes | Industrial Shields ESP32 Boards > ESP32 PLC Family |
| ESP32 PLC 14 | Yes | Yes | Industrial Shields ESP32 Boards > 14 IOS PLC Family |
| Ardbox | No | No. Use Modbus RTU on RS-485 |
For Linux based controllers, see the Modbus TCP and RTU guide for Raspberry PLC, with Python and Node-RED.
Ethernet network setup for Modbus TCP
- Power the PLCs with 12 to 24 Vdc.
- Connect them with Ethernet cables to a switch. If you connect two PLCs directly and the link does not come up, use a crossover cable.
- Give each device a fixed IP address in the same subnet, for example 192.168.1.10 for the master and 192.168.1.20 for the slave, and a different MAC address.
The Ethernet port of the M-Duino is always connected to the controller, so the position of the communication switches does not matter for Modbus TCP.
Modbus TCP master example for Arduino and ESP32 PLCs
This sketch connects to a slave at 192.168.1.20, reads 4 holding registers every second and prints them on the serial monitor. It compiles without warnings for an M-Duino 21+ with the Industrial Shields AVR boards 1.2.1 and the Modbus library 1.1.0. On an ESP32 PLC only the board selection changes.
// Modbus TCP master (client): read 4 holding registers from a slave every second
// M-Duino family with Ethernet and ESP32 PLC (Industrial Shields Modbus library 1.1.0)
#include <Ethernet.h>
#include <ModbusTCPMaster.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // unique on your network
IPAddress ip(192, 168, 1, 10); // this PLC
IPAddress slaveIp(192, 168, 1, 20); // the Modbus TCP slave (server)
const uint16_t SLAVE_PORT = 502; // standard Modbus TCP port
const uint8_t UNIT_ID = 1; // unit identifier (0 to 255): 1 for most devices, the RTU address behind a gateway
const uint16_t FIRST_REG = 0; // register 40001 = address 0
const uint16_t NUM_REGS = 4;
EthernetClient slave; // the TCP connection
ModbusTCPMaster master; // no begin() needed
uint32_t lastRequest = 0;
uint32_t lastConnect = 0;
void setup() {
Serial.begin(9600UL);
Ethernet.begin(mac, ip);
Serial.print("PLC IP: ");
Serial.println(Ethernet.localIP());
master.setTimeout(500); // response timeout in ms (default 1000)
}
void loop() {
// 1. The sketch manages the TCP connection, not the library
if (!slave.connected()) {
if (millis() - lastConnect >= 2000UL) { // retry every 2 s, connect() can block
slave.stop();
Serial.println(slave.connect(slaveIp, SLAVE_PORT) ? "Connected" : "Connection failed");
lastConnect = millis();
}
return;
}
// 2. One request at a time, every second
if (!master.isWaitingResponse() && millis() - lastRequest >= 1000UL) {
if (!master.readHoldingRegisters(slave, UNIT_ID, FIRST_REG, NUM_REGS)) {
Serial.println("Request not sent");
}
lastRequest = millis();
}
// 3. Poll for the answer
if (master.isWaitingResponse()) {
ModbusResponse response = master.available();
if (master.hasException()) { // timeout, wrong transaction id...
Serial.print("Communication error: ");
Serial.println(master.getExceptionMessage());
master.clearException();
}
if (response) {
if (response.hasError()) { // exception code from the slave
Serial.print("Slave exception: ");
Serial.println(response.getErrorMessage());
} else {
for (uint16_t i = 0; i < NUM_REGS; i++) {
Serial.print(response.getRegister(i));
if (i < NUM_REGS - 1) Serial.print(", ");
}
Serial.println();
}
}
}
}
- The sketch owns the connection. The library sends and receives on the
EthernetClientyou give it, but it does not open or reopen the connection. Keep the connection open between requests: opening one per request is slow and some devices limit the number of connections. - The unit identifier goes where the slave address goes in Modbus RTU. A device on the same network usually accepts any value, often 1 or 255. Behind a gateway, it selects the RTU slave.
- One request at a time. The functions return
falsewhile an answer is pending, so checkisWaitingResponse()before you send the next request.
To upload, select the board and the model in Tools > Board and Tools > Model, the port in Tools > Port, and use Sketch > Upload. The library also has one ready-made Modbus TCP master example for each function code in File > Examples > Modbus.
The slave side: another PLC as Modbus TCP server
To test the master with a second PLC, open File > Examples > Modbus > ModbusTCPSlave, set its IP address in the same subnet and upload it. The example serves the inputs and outputs of the first zone of the PLC on port 502: coils for the digital outputs, discrete inputs for the digital inputs, holding registers for the analog outputs and input registers for the analog inputs. Since library version 1.1.0 it detects the PLC model, so it is not limited to the M-Duino 21+.
With the example slave, the master can switch output Q0_0 with master.writeSingleCoil(slave, 1, 0, true), read the analog inputs with master.readInputRegisters(slave, 1, 0, 6) and the digital inputs with master.readDiscreteInputs(slave, 1, 0, 7). The slave of the library answers any unit identifier. How to build your own register map is in how to use the Modbus TCP slave library with an Arduino PLC.
Function reference of the Modbus TCP master
The functions are the same as in Modbus RTU, with the EthernetClient as first parameter and the unit identifier as second. They return true when the request is sent and false if the master is busy or the client is not connected.
| Function | FC | What it does |
|---|---|---|
readCoils(client, unit, addr, quantity) | 01 | Reads coils (read and write bits) |
readDiscreteInputs(client, unit, addr, quantity) | 02 | Reads discrete inputs (read-only bits) |
readHoldingRegisters(client, unit, addr, quantity) | 03 | Reads holding registers (16-bit, read and write) |
readInputRegisters(client, unit, addr, quantity) | 04 | Reads input registers (16-bit, read-only) |
writeSingleCoil(client, unit, addr, value) | 05 | Writes one coil (bool) |
writeSingleRegister(client, unit, addr, value) | 06 | Writes one holding register (uint16_t) |
writeMultipleCoils(client, unit, addr, values, quantity) | 15 | Writes a bool array to consecutive coils |
writeMultipleRegisters(client, unit, addr, values, quantity) | 16 | Writes a uint16_t array to consecutive holding registers |
The answer comes from master.available() as a ModbusResponse with hasError(), getErrorCode(), getErrorMessage(), getFC(), isCoilSet(i), isDiscreteInputSet(i) and getRegister(i). The address is the register number minus 1: holding register 40009 is address 8.
Common Modbus TCP errors
| Symptom | Likely cause | What to check |
|---|---|---|
| "Connection failed" | No TCP connection to the slave | IP addresses in the same subnet, cable and link LED, slave powered, port 502 open |
| "Request not sent" | The master is busy or the client is not connected | Send only when isWaitingResponse() is false and connected() is true |
| Timeout exception | The slave accepted the connection but did not answer | Unit identifier, function code supported by the device, timeout too short |
| Illegal data address (02) | The register does not exist in the slave | Register number minus 1, quantity, register map of the device |
| Values shifted by one register | Numbering from 1 instead of 0 | Holding register 40001 is address 0 |
Modbus TCP in real PLC projects
In the Modbus temperature network case study, a main PLC works as Modbus TCP client and reads the local PLCs, which collect the thermometers on Modbus RTU. The PLC Solution Library has code for related jobs: a Modbus TCP register map for a bioreactor plant and a Modbus TCP to RTU gateway on an M-Duino.
Read a Modbus TCP server from your browser. In the Modbus network simulator, an M-Duino 21 serves the data of four RTU slaves as a Modbus TCP server on port 502. Build a request from the laptop client, read outside the map and see the exception, frame by frame.
Open the Modbus network simulatorAsk an engineer about your Modbus network
FAQ about Modbus TCP on Arduino and ESP32 PLCs
Which port does Modbus TCP use?
Port 502 by default. The Industrial Shields slave lets you change it in the constructor, for example ModbusTCPSlave slave(510);, and the master connects to the port you give to connect().
What is the unit identifier in Modbus TCP?
The byte that replaces the slave address of Modbus RTU. Gateways use it to forward the request to an RTU slave. Devices connected directly to Ethernet usually ignore it or expect 1 or 255.
Can a PLC be Modbus TCP server and Modbus RTU master at the same time?
Yes. That is a gateway: the PLC polls its RS-485 slaves as RTU master and serves the values to the SCADA as TCP server. The M-Duino has Ethernet and RS-485, so it can do both.
How many clients can read one Modbus TCP server?
Modbus TCP allows several clients, but each server limits its open connections. Small devices often accept only a few. Keep one connection per client open instead of opening a new one for each request.
Can I use DHCP instead of a fixed IP?
For the master, yes: Ethernet.begin(mac) asks for an address. The slave should have a fixed IP address, or a reserved address in the DHCP server, so that the clients always find it.
Is Modbus TCP the same as Modbus RTU over TCP?
No. Modbus RTU over TCP sends the RTU frame, with its CRC and without the MBAP header, inside a TCP connection. Some gateways and meters use it. The Industrial Shields library implements standard Modbus TCP.