Modbus function codes explained: a complete installer reference (2026)
Modbus function codes explained for installers. Read holding registers (0x03), write multiple (0x10), exception codes, and the 40001 vs 0x0000 trap.

Modbus function codes tell every slave on the bus what the master wants: read or write, a bit or a 16-bit register, a single address or a block. If you know the codes by heart, you can read a Wireshark trace immediately and identify the cause of an exception response in five seconds. If you do not, every fault wastes an hour.
This article gives you the full function code list, the byte examples you actually see on an Eastron SDM630 or a Schneider PowerLogic meter, and the distinction between function code 0x03 and the legacy "40001" address from older PLC manuals. Aimed at installers, not protocol authors.
Key takeaways
- Modbus has four register types (coils, discrete inputs, holding registers, input registers) and nine widely used function codes that map one-to-one onto them.
- Function code 0x03 (read holding registers) and 0x10 (write multiple registers) together cover roughly 80 percent of traffic in HVAC and refrigeration.
- An exception response carries the same function code as the request, but with bit 7 set to 1: 0x03 becomes 0x83.
What is a Modbus function code, exactly?
A Modbus function code is a one-byte field in every Modbus frame that tells the slave which operation the master wants and which register class it targets. The code sits immediately after the slave address (RTU) or after the MBAP header (TCP), as defined in the Modbus Application Protocol Specification V1.1b3 maintained by the Modbus Organization.
Function codes are not the same as register addresses. The function code says WHAT you do (read or write, single or multiple, bit or word), the register address says WHERE. That distinction is the most-missed detail among installers new to Modbus. For the broader protocol context, see our what is Modbus guide and the Modbus RTU explained article for frame structure.
In practice there are two groups of function codes: public codes (assigned by Modbus.org and documented in the spec), and user-defined codes in the ranges 0x41 to 0x48 and 0x64 to 0x6E that manufacturers may use freely. For 99 percent of HVAC, energy metering and refrigeration work, you only need the first group.
The four register types and their function codes
Modbus organises memory into four register types. Each type has one read code and, if writable, two write codes (single and multiple). Nine function codes map onto this grid:
Coils are 1-bit values you can read and write. Examples: relay state or a digital output. Read Coils uses 0x01, Write Single Coil 0x05, Write Multiple Coils 0x0F.
Discrete inputs are 1-bit values that are read only. Examples: a pressure switch input or a limit switch. Read Discrete Inputs uses 0x02. There is no write code for this type because it is, by definition, an input.
Holding registers are 16-bit values you can read and write. This is where most HVAC and refrigeration work happens: setpoints, configuration values, mode selection. Read Holding Registers uses 0x03, Write Single Holding Register 0x06, Write Multiple 0x10.
Input registers are 16-bit values that are read only. Measurements like temperatures, pressures, power values and energy totals. Read Input Registers uses 0x04. As with discrete inputs, there is no write code.
In practice many manufacturers use only holding registers, even for measurements, because it simplifies the toolchain. An Eastron SDM630 puts both configuration and measurement data in holding registers (0x03), while a Carel pCO refrigeration controller respects the full distinction between holding and input registers.
The nine function codes you actually need
Of the 19 public function codes in the Modbus spec, nine appear daily. The other ten are mainly RTU bus diagnostics (0x07, 0x08, 0x0B, 0x0C, 0x11) or file record operations (0x14, 0x15) you rarely see in modern installations.
| Function code | Name | Application | Frequency |
|---|---|---|---|
| 0x01 | Read Coils | Read digital output states | medium |
| 0x02 | Read Discrete Inputs | Read digital input states | medium |
| 0x03 | Read Holding Registers | Setpoints, configuration, measurements | very high |
| 0x04 | Read Input Registers | Read-only measurements | high |
| 0x05 | Write Single Coil | One relay on or off | medium |
| 0x06 | Write Single Holding Register | One setpoint or configuration value | high |
| 0x0F | Write Multiple Coils | Several relays in one frame | low |
| 0x10 | Write Multiple Holding Registers | Atomic setpoint block writes | high |
| 0x17 | Read/Write Multiple Registers | Read and write in one transaction | low |
Function codes 0x03 and 0x10 are by far the most common. 0x03 because it is the generic "read data from this device" call; 0x10 because all modern integration tools (Home Assistant, Node-RED, PLCs) prefer FC 16, even for a single register, because the programming model is simpler.
Function code 0x03 in detail: reading holding registers
Function code 0x03 is by far the most-used code in any installation. A holding register read request consists of six bytes of data (after the slave address):
| Byte position | Field | Example value |
|---|---|---|
| 1 | Slave address | 0x01 |
| 2 | Function code | 0x03 |
| 3 to 4 | Starting register address | 0x0000 (= register 0) |
| 5 to 6 | Quantity of registers | 0x000A (= 10 registers) |
| 7 to 8 | CRC-16 | (computed over bytes 1 to 6) |
A concrete example: master asks slave 1 for the first 10 holding registers (0 to 9):
01 03 00 00 00 0A C5 CD
The slave responds with the slave address, the same function code, one byte byte count (number of data bytes, 2 times the register count), the register values, and the CRC:
01 03 14 00 64 00 C8 01 2C ... C5 7B
Bytes 1 to 3 are the header: slave address 0x01, function code 0x03, byte count 0x14 (= 20 data bytes, 10 registers x 2). Then 20 data bytes with the values, followed by 2 bytes of CRC.
Exception responses: what 0x83 means in a log
When a slave cannot serve a request, it does not respond with a new function code. It responds with the same function code with bit 7 (the MSB) set to 1. So function code 0x03 (read holding registers) becomes 0x83 in an exception response. 0x10 (write multiple) becomes 0x90. Setting bit 7 means OR with 0x80 (binary 1000 0000).
After the exception function code comes one byte of exception code that tells you WHY the request failed. The full exception response is always five bytes (RTU): slave address, exception function code, exception code, and 2 bytes of CRC. On Modbus TCP it is seven bytes after the MBAP header.
The Modbus Application Protocol Specification V1.1b3, section 7, defines nine standard exception codes:
| Exception code | Name | What it means in practice |
|---|---|---|
| 0x01 | Illegal Function | Slave does not support this FC (try 0x10 instead of 0x06) |
| 0x02 | Illegal Data Address | Address does not exist on this slave (HMI vs wire off-by-one) |
| 0x03 | Illegal Data Value | Value or register count outside the allowed range |
| 0x04 | Slave Device Failure | Hardware issue in slave (sensor dead, memory corrupt) |
| 0x05 | Acknowledge | Slave accepted, query later with poll status |
| 0x06 | Slave Device Busy | Slave in self-test or firmware update, wait and retry |
| 0x08 | Memory Parity Error | File record memory error (rare in HVAC) |
| 0x0A | Gateway Path Unavailable | Modbus gateway cannot reach the target slave (cable loose, slave off) |
| 0x0B | Gateway Target Failed to Respond | Target slave did not respond within timeout (wrong slave ID) |
A concrete exception example: master asks slave 1 for holding register 0x0064 (= 100), but the slave only has 64 registers. The slave responds:
01 83 02 C0 F1
Slave address 0x01, function code 0x83 (= 0x03 + bit 7), exception code 0x02 (illegal data address), CRC 0xC0F1. A logger that shows this as "function code 131" is technically correct, but it is clearer to read it as "FC 0x03, exception code 0x02".
The 40001 trap: 1-based versus 0-based addressing
One of the most stubborn problems in Modbus is the gap between what an HMI or PLC manual says and what actually happens on the wire. Lots of older documentation uses 5-digit addresses with a prefix:
- 0xxxx for coils (e.g. 00001 for the first coil)
- 1xxxx for discrete inputs (10001 for the first)
- 3xxxx for input registers (30001 for the first)
- 4xxxx for holding registers (40001 for the first)
This notation comes from the original Modicon Modbus Protocol Reference Guide PI-MBUS-300 from 1996 and is 1-based: 40001 refers to the FIRST holding register. The Modbus protocol itself uses 0-based addressing on the wire: the first holding register sits at address 0x0000.
When the manufacturer says 40001, you send 0x0000 on the wire. Always subtract 1 from the 4xxxx value before you put it in the request.
The practical rule: if a datasheet says "40001 = mains voltage", set register address 0 (not 1, not 40001) in your polling tool with function code 0x03. An Eastron SDM630 datasheet lists voltage L1 at "30001" (input register notation), which on the wire becomes address 0x0000 with function code 0x04.
FC 06 versus FC 16: which one when?
For writing to holding registers you have two options: FC 06 (write single) or FC 16 (write multiple). A fair question is when to use which:
Use FC 06 when:
- You are writing a single value and the slave supports it
- You want a minimal frame (8 bytes total in RTU)
- You are working with a legacy slave that only supports FC 06 (e.g. older Carel pCO controllers)
Use FC 16 when:
- You want to write several adjacent registers atomically (e.g. setpoint and mode at the same time)
- Your slave only supports FC 16 (e.g. Daikin Altherma)
- You want consistency in your code (always FC 16, even for one register)
The difference is not only the number of registers. FC 06 responds with an echo of the request (same 8 bytes back). FC 16 responds with a shorter frame: slave address, FC, start address, register count, CRC. For a logger that makes the response pattern easier to recognise.
- 1
Confirm which FC the slave supports
Read the datasheet or ask the manufacturer which write codes the device accepts. A device need not implement ALL write codes. Many HVAC devices support only 0x10.
- 2
Set your polling tool to that FC
In modpoll:
-m rtu -f 16for write multiple. In Home Assistant Modbus:write_type: holdingswithcount: 1triggers FC 16 for a single register. In a Python script: use pymodbus'sclient.write_registers(address, values), notwrite_register(address, value). - 3
Verify with a read after the write
Right after the write, send an FC 03 read at the same address and compare the read-back value with what you wrote. If the write succeeds but reads back different, the slave has internal scaling (e.g. factor 10, so writing 250 stores 25.0).
Common field scenarios
For anyone who troubleshoots faults regularly, here are the three function code situations you will see most often:
Scenario 1: a Modbus poller shows "FC 131" or "FC 0x83". That is not an unknown function code, it is an exception response to FC 0x03. Read byte 5 (the exception code) and look it up in the table above. Usually it is 0x02 (illegal data address: the address does not exist) or 0x03 (illegal data value: too many registers requested).
Scenario 2: writing to a holding register does not work. First check: is it actually a holding register and not an input register? Input registers are read only; write attempts return exception 0x01 (illegal function). Second check: does the slave support FC 06 or only FC 16? Switch as needed.
Scenario 3: read always returns zero. Often an address offset problem (see the 40001 section above). Try function code 0x04 (input registers) instead of 0x03. Some manufacturers put measurements in input registers and only configuration in holding registers.
For the broader context of Modbus RTU versus TCP and when to pick which transport layer, see our RTU vs TCP comparison. For specific heat pump scenarios with function codes in an installation context, see heat pump Modbus monitoring.
Frequently asked questions
What is the difference between Modbus function code 0x03 and 0x04?
Function code 0x03 reads holding registers (read/write, 16-bit), function code 0x04 reads input registers (read only, 16-bit). Both return 16-bit values, but holding registers can also be written via 0x06 and 0x10. Many manufacturers choose to use only holding registers, including for measurements, to simplify their toolchain.
How many Modbus function codes are there in total?
The Modbus Application Protocol spec defines 19 public function codes. In addition there are two user-defined ranges (0x41 to 0x48 and 0x64 to 0x6E) for manufacturer-specific functions. In practice, nine codes (0x01 to 0x06, 0x0F, 0x10 and 0x17) cover roughly 99 percent of traffic.
How do I read a Modbus exception code?
An exception response consists of the slave address, the original function code with bit 7 set (e.g. 0x83 for an FC 0x03 exception), and one byte of exception code. The nine standard exception codes run from 0x01 (illegal function) to 0x0B (gateway target failed to respond). Read byte 3 (after slave address and exception FC) and look up the meaning.
Why does my write to register 40001 not work?
The 4xxxx notation is 1-based and the protocol addressing on the wire is 0-based. If you send 40001 on the wire, you are addressing holding register 40000, which usually does not exist. The correct mapping is 40001 in the HMI equals 0x0000 on the wire. Always subtract 1 before putting it into your tool, or let the tool do the conversion.
What is the maximum number of registers in a Modbus request?
For function code 0x03 (read holding registers) the maximum is 125 registers per request. For 0x10 (write multiple registers) the maximum is 123 registers. For 0x01 and 0x02 (read coils and discrete inputs) the maximum is 2000 bits per request. Ask for more and you get exception code 0x03 (illegal data value) back.
Next steps
Function codes are the working language of Modbus: read them and you read the protocol. Combine this knowledge with the Modbus RTU frame structure to dissect a trace byte by byte, and with the Modbus TCP frame structure for the modern IP transport variant.
Do not want to hand-write these function codes in a script but see the data straight in the cloud? The ModbusCloud Gateway does the polling for you and exposes the registers via a dashboard plus alerts. Book a short demo to see how that plays out across your fleet.
Ready to get started?
Order the ModbusCloud Gateway and start monitoring your installations within 5 minutes.
View the gatewayReady to get started?
Order the ModbusCloud Gateway and start monitoring your installations within 5 minutes.
View the gateway