← Articles
Modbus/9 min read/ views

Why a Successful Modbus Write Doesn't Mean the Motor Started

A Modbus TCP write response says the transaction was accepted, not that the motor moved. Command discipline: permissives, readback, and the retry trap.

ModbusSCADAHMITroubleshootingChecklists

The write succeeded. The pump didn't start.

I've watched an operator hit a start button, get a clean green "command sent," and stand there while nothing spun. The Modbus TCP write returned a normal response — echoed address, echoed value, no exception. The gateway was happy. The 480 V drive behind it was in local mode and had ignored the whole thing.

That gap is the entire problem with Modbus writes. A read that fails is loud: stale value, bad quality, timeout on the screen. A write that "succeeds" but does nothing is silent, and by the time anyone notices, the operator has usually hit the button three more times.

Most Modbus TCP trouble gets caught on the read side — offset off by one, swapped words, polling timeouts. Writes are rarer but the blast radius is bigger. One bad write starts a motor, zeroes a totalizer, moves a setpoint, or clears the fault someone needed for diagnosis. So treat every writable point as a small control interface, not a cell in a spreadsheet.

Know which function code you're actually sending

This matters more than people expect, because the function codes behave differently and vendors implement them unevenly.

  • FC05 — Write Single Coil. The value field isn't a 1. Per the Modbus Application Protocol spec (V1.1b3, §6.5), ON is 0xFF00 and OFF is 0x0000; anything else is illegal and a strict device answers with exception 0x03 (ILLEGAL DATA VALUE). Some gateways are lenient and accept 0x0001 as ON — which means your write works in the lab and fails at a site with a stricter slave.
  • FC06 — Write Single Register. One 16-bit holding register.
  • FC15 (0x0F) / FC16 (0x10) — Write Multiple Coils / Registers. You need FC16 for any 32-bit value (float, DINT) so both words land in the same transaction. Writing two 16-bit halves with two FC06 requests means the device sees a half-updated value between them — fine for a display tag, dangerous for a setpoint the PLC acts on mid-write.

Check what your master library actually emits. More than one "intermittent" write bug has turned out to be a library sending FC16 to a device that only implements FC06.

Separate the command map from the read map

Don't let writable points sit casually among read-only process values in the same register table. Give commands their own section, and for each writable coil or register record:

FieldWhy it matters
Device and unit idThe unit id routes through the gateway to the right serial slave
Function codeFC05/06/15/16 — and whether the device really supports it
Address in vendor and zero-based notationKills the 40001-vs-0 offset mistake
Data type and rangeSigned/unsigned, scaling, 32-bit word order
Field action caused by the writeThe physical effect, not the tag name
Required permissivesWhat must be true before the command is allowed
Expected feedbackHow the HMI knows it actually happened
Reset behaviorMomentary, latched, or PLC auto-cleared

A writable address with no expected-feedback entry is an unfinished design. All the write proves is that the TCP transaction was accepted — not that anything in the field moved.

Let the PLC be the one that says no

For start, stop, reset, mode-change, and acknowledge, the pattern I trust is a command bit or command code the PLC validates. The HMI requests; the PLC decides:

  1. HMI writes a momentary request bit or command code (FC05 or FC06).
  2. PLC checks local/remote, auto/manual, safety permissives, and equipment availability.
  3. PLC latches an accepted/rejected result with a reason code.
  4. HMI clears the request, or the PLC auto-clears it after one scan.
  5. HMI shows feedback from real equipment state, never from the request bit.

The design I'd avoid: the HMI writing straight into internal state bits the PLC also uses for sequence logic. That bypasses interlocks and makes faults miserable to trace, because now two things are writing the same memory. For bare Modbus devices with no PLC in front — a smart valve, a soft starter — push the same checks into the SCADA command layer and keep the allowed range narrow.

The response only means "transaction accepted"

Back to the silent failure. A Modbus write response — including the address/value echo you get back from FC05 and FC06 — means the server accepted the transaction. Nothing more. It doesn't prove a valve stroked, a drive ran, or a setpoint took effect under load.

So carry two separate results for every command:

  • Communication result: accepted, timeout, exception code, connection closed, retries exhausted.
  • Process result: did the equipment reach the requested state within the expected time?

The HMI must say these differently. Write failed: timeout to gateway 10.20.4.7:502 is a comms problem. Command accepted but pump did not confirm run in 8 s is a field problem, and it points a very different person at the fault. This bites hardest on serial-behind-a-gateway setups: plenty of gateways ACK the write from their own buffer and return success before the RTU slave has done anything — or while it's offline.

Confirmations, but only where they're earned

Every write does not need a confirmation dialog. Popups on everything just train operators to click through, and then the popup that mattered gets clicked through too. Spend the friction where the consequence justifies it:

  • Remote start of large rotating equipment.
  • Opening or closing isolation valves.
  • Local↔remote or manual↔auto mode changes.
  • Resetting production counts, batches, or totalizers.
  • Setpoints affecting safety, product quality, or utility load.
  • Clearing alarms or faults someone may still need for diagnosis.

And make the dialog specific: equipment name, current state, requested action, target value. Are you sure? confirms nothing. Start P-101 (currently STOPPED)? confirms a field action.

Bound the setpoint before the write, read it back after

A numeric write needs more than a text box — engineering units, min/max, decimal handling, and a preview of the value about to go out. During commissioning, actually try the ugly cases:

  • Does the displayed scaling match the register scaling?
  • Signed or unsigned?
  • Does the device want integer tenths, hundredths, or raw EU? (A "50.0 °C" setpoint that's really 500 in the register is a classic factor-of-ten incident.)
  • Out-of-range value — does the device clamp, reject, wrap, or quietly accept it?
  • Does the value survive a device reboot?

For anything critical, read the register back after the write and compare to what you requested. If the device rounds or clamps, show the applied value, not the pretty number the operator typed.

Retry is a reader's habit that gets writers in trouble

Auto-retry is fine for reads and genuinely dangerous for commands, because of one case: the device acted on the first write and the response packet got lost. Retry it and you've done it twice.

Before you enable write retries, answer:

  • Is the command idempotent? Re-sending setpoint 50.0 is usually safe. A jog/step command twice is not.
  • Can the device expose a sequence number or transaction result you can check?
  • Can the HMI disable the button until feedback arrives or the command times out?
  • After a timeout, does the operator clearly see the state is unknown rather than failed or done?

For momentary action commands my default is conservative: send once, wait for process feedback, and if comms drop before confirmation, show command status unknown and stop. Note that the MBAP transaction identifier lets you match a response to its request, but it does not protect against a lost response after the slave already executed — that's your logic to handle, not the protocol's.

Log the request and the outcome, on one line

Wrote 1 to 40023 tells a future engineer nothing. The log should let someone reconstruct the event end to end:

  • User, station, timestamp.
  • Equipment name and command label.
  • Requested value in engineering units.
  • Raw Modbus address and value written.
  • Communication result and exception code (0x02 illegal data address, 0x04 server device failure, 0x06 server busy…).
  • Process feedback result and any timeout.
  • Rejection reason from the PLC or device.

This is what turns shift handovers and incident reviews from arguments into readouts, because the record keeps the requested command and the completed field action visibly separate.

The failures worth forcing during FAT

None of these show up on a single happy-path click. Break them on purpose:

  • Offset off by one — the write lands in the adjacent register.
  • 32-bit word order correct for reads, wrong for writes.
  • HMI holds a maintained 1; the device expected a pulse.
  • PLC auto-clears the command faster than the SCADA poll can render it, so the operator never sees it took.
  • Gateway ACKs while the serial slave behind it is offline.
  • Setpoint changed in local mode, then applied unexpectedly when remote returns.
  • Two HMI stations writing competing values with no ownership rule.

Before you hand writes to operators

  1. Every writable address is documented and reviewed separately from read-only data.
  2. The HMI shows equipment context and engineering units before the write.
  3. PLC- or device-side permissives reject unsafe requests, with a reason.
  4. Setpoints have bounds, scaling checks, and readback confirmation.
  5. Command buttons disable or clearly mark themselves when comms quality is bad.
  6. Timeout, exception, rejected, and unknown states each get a distinct message.
  7. Command logs capture user, station, raw value, engineering value, and feedback.
  8. Multi-station behavior is tested — two clients can't fight over one write.

Modbus TCP survived this long because it's simple. The catch is that the simplicity stops at the wire: the command safety richer protocols hand you for free is safety your project has to build itself.