Files
lmxopcua/docs/v2/dl205.md
Joseph Doherty 9de96554dc Phase 3 PR 41 — Document AutomationDirect DL205 / DL260 Modbus quirks. Adds docs/v2/dl205.md (~300 lines, 8 H2 sections, primary-source citations) covering every place the DL205/DL260 family diverges from textbook Modbus or has non-obvious behavior a generic client gets wrong. Replaces the placeholder _pending_ list in modbus-test-plan.md with a confirmed-behaviors table that doubles as the integration-test roadmap.
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>
2026-04-18 19:49:35 -04:00

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 (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 BCDV2000 = 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.