Modbus is old, simple and everywhere: energy meters, VFDs, temperature controllers, solar inverters, PLCs. That simplicity is exactly why it works so well on cheap boards. An Arduino can act as a small Modbus RTU device or read a meter, an ESP32 can put registers on Wi-Fi, and a Raspberry Pi can log, bridge and display everything.
This guide covers what each board is actually good at, how to wire an RS-485 transceiver correctly, working code for an Arduino master and slave, the Raspberry Pi serial setup that trips most people up, and how to test your build before it goes anywhere near a real panel.
Quick answer: which board for which Modbus job?
- Arduino (Uno, Nano, Mega): good for a small Modbus RTU slave that exposes a few sensors, or a simple RTU master that polls one or two meters. Deterministic timing, low power, no operating system to crash.
- Arduino with RS-485 on board (Opta, or MKR + MKR 485 Shield): same jobs, less wiring, and in the case of the Opta, a DIN-rail industrial form factor.
- ESP32: Modbus TCP over Wi-Fi, or a small RTU-to-TCP bridge. Cheap and capable, but a 3.3 V part.
- Raspberry Pi: data logger, gateway, dashboard, database, and test bench. A full Linux computer, so anything you can do in Python or a ready-made app is available.
| Arduino Uno / Nano | Arduino Opta / MKR + MKR 485 Shield | ESP32 | Raspberry Pi | |
|---|---|---|---|---|
| Typical Modbus role | RTU slave or simple master | RTU slave or master | TCP server/client, RTU-TCP bridge | Master, logger, gateway, simulator |
| RTU (RS-485) | Yes, with external transceiver | Built in (Opta) / shield (MKR) | Yes, with 3.3 V transceiver | Yes, USB adapter or HAT |
| TCP | Only with Ethernet shield | Opta: Ethernet built in; MKR: needs add-on | Yes (Wi-Fi, Ethernet on some boards) | Yes (Ethernet and Wi-Fi) |
| Real-time behaviour | Very predictable | Very predictable | Good (RTOS underneath) | Not real-time (Linux) |
| Logging, database, dashboard | Very limited | Limited | Small web pages possible | Excellent |
| Power draw | Very low | Low | Low to moderate | Moderate (needs a stable 5 V supply) |
| Ruggedness | Hobby grade | Opta: industrial DIN-rail; MKR: hobby/pro | Hobby grade | Hobby grade unless in an industrial enclosure |
| Price class | Lowest | Mid to high | Low | Low to mid |
One honest caveat before going further: an Arduino or Raspberry Pi is not a certified PLC or safety controller. For anything that switches real loads, protects people or runs unattended in a cabinet, you need proper isolation on the RS-485 side, a DIN-rail power supply, surge protection, an enclosure, and someone accountable for the design. For monitoring, prototyping, labs and test benches, these boards are excellent.
Realistic project ideas
Arduino as a Modbus RTU slave exposing sensors. Read a temperature sensor, a 4-20 mA transmitter through a proper current-loop input board (not a resistor taped to an analog pin), or count pulses from a water or gas meter, then publish the values as holding or input registers. Any PLC, SCADA or HMI on the bus can now read them like any other Modbus device.
Arduino as an RTU master reading an energy meter. Poll voltage, current and kWh from a DIN-rail meter every few seconds and show it on a small display, or forward it somewhere else.
ESP32 as a Modbus TCP device over Wi-Fi. Put a few sensor values on the network without running cable, or build a small bridge that answers TCP requests by polling an RTU device on its RS-485 port.
Raspberry Pi as logger, gateway and dashboard. Poll several devices, store the data, draw trends, send alerts. This is where the Pi shines, because it has storage, networking and a real operating system.
Raspberry Pi as a Modbus device simulator. Instead of reading real devices, the Pi pretends to be them: a meter with moving values, a controller with set registers. That is extremely useful for testing an Arduino master, a PLC program or a SCADA screen before the real hardware arrives.
Wiring an Arduino to RS-485 with a MAX485 module
Modbus RTU usually runs over RS-485, a differential two-wire bus. Arduinos speak TTL serial, so you need a transceiver. The cheap blue MAX485 modules are the usual choice for 5 V boards.
The module has these pins:
| Module pin | Connects to | Purpose |
|---|---|---|
| RO (Receiver Out) | Arduino RX | Data coming from the bus |
| DI (Driver In) | Arduino TX | Data going to the bus |
| DE (Driver Enable) | Digital pin (e.g. D2) | HIGH = transmit |
| RE (Receiver Enable, active low) | Same digital pin as DE | LOW = receive |
| VCC | 5 V | Power |
| GND | GND | Ground |
| A, B | RS-485 bus A and B | The twisted pair |
RS-485 is half-duplex: a node either talks or listens. Because DE is active high and RE is active low, tying them together to one digital pin gives you a single direction control: HIGH to transmit, LOW to receive. Your code (or the library) must switch it at exactly the right moments.
Arduino Mega / Nano Every MAX485 module RS-485 bus
+----------------------+ +-------------------+
| RX1 (19) <----+---------| RO |
| | | A |-------- A (D+) ----->
| TX1 (18) -----+-------->| DI |
| | | B |-------- B (D-) ----->
| D2 ------+---+---->| DE |
| | +---->| RE |
| 5V ------+-------->| VCC |
| GND ------+-------->| GND |-------- GND/common -->
+----------------------+ +-------------------+
Key points:
- Voltage matters. A classic MAX485 is a 5 V part. For an ESP32, Raspberry Pi GPIO or any 3.3 V board, use a 3.3 V transceiver such as the MAX3485 (or a module explicitly sold as 3.3 V compatible). Feeding 5 V logic into a 3.3 V pin can damage it.
- Connect the ground. RS-485 is differential, but transceivers still need a common reference. Run a ground or signal-common wire with the pair, or use isolated transceivers in industrial installs.
- A and B naming is not standardised between manufacturers. If nothing answers, swapping A and B is a legitimate first test and will not damage anything.
- Termination. On long runs or higher baud rates, put a 120 ohm resistor across A and B at each end of the bus (only the two ends). Many cheap modules already have one fitted, so check before adding more.
Pick a board with a spare hardware serial port. On an Arduino Uno or classic Nano, the only hardware serial port (pins 0 and 1) is shared with the USB connection. That means you cannot use Serial.print() for debugging while Modbus is running, and uploads can fail while the transceiver is connected. Boards with Serial1 (Mega, Leonardo, Micro, Nano Every, MKR family, ESP32) make life much easier. Software serial works for low baud rates, but it is the source of many unreliable builds.
Code example A: Arduino as a Modbus RTU master (ModbusMaster)
The ModbusMaster library by Doc Walker is the long-standing choice for RTU master work on Arduino. You install it from the Library Manager. It lets you register two callbacks, preTransmission and postTransmission, which is where we flip the DE/RE pin.
This example reads two holding registers from a meter at unit ID 1 and combines them into a 32-bit float.
#include <ModbusMaster.h>
const uint8_t DE_RE_PIN = 2; // DE and RE tied together
const uint8_t METER_ID = 1; // Modbus unit ID of the meter
ModbusMaster node;
void preTransmission() { digitalWrite(DE_RE_PIN, HIGH); } // talk
void postTransmission() { digitalWrite(DE_RE_PIN, LOW); } // listen
void setup() {
pinMode(DE_RE_PIN, OUTPUT);
digitalWrite(DE_RE_PIN, LOW); // start in receive mode
Serial.begin(115200); // USB debug output
Serial1.begin(9600, SERIAL_8N1);// Modbus line: must match the meter
node.begin(METER_ID, Serial1);
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
}
void loop() {
// Read 2 registers starting at protocol address 0
// (often documented as "40001" in the meter manual).
uint8_t result = node.readHoldingRegisters(0, 2);
if (result == node.ku8MBSuccess) {
uint16_t hi = node.getResponseBuffer(0);
uint16_t lo = node.getResponseBuffer(1);
// Word order "ABCD" (high word first). If values look absurd,
// swap hi and lo: many meters use "CDAB".
uint32_t raw = ((uint32_t)hi << 16) | lo;
float value;
memcpy(&value, &raw, sizeof(value));
Serial.print("Value: ");
Serial.println(value, 3);
} else {
Serial.print("Modbus error 0x");
Serial.println(result, HEX);
}
delay(1000);
}
About word order. A Modbus register is 16 bits. A 32-bit float or integer therefore occupies two registers, and the standard does not say which one comes first. Some devices send the high word first (often called ABCD or big-endian), others send the low word first (CDAB, "word swapped"). Less often, bytes inside each word are swapped too (BADC, DCBA). The meter manual should say; if it does not, try both orders and see which one gives a sensible voltage like 230.1 instead of a number like 1.4e-38.
If you are on an Uno and must use Serial for Modbus, delete the debug prints and show results on an LCD instead.
Code example B: Arduino as a Modbus RTU slave (ArduinoModbus)
For the slave (server) side, Arduino's official ArduinoModbus library sits on top of ArduinoRS485. Install both from the Library Manager.
ArduinoRS485 is designed around boards and shields with known pins, such as the MKR 485 Shield and the Arduino Opta. On those, the global RS485 object already knows which serial port and DE/RE pins to use, and the library examples work as-is. The default pins for other boards are set per architecture inside RS485.h, so they may not match your wiring.
For a generic board with a MAX485 module, the safest approach is to create your own RS485Class object, whose constructor takes the serial port and pins: RS485Class(HardwareSerial& hwSerial, int txPin, int dePin, int rePin). ModbusRTUServer.begin() has an overload that accepts that object. Check that your board's core and ArduinoRS485 version support your architecture before relying on it.
#include <ArduinoRS485.h>
#include <ArduinoModbus.h>
// Generic board with Serial1 and a MAX485 module.
// TX pin, DE pin, RE pin. DE and RE are tied together on pin 2,
// so we pass 2 for both. Check the TX pin number for your board.
const int TX_PIN = 18; // Serial1 TX on an Arduino Mega
const int DE_PIN = 2;
const int RE_PIN = 2;
RS485Class rs485(Serial1, TX_PIN, DE_PIN, RE_PIN);
const int SLAVE_ID = 10;
void setup() {
// On a MKR 485 Shield or Opta you can use the simpler form:
// ModbusRTUServer.begin(SLAVE_ID, 9600);
if (!ModbusRTUServer.begin(rs485, SLAVE_ID, 9600, SERIAL_8N1)) {
while (1); // failed to start: check wiring and board support
}
// Holding registers at protocol addresses 0..3
ModbusRTUServer.configureHoldingRegisters(0, 4);
}
void loop() {
// Answer any pending request from the master
ModbusRTUServer.poll();
// Example data: temperature in tenths of a degree,
// raw analog value, and an uptime counter in seconds.
int tempTenths = readTemperatureTenths();
ModbusRTUServer.holdingRegisterWrite(0, (uint16_t)tempTenths);
ModbusRTUServer.holdingRegisterWrite(1, analogRead(A0));
ModbusRTUServer.holdingRegisterWrite(2, (uint16_t)(millis() / 1000));
// Register 3 can be written by the master, e.g. a setpoint
long setpoint = ModbusRTUServer.holdingRegisterRead(3);
(void)setpoint; // use it to drive an output, etc.
}
int readTemperatureTenths() {
// Replace with your real sensor code (DS18B20, PT100 board, ...)
return 215; // 21.5 degrees
}
A few practical notes:
- Sending temperatures as integers in tenths (215 = 21.5) is common in Modbus devices and avoids float word-order confusion entirely.
- If you prefer read-only values, use
configureInputRegisters()andinputRegisterWrite()instead, so masters read them with function code 4. - Keep
loop()fast. Longdelay()calls mean the slave answers late and the master times out.
ESP32: Modbus TCP over Wi-Fi
The ESP32 is a natural fit for Modbus TCP. Two well-known libraries:
- eModbus: developed primarily for the ESP32. Supports RTU, ASCII and TCP, as client, server and bridge, with a non-blocking, task-based design. It includes server classes such as
ModbusServerWiFiand a bridge mode that is well suited to a small RTU-to-TCP gateway. - modbus-esp8266 by Alexander Emelianov: despite the name, it supports the ESP32 as well, and covers TCP and RTU, client and server.
Both have good examples in their repositories; start from those rather than from scratch, since the APIs differ.
Remember the hardware rule: the ESP32 is a 3.3 V chip. For RS-485, use a 3.3 V transceiver such as a MAX3485 module, or a module with automatic direction control. Also keep Wi-Fi in perspective: it is fine for monitoring, but for anything that matters, wired Ethernet is more predictable.
Raspberry Pi: serial setup and Python
Option 1: USB to RS-485 adapter (easiest)
A USB-RS485 adapter is the simplest route. It appears as /dev/ttyUSB0 (or similar), handles direction control automatically, and avoids every GPIO UART question below. Pick one with a known chipset (FTDI, CH340, CP210x) and, for field use, galvanic isolation.
Option 2: RS-485 HAT on the GPIO UART
HATs are neater but need some setup:
- Run
sudo raspi-config, go to Interface Options, Serial Port: disable the login shell over serial, enable the serial port hardware. Reboot. - On a Pi 3 or Pi 4, the full PL011 UART is used by Bluetooth by default, and GPIO 14/15 get the less robust mini-UART, whose baud rate depends on the CPU clock. For Modbus, add
dtoverlay=disable-btto/boot/firmware/config.txt(or/boot/config.txton older releases) to give the PL011 back to the GPIO pins. Then use/dev/serial0. - On a Pi 5, the UART on GPIO 14/15 is separate from Bluetooth and appears as
/dev/ttyAMA0once enabled (for example withdtparam=uart0=on). Note that on the Pi 5,/dev/serial0points to the small debug connector, not to GPIO 14/15, so use/dev/ttyAMA0explicitly. - Direction control: prefer HATs with automatic direction switching. Toggling DE/RE from a GPIO pin under Linux is possible but timing is not guaranteed, which causes intermittent errors at higher baud rates.
Reading a meter with Python (minimalmodbus)
MinimalModbus is a small, easy library for RTU masters (pip install minimalmodbus). pymodbus is the larger alternative if you also need TCP or a server.
import minimalmodbus
import serial
meter = minimalmodbus.Instrument('/dev/ttyUSB0', 1) # port, unit ID
meter.serial.baudrate = 9600
meter.serial.bytesize = 8
meter.serial.parity = serial.PARITY_NONE
meter.serial.stopbits = 1
meter.serial.timeout = 0.5 # seconds
# One 16-bit holding register at protocol address 0, scaled by 1 decimal
temperature = meter.read_register(0, number_of_decimals=1, functioncode=3)
# A 32-bit float in registers 0-1, high word first (ABCD)
voltage = meter.read_float(0, functioncode=3, number_of_registers=2,
byteorder=minimalmodbus.BYTEORDER_BIG)
# If the value looks wrong, try word-swapped (CDAB):
# byteorder=minimalmodbus.BYTEORDER_LITTLE_SWAP
print(temperature, voltage)
Or skip the code entirely
If what you actually need is a logger rather than a programming project, you can install ModLens on the Pi and run it as a 24/7 Modbus logger with a web dashboard: configure the devices and registers, and it polls, decodes and records them, viewable from any browser on the network.
And if you are building an Arduino master but the real meter has not arrived yet, ModLens Sim turns a Pi (or a Windows laptop) into one or more simulated Modbus devices over TCP or RTU, with moving values or data replayed from CSV, Excel, JSON or a web API. Your Arduino code gets realistic answers to test against on day one.
Testing your build on the bench
Most Modbus problems are not code problems: they are a wrong unit ID, wrong baud rate, swapped wires or a register offset error. Test in small steps.
Testing an Arduino slave. Connect a USB-RS485 adapter to your PC or Pi and use ModLens's Explorer or Scanner to query the Arduino. The scanner confirms the board answers at the unit ID and serial settings you expect, and the explorer lets you read registers 0 to 3 and see whether the values (and, for 32-bit values, the byte and word order) decode correctly. If the scan finds nothing, it is wiring or settings, not your register map.
Testing an Arduino master. Run ModLens Sim as a fake meter with the same register layout as the real one. You can confirm the request goes out, the float decodes correctly, and your error handling works when you stop the simulator.
Two USB keys, one computer. A cheap and very effective bench: two USB-RS485 adapters on the same PC, A to A, B to B, ground to ground. Run the simulator on one port and the master tool on the other. Once that works, you have a known-good reference, and you can swap one side for your Arduino.
Common mistakes and how to fix them
| Symptom | Likely cause | Fix |
|---|---|---|
| No response at all | A and B swapped | Swap A and B (harmless to try) |
| No response, or only garbage | DE/RE never toggled, or stuck HIGH | Check the direction pin and the pre/post transmission callbacks |
| Timeouts or CRC errors | Baud rate, parity or stop bits differ | Match the device exactly (e.g. 9600 8N1 vs 19200 8E1) |
| Intermittent errors, worse with distance | No common ground | Add a ground/common wire or use isolated transceivers |
| Board resets or a pin dies | 5 V transceiver on a 3.3 V board | Use a MAX3485 or 3.3 V module for ESP32, Pi GPIO, MKR |
| Uploads fail, debug output corrupts the bus | Uno's Serial used for both USB debug and Modbus |
Use a board with Serial1, or drop debug prints |
| Errors on long cable runs | Missing termination, or too many terminators | 120 ohm at both ends only; check onboard resistors |
| Exception 02 (illegal data address) | Reading "40001" as address 40001 | Register 40001 is protocol address 0; subtract the offset |
| Value is huge or tiny nonsense | Wrong word or byte order | Try CDAB instead of ABCD (or vice versa) |
| Master gets no reply from Arduino slave | Slow loop() with long delays |
Call poll() often; avoid blocking code |
FAQ
Can an Arduino Uno do Modbus RTU?
Yes, as a master or slave, with a MAX485 module. The limitation is that its single hardware serial port is shared with USB, so debugging is awkward. A Mega, Nano Every or Leonardo with a free Serial1 is a better starting point.
Arduino vs Raspberry Pi for Modbus: which should I choose? Choose an Arduino when you need a small, low-power device that exposes sensors or polls one meter with predictable timing. Choose a Raspberry Pi when you need logging, a database, a dashboard, a gateway or several devices at once. Many good projects use both: Arduino slaves in the field, a Pi collecting the data.
Can the Raspberry Pi be a Modbus slave instead of a master? Yes. pymodbus includes server functionality, so a Python script can expose registers over RTU or TCP. A dedicated simulator, like the one described above, does the same without writing code.
Do I need termination resistors? On a short bench cable at 9600 baud, usually not. On longer runs or higher speeds, fit 120 ohms across A and B at the two physical ends of the bus, and nowhere else.
Is an Arduino Opta a PLC? Arduino markets the Opta as a micro PLC with industrial form factor and IEC 61131-3 programming support. Whether it is suitable for a specific installation still depends on your local requirements, certifications and risk assessment, so check the datasheet against your project rather than assuming.