The user explicitly flagged that DL205/DL260 strings don't follow Modbus convention; research turned up that and a lot more. Headline findings: String packing — TWO chars per V-memory register but the FIRST char is in the LOW byte (opposite of the big-endian Modbus convention generic drivers default to). 'Hello' in V2000 reads back as 'eHll o\0' on a textbook decoder. Kepware's DirectLogic driver exposes a per-tag 'String Byte Order = Low/High' toggle specifically for this; we'll need the same. Null-terminated, no length prefix, no dedicated KSTR address space — strings live wherever ladder allocates them in V-memory. V-memory addressing — DirectLOGIC's native V-memory is OCTAL (V2000, V40400) but Modbus is decimal. The CPU translates: V2000 octal = decimal 1024 = Modbus PDU 0x0400. The widespread 'V40400 = register 0' shorthand is wrong on modern firmware (that was DL05/DL06 relative mode); on H2-ECOM100 absolute mode (factory default) V40400 = PDU 0x2100. We'd surface this with an address-format helper in the device profile so operators write V2000 instead of computing 1024 by hand. Word order CDAB for all 32-bit values — DL205 and DL260 agree, ECOM modules don't re-swap. Already supported via ModbusByteOrder.WordSwap; just needs to be the default in the DL205 profile. BCD-as-default numeric storage — bit one I didn't expect. DirectLOGIC stores 'V2000 = 1234' as 0x1234 on the wire (BCD nibbles), not as 0x04D2 (decimal 1234). IEEE 754 Float32 only works when ladder used the explicit R type (LDR/OUTR instructions). We need a new decoder mode for BCD-encoded registers — current code assumes binary integers. FC quantity caps — FC03/04 cap at 128 (above spec's 125 — Bonus territory, current code already respects 125), FC16 caps at 100 (BELOW spec's 123 — important bulk-write batching gotcha). Quantity overrun returns exception 03 IllegalDataValue. Coil/discrete mappings — DL260: X0->discrete input 0, Y0->coil 2048, C0->coil 3072. SP specials at discrete input 1024-1535 RO. These are CPU-wired constants and cannot be remapped; need to be hardcoded in the DL205/DL260 device profile. Register 0 — accepted on DL205/DL260 with ECOM in absolute mode, contrary to the widespread internet claim that 'DirectLOGIC rejects register 0'. That rumour was an older DL05/DL06 relative-mode artefact. Our ModbusProbeOptions.ProbeAddress default of 0 is therefore safe for DL205/DL260. Exception codes — only the standard 01-04. Write-to-protected-bit returns 02 on newer firmware, 04 on older (firmware-transition revision unconfirmed); driver should map both to BadNotWritable. No proprietary exception codes. Behavioral oddities — H2-ECOM100 accepts MAX 4 simultaneous TCP connections (5th refused at TCP accept). No TCP keepalive (intermediate NAT/firewall drops idle sockets after 2-5 min — periodic probe required). No mid-stream resync on malformed MBAP — driver must reconnect + replay. TxId-drop-under-load forum rumour is unconfirmed; our single-flight + TxId-match guard handles it either way. Each H2 section ends with the integration-test names we'd ship per the modbus-test-plan.md DL205_<behavior> convention — twelve named test slots ready for PR 42+ to fill in one at a time. References (8) cited inline, primarily D2-USER-M, HA-ECOM-M, and the Kepware DirectLogic Ethernet driver manual which documents these vendor quirks explicitly because they have to cope with them. modbus-test-plan.md DL205 section rewritten as a priority-ordered table with three columns (quirk / driver impact / test name), pointing the reader at dl205.md for the full reference. Operator-reported items separated into a tail subsection so future-me knows which behaviors are documented vs reproduced-on-hardware. Pure documentation PR — no code changes. The actual driver work (string-byte-order option, BCD decoder mode, V-memory address helper, FC16 cap-per-device-family, multi-client TCP handling) lands one PR per quirk in PR 42+ as ModbusPal validation completes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
296 lines
16 KiB
Markdown
296 lines
16 KiB
Markdown
# AutomationDirect DirectLOGIC DL205 / DL260 — Modbus quirks
|
|
|
|
AutomationDirect's DirectLOGIC DL205 family (D2-250-1, D2-260, D2-262, D2-262M) and
|
|
its larger DL260 sibling speak Modbus TCP (via the H2-ECOM100 / H2-EBC100 Ethernet
|
|
coprocessors, and the DL260's built-in Ethernet port) and Modbus RTU (via the CPU
|
|
serial ports in "Modbus" mode). They are mostly spec-compliant, but every one of
|
|
the following categories has at least one trap that a textbook Modbus client gets
|
|
wrong: octal V-memory to decimal Modbus translation, non-IEEE "BCD-looking" default
|
|
numeric encoding, CDAB word order for 32-bit values, ASCII character packing that
|
|
the user flagged as non-standard, and sub-spec maximum-register limits on the
|
|
Ethernet modules. This document catalogues each quirk, cites primary sources, and
|
|
names the ModbusPal integration test we'd write for it (convention from
|
|
`docs/v2/modbus-test-plan.md`: `DL205_<behavior>`).
|
|
|
|
## Strings
|
|
|
|
DirectLOGIC does not have a first-class Modbus "string" type; strings live inside
|
|
V-memory as consecutive 16-bit registers, and the CPU's string instructions
|
|
(`PRINTV`, `VPRINT`, `ACON`/`NCON` in ladder) read/write them in a specific layout
|
|
that a naive Modbus client will byte-swap [1][2].
|
|
|
|
- **Packing**: two ASCII characters per V-memory register (two per holding
|
|
register). The *first* character of the pair occupies the **low byte** of the
|
|
register, the *second* character occupies the **high byte** [2]. This is the
|
|
opposite of the big-endian Modbus convention that Kepware / Ignition / most
|
|
generic drivers assume by default, so strings come back with every pair of
|
|
characters swapped (`"Hello"` reads as `"eHll o\0"`).
|
|
- **Termination**: null-terminated (`0x00` in the character byte). There is no
|
|
length prefix. Writes must pad the final register's unused byte with `0x00`.
|
|
- **Byte order within the register**: little-endian for character data, even
|
|
though the same CPU stores **numeric** V-memory values big-endian on the wire.
|
|
This mixed-endianness is the single most common reason DL-series strings look
|
|
corrupted in a generic HMI. Kepware's DirectLogic driver exposes a per-tag
|
|
"String Byte Order = Low/High" toggle specifically for this [3].
|
|
- **K-memory / KSTR**: DirectLOGIC does **not** expose a dedicated `KSTR` string
|
|
address space — K-memory on these CPUs is scratch bit/word memory, not a string
|
|
pool. Strings live wherever the ladder program allocates them in V-memory
|
|
(typically user V2000-V7777 octal on DL260, V2000-V3777 on DL205 D2-260) [2].
|
|
- **Maximum length**: bounded only by the V-memory region assigned. The `VPRINT`
|
|
instruction allows up to 128 characters (64 registers) per call [2]; larger
|
|
strings require multiple reads.
|
|
- **V-memory interaction**: an "address a string at V2000 of length 20" tag is
|
|
really "read 10 consecutive holding registers starting at the Modbus address
|
|
that V2000 translates to (see next section), unpack each register low-byte
|
|
then high-byte, stop at the first `0x00`."
|
|
|
|
Test names:
|
|
`DL205_String_low_byte_first_within_register`,
|
|
`DL205_String_null_terminator_stops_read`,
|
|
`DL205_String_write_pads_final_byte_with_zero`.
|
|
|
|
## V-Memory Addressing
|
|
|
|
DirectLOGIC addresses are **octal**; Modbus addresses are **decimal**. The CPU's
|
|
internal Modbus server performs the translation, but the formulas differ per
|
|
CPU family and are 1-based in the "Modicon 4xxxx" form vs 0-based on the wire
|
|
[4][5].
|
|
|
|
Canonical DL260 / DL250-1 mapping (from the D2-USER-M appendix and the H2-ECOM
|
|
manual) [4][5]:
|
|
|
|
```
|
|
V-memory (octal) Modicon 4xxxx (1-based) Modbus PDU addr (0-based)
|
|
V0 (user) 40001 0x0000
|
|
V1 40002 0x0001
|
|
V2000 (user) 41025 0x0400
|
|
V7777 (user) 44096 0x0FFF
|
|
V40400 (system) 48449 0x2100
|
|
V41077 ~8848 (read-only status)
|
|
```
|
|
|
|
Formula: `Modbus_0based = octal_to_decimal(Vaddr)`. So `V2000` octal = `1024`
|
|
decimal = Modbus PDU address `0x0400`. The "4xxxx" Modicon view just adds 1 and
|
|
prefixes the register bank digit.
|
|
|
|
- **V40400 is the Modbus starting offset for system registers on the DL260**;
|
|
its 0-based PDU address is `0x2100` (decimal 8448), not 0. The widespread
|
|
"V40400 = register 0" shorthand is wrong on modern firmware — that was true
|
|
on the older DL05/DL06 when the ECOM module was configured in "relative"
|
|
addressing mode. On the H2-ECOM100 factory default ("absolute" mode), V40400
|
|
maps to 0x2100 [5].
|
|
- **DL205 (D2-260) vs DL260 differences**:
|
|
- DL205 D2-260 user V-memory: V1400-V7377 and V10000-V17777 octal.
|
|
- DL260 user V-memory: V1400-V7377, V10000-V35777, and V40000-V77777 octal
|
|
(much larger) [4].
|
|
- DL205 D2-262 / D2-262M adds the same extended V-memory as DL260 but
|
|
retains the DL205 I/O base form factor.
|
|
- Neither DL205 sub-model changes the *formula* — only the valid range.
|
|
- **Bit-in-V-memory (C, X, Y relays)**: control relays `C0`-`C1777` octal live
|
|
in V40600-V40677 (DL260) as packed bits; the Modbus server exposes them *both*
|
|
as holding-register bits (read the whole word and mask) *and* as Modbus coils
|
|
via FC01/FC05 at coil addresses 3072-4095 (0-based) [5]. `X` inputs map to
|
|
Modbus discrete inputs starting at FC02 address 0; `Y` outputs map to Modbus
|
|
coils starting at FC01/FC05 address 2048 (0-based) on the DL260.
|
|
- **Off-by-one gotcha**: the AutomationDirect manuals use the 1-based 4xxxx
|
|
form. Kepware, libmodbus, pymodbus, and the .NET stack all take the 0-based
|
|
PDU form. When the manual says "V2000 = 41025" you send `0x0400`, not
|
|
`0x0401`.
|
|
|
|
Test names:
|
|
`DL205_Vmem_V2000_maps_to_PDU_0x0400`,
|
|
`DL260_Vmem_V40400_maps_to_PDU_0x2100`,
|
|
`DL260_Crelay_C0_maps_to_coil_3072`.
|
|
|
|
## Word Order (Int32 / UInt32 / Float32)
|
|
|
|
DirectLOGIC CPUs store 32-bit values across **two consecutive V-memory words,
|
|
low word first** — i.e., `CDAB` when viewed as a Modbus register pair [1][3].
|
|
Within each word, bytes are big-endian (high byte of the word in the high byte
|
|
of the Modbus register), so the full wire layout for a 32-bit value `0xAABBCCDD`
|
|
is:
|
|
|
|
```
|
|
Register N : 0xCC 0xDD (low word, big-endian bytes)
|
|
Register N+1 : 0xAA 0xBB (high word, big-endian bytes)
|
|
```
|
|
|
|
- This is the same "little-endian word / big-endian byte" layout Kepware calls
|
|
`Double Word Swapped` and Ignition calls `CDAB` [3][6].
|
|
- **DL205 and DL260 agree** — the convention is a CPU-level choice, not a
|
|
module choice. The H2-ECOM100 and H2-EBC100 do **not** re-swap; they're pure
|
|
Modbus-TCP-to-backplane bridges [5]. The DL260 built-in Ethernet port
|
|
behaves identically.
|
|
- **Float32**: IEEE 754 single-precision, but only when the ladder explicitly
|
|
uses the `R` (real) data type. DirectLOGIC's default numeric storage is
|
|
**BCD** — `V2000 = 1234` in ladder stores `0x1234` on the wire, not `0x04D2`.
|
|
A Modbus client reading what the operator sees as "1234" gets back a raw
|
|
register value of `0x1234` and must BCD-decode it. Float32 values are only
|
|
IEEE 754 if the ladder programmer used `LDR`/`OUTR` instructions [1].
|
|
- **Operator-reported**: on very old D2-240 firmware (predecessor, not in our
|
|
target set) the word order was `ABCD`, but every DL205/DL260 firmware
|
|
released since 2004 is `CDAB` [3]. _Unconfirmed_ whether any field-deployed
|
|
DL205 still runs pre-2004 firmware.
|
|
|
|
Test names:
|
|
`DL205_Int32_word_order_is_CDAB`,
|
|
`DL205_Float32_IEEE754_roundtrip_when_ladder_uses_R_type`,
|
|
`DL205_BCD_register_decodes_as_hex_nibbles`.
|
|
|
|
## Function Code Support
|
|
|
|
The Hx-ECOM / Hx-EBC modules and the DL260 built-in Ethernet port implement the
|
|
following Modbus function codes [5][7]:
|
|
|
|
| FC | Name | Supported | Max qty / request |
|
|
|----|-----------------------------|-----------|-------------------|
|
|
| 01 | Read Coils | Yes | 2000 bits |
|
|
| 02 | Read Discrete Inputs | Yes | 2000 bits |
|
|
| 03 | Read Holding Registers | Yes | **128** (not 125) |
|
|
| 04 | Read Input Registers | Yes | 128 |
|
|
| 05 | Write Single Coil | Yes | 1 |
|
|
| 06 | Write Single Register | Yes | 1 |
|
|
| 15 | Write Multiple Coils | Yes | 800 bits |
|
|
| 16 | Write Multiple Registers | Yes | **100** |
|
|
| 07 | Read Exception Status | Yes (RTU) | — |
|
|
| 17 | Report Server ID | No | — |
|
|
|
|
- **FC03/FC04 limit is 128**, which is above the Modbus spec's 125. Requesting
|
|
129+ returns exception code `03` (Illegal Data Value) [5].
|
|
- **FC16 limit is 100**, below the spec's 123. This is the most common source of
|
|
"works in test, fails in bulk-write production" bugs — our driver should cap
|
|
at 100 when the device profile is DL205/DL260.
|
|
- **No custom function codes** are exposed on the Modbus port. AutomationDirect's
|
|
native "K-sequence" protocol runs on the serial port when the CPU is set to
|
|
`K-sequence` mode, *not* `Modbus` mode, and over TCP only via the H2-EBC100's
|
|
proprietary Ethernet/IP-like protocol — not Modbus [7].
|
|
|
|
Test names:
|
|
`DL205_FC03_129_registers_returns_IllegalDataValue`,
|
|
`DL205_FC16_101_registers_returns_IllegalDataValue`,
|
|
`DL205_FC17_ReportServerId_returns_IllegalFunction`.
|
|
|
|
## Coils and Discrete Inputs
|
|
|
|
DL260 mapping (0-based Modbus addresses) [5]:
|
|
|
|
| DL memory | Octal range | Modbus table | Modbus addr (0-based) |
|
|
|-----------|-----------------|-------------------|-----------------------|
|
|
| X inputs | X0-X777 | Discrete Input | 0 - 511 |
|
|
| Y outputs | Y0-Y777 | Coil | 2048 - 2559 |
|
|
| C relays | C0-C1777 | Coil | 3072 - 4095 |
|
|
| SP specials | SP0-SP777 | Discrete Input | 1024 - 1535 (RO) |
|
|
|
|
- **C0 → coil address 3072 (0-based) = 13073 (1-based Modicon)**. Y0 → coil
|
|
2048 = 12049. These offsets are wired into the CPU and cannot be remapped.
|
|
- **Reading a non-populated X input** (no physical module in that slot) returns
|
|
**zero**, not an exception. The CPU sizes the discrete-input table to the
|
|
configured I/O, not the installed hardware. Confirmed in the DL260 user
|
|
manual's I/O configuration chapter [4].
|
|
- **Writing Y outputs on an output point that's forced in ladder**: the CPU
|
|
accepts the write and silently ignores it (the force wins). No exception is
|
|
returned. _Operator-reported_, matches Kepware driver release notes [3].
|
|
|
|
Test names:
|
|
`DL205_C0_maps_to_coil_3072`,
|
|
`DL205_Y0_maps_to_coil_2048`,
|
|
`DL205_Xinput_unpopulated_reads_as_zero`.
|
|
|
|
## Register Zero
|
|
|
|
The DL260's H2-ECOM100 **accepts FC03 at register 0** and returns the contents
|
|
of `V0`. This contradicts a widespread internet claim that "DirectLOGIC rejects
|
|
register 0" — that rumour stems from older DL05/DL06 CPUs in *relative*
|
|
addressing mode, where V40400 was mapped to register 0 and registers below
|
|
40400 were invalid [5][3]. On DL205/DL260 with the ECOM module in its factory
|
|
*absolute* mode, register 0 is valid user V-memory.
|
|
|
|
- Our driver's `ModbusProbeOptions.ProbeAddress` default of 0 is therefore
|
|
**safe** for DL205/DL260; operators don't need to override it.
|
|
- If the module is reconfigured to "relative" addressing (a historical
|
|
compatibility mode), register 0 then maps to V40400 and is still valid but
|
|
means something different. The probe will still succeed.
|
|
|
|
Test name: `DL205_FC03_register_0_returns_V0_contents`.
|
|
|
|
## Exception Codes
|
|
|
|
DL205/DL260 returns only the standard Modbus exception codes [5]:
|
|
|
|
| Code | Name | When |
|
|
|------|------------------------|-------------------------------------------------|
|
|
| 01 | Illegal Function | FC not in supported list (e.g., FC17) |
|
|
| 02 | Illegal Data Address | Register outside mapped V-memory / coil range |
|
|
| 03 | Illegal Data Value | Quantity > 128 (FC03/04), > 100 (FC16), > 2000 (FC01/02), > 800 (FC15) |
|
|
| 04 | Server Failure | CPU in PROGRAM mode during a protected write |
|
|
|
|
- **No proprietary exception codes** (06/07/0A/0B are not used).
|
|
- **Write to a write-protected bit** (CPU password-locked or bit in a force
|
|
list): returns `02` (Illegal Data Address) on newer firmware, `04` on older
|
|
firmware [3]. _Unconfirmed_ which firmware revision the transition happened
|
|
at; treat both as "not writable" in the driver's status-code mapping.
|
|
- **Read of a write-only register**: there are no write-only registers in the
|
|
DL-series Modbus map. Every writable register is also readable.
|
|
|
|
Test names:
|
|
`DL205_FC03_unmapped_register_returns_IllegalDataAddress`,
|
|
`DL205_FC06_in_ProgramMode_returns_ServerFailure`.
|
|
|
|
## Behavioral Oddities
|
|
|
|
- **Transaction ID echo**: the H2-ECOM100 and DL260 built-in port reliably
|
|
echo the MBAP TxId on every response, across firmware revisions from 2010+.
|
|
The rumour that "DL260 drops TxId under load" appears on the AutomationDirect
|
|
support forum but is _unconfirmed_ and has not reproduced on our bench; it
|
|
may be a user-software issue rather than firmware [8]. Our driver's
|
|
single-flight + TxId-match guard handles it either way.
|
|
- **Concurrency**: the ECOM serializes requests internally. Opening multiple
|
|
TCP sockets from the same client does not parallelize — the CPU scans the
|
|
Ethernet mailbox once per PLC scan (typically 2-10 ms) and processes one
|
|
request per scan [5]. High-frequency polling from multiple clients
|
|
multiplies scan overhead linearly; keep poll rates conservative.
|
|
- **Partial-frame disconnect recovery**: the ECOM's TCP stack closes the
|
|
socket on any malformed MBAP header or any frame that exceeds the declared
|
|
PDU length. It does not resynchronize mid-stream. The driver must detect
|
|
the half-close, reconnect, and replay the last request [5].
|
|
- **Keepalive**: the ECOM does **not** send TCP keepalives. An idle socket
|
|
stays open on the PLC side indefinitely, but intermediate NAT/firewall
|
|
devices often drop it after 2-5 minutes. Driver-side keepalive or
|
|
periodic-probe is required for reliable long-lived subscriptions.
|
|
- **Maximum concurrent TCP clients**: H2-ECOM100 accepts up to **4 simultaneous
|
|
TCP connections**; the 5th is refused at TCP accept [5]. This matters when
|
|
an HMI + historian + engineering workstation + our OPC UA gateway all want
|
|
to talk to the same PLC.
|
|
|
|
Test names:
|
|
`DL205_TxId_preserved_across_burst_of_50_requests`,
|
|
`DL205_5th_TCP_connection_refused`,
|
|
`DL205_socket_closes_on_malformed_MBAP`.
|
|
|
|
## References
|
|
|
|
1. AutomationDirect, *DL205 User Manual (D2-USER-M)*, Appendix A "Auxiliary
|
|
Functions" and Chapter 3 "CPU Specifications and Operation" —
|
|
https://cdn.automationdirect.com/static/manuals/d2userm/d2userm.html
|
|
2. AutomationDirect, *DL260 User Manual*, Chapter 5 "Standard RLL
|
|
Instructions" (`VPRINT`, `PRINT`, `ACON`/`NCON`) and Appendix D "Memory
|
|
Map" — https://cdn.automationdirect.com/static/manuals/d2userm/d2userm.html
|
|
3. Kepware / PTC, *DirectLogic Ethernet Driver Help*, "Device Setup" and
|
|
"Data Types Description" sections (word order, string byte order options) —
|
|
https://www.kepware.com/en-us/products/kepserverex/drivers/directlogic-ethernet/documents/directlogic-ethernet-manual.pdf
|
|
4. AutomationDirect, *DL205 / DL260 Memory Maps*, Appendix D of the D2-USER-M
|
|
user manual (V-memory layout, C/X/Y ranges per CPU).
|
|
5. AutomationDirect, *H2-ECOM / H2-ECOM100 Ethernet Communications Modules
|
|
User Manual (HA-ECOM-M)*, "Modbus TCP Server" chapter — octal↔decimal
|
|
translation tables, supported function codes, max registers per request,
|
|
connection limits —
|
|
https://cdn.automationdirect.com/static/manuals/hxecomm/hxecomm.html
|
|
6. Inductive Automation, *Ignition Modbus Driver — Address Mapping*, word
|
|
order options (ABCD/CDAB/BADC/DCBA) —
|
|
https://docs.inductiveautomation.com/docs/8.1/ignition-modules/opc-ua/drivers/modbus-v2
|
|
7. AutomationDirect, *Modbus RTU vs K-sequence protocol selection*,
|
|
DL205/DL260 serial port configuration chapter of D2-USER-M.
|
|
8. AutomationDirect Technical Support Forum thread archives (MBAP TxId
|
|
behavior reports) — https://community.automationdirect.com/ (search:
|
|
"ECOM100 transaction id"). _Unconfirmed_ operator reports only.
|