Modbus Addressing Explained: PLC Address vs Protocol Address
The device manual says voltage lives in holding register 40321. You type 40321 into your poll tool and the value comes back wrong. You try 40320 and it works. This is not a bug in your tool, it is the single most confusing thing about Modbus, and it catches every engineer on their first project.
Modbus has two address systems that refer to the same register with different numbers. This guide explains both, why they differ by one, and how to stop second-guessing which number to type. For the data tables these addresses point at, see the register types guide; for what each read/write operation does, see function codes explained.
Two Numbers, One Register
Every register in a Modbus slave has two names:
- PLC address (also "display address" or "document address"). This is the human-readable number printed in the device manual,
40001,30010,00017. It is one-based and never appears on the wire. - Protocol address (also "register offset" or "wire address"). This is the zero-based number actually transmitted inside the Modbus frame,
0x0000,0x0009,0x0010.
They describe the same physical register. The PLC address is for humans reading documentation; the protocol address is for the protocol reading the wire. The difference between them is always exactly 1.
40001 = protocol address 0. PLC address 40002 = protocol address 1. The first register in any table is document number ...001 but wire number 0.
The Leading Digit Identifies the Table
The first digit of a PLC address is a prefix that tells you which of the four data tables the register lives in. It is documentation only, it is never sent on the wire.
So when a manual says 4321 or 4x321 or HR321, all of them mean holding register 40321, the 4 is the table marker. Some modern documentation drops the prefix entirely and writes 321, expecting you to know from context that it is a holding register. That convention is the source of half the addressing confusion in the field.
The Conversion Rule
To turn a PLC address into the protocol address you actually send:
- Strip the leading table digit (4, 3, 1, or 0).
40321→0321. - Subtract 1.
0321→0320. - Send as a 16-bit value, big-endian:
0x0140.
Worked examples:
40001→ strip4→0001→ minus 1 →0000→0x000040321→0321→0320→0x014030010→0010→0009→0x000900017→0017→0016→0x0010
To go the other way (wire address → document number), add 1 and prepend the table digit.
Why It Was Designed This Way
The split is historical. Modicon invented Modbus in 1979 for its PLC line. The PLC's user-facing reference data used one-based numbers because that is what ladder-logic programmers expected. But the protocol engineers wanted a clean zero-based offset so the slave could index directly into an array: register 0 = array element 0. Both sides won, the human-facing documentation kept its familiar one-based numbering, and the wire stayed zero-based.
The cost is that every new engineer rediscovers the off-by-one on their first commissioning job. It is a rite of passage, not a defect.
The Off-By-One in Practice
Here is the exact failure: the Schneider PM5560 manual lists "Phase Voltage L1-N" at 40321 (sometimes written 0x0140 directly, to save you the subtraction). You open your poll tool, enter 40321 in the address field, and read. The value is plausible but wrong, it is actually 40322, the next register down.
What happened: some poll tools expect the protocol address (so you must enter 320), others expect the PLC address and subtract 1 for you (so you enter 40321), and a few let you toggle a "1-based / 0-based" switch. There is no universal standard. You must learn which convention your tool uses.
The reliable fix is independent of the tool: read a register whose value you already know (a firmware version, a fixed model code, or a register set to a known constant) and confirm it matches. If it is off by one register, switch your tool's addressing mode or subtract 1 from what you typed.
Extended Addressing: 6-Digit and 99999+
The classic PLC address is five digits (40001–49999), which caps each table at 9999 registers. Modern devices with large register maps outgrow this. Two extensions exist:
- Six-digit addressing (
400001–465535), extends the document number to six digits while the protocol address is simply sent as a larger 16-bit value (0–65535). The leading table digit is still documentation only. - Raw zero-based addressing, drop the prefix entirely and document everything as a 0-based offset from
0to65535. Common in modern IoT devices and gateways that do not care about the legacy table convention.
The protocol address field is always a 16-bit unsigned integer, so the hard ceiling is 65536 registers per table regardless of how the documentation labels them.
Zero-Based vs One-Based: Which Is Your Tool?
Most modern poll tools accept either and expose a setting. The conventions you will see:
- Protocol (0-based): you type
320. The tool sends it verbatim. Most developer-focused tools and libraries (pymodbus, modbus-cli) work this way. - PLC (1-based): you type
40321. The tool strips the table digit and subtracts 1. Common in HMI/SCADA configuration and Windows-era Modbus software. - Zero-based with explicit table selection: you pick "Holding Register" from a dropdown and type
320. This is the clearest convention because it matches the wire directly and removes the prefix ambiguity.
If your tool has the third option, use it. It is the least error-prone.
Addressing Across Function Codes
The addressing rule is the same for every function code, but the table prefix changes which code you pair it with. A common mistake: reading a 3xxxx input register with FC03 (holding-register read). The slave returns exception 01 (Illegal Function) because the register exists in a different table. Match the code to the prefix, exception codes has the full table of what each error means.
Checklist for Your First Read
- Identify the table from the leading digit (4 = holding, 3 = input, 0 = coil, 1 = discrete).
- Pick the matching function code (FC03/FC04/FC01/FC02).
- Convert to the protocol address: strip prefix, subtract 1.
- Confirm your poll tool's addressing mode (0-based vs 1-based).
- Validate against a known register (firmware version, model ID) before trusting unknown values.
Frequently Asked Questions
Is Modbus address zero-based or one-based?
Both, depending on which number you mean. The human-readable PLC address printed on documentation (40001, 30001, 00001) is one-based. The protocol address sent on the wire is zero-based. So holding register 40001 in the manual is sent as protocol address 0000 in the frame. The difference is always exactly 1.
How do I convert a Modbus PLC address to a protocol address?
Strip the leading digit that identifies the table (4 for holding, 3 for input, 0 for coil, 1 for discrete input), then subtract 1 from what remains. Holding register 40321 becomes 0321, minus 1 = 0320, sent as 0x0140.
Why does my Modbus read return the wrong register?
Almost always an off-by-one. You entered 40321 from the device manual but your poll tool sends the value verbatim as protocol address 321, which the slave reads as the register documented as 40322. Subtract 1: send 320. The second most common cause is using the wrong function code for the table the address implies.
What is the difference between 4x and 0x addressing in Modbus?
The leading digit identifies the data table. 4xxxx (40001+) is holding registers, 16-bit read/write. 0xxxx (00001+) is coils, 1-bit read/write. 3xxxx is read-only input registers and 1xxxx is read-only discrete inputs. The leading digit is documentation only, it is never transmitted on the wire.
Stop Guessing at Addresses
MacTools Modbus Poll displays every register with its address, lets you toggle 0-based and 1-based modes, and includes a Schneider PM5560 simulator whose register map is already mapped correctly. Read the firmware-version register, confirm it matches, and trust the rest.
Get Modbus PollFurther Reading
- Modbus Register Types Explained: Coils, Discrete Inputs, Holding & Input Registers
- Modbus Function Codes Explained: FC01–FC06, FC15, FC16
- Modbus Exception Codes Explained: Illegal Function, Data, Address
- Schneider PM5560 Modbus Register Map and Simulator
- Modbus Float Byte Order Explained: ABCD, DCBA, BADC, CDAB