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-FunctionNameUse
0x0000Return Query DataLoopback test — slave echoes back the request data
0x000AClear CountersResets all diagnostic counters to zero
0x000BReturn Bus Message CountTotal messages received on the bus
0x000CReturn Bus Comm Error CountCRC errors detected
0x000EReturn Slave Message CountMessages addressed to this slave
0x000FReturn Slave No Response CountRequests 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

CodeNameMeaning
0x01Illegal FunctionThe device doesn't implement this function code
0x02Illegal Data AddressStarting address + quantity exceeds the device's register range
0x03Illegal Data ValueValue in the data field is outside allowed range (e.g., FC05 with value 0x0100)
0x04Slave Device FailureUnrecoverable error on the slave — hardware fault, firmware bug
0x05AcknowledgeSlave accepted the request but needs time — send again later
0x06Slave Device BusySlave is processing a long-duration operation
0x08Memory Parity ErrorSlave detected a parity error in extended memory
0x0AGateway Path UnavailableModbus gateway couldn't reach the target device
0x0BGateway Target No ResponseDevice 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

FCHexNameData TypeDirectionMax per Request
010x01Read CoilsBit (0x)Read2000 coils
020x02Read Discrete InputsBit (1x)Read2000 inputs
030x03Read Holding Registers16-bit (4x)Read125 registers
040x04Read Input Registers16-bit (3x)Read125 registers
050x05Write Single CoilBit (0x)Write1 coil
060x06Write Single Register16-bit (4x)Write1 register
080x08DiagnosticsLoopbackSerial only
150x0FWrite Multiple CoilsBit (0x)Write1968 coils
160x10Write Multiple Registers16-bit (4x)Write123 registers
220x16Mask Write Register16-bit (4x)R/W1 register
230x17Read/Write Multiple Regs16-bit (4x)R/WRead 125 / Write 121
240x18Read FIFO Queue16-bit (4x)Read31 registers
430x2BRead Device IdentificationReadDevice 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 Trial

For 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.