TIA Portal Mixed-Type Data in S7 DB Arrays: STRUCT, UDT, Variant

David Krause12 min read
SiemensTechnical ReferenceTIA Portal
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

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.

Field note: The homogeneity rule applies to S7-300 (STEP 7 V5.x), S7-400, S7-1200, and S7-1500. There is no firmware version, no TIA Portal version, and no optional package that breaks this rule. Attempting to declare a heterogeneous array is a language-level violation, not a controller limitation.

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;
Trade-off: A 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
Constraint: Both views must occupy the same start address. The TIA Portal compiler checks the length of "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;
Warning: Pointer arithmetic is not type-checked. The SCL compiler will not catch a stride mismatch. Always derive the stride from 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

  1. 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.
  2. 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.
  3. 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.
  4. Memory view. Switch the watch table to Absolute addressing. Verify the byte layout: Raw at offset 0 (2 B), Scaled at offset 2 (4 B), Enabled at offset 6 (1 B), padding byte at offset 7, Label starting at offset 8 (STRING[32] = 34 B including header). Total = 42 B per record; record n starts at offset 42·n.
  5. 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 capture Channels[0] over time and confirm a single assignment writes the full record atomically (no intermediate state visible to HMI).
  6. 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.

Back to blog