Problem: FC Output Parameter Fails to Update in the Calling FB
When a Function (FC) is called from multiple networks of a Function Block (FB), an output parameter written by the FC does not always reach downstream networks of the calling FB. The classic symptom is a bit that the FC clearly sets inside its own logic, yet the calling FB reads as zero on the next scan. In one representative case from the field, the calling FB looks like the STL code below.
// FB local interface
// TEMP Err_New : BOOL
// TEMP Ack : BOOL
// TEMP Alarm : BOOL
NW1: A #Err_New
= #Alarm
NW2: CALL FC 220
Accept := #Ack
Errors := MW82
New_Error := #Err_New
NW3: CALL FC 220
Accept := #Ack
Errors := MW86
New_Error := #Err_New
The author reports that the path through NW2 (Errors := MW82) successfully drives #Err_New high, and #Alarm therefore asserts in NW1. The path through NW3 (Errors := MW86) does the same internal work but #Err_New never becomes true, and the #Alarm bit in NW1 remains low. Reordering the networks, swapping to a Merkerbit, and double-checking the MW82/MW86 addresses produce no change. The fault is not in the addressing — it is in the variable class.
This article explains the underlying STEP 7 variable semantics, identifies the exact mechanism that drops the OUT value across FC calls, and provides three field-verified fixes plus a diagnostic procedure and worked code in STL, LAD, and SCL. The behavior is documented in the STEP 7 V5.x programming reference and the TIA Portal help; the manuals themselves are listed on Siemens Industry Online Support.
Root Cause: TEMP, OUT, and IN_OUT Variable Semantics in STEP 7
STEP 7 (TIA Portal and the classic V5.x line) partitions block-local variables into four classes that behave very differently across block calls. Misunderstanding one of them produces the exact symptom above: a value that the FC writes through its OUT parameter disappears by the time the calling FB reads it.
| Class | Stored in | Initial value | Read by FC/FB? | Written by FC/FB? | Survives scan cycle? |
|---|---|---|---|---|---|
| IN | Caller argument, copied to L stack | Value supplied by caller | Yes (treated as input) | No (write has no effect) | No — refreshed at call |
| OUT | Caller argument (passed by reference) | Undefined (caller value not imported) | No — caller value is not imported into the L stack | Yes | Caller can use after return |
| IN_OUT | Caller argument (passed by reference) | Caller value, imported into L stack | Yes | Yes | Caller value updated on return |
| TEMP | L stack of the calling OB | Undefined (whatever was on the stack) | Yes (but undefined) | Yes | No — not initialized; reused by other blocks |
| STAT | Instance DB (FB only) | Initial value from declaration | Yes | Yes | Yes — persistent across scans |
1 to its New_Error OUT, but the CPU does not copy the previous value of the caller's argument into the FC's local stack at entry. If the FC's logic checks the OUT before assigning to it, the check sees an undefined L-stack value, not #Err_New in the calling FB.The local #Err_New in the calling FB is declared TEMP. The FC's New_Error is declared OUT. Two separate STEP 7 mechanisms interact:
-
OUT is strictly a sink. FC220's
New_Erroris written by FC220's body, but FC220 does not read its previous value. If FC220 internally performsAN #New_ErrororA #New_Erroranywhere, the operand resolves to garbage on the L stack — not to#Err_Newin the calling FB. -
TEMP is not initialized. The CPU does not zero
#Err_Newat the start of each scan. Its L-stack slot holds whatever the previous block left there. After the NW2 call, the slot contains the value FC220 just wrote. After the NW3 call, the slot again contains whatever FC220 wrote. If FC220's logic for the MW86 path does not explicitly setNew_Errortrue,#Err_Newretains its prior value (the0from the previous scan), and NW1 sees0.
That is why the symptom appears as "FC works for one call path, fails for the other": the L-stack aliasing is identical, but FC220's internal logic only forces New_Error := TRUE under the conditions triggered by MW82, not MW86. Adding a new network, expanding the L-stack usage of any block in OB1, or changing the order of FC calls can shift what the residual L-stack contents are — which is exactly the kind of "spooky" behaviour that appears years into a working program.
Affected Block Types and STEP 7 / TIA Portal Versions
The mechanism is the same in every Siemens S7 platform because it is part of the IEC 61131-3 execution model that STEP 7 implements:
| Platform | Symptom present? | Compiler warning? | Notes |
|---|---|---|---|
| STEP 7 V5.5 / V5.6 (S7-300, S7-400, WinAC) | Yes | None by default | Strictest manifestation; L stack sized manually |
| TIA Portal V13–V18 (S7-300/400, S7-1200, S7-1500) | Yes | Info-level "Uninitialized operand" depending on block access setting | Optimized block access reduces cross-block L-stack overlap |
| S7-1200 / S7-1500 with optimized access | Reduced but not eliminated | Stronger (V18 adds unreachable-code checks) | TEMPs still not initialized; STAT remains the durable answer |
The fix is independent of the STEP 7 edition; only the warning visibility changes. See the STEP 7 V5.x "Programming and Operating Manual — Ladder Logic (LAD), Function Block Diagram (FBD), Statement List (STL)" on Siemens Product Support for the relevant section on block-local variable classes.
Fix 1: Change OUT to IN_OUT on the FC Parameter
This is the minimal change with the smallest surface area. It addresses the call-path problem directly: FC220 will see the prior value of New_Error from the caller, can read it, can clear it, and writes its result back to the same memory location.
Step-by-step:
- Open the FC220 interface declaration in the STEP 7 / TIA Portal editor.
- Change the declaration class of
New_ErrorfromOUTtoIN_OUT. Keep the data type (BOOL) and the name unchanged. - Save and compile the FC. The compiler regenerates the FC's call interface.
- Every CALL FC 220 site is automatically updated: the actual argument now flows in both directions. No code change is required in NW2 or NW3.
- Inside FC220, replace any
A #New_Error/AN #New_Errorusage with the same instructions — they now operate on the imported caller value, not on L-stack garbage.
Resulting FC220 interface and body:
FUNCTION FC 220 : VOID
VAR_INPUT
Accept : BOOL;
END_VAR
VAR_IN_OUT
New_Error : BOOL; // was OUT, now IN_OUT
END_VAR
VAR_TEMP
info : WORD;
END_VAR
BEGIN
// Example body: latch new error, clear on accept
IF (info <> 0) THEN
New_Error := TRUE;
ELSIF (Accept) THEN
New_Error := FALSE;
END_IF;
END_FUNCTION
The call sites in the FB stay syntactically identical:
NW1: A #Err_New
= #Alarm
NW2: CALL FC 220
Accept := #Ack
Errors := MW82
New_Error := #Err_New // now bi-directional
NW3: CALL FC 220
Accept := #Ack
Errors := MW86
New_Error := #Err_New // now bi-directional
#Err_New at entry. If the original code contained IF New_Error THEN RETURN; END_IF; (a "latch early" pattern), the call from NW3 will now see the value set by NW2's previous call. That is usually what you want for an alarm aggregator, but confirm against the function's specification.Fix 2: Convert TEMP to STAT in the Instance DB
The second fix removes the problem at the calling FB. Convert #Err_New from a TEMP local to a STAT static in the FB's instance DB. STATs are initialized to their declared default at the first call, retain their value across scans, and are visible to every network of the FB in deterministic order.
Step-by-step:
- Open the FB in the editor and select the interface section.
- Move the
Err_Newdeclaration from theTempcolumn to theStatcolumn. Set the initial value toFALSE. - Save the FB. The associated instance DB (DB-i for FB-y) is regenerated and
Err_Newbecomes a persistent symbol. - Re-run the program and download both the FB and the instance DB to the CPU. The DB is not retentive by default; if the value must survive a CPU restart, set the relevant area as retentive in the CPU properties or assign a start value via OB100.
FB interface (after fix):
FUNCTION_BLOCK FB 100
VAR
Err_New : BOOL; // STAT — persists across scans
Ack : BOOL;
Alarm : BOOL;
END_VAR
BEGIN
// NW1
IF Err_New THEN
Alarm := TRUE;
END_IF;
// NW2 (SCL)
FC220_Ack_82.Accept := Ack;
FC220_Ack_82.Errors := MW82;
FC220_Ack_82.New_Error := Err_New;
// NW3 — re-uses the call pattern for MW86
END_FUNCTION_BLOCK
The combination of STAT on the caller and IN_OUT on the callee is the most robust pattern. STAT guarantees that Err_New retains its value between the call in NW2 and the read in NW1 on the next scan, regardless of how the L stack is sized or ordered. The combined pattern is the recommended approach in the Siemens "Programming Guideline for S7-1200/S7-1500" available on Siemens Industry Online Support.
Fix 3: Use a Global Marker (M) Word with Single-Writer Discipline
If neither the FC nor the FB interface can be modified (third-party blocks, library objects, signed-off design), use a global Merkerbit and apply strict single-writer discipline. The M area is shared and initialized; reading it from another network returns the last value any code wrote.
NW1: A M 100.0
= #Alarm
NW2: CALL FC 220
Accept := #Ack
Errors := MW82
New_Error := M 100.0 // bit address, not TEMP
NW3: CALL FC 220
Accept := #Ack
Errors := MW86
New_Error := M 100.0
Rules for safe M-area use:
- Declare a Merker word or bit area as "used by FC220 alarm aggregator" in the project tag list. Document the owner.
- Use exactly one logical writer. Both NW2 and NW3 writing the same M bit is acceptable only when the logic is an OR-style aggregation, which is what the FC does internally.
- For S7-300/400, ensure the bit is inside the configured Merker area (CPU properties → "Memory"). Out-of-range bits read as
0silently. - Reserve a non-overlapping range (e.g. MB100–MB199) for FC220-family alarms to avoid collisions with other libraries.
M area is intentionally small (typically 4096 bytes) and its bits are not guaranteed byte-aligned for non-boolean accesses. Stick to %M booleans or move to instance-DB-backed tags. The M-area trick is a classic STEP 7 V5.x pattern and should be retired on new code.Best Practices: Variable Naming Conventions and Declaration Hygiene
The "everything compiles, the bit does not set" symptom almost always traces to a missing convention. Adopt the following prefix scheme, used widely in European STEP 7 shops and recommended in the Siemens "Programming Guideline for S7-1200/S7-1500" entry on Siemens Industry Online Support:
| Prefix | Variable class | Example |
|---|---|---|
i_ |
IN (block input) | i_Accept |
q_ |
OUT (block output) | q_Alarm |
iq_ |
IN_OUT (read+write parameter) | iq_NewError |
(lowerCamelCase) |
TEMP | tempIndex |
UPPER_SNAKE |
STAT (multi-word tag) | ALARM_STATE |
M_ |
Global Merkerword or Merkerbit (special) | M100_0_ALARM_A |
With this scheme, the line iq_NewError immediately communicates "this is a bi-directional parameter, the callee reads and writes it." A bug where a TEMP is used as an input to a callee is visible at code review: tempNewError is the wrong variable, q_NewError is the wrong direction.
Diagnostics: Confirming the Fault with Cross-References and VAT Monitoring
Before changing any code, reproduce and isolate the fault with the standard STEP 7 tools.
-
Cross-reference (XRef). Select
#Err_Newin the FB, right-click → "Go to → Cross-references". Confirm that NW1, NW2, and NW3 are the only writers/readers. If the symbol is used in any other FB, the residual L-stack value may belong to that block, not to FC220. -
Watch / VAT table. Create a Variable Table (VAT) containing
MW82,MW86, the address backing#Err_New(use the FB's instance DB and the offset; for TEMP, monitor the absolute L-stack address shown in the local data area), and the address of#Alarm. Trigger on the rising edge of either MW and observe the sequence. -
Force the path. In online mode, force
MW82 := 0andMW86 := W#16#FFFF. Run the program. If the alarm triggers, the FC220 path for MW86 is correct and the bug is in the L-stack/TEMP interaction. If it does not, the bug is inside FC220 for the MW86 path. -
Single-step. Set a breakpoint at NW1 and at the start of FC220. Step through a single scan. Watch the value of the L-stack slot associated with
#Err_Newat every instruction. The value should change only when FC220 writes it; if it changes between calls without a write, the L stack is being clobbered by another block. - L-stack sizing check. Open the CPU hardware configuration, select the CPU, and inspect the "Local Data" area for OB1 priority. S7-300 default is 256 or 512 bytes; S7-400 default is 1024 bytes. If the sum of TEMP sizes of all blocks called in OB1 exceeds this value, the CPU enters STOP with SF "Local data length error" — but the OB can continue until the overflow occurs. A latent STOP cause may explain why a TEMP value is suddenly wrong after a code change.
| Symptom observed | Most likely cause | Confirm with | Fix |
|---|---|---|---|
| One FC call path sets the bit, the other does not | FC body never writes OUT in the failing path | Force MW and step FC | Change OUT to IN_OUT; fix FC body |
| Bit never visible in calling FB despite FC writing it | TEMP L-stack aliasing | Monitor L-stack slot, cross-ref | Convert TEMP to STAT |
| Behaviour changed after adding a new network | OB1 local-data size exceeded | SF diagnostic buffer, L-stack size | Reduce TEMP usage or raise OB1 local-data size |
| Bit only sets on the first scan after download | Instance DB re-initialized, STAT defaults restored | Compare DB online/offline | Add start value in OB100, mark area retentive |
Verified Working Code in STL, LAD, and SCL
Three equivalent implementations of the same alarm-aggregator pattern, with the TEMP/OUT issue fixed.
STL with IN_OUT (recommended minimum change)
// FC 220
FUNCTION FC 220 : VOID
VAR_INPUT
Accept : BOOL;
END_VAR
VAR_IN_OUT
New_Error : BOOL; // IN_OUT — read+write
END_VAR
VAR_TEMP
info : WORD;
END_VAR
BEGIN
info := LW0; // example, replace with actual input
IF (info <> 0) THEN
New_Error := TRUE;
ELSIF (Accept) THEN
New_Error := FALSE;
END_IF;
END_FUNCTION
// FB 100 call sites — unchanged
NW1: A #Err_New
= #Alarm
NW2: CALL FC 220
Accept := #Ack
Errors := MW82
New_Error := #Err_New
NW3: CALL FC 220
Accept := #Ack
Errors := MW86
New_Error := #Err_New
LAD with STAT
In the FB interface change Err_New from TEMP to STAT with initial value FALSE. LAD network:
Network 1 — read:
--[ ]--ErrNew-------------( )--
| Alarm
Network 2 — FC220 call (MW82):
FC220
EN ENO
Accept Ack
Errors MW82
New_Error ErrNew
Network 3 — FC220 call (MW86):
FC220
EN ENO
Accept Ack
Errors MW86
New_Error ErrNew
SCL (Structured Control Language) with explicit latch
FUNCTION_BLOCK 'FB_AlarmAgg'
VAR
ErrNew : BOOL; // STAT
Ack : BOOL;
Alarm : BOOL;
END_VAR
VAR_TEMP
info : WORD;
END_VAR
BEGIN
// NW2
'FC220_Ack_82'(Accept := Ack,
Errors := 'MW82',
New_Error := ErrNew);
// NW3
'FC220_Ack_86'(Accept := Ack,
Errors := 'MW86',
New_Error := ErrNew);
// NW1 — must run after both calls in the same scan
IF ErrNew THEN
Alarm := TRUE;
END_IF;
END_FUNCTION_BLOCK
SCL makes the execution order explicit and helps the compiler detect uninitialized reads. Combine with the "single instance per call" rule: for FBs used as multi-instance, give each call its own instance DB so STATs do not alias. Naming standards and best-practice patterns are documented in the TIA Portal programming manual on Siemens Product Support.
TIA Portal Pitfalls: Optimized Block Access and Watch Tables
Modern TIA Portal projects default to "Optimized block access" on every new FB/FC. This is generally an improvement — it removes the absolute-address requirement and protects TEMP variables from L-stack aliasing between blocks — but it changes the diagnostic procedure:
- You can no longer read
Err_Newin a VAT by absolute address. Use the symbolic name from the instance DB, or pin a watch on the local in "Monitor". - With optimized access, the compiler may inline an FC (single-instance inlining) when only one call exists. With two calls (NW2 and NW3) the compiler must keep the call; the inlining heuristic is therefore not the cause of the original symptom, but it is worth checking "Compiler → Block size" to confirm.
- In TIA Portal V16+, the project-wide "Uninitialized variable usage" check (Settings → Compile → "Extended consistency check") can be enabled to flag reads of a TEMP before any write. Enable it for new projects.
Err_New as seen by NW1, place the watch on the instance DB symbol (after converting to STAT) or trigger on the rising edge of MW82/MW86 with a latching reference.L Stack Sizing and CPU Local Data Limits
The L stack is allocated per OB priority class. Sizing it correctly is the first defence against residual-value bugs.
| CPU family | Default OB1 local data | Maximum | Failure mode if exceeded |
|---|---|---|---|
| S7-300 (e.g. CPU 315-2 PN/DP) | 256 B | 4096 B | STOP, SF "Local data length error" |
| S7-400 (e.g. CPU 416-3) | 1024 B | 32 KB | STOP, SF same |
| S7-1200 (e.g. CPU 1215C) | n/a (optimized access) | n/a | Compiler warning instead of runtime error |
| S7-1500 (e.g. CPU 1515-2 PN) | n/a (optimized access) | n/a | Compiler warning + runtime diagnostic buffer entry |
For STEP 7 V5.x projects, the local-data sizes per priority class are configurable in HW Config → CPU → Properties → Local Data. Calculate the sum of the largest TEMP footprint of every block reachable from OB1 (use the cross-reference "Local data usage" view) and reserve headroom of at least 10%. The "worked for years, broke after adding a network" pattern is the classic signature of a project that grew past its configured L stack.
Frequently Asked Questions
Why does the FC output work for one call path and fail for the other?
Because the local #Err_New is declared TEMP, its value is whatever the L stack contains at the moment NW1 reads it. The two FC220 call paths leave different residual values — one path's internal logic writes the OUT explicitly, the other path does not. The L stack is not zeroed between calls, so NW1 sees a stale value that is not what FC220 produced.
Is it ever safe to read a TEMP variable in a Siemens S7 block?
Only after every code path in the block has written the TEMP. If the block contains an early return, an ELSIF branch that does not assign, or a function that conditionally assigns, the TEMP is only defined on the path that ran. Use STAT or initialize the TEMP explicitly (e.g. L FALSE; T #Err_New as the first instruction) to make the value deterministic.
Should I change OUT to IN_OUT on every FC that returns a state bit?
Yes, if the FC needs to see the prior value to compute the new value (typical for latch/clear patterns like an alarm handler). If the FC only emits a value and never reads the prior state, OUT is correct and slightly more efficient. The deciding question is: does the FC's body contain an instruction that addresses the parameter as an input (e.g. A #New_Error)? If yes, it must be IN_OUT.
Why did the bug appear only after I added a new network?
Two likely reasons. First, the new network pushed the total L-stack usage of OB1 over the configured local-data size, causing the CPU to use adjacent memory for the new TEMPs and corrupting the slot shared with #Err_New. Second, the new network changed the execution order or the residual L-stack contents enough that FC220's two call paths now leave different values behind. Either way, the fault was latent from day one and the new network is the trigger, not the cause.
Does TIA Portal's optimized block access make TEMP safe?
No. Optimized access prevents L-stack aliasing between blocks, which removes the cross-block contamination. It does not initialize a TEMP to a defined value at the start of each call, and it does not persist a TEMP across scans. The OUT-vs-IN_OUT distinction is unchanged. To eliminate the entire class of "value disappeared" bugs, convert the variable to STAT (in the instance DB) and use IN_OUT on every FC that reads and writes the same parameter.