Modbus Function Codes Explained — FC01 to FC23 Complete Reference (2026)
Every Modbus transaction hinges on one byte: the function code. Get it wrong and you get an exception. Understand it and you can debug any Modbus issue in seconds. This reference covers every function code you'll encounter in real PLC and SCADA work — with exact frame formats, byte-level examples, and the constraints most documentation leaves out.
How Function Codes Work
A Modbus PDU (Protocol Data Unit) consists of exactly two parts: a 1-byte function code followed by the function-specific data. That's it. The function code tells the slave what to do; the data provides the parameters.
Modbus PDU structure:
┌──────────────────┬─────────────────────────────┐
│ Function Code │ Data │
│ (1 byte) │ (0–252 bytes) │
└──────────────────┴─────────────────────────────┘
The full Modbus frame adds framing around this PDU depending on the transport:
Modbus RTU frame:
┌───────────┬──────────────────┬──────────┬──────────┐
│ Unit ID │ Function Code │ Data │ CRC-16 │
│ (1 byte) │ (1 byte) │ (N bytes)│ (2 bytes)│
└───────────┴──────────────────┴──────────┴──────────┘
Modbus TCP frame (MBAP header + PDU):
┌──────────────┬──────────────┬────────┬───────────┬──────────────────┬──────────┐
│ Trans. ID │ Protocol ID │ Length │ Unit ID │ Function Code │ Data │
│ (2 bytes) │ (2 bytes) │(2 bytes)│ (1 byte) │ (1 byte) │ (N bytes)│
└──────────────┴──────────────┴────────┴───────────┴──────────────────┴──────────┘
When a slave successfully processes a request, its response contains the same function code. When it can't process the request, it returns the function code with the most significant bit set (FC | 0x80), followed by an exception code byte. So FC03 (0x03) becomes 0x83 in an error response.
Function codes are grouped by operation type. The codes you'll use in 95% of real work:
- FC01, FC02 — Read bit values (coils, discrete inputs)
- FC03, FC04 — Read 16-bit register values
- FC05, FC06 — Write single bit / register
- FC15, FC16 — Write multiple bits / registers
- FC23 — Read and write in one transaction
FC01 & FC02 — Read Coils and Discrete Inputs
FC01 — Read Coils
Reads 1 to 2000 coils (1-bit read/write values) starting at a specified address. Coils map to the 0x address space (Modbus addresses 00001–09999, PDU addresses 0x0000–0x270E).
Request:
Byte 0: Function Code = 0x01
Byte 1-2: Starting Address (big-endian)
Byte 3-4: Quantity of Coils (1–2000)
Example — read 10 coils starting at address 0x0013 (coil 20):
01 00 13 00 0A
Response:
Byte 0: Function Code = 0x01
Byte 1: Byte Count = ceil(N / 8)
Byte 2+: Coil values, LSB of first byte = first coil
Example response (10 coils, 2 data bytes):
01 02 CD 01
Binary breakdown:
Byte 0xCD = 11001101 → Coils 1–8: ON OFF ON ON OFF OFF ON ON
Byte 0x01 = 00000001 → Coils 9–10: ON OFF (upper 6 bits unused)
FC02 — Read Discrete Inputs
Identical frame format to FC01, but targets the 1x address space (discrete inputs, 10001–19999). Discrete inputs are read-only — sourced from physical input terminals on the device. You cannot write to them with any function code.
Example — read 8 discrete inputs starting at address 100:
02 00 64 00 08
Response — 8 inputs packed in 1 byte:
02 01 AC (0xAC = 10101100)
Practical note: FC02 is less common than FC01. Many devices combine coils and discrete inputs into the same address space and only implement FC01. If you get exception code 0x01 (Illegal Function) on FC02, the device doesn't support it — try FC01.
FC03 & FC04 — Read Holding and Input Registers
FC03 — Read Holding Registers
Reads 1 to 125 holding registers (16-bit read/write values) from the 4x address space (40001–49999, PDU addresses 0x0000–0xFFFF). This is the most-used function code in industrial Modbus — energy meters, VFDs, PLCs, sensors — almost everything uses FC03 for its primary data.
Request:
Byte 0: Function Code = 0x03
Byte 1-2: Starting Address (big-endian)
Byte 3-4: Quantity of Registers (1–125)
Example — read 3 holding registers starting at address 40 (PDU address 0x0028):
03 00 28 00 03
Response:
Byte 0: Function Code = 0x03
Byte 1: Byte Count = N × 2
Byte 2+: Register values, 2 bytes each, big-endian
Example response (3 registers: values 1234, 5678, 9012):
03 06 04 D2 16 2E 23 34
Register 1: 0x04D2 = 1234
Register 2: 0x162E = 5678
Register 3: 0x2334 = 9012
The 125-register limit: This comes from the PDU size limit (253 bytes max data). 125 registers × 2 bytes = 250 bytes + 2 bytes overhead = 252 bytes. If you try to request 126 or more, a compliant slave will return exception code 0x03 (Illegal Data Value).
FC04 — Read Input Registers
Same frame format as FC03, but targets the 3x address space (30001–39999). Input registers are read-only — typically wired to ADC inputs or calculated by firmware. The spec treats them as immutable from the master side.
Example — read 2 input registers starting at address 100:
04 00 64 00 02
Response:
04 04 01 F4 03 E8 (values: 500 and 1000)
FC03 vs FC04 in practice: Most modern devices only implement FC03 and expose all data (including analog inputs) as holding registers. If FC04 returns exception 0x01, use FC03 instead. The address map in the device manual always clarifies which function code applies to which register.
FC05 & FC06 — Write Single Coil / Register
FC05 — Write Single Coil
Writes a single coil ON or OFF. The value field must be exactly 0xFF00 (ON) or 0x0000 (OFF). Any other value returns exception 0x03.
Request — write coil at address 0x00AC to ON:
05 00 AC FF 00
Request — write the same coil OFF:
05 00 AC 00 00
Response (echo of request on success):
05 00 AC FF 00
The response is an echo of the request. This means you can confirm the write was accepted without a separate read.
FC06 — Write Single Register
Writes a single 16-bit holding register. The value can be any 16-bit integer (0x0000–0xFFFF).
Request — write value 0x03E8 (1000) to register address 0x0001:
06 00 01 03 E8
Response (echo):
06 00 01 03 E8
When FC06 is not enough: If you need to write a 32-bit float or integer, you need two consecutive registers written atomically. FC06 can only write one register — if you call it twice, there's a window between writes where another master could read a split value. Use FC16 instead.
FC15 & FC16 — Write Multiple Coils / Registers
FC15 — Write Multiple Coils
Writes 1 to 1968 coils starting at a specified address. Coil values are packed into bytes, LSB first.
Request — write 10 coils starting at address 0x0013, values: ON OFF ON ON OFF OFF ON ON ON OFF:
0F 00 13 00 0A 02 CD 01
Byte breakdown:
0F = FC15
00 13 = Start address 19
00 0A = Quantity: 10 coils
02 = Byte count: 2 bytes (ceil(10/8))
CD 01 = Coil values:
0xCD = 11001101 → coils 1–8
0x01 = 00000001 → coils 9–10 (upper 6 bits ignored)
Response (success — address and quantity echo, not coil values):
0F 00 13 00 0A
FC16 — Write Multiple Registers
Writes 1 to 123 holding registers in a single transaction. This is the correct way to write 32-bit values, floating point data, or any multi-register structure where atomicity matters.
Request — write 2 registers starting at address 0x0001, values 0x000A and 0x0102:
10 00 01 00 02 04 00 0A 01 02
Byte breakdown:
10 = FC16
00 01 = Start address 1
00 02 = Quantity: 2 registers
04 = Byte count: 4 bytes (2 registers × 2 bytes)
00 0A = Register 1 value: 10
01 02 = Register 2 value: 258
Response (address and quantity, not values):
10 00 01 00 02
Writing a 32-bit IEEE 754 float with FC16:
Value: 123.45 → IEEE 754 hex: 0x42F6E666
Register 1 (high word): 0x42F6
Register 2 (low word): 0xE666
FC16 request (starting at register 100, PDU address 99 = 0x0063):
10 00 63 00 02 04 42 F6 E6 66
Whether the high word or low word goes first depends on the device — check the manual for byte order (big-endian vs little-endian, and word order). This is one of the most common sources of incorrect readings when integrating third-party devices.
FC23 — Read/Write Multiple Registers
FC23 performs a write and a read in a single Modbus transaction. The master specifies a read range and a write range. The slave executes both atomically and returns the read data.
Request structure:
Byte 0: Function Code = 0x17
Byte 1-2: Read Starting Address
Byte 3-4: Quantity to Read (1–125)
Byte 5-6: Write Starting Address
Byte 7-8: Quantity to Write (1–121)
Byte 9: Write Byte Count = Quantity to Write × 2
Byte 10+: Write Values
Example — read 2 registers starting at address 3, write 2 registers starting at address 14:
17 00 03 00 02 00 0E 00 02 04 00 FF 00 FF
Response (read data only):
17 04 00 FE 09 02 (2 registers read: 0x00FE, 0x0902)
FC23 is useful in closed-loop control where you need to write a new setpoint and immediately read back the current process value — done in one round trip instead of two. Not all devices support FC23; test with your target device or simulator first.
FC22 — Mask Write Register
Modifies specific bits in a holding register without a read-modify-write cycle. The request contains an AND mask and an OR mask. The slave applies them: result = (current AND and_mask) OR (or_mask AND NOT and_mask).
Request — at register 4, set bits 1 and 4 to ON without affecting other bits:
16 00 04 FF ED 00 12
Byte breakdown:
16 = FC22
00 04 = Register address 4
FF ED = AND mask: 0xFFED (bits 1,4 cleared = allow OR mask to set them)
00 12 = OR mask: 0x0012 (bits 1,4 set)
FC22 is rarely seen in the field but becomes essential when a device shares status and control bits in the same register — for example, bit 0 = enable, bits 1–3 = mode, bit 4 = reset. Writing the whole register with FC06 risks corrupting the status bits.
FC08 — Diagnostics (Serial Line Only)
FC08 is a serial-only function code for testing the communication link and retrieving counters. It's not available over Modbus TCP (the TCP transport doesn't need it — TCP has its own error detection).
FC08 uses sub-function codes to select the specific diagnostic operation:
| Sub-Function | Name | Use |
|---|---|---|
| 0x0000 | Return Query Data | Loopback test — slave echoes back the request data |
| 0x000A | Clear Counters | Resets all diagnostic counters to zero |
| 0x000B | Return Bus Message Count | Total messages received on the bus |
| 0x000C | Return Bus Comm Error Count | CRC errors detected |
| 0x000E | Return Slave Message Count | Messages addressed to this slave |
| 0x000F | Return Slave No Response Count | Requests that got no response |
Loopback test request (sub-function 0x0000, data 0xA537):
08 00 00 A5 37
Expected response (echo):
08 00 00 A5 37
In the field, FC08 subfunction 0x0000 (loopback) is a clean way to verify a serial device is alive without reading any registers — useful when you don't know the register map yet.
Exception Responses
When a slave can't execute a request, it returns an exception response instead of normal data. Recognizing these is critical for debugging.
Exception response structure:
Byte 0: Function Code | 0x80 (e.g., FC03 = 0x03 → exception = 0x83)
Byte 1: Exception Code
Example — FC03 request to invalid address returns:
83 02 (FC03 exception, code 0x02 = Illegal Data Address)
Exception Code Reference
| Code | Name | Meaning |
|---|---|---|
| 0x01 | Illegal Function | The device doesn't implement this function code |
| 0x02 | Illegal Data Address | Starting address + quantity exceeds the device's register range |
| 0x03 | Illegal Data Value | Value in the data field is outside allowed range (e.g., FC05 with value 0x0100) |
| 0x04 | Slave Device Failure | Unrecoverable error on the slave — hardware fault, firmware bug |
| 0x05 | Acknowledge | Slave accepted the request but needs time — send again later |
| 0x06 | Slave Device Busy | Slave is processing a long-duration operation |
| 0x08 | Memory Parity Error | Slave detected a parity error in extended memory |
| 0x0A | Gateway Path Unavailable | Modbus gateway couldn't reach the target device |
| 0x0B | Gateway Target No Response | Device connected through a gateway didn't respond |
0x02 is the most common exception you'll encounter. It almost always means the register address in your request is outside the range implemented by the device — either you're off by one (forgot that PDU addresses are 0-based while Modbus addresses are 1-based) or you're reading beyond the device's defined register map.
0x01 is the second most common. It means the device doesn't support that function code at all. Many simple sensors only implement FC03 and FC06. If you try FC04, you'll get 0x01 back.
Quick Reference Table
| FC | Hex | Name | Data Type | Direction | Max per Request |
|---|---|---|---|---|---|
| 01 | 0x01 | Read Coils | Bit (0x) | Read | 2000 coils |
| 02 | 0x02 | Read Discrete Inputs | Bit (1x) | Read | 2000 inputs |
| 03 | 0x03 | Read Holding Registers | 16-bit (4x) | Read | 125 registers |
| 04 | 0x04 | Read Input Registers | 16-bit (3x) | Read | 125 registers |
| 05 | 0x05 | Write Single Coil | Bit (0x) | Write | 1 coil |
| 06 | 0x06 | Write Single Register | 16-bit (4x) | Write | 1 register |
| 08 | 0x08 | Diagnostics | — | Loopback | Serial only |
| 15 | 0x0F | Write Multiple Coils | Bit (0x) | Write | 1968 coils |
| 16 | 0x10 | Write Multiple Registers | 16-bit (4x) | Write | 123 registers |
| 22 | 0x16 | Mask Write Register | 16-bit (4x) | R/W | 1 register |
| 23 | 0x17 | Read/Write Multiple Regs | 16-bit (4x) | R/W | Read 125 / Write 121 |
| 24 | 0x18 | Read FIFO Queue | 16-bit (4x) | Read | 31 registers |
| 43 | 0x2B | Read Device Identification | — | Read | Device info |
Testing Function Codes Without Hardware
Understanding function codes in theory is one thing. Seeing exact bytes in a real request/response is what builds real debugging instinct. The fastest way to do that is with a Modbus simulator that acts as a slave device and responds to every function code.
With ModbusSimulator, you can:
- Configure holding registers (FC03), input registers (FC04), coils (FC01/FC05/FC15), and discrete inputs (FC02) independently
- Set register values and watch them update in real time as your master application polls
- Trigger exception responses by requesting addresses outside your configured range — test your master's exception handling
- Test FC16 and FC23 write operations and verify the data lands in the correct registers
- Run over Modbus TCP (port 502) or Modbus RTU via virtual COM port — the same simulator handles both
This matters because function code bugs are subtle. A master that reads 40 registers at once (FC03) might work fine with a real PLC but fail with a sensor that only supports 20 registers per request. You need to test the edge cases before they appear on a live plant floor.
Test Every Modbus Function Code
ModbusSimulator responds to FC01 through FC23. Configure registers, trigger exceptions, test your master application — without touching real hardware.
Download Free TrialFor more on how register types map to function codes, see Modbus Register Types Explained. For troubleshooting when function codes return unexpected exceptions, see Modbus Troubleshooting: Common Errors and Fixes.
If you're working with RTU over serial, pay attention to the framing requirements. The RTU spec requires a 3.5-character silent gap before and after each frame — violations cause CRC errors that look like data corruption but are really timing issues. See Modbus RTU Simulator Setup Guide for how to configure timing parameters correctly.
Frequently Asked Questions
What is a Modbus function code?
A Modbus function code is a 1-byte value in every Modbus request that tells the slave device what operation to perform — read coils, write holding registers, read input registers, etc. The slave echoes the same function code in its response if successful, or returns the function code ORed with 0x80 (adding 128) to signal an exception.
What is the difference between FC03 and FC04?
FC03 reads Holding Registers (address range 40001–49999), which are read/write. FC04 reads Input Registers (address range 30001–39999), which are read-only — typically sourced directly from hardware sensors or analog inputs. In practice, many devices only implement FC03 and map everything as holding registers.
What is the maximum number of registers I can read in one FC03 request?
125 holding registers per request. This is constrained by the PDU size limit of 253 bytes: 125 registers × 2 bytes = 250 bytes of data, plus 1 byte function code and 1 byte byte count = 252 bytes, within limit. For FC01 (coils), the limit is 2000 coils per request.
How do exception codes work in Modbus?
When a Modbus slave cannot process a request, it returns an exception response. The function code byte is set to the original FC | 0x80 (e.g., FC03 → 0x83). The next byte is the exception code: 0x01 (Illegal Function), 0x02 (Illegal Data Address), 0x03 (Illegal Data Value), 0x04 (Slave Device Failure). Exception code 0x02 is the most common — it means you requested a register address the device doesn't have.
What is FC16 and when should I use it instead of FC06?
FC06 writes a single holding register (2 bytes). FC16 writes multiple holding registers in one request (up to 123 registers). Use FC16 whenever you need to write more than one register atomically — writing a 32-bit float requires two consecutive registers that must be written together. Writing them individually with FC06 risks reading split values mid-write.
Does FC15 write individual bits or whole bytes?
FC15 (Write Multiple Coils) writes individual bit values. The coil values are packed into bytes: the first coil maps to bit 0 of the first data byte, the second coil to bit 1, and so on. Even if you're writing 3 coils, the data occupies 1 full byte with the upper 5 bits set to zero.
What is FC23 (Read/Write Multiple Registers) used for?
FC23 combines a write operation and a read operation in a single Modbus transaction. It's used in high-speed polling scenarios to reduce round-trip latency — write a setpoint and immediately read back the current process value in one shot.
Can I test all Modbus function codes without real hardware?
Yes. A Modbus simulator like ModbusSimulator acts as a fully compliant slave device that responds to FC01 through FC23. You can test every request format, trigger exception responses by requesting out-of-range addresses, and verify your master application handles all edge cases without needing physical hardware.