Resolving Siemens S7 FC OUT Variable Not Setting on Calls

David Krause17 min read
HMI ProgrammingSiemensTroubleshooting
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: 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
Critical: An OUT parameter is a write-only sink from the called block's point of view. The FC may write 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:

  1. OUT is strictly a sink. FC220's New_Error is written by FC220's body, but FC220 does not read its previous value. If FC220 internally performs AN #New_Error or A #New_Error anywhere, the operand resolves to garbage on the L stack — not to #Err_New in the calling FB.
  2. TEMP is not initialized. The CPU does not zero #Err_New at 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 set New_Error true, #Err_New retains its prior value (the 0 from the previous scan), and NW1 sees 0.

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
TIA Portal caveat: "Optimized block access" (default for S7-1200/1500) maps block-local variables to symbolic offsets in the instance DB rather than to fixed L-stack slots. This eliminates L-stack aliasing between blocks, so the spurious "left-over value" effect is reduced. It does not make a TEMP variable persistent across scans — it only removes the cross-block contamination. The OUT-vs-IN_OUT semantics are unchanged.

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:

  1. Open the FC220 interface declaration in the STEP 7 / TIA Portal editor.
  2. Change the declaration class of New_Error from OUT to IN_OUT. Keep the data type (BOOL) and the name unchanged.
  3. Save and compile the FC. The compiler regenerates the FC's call interface.
  4. 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.
  5. Inside FC220, replace any A #New_Error / AN #New_Error usage 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
Behavioural change to validate: After changing to IN_OUT, FC220 can read the current value of #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:

  1. Open the FB in the editor and select the interface section.
  2. Move the Err_New declaration from the Temp column to the Stat column. Set the initial value to FALSE.
  3. Save the FB. The associated instance DB (DB-i for FB-y) is regenerated and Err_New becomes a persistent symbol.
  4. 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 0 silently.
  • Reserve a non-overlapping range (e.g. MB100–MB199) for FC220-family alarms to avoid collisions with other libraries.
Migration warning: On S7-1200/1500 the 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.

  1. Cross-reference (XRef). Select #Err_New in 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.
  2. 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.
  3. Force the path. In online mode, force MW82 := 0 and MW86 := 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.
  4. 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_New at 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.
  5. 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_New in 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.
Watch table caveat: When you put a TEMP variable into a TIA Portal watch table, the value displayed is the value at the entry of the current block invocation, not at the moment the FB ends. To observe the post-FC value of 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.

Back to blog