Fixing TIA Portal UDT Array Index Shift Between PLC and HMI Tags

David Krause12 min read
SiemensTIA PortalTroubleshooting
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

Problem Overview

When engineers declare an array with a non-zero lower bound inside a PLC user-defined data type (UDT) in TIA Portal V15 Update 3 and later, the same UDT exposed as an HMI tag displays the array as 0-based. For example, a UDT element declared as ARRAY[1..10] OF INT in the S7-1500 data block editor appears in WinCC (TIA Portal) tag handling as ARRAY[0..9] OF INT. The shift is silent, undocumented in the editor, and breaks any HMI script, faceplate, or trend that references an element by its PLC index.

Important: This is documented WinCC runtime behavior, not a TIA Portal compilation fault. The PLC program continues to operate against [1..10]; only the HMI view of the array element is renumbered. Misalignment between PLC logic and HMI visualization produces off-by-one reads, wrong machine labels, and intermittent tag-quality faults.

Affected Hardware and Software Versions

Component Part Number Firmware / Version Status
S7-1500 CPU (any family) 6ES7 5xx-xxxxx Firmware ≥ V2.0 Affected
ET 200SP PN/PN Coupler (as in source case) 6ES7158-3AD10-0XA0 Firmware V4.0+ Affected
TP1500 Comfort Panel (as in source case) 6AV2124-0QC02-0AX0 Image ≥ V15.0 Affected
TIA Portal (PLC + HMI) 6ES7822-0AA05-0YA5 / -0AE05 V15 Update 3 and later (V15, V15.1, V16, V17, V18, V19) Affected — by design
PLCSIM Advanced V2.0 SP1 / V3.0 Reproduced in simulation — no hardware required

The behavior persists across every TIA Portal release since V15 because the WinCC (TIA Portal) tag editor enforces a fixed zero-based representation for any UDT-internal array, regardless of how the PLC data type is declared. Siemens support classifies the behavior as "by design" for the WinCC Comfort / Advanced / Professional / Unified tag interface.

Root Cause Analysis

The PLC and the HMI use two different parsers for the same project:

  1. S7-1500 STEP 7 (PLC compiler): Honours the lower and upper array bounds as written in the UDT definition. An ARRAY[1..10] occupies 10 elements, indexed 1 through 10.
  2. WinCC (TIA Portal) HMI tag editor: Always reflects a UDT-derived array as 0-based (ARRAY[0..9]) in the tag list, faceplate interface, and tag-prefix generator. The element count is preserved; only the index origin is normalized.

The normalization occurs because WinCC stores tag references as an offset count from element 0, mirroring the OPC-DA and OPC-UA array semantics documented in the STEP 7 (TIA Portal) Programming and Operating Manual — S7-1500 and the OPC UA Part 3 Address Space model. The internal element count is identical (10 elements), so data width and memory map are unchanged; only the visible index origin shifts. From the HMI perspective, "Machine1" reads from offset 0 instead of offset 1, while the PLC logic still writes Machine1 to offset 1. The mismatch is invisible until the HMI script dereferences a non-zero index.

How to Reproduce the Shift in PLCSIM Advanced

  1. Open TIA Portal V15 Update 3 and create a new project named UDT_Array_Shift.
  2. Add an S7-1500 CPU or, as in the source case, an ET 200SP station with a PN/PN coupler (6ES7158-3AD10-0XA0).
  3. Add a TP1500 Comfort (6AV2124-0QC02-0AX0) to the same project and assign it as the HMI for the PLC.
  4. Create a new PLC data type typeMachineData containing:
TYPE "typeMachineData"
VERSION : 0.1
   STRUCT
      sName : STRING[20];
      iCount : INT;
      aMachines : ARRAY[1..10] OF INT;   // Lower bound intentionally 1
   END_STRUCT;
END_TYPE
  1. Instantiate typeMachineData as "dbMachineList" in a global DB.
  2. Compile the PLC project. Confirm the DB shows aMachines[1] ... aMachines[10] in the PLC tag list.
  3. Open the HMI tag editor, expand dbMachineList, and inspect aMachines. The HMI shows aMachines[0] ... aMachines[9].
  4. Drag a "Text field" or "Symbolic I/O field" onto the TP1500 screen and bind it to aMachines[5]. The HMI generates a tag prefix that points to element index 4 internally; the HMI's "5" reads the PLC's element 5, but the HMI's "1" reads the PLC's element 2.
Verification: Write 10 into dbMachineList.aMachines[1] in the PLC and read back via the HMI. The HMI's element 1 (its index 0) returns 10 only if you read PLC element 2. The mismatch is confirmed and the off-by-one is reproducible.

Solution Strategy Selection Matrix

# Strategy PLC Code Change HMI Code Change Pros Cons
1 Use 0-based everywhere in the PLC High — refactor every reference None Clean, no index math "Machine0" semantically wrong; many rewrites
2 Keep [1..10] in PLC, add +1 offset on use Low — add translation in FBs None Documented mapping, preserves Machine1 semantics Index math still required
3 Drop custom lower bound, ignore element 0 Low — minor edits None Compact, intuitive Wastes one element per array
4 HMI-side wrapper tag with -1 translation None Low — bind through wrapper tag Zero impact on PLC logic Extra tag per array

Solution 1: Zero-Based Indexing in the PLC (Siemens-Recommended)

Siemens' official guidance in the STEP 7 (TIA Portal) Programming and Operating Manual — S7-1500, section "Declaring ARRAY", recommends that user arrays start at index 0. Edit the UDT so that the array lower bound is 0:

TYPE "typeMachineData"
VERSION : 0.1
   STRUCT
      sName : STRING[20];
      iCount : INT;
      aMachines : ARRAY[0..9] OF INT;   // 0-based, matches WinCC tag editor
   END_STRUCT;
END_TYPE

After recompile, both PLC and HMI expose the array as [0..9]. Update all PLC references to subtract 1 from the conceptual machine number: dbMachineList.aMachines[MachineNo - 1]. Where the source case wanted "Machine1 ... Machine10", the new PLC index becomes "Machine1 → 0" and "Machine10 → 9". Use an FB input parameter iMachineIndex : INT with explicit bounds check:

FUNCTION_BLOCK "FB_ReadMachine"
VAR_INPUT
    iMachineIndex : INT;       // 0-based index from HMI / external
END_VAR
VAR_OUTPUT
    iCount : INT;
    bError : BOOL;
END_VAR
BEGIN
    IF iMachineIndex < 0 OR iMachineIndex > 9 THEN
        bError := TRUE;
        iCount := 0;
        RETURN;
    END_IF;
    bError := FALSE;
    iCount := "dbMachineList".aMachines[iMachineIndex];
END_FUNCTION_BLOCK;

Solution 2: Keep the Lower Bound at 1 in the PLC, Offset on Use

This minimizes rewrites while still allowing "Machine1" semantics in the PLC. Define a pair of symbolic constants in the PLC constant table or in a dedicated constants DB:

VAR CONSTANT
    cMACHINE_OFFSET : INT := 1;       // First valid index (PLC semantics)
    cMACHINE_COUNT  : INT := 10;      // Total number of machines
END_VAR

When the HMI sends a 0-based index, convert it inside the PLC before array access:

iMachineIndex := iHMI_Index + cMACHINE_OFFSET;   // 0 → 1, 9 → 10
IF iMachineIndex < cMACHINE_OFFSET OR
   iMachineIndex >= cMACHINE_OFFSET + cMACHINE_COUNT THEN
    bError := TRUE;
    RETURN;
END_IF;
iCount := "dbMachineList".aMachines[iMachineIndex];

This satisfies the source engineer's stated requirement that "Machine0 doesn't sound right" without changing the HMI tag structure or adding wrapper tags.

Solution 3: Drop the Custom Lower Bound and Ignore Element 0

The cleanest pragmatic approach for greenfield projects: declare the array as ARRAY[0..10] in the PLC, accept that the HMI also reads [0..10], and reserve element 0 as a "null" or "active machine" pointer. The field report explicitly endorsed this pattern:

"I also tend to use index 0 as the 'active' selected choice. The other indexes are for the selections to choose from."

Programming becomes intuitive because the HMI's index 0 is the user-visible "active machine" indicator, while indexes 1..10 are the selectable list. This pattern also reduces programming errors because every loop naturally begins at 1, which mirrors how operators count machines in the field.

Solution 4: HMI-Side Wrapper Tag (Zero PLC Impact)

For legacy PLC code that cannot be touched, create a derived HMI tag that subtracts 1 in a small SCL function block whose output is bound to the original UDT array element. This requires no PLC code change but adds one wrapper tag per accessed element:

FUNCTION_BLOCK "FB_HMI_IndexAdapter"
VAR_INPUT
    iHMI_Index : INT;       // 0..9 as exposed to HMI
END_VAR
VAR_OUTPUT
    iPLC_Index : INT;       // 1..10 for direct UDT access
END_VAR
BEGIN
    IF iHMI_Index < 0 OR iHMI_Index > 9 THEN
        iPLC_Index := -1;   // Sentinel for HMI error handling
    ELSE
        iPLC_Index := iHMI_Index + 1;
    END_IF;
END_FUNCTION_BLOCK;

Bind the wrapper's iPLC_Index output to a symbolic HMI tag and let the HMI display a "Bad Index" screen whenever the value equals -1. This pattern is especially valuable on retrofits where the PLC program must remain bit-for-bit identical to its pre-retrofit state.

Bounds Checking: Mandatory Regardless of Solution

Every solution above still requires strict array-bounds enforcement, because both the S7-1500 CPU and the TP1500 runtime will trigger an OB121 (Programming Error) or an HMI connection fault when an out-of-range index is dereferenced. According to the S7-1500 System Manual, section "Diagnostic events of the CPU", an ARRAY violation inside an FB raises an SF diagnostics event with fault code 2522 ("Array index out of range"). Use one of the following defensive patterns in every FB that takes an index:

// Defensive index check — SCL pattern
IF iIdx < LOWER_BOUND(aMyArray) OR iIdx > UPPER_BOUND(aMyArray) THEN
    "DB_Diagnostics".iLastFault := 16#0001;        // Custom fault code
    RETURN;
END_IF;

On the HMI side, the SIMATIC HMI Panels Comfort Panels Operating Instructions, section "Tag editor", requires that tag quality be checked before any read; a "bad quality" tag implies that the underlying connection has been disrupted and the index origin may no longer be reliable.

Verification Procedure

  1. After applying any of the four solutions, perform a full Compile (Hardware and Software) in TIA Portal V15 UPD3.
  2. Download the project to PLCSIM Advanced and start the S7-1500 instance.
  3. Run the TP1500 Comfort simulator (WinCC RT Advanced) and trigger a tag read on each element 0..9.
  4. Confirm that the value displayed on the HMI matches the value written in dbMachineList.aMachines[N] in PLCSIM (Watch table).
  5. Force an out-of-range index (e.g., -1 or 99) from the HMI and confirm that the configured error response fires — the SCL returns -1 and the HMI shows the configured error graphic.
  6. Save the compiled project archive (Project → Archive → Project without password) so the index mapping is documented for future maintainers.
Documentation tip: Always add a comment block at the top of each UDT describing whether the array is 0- or 1-based, and which TIA Portal version the project was last compiled in. This is the single highest-value preventive measure for multi-programmer teams.

Edge Cases and Field-Proven Caveats

  • UDT-of-UDT arrays: When an outer UDT contains an inner UDT that itself contains an array, the shift still occurs only at the inner array; the outer element count remains correct.
  • OPC UA server on the S7-1500: When published via the integrated OPC UA server (firmware V2.5+), array indexing follows OPC UA conventions and is always 0-based. Any client reading the UDT over OPC UA sees the same shift as the HMI.
  • Multi-user projects: If two engineers edit the UDT and one declares [1..10] while the other declares [0..9], TIA Portal will raise a version conflict during check-in. The mismatch is preserved through the merge cycle and must be resolved manually.
  • Web server API: The S7-1500 web server's variable view uses the same 0-based numbering as the HMI tag editor, so a custom HTML dashboard must also be built around [0..9].
  • PLCSIM vs hardware: The shift is fully observable in PLCSIM Advanced alone, with no real PLC or HMI hardware required. This is the recommended approach for design reviews and unit testing.
  • Changing the array size at runtime: Modifying UDT bounds while the PLC is in RUN raises a STOP / restart on the S7-1500 if the change cannot be reloaded online. Always verify the change in STOP first or use a separate runtime instance.

Native Behavior Comparison Across HMI Product Lines

HMI Product Line UDT Array Display Configurable Origin? Tag-Editor Behavior
Comfort Panels (TP1500) 0-based No Forces 0-based for any UDT array
WinCC Runtime Advanced (PC) 0-based No Identical to Comfort Panels
WinCC Runtime Professional 0-based (UDT) / configurable (internal tags) Partial UDT arrays still 0-based
WinCC Unified (V17+) 0-based No Same rule applies
S7-1500 Web Server 0-based No Identical to HMI tag editor
S7-1500 OPC UA Server 0-based (per OPC UA spec) No Identical to HMI tag editor

The behavior is consistent across the entire WinCC family because all variants share the same OPC-rooted array model.

Best-Practices Checklist

  • Declare arrays as ARRAY[0..n-1] in any new UDT to align with HMI, OPC UA, and web-server representation.
  • Use symbolic constants for the array bounds instead of hard-coded literals so the bounds are documented in one place.
  • Always bounds-check every array access in FBs using LOWER_BOUND() and UPPER_BOUND() STL operators.
  • Document the array origin at the top of each UDT and the matching faceplate / script.
  • Train maintenance programmers that "0 counts" — bit 0 matters, byte 0 matters, index 0 matters.
  • Use PLCSIM Advanced for unit testing of array-bound code so no physical hardware is required.
  • Maintain a project-wide naming convention such as aName_0based vs. aName_1based to make the origin explicit in tag browsing.
  • After every UDT change, re-compile the entire project (Hardware and Software) before downloading to avoid stale HMI tag prefixes.

Standards and Reference Documentation

Although this is a vendor-specific behavior, the underlying convention is documented in:

FAQ

Why does my UDT array declared as [1..10] in TIA Portal show as [0..9] on the HMI?

This is documented WinCC (TIA Portal) behavior: the HMI tag editor and runtime always normalize UDT-internal arrays to 0-based indexing, regardless of how the array is declared in the PLC data block editor. The PLC still uses [1..10]; only the HMI's view of the array is renumbered to [0..9].

Is this a bug in TIA Portal V15 Update 3?

No. Siemens classifies this as "by design" because WinCC tags are exposed through an OPC-UA-style interface that is always 0-based. The behavior persists in TIA Portal V15, V15.1, V16, V17, V18, and V19, both with the TP1500 Comfort panel and with WinCC Runtime Advanced / Professional / Unified.

What is the cheapest fix that keeps Machine1 ... Machine10 semantics?

Apply Solution 2: keep the PLC array at [1..10] and add an offset translation (iPLC_Index := iHMI_Index + 1) inside every FB that dereferences the array. Define symbolic constants for the lower bound and total count so the translation is documented and easy to audit.

Does the same shift affect OPC UA clients on the S7-1500?

Yes. The integrated OPC UA server (firmware V2.5 and later) and the S7-1500 web server both expose UDT arrays as 0-based, identical to the HMI. Any external client (Ignition, Kepware, custom .NET application) must read indexes 0..9 instead of 1..10.

What happens if the HMI sends an out-of-range index?

The S7-1500 raises OB121 (Programming Error) with diagnostic event code 2522 "Array index out of range", and the HMI tag enters "bad quality". Always bounds-check every index inside the FB before dereferencing the array, and propagate an error flag to the HMI for display.

Can I change the array lower bound for an existing project without recompiling everything?

No. Changing the lower bound on a UDT array forces a re-compile of every block that references it, including the HMI tag prefixes. Plan the change during a maintenance window and use PLCSIM Advanced to validate the entire tag list before downloading to live hardware.

Back to blog