Welcome to 1979
Modbus was born in 1979 at Modicon (now Schneider Electric). Jimmy Carter was president. The Sony Walkman had just launched. TCP/IP wouldn't be standardized for another four years. And someone decided that industrial equipment needed a networking protocol.
They designed something simple. Brutally simple. A master sends a request with a device address, a function code, and a register address. The slave responds with data. No handshake, no encryption, no authentication, no schema negotiation. Just raw bytes on a wire.
Forty-seven years later, we're still using it. Not because it's good. Because it's everywhere, and the equipment it controls costs more than your house.
Two flavors of pain
Modbus comes in two variants. Modbus RTU runs over serial (usually RS-485) and frames messages with timing gaps. Modbus TCP wraps the same protocol in a TCP/IP packet with a 6-byte header.
RTU is the original haunting. TCP is the ghost that learned to use the internet.
Most modern installations use TCP because Ethernet infrastructure is everywhere and nobody wants to run RS-485 cable anymore. But the protocol inside the TCP wrapper is still Modbus. It still thinks in 16-bit registers. It still has the same limitations. You just moved the haunted house to a nicer neighborhood.
The byte order dance of the damned
Every Modbus register is 16 bits. Simple enough. But a 32-bit float spans two registers, and this is where the fun begins.
The Modbus specification does not define byte ordering for multi-register values.
Some devices are big-endian (AB CD). Some are little-endian (CD AB). Some swap the words but not the bytes (CD AB, also called "mid-big-endian," a term I wish I'd made up). Some swap the bytes but not the words. Some are just wrong, and the documentation contradicts the firmware.
I once spent two days trying to read a power meter's voltage register. The documentation said "32-bit float, registers 40001-40002." It read as 1.23e-38, which is technically a valid float but not a valid voltage for a 480V industrial bus. Swapping the word order gave me 483.7V. The documentation was wrong about the byte order. Or the firmware was wrong about the documentation. At this point, the distinction is philosophical.
The timing ghost on the RS-485 bus
Modbus RTU uses timing to delimit frames. Specifically, a silence of 3.5 character times marks the end of a frame. At 9600 baud, one character (11 bits with start, parity, and stop) takes about 1.146ms. So 3.5 characters is about 4ms of silence.
Get this wrong and one of two things happens. If your gap is too short, the slave interprets two separate messages as one corrupted frame and ignores both. If your gap is too long, the slave sees your single message as two fragments and ignores both.
And it gets better. At higher baud rates (38400, 115200), the character time is shorter, so the required gap is tighter. At 115200 baud, your inter-frame gap is about 0.3ms. Operating system scheduling jitter alone can blow that deadline. This is why serious Modbus RTU implementations use hardware timers, not software delays.
Also, USB-to-RS-485 converters add their own latency. The FTDI chip buffers data in 16ms chunks by default. That's wildly longer than any valid Modbus frame gap. You'll spend an afternoon configuring the FTDI latency timer before a single frame gets through, and you'll enjoy none of it.
Function codes from the shadow realm
The Modbus specification defines function codes 1 through 6 as standard. Read coils, read discrete inputs, read holding registers, read input registers, write single coil, write single register. There are also multi-write function codes (15, 16) and a few diagnostic ones.
That's the spec. Here's reality.
Manufacturers add custom function codes in the 65-72 range. Sometimes documented. Sometimes documented in a PDF from 2003 that's hosted on a website that no longer exists. Sometimes not documented at all, but referenced in a forum post by someone named "IndustrialGuy47" who was last active in 2011.
One energy meter I integrated required function code 68 to reset its peak demand registers. This was documented in a supplementary technical note that shipped with the device on a CD-ROM. A physical CD-ROM, in 2019.
Error responses, or "good luck with that"
When a Modbus slave encounters an error, it responds with an exception code. There are six standard ones:
- Illegal function
- Illegal data address
- Illegal data value
- Slave device failure
- Acknowledge
- Slave device busy
Code 2, "Illegal data address," is the most common. It means something is wrong with the register address you requested. What exactly? Could be the register doesn't exist. Could be you requested too many registers at once. Could be the device is in a state where that register isn't accessible. Could be the firmware has a bug. The error code won't tell you which. You get a single byte that says "nope" and your job is to figure out why.
Some devices don't even respond to invalid requests. They just silently ignore them. Your master times out, and you get to wonder whether the device is offline, the RS-485 wiring is bad, or your request was malformed. All three look identical from the master's perspective.
Register maps that lie
Every Modbus device comes with a register map. A table that says register 40001 is voltage, 40003 is current, 40005 is power, and so on. This document is the foundation of your integration.
It lies.
Not always. Not deliberately (usually). But register maps accumulate errors across firmware versions, documentation translations, and copy-paste jobs. The map says 40001 is voltage but the firmware moved it to 40003 in version 2.1 and nobody updated the PDF. Or the map uses Modbus convention addressing (starting at 1) but the firmware uses protocol addressing (starting at 0), so you're always off by one. Or the map lists the registers for the base model but your device is the "Pro" variant that inserted additional registers before the ones you need.
I once discovered that a building management controller's register map was for a completely different product line. Same manufacturer, same form factor, different register layout. The part number on the documentation didn't match the part number on the device. Nobody noticed because the system had been commissioned by reading registers one at a time until numbers that "looked right" appeared.
Survival guide
After years of Modbus integration work, including bridging a 1990s RTU network to a modern TCP/IP stack for a smart building energy monitoring system, here are the rules I live by.
Always scope the bus first. Before writing a single line of code, connect a logic analyzer to the RS-485 bus (or Wireshark on the TCP port) and watch the existing traffic. You'll learn more in 10 minutes of observation than an hour of reading documentation.
Never trust the register map. Verify every register manually using Modbus Poll, QModMaster, or a similar diagnostic tool. Read each register, confirm the value makes physical sense, and document what you actually see versus what the manual claims.
Use conservative timeouts. 500ms minimum for RTU responses. Some devices are slow. Some are very slow. An old VFD I once integrated took 1.2 seconds to respond to holding register reads because its firmware processed Modbus in its idle loop, and the motor control loop took priority. Correctly so, but annoying.
Probe byte ordering before parsing. For every multi-register value, read the raw bytes, try all four byte orderings, and see which one gives a physically plausible result. Then document the ordering per device, because different devices on the same bus might use different orderings.
Log raw frames during development. When something doesn't work, you want to see the exact bytes on the wire, not your parsing library's interpretation of them. Hex dumps are your best friend.
Expect the unexpected. Devices that respond to broadcast address when they shouldn't. Registers that return stale data after a power cycle until the first measurement completes. Function codes that work in the documentation but return exception code 1 on the actual device.
It's still worth it
For all its ghosts and hauntings, Modbus works. It's simple enough to implement from scratch in an afternoon, interoperable enough that devices from different decades can share a bus, and reliable enough that critical industrial infrastructure runs on it worldwide.
Just bring a logic analyzer. And maybe some sage.