Resolving S7-1200 TSEND_C STRING Data in STRUCT Transfer Errors
When transferring batch numbers, lot codes, or any alphanumeric identifier between two SIMATIC S7-1200 CPUs over Industrial Ethernet, engineers frequently encounter a behavior where a STRING element declared inside a STRUCT cannot be used as a source operand on the DATA input of TSEND_C. The error appears even though a stand-alone STRING tag at the data block root transfers without complaint. This article walks through the underlying cause of that behavior, the precise byte-level layout of the S7-1200 STRING data type, three field-proven workarounds, the rules for running multiple TSEND_C / TRCV_C pairs in parallel, and the connection-resource limits you must respect on CPU firmware V4.x and V5.x.
STRING storage layout and the TSEND_C / TRCV_C instruction behavior are defined in the S7-1200 System Manual and the STEP 7 Basic / Professional online help. See SIMATIC S7-1200 Programmable Controller System Manual (09/2023).
1. Problem Description
The reported symptom in TIA Portal is one or more of the following compiler and runtime conditions:
- The STRING entry in the data block type-selection drop-down appears red when added under a
STRUCTin the data block editor. - Connecting a tag of type
STRINGdeclared inside aSTRUCTto theDATAinput pin ofTSEND_Cproduces a compile error indicating that the data type is not permitted at this location. - Attempting to use
MOVE_BLKfrom"DB".stringto"DB".array_of_charproduces the compiler messages "only ARRAY elements can be used here" and "Access of elements of data type STRING is not allowed here". - Selecting
BLKMOVon a partial slice of aSTRINGis rejected by the compiler with the same access error.
Despite the failure inside the STRUCT, a top-level STRING tag in the same data block transmits and receives correctly when bound to TSEND_C.DATA and TRCV_C.DATA as a flat element. The inconsistency is the actual root cause: the TIA Portal compiler and the TSEND_C instruction treat struct member visibility differently from block-root tag visibility depending on firmware version.
2. Root Cause: How STRING Is Stored in the S7-1200
The S7-1200 STRING data type is not a flat sequence of ASCII bytes. It is a header-prefixed byte array with the layout shown in the table below. This layout is documented in the S7-1200 System Manual, Chapter 4 "Data types".
| Byte Offset | Contents | Description |
|---|---|---|
| 0 | Maximum length | One byte holding the maximum number of characters the string can contain (default 254). |
| 1 | Actual length | One byte holding the number of valid ASCII characters currently stored. |
| 2 .. n+1 | ASCII character payload | The character data itself, starting with the first character at byte offset 2 of the tag. |
| n+2 .. 255 | Unused / zero fill | Filler up to the declared maximum. Initialized to 16#00. |
The full byte footprint of a STRING element is therefore 2 + max_length. A STRING[254] occupies 256 bytes, a STRING[10] occupies 12 bytes, and so on. When the tag is placed inside a STRUCT, the two-byte header is still present at the beginning of the struct member, and the DATA input of TSEND_C would, by default, transmit the entire 2 + max_length block — including the two header bytes that the receiver cannot interpret as ASCII.
This is exactly why TSEND_C with DATA = "DB".batch works when the batch tag is at the block root (the receiver buffers the whole 256 bytes and discards the header), but behaves inconsistently when batch is a member of a nested STRUCT: on some firmware versions the compiler refuses the binding outright, and on others the binding succeeds but the receiving CPU cannot decode the header bytes as part of the user data because the struct layout shifts the offsets of subsequent fields.
3. Why the Compiler Rejects Strings Inside Structs
When you create a data block in TIA Portal and add a STRUCT, then try to declare an element whose data type is STRING, the editor displays the type in red. The compiler does not actually forbid the declaration; it is signalling that the default maximum length of 254 cannot be inferred for a nested element. The remedy is to specify the maximum length explicitly, for example STRING[20]. The declaration must include the bracketed length:
TYPE "udtBatch"
STRUCT
BatchNumber : STRING[20]; // 22 bytes total: 2 header + 20 chars
Quantity : INT; // 2 bytes
Operator : STRING[8]; // 10 bytes total
END_STRUCT;
END_TYPE
If the brackets are omitted, TIA Portal flags the entry. Once the length is declared, the element is valid inside the struct, can be referenced as "DB".Batch.BatchNumber, and can be bound to TSEND_C.DATA as a single, flat tag.
IN_OUT parameter to a function block, because the block interface must know the byte footprint at compile time.
4. Solution 1 — Declare STRING With Explicit Length Inside the Struct
This is the smallest possible change. It keeps the STRING element but forces TIA Portal to allocate the exact byte footprint, which the compiler and the TSEND_C instruction can both understand.
Step-by-step
- Open the data block in TIA Portal.
- Inside the
STRUCT, create a new element. - Set Data type to
STRING. - In the same row, after the data type, change the declaration from
STRINGtoSTRING[20](replace 20 with your required maximum). - Compile the block. The red highlight on the type name disappears.
- Bind the element to
TSEND_C.DATAas"MyDB".MyStruct.BatchNumber.
Verification
Compile the project. The S7-1200 accepts the binding without error. Place the CPU in RUN, trigger a send, and confirm at the receiver that the entire 2 + length block arrives in the matching TRCV_C receive area. Read back the received string with the online watch table; the first two bytes are the header (max and actual length) and the ASCII payload starts at byte offset 2.
5. Solution 2 — Replace STRING With ARRAY OF CHAR Inside the Struct
Many engineers prefer a flat byte view of the payload because the receiving CPU can index the array without first reading the length prefix. The replacement is mechanical:
TYPE "udtBatch"
STRUCT
BatchNumber : ARRAY[1..20] OF CHAR; // 20 bytes, no header
Quantity : INT;
Operator : ARRAY[1..8] OF CHAR; // 8 bytes
END_STRUCT;
END_TYPE
With this layout you lose the two-byte length header — the receiver cannot know how many characters are "valid" without a separate INT length tag. To compensate, add a length field:
TYPE "udtBatch"
STRUCT
BatchNumberLen : INT; // number of valid characters
BatchNumber : ARRAY[1..20] OF CHAR; // payload
Quantity : INT;
OperatorLen : INT;
Operator : ARRAY[1..8] OF CHAR;
END_STRUCT;
END_TYPE
This variant transfers cleanly with TSEND_C because every element is a fixed-width primitive, and there is no STRING header to confuse the receiving PLC. It is the approach recommended in the STEP 7 Basic V19 Online Help entry for TSEND_C / TRCV_C when transmitting structures that contain alphanumeric data.
Copying a STRING into an ARRAY OF CHAR
To move a value from a STRING field into the ARRAY OF CHAR field at runtime, use the BLKMOV (block move) instruction with an ANY pointer that points at byte offset 2 of the source string. The snippet below shows the parameterization in Structured Text for a 20-character payload:
// Source: "DB_Src".stBatch.BatchNumber (STRING[20], starts at DB_Src.DB[0])
// Target: "DB_Dst".stBatch.BatchNumber (ARRAY[1..20] OF CHAR)
// Copy 20 bytes starting from the 3rd byte of the source STRING.
"DB_Dst".stBatch.BatchNumberLen := 20;
BLKMOV(
SRCBLK := P#"DB_Src".stBatch.BatchNumber Byte 3, // skip 2-byte header
RET_VAL := "DB_Dst".stBatch.iBLKMOV_RetVal,
DSTBLK := "DB_Dst".stBatch.BatchNumber
);
The expression P#"DB_Src".stBatch.BatchNumber Byte 3 builds an ANY pointer of type BYTE with a 20-byte length, anchored at byte offset 3 of the source string. The first two bytes of the source — the maximum-length and actual-length fields — are skipped, and exactly the 20 ASCII characters are copied into the target array.
"DB_Src".stBatch.BatchNumber[3] as the source operand is rejected with "Access of elements of data type STRING is not allowed here". The compiler permits array-element access but not string-element access. Use an ANY pointer, as shown, to bypass the type check.
6. Solution 3 — Run Two TSEND_C / TRCV_C Pairs in Parallel
The original poster also asked whether two TSEND_C instances on the sender can target two TRCV_C instances on the receiver without cross-talk. The answer is yes, subject to three constraints:
-
Distinct connection IDs. Every
TSEND_CandTRCV_Cblock must use a uniqueIDvalue in the range 0x0001 to 0x0FFF (the user connection range for the S7-1200 open user communication). - Distinct connection resources. The S7-1200 CPU firmware defines the maximum number of parallel open-user-communication connections. CPU firmware V4.x supports up to 8 active connections for TSEND_C / TRCV_C pairs. CPU firmware V5.x extends this to 16. See S7-1200 System Manual, Chapter on Communication.
-
Distinct connection parameters. Each pair needs its own
CONNECTparameter block. For ISO-on-TCP connections this is aTCON_IP_v4(orTCON_IP_RFC_v4) block with its own remote IP, remote TSAP, and local TSAP.
The configuration in the project tree typically looks like the following. Connection_1 and Connection_2 are two separate TCON configuration blocks, each referenced by its own TSEND_C and TRCV_C instance on each side.
// --- Sender (CPU A) ---
"instTSEND_C_1"(REQ := bTrigger1, DATA := "DB".Batch,
CONNECT := "Connection_1", ID := 16#0001);
"instTSEND_C_2"(REQ := bTrigger2, DATA := "DB".Quantity,
CONNECT := "Connection_2", ID := 16#0002);
// --- Receiver (CPU B) ---
"instTRCV_C_1"(EN_R := TRUE, DATA := "DB_R".Batch,
CONNECT := "Connection_1", ID := 16#0001);
"instTRCV_C_2"(EN_R := TRUE, DATA := "DB_R".Quantity,
CONNECT := "Connection_2", ID := 16#0002);
The block pairs are isolated at the TCP layer. The data sent on TSEND_C_1 / ID 16#0001 arrives only at TRCV_C_1 / ID 16#0001 on the partner. There is no broadcast or fan-out: a single TSEND_C cannot deliver to two TRCV_C blocks, and a single TRCV_C cannot receive from two TSEND_C blocks.
7. Maximum Data Block Size for TSEND_C / TRCV_C
The S7-1200 TSEND_C instruction accepts a DATA parameter of any length up to the limit of the CPU. Practical upper bounds are summarized in the table below. The values apply to firmware V4.2 and later, which is when TSEND_C replaced the older TSEND / TRCV pair.
| Parameter | Limit | Notes |
|---|---|---|
| Maximum LEN per send call | 32,768 bytes (32 KB) | Length is the number of bytes transferred in a single REQ. |
| Maximum data block size | 64 KB (work memory limit) | Limited by CPU work memory; see System Manual for the specific CPU order number. |
| Maximum open user communication connections | 8 (FW V4.x), 16 (FW V5.x) | Sum of active TSEND_C, TRCV_C, TCON, TDISCON, and TUSEND / TURCV instances. |
| Local TSAP range | 16#0001 to 16#0100 | Each connection needs a unique local TSAP on each side. |
For the original use case — a single batch number — a 256-byte STRING or a 32-byte ARRAY OF CHAR is far below any of these limits. The size-related "odd behavior" mentioned in the source content is therefore almost certainly caused by the STRING length declaration, not by a connection-level payload limit.
8. Status and Error Codes for TSEND_C / TRCV_C
The STATUS output of TSEND_C and TRCV_C returns a 16-bit word. The low byte carries the standard STATUS from TCON, TDISCON, TSEND, or TRCV depending on which sub-step is active. The high byte carries the function result. Selected codes that the field engineer will most often see:
| STATUS (hex) | Meaning | Recommended Action |
|---|---|---|
| 16#0000 | Connection established, no error. | None. |
| 16#7000 | Block idle, no job active. | None. |
| 16#7001 | Block waiting for partner. | Check that the partner CPU is in RUN and that TRCV_C is called there with the same ID and CONNECT. |
| 16#7002 | Data being sent. | None — informational. |
| 16#7003 | Connection being established. | None — transient during first call. |
| 16#7004 | Connection established, no data yet. | Trigger a send from the partner. |
| 16#8085 | LEN parameter is 0 or greater than the data area. | Re-check LEN; set explicitly or assign 0 to use the full length of DATA. |
| 16#80A1 | Connection or port already in use. | Verify that no other block is using the same ID or local TSAP. |
| 16#80AB | Data length exceeds maximum. | Reduce LEN or split the payload across multiple REQ cycles. |
| 16#80C3 | Temporary resource shortage. | Retry; if persistent, reduce the number of parallel open-user-communication blocks. |
| 16#80C4 | Connection terminated by partner (TDISCON / power off / reset). | Allow the block to re-establish automatically; trigger a fresh REQ after BUSY clears. |
| 16#80D2 | Confirm denied — partner rejected the connection. | Check the partner's TRCV_C / TCON configuration. |
These codes are documented in full in the STEP 7 online help under TSEND_C > STATUS output parameter and TRCV_C > STATUS output parameter, mirrored in Siemens Support entry 109748124.
9. Step-by-Step Commissioning Procedure
The following procedure assumes two S7-1200 CPUs on the same subnet, a single ISO-on-TCP connection, and the ARRAY OF CHAR solution for batch numbers. Adapt the IP addresses and TSAP values to the plant.
9.1 Prerequisites
- Both CPUs are configured in the same TIA Portal project, each with its own PROFINET interface and IP address (for example, CPU A: 192.168.0.10; CPU B: 192.168.0.11).
- The Ethernet subnet mask on both interfaces is identical (255.255.255.0 in this example).
- The PROFINET cable is connected between the two CPUs or via a switch.
- Both CPUs are online-reachable from the programming device.
- Firmware on both CPUs is V4.2 or later (V4.0 / V4.1 also work but predate some of the open-user-communication enhancements).
9.2 Create the connection block
- In the project tree of CPU A, navigate to Devices & Networks > Network view.
- Click the PROFINET interface of CPU A, then Properties > Ethernet addresses, and confirm the IP.
- Repeat for CPU B.
- From the right-hand Instructions palette, drag
TSEND_CintoOB1of CPU A. - In the project tree of CPU A, double-click Connections; the connection wizard opens.
- Select the partner as CPU B, choose ISO-on-TCP connection, set a unique local TSAP (for example
16#0001) and remote TSAP (also16#0001). - Confirm. TIA Portal generates a
Connection_1data block. - Repeat for a second pair if you want to keep batch and quantity on separate connections; otherwise use one connection for both.
9.3 Create the data blocks
- On CPU A, add a new global data block called
DB_Sendwith Standard access mode (not Optimized — open user communication requires standard block access on firmware V4.x; firmware V5.x supports optimized blocks for TSEND_C / TRCV_C). See S7-1200 System Manual, "Communication". - Add the following elements:
DATA_BLOCK "DB_Send" STRUCT BatchLen : INT; Batch : ARRAY[1..20] OF CHAR; Quantity : INT; END_STRUCT; END_DATA_BLOCK - Create a matching
DB_Rcvon CPU B with identical structure.
9.4 Wire up TSEND_C
// OB1, network 1 — send batch number on rising edge of bTrigger
"instTSEND_C"(
REQ := bTrigger,
CONT := TRUE,
LEN := 22, // 2 (INT length) + 20 (CHAR payload)
CONNECT := "Connection_1",
ID := 16#0001,
DATA := "DB_Send", // whole DB; first 22 bytes are INT + ARRAY OF CHAR
COM_RST := FALSE,
DONE => bDone,
BUSY => bBusy,
ERROR => bError,
STATUS => wStatus
);
9.5 Wire up TRCV_C on the partner
// OB1, network 1 — receive batch number
"instTRCV_C"(
EN_R := TRUE,
CONT := TRUE,
LEN := 22,
CONNECT := "Connection_1",
ID := 16#0001,
DATA := "DB_Rcv",
COM_RST := FALSE,
DONE => bDone,
BUSY => bBusy,
ERROR => bError,
STATUS => wStatus
);
9.6 Verification
- Download both station configurations to the CPUs.
- Set both CPUs to
RUN. - Open an online watch table on CPU A and write 20 ASCII characters into
DB_Send.Batch; write 22 intoDB_Send.BatchLen. - Trigger
bTriggeron CPU A. Observe thatbBusypulses, thenbDoneasserts andwStatusreads 16#0000. - On CPU B's online watch table, observe that
DB_Rcv.Batch[1]throughDB_Rcv.Batch[20]now contain the same characters andDB_Rcv.BatchLenreads 22. - Force a fault condition by setting
LENto 0. ObserveSTATUS = 16#8085(LEN out of range), confirming that the diagnostic code path is wired correctly.
10. Troubleshooting Matrix
| Symptom | Likely Cause | Remedy |
|---|---|---|
| STRING element in struct appears red in the type column. | Maximum length not declared. | Change STRING to STRING[n] with the required maximum length. |
| Compiler error: Access of elements of data type STRING is not allowed here. |
MOVE_BLK / BLKMOV on a string slice is not permitted. |
Use an ANY pointer at byte offset 3 of the string as the source. |
| Compiler error: only ARRAY elements can be used here. | A scalar element of a STRING field is referenced like an array element. |
Declare the destination as ARRAY[..] OF CHAR or use a temporary ANY pointer. |
STATUS 16#8085 on TSEND_C. |
LEN < 1 or LEN > byte length of DATA. | Compute LEN from the byte width of the tag; or assign 0 to send the whole tag. |
| STATUS 16#80A1 on first call. | Duplicate ID or local TSAP. | Verify that each TSEND_C instance uses a unique ID and that each connection has a distinct local TSAP. |
| STATUS 16#7001 stays set indefinitely. | Partner TRCV_C never executes (OB1 priority), wrong IP, wrong TSAP, wrong ID. | Verify IP routing, partner CPU in RUN, partner TRCV_C.ID matches sender's ID. |
| Receiver sees ASCII characters preceded by garbage bytes. | Entire STRING (header + payload) transmitted without offset. | Either transmit only the payload with an ANY pointer at byte offset 3, or accept the header and strip it on the receiver with the same technique. |
| Connection works for a single send, fails after 8 parallel jobs. | Open-user-communication connection limit exceeded on CPU firmware V4.x. | Reduce the number of parallel connections or upgrade to firmware V5.x which lifts the limit to 16. |
11. Best Practices
-
Always declare the maximum string length with brackets.
STRINGwithout a length is invalid as a struct element and ambiguous as a block parameter. - Prefer ARRAY OF CHAR for transmitted payloads. A flat byte layout avoids the two-byte header, simplifies length handling on the receiver, and removes the most common compile errors.
- Reserve a separate INT length tag. Without it, the receiver cannot determine how many bytes are valid ASCII.
- Use unique IDs and TSAPs. Two TSEND_C blocks on the same CPU must use different IDs and different local TSAPs; the same applies to the partner.
-
Monitor STATUS at every cycle. Logging
STATUSto a watch table or HMI tag is the fastest way to localize a connection or data-length fault. - Avoid optimized blocks for TSEND_C on firmware V4.x. Open user communication historically requires standard (non-optimized) data block access on these firmware versions. Firmware V5.x removes this restriction; check the CPU's manual for the exact release notes.
-
Document the byte layout. When sending a
STRUCT, the partner'sTRCV_Cmust reference a struct of identical layout and identical length; mismatches cause silent truncation.
12. FAQ
Why does TSEND_C reject a STRING element declared inside a STRUCT but accept the same STRING at the data block root?
The S7-1200 STRING type occupies 2 + max_length bytes because of the max-length and actual-length header. TIA Portal refuses an unparameterized STRING inside a STRUCT because it cannot infer the byte footprint. Declare the length explicitly, for example STRING[20], and the binding becomes valid on every supported firmware version.
Can two TSEND_C blocks on one S7-1200 send to two TRCV_C blocks on the partner at the same time?
Yes. Each TSEND_C / TRCV_C pair must use a unique ID in the range 16#0001 to 16#0FFF and a unique local TSAP per connection. CPU firmware V4.x supports up to 8 parallel open-user-communication connections; firmware V5.x supports up to 16. The data path is fully isolated at the TCP layer, so TSEND_C_1 cannot reach TRCV_C_2.
How do I copy the ASCII payload of a STRING into an ARRAY OF CHAR without losing the first two characters?
Use BLKMOV with an ANY pointer anchored at byte offset 3 of the source STRING, for example P#"DB_Src".Tag Byte 3. The pointer's data type is BYTE, the byte length equals the array size, and the two header bytes are skipped. Do not use MOVE_BLK on individual string elements — the compiler rejects element access on a STRING.
What is the maximum payload that TSEND_C can transfer in a single REQ on the S7-1200?
32,768 bytes per send call. For larger payloads, segment the data and trigger a new REQ after each DONE. CPU work memory additionally caps the absolute data block size to roughly 64 KB on standard CPUs; larger data blocks require firmware V5.x or the S7-1500 platform.
Does TSEND_C require the data block to be non-optimized?
On firmware V4.x, yes — TSEND_C / TRCV_C require standard (non-optimized) block access because the ANY pointer passed to the instruction encodes the absolute byte offset. Firmware V5.x extends open user communication to support optimized blocks; verify with the S7-1200 System Manual for the exact CPU order number before relying on optimized blocks in production.