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 name | ModbusMaster |
|---|---|
| Author | Doc Walker |
| Include | #include <ModbusMaster.h> |
| Boards | every board Mokxi compiles for but ATtiny85 |
| In the simulator | Runs, with a gap the note below names. |
| Parts it drives | RS-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.
#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() {}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.
Keep going
More communication libraries
Try ModbusMaster without installing anything
Open the example, change the sketch, and keep your version in a free account.