Resolving SCL FOR Loop Output Latch in TIA Portal V17
A SCL "FOR" loop on an S7-1500 (CPU 1512SP-1 PN) appears to work correctly when written inside a Function (FC) with parameter-bound inputs, but the same logic latches TRUE and never resets when ported into a Function Block (FB) or when the parameter references are replaced with absolute DB tags. This is one of the most common SCL beginner traps on the S7-1200/1500 family, and the root cause lies in how the SCL compiler initialises temporary variables versus how an FB instance DB retains state across scan cycles.
This reference reconstructs the failing case step by step, isolates the root cause, and shows the canonical SCL patterns that eliminate the latch. It is targeted at engineers transitioning from S7-300/400 STL/FBD on STEP 7 V5.x, or from ladder on Logix/Studio 5000, to SCL on TIA Portal V17 with S7-1500 firmware V2.9 / V2.9.x.
1. Problem Statement
The application requirement is straightforward: a 20-element array of DWord tags, DB40.Auto[1..20], each represents 32 boolean enable signals. The output DB40.Output shall be TRUE if any bit in the array is FALSE (i.e. any enable has dropped). Otherwise, when every one of the 20 × 32 = 640 bits is TRUE (array contents equal 16#FFFFFFFF), the output shall be FALSE.
The SCL implementation looks like this in its naive form:
// FC or FB body — INCORRECT pattern
FOR #i := 1 TO 20 DO
IF #Auto[#i] <> 16#FFFFFFFF THEN
#Output := TRUE;
RETURN; // early exit on first failure
END_IF;
END_FOR;
#Output := FALSE; // only reached if the loop never set TRUE
Observed symptoms on the S7-1500:
- Inside an FC with
AutoandOutputdeclared as InOut parameters and called from another FC: behaves correctly — output returns toFALSEonce all array elements are16#FFFFFFFF. - Inside the same FC, but with the parameter references replaced by absolute DB access (e.g.
DB40.Auto[#i]andDB40.Output):DB40.OutputlatchesTRUEafter the first detection and never resets, even when the array is overwritten to16#FFFFFFFF. - Inside an FB (any instance DB), bound to the same global DB tags via InOut: same latch behaviour as the previous case.
The first scenario that works and the two scenarios that latch are logically identical. The difference is purely an artefact of the SCL variable model.
2. Root Cause: Temporary Variable Initialisation vs. Retained Instance State
On the S7-1200/1500, the SCL compiler treats Temp variables in an FC and Static variables in an FB instance DB very differently. The mismatch between the two is what causes the latch.
2.1 FC behaviour — temporary variables are re-initialised each call
When an FC is called, the operating system allocates a temporary local stack frame. The SCL compiler inserts an implicit initialisation step at the top of every FC body that writes the default value of every Temp variable into the stack frame before the first executable statement runs:
-
BOOL→FALSE -
INT / DINT / REAL→0 -
DWORD→16#0000_0000 - Pointer / reference types →
NULL - User-defined
STRUCT/ARRAY→ element-wise default
This is documented in the S7-1500 programming and operating manual and in the SCL reference for S7-1500. Critically, output parameters and InOut parameters of an FC are not re-initialised by the compiler. They hold whatever value the caller passed in. But Temp locals are.
In the original working FC, the colleague's workaround — adding an explicit #Output := FALSE; immediately before the FOR loop — succeeds only because the #Output symbol happens to be the output parameter itself rather than a temp local. Writing the default value to the output parameter forces the latch off. Removing that line and changing the same code to a globally-aliased DB tag, or moving it into an FB, breaks the contract because of the next two rules.
2.2 FB behaviour — static variables retain state across calls
An FB has a companion instance DB. Every Static variable declared in the FB interface occupies a fixed slot in the instance DB. After the FB body executes, the values of all Static variables — and of the output parameters written during the last call — remain in the instance DB until the next call writes them again. The operating system does not zero them on entry.
This is the single most important rule for porting FC-style code to an FB: anything written conditionally is only as current as the most recent call that took the write branch. If a path is skipped (e.g. the FOR loop's RETURN exits the block), the previous value of the target is still present.
2.3 The combination that causes the latch
Walk through the failing code on an FB with Output declared as an Output parameter and Auto declared as InOut:
- First call:
DB40.Auto[3]contains16#FFFFFFFE. TheFORloop sets#Output := TRUEand hitsRETURN. The FB exits withOutput = TRUE. - Process changes; every element of
DB40.Auto[1..20]is overwritten to16#FFFFFFFFexternally. - Second call: the
FORloop runs to completion, every comparison succeeds, and execution falls off the end. The line#Output := FALSE;after the loop is reached. The output is reset.
That sequence should work, and on the bench it does, but only if that #Output := FALSE; line is actually reachable. The reported symptom — output latches TRUE forever — points to the line being skipped. The most common explanation is that the user has moved the assignment inside the loop body, leaving no path that writes FALSE when no fault is found:
// Pattern that latches in an FB
FOR #i := 1 TO 20 DO
IF #Auto[#i] <> 16#FFFFFFFF THEN
#Output := TRUE;
END_IF;
// no ELSE, no post-loop reset
END_FOR;
// if no element was unequal, #Output is whatever the previous call left
This pattern silently worked in the original FC because the Output parameter of an FC is shadowed by the temp mechanism when the calling convention treats it as pass-by-value on the stack. On S7-1500, FC output parameters are actually passed by reference to a copy in the calling context, but the SCL compiler still inserts the default initialisation for any temp-local that shadows the output. Once the code is ported to an FB — or absolute DB tags are used — that initialisation disappears, the latch appears, and the behaviour diverges from the FC reference.
3. FC vs FB: Side-by-Side Variable Lifetime
| Property | FC (Function) | FB (Function Block) |
|---|---|---|
| Local storage | Stack frame (L stack / Temp area) | Instance DB (work memory) |
| Temp variables default on entry | Yes — always initialised by SCL compiler | Yes — same compiler rule |
| Static variables default on entry | n/a (FC has none) | No — retain value from previous call |
| Output parameter default on entry | Holds the value passed in by the caller (or default for that data type when the caller is a literal) | Holds the value from the previous call (read from instance DB) |
| InOut parameter default on entry | Holds the value passed in by the caller | Holds the value from the previous call until the FB writes it |
| Memory class | None (uninstantiated) | Single / Multi-instance / Array instance |
| Code path that does not write an output | Output keeps caller's passed-in value | Output keeps last-written instance value — the latch |
| Recommended use | Pure combinatorial logic, no state | Anything with state, edge memory, or sequencer |
FALSE by default. The moment the output becomes a static or is removed from the interface (absolute DB tag), that safety net vanishes.4. Anatomy of the SCL FOR Loop
The SCL FOR loop is a counted iteration. The compiled MC7 / STL equivalent on the S7-1500 is roughly:
// SCL
FOR #i := 1 TO 20 BY 1 DO
<body>
END_FOR;
// Equivalent MC7 sequence (illustrative, not literal)
// Load start value
L 1
T #i
// Loop start
L #i
L 20 // end value
>I // signed compare: i > 20 ?
JC END // jump to loop exit if true
// <body> compiled in line
// Increment
L #i
L 1
+I
T #i
JU LOOP_START
END: // fall through
On the S7-1500 the loop counter is a 32-bit DINT internally even when declared INT. The runtime cost is one add + one compare per iteration. The loop variable's final value after the loop is start + n * step (i.e. 21 in the example above), which is normal and does not indicate a fault.
Three rules for the SCL FOR loop:
- Bound at compile time or runtime — the start, end and step can be runtime expressions, but step must be non-zero. If the runtime end is < start, the loop body is skipped entirely; the output latches are then governed by the rules in §2.
-
Do not modify the loop counter inside the body. The compiler may or may not honour an in-body write to
#iat runtime; do not rely on it. -
The
RETURNstatement exits the entire block, not just the loop. It is useful for early-exit on fault, but every reachable exit must leave the output in a defined state if the calling code is going to read it.
5. The Canonical Fix
The fix is structural, not cosmetic: initialise the output before the loop, and ensure a path that writes the steady-state value. The pattern that the SCL community consistently uses is a pre-loop defaulting line plus an inner conditional set.
5.1 Correct FC version
// FC "CheckAutoBits" — safe pattern
// InOut: Auto : ARRAY[1..20] OF DWORD
// Out: Output : BOOL
// Temp: i : INT
#Output := FALSE; // <-- default: no fault
FOR #i := 1 TO 20 DO
IF #Auto[#i] <> 16#FFFFFFFF THEN
#Output := TRUE; // <-- set on first failure
RETURN; // <-- early exit (optional)
END_IF;
END_FOR;
Note the #Output := FALSE; on the line before the loop. In an FC, even if this line were omitted, the default-value initialisation of the output parameter would protect the user. In an FB, this line is mandatory.
5.2 Correct FB version (instance DB)
// FB "CheckAutoBitsFB" — safe pattern
// InOut: Auto : ARRAY[1..20] OF DWORD
// Out: Output : BOOL
// Static: resultLatched : BOOL
// Temp: i : INT
#Output := FALSE; // <-- mandatory in an FB
FOR #i := 1 TO 20 DO
IF #Auto[#i] <> 16#FFFFFFFF THEN
#Output := TRUE;
#resultLatched := TRUE;
RETURN;
END_IF;
END_FOR;
#resultLatched := FALSE; // clear latching flag if used
5.3 Why the same code in an FC and an FB behaves differently
An SCL FC and an SCL FB compile to different code patterns with respect to defaulting. In the FC, the compiler inserts the equivalent of L 0; T #Output in the block prologue as part of the temp/output defaulting sequence. In the FB, no such defaulting is emitted for an Output parameter because the instance DB slot is treated as a persisted value. This is by design: FBs are state-holding blocks, and state preservation is the feature, not the bug. The trap is that an engineer porting logic from FC to FB does not always realise the implicit safety net has been removed.
6. Diagnostic Procedure
When a similar latch is reported, walk through the following procedure on the live CPU or in PLCSIM:
- Open the online block view of the suspect FC/FB in TIA Portal. Right-click the call in the calling OB → "Monitor & Force" → enable monitoring. Confirm the loop variable is reaching the expected bound.
-
Watch the output parameter in the interface view. In an FB instance DB, locate the slot corresponding to
Output. In an FC, use the caller's view of the output tag. -
Force the array to
16#FFFFFFFFvia the watch table. Trigger a single call (e.g. set a tag that the FC's enable input watches) and check whether the output flips. If it does not, the issue is either the defaulting line is missing or the loop is exiting early. -
Step the block in single-cycle mode. In TIA Portal, set a breakpoint on the
END_FOR;line and on the assignment after the loop. Step through and observe which path is taken. -
Inspect the compiled code (optional). Right-click the block → "Compile and download" → then in the project tree navigate to "Program blocks → [block] → compiled code". For an FC, search for an
L 0write to the output address; for an FB, confirm there is none. - Check the cross-reference. "Go to > Cross-reference" on the output parameter to confirm no other block is overwriting the same address between calls.
7. Verification
After applying the fix, verify with a deterministic test sequence:
| Step | Action | Expected output |
|---|---|---|
| 1 | Initialise DB40.Auto[1..20] := 16#FFFFFFFF
|
DB40.Output = FALSE |
| 2 | Set DB40.Auto[7] := 16#0000_0001 (single bit drop) |
DB40.Output = TRUE |
| 3 | Restore DB40.Auto[7] := 16#FFFFFFFF
|
DB40.Output = FALSE on the next call |
| 4 | Set DB40.Auto[1..20] := 16#0000_0000 (all bits low) |
DB40.Output = TRUE |
| 5 | Cycle power to the CPU and re-run steps 1–4 | Identical results (confirms no remanent latch from retentive statics) |
For an FB, additionally confirm the instance DB contents in online view. Output should read FALSE immediately after the prologue runs and before the loop body executes.
8. Common Pitfalls and Field-Edge Cases
8.1 Using RETURN instead of a default write
RETURN is legal inside a FOR loop in SCL and exits the block immediately. It does not execute any code after the END_FOR;. Therefore any defaulting assignment after the loop is bypassed. If you need a true default at the end, use an ELSE branch or a flag-and-finalise pattern.
8.2 Mistaking an InOut for an Input
If Auto is declared as Input (not InOut), the FB makes a local copy on entry and the loop reads from the copy. Writes by the block back to the array are discarded. Use InOut when the block must mutate the array, or pass a separate output array.
8.3 Array index off-by-one
S7-1500 arrays default to lower bound 0. If the array is declared ARRAY[1..20], the FOR loop must iterate 1 to 20. A common bug is iterating 0 to 20, which reads Auto[0] (a runtime error or silent zero) and adds a phantom comparison.
8.4 Mixing literal constants and DB-tag constants
16#FFFFFFFF is a DWORD literal. If the array element is declared WORD, the comparison promotes to WORD and the high word of the literal is truncated, defeating the check. Always match the literal's data type to the array element's data type.
8.5 Optimised block access
S7-1500 blocks are stored in an optimised form by default (no absolute addresses). Forcing absolute access on the DB to use DB40.Output in non-SCL code disables optimisation and can cause surprising behaviour when the same address is also touched by an HMI tag. Prefer symbolic access.
8.6 Retentive statics in an FB
If the application actually needs the output to latch (e.g. a real fault memory), declare a Static FaultLatched : BOOL with the Set in IDB / Retain attribute and write to it inside the loop. The single-shot pulse is then the trigger, not the persistent state. Do not rely on the loop's exit state to retain a latched fault.
9. Comparison with Ladder and STL Equivalents
The same pattern expressed in ladder on the S7-1500 uses parallel contacts and an OR/AND network. There is no shared loop variable and the output is the result of the wire it sits on, so the FC/FB distinction does not produce a latch because each rung evaluates the output coil on every scan. The SCL programmer needs to emulate that wire-like evaluation explicitly with the pre-loop default write.
In classic STL (S7-300/400), the equivalent pattern uses L 0; T #Output at the top of the block and conditional loads inside the loop. STL programmers carry that habit over. SCL hides the same obligation, which is why the issue is more common in SCL than in STL.
10. Compiler Details and Firmware Notes
The default-value initialisation for FC output parameters is documented in the Siemens S7-1500 SCL programming manual. It is not optional and is not affected by the Optimised block access setting.
CPU firmware V2.9.x on the S7-1500 family (including 1512SP-1 PN 6ES7512-1DK02-0AB0) is the production baseline as of TIA Portal V17. The bug described here is a logic issue, not a firmware issue — no firmware update will change the behaviour of the SCL compiler with respect to FB output defaulting.
The Siemens Industry Online Support portal is the canonical source for the S7-1500 system manual, the S7-1500 SCL programming manual, and the application examples for FC/FB patterns. Search the knowledge base for "SCL FOR loop" and "FC output parameter initialisation" for the most current app notes.
11. Recommended Best Practice
-
Default the output before the loop. Always write
#Output := <steady-state value>;as the first executable statement in the block, regardless of FC or FB. -
Avoid
RETURNas a defaulting mechanism. Use a flag that the calling code checks, or set the output in theELSEbranch. -
Make the loop bounds runtime expressions of the array's lower and upper bounds. Use
LOWER_BOUNDandUPPER_BOUNDoperators (S7-1500) so the loop tracks array resizes. - Keep FBs stateless unless the application truly needs state. A pure-function FC is easier to reason about and harder to miscompile.
- Use symbolic access and keep blocks optimised. Only switch to absolute access for legacy integrations.
-
Document the state model in the block header. Note in the block comment whether
Outputis a momentary evaluation or a latched evaluation, and what the reset conditions are.
12. Variant: Doing It Without a FOR Loop
For arrays of DWORD the entire 640-bit check can be expressed in two SCL statements if the array is contiguous and the loop is not required for clarity:
#Output := FALSE;
FOR #i := 0 TO 19 DO
#Output := #Output OR (#Auto[#i] <> 16#FFFFFFFF);
END_FOR;
This avoids the RETURN and produces the same result with one extra logical OR per iteration. For larger arrays (e.g. > 1000 elements) the cumulative OR pattern can be replaced with a scalar sum and a check against the expected total, which executes faster on the S7-1500 because the loop body is shorter.
13. Frequently Asked Questions
Why does the same SCL code work in an FC but latch in an FB on the S7-1500?
The SCL compiler inserts default-value initialisation for FC output parameters (and for FC temp locals) at the top of the block body. In an FB, output parameters and static variables are stored in the instance DB and retain their last-written value across calls. If your code path can exit without writing the output, an FC defaults it to FALSE, an FB keeps the previous value — that is the latch.
Is RETURN inside a SCL FOR loop safe?
RETURN exits the entire block immediately, skipping any code after the END_FOR;. If your defaulting assignment sits after the loop, a RETURN from inside the loop will leave the output at whatever the previous call set it to. Either remove the RETURN and use a flag, or default the output on the line immediately before the loop.
Do I need to initialise output parameters of an FB explicitly?
Yes. The SCL compiler does not emit a default-value write for FB output parameters. Always write the steady-state value as the first statement in the block, before any conditional logic that may skip a write path.
Does this issue affect SCL on S7-1200 as well as S7-1500?
Yes. S7-1200 firmware V4.4 and later, and the S7-1500 family, both use the same SCL compiler rules for FC output defaulting and FB instance retention. The bug is identical on both platforms and on the ET 200SP CPU family.
What is the difference between an InOut and an Input parameter for the array?
An Input parameter is passed by value; the FB works on a local copy and cannot mutate the caller's array. An InOut parameter is passed by reference; the FB reads and writes the caller's variable directly. For an array of DWORD use InOut only if the block must modify the array. For a read-only scan such as the 20-DWord OR-check, Input is the correct choice and avoids accidental writes.