Resolving Modbus FC03 Holding Register Addressing: PDU vs 40001

David Krause11 min read
ModbusSiemensTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview: The Modbus Holding Register Addressing Question

When integrating a Modbus RTU slave device such as a Siemens SINAMICS S200 drive into a Siemens WinCC flexible HMI or SIMATIC S7 controller, the most common configuration mistake is treating the human-readable "register number" (e.g., 40001, 40108) as the value loaded into the Protocol Data Unit (PDU) starting address field. The Modbus Application Protocol Specification V1.1b3 defines FC 03 (Read Holding Registers) as reading the binary contents of holding registers, but it deliberately leaves the actual on-wire starting register address as a zero-based offset. This produces the well-documented confusion between addresses 40000 and 40001.

This reference clarifies the difference between the PDU starting address (zero-based, range 0x0000–0xFFFF, 16-bit unsigned) and the remote device register number (one-based, conventional 4xxxx prefix shown in vendor documentation and I/O schedules). It also covers the exact FC 03 request/response frame layout, the byte-order and quantity limits, and how to enter the address correctly in WinCC flexible, TIA Portal, and third-party Modbus masters.

Prerequisites

  • A copy of the Modbus Application Protocol Specification V1.1b3 (or later revision) for reference.
  • The target slave device's register map (vendor-specific), expressed in 4xxxx form.
  • The master configuration tool: WinCC flexible 2008 SP5 or later, TIA Portal V13 SP1 or later, or any Modbus master (OPC server, SCADA node, gateway).
  • A serial line analyzer (e.g., Modbus Poll, Wireshark with the Modbus dissector, or a dedicated protocol tap) for verification.
  • Knowledge of the slave's RS-485 termination, baud rate, parity, and unit identifier (slave address 1–247).

Modbus Addressing Models: Three Different Numbers, One Register

Every holding register can be identified by three numerically different addresses depending on which layer of the protocol you are looking at. Engineers must keep all three straight when building an I/O schedule.

Address Model Format Range for Holding Registers Used In
Conventional / Vendor (4xxxx) 1-based, 5 decimal digits, prefix 4 40001–49999 Vendor datasheets, HMI tag lists, I/O schedules, WinCC flexible symbolic addressing
Modicon 984 PLC (6-digit) 1-based, 6 decimal digits, prefix 4 400001–465536 Legacy Modicon ladder logic, Schneider SoMachine
PDU / On-Wire 0-based, 16-bit unsigned hex or decimal 0x0000 (0)–0xFFFF (65535) Request frame field "Starting Address", response frame payload offset

The conversion rule defined by the Modbus Organization is unambiguous:

  • PDU Starting Address = (Conventional Register Number) − 1
  • Conventional Register Number = (PDU Starting Address) + 1

Therefore, the first holding register visible as "40001" in vendor documentation is sent on the wire as 0x0000. The register shown as "40108" goes on the wire as 0x006B (decimal 107). The register shown as "40000" is not a valid Modbus register — it is an off-by-one artifact introduced either by tooling that already subtracted one for you, or by a typographical error in the I/O schedule.

Function Code 03 — Read Holding Registers Specification

Per FC 03 of the Modbus specification, the request and response PDU frames have the following structure. The Application Data Unit (ADU) adds the unit identifier and (for RTU) a 16-bit CRC.

Request Frame (Master → Slave)

Field Length Value / Description
Unit Identifier (slave address) 1 byte 1–247 (0 = broadcast, reserved)
Function Code 1 byte 0x03 = Read Holding Registers
Starting Address Hi 1 byte High byte of PDU address (0x00 for registers 1–256)
Starting Address Lo 1 byte Low byte of PDU address
Quantity of Registers Hi 1 byte High byte of register count (0x00 for count ≤ 256)
Quantity of Registers Lo 1 byte Low byte of register count, 1–125
CRC (RTU only) 2 bytes CRC-16 (poly 0xA001, init 0xFFFF)

Response Frame (Slave → Master)

Field Length Value / Description
Unit Identifier 1 byte Echoed from request
Function Code 1 byte 0x03
Byte Count 1 byte 2 × Quantity of Registers (max 250)
Register Value 1 Hi 1 byte First register requested, MSB
Register Value 1 Lo 1 byte First register requested, LSB
… … …
CRC (RTU only) 2 byte CRC-16

Limits and Exception Codes

Parameter Value Notes
Maximum registers per request 125 Defined by Modbus spec to keep PDU ≤ 253 bytes
Maximum bytes in response payload 250 Byte count field maximum value
Quantity of Registers = 0 Exception 03 (ILLEGAL DATA VALUE) Vendor may ignore or reject
Quantity of Registers > 125 Exception 03 Some slaves accept 126; spec forbids
Starting Address > max valid Exception 02 (ILLEGAL DATA ADDRESS) Slave reports FC 0x83 + 0x02
Read past implemented register map Exception 02 Check slave register table end address

Worked Example: Reading Holding Registers 40108–40110

Following the canonical example documented at Simply Modbus FC 03 message example, the request reads three consecutive registers from slave address 17 (0x11).

Conventional register numbers: 40108, 40109, 40110

PDU starting addresses (after subtracting 1): 40108 − 1 = 40107, 40110 − 1 = 40109

Quantity of Registers: 3 (i.e., 40107, 40108, 40109 in PDU terms)

Hexadecimal Form for the Wire

Decimal 40107 = 0x9CCB. Decimal 3 = 0x0003. The full RTU request becomes:

Request:  11 03 9C CB 00 03 [CRC Lo] [CRC Hi]
Response: 11 03 06 [R1 Hi] [R1 Lo] [R2 Hi] [R2 Lo] [R3 Hi] [R3 Lo] [CRC Lo] [CRC Hi]

The byte count in the response is 6 because three 16-bit registers occupy 6 bytes. If the slave returns 11 83 02, the master is reading an illegal address — most often because the PDU offset points one register too high (i.e., the master accidentally sent 0x9CCC, which is decimal 40108, instead of 0x9CCB).

Why Your I/O Schedule Shows 40000 Instead of 40001

Several root causes exist for an I/O schedule that lists a first holding register as 40000 rather than the spec-mandated 40001:

  1. Double-offsetting by the scheduler: Some legacy master tools accept the conventional number as input and automatically subtract 1 for the PDU. The scheduler's author then typed the conventional number manually, producing a "double-subtracted" entry of 40000. The correct fix is to enter 40001 in the conventional column.
  2. Confusion with the legacy Modicon 984 base: Modicon PLCs historically numbered 400000–465535. A tool migrating an old project sometimes drops the leading digit.
  3. Vendor documentation starts at zero: Some modern sensors publish their register map as 0–N. When the engineer copies the map into a Modbus-aware tool expecting the 4xxxx convention, no conversion is applied and the registers appear "off by one" downstream.
  4. OPC-DA bridge auto-translation: An OPC server such as KEPServerEX or the Modbus driver in WinCC may apply its own +1 offset. The schedule reflects what the OPC layer exposes, not what goes on the wire.

In all four cases, the engineering rule is the same: trust the slave device register map, expressed in the conventional 4xxxx numbering, and let the master tool handle the PDU offset internally. If the master expects you to enter the PDU address directly, subtract one from every conventional number before typing it in.

Configuring the Address in WinCC flexible and TIA Portal

WinCC flexible (WinCC flexible 2008 SP5 and later)

  1. Open the tag editor and create a new tag with connection type "Modicon MODBUS (984)" or "MODBUS RTU", depending on your CP driver.
  2. Select "Holding Register (4xxxx)" as the address type.
  3. Enter the conventional register number in the "Address" field. For 40108 type 40108, not 107.
  4. Set the data type to match the slave encoding (typically WORD, INT, REAL, or DWORD).
  5. For 32-bit values spanning two registers, enable "Word swap" or "Byte swap" per the slave's endianness. Siemens SIMATIC S7 and S200 drives default to Big-Endian for Modbus.

TIA Portal (V13 SP1+)

  1. Add the Modbus_Comm_Load and Modbus_Master blocks from the "MODBUS" library (instructions catalog → Communication → MODBUS).
  2. Configure the Mode parameter to 4 (Read Holding Registers) and the DataPtr to a data block with at least Quantity × 2 bytes allocated.
  3. Set the DataAddr (the PDU starting address) to (conventional number − 1). For 40108, enter 40107.
  4. Compile and download. The MB_HOLD_REG tag in the HMI tag table still uses the 4xxxx convention.

Verification Procedure

  1. Connect a serial tap or Modbus Poll in slave-emulator mode between the master and the physical slave.
  2. Force the master to read a single known register and capture the RTU frame. Confirm the starting address field equals (conventional − 1).
  3. Compare the response payload to a vendor-supplied reference value. A correctly addressed read returns the documented register content.
  4. Trigger a deliberate illegal read (e.g., address 0 with slave map starting at 40001) and confirm the slave responds with 83 02. This proves the wiring and CRC are healthy.
  5. Repeat with the I/O schedule in question. If reading 40000 returns exception 02, the schedule is wrong by one. If reading 40001 returns the correct value, update the schedule.

Troubleshooting Matrix

Symptom Root Cause Diagnostic Fix
Every read returns Exception 02 (ILLEGAL DATA ADDRESS) PDU address off by one Capture RTU frame, compare Starting Address hex to (conventional − 1) Adjust address by ±1 per the conversion rule
Reads succeed but values are scrambled (Hi/Lo swapped) Endianness mismatch between master and slave Compare response payload byte order against vendor endianness Enable "Byte swap" / "Word swap" in master tag
Reads succeed but value is shifted by one register Quantity of Registers mismatch or PDU base off by one Compare response byte count to expected 2 × N Correct Quantity or PDU base
No response from slave, RTU CRC errors on master Baud rate, parity, or termination mismatch Verify baud (e.g., 19200 8E1), 120 Ω termination at line ends Match slave serial settings, install termination
Slave returns Exception 03 (ILLEGAL DATA VALUE) Quantity of Registers = 0 or > 125 Inspect Quantity field in RTU frame Constrain Quantity to 1–125 per Modbus spec
Intermittent timeouts on long poll lists Silent interval 3.5 char not respected, multi-drop contention Check inter-frame gap, verify only one master on bus Insert 3.5-character idle gap, remove rogue masters
HMI shows constant 0x8000 or 0x7FFF Word swap enabled when not required (or vice versa) Toggle swap flag, observe value Match vendor endianness documentation

Special Cases: 32-Bit and 64-Bit Values Across Two or Four Registers

Many process variables (REAL / FLOAT, DINT, LREAL) span multiple 16-bit holding registers. The Modbus specification is silent on byte order; the slave vendor chooses.

Encoding Byte Order in Register N Register Pair Address Span (32-bit)
Big-Endian (Modbus-Standard "ABCD") Hi byte first, Lo byte second Register N = High Word, Register N+1 = Low Word
Byte-Swapped ("BADC") Lo byte first, Hi byte second Same register span, swap bytes within each word
Word-Swapped ("CDAB") Hi-Lo per register Register N = Low Word, Register N+1 = High Word
Little-Endian / Reverse ("DCBA") Lo-Hi per register Word and byte swap both required

Siemens S200 drives typically use Big-Endian "ABCD" for REAL values. Schneider Electric Modicon M340 controllers and many ABB drives use "DCBA". Always consult the slave's register map before configuring swap logic in the master.

Register Count vs. Byte Count: Common Pitfall

FC 03 request carries the register count (e.g., 3). FC 03 response carries the byte count (e.g., 6). Confusing these two values is a frequent source of parser bugs in custom Modbus masters. The relationship is:

Byte Count = Quantity of Registers × 2

If the master requests 3 registers, the slave must reply with a byte count of 6, followed by exactly 6 bytes of register data and the CRC. Anything else means either an exception response or a corrupted frame.

Notes on TCP (Modbus TCP) vs. RTU

On Modbus TCP (port 502), the same FC 03 PDU is wrapped in an MBAP header (7 bytes: Transaction ID, Protocol ID 0x0000, Length, Unit Identifier). The PDU starting address rule is unchanged — the body of the MBAP payload still uses zero-based PDU addresses. Many gateways auto-translate between the wire format and the conventional 4xxxx numbering exposed to upstream tools, which is why a "connection problem" can suddenly appear as an "off-by-one addressing" problem when moving from a serial RTU master to a TCP master.

Should I correct my I/O schedule from 40000 to 40001?

Yes. Modbus Application Protocol Specification V1.1b3 defines the first holding register as 40001 (PDU address 0). A schedule entry of 40000 indicates a double-offset error or a vendor documentation artifact; correct it to 40001 so the master loads PDU address 0x0000 on the wire.

What is the difference between a PDU address and a remote device register number?

The PDU (Protocol Data Unit) address is the zero-based value placed in the request frame's Starting Address field, range 0–65535. The remote device register number is the one-based, conventional identifier such as 40108 shown in vendor datasheets. The conversion is: PDU = Conventional − 1.

How many registers can a single FC 03 request read?

The Modbus specification allows a maximum of 125 holding registers per FC 03 request, producing a response payload of 250 bytes plus headers. Requesting more than 125 typically returns Exception 03 (ILLEGAL DATA VALUE) from spec-compliant slaves.

My slave returns exception code 02. What does it mean?

Exception 02 (ILLEGAL DATA ADDRESS) means the Starting Address field plus the Quantity of Registers points to one or more registers that are not implemented in the slave. Most often this is an off-by-one addressing mistake: the master sent PDU 40107 when it intended PDU 40106, or vice versa.

Does Modbus TCP use the same addressing as Modbus RTU?

Yes. The PDU body — including the FC 03 Starting Address field — is identical between RTU and TCP. Only the framing differs: RTU uses a Unit Identifier + CRC-16, while TCP wraps the PDU in a 7-byte MBAP header and uses a TCP checksum. Both still address register 40001 as PDU 0x0000.

Back to blog