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>
16 KiB
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 (
0x00in the character byte). There is no length prefix. Writes must pad the final register's unused byte with0x00. - 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
KSTRstring 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
VPRINTinstruction 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-C1777octal 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].Xinputs map to Modbus discrete inputs starting at FC02 address 0;Youtputs 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, not0x0401.
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 Swappedand Ignition callsCDAB[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 = 1234in ladder stores0x1234on the wire, not0x04D2. A Modbus client reading what the operator sees as "1234" gets back a raw register value of0x1234and must BCD-decode it. Float32 values are only IEEE 754 if the ladder programmer usedLDR/OUTRinstructions [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 isCDAB[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-sequencemode, notModbusmode, 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.ProbeAddressdefault 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,04on 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
- 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
- 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 - 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
- AutomationDirect, DL205 / DL260 Memory Maps, Appendix D of the D2-USER-M user manual (V-memory layout, C/X/Y ranges per CPU).
- 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
- 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
- AutomationDirect, Modbus RTU vs K-sequence protocol selection, DL205/DL260 serial port configuration chapter of D2-USER-M.
- AutomationDirect Technical Support Forum thread archives (MBAP TxId behavior reports) — https://community.automationdirect.com/ (search: "ECOM100 transaction id"). Unconfirmed operator reports only.