Arduino library 路 Communication

ModbusMaster: read meters and controllers over Modbus RTU

Modbus RTU is the industrial protocol behind energy meters such as the PZEM-004T, solar charge controllers, VFDs and PLCs. The ModbusMaster library makes the Arduino the master: it sends a request frame to one slave address, waits for the answer, checks its CRC and puts the registers it read into a response buffer.

Built in on Mokxi: #include <ModbusMaster.h> and it builds, with nothing to install. Free, and no account needed.

What ModbusMaster does

Most devices speak it over RS-485, a two-wire differential bus, through a MAX485 or similar transceiver. That chip has a driver enable pin (DE, usually tied to RE) that must be high while the Arduino talks and low while it listens, and the library鈥檚 preTransmission() and postTransmission() callbacks are where you switch it.

Register addresses are where most time goes. Datasheets list them in different styles (40001 notation, decimal offsets or hex such as 0x3100), and the library wants the zero-based offset. Holding registers are read with function 3 and input registers with function 4; asking with the wrong one returns an exception, not data.

Library Manager nameModbusMaster
AuthorDoc Walker
Include#include <ModbusMaster.h>
Boardsevery board Mokxi compiles for but ATtiny85
In the simulatorRuns, with a gap the note below names.
Parts it drivesRS-485 module, MAX485

Install it

It builds on every board Mokxi compiles for but ATtiny85.

In Mokxi it is built in: #include <ModbusMaster.h> and the sketch compiles, with no download, no zip and no Library Manager.

On a real board, install "ModbusMaster" by Doc Walker from the Arduino IDE鈥檚 Library Manager (Sketch, Include Library, Manage Libraries), or add ModbusMaster to lib_deps in PlatformIO. Mokxi follows the calls of release 2.0.1, so a sketch written here builds there too.

What to know in the simulator: The pzem004t part (the PZEM-004T energy meter) answers as a slave. Through max485 modules the frames really cross the A/B pair: preTransmission() and postTransmission() switch DE, and the port is flushed before postTransmission() as upstream does. There is no Modbus slave part for RS-485 yet. A request on a port with nothing on it times out after 2 seconds, as on a bench, and a Stream of the sketch鈥檚 own can answer as a slave.

ESP32-C3 路 Arduino Uno 路 Arduino Nano 路 Arduino Mega 2560 路 Arduino Leonardo 路 SparkFun Pro Micro 路 Arduino Micro 路 ESP32-C6 路 Raspberry Pi Pico 路 Raspberry Pi Pico W 路 Seeed XIAO SAMD21 路 STM32 路 STM32 Nucleo-F411RE 路 STM32 Blue Pill 路 micro:bit V2 路 Arduino Uno R4 路 ESP32 DevKit 路 ESP32-S3-DevKitC-1 路 ESP8266 NodeMCU 路 Arduino Pro Mini 3.3 V 路 Seeed XIAO ESP32-C3 路 ESP32-C3 SuperMini 路 Seeed XIAO ESP32-C6 路 ESP32-DevKitC V4 路 LOLIN32 Lite 路 Wemos D1 R32 路 Cheap Yellow Display 路 TTGO T-Display 路 Seeed XIAO ESP32-S3 路 Arduino Nano ESP32 路 Arduino Nano RP2040 Connect 路 Seeed XIAO RP2040 路 Waveshare RP2040-Zero 路 Arduino Uno R4 WiFi

The calls you will use

In the order a sketch meets them.

ModbusMaster node

One object per slave device you talk to.

node.begin(1, Serial2)

Sets the slave address (1 here) and the serial port, which you start at the device鈥檚 baud rate first, often 9600.

node.preTransmission(preTransmission)

Calls your function before each request; set the MAX485鈥檚 DE and RE pins high in it. node.postTransmission() sets them low again.

void preTransmission() { digitalWrite(MAX485_DE, HIGH); }
void postTransmission() { digitalWrite(MAX485_DE, LOW); }

node.readHoldingRegisters(0x3100, 6)

Reads six holding registers from 0x3100 (function 3). Returns node.ku8MBSuccess, 0, when the answer came back valid.

node.readInputRegisters(0x0000, 10)

Reads input registers (function 4), which is where a PZEM-004T keeps voltage, current and power.

node.getResponseBuffer(0)

The first register read, a 16-bit word. Values wider than 16 bits span two registers you combine yourself.

node.writeSingleRegister(0x0001, 100)

Writes one holding register (function 6), for setting a parameter on the slave.

Example: Holding registers read from a slave the sketch plays itself

On the Arduino Uno R3. It runs in the browser exactly as printed.

sketch.ino 路 Arduino Uno R3
#include <ModbusMaster.h>

// A pretend energy meter at slave 1: it answers function 3 with
// registers that hold 230, 5 and 1150, CRC and all.
class Meter : public Stream {
 public:
  uint8_t in[16], inLen = 0, out[16], outLen = 0, outPos = 0;
  size_t write(uint8_t b) override {
    if (inLen < sizeof(in)) in[inLen++] = b;
    if (inLen == 8 && in[0] == 1 && in[1] == 3) answer();
    return 1;
  }
  int available() override { return outLen - outPos; }
  int read() override { return outPos < outLen ? out[outPos++] : -1; }
  int peek() override { return outPos < outLen ? out[outPos] : -1; }

 private:
  static uint16_t crc(const uint8_t *b, uint8_t n) {
    uint16_t c = 0xFFFF;
    for (uint8_t i = 0; i < n; i++) {
      c ^= b[i];
      for (uint8_t k = 0; k < 8; k++) c = (c & 1) ? (c >> 1) ^ 0xA001 : c >> 1;
    }
    return c;
  }
  void answer() {
    const uint16_t regs[3] = {230, 5, 1150};
    uint8_t qty = in[5];
    outLen = 0;
    outPos = 0;
    out[outLen++] = 1;
    out[outLen++] = 3;
    out[outLen++] = qty * 2;
    for (uint8_t i = 0; i < qty && i < 3; i++) {
      out[outLen++] = regs[i] >> 8;
      out[outLen++] = regs[i] & 0xFF;
    }
    uint16_t c = crc(out, outLen);
    out[outLen++] = c & 0xFF;
    out[outLen++] = c >> 8;
    inLen = 0;
  }
};

Meter meter;
ModbusMaster node;

void setup() {
  Serial.begin(9600);
  node.begin(1, meter);
  uint8_t result = node.readHoldingRegisters(0x0000, 3);
  if (result == node.ku8MBSuccess) {
    Serial.print("Voltage: ");
    Serial.print(node.getResponseBuffer(0));
    Serial.print(" V, current: ");
    Serial.print(node.getResponseBuffer(1));
    Serial.print(" A, power: ");
    Serial.print(node.getResponseBuffer(2));
    Serial.println(" W");
  } else {
    Serial.print("Error 0x");
    Serial.println(result, HEX);
  }
  ModbusMaster nobody;
  nobody.begin(2, Serial);
  uint8_t timedOut = nobody.readHoldingRegisters(0, 1);  // its request goes out on Serial
  Serial.println();
  Serial.print("Slave 2: 0x");
  Serial.println(timedOut, HEX);
}

void loop() {}

Open it in the editor, wired and ready to run

Common errors and how to fix them

result is 0xE2 (ku8MBResponseTimedOut) on every request

Nothing answered within 2 seconds. Check the slave address and baud rate, that A and B are not swapped, that the DE pin is switched in the callbacks, and that GND is shared with the device.

result is 0x02, illegal data address

The slave answered with an exception: the register does not exist. Convert the datasheet鈥檚 address to a zero-based offset (40001 is 0) and check whether it is a holding or an input register.

result is 0xE3 (invalid CRC) now and then

Noise or timing on the bus. Add 120 ohm termination at the ends of a long cable, keep the DE switch tight around the send, and avoid SoftwareSerial at high baud rates on an Uno.

Try ModbusMaster without installing anything

Open the example, change the sketch, and keep your version in a free account.