Modbus Mask Writes and Read/Write Registers: Avoid Unsafe Read-Modify-Write Assumptions
Two clients can each write the correct value and still lose one another’s changes. Client A reads a shared register, modifies one bit and writes the whole word. Client B does the same using an older snapshot. Serializing individual network requests does not make either multi-request read-modify-write sequence indivisible.
Modbus Application Protocol V1.1b3 defines function 22, hexadecimal 0x16, for mask writes and function 23, hexadecimal 0x17, for combined read/write operations. They solve different problems. This guide uses a synthetic register model to decide which operation fits, while leaving device-specific process and firmware guarantees explicit.

Identify every writer before choosing a function
Inventory the HMI, supervisory client, maintenance tool, gateway logic and device control program that can modify the register. Record whether each writer owns the whole word or selected bits. Two TCP clients may share one gateway without becoming two serial masters; their operations are still distinct requests presented to one downstream scheduler.
Start with an intentionally simple example. Both clients read 0x0000. A wants bit 3 set and writes 0x0008. B wants bit 5 set and writes its independently computed 0x0020. The final value can be 0x0020, losing A’s bit. Every frame and response may be valid. A faster polling rate only changes the race window, not the ownership problem.
Apply the mask to current contents, not a stale copy
The normative function-22 calculation is given in section 6.16 of the application specification: result = (current AND and-mask) OR (or-mask AND NOT and-mask). Treat all terms as 16-bit values. An AND-mask bit of one preserves the current bit; an AND-mask bit of zero takes the corresponding OR-mask bit.
To set bit 3, use AND mask 0xFFF7 and OR mask 0x0008. To clear bit 3, retain the same AND mask and use OR mask 0x0000. Bits not selected for change retain their current server-side values. This removes the client’s separate read and its stale whole-word replacement from that update path.
Distinguish disjoint updates from conflicting intent
If A sets bit 3 and B sets bit 5 through correctly implemented mask operations, either operation order produces 0x0028 from zero. That is a property of these disjoint masks, not a general promise that all concurrent commands commute. If both clients target bit 3 with opposite values, ordering still determines the final result.
Do not turn mask writes into an ownership substitute. Assign an authoritative writer or arbitration rule for conflicting bits. Also verify whether the register has special behavior, such as write-one-to-clear status or command-trigger semantics. A generic mask calculation is not permission to treat such an object as an ordinary persistent bit field.
Use function 23 for write-then-read, not compare-and-swap
Function 23 combines a write and a read in one Modbus transaction, with the write performed first. The request carries separate read and write ranges; its normal response contains the requested read values. Section 6.17 permits 1–125 registers read and 1–121 written, subject to valid device addresses and implementation support.
The write values are already present in the request. The server does not first return a value for the client to modify inside that same transaction. Function 23 is therefore neither compare-and-swap nor a general replacement for a bit-mask update. It can support a documented “write setpoint, then read status” exchange, but does not by itself promise process completion, persistence or isolation from the device’s own control logic.
Check native support and expose weaker emulation
Confirm the exact device model, firmware, register class and supported functions. A valid Illegal Function exception reports rejection of that function in the current context; documentation must distinguish absent support from a state restriction. Silence is different evidence because routing or communication failure may explain it. Keep those outcomes separate while establishing the capability record.
A gateway may emulate function 22 by reading and writing downstream. If it does, ask what serializes competing writers and whether device-internal updates can occur between those operations. A mutex in the gateway cannot automatically lock a PLC program or a second management interface. Document weaker semantics instead of presenting the emulation as equivalent to a native server-side mask operation.
Test the bit contract in a safe model first
Create an in-memory 16-bit register and a scheduler that can pause between client reads and writes. Reproduce the lost-update sequence above, then substitute mask operations and verify both orderings for disjoint bits. Test preserving all bits, replacing all bits, setting a selected bit and clearing it.
For device acceptance, use an isolated simulator or approved harmless register. Exercise function support, echoed mask fields, incorrect addresses and overlapping writer intent. For function 23, verify write-before-read using an ordinary writable location with documented semantics. Do not infer controller scan isolation from one successful readback; obtain the vendor guarantee or measure the explicitly scoped behavior.
Choose an ownership fallback when support is absent
If native mask support is unavailable, prefer a single writer that receives bit-change intents and owns the full register. Alternatives include separate registers per owner or a device-defined command mailbox. Any proposed lock must include every writer and define failure recovery; an application-local lock shared by only one client cannot prevent another client’s update.
- Function numbers are unambiguous in decimal and hexadecimal.
- Mask tests preserve unselected bits and expose conflicting writers.
- Function 23 is described as write-before-read, with no invented conditional-update guarantee.
- Native support, gateway emulation and device-internal concurrency are documented separately.
- The fallback identifies one authority for each shared bit or whole register.
The deliverable is a concurrency contract. Correct response echoes help validate requests, but they cannot replace a clear rule for who is allowed to change shared state.