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:
- FB1 declares a STATIC variable
XY : BOOL. - FB1 calls FB5 with
XYwired to FB5'sIN_OUT X : BOOL. - FB5 sets
X := TRUE(meaning "event pending, please handle"). - FB1 then calls FB6 with the same
XYwired to FB6'sIN_OUT Y : BOOL. - FB6 reads
Yas TRUE, performs its work, then resetsY := FALSE. - FB5 re-reads
Xin 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:
- 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.
- 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.
-
Compiler optimization: TIA Portal V18 and later aggressively optimize BOOL transit accesses. The compiler may load the
IN_OUTinto 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".
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):
- Insert a new DB (for example
DB100 "FB_Mailbox") with the attribute Shared DB, Non-optimized access. - Declare the flag area, for example
fb1_to_fb5 : BOOL;andfb5_to_fb6 : BOOL;at known byte offsets. - Wire the DB symbols to FB inputs (
IN, notIN_OUT) by symbolic or absolute addressing. - 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.
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:
-
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_OUTsymbol appears as a write target in exactly one FB. - 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.
- 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.
- 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.
- 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
-
One FB owns each bit. Never share an
IN_OUTBOOL between two callees. Document the producer and the consumer in the FB header comment. -
Prefer
OUT+INoverIN_OUTwhen the callee only needs to read or only needs to write.IN_OUTis appropriate when the callee must mutate the caller's state in place — for example, swapping two bytes in a buffer. -
Use structured
IN_OUTonly when the callee genuinely needs to mutate the structure. For read-only access, pass aINof the UDT. -
Keep
IN_OUTtransit 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. - 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.
- 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.
-
Add a code-review checklist item: "Every
IN_OUTparameter 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.