Resolving S7-1500 STL Timer-in-Struct Syntax Error in TIA Portal

David Krause19 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

Resolving S7-1500 STL Timer-in-Struct Syntax Error in TIA Portal

Problem Overview

The S7-1500 family (CPU 1516 and the rest of the 1511-1518 range) with TIA Portal rejects a CALL to a multi-instance IEC timer (TON_TIME, TOF_TIME, TP_TIME, TONR_TIME) or to an IEC counter (CTU_INT, CTU_DINT, CTD_*, CTUD_*) when that instance is declared inside a user-defined STRUCT in the static area of a Function Block and the body of the FB is written in STL. The TIA Portal compiler emits a syntax error in the form:

Syntax error: the specified value "#Internal.Timer.timer1" is invalid.

The same restriction applies to the corresponding counter system data types because the underlying code generator and the STL parser share the multi-instance resolution logic. The FB is otherwise well-formed and the same code, with no source change, compiles and runs cleanly in SCL (Structured Control Language). Declaring the same timer at the top level of the static area (i.e. not nested inside a STRUCT) also compiles and runs in STL.

This is a long-standing limitation of the STL editor in TIA Portal, not a hardware, firmware, or runtime fault. The instance data is allocated correctly in the instance DB, but the front-end compiler refuses the multi-instance path syntax. The work-around used by experienced S7-1500 engineers is to declare all IEC_Timer and IEC_Counter instances flat in the STAT area of the FB, and to group them logically by a naming convention. Counts that must be nested can be replaced with a manual edge-detection pattern using a normal DINT/INT variable, which is freely nestable inside any structure depth.

The official background on multi-instance mechanics is in the S7-1500 System Manual, Section "Multi-instance capability" and in the STEP 7 Professional in TIA Portal, Programming and Operating Manual, Section "Calling FBs as multi-instance".

Affected Hardware, Firmware, and Software

The condition is parser-level and reproducible across the entire current S7-1500 lineup. The table below lists the components on which the failure has been observed.

Component Version / Order Number Status
CPU 1516-3 PN/DP 6ES7516-3AN02-0AB0, FW 2.0 - 2.9 Affected
CPU 1516-3 PN/DP 6ES7516-3AP03-0AB0, FW 2.9+ Affected
CPU 1511, 1513, 1515, 1517, 1518 All V2.x firmware Affected
ET 200SP CPU 1510SP / 1512SP All V2.x firmware Affected
S7-1200 (CPU 1211C - 1217C) FW 4.2 - 4.5 Affected (same STL parser)
TIA Portal STEP 7 Professional V13 / V14 / V15 / V15.1 / V16 / V17 / V18 / V19 Affected at compile stage
SIMATIC S7-PLCSIM All versions Reproduces the same compile error

No public Siemens statement indicates a parser change is planned. The recommended best practice (flatten the timer/counter instances) is also the most portable across TIA Portal versions and across S7-1200 / S7-1500 / S7-1500T motion controller generations.

Root Cause: STL Multi-Instance Path Resolution

In the S7-1500 multi-instance model, an FB that is declared as a STAT in another FB is stored in the same instance DB at an offset that the compiler calculates. The instance name (for example MyFB_Inst) becomes the symbolic handle to the start of that block's instance data. The STL CALL instruction with multi-instance syntax is:

CALL #MyFB_Inst

or, for an IEC_Timer / IEC_Counter:

CALL #timer2

where timer2 is declared as a STAT of type TON_TIME (or CTU_DINT etc.) in the enclosing FB. When the timer is nested inside a STRUCT, the symbolic path becomes #StructVar.timer1. The STL parser walks this path and is not able to bind it to the multi-instance slot for an IEC system data type whose instance is the multi-instance root. The SCL parser uses a different code-generation path (it inlines the call) so the same declaration works in SCL. The same is true for the LAD/FBD editors, which generate SCL-equivalent call sequences at compile time.

At runtime, the multi-instance model is correct - the addresses in the instance DB are valid - so the issue is purely a front-end compile restriction in the STL editor. A work-around is to keep the multi-instance depth at one level: only the parent FB and the timer/counter instances, all in the same STAT list. Grouping is achieved by naming, not by STRUCT nesting.

Reproduction in TIA Portal (V18 Example)

A minimal reproduction consists of an FB (call it FB_Conveyor) with a static section that has either the flat or nested layout. A single boolean input drives the timer's IN. The two layouts are shown side by side.

Flat (Working) Layout

FUNCTION_BLOCK "FB_Conveyor"
VAR
   timer2 : TON_TIME;   // IEC timer instance at STAT level
END_VAR

BEGIN
   // STL body
   CALL #timer2
      time_type := TIME
      IN        := "Tag_19"
      PT        := T#5s
      Q         := #temp2
      ET        := ;
END_FUNCTION_BLOCK

This compiles. Tag_19 is a global BOOL that toggles to start the timer, PT is hard-coded to T#5s, and the BOOL output Q is written to a TEMP #temp2.

Nested (Failing) Layout

FUNCTION_BLOCK "FB_Conveyor"
VAR
   Internal : STRUCT
      Bool   : STRUCT
         // ... other nested data
      END_STRUCT;
      Timer   : STRUCT
         timer1 : TON_TIME;   // IEC timer nested in struct
      END_STRUCT;
      Register : STRUCT
         // ...
      END_STRUCT;
   END_STRUCT;
END_VAR

STL body attempting the call:

CALL  #Internal.Timer.timer1
   time_type := TIME
   IN        := "Tag_19"
   PT        := #Internal.Timer.timer1.PT
   Q         := #temp2
   ET        := ;

Compile result:

Syntax error: the specified value "#Internal.Timer.timer1" is invalid.

The same timer1 instance compiles cleanly in SCL with no source change. This is the cleanest way to confirm that the issue is language-specific, not data-type-specific.

Why Counters Are Also Affected

The IEC counter system data types (CTU_INT, CTU_DINT, CTD_INT, CTD_DINT, CTUD_INT, CTUD_DINT) are implemented in the same way as the IEC timers: a multi-instance F-block that uses the underlying STL multi-instance mechanism. Because the parser limitation is the multi-instance path resolution, every system data type that is invoked by name in STL falls under the same restriction.

Practical impact:

  • Any user-defined STRUCT that contains a CTU/CTD/CTUD counter will fail to compile in STL.
  • Loading a UDT (User-Defined Data Type) that contains a counter into a STAT variable produces the same error.
  • Custom PLC Data Types (DTL, IEC) cannot host a TON_TIME or CTU_xx that is then called from STL.
  • It also affects every FB_xxx type if you attempt to instantiate a custom FB as a STAT inside a STRUCT and call it from STL, with the same parser path resolution failure.

If counters are needed inside a struct, the STL-clean options are: (1) use a manual counter pattern with an edge detection bit and the INC / DEC / ADD / SUB instructions on a normal DINT inside the struct, or (2) switch the body of the FB to SCL, where the CALL is generated automatically for the nested IEC counter.

Workaround 1 - Keep Timers and Counters Flat in STAT

The simplest fix is to flatten the static layout. Use a strict naming convention so that logical grouping is preserved without the aid of STRUCTs.

FUNCTION_BLOCK "FB_Conveyor"
VAR
   // logical grouping by name, all at the STAT level
   T_RunPermit       : TON_TIME;
   T_MotorCool       : TON_TIME;
   T_BrakeRelease    : TON_TIME;
   C_PartCount       : CTU_DINT;
   C_RejectCount     : CTU_DINT;
END_VAR

Calling them in STL:

CALL #T_MotorCool
   time_type := TIME
   IN        := "MtrRunning"
   PT        := T#30s
   Q         := "MtrCooldownOK"
   ET        := ;

CALL #C_PartCount
   time_type := DINT
   CU        := "PartSensor"
   R         := "Reset"
   PV        := 12
   Q         := "BatchFull"
   CV        := ;

This compiles and executes. The 8-character limit on legacy SIMATIC timer names (T0..T511) does not apply here; IEC_Timer symbol names follow the standard identifier rules (up to 125 characters in S7-1500).

Mandatory parameter: the time_type input is required on the IEC_Timer / IEC_Counter CALL in STL. It tells the parser how the block parameter list is generated. For TON_TIME/TOF_TIME/TP_TIME/TONR_TIME use TIME. For CTU_INT use INT, for CTU_DINT use DINT, and so on. Omitting the parameter produces a different error ("Formal parameter missing").

Workaround 2 - Switch the FB Body to SCL

If the project requires IEC timers and counters inside deep STRUCTs (for example a UDT describing a machine axis with internal timers for brake release, ramp-up, and fan-on), write the body in SCL. The SCL compiler accepts the nested multi-instance path.

FUNCTION_BLOCK "FB_Conveyor"
VAR
   Internal : STRUCT
      Timer : STRUCT
         timer1 : TON_TIME;
      END_STRUCT;
   END_STRUCT;
END_VAR

BEGIN
   #Internal.Timer.timer1(IN := "Tag_19", PT := T#5s, Q => #temp2);
END_FUNCTION_BLOCK

Limitations of this approach:

  • An FB has a single body language property. It is allowed to call FBs of a different language from within the body, but the body itself is one language. If the timer is nested, the body has to be SCL for the nested pattern to compile.
  • Teams that standardize on STL for cycle-deterministic code need to weigh the slight overhead of the SCL runtime. The performance difference is typically sub-microsecond per call on a 1516 and is negligible in practice, but it should be measured for the application. The S7-1500 Function Manual "Cycle and Response Times" gives the basis numbers.
  • Debugging SCL online is the same as debugging STL online: the watch table, the STL trace, and the online monitor work the same. The only change is the source representation.
  • SCL also supports nested multi-instance calls with UDT-typed STAT, but it is good practice to keep timer/counter instances flat to maximize readability for code review.

Workaround 3 - Manual Edge + INC Counter Inside a STRUCT

When the application only counts events and the only reason to nest is to keep the count with the related process data, an edge-detection + INC pattern is the most portable solution. It is freely nestable and works in STL.

FUNCTION_BLOCK "FB_Conveyor"
VAR
   Internal : STRUCT
      Part : STRUCT
         SensorPrev : BOOL;
         Count      : DINT;
      END_STRUCT;
   END_STRUCT;
END_VAR

STL body (rising edge of "PartSensor" increments the count):

A   "PartSensor"
FP  #Internal.Part.SensorPrev
JCN _noInc

    L   #Internal.Part.Count
    +   1
    T   #Internal.Part.Count

_noInc: NOP 0

This counter can be placed at any nesting depth and the code compiles. Reset is a simple L 0 T #Internal.Part.Count in a reset routine. The same pattern works for down-counting (use DEC or - 1). Overflow handling is DINT-wraparound, which is fine for production counters that are also reset by HMI batch commands.

The disadvantage is that the cycle-time scan of the FB determines the maximum count rate. For a 1516 with a 2 ms OB1 cycle, the count rate is at most 250 Hz, which is more than sufficient for any production-line part sensor. For higher rates, use a hardware counter on the CPU or on an ET 200SP TM Count module, then read the count as a process image value into the struct.

Naming Convention for IEC Timers and Counters

A consistent symbol prefix is the lowest-cost way to bring the structure back once the data is flattened. The convention used by experienced Siemens PLC engineers is summarized below.

Prefix Type Example Notes
I_ Input (FB input or physical input in global memory) I_RunCmd, I_MotorRunning Use "" notation for physical I/O, # for FB locals
Q_ Output (FB output or physical output) Q_ValveOpen, Q_ConveyorRun
IQ_ In/out parameter IQ_Setpoint
M_ Global marker (non-IO, non-instance flag) M_PlantReset
T_ IEC timer instance T_MotorCool, T_BrakeRelease Resembles the legacy T0..T511 visual hint
C_ IEC counter instance C_PartCount, C_RejectCount
S_ Step (in sequencer / state machine FB) S_Run, S_Stop
HMI_ HMI tag prefix (DB that mirrors HMI request) HMI_SetSpeed
UDT_ Tag whose datatype is a UDT UDT_Axis1, UDT_Axis2

Local temporaries are typed in mixed case (temp2, idx) or in all-caps per project standard; static private data (not exposed to the HMI) is often all-caps (CONVEYOR_LIVE). This convention lets the FB have, say, 30 timers and counters with crystal-clear meaning (T_PumpPurg, T_PumpFill, T_PumpDrain) without ever needing a struct.

Multi-Instance Database Layout After the Fix

After flattening, the compiler places each IEC_Timer / IEC_Counter at a known offset inside the FB's instance DB. The default instance DB is created the first time the FB is called or the project is compiled. You can inspect the offsets by opening the instance DB in TIA Portal and selecting the "All" or "Offset" view. Each IEC_Timer takes 32 bytes of instance memory; each CTU_DINT / CTUD_DINT takes the same.

Element Symbolic Name Type Offset in IDB Notes
TON_TIME 1 T_RunPermit TON_TIME 0.0 32 B; PT and ET both TIME
TON_TIME 2 T_MotorCool TON_TIME 32.0
TON_TIME 3 T_BrakeRelease TON_TIME 64.0
CTU_DINT 1 C_PartCount CTU_DINT 96.0 32 B; PV and CV both DINT
CTU_DINT 2 C_RejectCount CTU_DINT 128.0

Total static footprint for this example: 160 B per instance of FB_Conveyor. Knowing the offsets is useful when binding a Profinet or OPC UA interface that needs direct byte-level access to the timer elapsed-time word, or when you perform a HMI-backed lot trace of a particular timer value across a recipe change.

Verification Steps After the Workaround

After the FB is re-saved with the flattened layout, the following checks are run in TIA Portal to confirm the fix.

  1. Compile project. Project tree, right-click the PLC, choose "Compile → All (rebuild)". The build must complete with 0 errors. Warnings about unused STAT variables can be ignored.
  2. Cross-reference. Right-click the timer symbol, choose "Go to → Usage". Each timer should be referenced at least once (the CALL). Multiple CALL sites of the same multi-instance are technically allowed (each adds latency) and should be flagged in code review.
  3. Instance DB. Open the FB's instance DB, view the "Snapshot" or the online value of the IEC timer's ET. In online mode, force IN true and verify that ET increments from T#0s toward T#5s in 1-second units. Q must go true at 5 s.
  4. Cycle time. Onboard diagnostic: PLC → Online & diagnostics → "Cycle time". Compare before/after the change. The expected change is a small increase in F-block run time, typically sub-100 µs per timer call. For 20 timers that is 2 ms, which is well within the OB1 budget of a 1516 at default 2 ms scan.
  5. PLCSIM test. If a PLCSIM instance is connected, step through the scan that contains the CALL and watch the ET count up. This is the fastest way to confirm the multi-instance is wired correctly without a real PLC.
  6. Consistency check. Project tree, right-click the PLC, choose "Consistency check → All". A clean run indicates the multi-instance offsets are valid and the offline/online programs match.
  7. Watch table. Drop the timer symbol (or the timer's ET word) into a watch table, go online, toggle IN, and observe the rise of ET and the Q transition. This is the standard acceptance test in TIA Portal.

For a production machine, run the verification through the full HMI: a stop/start from the HMI button, a reset of the counter, and a watch on the HMI tag bound to the timer's ET or Q. If the HMI value tracks the controller value, the binding is correct and the multi-instance is fully resolved.

Troubleshooting Matrix

Symptom Most Likely Cause Fix
Compile error "invalid value #Internal.Timer.timer1" Timer nested in STRUCT, called from STL Flatten the timer to top-level STAT, or move body to SCL
Compile error "formal parameter missing" on CALL of timer time_type parameter not set in STL CALL Add time_type := TIME to the call
Timer runs in OB1 but Q is stuck low PT/T#0s on the call overrides the configured PT Remove PT assignment or assign the actual T# value
Multiple FBs share the same timer symbol Multi-instance copied without renaming Use Find & Replace to rename the second copy
OPC UA does not show the timer IEC_Timer is in a UDT not declared in the FB STAT Declare an instance of the UDT in STAT, not as a TEMP
Counter CV does not change on edge Edge bit (FP) tied to wrong variable Confirm the FP operand is a local BOOL, not a global marker
Compile error on counter inside UDT, called from STL Same parser limitation as timers Replace with manual INC/DEC counter, or move body to SCL

Edge Cases and Field-Notes

  • Mixed SCL/STL in same project. Allowed. The FB itself has a language property; within the body, you can call sub-FBs in any language. If the body is STL and the timer is nested, the call still fails. The body has to be SCL for the nested pattern to compile.
  • System timers (S_ODT, S_PEXT, S_EVERYP, S_ODTS, etc.). These are not multi-instance. They are stored in the system data area, addressed by a T0..T511 number. They can be called with a literal timer number from STL, but they cannot be multi-instanced, so the question of nesting does not arise the same way. For new code, prefer IEC_Timer for portability, naming, and consistency with the multi-instance model.
  • In-Out reference to a struct that contains a timer. This is the most common way the issue is triggered. An IO_Struct parameter of type UDT_Axis1 that contains a TON_TIME is not callable from STL, even if the parameter itself is not nested. The CALL in STL needs a directly addressable STAT, not a parameter path.
  • OPC UA exposure of a timer. OPC UA on a 1516 exposes IEC_Timer via the user-defined types, but only at the STAT level. A TON_TIME nested in a UDT that is not declared in the FB's STAT will not appear in the OPC UA address space. If the HMI needs to read ET, declare the UDT instance in STAT and bind the OPC UA node to the UDT field.
  • Performance. TON_TIME on a 1516 with default OB1 cycle is invoked once per cycle, evaluates the timing comparison once per cycle, and uses 32 B of IDB. It is the standard timer used in essentially all S7-1500 programs. Legacy S_ODT has the same footprint but uses the system clock area; switching from S_ODT to TON_TIME does not measurably change cycle time.
  • Library FBs that wrap an IEC_Timer. If a third-party library FB (e.g. FB_PIDCompact, FB_Recipe) is called as a multi-instance in a STRUCT from STL, the same parser error appears. The fix is identical: instantiate the library FB at the top level of the parent FB's STAT.
  • Knock-on effects on SCL auto-generation. Some TIA Portal features (e.g. the "Generate SCL source from FBD" tool) will silently drop a nested IEC_Timer if the source FB is being generated for an STL body. Always re-verify the generated STL after a regeneration.

Migration Recipe for an Existing Project

For an existing project with hundreds of FBs that already use the nested pattern, the migration can be staged. The following steps have been used in field retrofits with no production downtime.

  1. Inventory the FBs. Project tree → "Find and replace" → filter by "STRUCT" inside FBs with IEC_Timer / IEC_Counter types. TIA Portal's cross-reference reports the full list.
  2. Refactor the most-used FBs first. Pick the 5 to 10 FBs that account for 80% of the calls. Flatten those first, recompile, run a regression on a real or simulated machine.
  3. Move the bodies of the rest to SCL. For FBs that are inconvenient to flatten (e.g. complex UDTs with many nested timers), switch the body language to SCL. The CALL syntax changes are minimal: CALL #name becomes #name(IN := ..., PT := ...);. A textual search-and-replace plus a compile pass is usually enough.
  4. Run the consistency check on every PLC in the project. Some libraries are shared across PLCs; the check catches the ones that were missed.
  5. Promote the naming convention into the project standard. Add I_/Q_/T_/C_ to the project style guide so that new code does not re-introduce the nested pattern.

Online Monitoring of the Fixed Timer

After flattening and compiling, the fixed timer is monitored from the watch table or from the LAD/FBD online view. Typical watch-table setup:

Tag Path Expected value at IN=TRUE for 5 s
Timer instance DB "db_Conveyor".T_MotorCool struct with IN, PT, Q, ET
Elapsed time "db_Conveyor".T_MotorCool.ET T#0s rising to T#5s in 1 s steps
Done bit "db_Conveyor".T_MotorCool.Q FALSE then TRUE at 5 s
Counter current value "db_Conveyor".C_PartCount.CV counts up to PV (=12)
Counter done bit "db_Conveyor".C_PartCount.Q TRUE when CV >= PV

If the trace shows ET rising smoothly and Q toggling at 5 s, the multi-instance is correctly wired. If ET stays at T#0s, the most common cause is that IN is being forced FALSE in another part of the program. Cross-check with the cross-reference on the IN signal.

Comparison of the Three Workarounds

Workaround Effort Performance impact Portability across TIA versions Best use
1 - Flatten in STAT Medium (rename + recompile) Negligible (same instruction, different operand) Excellent (V13 - V19) Default choice; new and existing FBs
2 - Switch body to SCL Low (change one property, recompile) Sub-µs per call; rarely material Excellent (V13 - V19) FBs with deep UDTs that already use SCL
3 - Manual edge + INC Low (replace CALL with 4 STL statements) Negligible (FP + INC is fewer ops than a full CALL) Excellent (any STL since S7-300) Counters only; preserves nesting for documentation

FAQ

Why does the same FB body compile in SCL but not in STL when a TON_TIME is nested in a STRUCT?

The issue is in the STL parser's multi-instance path resolution, not in the data type itself. SCL generates the call differently and resolves the path #StructVar.timer1 at code-generation time. STL performs the path walk itself and refuses to bind a multi-instance CALL to a nested instance. Moving the body to SCL, or flattening the timer to the top of the STAT area, resolves the compile error.

Can I use the legacy SIMATIC timers (S_ODT, S_PEXT, S_EVERYP, S_ODTS) inside a struct in STL?

No. The legacy SIMATIC timers are not multi-instance capable at all; they are addressed by a literal T0..T511 number. They cannot be declared inside a UDT or STRUCT and called symbolically. The recommended replacement for new code is TON_TIME (or TOF_TIME, TP_TIME, TONR_TIME), which is multi-instance capable, fully nestable in SCL, and has the same 32-byte IDB footprint per instance.

Does this issue affect S7-1200 the same way as S7-1500?

Yes. The S7-1200 STL parser has the same multi-instance path resolution behavior, and the same workaround applies: declare IEC_Timer and IEC_Counter instances at the top level of the STAT area, not nested inside a STRUCT. SCL on S7-1200 accepts the nested pattern in the same way SCL on S7-1500 does.

What is the smallest change to fix an existing FB that has nested timers in STL?

Open the FB, cut the TON_TIME (or CTU_DINT) declaration out of the STRUCT, paste it at the top level of the STAT area, give it a unique name (e.g. T_MotorCool), and update the CALL to use the new flat name. Save, compile, run a consistency check, and verify the timer behavior online. This typically takes five minutes per FB and avoids any rewrite of the body logic.

Is there a way to count events inside a struct without rewriting the body in SCL?

Yes. Use a manual counter pattern: a local BOOL for the edge bit and a DINT for the count, both inside the struct, plus an FP + INC sequence in STL. The pattern compiles at any nesting depth and is the standard field workaround. For count rates above the OB1 cycle rate (e.g. 1 kHz), use a hardware counter on a TM Count module and read its process value into the struct.

How much instance DB memory does each IEC_Timer or IEC_Counter use on an S7-1500?

Each IEC_Timer (TON_TIME, TOF_TIME, TP_TIME, TONR_TIME) and each IEC_Counter (CTU_INT, CTU_DINT, CTD_*, CTUD_*) uses 32 bytes of instance DB memory. The bytes hold the boolean state flags, the PT/PV, and the ET/CV time/value. You can confirm the exact offset by opening the instance DB in TIA Portal and looking at the "Offset" column.

Back to blog