1. Problem: A Siemens S7 Array Can Hold Only One Data Type
In SIMATIC S7-1200/S7-1500 programming guideline and the S7-300/400 reference, every ARRAY declaration in a Data Block (DB), FB static area, or tag table is strictly homogeneous: every element of ARRAY[x..y] OF <type> must resolve to the same elementary type, the same STRUCT, the same UDT, or the same PLC data type. The original code,
IF Enable THEN
FOR i := 0 TO (NoMaxVar - 1) BY 1 DO
ACC.VARIABLES[BufferLevel, i] := NewData[i];
END_FOR;
END_IF;
only compiles when VARIABLES is declared as ARRAY[*, *] OF <single_type> and NewData is the same <single_type>. The SCL compiler rejects any attempt to declare a 2-D array whose columns carry different elementary types (INT, REAL, BOOL, STRING). The TIA Portal compiler reports "Incompatible types in ARRAY definition" or "Element types do not match" at compile time of the DB.
2. Why Siemens Enforces Homogeneity
The S7 runtime computes the byte offset of an element with a single multiplication: offset = (index - low_limit) * sizeof(element). Mixed-type elements would require a non-contiguous stride or per-element type tags, neither of which the load/store microcode of the S7 CPU family supports. The same constraint is documented in IEC 61131-3, table 17, where array_type is defined with a single type_name. The PLCopen TC6 working group kept the rule when defining the SCL extensions used by STEP 7 / TIA Portal.
For 2-D arrays the stride is fixed per column, so ACC.VARIABLES[BufferLevel, i] is effectively a sliced homogeneous array across one row. The runtime cannot re-interpret the bytes of ACC.VARIABLES[BufferLevel, 0] as INT if ACC.VARIABLES[BufferLevel, 1] is declared REAL.
3. Solution Map: Five Engineering-Approved Patterns
| Pattern | Memory layout | S7-300/400 | S7-1200 | S7-1500 | Indexed access |
|---|---|---|---|---|---|
| Parallel arrays per type | Contiguous per type | Yes | Yes | Yes | Yes (per array) |
| ARRAY OF STRUCT (inline) | Contiguous record | Yes | Yes | Yes | Yes (by record) |
| ARRAY OF UDT | Contiguous record | Yes (DBT) | Yes (PLC DT) | Yes (PLC DT) | Yes (by record) |
| AT-view overlay | Shared memory | No | Yes (V4+) | Yes (V1+) | Yes (overlay) |
| VARIANT + MOVE_BLK_VARIANT | Generic, by ref | Limited | Yes (V4.4+) | Yes (V1.8+) | Yes (by ref) |
Each pattern addresses a different requirement: data-table homogeneity, reusable record definition, type-agnostic blocks, or generic field-bus exchange. Choose the pattern that matches the data being buffered, not the pattern that is shortest to type.
4. Pattern A — Parallel Arrays of One Type Each
This is the simplest extension of the original code. Keep VARIABLES as a 2-D homogeneous array, but allocate one 2-D array per elementary type and a parallel BufferLevel index:
// DB "ACC" declarations (non-optimized or optimized both work)
DATA_BLOCK ACC
STRUCT
VARIABLES_INT : ARRAY[0..15, 0..63] OF INT; // 2 kB
VARIABLES_REAL : ARRAY[0..15, 0..63] OF REAL; // 4 kB
VARIABLES_BOOL : ARRAY[0..15, 0..63] OF BOOL; // 128 B
VARIABLES_STRING: ARRAY[0..15, 0..3] OF STRING[80]; // ~5 kB
END_STRUCT;
END_DATA_BLOCK
// SCL access
IF Enable THEN
IF TypeTag[i] = 1 THEN // INT slot
ACC.VARIABLES_INT [BufferLevel, i] := NewData_INT [i];
ELSIF TypeTag[i] = 2 THEN // REAL slot
ACC.VARIABLES_REAL [BufferLevel, i] := NewData_REAL [i];
ELSIF TypeTag[i] = 3 THEN // BOOL slot
ACC.VARIABLES_BOOL [BufferLevel, i] := NewData_BOOL [i];
END_IF;
END_IF;
TypeTag array (typically ARRAY[..] OF USINT or WORD) becomes the dispatcher. The pattern preserves the 1-D FOR i loop of the original snippet, but the code forks into one IF/CASE branch per supported type. It is the only pattern that works on S7-300 CPUs (CPU 315, 317, 319) running classic STEP 7 V5.x with SCL compiled to MC7.5. Pattern B — ARRAY OF STRUCT (Inline Record)
Replace the 2-D array with an array of records. Each record carries one of every supported type, so a single VARIABLES[BufferLevel] returns a heterogeneous row by design:
TYPE Record_t
STRUCT
Slot_INT : INT;
Slot_REAL : REAL;
Slot_BOOL : BOOL;
Slot_STRING: STRING[80];
END_STRUCT;
END_TYPE
DATA_BLOCK ACC
STRUCT
VARIABLES : ARRAY[0..15] OF Record_t;
END_STRUCT;
END_DATA_BLOCK
// Read/write the i-th record
IF Enable THEN
ACC.VARIABLES[BufferLevel].Slot_INT := NewData[i].Slot_INT;
ACC.VARIABLES[BufferLevel].Slot_REAL := NewData[i].Slot_REAL;
ACC.VARIABLES[BufferLevel].Slot_BOOL := NewData[i].Slot_BOOL;
ACC.VARIABLES[BufferLevel].Slot_STRING:= NewData[i].Slot_STRING;
END_IF;
Layout cost for the example: 2 + 4 + 1 + 82 (STRING[80] header 2 B + 80 B payload) = 89 B per record, aligned by the compiler. For 16 records the DB is ~1.4 kB plus the standard STRING overhead. Optimized DB access hides the alignment, so any individual tag is reachable by symbolic name from the PLC tag table.
Use STRUCT when the record is local to one DB. Promote to UDT the moment the same record is needed in two DBs (Pattern C).
6. Pattern C — UDT (PLC Data Type in TIA Portal)
A User-Defined Data Type — "PLC data type" in the TIA Portal tree — is a named, versioned struct that can be reused across DBs, FB static sections, and interfaces. The TIA Portal PLC data type programming guide documents that modifying a UDT propagates a recompile to every block that uses it; in STEP 7 V5.x the equivalent is the DBT (Data Block Type).
// TIA Portal: "Add new PLC data type" -> "Channel"
TYPE "Channel"
STRUCT
Raw : INT;
Scaled : REAL;
Enabled : BOOL;
Label : STRING[32];
END_STRUCT;
END_TYPE
// DB "ACC"
DATA_BLOCK "ACC"
STRUCT
Channels : ARRAY[0..255] OF "Channel";
END_STRUCT;
END_DATA_BLOCK
To use the original FOR i loop, the input array must also be of type "Channel":
IF Enable THEN
FOR i := 0 TO (NoMaxVar - 1) BY 1 DO
ACC.Channels[i] := NewChannel[i]; // NewChannel : ARRAY[*] OF "Channel"
END_FOR;
END_IF;
The := assignment copies the entire record. If the SCL compiler reports "Left side cannot be assigned to" on an optimized DB, expose the DB with "Accessible from HMI/OPC UA" and set the block attribute "Set in IDB" to no, or move the record into an FB static Channels (recommended for encapsulation).
7. Pattern D — AT-View Overlay (Type Reinterpretation)
The AT overlay, introduced for S7-1500 in firmware V1.0 and for S7-1200 in firmware V4.0, lets one memory area be viewed as multiple types simultaneously. The original AT-view documentation shows the syntax:
DATA_BLOCK "ACC"
STRUCT
Raw : ARRAY[0..1023] OF BYTE; // 1 kB physical buffer
Raw_AsWord : ARRAY[0..511] OF WORD AT Raw; // overlapping view
Raw_AsReal : ARRAY[0..255] OF REAL AT Raw;
END_STRUCT;
END_DATA_BLOCK
You can also overlay a struct of mixed type on a raw byte array, which is the closest equivalent to a "mixed-type array" the S7 language permits:
DATA_BLOCK "ACC"
STRUCT
Buffer : ARRAY[0..15, 0..63] OF BYTE; // 1 kB
AsMixed: ARRAY[0..15] OF "Channel" AT Buffer; // 16 records of 64 B each
END_STRUCT;
END_DATA_BLOCK
"Channel" and rejects the overlay if it exceeds the underlying byte count. The pattern is not available on S7-300/S7-400.8. Pattern E — VARIANT, MOVE_BLK_VARIANT, and Type-Agnostic FBs
For blocks that must consume a record of any type — common in modular instrumentation, OPC UA pub/sub, or recipe handling — declare the formal parameter as VARIANT and use the type-checking instructions from the S7-1500 extended instructions:
FUNCTION_BLOCK "TypeSafeWriter"
{ S7_Optimized_Access := 'TRUE' }
VAR_INPUT
Enable : BOOL;
pBuffer : POINTER TO BYTE; // destination
pSource : VARIANT; // any record
ByteLen : UDINT; // length of one record
END_VAR
VAR
srcOffset : DINT;
ret : INT;
END_VAR
BEGIN
IF Enable THEN
ret := MOVE_BLK_VARIANT(
SRCBLK := pSource,
INDEX := 0,
COUNT := 1,
SRCINDEX := 0,
DSTBLK := pBuffer,
DSTINDEX := srcOffset);
END_IF;
END_FUNCTION_BLOCK
To validate that pSource is actually a "Channel", use the TypeOf() instruction from the Extensions / Type handling palette in TIA Portal V16+. Pair this with IS_ARRAY, IS_STRUCT, and the VariantGet/VariantPut helpers to write a generic dispatcher. The "Programming guideline for S7-1200/S7-1500" lists the available type-query instructions and the firmware required for each.
9. Pointer Arithmetic: Why P#DBx.DBBy and POKER Are Still Useful
Indirect access by symbolic index — what the original snippet does with ACC.VARIABLES[BufferLevel, i] — is the safe, compiler-checked path. When a library must walk an array without knowing the type (e.g. a recipe loader), the legacy POINTER TO BYTE plus P# pointer arithmetic is still the lowest-level portable technique:
VAR_TEMP
pRecord : POINTER TO BYTE;
i : DINT;
offset : DINT;
END_VAR
IF Enable THEN
FOR i := 0 TO (NoMaxVar - 1) BY 1 DO
offset := i * SIZEOF("Channel"); // SCL intrinsic
pRecord := P#DB_ACC.DBX0.0 BYTE; // base pointer
pRecord := pRecord + offset; // walk the record
// Dereference with care: use STRUCT or AT-view to interpret the bytes.
END_FOR;
END_IF;
SIZEOF(UDT) and validate the destination length against the DB size. Optimized DBs restrict raw pointer access; switch to standard (non-optimized) DBs for this pattern, or use the new PEEK/POKE byte-level intrinsics on S7-1500 firmware V2.6+.10. S7-1500 Slice Access — A Modern Alternative
From firmware V2.0, S7-1500 CPUs support slice access on elementary types and on UDTs of fixed length. This is the most compact way to address a sub-field of a record without expanding it into a struct:
// All members of "Channel" are addressable by name without overlay
ACC.Channels[i].Scaled := 23.5;
ACC.Channels[i].Raw.%X0 := TRUE; // bit 0 of Raw (INT) on S7-1500
ACC.Channels[i].Raw.%B1 := 16#A5; // low byte of Raw
ACC.Channels[i].Label := 'Tank 04';
Slice access requires the DB to be marked optimized. The compiler resolves the slice offset at compile time; no runtime indexing cost is added. For arrays of UDTs this is the preferred pattern in green-field projects.
11. Optimized vs. Non-Optimized DB: What It Means for Mixed-Type Records
| Attribute | Optimized DB (default on S7-1500) | Non-optimized DB |
|---|---|---|
| Symbolic access | Yes, by name only | Yes (name) and absolute (e.g. DB100.DBD0) |
| Bit/byte/word/dword on member | Slice (%X, %B, %W) — S7-1500 fw V2.0+ | Direct (e.g. DB100.DBX0.0) |
| Pointer arithmetic to member | Not supported | Supported (with care) |
| AT-view overlay | Supported | Supported |
| Download in RUN | Yes (no real loss of state) | Restricted (re-init may be needed) |
| OPC UA / HMI binding | Native, fast | Slower; requires offsets |
For mixed-type records the recommended combination is optimized DB + UDT + slice access on S7-1500. Fall back to non-optimized only when the application must hand a typed pointer to a third-party C/C++ library through the S7 Open Development Kit (ODK).
12. Verification: Test the Mixed-Type Record End-to-End
- Compile. Right-click the project in TIA Portal and select Compile > Software (rebuild all). The SCL compiler must report 0 errors; warnings about UDT length are acceptable but should be reviewed.
- Load. Download the program to the PLC (CPU in STOP if the DB layout changed). The Online & Diagnostic view should report the new DB size matches the offline build.
-
Watch table. Open the DB in the watch table. Force
Channels[0].Raw := 1234,Channels[0].Scaled := 1.0,Channels[0].Enabled := TRUE. Confirm the symbolic values reflect the change. -
Memory view. Switch the watch table to Absolute addressing. Verify the byte layout:
Rawat offset 0 (2 B),Scaledat offset 2 (4 B),Enabledat offset 6 (1 B), padding byte at offset 7,Labelstarting at offset 8 (STRING[32] = 34 B including header). Total = 42 B per record; record n starts at offset 42·n. -
Loop test. From an SCL source file, run the
FOR i := 0 TO 255 DO Channels[i] := TestRecord[i]; END_FOR;and confirm each of the 256 records is written. Use the trace recorder on the S7-1500 to captureChannels[0]over time and confirm a single assignment writes the full record atomically (no intermediate state visible to HMI). - Cross-block read. From a second FB that imports the same UDT, perform a symbolic read. If the second block compiles, the type linkage is sound. If the second block fails to compile with "Type 'Channel' not declared in this block", add the PLC data type to the block's Used PLC data types list.
13. Troubleshooting Matrix
| Symptom | Root cause | Fix |
|---|---|---|
| "Incompatible types in ARRAY definition" | Trying to mix INT and REAL in the same array |
Switch to UDT/STRUCT or to Pattern A parallel arrays |
"Element type does not match" on ACC.VARIABLES[BufferLevel, i] := NewData[i]
|
NewData declared as a different elementary type | Match the type of NewData to the element type, or change the array to UDT |
| "POINTER to ARRAY is not supported" | Pointer arithmetic against an optimized DB | Use non-optimized DB, or use VARIANT + MOVE_BLK_VARIANT
|
| AT-view rejected with "Length mismatch" | Underlying byte array too short for the overlaid UDT | Resize the underlying array to RecordCount * SIZEOF(UDT)
|
Slice access %X0 rejected on S7-1200 |
Firmware < V4.0 (V4.0 introduced slice access for S7-1200) | Upgrade firmware or rewrite the access to a temp BYTE |
| STRING[] overflow at runtime | Target STRING length shorter than source | Match STRING widths, or use STRING[n] with n >= source width + 2 |
| HMI shows "Invalid value" on a UDT member | DB is non-optimized and HMI was bound to an absolute address that has shifted | Re-bind the HMI tag symbolically and re-download |
14. Frequently Asked Questions
Can a Siemens S7 ARRAY contain different elementary data types?
No. Every ARRAY[..] OF <type> in SCL resolves to a single elementary type, a single UDT/PLC data type, or a single STRUCT. To mix types, wrap them in a UDT/STRUCT and use ARRAY OF UDT, or keep one parallel array per type with a TypeTag dispatcher.
What is the difference between STRUCT and UDT (PLC data type) in TIA Portal?
A STRUCT is a one-off record declared inside a DB or FB static section. A UDT (called "PLC data type" in the TIA Portal project tree) is a named, versioned template that can be referenced from many DBs and FBs. Use a UDT the moment two blocks need the same record layout; the editor propagates changes to all consumers on recompile.
How do I reinterpret one byte array as mixed-type data?
Use the AT-view overlay, available on S7-1500 (all firmware) and S7-1200 (V4.0+). Declare an ARRAY[..] OF BYTE and add a second declaration of the same name with the keyword AT followed by the mixed-type UDT. The compiler checks that the overlaid structure fits inside the underlying byte buffer.
Can I use a pointer to walk an ARRAY OF UDT?
On non-optimized DBs yes, with POINTER TO BYTE plus SIZEOF("Channel") stride and the legacy P# syntax. On optimized DBs, prefer the new PEEK/POKE byte intrinsics (S7-1500 fw V2.6+) or a VARIANT parameter passed to MOVE_BLK_VARIANT. Both routes keep the operation type-checked by the compiler.
Which TIA Portal version introduced the VARIANT-based type dispatch?
The VARIANT data type and MOVE_BLK_VARIANT instruction shipped with STEP 7 V11 (TIA Portal) for S7-1500. Type queries such as TypeOf(), IS_STRUCT, and IS_ARRAY were added in V16 for S7-1500 and V4.4 for S7-1200. The programming guideline for S7-1200/S7-1500 lists the minimum firmware for every extended instruction.