Modbus Address Conventions and Function Codes: The Practical Guide Every IIoT Engineer Needs [2026]
If you've ever stared at a PLC register map wondering why address 300001 means something completely different from 400001, or why your edge gateway reads all zeros from a register that should contain temperature data — this guide is for you.
Modbus has been the lingua franca of industrial automation for nearly five decades. Its longevity comes from simplicity, but that simplicity hides a handful of conventions that trip up even experienced engineers. The addressing scheme and its relationship to function codes is the single most important concept to nail before you write a single line of polling logic.
Let's break it apart.
The Four Register Spaces
Every Modbus device exposes up to four distinct data spaces. These aren't just categories — they determine which wire-level function code your gateway must use to read (or write) the data:
| Address Prefix | Register Type | Data Width | Access | Function Code (Read) | Function Code (Write) |
|---|---|---|---|---|---|
| 0xxxx (0–65535) | Coils | 1 bit | Read/Write | FC 01 | FC 05, FC 15 |
| 1xxxx (100000–165535) | Discrete Inputs | 1 bit | Read-only | FC 02 | — |
| 3xxxx (300000–365535) | Input Registers | 16 bits | Read-only | FC 04 | — |
| 4xxxx (400000–465535) | Holding Registers | 16 bits | Read/Write | FC 03 | FC 06, FC 16 |
This prefix convention — often called the "Modicon convention" after the original Modbus creator — is the Rosetta Stone of Modbus communication. When a PLC vendor hands you a register map that says "address 300001," they're telling you:
- Strip the prefix: the actual register address on the wire is 1 (or 0, depending on zero-based vs one-based — more on that in a moment)
- Use Function Code 04 (Read Input Registers) to fetch it
- The response will contain a 16-bit value
Get the function code wrong and you'll either read garbage from a different register space or trigger an exception response from the PLC.
The Zero-Based vs One-Based Minefield
Here's where things get dangerous. The Modbus protocol specification defines addresses as zero-based: register 0 is the first register. But the Modicon convention uses one-based display addresses — 400001 means holding register 0, and 400002 means holding register 1.
This off-by-one discrepancy has caused more commissioning delays than any other single issue in IIoT deployments.
When you see a register map from a PLC vendor:
Tag: Tank Temperature
Address: 300001
Type: int16
The actual Modbus address you send on the wire is 0 (for zero-based implementations) or 1 (for one-based). Your gateway library's behavior here depends on its implementation. Most modern libraries (libmodbus, pymodbus, node-red-contrib-modbus) expect the zero-based address — meaning you'd pass 0 to read what the vendor calls 300001.
The rule of thumb: Always read one register at a known value during commissioning. If the temperature sensor reads 72°F and you see 72 at address 0, you're zero-based. If you see it at address 1, your library is one-based. Document this once and never think about it again.
Function Code Selection Logic
In practice, a well-designed edge gateway doesn't ask the engineer which function code to use — it derives the function code directly from the address prefix. The logic is straightforward:
Address 0 – 65,535 → Function Code 01 (Read Coils)
Address 100,000 – 165,535 → Function Code 02 (Read Discrete Inputs)
Address 300,000 – 365,535 → Function Code 04 (Read Input Registers)
Address 400,000 – 465,535 → Function Code 03 (Read Holding Registers)
This means your tag configuration only needs to specify the full Modbus address (including prefix). The gateway handles function code selection automatically. This eliminates an entire class of human error from the configuration process.
Why this matters for IIoT platforms: When you're configuring hundreds of tags across dozens of machines, any step you can automate reduces commissioning time. A gateway that requires you to manually specify both address and function code is asking you to maintain redundant information — and inviting mistakes.
Contiguous Register Grouping: The Performance Secret
Here's where the real optimization lives. The Modbus specification allows reading multiple contiguous registers in a single request. Instead of sending 50 individual Read Holding Registers requests to fetch 50 tags, you can send one request that reads registers 0 through 49 in a single PDU.
The impact on polling performance is dramatic:
| Approach | Requests per Cycle | Overhead per Request | Total Overhead |
|---|---|---|---|
| Individual reads (50 tags) | 50 | ~10ms RTT + framing | ~500ms |
| Grouped read (50 contiguous) | 1 | ~10ms RTT + framing | ~10ms |
| Grouped reads (3 non-contiguous blocks) | 3 | ~10ms RTT + framing | ~30ms |
For Modbus TCP, each request incurs TCP overhead (SYN-ACK, Nagle's algorithm delays, etc.). For Modbus RTU over serial, each request requires a 3.5-character silence period before and after the frame, plus the slave's processing time. Grouping contiguous registers eliminates most of this overhead.
How Grouping Works in Practice
A well-designed polling engine examines all configured tags and groups them by:
- Same function code — you can't mix coil reads (FC 01) with input register reads (FC 04) in one request
- Contiguous addresses — registers must be adjacent with no gaps
- Same polling interval — a tag polled every second shouldn't be grouped with one polled every minute
- Maximum register count — the Modbus spec limits a single read to 125 registers (250 bytes). In practice, keeping groups under 50 registers improves reliability on noisy RS-485 networks
When a tag breaks any of these conditions, the engine starts a new group. The result is the minimum number of Modbus transactions needed to fetch all your data.
Example: Consider these tags from a central chiller:
Tag: Tank Temperature addr: 300001 interval: 60s
Tag: Circuit 1 Approach Temp addr: 300002 interval: 60s
Tag: Circuit 1 Chill In Temp addr: 300003 interval: 60s
Tag: Circuit 1 Chill Out Temp addr: 300004 interval: 60s
...
Tag: Circuit 1 Superheat Temp addr: 300017 interval: 60s
Tag: Chiller Alarm Word addr: 300162 interval: 1s
Tag: Circuit 1 Alarm Word 1 addr: 300163 interval: 1s
The engine creates two groups:
- Group 1: Addresses 300001–300017 (17 registers, FC 04, 60-second interval) — one transaction
- Group 2: Addresses 300162–300163 (2 registers, FC 04, 1-second interval) — one transaction
Without grouping, this would be 19 separate Modbus transactions. With grouping: 2.
Sorted Tag Lists
For grouping to work efficiently, tags must be sorted by address within each function code space. If tags arrive in arbitrary order (as they often do in JSON configuration files), the gateway needs to sort them before building read groups.
This is a small detail that makes a huge difference in scan cycle time. An unsorted tag list means the grouping algorithm can't identify contiguous blocks — and falls back to individual reads.
Data Types and Register Widths
Modbus registers are 16 bits wide. Everything else is encoded into that 16-bit container:
| Data Type | Registers Used | Byte Order Matters? |
|---|---|---|
bool (coil/discrete) | 1 bit | No |
int16 / uint16 | 1 register | No |
int32 / uint32 | 2 registers | Yes |
float (IEEE 754) | 2 registers | Yes |
When a value spans two registers (32-bit integers, floats), byte ordering becomes critical. The Modbus spec doesn't mandate a word order for multi-register values, so different PLC vendors implement it differently:
- Big-endian (AB CD): High word first —
register[n]contains the most significant 16 bits - Little-endian (CD AB): Low word first —
register[n]contains the least significant 16 bits - Mid-endian variants (BA DC, DC BA): Byte-swapped within words — rarer, but they exist
For a 32-bit integer stored across registers n and n+1:
Big-endian: value = (register[n+1] << 16) | register[n] — or —
value = (register[n] << 16) | register[n+1]
The practical fix: Read a known 32-bit value (like a serial number) and check which word order produces the correct number. Document the PLC's word order in your configuration. This is a per-vendor (sometimes per-model) setting — not a global standard.
For IEEE 754 floats, most Modbus libraries provide a modbus_get_float() helper that handles the byte-swapping for you. Use it instead of manually reassembling bytes.
Alarm Words: Bit-Level Decoding
Many PLCs pack multiple alarm states into a single 16-bit register. Each bit represents a different alarm condition. A register value of 0x0005 (binary 0000000000000101) means alarms at bit positions 0 and 2 are active.
To extract individual alarm flags from an alarm word:
alarm_bit_3 = (alarm_word >> 3) & 0x01
This pattern — reading a uint16 register and extracting individual bits via shift-and-mask operations — is fundamental to industrial alarm monitoring. A well-designed IIoT platform should handle this decoding at the edge, delivering individual boolean alarm tags rather than forcing the cloud layer to understand register-level bit packing.
Calculated Tags
Advanced edge gateways support "calculated" tags that derive their values from parent tags. An alarm word register (uint16) can spawn multiple boolean child tags, each representing a single alarm bit. When the alarm word changes, the gateway:
- Reads the new uint16 value
- Applies the shift and mask for each calculated tag
- Compares each result to its previous value
- Delivers only the bits that actually changed
This is significantly more efficient than polling each alarm as a separate coil — especially when alarm words contain 16+ conditions packed into a single register.
Change-Based vs Time-Based Delivery
Not all data should be sent to the cloud every polling cycle. For slowly-changing values like serial numbers, configuration settings, or steady-state temperatures, comparing the new value to the previous value and only transmitting on change dramatically reduces bandwidth and storage costs.
For rapidly-changing operational values (cycle counts, production rates), time-based delivery at fixed intervals makes more sense — you want a regular time series regardless of whether the value changed.
The best approach combines both:
- Compare-enabled tags (alarms, status bits, setpoints): Only transmit when the value differs from the last transmitted value
- Time-interval tags (temperatures, pressures, flow rates): Transmit at the configured interval regardless of change
- Hourly full refresh: Re-transmit all tags once per hour to catch any missed updates and maintain data integrity
Common Pitfalls
1. Wrong Function Code for the Register Type
Symptom: Gateway returns all zeros or Modbus exception code 02 (Illegal Data Address). Cause: Using FC 03 (Read Holding Registers) on an input register (3xxxx) address, or vice versa. Fix: Verify the address prefix matches the function code.
2. Off-by-One Address Errors
Symptom: Data appears shifted — you read the value from the next register over. Cause: Zero-based vs one-based addressing mismatch between your vendor's documentation and your Modbus library. Fix: Read a known value and verify alignment.
3. Unsorted Tag Configurations Breaking Grouping
Symptom: Scan cycle takes 10x longer than expected. Cause: Tags configured in random address order, preventing contiguous grouping. Fix: Ensure the gateway sorts tags by address before building read groups.
4. Reading Too Many Registers Per Request
Symptom: Intermittent timeouts or CRC errors on RS-485 networks. Cause: Requesting 125 registers per PDU on a noisy serial bus. Fix: Cap read groups at 50 registers on serial networks.
5. Missing Retry Logic for Transient Errors
Symptom: Occasional "connection lost" events followed by immediate recovery. Cause: Single-attempt reads with no retry on timeout. Fix: Implement a retry count (typically 3 attempts) before declaring a tag read failure.
How machineCDN Handles Modbus
machineCDN's edge gateway handles all of this natively — from automatic function code selection based on address prefix, to contiguous register grouping with configurable group size limits, to alarm word decoding with bit-level calculated tags. Engineers define their tag maps in a straightforward JSON configuration, and the gateway handles the Modbus protocol complexity at the edge.
The platform supports both Modbus TCP and Modbus RTU (RS-485) with configurable serial parameters (baud rate, parity, data bits, stop bits, byte timeout, response timeout), automatic connection recovery, and binary telemetry encoding that minimizes bandwidth usage over cellular backhaul links.
For plant engineers connecting PLCs to the cloud for the first time, this means less time debugging protocol issues and more time analyzing the data that matters.
Want to connect your Modbus PLCs to a modern IIoT platform without becoming a protocol expert? See how machineCDN handles industrial data collection →