← All Posts

How to Read Modbus Holding Registers (FC03): A Practical Walkthrough

Reading a Modbus device comes down to one operation more than any other: read holding registers, function code 03. Setpoints, configuration values and, on many devices, voltage, current, power and energy counters live in holding registers, and FC03 is how you pull them. Some devices put live measurements in input registers instead (FC04), so check the register map.

This walkthrough shows exactly what goes on the wire, how the slave replies, and the two gotchas (the off-by-one address and the 32-bit float byte order) behind many "my reading is wrong" problems. For the full function-code reference, see Modbus Function Codes Explained.

What You Need Before You Start

The FC03 Request Frame

An FC03 request carries the function code 0x03, the starting address, and the quantity of registers. The slave ID (unit ID) addresses the device. To read holding register 40001 (protocol address 0) for 2 registers from slave 1:

Slave ID:   01
Function:   03  (Read Holding Registers)
Start addr: 0000  (protocol address, 0-based)
Quantity:   0002  (two registers = 4 bytes)

On the wire, over Modbus TCP (port 502) the slave ID moves into the MBAP header (transaction ID, protocol ID 0, length, unit ID) and there is no CRC. Over RTU it is one continuous frame starting with the slave address and ending with a 2-byte CRC:

RTU request:  01 03 00 00 00 02 C4 0B
TCP request:  00 01 00 00 00 06 01 03 00 00 00 02
              (transaction 0001, protocol 0000, length 0006, unit 01, then the PDU)
Address gotcha: holding register 40001 maps to protocol address 0, not 1. The 4xxxx range is a human convention; the wire is 0-based. Off-by-one is one of the most common Modbus mistakes, see Modbus Addressing Explained.

The FC03 Response

The slave replies with the function code, a byte count, and the data. Example response (illustrative values, not captured from a device):

Slave ID:   01
Function:   03
Byte count: 04   (2 registers × 2 bytes)
Data:       00 7B  02 58   (register 0 = 123, register 1 = 600)

RTU response: 01 03 04 00 7B 02 58 8A B0
TCP response: 00 01 00 00 00 07 01 03 04 00 7B 02 58

Each register is 16 bits, sent big-endian within the register (high byte first). That much is standardized. Where vendors diverge is how they pack values larger than 16 bits. A single FC03 request can ask for at most 125 registers (250 data bytes); a slave may return an exception for a larger quantity.

Reading 32-bit Values (Floats and Integers)

A 32-bit float or integer occupies two consecutive registers. Modbus does not define the order, so every vendor picks one. The four conventions, named by how the four bytes ABCD are arranged in memory:

If your 230 V reading shows as a tiny or very large number, the byte or word order is probably wrong. Cycle the four conventions until the value matches expectation. See Modbus Float Byte Order Explained for the full worked example.

Step-by-Step in a Poll Tool

  1. Open the master and create a connection (TCP: IP + port 502; RTU: serial port, baud, parity).
  2. Set the slave ID to the device's address.
  3. Enter function code 03, start address, and quantity.
  4. Poll. The grid fills with raw 16-bit values, refreshed at the poll interval.
  5. For 32-bit values, set the cell type to Float/INT32 and the byte order to match the vendor's map.

Read Registers on macOS

MacTools Modbus Poll issues FC03 (and every other standard code) on TCP and RTU, with per-cell type and byte-order selection and built-in device templates. $19.99 one-time.

Get Modbus Poll on the Mac App Store

Frequently Asked Questions

What is Modbus function code 03?

Function code 03 is Read Holding Registers. The master requests a contiguous block of 16-bit holding registers from a slave by specifying the slave ID, a start address (protocol address, 0-based), and the number of registers to read. The slave returns the byte count followed by the register values, high byte first (big-endian within each register).

How many holding registers can I read in one FC03 request?

The Modbus specification allows up to 125 holding registers (250 data bytes) in a single FC03 request. In practice the limit also depends on the slave's buffer size and the RTU frame timing. For values larger than 16 bits (floats, 32-bit integers), one logical value spans two or more registers.

Why does my 32-bit Modbus float read wrong?

A 32-bit float occupies two consecutive 16-bit registers, and vendors disagree on byte order. The four common word/byte orders are ABCD, DCBA, BADC, and CDAB (Big-Endian, Little-Endian, Big-Endian Byte Swap, Little-Endian Byte Swap). If your reading shows a tiny or nonsensical value, cycle through the four byte orders until it matches the expected engineering value.

Is holding register 40001 address 0 or 1?

40001 is the human-readable PLC address (the 4xxxx range denotes holding registers). The protocol address on the wire is 0-based, so 40001 maps to protocol address 0, 40002 to 1, and so on. The off-by-one is a very common Modbus addressing mistake.

Related: Continuous Monitoring

When the reads are correct, Voltrus SCADA takes over: polling, dashboards, alarms, historian. Free open source core (AGPL-3.0).