Sponsored Content

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)