Beckhoff ADS Symbolic Structure Transfer Between TwinCAT PLCs

Stefan Weidner6 min read
BeckhoffOther TopicTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview: Symbolic ADS for Inter-PLC Data Exchange

Passing a user-defined STRUCT between two Beckhoff TwinCAT PLCs by symbolic name (e.g., Main.receivedTrayData) eliminates the brittle practice of reserving overlapping %MB ranges or hard-coding byte offsets. The mechanism relies on the ADS symbol table being generated on the target PLC, the TwinCAT router being reachable, and the symbolic access function blocks resolving the variable by name at runtime.

This approach mirrors how the Beckhoff ADS .NET client and Node libraries such as jisotalo/ads-client resolve symbols via the ADS router. The same rules apply in-plc: the pointer supplied to the FB must address a variable inside the target PLC, and the symbol must exist in the compiled symbol description. See the Beckhoff Information System — pointer and symbol requirements.

Prerequisites

  1. TwinCAT 3 XAE on the engineering PC; TwinCAT 3 PLC build 4024 or later is recommended for the tc2_dataexchange library functions.
  2. Library reference to Tc2_DataExchange on both the source and target PLC projects.
  3. Static, routable TwinCAT AmsNetId for the target (example: 192.168.0.2.1.1) and the target's ADS port for the PLC runtime (851 for the first PLC instance, 852 for the second, and so on). Port mapping is documented in the Tc2_DataExchange reference.
  4. Symbol table generation enabled on the target PLC (Project → Properties → Compile Options → TwinCAT PLC): set Create symbol file / download symbol description to enabled. Without this file, FB_ReadAdsSymByName returns bError := TRUE with ADS error code 0x704 (symbol not found).
  5. Firewall rules: TCP/UDP 48898 (ADS) and UDP 8016 (broadcast) reachable between both controllers.
Symbol-availability rule: Per Beckhoff's documentation, the pointer passed to the symbolic FB must address a variable inside the target PLC. Pointers into a memory-mapped external area (%MB, retained memory outside the PLC instance) are rejected at runtime. See the symbol description requirement in the TwinCAT 3 PLC introduction.

Function Blocks: FB_WriteAdsSymByName and FB_ReadAdsSymByName

The two FBs that perform name-based ADS read and write are documented in the Tc2_DataExchange library — FB_ReadAdsSymByName and FB_WriteAdsSymByName.

Parameter Direction Type Description
sNetId INPUT STRING AmsNetId of the target PLC (e.g., '192.168.0.2.1.1').
nPort INPUT UINT ADS port of the target runtime (851 = PLC1, 852 = PLC2).
eComMode INPUT E_AdsComMode Use eAdsComModeSecureCom for routed ADS via TwinCAT router; eAdsComModeCycle triggers a fresh request each call.
tTimeout INPUT TIME ADS request timeout. Use DEFAULT_ADS_TIMEOUT as a baseline.
sVarName INPUT STRING Fully qualified symbolic path, case-sensitive: 'Main.receivedTrayData'.
nLen INPUT UDINT Payload size in bytes. Use SIZEOF() on the local variable — never hard-code a constant.
nSrcAddr / pDestAddr INPUT POINTER Address of the local variable; obtained with ADR(...).
bRead / bWrite INPUT BOOL Rising edge triggers the ADS transaction. Must be reset on every cycle until bBusy drops.
bBusy OUTPUT BOOL TRUE while the asynchronous request is in flight.
bError OUTPUT BOOL TRUE on ADS error; nErrId contains the ADS return code.
nErrId OUTPUT UDINT ADS error code (see verification section).

Structured Text Example: Writing a Tray Structure to a Peer PLC

The pattern below shows a four-state machine (IDLE → WRITE → BUSY → ERROR) modeled on the original discussion. The same pattern applies for FB_ReadAdsSymByName.

PROGRAM MAIN
VAR
    trigger           : BOOL;
    adsWriteStatus    : (IDLE, WRITE, BUSY, ERROR);

    adsWrite : FB_WriteAdsSymByName := (
        sNetId    := '192.168.0.2.1.1',
        nPort     := 851,
        eComMode  := E_AdsComMode.eAdsComModeSecureCom,
        tTimeout  := DEFAULT_ADS_TIMEOUT
    );

    sendTrayData      : MyTrayStructure;   (* payload, defined identically on both PLCs *)
    adsWriteBusyEdge  : BOOL;
END_VAR

CASE adsWriteStatus OF

    IDLE :
        IF trigger THEN
            trigger := FALSE;
            adsWriteStatus := WRITE;
        END_IF

    WRITE :
        adsWrite(bWrite := FALSE);
        adsWrite(
            sVarName  := 'Main.receivedTrayData',
            nLen      := SIZEOF(sendTrayData),
            nSrcAddr  := ADR(sendTrayData),
            bWrite    := TRUE
        );
        adsWriteStatus := BUSY;

    BUSY :
        adsWrite(bWrite := FALSE);
        IF NOT adsWrite.bBusy THEN
            IF NOT adsWrite.bError THEN
                adsWriteStatus := IDLE;
            ELSE
                adsWriteStatus := ERROR;
            END_IF;
        END_IF

    ERROR :
        (* Latch nErrId, raise alarm, retry after backoff *)

END_CASE
Rising-edge discipline: Holding bWrite := TRUE causes the FB to re-trigger every PLC cycle, which floods the ADS router. Always pulse bWrite := FALSE on subsequent calls until bBusy falls. This is the most common field bug with this library.

Struct Definitions Must Match on Both PLCs

ADS is a byte-stream protocol. The layout, padding, and __attribute__((packed)) status of MyTrayStructure on the sender must match the receiver or the receiver will interpret shifted data. If alignment is critical, declare the STRUCT with {attribute 'pack_mode' := '0'} on both sides, and confirm via the TwinCAT symbol table that Size matches SIZEOF() on each side.

Verification

  1. Set a breakpoint in the target PLC on the line that writes receivedTrayData; trigger from the sender; confirm the value updates.
  2. Use the TwinCAT 3 ADS Monitor on the engineering PC: filter on AmsNetId, watch for ReadWrite index-group 0xF005 (symbol info by name). Each transfer should produce one round-trip.
  3. Check adsWrite.nErrId after a transaction. Common codes:
    • 0x0 — no error.
    • 0x704 — symbol not found (case mismatch, missing namespace, or symbol table not downloaded).
    • 0x705 — symbol size mismatch; ensure SIZEOF() matches receiver layout.
    • 0x702 — target port unreachable; verify AmsNetId and that TwinCAT router is running on the target.
  4. For a third-party test, use the jisotalo/ads-client Node tool to read Main.receivedTrayData from the target and diff against the sender's sendTrayData.

When to Use EAP Instead

EtherCAT Automation Protocol (EAP) is appropriate when the second controller is a slave of the same EtherCAT segment and you need deterministic, cyclic transfer with protocol-level timestamping. ADS is preferable for ad-hoc, triggered transfers such as a tray hand-off. The trade-offs are covered in the plccoder ADS write-up and the TwinCAT 3 ADS introduction video.

Cross-Vendor Integration Notes

When a non-Beckhoff controller (e.g., Siemens S7-1500 or Allen-Bradley CompactLogix) must read the same tag symbolically, the Beijer Beckhoff ADS Symbolic v4.14 driver or the Siemens LCCF ADSCom block provide symbol-driven HMI/PLC access without manual offset calculation. The symbol table becomes the single source of truth across vendors.

FAQ

Do FB_ReadAdsSymByName and FB_WriteAdsSymByName support arrays and nested structures?

Yes. Both FBs operate on a byte-count payload resolved by name, so any user-defined type (ARRAY OF STRUCT, nested STRUCT, STRING) is supported as long as the symbol exists in the target's symbol table and the size returned by SIZEOF() matches on both sides.

Why does bError return 0x704 even though the variable is declared on the target?

Error 0x704 means the target's symbol file is empty or was not downloaded. Open the target's PLC project properties, enable Create symbol file, and rebuild. Symbols are uploaded at runtime by the PLC's ADS server; without them, name lookups fail.

Can I use FB_WriteAdsSymByName with bWrite := TRUE held continuously?

No. The FB is edge-triggered; holding the input high re-issues the request every PLC cycle and saturates the ADS router. Always reset bWrite := FALSE after the rising edge, then re-assert on the next trigger.

Which ADS port do I target on a second PLC runtime?

Port 851 is reserved for the first PLC runtime, 852 for the second, and so on. The mapping is fixed by the TwinCAT router and is referenced in the Tc2_DataExchange documentation.

Is symbolic ADS slower than index-group + offset access?

Yes — symbolic reads do a name lookup the first time, but Tc2_DataExchange caches the resolved handle. In typical configurations the cache hit makes subsequent transfers equivalent in latency to direct index-group calls. The trade-off is eliminated offset-management risk.

Back to blog