TestBench.tools

Modbus Address Converter

Translate 0-based, 1-based and 4xxxx Modbus register notations.

Entity
Holding register (read/write 16-bit)
Protocol address (on the wire, 0-based)
10
1-based address
11
5-digit notation
40011
6-digit notation
400011

This tool translates between the three ways a Modbus register gets named: data-model notation (40011), 1-based addressing (11) and the 0-based protocol address that actually travels on the wire (10). It understands all four entity tables — coils, discrete inputs, input registers and holding registers — in both 5- and 6-digit forms.

How it works

The Modbus protocol transmits a plain 0-based offset; which of the four data tables it indexes is implied by the function code, not the address. Documentation, however, traditionally names points with a table prefix and a 1-based index: 4 for holding registers, 3 for input registers, 1 for discrete inputs, 0 for coils. Both conventions describe the same register, shifted by one and dressed differently — and mixing them silently produces the infamous off-by-one errors.

Paste a documented address like 40011 to get the wire offset, or go the other way from a captured request back to the documentation name. Offsets beyond 9998 only exist in the 6-digit notation, which the tool selects automatically.

Worked example

40011 → holding register · 1-based 11 · protocol offset 10 (0x000A)
30001 → input register · protocol offset 0
100005 → discrete input · protocol offset 4

Parameters

ParameterValueNotes
0xxxxcoilsread/write bits, FC 01/05/15
1xxxxdiscrete inputsread-only bits, FC 02
3xxxxinput registersread-only 16-bit, FC 04
4xxxxholding registersread/write 16-bit, FC 03/06/16
Wire format0-based offset0 … 65535, table via function code

Python implementation

Python
# Data-model notation -> (entity, 0-based protocol address)
TABLES = {0: "coil", 1: "discrete input", 3: "input register", 4: "holding register"}

def parse_data_model(addr: str):
    table, offset = int(addr[0]), int(addr[1:])
    return TABLES[table], offset - 1        # 1-based -> 0-based

assert parse_data_model("40011") == ("holding register", 10)
assert parse_data_model("30001") == ("input register", 0)

FAQ

Why does register 40011 go on the wire as address 10?

40011 is data-model notation: the leading 4 identifies a holding register and the remaining digits are a 1-based index (the 11th register). The protocol itself transmits a 0-based offset with the entity implied by the function code — so 40011 → offset 11−1 = 10, i.e. 0x000A in the request.

What do the leading digits 0, 1, 3 and 4 mean?

They encode the entity table: 0xxxx coils (read/write bits), 1xxxx discrete inputs (read-only bits), 3xxxx input registers (read-only 16-bit), 4xxxx holding registers (read/write 16-bit). The digit is a naming convention only — it never appears in the frame.

Why do 5-digit and 6-digit notations both exist?

5-digit notation (40001–49999) can only name 9999 registers, but the protocol allows 65536. Modern documentation therefore uses 6 digits (400001–465536). The tool shows both, and marks the 5-digit form unavailable when the offset exceeds 9998.

My device documentation and my master are off by one — which side is wrong?

Neither: one speaks 1-based (data model) and the other 0-based (protocol). Off-by-one is the single most common Modbus commissioning problem. Convert the documented address here and compare the 0-based value with what your master actually transmits.

Is my data uploaded?

No. Conversion happens locally in your browser.

Related tools