Modbus RTU over RS-485 is the serial workhorse of industrial field wiring — the variant you'll meet when connecting an ESP32 directly to an energy meter, PLC, VFD, or temperature transmitter over a wired bus. This tutorial walks through wiring the hardware, installing a library, and flashing working RTU master code.
What You'll Build
- A Modbus RTU master on ESP32 that polls holding registers from an RS-485 slave device over a wired bus.
- An understanding of register types, addressing, and the reliability practices that separate a demo from a production deployment.
Prerequisites
- Arduino IDE (or PlatformIO) with the ESP32 board package installed.
- An ESP32 dev board, or an industrial ESP32 controller with a built-in RS-485 transceiver such as the NORVI X — this saves you from wiring a separate MAX485 module.
- A Modbus RTU slave device (energy meter, sensor, or PLC).
- Basic familiarity with the Arduino C++ syntax and serial monitor debugging.
A 60-Second Modbus Primer
Modbus is a master–slave protocol dating back to 1979. One master polls up to 247 slave devices, each with a unique address (1–247). Data lives in four register types, and knowing which one you need is half the battle:
| Register Type | Access | Width | Typical Use |
|---|---|---|---|
| Coils (0x) | Read/Write | 1-bit | Relay outputs, digital controls |
| Discrete Inputs (1x) | Read only | 1-bit | Digital sensor inputs, switch states |
| Input Registers (3x) | Read only | 16-bit | Analog sensor values, process data |
| Holding Registers (4x) | Read/Write | 16-bit | Setpoints, configuration parameters |
Modbus RTU over RS-485
Step 1 — Wire the Hardware
The ESP32's UART pins output 3.3V TTL logic, but RS-485 uses a differential voltage signal — so you need a TTL-to-RS-485 transceiver (typically a MAX485 or MAX3485 chip) between the ESP32 and the bus.
- UART TX → transceiver DI (driver input)
- UART RX ← transceiver RO (receiver output)
- A spare GPIO → transceiver DE and RE tied together (direction control)
- Transceiver A/B terminals → the RS-485 A+/B− pair on your slave device
Skip the transceiver wiring
Industrial controllers like the NORVI X have the TTL-to-RS-485 converter built into the board, so RS-485 slaves connect straight to a screw terminal — no breadboard, no separate module, and DE/RE switching is handled for you.
Two more wiring details that matter more than they look:
- Place a 120Ω termination resistor at each physical end of the RS-485 bus if the run is longer than a few meters.
- Give every slave device a unique address in the 1–247 range — duplicate addresses cause bus conflicts.
Step 2 — Install a Modbus Library
For RTU master mode on Arduino, ModbusMaster is the simplest starting point. Install it from the Library Manager:
Arduino IDE → Sketch → Include Library → Manage Libraries...
Search: "ModbusMaster" by Doc Walker → Install
Other options worth knowing about, depending on your needs:
- modbus-esp32 — supports both RTU and TCP/IP, master and slave modes.
- ArduinoModbus — the official Arduino library, straightforward setup.
- esp32ModbusRTU — interrupt-driven and non-blocking, useful if your loop() is already busy.
Step 3 — Write the RTU Master Sketch
This sketch polls two holding registers from slave address 1 every half-second and prints the values to the serial monitor:
#include <ModbusMaster.h>
#define SLAVE_ID 1
#define RXD2 16 // ESP32 UART2 RX → transceiver RO / NORVI X RS-485 header
#define TXD2 17 // ESP32 UART2 TX → transceiver DI / NORVI X RS-485 header
ModbusMaster node;
void setup() {
Serial.begin(115200);
Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); // match your slave's baud/parity
node.begin(SLAVE_ID, Serial2);
Serial.println("Modbus RTU master ready");
}
void loop() {
// Read 2 holding registers starting at address 0x0000
uint8_t result = node.readHoldingRegisters(0x0000, 2);
if (result == node.ku8MBSuccess) {
uint16_t reg0 = node.getResponseBuffer(0);
uint16_t reg1 = node.getResponseBuffer(1);
Serial.printf("Register 0: %u | Register 1: %u\n", reg0, reg1);
} else {
Serial.printf("Modbus read failed — error code: 0x%02X\n", result);
}
delay(500); // respect the slave's minimum poll interval
}
A few things to adjust for your setup:
- Baud rate, parity, and stop bits in Serial2.begin() must match your slave device exactly — check the datasheet. 9600 8N1 is the most common default.
- The register start address (0x0000) and quantity (2) depend on your device's register map.
- If you're using an external MAX485 module instead of a built-in transceiver, you'll also need to toggle a DE/RE GPIO to HIGH before transmitting and LOW after — libraries like esp32ModbusRTU handle this automatically.
Reliability Checklist for Production (RTU)
A working sketch on the bench is not the same as a stable deployment on a factory floor. Before you ship:
- Termination: 120Ω resistors at both physical ends of an RS-485 bus longer than a few meters — omitting this causes reflections and intermittent read failures.
- Unique addressing: every RTU slave needs a distinct 1–247 address.
- Retry logic: treat a single failed poll as noise, not a fault. Retry two or three times before flagging an alarm.
- CRC validation: RTU frames carry a CRC checksum — confirm your library validates it (most do by default) rather than trusting raw bytes.
- Poll interval: don't poll faster than the slave's documented response time — 100–500 ms minimum is typical. Hammering the bus causes timeouts, not faster data.
- Floating-point data: Modbus registers are 16-bit integers. Many analog sensors split a float across two consecutive registers in IEEE 754 format — check the datasheet's register map before assuming a raw integer.
Troubleshooting Common Issues (RTU)
| Symptom | Likely Cause |
|---|---|
Reads return 0xE2 or timeout errors |
Baud rate, parity, or stop bits mismatch with the slave |
| Intermittent garbage on long RS-485 runs | Missing or incorrect 120 Ω termination resistors |
| Bus works with one slave, fails with several | Duplicate slave addresses on the bus |
| Values look scrambled or nonsensical | Register map mismatch — wrong start address or misreading a float as two integers |
Wrapping Up
The ESP32 handles Modbus RTU comfortably, and the code in this tutorial is enough to get real data flowing from an RS-485 slave device today. RTU is the right call for field-level wiring to sensors, meters, and PLCs where a wired serial bus makes more sense than a network connection.
From here, the natural next steps are adding retry/backoff logic around each poll, mapping out your specific device's full register table, and — if you're moving from prototype to a real deployment — considering hardware that removes the RS-485 wiring step entirely.
Further Reading and Resources
- Original field guide this tutorial is based on: Modbus with ESP32 — RTU & TCP Guide (NORVI Blog)
- Hardware used in this tutorial: NORVI X — ESP32-S3 modular industrial IoT controller
- Full technical specs: NORVI X datasheet (PDF)
- Related deep-dive: NORVI X SCADA Integration — Modbus TCP to OPC-UA Gateway Setup
- Sample code and firmware: NORVI Controllers on GitHub
- Full documentation library: norvi.io/docs
- Questions about sizing hardware for your project? Book a free consultation
Top comments (0)