# 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_`). ## 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.