← All posts
26 May 2026·10 min read·Debugging

Modbus function codes: reads, writes, and why some are read-only.

The major Modbus function codes (01-06, 15, 16) - what each one targets, real examples of the data they carry, and why the protocol splits read-only and read-write into separate tables.

Why Modbus function codes matter

The function code is the second byte in every Modbus request and it does most of the work in the protocol. It tells the slave two things at once: which of the four register tables you're talking to (coils, discrete inputs, input registers, or holding registers) and whether the request is a read or a write. Once the slave decodes the function code, everything that follows on the wire - the address, the quantity, the data payload - is interpreted in the context of that table and that operation.

There are about twenty function codes defined in the Modbus Application Protocol Specification, but in practice eight of them carry more than 99% of real-world traffic. The rest are either legacy curiosities (the 1970s Modicon programming functions) or specialised codes that almost no field device implements. This post covers those eight - the read four and the write four - with the data they were designed to carry, plus the design choices that explain why some tables are read-only and others aren't.

Field tip. When a vendor manual is silent on which function code to use - and many are - reach for FC 03 (Read Holding Registers) first. It's the most universally implemented read code, and a surprising number of devices put all their data (measurements included) in the holding-register table just so FC 03 finds it. If FC 03 returns Exception 01 (Illegal Function) or 02 (Illegal Data Address), fall back to FC 04. One of the two almost always works for measurement data. Coilware Modbus lets you flip between function codes from the UI, so the whole experiment takes about ten seconds. See /coilware-modbus.

The four Modbus register tables

Before the function codes themselves, the table they target. Modbus organises every addressable piece of data on a slave into one of four separate tables, each with its own access semantics:

TableSizeAccessModicon prefixWhat it's meant for
Coil1 bitR/W0xxxxMaster-driven binary commands and binary outputs
Discrete Input1 bitR/O1xxxxPhysical binary input states from the field
Input Register16 bitR/O3xxxxDevice-measured / sensed analogue values
Holding Register16 bitR/W4xxxxMaster-writeable configuration, setpoints, and state

The pattern is consistent: if the data describes physical reality coming back from the world (a sensor reading, a limit switch state), the table is read-only. If the data is something the master sets or commands (a setpoint, an output state, a configuration parameter), the table is read-write. The function codes that exist - and the ones that don't exist - enforce that split.

There's no "write to a discrete input" function code in the spec. There's no "write to an input register" function code in the spec. The read-only tables are read-only because the protocol provides no client-facing way to write to them. We'll come back to this when we cover the writes.

Modbus function code 01 - Read Coils

Reads one or more 1-bit coils from the slave. A coil is a binary value that the master is allowed to write back to - so the reverse direction (writing) is also supported via FC 05 and FC 15. Coils are typically things like:

  • "Pump 1 enable" command bit driven from a SCADA HMI button
  • "Reset fault" pulse from the operator panel to a drive
  • "Output relay 3 commanded state" on a remote I/O block
  • "Auto / manual mode" selection bit on a controller

A single Read Coils request can ask for up to 2,000 coils in one transaction. The slave returns them packed eight to a byte, padded to a whole byte at the end - so reading 13 coils takes 2 bytes in the response, not 13.

The most common confusion with FC 01 is reading it when you actually want FC 02. If the bit you're after is a physical input from the field (an end-of-travel limit switch, a tank-full float), it lives in the discrete input table, not the coil table - even though both are 1-bit. The function code is what selects which table you're addressing; same address number in the coil table and the discrete input table refers to two completely different bits.

Modbus function code 02 - Read Discrete Inputs

Reads one or more 1-bit discrete inputs from the slave. Discrete inputs are read-only by design: they represent physical input states that the device has received from the field, and there's nothing meaningful for the master to write to them. Even if you tried, the protocol has no write function code targeting this table. Typical discrete inputs:

  • "E-stop pressed" line from a hardwired safety circuit
  • "Door interlock open" from a door switch
  • "Tank full" signal from a float switch
  • "Motor running" feedback from a contactor auxiliary

Same packing as FC 01: bits returned 8 to a byte, padded to a whole byte. Same upper limit (2,000 bits per request).

Why is this table read-only at the protocol level? Because writing a value to it would be a lie - the field doesn't care what the master thinks the input state is, and the next sensor scan inside the device would overwrite anything the master wrote anyway. The spec designers solved that by simply not defining a write FC for the discrete input table. The data flows one way (from the world, through the device, to the master) and the function code set reflects that.

Modbus function code 03 - Read Holding Registers

The workhorse of the protocol, and the function code you'll see most often in any real-world capture. It reads one or more 16-bit holding registers from the slave. Holding registers can be written by the master (via FC 06 or FC 16), so they're explicitly read-write at the protocol level - but in practice they end up carrying a much wider range of data than just configuration:

  • Setpoints: target temperature, target pressure, target speed
  • PID gains: Kp, Ki, Kd
  • Alarm thresholds: high-high, high, low, low-low
  • Command words: motor speed reference to a drive, valve position command
  • Configuration: comm address, baud rate, serial number (often read-only via vendor convention, even though the protocol would allow writes)
  • Measured values that should arguably live in input registers but don't: power meter readings, voltages, currents, temperatures

The last bullet is the bit worth dwelling on - it's covered in its own section below. The short version: the "everything is a holding register" pattern is so common in commercial devices that you'll spend the bulk of your career reading what is really sensor data via FC 03 rather than FC 04. It's not how the spec intended things to be organised, but it's how the field actually works.

A single FC 03 request can read up to 125 registers in one transaction (because the response payload is capped at 253 bytes of PDU and each register takes 2 bytes). That cap matters - try to read 200 registers in a single FC 03 and you'll get back Exception 03 (Illegal Data Value), not because your address is wrong but because the quantity field violates the spec's range.

Modbus function code 04 - Read Input Registers

Reads one or more 16-bit input registers from the slave. The input register table is read-only - same design reason as discrete inputs: it's meant for data the device has measured or sensed, and the master writing to it would make no sense. Typical input register content:

  • ADC raw counts from an analogue input module
  • Calibrated voltage, current, temperature readings
  • Encoder counts
  • Process variable feedback (the measured PV that the PID loop is trying to drive to its setpoint)
  • Vendor-specific status words

In a clean implementation, FC 04 reads what FC 03 cannot reach: the sensor side of the device. The split was meant to make it obvious from the function code alone whether the value at a given address was master-writeable or strictly device-sourced.

In real implementations, vendors often skip the input register table entirely and put everything in holding registers. The result: many devices support FC 03 with broad coverage and either don't implement FC 04 at all, or use it for an obscure subset. When you see Exception 01 (Illegal Function) on an FC 04 request, that's usually what's happening - the vendor never implemented it. Same address with FC 03 will often work.

Modbus function code 05 - Write Single Coil

Writes one 1-bit coil on the slave. The on/off encoding in the payload is one of the small protocol quirks worth knowing: the value field is two bytes, and only two values are legal - 0xFF00 for "on" and 0x0000 for "off". Any other value is an Exception 03 (Illegal Data Value).

Typical uses:

  • Pulsing a "reset fault" coil from an HMI button
  • Turning a "manual override enable" coil on for a maintenance procedure
  • Setting an output relay state directly from a SCADA scan

FC 05 is concise on the wire - five bytes of PDU payload (FC + address + value). For a one-off command it's slightly smaller than the multi-coil version (FC 15), which is why both exist. We come back to the single-vs-multiple question after FC 16.

Modbus function code 06 - Write Single Register

Writes one 16-bit holding register on the slave. The data payload is the 2-byte value itself, MSB first per the protocol's per-register big-endian rule. Typical uses:

  • Pushing a single setpoint value from a SCADA tag write to a controller
  • Updating a configuration parameter without modifying the surrounding map
  • Triggering an action by writing a specific command code to a "command word" register

FC 06 is the simplest possible write operation in Modbus - the request is just five bytes of payload (FC + 2-byte address + 2-byte value), the response is an echo of the request, and there's nothing to misinterpret. The catch is that many devices don't implement it. They implement only FC 16 (Write Multiple) and reject FC 06 with Exception 01.

Modbus function code 15 (0x0F) - Write Multiple Coils

Writes any number of 1-bit coils in a single request - up to 1,968 coils per transaction (the limit is set by the PDU length cap). The data payload is the coils packed 8 to a byte, just like the response from FC 01.

Typical uses:

  • Setting an entire output register on a remote I/O module in one transaction
  • Writing back a saved set of binary states from a recipe load
  • Atomically commanding multiple outputs from one HMI control

The advantage over FC 05 is throughput and atomicity: instead of 16 separate "Write Single Coil" requests to set 16 output bits, one FC 15 request does the same job in a single round-trip - and the slave applies all 16 changes together, with no intermediate state visible.

Modbus function code 16 (0x10) - Write Multiple Registers

Writes any number of 16-bit holding registers in a single request - up to 123 registers per transaction. The data payload is the register values packed back-to-back, MSB first per register.

Typical uses:

  • Loading an entire recipe or parameter block onto a controller in one go
  • Pushing a 32-bit float (which spans two registers) atomically
  • Synchronising a date-time block (year / month / day / hour / minute / second across multiple registers)
  • Sending a multi-word command structure to a drive

The 32-bit float case is the one to highlight. If you write a float as two separate FC 06 requests - one for the high word, one for the low word - the device sees the value change in two steps. Depending on the cadence, a fast-scanning device might briefly observe a garbled intermediate value (high word updated, low word still stale) and act on it. FC 16 avoids that entirely: both words land together.

Why both single and multiple write codes exist

The split between FC 05 / FC 15 and FC 06 / FC 16 is the most-asked Modbus design question. Two reasons:

1. Bandwidth, historically. The protocol was designed in 1979 over slow serial links. FC 06 to write one register is 5 bytes of PDU; FC 16 to do the same is 9 bytes minimum (FC + address + quantity + bytecount + data). On a 1200-baud line, the four-byte difference was real. Single-write codes existed so you didn't pay the multi-write overhead for the common case.

2. Atomicity. A multi-write places all the values in the slave's table together, as a single operation. A series of single-writes lets the device's scan see intermediate states. For tightly coupled values (a high word and a low word of a 32-bit float, a Kp / Ki / Kd triple, a date-time block), atomicity matters - and that's what the multi-write codes guarantee.

In practice, device support is uneven. You'll see all four patterns in the field:

  • Devices that support both FC 06 and FC 16 - the cleanest case, you choose.
  • Devices that only support FC 16 - common, especially on power meters and modern controllers. Single writes have to be sent as FC 16 with quantity = 1.
  • Devices that only support FC 06 - rarer, mostly small embedded sensors.
  • Same for the coil side: FC 05-only, FC 15-only, or both.

The safest client behaviour is to try FC 16 first for writes and fall back to FC 06 on Exception 01 (Illegal Function). FC 16 is more universally implemented in modern devices.

Why "everything's a holding register" in practice

A pattern that catches new Modbus engineers off-guard: open the register map of almost any modern power meter, drive, or process instrument, and you'll find the measured values - voltage, current, temperature, frequency, power - sitting in the holding register table (4xxxx), readable via FC 03. Not in the input register table (3xxxx) via FC 04, where the spec arguably intended them to be.

Why? Three reasons that compound:

  1. FC 03 has the most universal client support. Every Modbus master in existence implements FC 03. Some skip FC 04 entirely. A vendor who wants their device to "just work" with the widest range of masters puts everything in holding registers.

  2. It simplifies the device's internal logic. Maintaining two separate tables (one R/O, one R/W) doubles the address-decoding code on a resource-constrained embedded slave. Putting everything in one table is simpler.

  3. The "writeability" is enforced at the register level, not the table level. A vendor can put a measurement value at, say, holding register 40123, and simply have the firmware reject any FC 06 or FC 16 write to that address with Exception 03 (Illegal Data Value) or Exception 04 (Slave Device Failure). The register lives in a R/W table, but the device makes it effectively read-only.

The practical consequence: don't assume "holding register" means "writeable" and "input register" means "measured." Always check the vendor's documented mode column (R/O, R/W, W/O) for the specific register you're after.

Read-only by convention: when a holding register isn't writable

Following on from the previous section, this is the most common protocol-vs-vendor mismatch you'll hit. A register in the holding table - which the spec says is R/W - is implemented by the vendor as read-only. The device exposes it via FC 03 (you can read it), but rejects FC 06 and FC 16 writes.

How the rejection surfaces is vendor-specific:

  • Exception 03 (Illegal Data Value) - some devices return this for writes to register-marked R/O.
  • Exception 04 (Slave Device Failure) - others use 04 as a generic "I can't do that."
  • Exception 02 (Illegal Data Address) - sometimes returned because the device's internal map has the register only registered for reads.
  • Silent success but no change - the worst case. The write returns a normal response, the master thinks it worked, but the device internally discards the value. The next read shows the old value.

The right way to handle this: read the vendor docs for each register's mode column before attempting any write. If the docs are missing or ambiguous, do a controlled write-then-read test on a non-production device. The write either takes (you read back what you wrote), errors (one of the exceptions above), or silently fails (you read back the original value).

The rest of the function codes, briefly

The eight codes above cover most field traffic. The rest exist in the spec but appear rarely:

  • FC 07 - Read Exception Status (legacy Modicon 984): returns 8 status bits from a fixed address. Replaced in practice by reading a holding register at a known location.
  • FC 08 - Diagnostics: subfunctions for serial-line diagnostics (return query data, reset counters, force listen-only). Useful on RTU; effectively unused on TCP.
  • FC 11 / 12 - Get Comm Event Counter / Log (legacy): mostly historical.
  • FC 17 - Report Server ID (legacy): originally returned a string identifying the controller; varies wildly by vendor today.
  • FC 20 / 21 - Read / Write File Record: file-style access to a record-addressed structure. Niche, mostly on legacy Modicon kit.
  • FC 22 - Mask Write Register: read-modify-write of a single holding register with an AND-mask and an OR-mask. Useful for bitfield-style registers but rarely implemented.
  • FC 23 (0x17) - Read/Write Multiple Registers: writes some registers and reads others in a single atomic transaction. Useful when a device needs a write-then-immediately-read pattern (push a command, read the response). Worth knowing about; implemented mainly on higher-end controllers.
  • FC 24 - Read FIFO Queue: pop values from a slave-side FIFO. Niche.
  • FC 43 (0x2B) - Encapsulated Interface Transport: subfunction 14 is the modern "Read Device Identification" call (vendor name, product code, version), worth knowing if you're writing client code that auto-discovers connected devices.

If you ever see one of these in a frame capture, it's worth a quick look at the spec to confirm what's expected; otherwise you can safely treat the eight above as "Modbus in practice."

Frequently asked questions

What's the difference between FC 03 and FC 04 in Modbus?

Both read 16-bit registers, but they target different tables. FC 03 (Read Holding Registers) reads from the read-write holding-register table; FC 04 (Read Input Registers) reads from the read-only input-register table. The two tables are independent memory spaces inside the slave, so "address 0 via FC 03" and "address 0 via FC 04" point at completely different registers. In practice, many devices put their measurements in holding registers anyway and don't implement FC 04 at all, so FC 03 is the universal default.

Why is FC 04 read-only?

Because the protocol doesn't define a write function code that targets the input-register table. Input registers were designed to hold values the device has measured or sensed - data flowing from the field, through the device, to the master - and there's nothing meaningful for the master to write to them. The Modbus designers enforced that by simply not providing a write FC for that table.

Why does Modbus have both FC 06 and FC 16 for writing registers?

History and atomicity. FC 06 (Write Single Register) is more bandwidth-efficient for one-register writes - five bytes of payload versus nine for FC 16. FC 16 (Write Multiple Registers) writes any number of registers in one atomic operation, which matters for tightly coupled values like the two halves of a 32-bit float or a date-time block. Modern device support is uneven: many devices implement FC 16 but not FC 06, so the safe client pattern is to try FC 16 first and fall back to FC 06 if you get Exception 01.

Can I write to a holding register and have it ignored?

Yes - this is the "read-only by convention" pattern. A holding register is read-write at the protocol level, but the vendor can implement it as effectively read-only by rejecting writes (with Exception 03 or 04) or, worse, silently discarding them while returning a successful response. Always check the vendor's mode column (R/O vs R/W) for each register, and verify with a write-then-read test on non-production hardware if you're not sure.

What's the maximum number of registers I can read in one Modbus request?

For FC 03 and FC 04 (read register codes), the spec limit is 125 registers per request - capped by the 253-byte PDU length. For FC 16 (write multiple registers), the limit is 123 registers. For the coil codes, the limits are 2,000 (FC 01, FC 02) and 1,968 (FC 15). Exceeding these limits returns Exception 03 (Illegal Data Value), not Exception 02.

Which function code do I use to read a coil vs a discrete input?

FC 01 (Read Coils) for the read-write coil table; FC 02 (Read Discrete Inputs) for the read-only discrete-input table. Both return 1-bit values packed 8 to a byte, so the response looks similar - the difference is which table you addressed. A common confusion is using FC 01 to read what is actually a discrete input (a physical input from the field). Same address, wrong function code, Exception 02 returned because nothing exists at that address in the coil table.

Decision flowchart: picking the right function code

When you have a piece of data on a device and you need to decide which function code to send, work through the following in order:

The five-second function-code picker
Q1step 1Is the data a single bit or a 16-bit register?Single bit → coil or discrete input (FC 01, 02, 05, 15). Register → holding or input (FC 03, 04, 06, 16).
Q2step 2Are you reading or writing?Reading → see Q3. Writing → see Q4 (writes only target the R/W tables: coils and holding registers).
Q3step 3For a read: is the data sensor-style (came from the field) or master-style (configured / commanded)?Single-bit master command → FC 01. Single-bit field input → FC 02. Register-style measurement → try FC 03 first, fall back to FC 04. Register-style setpoint/config → FC 03.
Q4step 4For a write: single value or multiple values that should land together?Single bit → FC 05. Single register → FC 06 (or FC 16 with qty=1). Atomic multi-write → FC 15 (coils) or FC 16 (registers). Default to the multi codes - they have broader vendor support.
Q5step 5Got Exception 01 (Illegal Function)?The slave doesn't implement that specific FC. Fall back to the most compatible alternative: FC 16 → FC 06, FC 15 → FC 05, FC 04 → FC 03, etc. Check the device's vendor docs for which codes it actually advertises.

Wrap-up

The eight major Modbus function codes carry almost every real-world transaction you'll see on the wire. The split between read-only and read-write tables is intentional - it reflects the difference between data the world sends to the device (which the master has no business overwriting) and data the master controls (which the device shouldn't change unilaterally). The function codes that exist, and the ones that don't, enforce that split at the protocol level.

In practice, vendor conventions blur the lines: holding registers carry measurement data when they shouldn't, FC 04 goes unimplemented on devices that should support it, and individual registers turn out to be read-only despite living in a read-write table. The eight codes above are the universal language; the field-specific quirks live in the vendor's mode column and need to be respected per-device.

We're writing the Modbus Debugging Field Manual right now - a free ebook that covers every major function code in depth, with worked examples from real plant work and the most common vendor implementation quirks. It's not out yet. Join the waitlist and we'll email you the day it ships.

For related topics: Modbus exception codes explained covers what comes back when a function-code request goes wrong, and Modbus byte order explained handles the multi-register decoding that 32-bit values need on top of FC 03 / FC 16.