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
STRUCTthat contains aCTU/CTD/CTUDcounter will fail to compile in STL. - Loading a UDT (User-Defined Data Type) that contains a counter into a
STATvariable produces the same error. - Custom PLC Data Types (DTL, IEC) cannot host a
TON_TIMEorCTU_xxthat is then called from STL. - It also affects every
FB_xxxtype if you attempt to instantiate a custom FB as aSTATinside aSTRUCTand 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).
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.
- 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.
- 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.
-
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
INtrue and verify thatETincrements from T#0s toward T#5s in 1-second units.Qmust go true at 5 s. - 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.
- 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.
- 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.
-
Watch table. Drop the timer symbol (or the timer's
ETword) into a watch table, go online, toggleIN, and observe the rise ofETand theQtransition. 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_Timerfor 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_Structparameter of typeUDT_Axis1that contains aTON_TIMEis 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_Timervia the user-defined types, but only at theSTATlevel. ATON_TIMEnested in a UDT that is not declared in the FB'sSTATwill 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_TIMEon 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. LegacyS_ODThas the same footprint but uses the system clock area; switching fromS_ODTtoTON_TIMEdoes 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.
-
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. - 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.
-
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 #namebecomes#name(IN := ..., PT := ...);. A textual search-and-replace plus a compile pass is usually enough. - Run the consistency check on every PLC in the project. Some libraries are shared across PLCs; the check catches the ones that were missed.
-
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.