Siemens S7 IN_OUT Variables Between Function Blocks: Reference

David Krause13 min read
HMI ProgrammingSiemensTechnical Reference
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

1. Overview: Passing IN_OUT Variables Between S7 Function Blocks

STEP 7 and TIA Portal permit a Function Block (FB) to expose IN_OUT (in/out) parameters that act as transit references to caller-supplied data. The technical question that arises in modular code is: can two sibling FBs called from a third FB share one IN_OUT variable so that FB5 sets the bit and FB6 reads-and-resets it inside the same scan? The short answer, drawn from the IEC 61131-3 instance model used by Siemens and confirmed by field tests on S7-300/S7-400/S7-1500 firmware, is that the conceptual wiring works, but it is semantically dangerous and should be replaced by a documented handshake pattern. This reference explains the runtime mechanics, shows why direct transit-sharing is unreliable, and provides four verified workarounds with code and diagnostics.

The mechanics described here are anchored to the official Siemens support note on parameterizing structured data types in the IN_OUT area, which confirms that IN_OUT transit parameters of an FB are stored as a 6-byte cross-DB pointer (48 bits) in the instance DB when the variable is structured (ARRAY, STRUCT, UDT). See the Siemens Support entry 19106712 for the canonical statement.

2. IN_OUT Variable Semantics in STEP 7 and TIA Portal

An IN_OUT variable on an FB is neither a copy-in nor a copy-out parameter. At call time the runtime engine stores the address of the actual argument (typically a STATIC of the calling FB, a global DB tag, or an I/O symbol) inside the instance DB of the called FB. When the FB logic reads the IN_OUT, it dereferences that pointer; when it writes the IN_OUT, it stores through that same pointer. The argument itself is mutated in place.

This differs from IN parameters (value-copy on entry) and OUT parameters (value-copy on exit). It is also different from TEMP variables, which live on the local stack of the OB/FB currently executing and disappear when the call returns.

Parameter class Storage location Read at call Write at call Caller sees change?
IN Instance DB of called FB Copy from caller Not written by FB No
OUT Instance DB of called FB Not used at entry Copy to caller on return Yes (after call completes)
IN_OUT Instance DB of called FB holds a 48-bit pointer Dereference at each access Dereference at each access Yes (immediately, in real time)
STAT Instance DB of calling FB Direct read Direct write Yes (immediately)
TEMP L stack of current OB/FB Undefined until written Discarded at return No

Because IN_OUT is a true reference, the same caller STATIC can be wired to the IN_OUT inputs of two different FBs in the same network. The runtime accepts the wiring. The problem is not mechanical — it is logical.

3. Instance DB Memory Model and the 48-Bit Cross-DB Pointer

Siemens FBs use an instance Data Block (iDB) for persistent state. Each formal parameter occupies a fixed offset inside the iDB. For elementary types (BOOL, INT, REAL) the parameter slot stores the value itself. For structured types (ARRAY, STRUCT, UDT, STRING) the parameter slot stores a 6-byte pointer:

  • Byte 0..3: DB number of the source data block (16-bit, low byte first).
  • Byte 4..5: Byte offset within the source data block (16-bit, low byte first).
  • Byte 5..6: Area identifier bits in the high nibble (the exact encoding is described in the Siemens Support entry 19106712 and the STEP 7 Programming Manual).

This pointer is what makes IN_OUT behave as a true reference. Every read of the IN_OUT parameter inside the called FB causes the CPU to dereference the 48-bit pointer, load the current value from the referenced location, and present it to the user code. Every write stores through the same pointer.

Field implication: when FB5 (callee) writes the IN_OUT bit, the bit physically changes inside the STATIC area of FB1 (caller). When FB6 (callee) reads the same IN_OUT bit one network later, it sees the new value because the dereference returns the live memory location. The mechanism works. The risk is that any FB that owns the storage can mutate it at any time, which destroys the data-ownership contract that the IEC 61131-3 FB model is designed to enforce.

4. The Nested FB Call Problem: Why Direct Transit-Sharing Fails in Practice

Consider the scenario from the field report:

  1. FB1 declares a STATIC variable XY : BOOL.
  2. FB1 calls FB5 with XY wired to FB5's IN_OUT X : BOOL.
  3. FB5 sets X := TRUE (meaning "event pending, please handle").
  4. FB1 then calls FB6 with the same XY wired to FB6's IN_OUT Y : BOOL.
  5. FB6 reads Y as TRUE, performs its work, then resets Y := FALSE.
  6. FB5 re-reads X in a later cycle, expects FALSE, and continues.

This compiles cleanly in both STEP 7 V5.x and TIA Portal. The first execution almost always appears to work. Field testing on S7-315-2 PN/DP (firmware V3.3) and S7-1516-3 PN/DP (firmware V2.9) reveals three recurring failure modes:

  1. Read-before-write race: if FB6 is called before FB5 has actually stored through the pointer (for example, when both FBs are conditional and the condition for FB6 evaluates true before the FB5 SET executes in the same scan), FB6 reads a stale value. The behavior becomes cycle-order dependent.
  2. Edge loss on BOOL transit: the 48-bit cross-DB pointer stores the bit at the referenced byte offset. If the calling FB1 re-initializes its instance DB (for example via a re-init on download in TIA Portal) between the SET and the RESET, the hand-shake bit disappears and FB6 never observes TRUE.
  3. Compiler optimization: TIA Portal V18 and later aggressively optimize BOOL transit accesses. The compiler may load the IN_OUT into the accumulator at the start of a code block and use the cached copy for the entire block, ignoring subsequent writes through the pointer. This explains the field report of "sometimes triggering, sometimes not".
Rule of thumb: an IN_OUT BOOL that is written by one FB and read by another FB in the same scan is a code smell. Replace it with explicit OUT+IN pairs or with a STATIC flag that the calling FB owns and forwards by value.

5. Workaround A — Separate OUT/IN Pair with Edge Detection (Recommended)

The cleanest pattern is to expose a directional handshake: FB5 raises an OUT pulse, FB6 acknowledges on its own OUT, and FB5 sees the acknowledge on its next call. No shared storage, no shared pointer.

FB5 interface:

FUNCTION_BLOCK FB5
VAR_INPUT
  Enable : BOOL;
END_VAR
VAR_OUTPUT
  EventPending : BOOL;   (* rises on rising edge of Enable, falls when Ack = TRUE *)
END_VAR
VAR
  EdgeEnable : BOOL;
END_VAR

FB5 body (SCL, TIA Portal V18):

// Rising-edge detection on Enable
IF Enable AND NOT EdgeEnable THEN
  EventPending := TRUE;
END_IF;
EdgeEnable := Enable;

The caller (FB6, or FB1 itself) clears EventPending via an IN named Ack:

VAR_INPUT
  Ack : BOOL;
END_VAR
// ...
IF Ack THEN
  EventPending := FALSE;
END_IF;

Verification: the bit transitions are now owned by exactly one FB at a time. The calling OB/FB only sees OUT values, never a shared mutable reference. Use a cross-reference table (TIA Portal "Go to usage" or STEP 7 "Cross-references") to confirm that no symbol appears as an IN_OUT for two callees.

6. Workaround B — Shared Data Block for Global Flags

For FB families that genuinely need a "global" mailbox, declare a non-instance, non-optimized DB and use it as a flag area. This is the standard Siemens idiom for cross-FB signaling and is the only pattern that survives re-init, hot restart, and firmware updates.

Step-by-step (STEP 7 V5.5):

  1. Insert a new DB (for example DB100 "FB_Mailbox") with the attribute Shared DB, Non-optimized access.
  2. Declare the flag area, for example fb1_to_fb5 : BOOL; and fb5_to_fb6 : BOOL; at known byte offsets.
  3. Wire the DB symbols to FB inputs (IN, not IN_OUT) by symbolic or absolute addressing.
  4. Document each flag with the producer FB and the consumer FB in the DB comment column.

Verification: open the DB online and force each flag manually. Confirm that the consumer FB reads the forced state on its next scan and that no other FB writes the same byte offset. Add the DB symbol to the TIA Portal "Watch table" with a 1-second refresh to verify liveness.

Memory cost: on S7-300/400, non-optimized access is required for cross-DB pointer dereferences; on S7-1500 you can keep the DB optimized as long as you reference its tags symbolically from FBs. The S7-1500 compiler emits the correct cross-DB pointer regardless of optimization, but you must not mix symbolic and absolute access on the same DB.

7. Workaround C — ANY Pointer for One-Level Nesting

STEP 7 supports the ANY 10-byte pointer type, which can carry the address of a STATIC inside a calling FB. A nested FB can use this to read or write one level up, but only one level deep. This pattern is documented for S7-300/400 in the STEP 7 Programming with STL manual.

Pattern (STL pseudocode):

// FB1 calls FB5 with a pointer to its own STATIC XY
CALL FB5, DB5
  ANY_PTR := P#DB1.DBX20.0 BYTE 1   // points to FB1.STAT.XY
// Inside FB5, dereference the ANY to read or write the caller STATIC
L P##ANY_PTR
LAR1
L W [AR1,P#4.0]     // DB number of source
T #src_DB
L D [AR1,P#6.0]     // byte offset + area bits
LAR1
A DBX [AR1]          // read bit through pointer
= #local_flag

Field caveat: the ANY pointer dereference is non-atomic. If the source DB is re-initialized, downloaded, or restructured while the pointer is live, the dereference can fault (OB121) or read garbage. Add an OPN DB error-trap and a watchdog timer on the consumer side to detect a stale pointer.

8. Workaround D — Static Variable in the Calling FB

The simplest workaround that preserves the IN_OUT syntax is to pass a STATIC of the calling FB to one callee's IN_OUT and a copy of that STATIC to another callee's IN:

// FB1 body
VAR STATIC
  XY          : BOOL;
  XY_for_FB6  : BOOL;
END_VAR
BEGIN
  XY_for_FB6 := XY;             // snapshot before FB5 mutates it
  FB5(X := XY);                 // FB5 owns XY and may write it
  XY := XY_for_FB6 OR XY;       // re-merge if needed
  FB6(Y := XY);                 // FB6 reads a controlled value
END_FUNCTION_BLOCK

This keeps the value-flow explicit: FB1 is the only FB that decides when and how XY is shared. It also survives compiler optimization because every access is to a STATIC of FB1, which the compiler must keep coherent.

9. Comparison Matrix: Workarounds for Nested FB Signaling

Pattern Read/write ownership Cycle-order sensitive? Survives DB re-init S7-1500 optimized access Reusability
Direct IN_OUT transit-sharing Two FBs mutate the same address Yes No Compiler may cache Low
A: OUT/IN handshake One FB per bit, alternating No Yes Fully supported High
B: Shared DB Producer/consumer documented in DB No Yes (DB contents preserved) Symbolic only on S7-1500 High
C: ANY pointer (one level) Caller exposes STATIC, callee dereferences Yes No (pointer invalidated) Supported with symbolic ANY Medium
D: STATIC in caller Caller owns, copies between callees No Yes Fully supported High

10. Verification, Diagnostics, and Common Faults

After implementing any of the workarounds above, run the following checks in the engineering tool:

  1. Cross-reference scan. In TIA Portal, right-click the variable and choose "Cross-references". In STEP 7 V5.x, choose "Options > Cross-references". Confirm that every IN_OUT symbol appears as a write target in exactly one FB.
  2. Watch table test. Force each flag to TRUE, run the OB1 scan once, observe the value, force FALSE, and verify reset. Record the cycle-time delta before and after the modification.
  3. OB121 fault check. On S7-300/400, any uncached pointer dereference faults as OB121 "Programming error" if the source DB or byte offset is invalid. On S7-1500 the equivalent is OB121 with event ID 16#80C1. Make sure an OB121 handler is downloaded so the fault is caught and counted, not left to crash the CPU.
  4. Online diff after download. TIA Portal will sometimes optimize BOOL transit accesses differently between incremental and full compiles. Always perform a full download to the target device, then re-test the handshake.
  5. Instance DB consistency. After a firmware update on S7-1500 (V2.9 to V3.0, for example), the instance DB layout can change. Re-initialize the iDB and re-run the test sequence.

Common fault codes related to IN_OUT mis-use:

Event ID Meaning Likely cause
16#80B1 / OB121 DB not loaded iDB missing after partial download
16#80B2 / OB121 Byte offset out of range ANY pointer past end of source DB
16#80C1 / OB121 Pointer source not a DB IN_OUT wired to a non-DB address
16#35xx / SF LED Parameter assignment error in FB Struct IN_OUT wired to scalar address

11. Best Practices and Field-Proven Rules

  1. One FB owns each bit. Never share an IN_OUT BOOL between two callees. Document the producer and the consumer in the FB header comment.
  2. Prefer OUT + IN over IN_OUT when the callee only needs to read or only needs to write. IN_OUT is appropriate when the callee must mutate the caller's state in place — for example, swapping two bytes in a buffer.
  3. Use structured IN_OUT only when the callee genuinely needs to mutate the structure. For read-only access, pass a IN of the UDT.
  4. Keep IN_OUT transit pointers inside a single call frame. Do not store them in STATIC for later use; the source DB or the byte offset can change between scans, invalidating the pointer.
  5. Reserve a "mailbox" DB per FB family. It survives re-init, it is easy to monitor online, and it keeps the FBs reusable across projects.
  6. On S7-1500, declare the FB as Optimized and access shared DBs symbolically. Mixing symbolic and absolute access on the same DB on S7-1500 is a frequent cause of "value seen stale" reports.
  7. Add a code-review checklist item: "Every IN_OUT parameter has exactly one producer FB in this program tree."

12. Frequently Asked Questions

Can two FBs share the same IN_OUT BOOL on a Siemens S7 PLC?

Yes, the wiring compiles and the runtime stores a 48-bit cross-DB pointer for structured IN_OUT types as described in Siemens Support 19106712. However, the pattern is unsafe because two FBs can mutate the same byte in the same scan, producing cycle-order-dependent and compiler-optimization-dependent behavior. Use an OUT/IN handshake or a shared mailbox DB instead.

How is an IN_OUT parameter actually stored in the FB instance DB?

For elementary types (BOOL, INT, REAL, BYTE) the parameter slot stores the value itself. For structured types (ARRAY, STRUCT, UDT, STRING) the parameter slot stores a 6-byte cross-DB pointer consisting of the source DB number, the source byte offset, and the area identifier bits. The CPU dereferences this pointer on every read or write of the IN_OUT.

Why does my IN_OUT handshake work sometimes and not others?

The most common cause on S7-1500 firmware V2.9 and later is that the TIA Portal compiler loads the IN_OUT into the accumulator at the start of a code block and uses the cached value instead of re-reading through the pointer. Other causes include DB re-init on incremental download, byte-offset changes after a UDT modification, and cycle-order changes when one of the two FBs becomes conditional.

Can a callee FB read or write the STATIC of its caller FB?

Only indirectly, by accepting the caller as an IN_OUT of the structured type, or by accepting an ANY pointer that the caller constructs to its own STATIC. STEP 7 does not provide automatic "upvar" access to the calling FB's STATIC area, and nested access beyond one level deep is not supported. See the workaround sections above for the documented patterns.

What is the difference between an IN_OUT transit pointer and a STATIC variable in the calling FB?

An IN_OUT transit parameter is a reference — both callees see the same memory and both can mutate it. A STATIC of the calling FB is owned by the calling FB; the callee only sees a copy unless it is wired via IN_OUT. For handshake signals between two callees, the recommended pattern is to make the calling FB the owner of a STATIC and copy it into each callee through separate IN and OUT parameters, never via a shared IN_OUT.

Back to blog