Advisory Schedule a Technical Discovery Call — Book your session today! »

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

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.

AreaObject TypeAccessAddressTypical Use
CoilsBitR/W00001 - 09999Digital Outputs (Relays, Contactors)
Discrete InputsBitRead Only10001 - 19999Digital Inputs (Sensors, Buttons)
Input Registers16-bit WordRead Only30001 - 39999Analog Measurements (ADC, Temperature)
Holding Registers16-bit WordR/W40001 - 49999Setpoints, Configuration, R/W Values

Critical Note: On the wire (the protocol itself), addresses are Zero-Based.

  • Holding Register 40001 is address 0.
  • Holding Register 40101 is address 100.

Libraries like pymodbus and libmodbus use 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, ON is sent as 0xFF00 and OFF as 0x0000. This prevents accidental activation by noise (random noise rarely equals exactly 0xFF).

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 0x06 writes, it will read corrupt garbage. Always use 0x10 for 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.

CodeNameReal Meaning
01Illegal FunctionYou asked for something it can’t do (e.g., writing to an Input Register).
02Illegal AddressThe address does not exist in the device’s memory map.
03Illegal Data ValueThe value is out of range or the requested quantity is invalid.
04Server FailureThe slave failed internally (hardware error, etc.).
06Server 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

  1. Always use 0x10 (Write Multiple) for complex numeric values (Floats, Int32) to ensure atomicity.
  2. Handle exceptions, not just timeouts. An Illegal Address requires code correction; a Timeout requires a retry.
  3. 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.
  4. Watch out for Endianness. If you read “weird” but consistent values, you likely have swapped bytes or words (ABCD vs CDAB).
  5. 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.

Back to Blog

Related Posts

View All Posts »