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
- TwinCAT 3 XAE on the engineering PC; TwinCAT 3 PLC build 4024 or later is recommended for the
tc2_dataexchangelibrary functions. - Library reference to
Tc2_DataExchangeon both the source and target PLC projects. - Static, routable TwinCAT AmsNetId for the target (example:
192.168.0.2.1.1) and the target's ADS port for the PLC runtime (851for the first PLC instance,852for the second, and so on). Port mapping is documented in the Tc2_DataExchange reference. - 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_ReadAdsSymByNamereturnsbError := TRUEwith ADS error code0x704(symbol not found). - Firewall rules: TCP/UDP 48898 (ADS) and UDP 8016 (broadcast) reachable between both controllers.
%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
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
- Set a breakpoint in the target PLC on the line that writes
receivedTrayData; trigger from the sender; confirm the value updates. - Use the TwinCAT 3 ADS Monitor on the engineering PC: filter on AmsNetId, watch for
ReadWriteindex-group0xF005(symbol info by name). Each transfer should produce one round-trip. - Check
adsWrite.nErrIdafter a transaction. Common codes:-
0x0— no error. -
0x704— symbol not found (case mismatch, missing namespace, or symbol table not downloaded). -
0x705— symbol size mismatch; ensureSIZEOF()matches receiver layout. -
0x702— target port unreachable; verify AmsNetId and that TwinCAT router is running on the target.
-
- For a third-party test, use the jisotalo/ads-client Node tool to read
Main.receivedTrayDatafrom the target and diff against the sender'ssendTrayData.
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.