Quick Start Guide

This quick start guide presents the basic usage of EZModbus for Client, Server & Bridge applications, with the Arduino API.

ESP-IDF only differs in not including Arduino.h & using the IDF-style struct for UART configuration (see Core concepts > HAL wrappers for more information)

Modbus Client

This example shows how to read a single holding register over Modbus RTU. Here, we use a simple "blocking" read (synchronous). Detailed use of asynchronous requests is introduced in the How To Guides > Modbus Client (Master) section of this guide.

#include <Arduino.h>
#include <EZModbus.h>

// Target device and register
#define SLAVE_ID       1
#define REG_ADDRESS    100  // Holding register 100

// Define UART config
ModbusHAL::UART::Config uartConfig = {
    .serial = Serial2,
    .baud = 9600,
    .config = SERIAL_8N1,
    .rxPin = 15,     
    .txPin = 14,
    .dePin = 5
}

// Instantiate UART, interface and client
ModbusHAL::UART         uart(uartConfig);
ModbusInterface::RTU    rtu(uart, Modbus::CLIENT);
Modbus::Client          client(rtu);

void setup() {
  Serial.begin(115200);  // Debug serial

  // Initialize UART + Modbus Client
  uart.begin();
  client.begin();

  Serial.println("✅ Modbus RTU client ready");
}

void loop() {
  // Build request frame for holding register
  Modbus::Frame request = {
    .type       = Modbus::REQUEST,
    .fc         = Modbus::READ_HOLDING_REGISTERS,
    .slaveId    = SLAVE_ID,
    .regAddress = REG_ADDRESS,
    .regCount   = 1, // Here we will read 1 register at REG_ADDRESS
    .data       = {} // No data in the request for reads
  };

  // Placeholder for response
  Modbus::Frame response;

  // Send synchronously (waits until response or timeout)
  if (client.sendRequest(request, response) == Modbus::Client::SUCCESS) {
    auto value = response.getRegister(0); // Get the first (and only) register value
    Serial.printf("Register %d value: %d\n", REG_ADDRESS, value);
  } else {
    Serial.println("⚠️ Read failed or timed out");
  }

  delay(1000);
}

That’s it! Your ESP32 now reads a register every second.

Modbus Server

Here's how to implement a basic Modbus slave/server that exposes a few registers (Words). Here, we use a static Word store & basic pointer access to user variables. More advanced examples using custom callbacks are provided in the How-To Guides > Modbus Server (Slave) section of this guide.

#include <Arduino.h>
#include <EZModbus.h>

// Our server slave ID
#define SERVER_SLAVEID      1

// Define UART config
ModbusHAL::UART::Config uartConfig = {
    .serial = Serial2,
    .baud = 9600,
    .config = SERIAL_8N1,
    .rxPin = 15,     
    .txPin = 14,
    .dePin = 5
}

// Store to hold Modbus register map
Modbus::StaticWordStore<10> store;

// Create the RTU interface and server
ModbusHAL::UART      uart(uartConfig);
ModbusInterface::RTU rtu(uart, Modbus::SERVER);
Modbus::Server       server(rtu, store, SERVER_SLAVEID);

// Variables that will be exposed as Modbus registers
volatile uint16_t temperature = 230;  // 23.0°C (stored as fixed-point)
volatile uint16_t humidity = 450;     // 45.0% (stored as fixed-point)
volatile uint16_t fanState = 0;       // 0=off, 1=on

void setup() {
  Serial.begin(115200);
  Serial.println("Starting Modbus Server...");
  
  // Register our variables as Modbus Words
  server.addWords({
    {Modbus::INPUT_REGISTER, 100, 1, &temperature},
    {Modbus::INPUT_REGISTER, 101, 1, &humidity},
    {Modbus::COIL,           1,   1, &fanState}
  });
  
  // Initialize UART + Modbus Server
  uart.begin();
  server.begin();
  
  Serial.println("Modbus server initialized!");
}

void loop() {
  // Update sensor values (in a real application, read from actual sensors)
  temperature = 230 + random(-10, 10);
  humidity = 450 + random(-20, 20);
  delay(1000); // Loop every second
}

No need to poll the server, it will process automatically incoming requests and fetch/update values in the background.

Modbus Bridge

#include <Arduino.h>
#include <EZModbus.h>
#include <WiFi.h>

// WiFi settings
const char* ssid = "YourNetworkName";
const char* password = "YourPassword";

// Define UART config
ModbusHAL::UART::Config uartConfig = {
    .serial = Serial2,
    .baud = 9600,
    .config = SERIAL_8N1,
    .rxPin = 15,     
    .txPin = 14,
    .dePin = 5
}

// Create TCP server on port 502 (standard Modbus TCP port) 
// & setup UART port with RS485 config
ModbusHAL::TCP  tcpServer(502);
ModbusHAL::UART uart(uartConfig);

// Create interfaces with complementary roles
ModbusInterface::RTU rtu(uart, Modbus::CLIENT);
ModbusInterface::TCP tcp(tcpServer, Modbus::SERVER);

// Create bridge to connect the interfaces
Modbus::Bridge bridge(rtu, tcp);

void setup() {
  Serial.begin(115200);
  Serial.println("Starting Modbus Bridge...");
  
  // Connect to WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("Connected to WiFi. IP address: ");
  Serial.println(WiFi.localIP());
  
  // Initialize TCP & UART
  tcpServer.begin();
  uart.begin();
  
  // Start the bridge, it will automatically start the interfaces
  bridge.begin();
  
  Serial.println("Modbus bridge initialized!");
}

void loop() {
  // The bridge handles everything automatically in the background
  // If you just want to let it run, you can safely delete the main task
	vTaskDelete(NULL);
}

The bridge will automatically forward requests & responses in both directions.

Last updated