· Eduardo Vieira · Industrial Protocols · 5 min read
Modbus Function Codes: The Definitive Guide (0x01 to 0x17)
Master Modbus function codes with real-world examples in C (libmodbus) and Python (pymodbus). A complete guide to reading coils, inputs, and registers without errors.

If you’ve ever stared at a packet analyzer wondering why your PLC is returning a 0x83, this guide is for you.
Modbus function codes are the core of the protocol. They define what action the server (slave) must perform. Many tutorials only cover 0x03 (Read Holding Registers), but in the real industry, you need to master the full spectrum to interact with remote I/O, VFDs (Variable Frequency Drives), and complex sensors.
In this definitive guide, we will break down all standard function codes, with bit-by-bit frame diagrams and production code in C (libmodbus) and Python (pymodbus).
The Modbus Data Model: The 4 Tables
Before coding, understand where you stand. Modbus divides memory into 4 distinct areas. Confusing them is the #1 cause of “Illegal Data Address” errors.
| Area | Object Type | Access | Address | Typical Use |
|---|---|---|---|---|
| Coils | Bit | R/W | 00001 - 09999 | Digital Outputs (Relays, Contactors) |
| Discrete Inputs | Bit | Read Only | 10001 - 19999 | Digital Inputs (Sensors, Buttons) |
| Input Registers | 16-bit Word | Read Only | 30001 - 39999 | Analog Measurements (ADC, Temperature) |
| Holding Registers | 16-bit Word | R/W | 40001 - 49999 | Setpoints, Configuration, R/W Values |
Critical Note: On the wire (the protocol itself), addresses are Zero-Based.
- Holding Register
40001is address0.- Holding Register
40101is address100.Libraries like
pymodbusandlibmodbususe Zero-Based addressing (0-65535).
PDU Frame Structure
The PDU (Protocol Data Unit) is the part of the message that is independent of the physical medium (TCP or Serial).
+---------------+------------------------+
| Function Code | Data |
+---------------+------------------------+
| 1 Byte | N Bytes |
+---------------+------------------------+- Range: 0x01 to 0x7F (1-127).
- Exception: If the server rejects the request, it returns the function code with the MSB set to 1 (e.g.,
0x03$\to$0x83).
1. Bit Access Functions (Read)
0x01: Read Coils
Reads the ON/OFF status of discrete digital outputs.
- Request:
[Func Code] [Start Addr HI] [Start Addr LO] [Quantity HI] [Quantity LO] - Response:
[Func Code] [Byte Count] [Coil Status 1] ... [Coil Status N]
Python (pymodbus)
# Read digital outputs 0 to 7
rr = client.read_coils(address=0, count=8, slave=1)
if not rr.isError():
print(f"Outputs: {rr.bits}")
else:
print(f"Error: {rr}")C (libmodbus)
uint8_t bits[8];
int rc = modbus_read_bits(ctx, 0, 8, bits);
if (rc == -1) {
fprintf(stderr, "%s\n", modbus_strerror(errno));
}0x02: Read Discrete Inputs
Same as 0x01, but for reading physical inputs (sensors, limit switches) that cannot be written to. Often ignored, but vital for safety status monitoring.
Python (pymodbus)
# Read sensors on input pins
rr = client.read_discrete_inputs(address=0, count=16, slave=1)C (libmodbus)
uint8_t inputs[16];
// Note the function "input_bits"
int rc = modbus_read_input_bits(ctx, 0, 16, inputs);2. Word Access Functions (Read)
0x03: Read Holding Registers
The “Swiss Army Knife” of Modbus. Reads 16-bit read/write registers. Used for everything: configurations, analog values, counters.
- Request:
03 [Addr Hi] [Addr Lo] [Qty Hi] [Qty Lo] - Response:
03 [Byte Count] [Val Hi] [Val Lo] ...
Python (pymodbus) - Type Handling
Modbus only moves bits. pymodbus helps us decode them into real types (float, int32).
# Read 2 registers (4 bytes) for a 32-bit Float
rr = client.read_holding_registers(address=100, count=2, slave=1)
if not rr.isError():
# Decode float (IEEE 754)
decoder = BinaryPayloadDecoder.fromRegisters(
rr.registers,
byteorder=Endian.Big,
wordorder=Endian.Big
)
temperature = decoder.decode_32bit_float()C (libmodbus)
uint16_t tab_reg[2];
int rc = modbus_read_registers(ctx, 100, 2, tab_reg);
if (rc != -1) {
// Manually convert to float
float temp = modbus_get_float_abcd(tab_reg);
printf("Temp: %.2f\n", temp);
}0x04: Read Input Registers
For reading read-only analog data (ADC measurements, calibrated sensor values). Many devices map everything to Holding Registers, but strict implementations use Input Registers for measurements.
Python (pymodbus)
rr = client.read_input_registers(address=0, count=1, slave=1)C (libmodbus)
uint16_t val;
modbus_read_input_registers(ctx, 0, 1, &val);3. Single Write Functions
0x05: Write Single Coil
Forces an output to ON or OFF.
- Fun Fact: In the protocol,
ONis sent as0xFF00andOFFas0x0000. This prevents accidental activation by noise (random noise rarely equals exactly0xFF).
Python
client.write_coil(address=5, value=True, slave=1)C (libmodbus)
modbus_write_bit(ctx, 5, TRUE);0x06: Write Single Register
Writes a single 16-bit register.
⚠️ DANGER: Do not use this to write 32-bit values (like Floats or Longs). It breaks atomicity. If your process reads the value between the two
0x06writes, it will read corrupt garbage. Always use0x10for data > 16 bits.
Python
client.write_register(address=200, value=1234, slave=1)C (libmodbus)
modbus_write_register(ctx, 200, 1234);4. Multiple Write Functions (The Ones You Should Use!)
0x0F: Write Multiple Coils
Sets an entire bank of outputs in a single atomic transaction. Much more efficient than calling write_coil in a loop.
Python
states = [True, False, True, True, False]
client.write_coils(address=0, values=states, slave=1)C (libmodbus)
uint8_t bits[] = {1, 0, 1, 1, 0};
modbus_write_bits(ctx, 0, 5, bits);0x10: Write Multiple Registers
The industrial standard for writing parameters. Allows writing entire blocks of configuration or complex values (Floats, Strings) atomically.
Python
# Write a Float (2 registers)
builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big)
builder.add_32bit_float(23.5)
payload = builder.to_registers()
client.write_registers(address=400, values=payload, slave=1)C (libmodbus)
float setpoint = 23.5;
uint16_t raw[2];
modbus_set_float_abcd(setpoint, raw);
modbus_write_registers(ctx, 400, 2, raw);0x17: Read/Write Multiple Registers
The hidden gem. Allows writing a block and reading another in a single transaction. Ideal for high-speed synchronization where you want to update setpoints and receive current status without round-trip latency.
Python (pymodbus)
# Write 2 registers at addr 100, Read 2 from addr 200
rr = client.read_write_multiple_registers(
read_address=200, read_count=2,
write_address=100, write_registers=[10, 20],
slave=1
)C (libmodbus)
uint16_t write_cols[] = {10, 20};
uint16_t read_cols[2];
modbus_write_and_read_registers(ctx, 100, 2, write_cols, 200, 2, read_cols);Modbus Exception Codes
When something fails, the slave doesn’t stay silent (nor should it). It responds with an error code. If your code doesn’t handle this, you are programming blind.
| Code | Name | Real Meaning |
|---|---|---|
| 01 | Illegal Function | You asked for something it can’t do (e.g., writing to an Input Register). |
| 02 | Illegal Address | The address does not exist in the device’s memory map. |
| 03 | Illegal Data Value | The value is out of range or the requested quantity is invalid. |
| 04 | Server Failure | The slave failed internally (hardware error, etc.). |
| 06 | Server Busy | ”Hold on, I’m busy.” Retry later. |
Handling in Python
from pymodbus.pdu import ExceptionResponse
rr = client.read_holding_registers(0, 10, slave=1)
if rr.isError():
if isinstance(rr, ExceptionResponse):
print(f"Modbus Exception: {rr.exception_code}")
else:
print("Communication Error (Timeout/Connection)")Summary and Best Practices
- Always use 0x10 (Write Multiple) for complex numeric values (Floats, Int32) to ensure atomicity.
- Handle exceptions, not just timeouts. An
Illegal Addressrequires code correction; aTimeoutrequires a retry. - Group reads. It is better to read 100 continuous registers at once (Function 0x03) than to make 100 requests for 1 register. Modbus is slow due to latency, not bandwidth.
- Watch out for Endianness. If you read “weird” but consistent values, you likely have swapped bytes or words (
ABCDvsCDAB). - Use Zero-Based Addressing. Remember that 40001 is address 0.
Mastering these codes distinguishes a programmer who guesses from an integrator who understands exactly what’s happening on the wire.



