DEV Community

Getting Started with Modbus RTU on ESP32

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

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)