Skip to Content

← Modbus TCP to RTU Gateway on an M-Duino PLC

Bioreactor controlM-DuinoModbus TCPModbus RTURS485Communication

Modbus TCP to RTU Gateway on an M-Duino PLC — full example

Turn an M-Duino into a Modbus TCP slave and RTU master gateway. Expose a whole bioreactor plant on port 502 while polling six RS485 slaves. Full code.

Complete, runnable program for the M-Duino (gateway-modbus-tcp-rtu.ino): wiring header, requirements and integration notes included.

Download the full project pack — freeThis example + the related ones + bill of materials

Read-only preview.

/*
 * COMPLETE EXAMPLE — Gateway Modbus TCP slave <-> Modbus RTU master
 *
 * Device:  M-Duino (Industrial Shields, Arduino PLC with Ethernet + RS485)
 * Based on: bioreactor control project, ModbusTCPSlaveRTUMaster.ino
 *
 * Topology:
 *   SCADA / client  --Modbus TCP (port 502)-->  M-Duino  --Modbus RTU (RS485, 9600 8N1)-->  6 slaves
 *
 * RTU bus (RS485, 9600 8N1) with 6 slaves:
 *   addr 1,2  Thermoelectric chillers
 *   addr 3,4  Variable frequency drives
 *   addr 5,6  Flow meters
 *
 * Modbus TCP map exposed by this gateway (summary):
 *   Coils 0-17        : 8 relays + 8 pilot lights + 2 counter resets
 *   Holding 0-9       : setpoints (temperature x10, RPM x100) and run commands
 *   Input regs 0-11   : actual temperature, pressure, pH, liters and RPM
 *
 * Key idea: the TCP side is just memory (modbus.update() keeps it alive)
 * and a round-robin scheduler moves those values to the RTU bus without blocking.
 *
 * Integration: the catalog modules "Chiller control", "Drive control"
 * and "Flow meter reading" implement the detail of each slave; here the
 * whole set is orchestrated.
 */

#include 
#include 
#include 

// --- RTU side: master on the M-Duino RS485
ModbusRTUMaster master(RS485);

// --- TCP side: slave on port 502
ModbusTCPSlave modbus;

// --- RTU addresses of the 6 slaves
const uint8_t CHILLER[2]   = {1, 2};
const uint8_t DRIVE[2]     = {3, 4};
const uint8_t FLOW[2]      = {5, 6};

// --- Memory blocks exposed over TCP
bool     coils[18];            // 0-7 relays, 8-15 pilot lights, 16-17 resets
uint16_t holdingRegs[10];      // setpoints and run commands
uint16_t inputRegs[12];        // real-time measurements

// RTU registers used (see the datasheet of each slave)
const uint16_t REG_CHILLER_RUN = 0x06;    // chiller start/stop
const uint16_t REG_VAR_RPM_SP  = 0x0144;  // RPM setpoint x100
const uint16_t REG_VAR_RPM_RD  = 0x2149;  // actual RPM reading (2 regs)
const uint16_t REG_FLOW_ACC    = 0x100;   // accumulated liters (2 regs)

uint32_t tPoll = 0;
uint8_t  step  = 0;            // round-robin over the slaves

void setup() {
  Serial.begin(115200);

  // RTU master at 9600 8N1 (short timeout: we do not want to block the TCP)
  master.begin(9600);
  master.setTimeout(200);

  // TCP slave: publish the three memory blocks
  modbus.addCoils(0, coils, 18);
  modbus.addHoldingRegisters(0, holdingRegs, 10);
  modbus.addInputRegisters(0, inputRegs, 12);
  modbus.begin();             // listen on port 502
}

void loop() {
  // 1. Serve incoming TCP requests (non-blocking)
  modbus.update();

  // 2. Every 200 ms take one round-robin step over the RTU bus
  if (millis() - tPoll > 200) {
    tPoll = millis();
    serveSlave(step);
    step = (step + 1) % 6;    // 0,1 chillers · 2,3 drives · 4,5 flow meters
  }
}

// Translates a round-robin slot into a concrete RTU transaction
void serveSlave(uint8_t slot) {
  if (slot < 2) {
    // Chiller: push the run command the SCADA left in holding 8/9
    uint8_t run = holdingRegs[8 + slot] ? 1 : 0;
    master.writeSingleRegister(CHILLER[slot], REG_CHILLER_RUN, run);
    drainResponse();
  } else if (slot < 4) {
    // Drive: write setpoint and read actual RPM (2 x 16-bit regs)
    uint8_t i = slot - 2;
    master.writeSingleRegister(DRIVE[i], REG_VAR_RPM_SP, holdingRegs[6 + i]);
    drainResponse();
    if (master.readHoldingRegisters(DRIVE[i], REG_VAR_RPM_RD, 2)) {
      if (master.isWaitingResponse()) {
        ModbusResponse r = master.available();
        if (r && !r.hasError())
          inputRegs[10 + i] = (uint16_t)((r.getRegister(0) << 16) | r.getRegister(1));
      }
    }
  } else {
    // Flow meter: read 32-bit accumulated value into input regs
    uint8_t i = slot - 4;
    if (master.readHoldingRegisters(FLOW[i], REG_FLOW_ACC, 2)) {
      if (master.isWaitingResponse()) {
        ModbusResponse r = master.available();
        if (r && !r.hasError())
          inputRegs[6 + i] = (uint16_t)((r.getRegister(0) << 16) | r.getRegister(1));
      }
    }
  }
}

// Consumes the response of a write so the master is not left busy
void drainResponse() {
  if (master.isWaitingResponse()) {
    ModbusResponse r = master.available();
    (void)r;
  }
}
Download the full project pack — freeThis example + the related ones + bill of materials