Modbus Function Code 03 (Read Holding Registers): Protocol Specification & Hex Analysis

An exhaustive engineering reference for Modbus Function Code 03 (0x03). Learn the exact protocol request and response structures, Modbus RTU vs TCP frame formats, 1-based vs 0-based register mapping, 32-bit floating point parsing, and practical Python testing workflows.

In industrial automation, supervisory control and data acquisition (SCADA), and programmable logic controller (PLC) engineering, Modbus Function Code 03 (Read Holding Registers / 0x03) is undeniably the most frequently transmitted message across factory floors, utility substations, and building management networks. Whether you are interrogating an energy meter for total active power, polling an analog pressure transmitter, or reading operational speed setpoints from a variable frequency drive (VFD), Function Code 03 is the primary vehicle for retrieving 16-bit analog data.

Despite its ubiquitous deployment since the original Modicon protocol definition in 1979, engineers and software developers regularly encounter integration headaches: 1-off register addressing offsets (40001 vs 0x0000), endianness byte-ordering mismatches on 32-bit floating point numbers, and unhandled exception responses.

This guide provides an exhaustive technical analysis of Function Code 03 under the official Modbus Application Protocol Specification V1.1b3. We examine the exact request and response byte formats on both Modbus RTU and Modbus TCP, examine packet hex captures, analyze IEEE 754 float conversion, and demonstrate how to simulate holding register tables using ModbusSimulator and Python.

1. Protocol Fundamentals: Holding Registers vs Other Data Tables

The Modbus data model categorizes memory into four distinct primary tables. Each table represents a specific combination of bit-level or word-level data, with either read-only or read/write access:

Data Table Type Object Type Access Level Classic Modicon Address Standard Read Function Code
Discretes Input Single bit Read-Only 10001 – 19999 (1xxxx) FC 02 (Read Discrete Inputs / 0x02)
Coils Single bit Read-Write 00001 – 09999 (0xxxx) FC 01 (Read Coils / 0x01)
Input Registers 16-bit word Read-Only 30001 – 39999 (3xxxx) FC 04 (Read Input Registers / 0x04)
Holding Registers 16-bit word Read-Write 40001 – 49999 (4xxxx) FC 03 (Read Holding Registers / 0x03)

Holding registers are 16-bit words (2 bytes, 65,536 discrete states or unsigned values from 0 to 65,535; signed -32,768 to +32,767). Because holding registers support both read and write operations (via FC 06 Write Single Register and FC 16 Write Multiple Registers), modern smart instrumentation commonly places both live process measurements and configurable operational parameters in the holding register space.

2. Addressing Schemes: Modicon 40001 vs Protocol Wire Address 0x0000

The single most widespread source of confusion in Modbus field integration is the difference between human-facing PLC addressing and binary protocol wire addressing.

  • Modicon 1-Based Notation: Traditional PLCs (Schneider Modicon, GE, early Allen-Bradley integrations) assign the prefix "4" to designate holding registers. In user documentation, the first register is labelled 40001 (or 400001 in extended 6-digit notation).
  • Protocol Wire Addressing (0-Based Offset): In the actual binary packet transmitted across the RS-485 serial bus or TCP/IP socket, the starting address is an unsigned 16-bit integer ranging from 0x0000 to 0xFFFF (0 to 65,535). Protocol address 0x0000 represents the first physical holding register.
PLC Documentation Address:  40001  -->  Wire Protocol Hex Offset: 0x0000 (0 decimal)
PLC Documentation Address:  40002  -->  Wire Protocol Hex Offset: 0x0001 (1 decimal)
PLC Documentation Address:  40100  -->  Wire Protocol Hex Offset: 0x0063 (99 decimal)
PLC Documentation Address:  41001  -->  Wire Protocol Hex Offset: 0x03E8 (1000 decimal)

If a field vendor's user manual instructs you to read "Holding Register 40050", your Modbus master software (such as ModbusSimulator or SCADA driver) must query starting address 49 (0x0031). If you enter 50 directly, you will read register 40051, causing an off-by-one data offset error or an Exception Code 02 (Illegal Data Address) if you exceed the end of the register table.

3. Function Code 03 Request and Response PDU Format

The Modbus Protocol Data Unit (PDU) is independent of the underlying communication layer (Serial RTU, Serial ASCII, or Ethernet TCP). The PDU consists strictly of the Function Code and Function-specific Data payload.

Request PDU Structure

A master initiates Function Code 03 by specifying the 16-bit starting address and the 16-bit quantity of consecutive registers to read:

Field Name Length Hex Value / Range Description
Function Code 1 Byte 0x03 Read Holding Registers function code
Starting Address 2 Bytes 0x0000 to 0xFFFF 0-based register offset (Hi Byte, Lo Byte)
Quantity of Registers 2 Bytes 0x0001 to 0x007D 1 to 125 registers (Hi Byte, Lo Byte)

Constraint Rule: The maximum number of registers readable in a single request is 125 registers (0x007D). Requesting 0 registers or more than 125 registers violates protocol limits and causes the server to immediately return an Exception Code 03 (Illegal Data Value).

Normal (Success) Response PDU Structure

If the server successfully processes the request, it responds with the function code, a byte count indicating the size of the payload, and the register data:

Field Name Length Hex Value / Range Description
Function Code 1 Byte 0x03 Confirms successful FC 03 execution
Byte Count 1 Byte N * 2 (2 to 250) Number of data bytes following (2 bytes per register)
Register Values N * 2 Bytes Variable High Byte followed by Low Byte for each register

4. Wire Frame Analysis: Modbus RTU vs Modbus TCP

To inspect how Function Code 03 travels across industrial networks, we analyze complete Application Data Units (ADUs) for both serial RS-485 and Ethernet connections.

Example Scenario

  • Client Goal: Read 2 holding registers starting at address 40108 (Wire offset: 107 decimal = 0x006B).
  • Target Server ID: Unit 1 (0x01).
  • Expected Data: Register 40108 contains 0x02AE (686 decimal); Register 40109 contains 0x0014 (20 decimal).

Modbus RTU Frame Breakdown (Serial RS-485 / RS-232)

In Modbus RTU, the PDU is framed by a 1-byte Slave ID and protected by a 16-bit Cyclic Redundancy Check (CRC-16):

Request Frame (8 Bytes):
[ 01 ] [ 03 ] [ 00 6B ] [ 00 02 ] [ B4 17 ]
  │      │       │         │         └─ CRC-16 Checksum (Lo: 0xB4, Hi: 0x17)
  │      │       │         └─ Quantity: 2 registers (0x0002)
  │      │       └─ Starting Address: 107 (0x006B)
  │      └─ Function Code: 0x03
  └─ Slave Address: 0x01

Response Frame (9 Bytes):
[ 01 ] [ 03 ] [ 04 ] [ 02 AE ] [ 00 14 ] [ 3A 79 ]
  │      │      │       │         │         └─ CRC-16 Checksum (Lo: 0x3A, Hi: 0x79)
  │      │      │       │         └─ Reg 40109 Value: 0x0014
  │      │      │       └─ Reg 40108 Value: 0x02AE
  │      │      └─ Byte Count: 4 bytes (2 registers * 2)
  │      └─ Function Code: 0x03
  └─ Slave Address: 0x01

Modbus TCP Frame Breakdown (Ethernet Port 502)

In Modbus TCP, the serial CRC and slave framing are replaced by the 7-byte MBAP (Modbus Application Protocol) Header:

Request Frame (12 Bytes):
[ 00 01 ] [ 00 00 ] [ 00 06 ] [ 01 ] [ 03 ] [ 00 6B ] [ 00 02 ]
  │         │         │         │      │       │         │
  │         │         │         │      │       │         └─ Quantity: 2 (0x0002)
  │         │         │         │      │       └─ Starting Address: 107 (0x006B)
  │         │         │         │      └─ Function Code: 0x03
  │         │         │         └─ Unit Identifier: 0x01
  │         │         └─ Message Length: 6 bytes following (0x0006)
  │         └─ Protocol Identifier: 0x0000 (Modbus Protocol)
  └─ Transaction Identifier: 0x0001 (Echoed in response)

Response Frame (13 Bytes):
[ 00 01 ] [ 00 00 ] [ 00 07 ] [ 01 ] [ 03 ] [ 04 ] [ 02 AE ] [ 00 14 ]
  │         │         │         │      │      │       │         │
  │         │         │         │      │      │       │         └─ Reg 40109: 0x0014
  │         │         │         │      │      │       └─ Reg 40108: 0x02AE
  │         │         │         │      │      └─ Byte Count: 4 bytes
  │         │         │         │      └─ Function Code: 0x03
  │         │         │         └─ Unit Identifier: 0x01
  │         │         └─ Length: 7 bytes following (0x0007)
  │         └─ Protocol Identifier: 0x0000
  └─ Transaction Identifier: 0x0001

5. Handling 32-Bit Floating Point & Multi-Register Data Types

Because standard Modbus registers are strictly 16-bit unsigned entities, storing a 32-bit integer or a single-precision 32-bit floating point number (IEEE 754) requires spanning two consecutive holding registers.

Unfortunately, the original Modbus standard does not specify byte or word order for 32-bit values. As a result, industrial vendors implemented different endianness schemes:

Endianness Mode Word Order Byte Order Common Vendor Deployments
Big-Endian (ABCD) High Word First High Byte First Schneider Electric, Emerson DeltaV, Yokogawa
Little-Endian (CDAB) Low Word First High Byte First Rockwell Allen-Bradley, Modicon Quantum, ProSoft
Byte-Swapped Big (BADC) High Word First Low Byte First Omni Flow Computers, certain gas chromatographs
Byte-Swapped Little (DCBA) Low Word First Low Byte First Legacy PC-based DAQ boards

When reading float values using Function Code 03, always configure your master software or SCADA tags with the matching word-swapping option. In ModbusSimulator, you can toggle between Big-Endian, Little-Endian, and Swapped modes instantly to determine the correct representation of incoming live data.

6. Exception Handling: What Happens When FC03 Fails?

When a Modbus server receives a malformed or invalid Function Code 03 request, it responds with an Exception Response rather than a timeout. In an exception response:

  1. The Function Code byte has its highest-order bit (0x80) set: 0x03 + 0x80 = 0x83.
  2. A single-byte Exception Code follows immediately, indicating the failure mode:
Exception Code Name Common Cause During FC03 Execution
0x01 Illegal Function Target device does not support holding registers (e.g., discrete I/O module only).
0x02 Illegal Data Address Starting address does not exist, or Starting Address + Quantity exceeds physical memory table.
0x03 Illegal Data Value Quantity of registers is set to 0, or exceeds the maximum protocol threshold of 125 registers.
0x04 Slave Device Failure Unrecoverable hardware error or flash memory parity failure occurred while retrieving data.

7. Python Implementation: Polling FC03 with pymodbus

To poll holding registers programmatically in automated test rigs or custom SCADA gateways, Python with the pymodbus library provides a fast, standardized implementation:

from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

# Connect to Modbus TCP Server (ModbusSimulator running on localhost)
client = ModbusTcpClient(host='127.0.0.1', port=502)

if client.connect():
    print("[+] Connected to Modbus Server.")

    # Read 4 holding registers starting at wire address 100 (40101)
    # slave=1 specifies Unit ID 1
    response = client.read_holding_registers(address=100, count=4, slave=1)

    if not response.isError():
        raw_registers = response.registers
        print(f"[*] Raw 16-bit register values: {raw_registers}")

        # Decode registers 0 & 1 as a 32-bit IEEE 754 Float (Big-Endian)
        decoder = BinaryPayloadDecoder.fromRegisters(
            raw_registers[0:2],
            byteorder=Endian.BIG,
            wordorder=Endian.BIG
        )
        float_value = decoder.decode_32bit_float()
        print(f"[*] Decoded Process Temperature: {float_value:.2f} °C")
    else:
        print(f"[-] Modbus Exception Received: {response}")

    client.close()
else:
    print("[-] Failed to connect to Modbus TCP Server.")

8. Simulating Holding Registers with ModbusSimulator

Testing PLC control sequences, SCADA dashboards, or Python data ingestion pipelines against live physical hardware in a running production plant introduces significant risk of accidental tripping, register corruption, or equipment downtime.

Using ModbusSimulator, you can spin up a fully compliant Modbus TCP server on port 502 or a Modbus RTU slave on any physical or virtual serial COM port in seconds:

  • Pre-Populated 4xxxx Table: Configure up to 65,536 holding registers with realistic test values.
  • Dynamic Signal Generators: Assign automated waveform generators (Sine, Ramp, Random Noise, Square Wave) to registers to test alarm high/low thresholds in SCADA.
  • Real-Time Hex Sniffer: Inspect incoming Function Code 03 packets byte-for-byte to verify your PLC master's starting address offset and CRC calculation.
  • Fault Injection Testing: Deliberately configure Exception 02 (Illegal Address) or Exception 04 (Device Failure) responses to verify your master application's failover recovery logic.

Simulate Modbus Holding Registers with Zero Hardware

Download ModbusSimulator for Windows today. Run multi-slave Modbus TCP and RTU simulations with live hex logging and dynamic signal generation.

Download Free 30-Day Trial →