Modbus data types explained — coils, discrete inputs, input registers and holding registers
Modbus data types in practice: coils, discrete inputs, input registers and holding registers, addressing, function codes and real field pitfalls.
The engineer reads PLC Modbus documentation for the first time and finds four tables with similar names, five-digit prefixes starting at 0, 1, 3 or 4, and a function code per operation. Then comes the discovery that logical address 40001 becomes 0x0000 on the wire, that the most obvious register shows up off by one position, and that half of the test clients display a different value than the other half. That confusion is historical baggage from a 1979 protocol whose data type names carry the memory map of the Modicon 984 PLC. This post separates the four tables, shows the function codes that touch each one, and closes with the pitfalls that appear on day one in the field.
The 4 Modbus data types
The original Modbus data model has exactly four tables. Each one defines the type (bit or 16-bit word), the direction (read, write or both), the logical address prefix and the function codes that operate on it.
| Table | Type | Access | Logical prefix | Function codes |
|---|---|---|---|---|
| Coils | 1 bit | Read and write | 0xxxx (00001–09999) | 0x01, 0x05, 0x0F |
| Discrete inputs | 1 bit | Read only | 1xxxx (10001–19999) | 0x02 |
| Input registers | 16 bits | Read only | 3xxxx (30001–39999) | 0x04 |
| Holding registers | 16 bits | Read and write | 4xxxx (40001–49999) | 0x03, 0x06, 0x10 |
Coils and discrete inputs travel packed, eight per byte. Input and holding registers travel as 16-bit big-endian words. Trying to write an input register returns exception 0x01 (Illegal Function); reading a coil with function code 0x02 also returns 0x01.
Coils (0x) — read/write bit
Coils represent individual digital outputs. The name comes from the coil of an internal relay: switching contacts on and off, actuating solenoid valves, energizing lamps, commanding starts and stops. In modern devices, coils also appear as command flags — counter reset, alarm clear, calibration trigger.
Three function codes touch coils:
- 0x01 Read Coils: reads from 1 to 2000 consecutive coils; the response carries
ceil(N/8)packed bytes. - 0x05 Write Single Coil: writes a single coil. The value is 0xFF00 for on and 0x0000 for off; any other value is illegal.
- 0x0F Write Multiple Coils: writes up to 1968 coils in a single transaction.
Frequent pitfall: a coil written as 0xFF00 reads back as bit 1 on the next read. A client that persists the written value instead of re-reading may flag inconsistency where there is none. Always compare against the read representation.
Discrete inputs (1x) — read-only bit
Discrete inputs represent digital inputs wired to binary sensors. The classic case is a dry contact: limit switch, inductive sensor, pressure switch, thermostat, emergency button, breaker auxiliary contact. In measurement devices they also expose state flags.
Only one function code operates:
- 0x02 Read Discrete Inputs: reads from 1 to 2000 consecutive inputs, with eight bits packed per byte.
There is no write on a discrete input. The attempt returns exception 0x01 or 0x02 depending on the implementation. The most common confusion happens when the same device exposes a status (sensor) and a command (actuator) over the same concept: the careful vendor allocates a discrete input for "alarm active" and a coil for "clear alarm".
Input registers (3x) — read-only word
Input registers are 16-bit read-only words. The original use was to expose the digitized value of a PLC analog input (thermocouple, 4–20 mA, pressure sensor, encoder). In dedicated devices, they hold measured quantities: voltage, current, temperature, counters.
- 0x04 Read Input Registers: reads from 1 to 125 consecutive registers. The response carries
2×Nbytes in big-endian order.
The semantic split matters: the input register is the contract "this is what the device measures, you cannot touch it". In Secure by Design architectures, keeping measured quantities in a read-only table simplifies audit. In practice, many vendors consolidate everything into holding registers; the AEM-60DC8 follows that approach with 147 holding registers documented.
Holding registers (4x) — read/write word
Holding registers are 16-bit read/write words. It is the most used table because everything fits: setpoints, alarm limits, configuration, status, telemetry, counters, identification, calibration.
Three function codes dominate:
- 0x03 Read Holding Registers: reads from 1 to 125 consecutive registers. The most frequent operation in industrial supervisors.
- 0x06 Write Single Register: writes a 16-bit word to a single register.
- 0x10 Write Multiple Registers: writes up to 123 registers in a network-atomic transaction.
Secondary function codes include 0x16 (mask write) and 0x17 (read/write multiple), with support that varies by vendor. The contract of a holding register must document, beyond the address: unit, scale, offset, valid range, sentinel value for "invalid data" (0x8000 in int16) and persistence.
Addressing — 0-origin vs 1-origin, Modicon offset, hex × decimal map
The point where most integrations stumble is not the function code, it is the address. Two conventions coexist:
- Logical address (1-based, with prefix): the Modicon notation. First coil 00001, first discrete input 10001, first input register 30001, first holding register 40001.
- Protocol address (0-based, no prefix): what goes inside the frame. First register of any table is 0x0000. What distinguishes the table is the function code.
| Table | Logical address | Protocol address | Function code |
|---|---|---|---|
| Coils | 00001 | 0x0000 | 0x01 |
| Coils | 00100 | 0x0063 | 0x01 |
| Discrete inputs | 10001 | 0x0000 | 0x02 |
| Input registers | 30016 | 0x000F | 0x04 |
| Holding registers | 40001 | 0x0000 | 0x03 |
| Holding registers | 40147 | 0x0092 | 0x03 |
Practical rule: subtract the prefix, subtract another 1 and convert to hex. Some tools adopt a third convention with prefix 4 and 0-based address, where 40000 refers to the same register as 40001 in the classic notation. When documentation is ambiguous, capture the frame with a serial analyzer and read the byte directly.
How the AEM-60DC8 organizes its 147 holding registers
The AEM-60DC8 (Industrial DC Monitoring Platform, firmware v1.03) exposes 147 holding registers in 17 functional blocks. The excerpt below is a simplified example for didactic purposes; the authoritative map is the annex of the technical manual.
| Block | Range (hex) | Registers | Direction | Content |
|---|---|---|---|---|
| Voltage CH1–CH8 instantaneous | 0x0000–0x0007 | 8 | Effective RO | uint16 with ×100 scale |
| Voltage CH1–CH8 1 s average | 0x0008–0x000F | 8 | Effective RO | 1-second moving window |
| Per-channel status | 0x0010–0x0017 | 8 | Effective RO | Bitfield: present, in alarm, calibrated |
| Upper/lower limits | 0x0020–0x002F | 16 | RW | Setpoints per channel |
| Channel configuration | 0x0040–0x004F | 16 | RW | Enable, mode, oversampling |
| Communication configuration | 0x0050–0x0057 | 8 | RW | Slave ID, baudrate, parity, timeout |
| Event counters | 0x0060–0x006F | 16 | Effective RO | Alarm rise and fall |
| Forensic telemetry | 0x0070–0x007F | 16 | Effective RO | Uptime, watchdog resets, CRC errors |
| Factory calibration | 0x0080–0x008F | 16 | RW (protected) | Gain and offset, requires code |
| Identification | 0x0090–0x0092 | 3 | Effective RO | Model, serial, firmware v1.03 |
Holding registers marked "Effective RO" accept writes at protocol level, but the firmware rejects them with exception 0x04 when the content is a measurement. Publishing a map without indicating effective direction, unit, scale and persistence turns every integration into a negotiation.
Common reading errors
Five pitfalls explain most of the "Modbus that doesn't work" complaints in the field:
Off-by-one: reading holding register 40016 by writing 0x0010 instead of 0x000F. The server returns the value of 40017. Fix: subtract the prefix and another 1 before converting.
Byte order: Modbus is big-endian within the 16-bit register — high byte first. Little-endian implementations read with bytes swapped. Symptom: 24.00 V becomes 0.06 V (0x0960 read as 0x6009).
Word swapping in 32-bit values: floats, int32 and counters occupy two consecutive registers. Word order (high word first vs low word first) is not standardized. All four combinations (ABCD, CDAB, BADC, DCBA) appear in the field. Symptom: small values correct, large values absurd.
Signed vs unsigned: the protocol carries raw 16 bits without sign. -100 arrives as 0xFF9C; read as uint16 it becomes 65436. Voltages with floating reference can be negative and require signed interpretation.
Quantity out of range: 0x03 and 0x04 accept up to 125 registers per request; 0x10 accepts up to 123. Larger requests return exception 0x03. Reading 147 registers in a single request does not work — split into two reads.
Frequently asked questions
Why are there four tables instead of just one? The split reflects the memory of the 1980s Modicon 984 PLC, where digital outputs, digital inputs, analog inputs and configuration memory occupied distinct physical areas. The specification preserved the split to make the RO vs RW and bit vs word semantics explicit.
Can I use holding registers for everything? Technically yes, and many vendors consolidate measurements, setpoints and status into holding registers. The loss is semantic: the client has to know by convention which registers are effectively read-only.
How do I read a 32-bit float in Modbus? By reading two consecutive holding registers and reconstructing the float per the documented word order. The most common order in modern devices is CDAB (low word first), but ABCD appears in European devices.
Does the AEM-60DC8 expose coils or discrete inputs? No. The AEM-60DC8 consolidates all 147 variables into holding registers, with support for function codes 0x03 and 0x10. Channel status and alarm flags are exposed as bitfields.
How do I know if the address in the documentation is logical or protocol? By the number: five digits starting with 0, 1, 3 or 4 is a logical address. Hexadecimal or starting with 0x0000 is a protocol address. In ambiguity, capture the frame and read the byte directly.