VAR_IN_OUT vs VAR_INPUT and VAR_OUTPUT in IEC 61131-3 Function Blocks
Programmable Logic Controllers (PLCs) that conform to IEC 61131-3 implement user-defined Function Blocks (FBs) with three distinct interface variable categories: VAR_INPUT, VAR_OUTPUT, and VAR_IN_OUT. Engineers coming from a procedural language background, or from platforms that predate the IEC standard, frequently confuse the three because the keywords look similar but produce fundamentally different machine code at the call boundary. Choosing the wrong one yields a program that compiles cleanly, runs without an error, and silently returns wrong values - one of the most expensive classes of bug in any control system.
This reference covers the language-level contract that the IEC 61131-3 standard defines for the three parameter types, the implementation differences between Siemens TIA Portal, Mitsubishi GX Works, Beckhoff TwinCAT, Schneider Electric Machine Expert, and Bosch Rexroth, and the correct interaction with retain / persistent storage for variables that must survive a power cycle.
1. The IEC 61131-3 POU Interface Model
IEC 61131-3 defines a Program Organisation Unit (POU) as one of three things: a function, a function block, or a program. Functions and function blocks carry typed formal-parameter lists in their declaration part. The standard recognises four parameter categories for FBs:
| Keyword | Direction | Read in FB? | Write in FB? | Caller can read? | Caller can write? |
|---|---|---|---|---|---|
VAR_INPUT |
Caller → FB | Yes | Forbidden (semantic) | Yes (source) | Yes (source) |
VAR_OUTPUT |
FB → Caller | Forbidden (semantic) | Yes | Yes (after call) | No (before assignment) |
VAR_IN_OUT |
Caller ↔ FB | Yes | Yes | Yes | Yes |
VAR (static) |
Local to FB instance | Yes | Yes | No (encapsulated) | No |
The semantic column above is critical. IEC 61131-3:2013 Section 6.5.2 specifies that the implementation may permit a write to a VAR_INPUT variable in textual languages - nothing in the standard physically prevents the compiler from accepting Input1 := 0; - but the standard mandates that any write to a formal input parameter is discarded on return from the FB instance. The next invocation starts from the value the caller provided. This is the source of the most common bug: a developer writes a counter, the counter increments inside the FB, the FB returns, the value is back to what the caller put in, and the process runs once instead of accumulating.
2. The Pass-By-Value vs Pass-By-Reference Boundary
The semantic table maps directly to two implementation strategies at the call site:
2.1 Pass-by-value (copy semantics)
For VAR_INPUT parameters, the compiler emits code that copies the caller's source operand into a scratch location owned by the FB before the FB body executes. For VAR_OUTPUT, the compiler emits code that copies a scratch location back to the caller's destination operand after the FB body finishes. The scratch is typically allocated in a reserved range of the data area. The exact range is platform-specific:
| Platform | Scratch region for FB temps | Source |
|---|---|---|
| Mitsubishi GX Works2 / GX Works3 (FX, Q, iQ-R) | Upper D-register range above D9999 (e.g. D12285–D12287) and reserved M/BN contacts | GX Works3 Simple Project FB manual |
| Siemens TIA Portal S7-1200/S7-1500 | Temp area inside the FB's instance DB / block-local TEMP | S7-1200 / S7-1500 System Manual |
| Beckhoff TwinCAT 3 | Process image copy in the PLC task stack | TwinCAT 3 PLC Introduction |
| Schneider Machine Expert (M241/M251/M580) | IEC task stack per POU invocation | Machine Expert SoM Prog Help |
| Bosch Rexroth IndraWorks (MLC / ctrlX) | POU-local stack frame on call | IndraLogic 2G Programming Manual |
Because the temp region is shared across every FB invocation in the same task, two consequences follow:
- Writing to the temp from HMI, a programming console, or a different POU is undefined behaviour - the next FB call will overwrite it.
- Re-entrancy (calling the same FB instance from inside its own body, or from an alarm-interrupted context) is not safe for value-copied parameters on platforms that do not protect the stack.
2.2 Pass-by-reference (pointer semantics)
For VAR_IN_OUT parameters, the compiler emits code that loads the address of the caller's variable into a register and passes that address to the FB. Inside the FB body, every read or write dereferences the pointer. The FB does not own a copy; the caller's memory is the storage. The standard states that VAR_IN_OUT is intended for composite data types and for any parameter that must be modified in place by the FB and observed by the caller after the call returns.
3. The Rule of Thumb for Each Category
3.1 VAR_INPUT
Use VAR_INPUT for operands whose value the FB consumes and never returns. Examples: a setpoint, a mode selector, a measurement from an analog input, an enable bit, a recipe index. The caller retains ownership of the source location and can overwrite it freely after the call.
3.2 VAR_OUTPUT
Use VAR_OUTPUT for results that the FB produces for the caller. Examples: a completion flag, a measured duration, a derived alarm, an output value that the FB computes. The caller must not write to the output before the call returns - doing so is a semantic violation because the FB's own write will be the last assignment observed by the caller.
3.3 VAR_IN_OUT
Use VAR_IN_OUT when the caller supplies a value that the FB will mutate in place. Examples: a counter word that the FB increments, a string buffer that the FB appends to, an array that the FB sorts, a state variable that the FB advances, a buffer that the FB fills. Because the storage is the caller's, the value persists across calls and is visible to every other POU that references the same variable.
4. How Siemens TIA Portal Handles Each Type
In S7-1200 and S7-1500 programming with TIA Portal V16 and later, the FB call interface exposes In, Out, InOut, Static, and Temp sections. The compiler maps these to:
- In – value copied to the FB's block-local TEMP on each call (pass-by-value for elementary types, pass-by-reference for VARIANT and complex types such as arrays and STRUCTs of small size that the compiler fits in a register).
- Out – value copied from a block-local TEMP to the caller's destination after the call. Unconnected Out pins are skipped at the call site, so the compiler does not emit a store instruction.
- InOut – a pointer (ANY-pointer for elementary types, area-internal pointer for complex types) to the caller's operand is passed. Unconnected InOut pins generate a compile error in the SCL/ST language; in LAD/FBD, the call site must have a valid address.
- Static – persistent across calls for the lifetime of the instance DB. Survives a power cycle only if the instance DB is configured as retentive.
- Temp – scratch, re-initialised on every call. Never retentive.
5. How Mitsubishi GX Works Handles Each Type
The Mitsubishi implementation under GX Works2 and GX Works3 deviates from the IEC 61131-3 reference model because the platform evolved from a fixed-memory architecture. When a FB is created in the IEC language editor, the compiler pre-allocates scratch registers from the upper end of the D-register range (and from reserved M/B contacts) to use as temporaries for value-copied VAR_INPUT and VAR_OUTPUT arguments. Consider a simple FB that adds two inputs to one output, called in a program:
// FB declaration
FUNCTION_BLOCK FB_Add
VAR_INPUT
a : INT;
b : INT;
END_VAR
VAR_OUTPUT
c : INT;
END_VAR
When the compiled program executes the call FB_Add(i_a := D0, i_b := D1, o_c => D100); the runtime performs:
- Move
D0 → D12286(a in temp) - Move
D1 → D12287(b in temp) - Jump to the FB body, execute the ADD, store result in
D12285(c in temp) - Return to the program, move
D12285 → D100(c out)
Two field-proven caveats follow from this architecture:
- Any attempt to read
D12285..D12287from HMI, a script, or a non-IEC program is unsafe. The values are valid only at the boundaries of the call. Always rely on the connectedVAR_OUTPUToperands, not on the scratch. - Re-entrant FBs that call themselves (directly or via an interrupt-triggered instance sharing the same static area) can corrupt the call in progress. Mitsubishi's solution is to declare the FB as macro type or to use the function-block instance label to give every instance its own DB in the upper range.
6. How Beckhoff TwinCAT Handles Each Type and Adds PERSISTENT
TwinCAT 3 declares the interface of a POU in its declaration part, between the POU type keyword and END_VAR blocks. Inside an FB, the parameter blocks map to the IEC standard:
FUNCTION_BLOCK FB_Counter : FB_Base
VAR_INPUT
iEnable : BOOL;
iStep : INT;
END_VAR
VAR_OUTPUT
oDone : BOOL;
END_VAR
VAR_IN_OUT
ioCount : INT; // pass-by-reference, persistent if backed by a PERSISTENT variable
END_VAR
VAR PERSISTENT
pLastValue : INT; // retains across power-cycle if the variable is also RETAIN
END_VAR
VAR
sLocal : INT; // initialised on instance start, not retained
END_VAR
Per the TwinCAT 3 PLC introduction, the keyword PERSISTENT can be combined with VAR, VAR_GLOBAL, and VAR_IN_OUT declarations. The Beckhoff documentation states: "You can declare persistent variables by adding the keyword PERSISTENT after the keyword for the variable type (VAR, VAR_GLOBAL, etc.) in the declaration part of the POU." A persistent variable is written to a backed-up memory area at the end of each PLC cycle and re-read at the start of the next cycle. On a warm restart the value is restored; on a cold restart the value is initialised.
PERSISTENT variables are loaded from a backup file on a warm restart. RETAIN variables survive an uncontrolled stop (power loss) only if the controller is equipped with a UPS that the runtime can use to flush the retain image. For a variable that must survive a hard power cycle, declare it RETAIN and configure the UPS or battery-backed SRAM in the controller hardware description. For a variable that must survive a controlled stop/restart but not necessarily a power loss, PERSISTENT is sufficient.7. How Schneider Electric Machine Expert Handles Each Type
EcoStruxure Machine Expert (formerly SoMachine) and Machine Expert V1.1 use the same IEC 61131-3 syntax as Codesys-based platforms. The product help states: "Between the keywords VAR_IN_OUT and END_VAR, all variables are declared that serve as input and output variables for a POU. VAR_IN_OUT variables of a function…" The help continues by specifying that VAR_IN_OUT parameters are passed by reference and may not be assigned a constant at the call site - the caller must supply a writable address. The same restriction applies to TwinCAT, IndraWorks, and TIA Portal. A constant (literal) is not a writable address; the compiler rejects the call.
| Parameter block | Schneider Machine Expert direction | Pass mechanism | Caller may omit? |
|---|---|---|---|
VAR_INPUT |
Caller → FB | By value (elementary), by reference (composite) | Yes (default applies) |
VAR_OUTPUT |
FB → Caller | By value (copy back) | Yes |
VAR_IN_OUT |
Caller ↔ FB | By reference (pointer) | No - must be connected to a variable |
VAR (instance) |
Local | Instance memory | n/a |
8. How Bosch Rexroth IndraWorks Handles Each Type
The IndraLogic 2G and ctrlX AUTOMATION programming systems document the same three categories. The Bosch Rexroth help states that a VAR_IN_OUT variable is an input/output variable, part of a POU interface, that serves as a formal pass-by-reference parameter. The IndraWorks implementation matches the IEC standard: VAR_IN_OUT must be connected to a writable operand and is read or written through a pointer at the call site. Rexroth's global variable lists support the RETAIN attribute separately, so a global VAR_GLOBAL RETAIN variable connected to a FB's VAR_IN_OUT pin will survive a power cycle as long as the MLC or ctrlX runtime is configured to retain that global list.
9. VAR_IN_OUT as the Correct Vehicle for "Persistent" State in a FB
The original question was whether VAR_OUT can be used to return a value that "persists" across calls. The answer is yes, but with a different scope than the question intended:
-
Persistence across calls, same FB instance, same task: any of the three mechanisms works -
VAR_OUTPUTwritten each cycle,VAR_IN_OUTmutated in place, or aVARstatic member of the FB instance. The value survives between calls because either the caller (InOut), the caller's destination (Out), or the instance DB (Static) stores it. -
Persistence across power cycle: requires backing storage. In TwinCAT, the variable must be
PERSISTENTand the controller must have a backup file mechanism. In Siemens, the instance DB must be marked retentive in the PLC properties. In Mitsubishi, the connected D- or W-register must be in the latch range (e.g. D200–D511 on FX5U) and the latch range must be enabled in PLC parameters. -
Persistence across download (online change) of the program: only true of
RETAIN/PERSISTENTvariables; ordinaryVARandVAR_OUTPUTvalues are reset on initialisation.
For state that the FB must own, such as a sequence step, an elapsed-time accumulator, or a debounce timer, the cleanest design is a VAR (static) inside the FB. The FB instance DB lives for as long as the program runs; the static value persists across calls; and marking the instance DB as retentive (Siemens), the FB instance as retain in the POUs folder (Codesys / Schneider), or adding the RETAIN qualifier to the VAR block (Beckhoff) extends the lifetime to power-cycle.
10. Practical Example: A Latch That the HMI Both Sets and Resets
The classic use case for VAR_IN_OUT is a bit that the FB both reads and writes. Consider an FB that latches a "Start" command from an HMI button and only resets it after a safety condition is satisfied:
FUNCTION_BLOCK FB_LatchedStart
VAR_INPUT
iSafetyOK : BOOL;
END_VAR
VAR_IN_OUT
ioRun : BOOL; // the HMI writes it, the FB reads and clears it
END_VAR
VAR
sLatched : BOOL;
END_VAR
IF ioRun AND iSafetyOK THEN
sLatched := TRUE;
ioRun := FALSE; // acknowledge the command so the HMI clears the button
END_IF;
The same effect can be obtained with a VAR_OUTPUT for the running bit and a VAR static for the latch, but the VAR_IN_OUT design keeps the data flow visible at the call site: the HMI's Start_PB tag is the same tag the FB reads and the same tag the FB clears, so a single network diagram shows the entire contract.
11. Practical Example: A Counter with VAR_IN_OUT
FUNCTION_BLOCK FB_Counter : FB_Base
VAR_INPUT
iEnable : BOOL;
iStep : INT := 1;
END_VAR
VAR_IN_OUT
ioCount : INT;
END_VAR
IF iEnable THEN
ioCount := ioCount + iStep; // read-then-write via the same reference
END_IF;
Calling FB_Counter(iEnable := bRun, iStep := 1, ioCount := iProductionCount); accumulates iProductionCount in place. If the developer accidentally moves iProductionCount to a VAR_INPUT parameter, the increment will appear in the FB body's local copy and will be discarded on return - the production count will stay at zero. This is the most common IEC 61131-3 bug in counter FBs.
12. Diagnostic Checklist When a FB "Loses" Its Value
| Symptom | Likely cause | Fix |
|---|---|---|
| Counter resets to zero on every cycle | Counter is a VAR_INPUT or a VAR_TEMP that is being written |
Move it to VAR_IN_OUT or to a retentive VAR
|
| Output never updates even though the FB body sets it | The VAR_OUTPUT is not connected at the call site |
Connect the output pin to a valid destination |
| Compile error "Constant cannot be passed to VAR_IN_OUT" | A literal is wired to an InOut pin | Create a VAR_GLOBAL or VAR of the right type and wire the variable |
| Value resets after power cycle | Instance DB / instance memory is not retentive | Mark the instance DB retentive (TIA) / add RETAIN qualifier (Codesys, IndraWorks) / enable latch range (Mitsubishi) / add UPS for PERSISTENT (TwinCAT) |
| Value is correct in the FB body but wrong at the call site | Output is connected to a tag that another POU overwrites in the same cycle | Move the destination to a tag that is written by the FB only |
| Two FBs of the same instance share state | FB was called with no instance DB (Siemens) or with a shared instance | Give the call its own instance DB; in Mitsubishi use the FB's instance label |
13. Platform-Specific Gotchas in One Place
| Platform | VAR_IN_OUT data type | Required operand | Retain attribute | Documentation |
|---|---|---|---|---|
| Siemens TIA Portal S7-1200 / S7-1500 | ANY pointer for elementary types, area-internal for STRUCT/ARRAY | Writable global / local tag (no constant) | Instance DB "retain" flag | S7-1200 / S7-1500 System Manual |
| Mitsubishi GX Works3 (iQ-R / FX5) | Pointer to D, W, L, or Z register area | Writable operand, not a constant | Latch range enable + RETAIN D/W device | GX Works3 FB manual |
| Beckhoff TwinCAT 3 | Pointer to PLC variable | Writable variable (not literal) | Add PERSISTENT / RETAIN to VAR or VAR_GLOBAL
|
TwinCAT 3 PLC Introduction - Persistent |
| Schneider Machine Expert (M241 / M251 / M580) | Pointer to IEC variable | Writable variable | RETAIN qualifier on VAR_GLOBAL or POUs instance |
Schneider Machine Expert Variable Types |
| Bosch Rexroth IndraWorks / ctrlX | Pointer to IndraLogic variable | Writable variable | RETAIN on VAR_GLOBAL; persistent storage configured at controller level |
IndraWorks VAR_IN_OUT |
14. Recommended Decision Flow for New FB Designs
- List every input the FB needs. Tag each as consumed (setpoint, mode, measurement) or mutated (counter, string buffer, state word).
- Declare the consumed items as
VAR_INPUTand the mutated items asVAR_IN_OUT. - List every output the FB produces. Declare them as
VAR_OUTPUTwith sensible defaults so that callers may omit them. - Decide which state the FB must own. Declare it in
VAR(static). If the state must survive a power cycle, add theRETAIN(Siemens / Codesys) orPERSISTENT(TwinCAT) qualifier and confirm the controller's storage support. - For a "persistent variable" that the question originally asked about, connect a retentive global tag to the
VAR_IN_OUTpin of the FB. The variable persists in the caller's memory; the FB simply mutates it; the storage survives because the underlying tag is retentive. - Verify on the bench with an online watch window that the value increments (or transitions) at the call boundary, not just inside the FB body.
ioCount : INT in the declaration tells the implementer nothing about whether the value will survive a download. A line in the function-block header that says "ioCount is a VAR_IN_OUT; the caller must supply a RETAIN global of type INT" removes the ambiguity.FAQ
What is the practical difference between VAR_INPUT and VAR_IN_OUT in IEC 61131-3?
VAR_INPUT is passed by value: the FB receives a copy and any write inside the FB is discarded on return. VAR_IN_OUT is passed by reference: the FB receives a pointer to the caller's variable, so reads and writes inside the FB affect the caller's storage directly. Use VAR_IN_OUT when the FB must mutate the caller's variable (counters, buffers, state words).
Why does my counter reset to zero on every scan when I use a VAR_OUTPUT?
A VAR_OUTPUT is a write-only copy-back parameter. If the value the FB produces is not assigned to a tag that the caller persists (a global variable, a retentive instance-DB tag, or the same VAR_IN_OUT), the caller's local copy overwrites it on the next call. For an accumulating counter, declare the count as a VAR_IN_OUT or as a retentive VAR (static) inside the FB instance.
Can I assign a constant or a literal to a VAR_IN_OUT pin?
No. The IEC 61131-3 standard requires a VAR_IN_OUT to be connected to a writable address, which a literal is not. The compiler in TIA Portal, GX Works, TwinCAT, Machine Expert, and IndraWorks all reject the call. Create a global or local variable of the correct type and connect that variable to the pin.
How do I make a FB variable survive a power cycle in TwinCAT 3?
Add the keyword PERSISTENT after the variable type in the declaration (for example, VAR PERSISTENT pValue : INT; END_VAR), or combine it with VAR_GLOBAL for cross-POU state. PERSISTENT alone covers a warm restart; for an uncontrolled power loss, also configure the controller's UPS or battery-backed SRAM so the runtime can flush the persistent image. See the Beckhoff TwinCAT 3 PLC Introduction page for the exact syntax.
How is a Mitsubishi FB call compiled at the machine-code level?
GX Works allocates scratch D-registers (and reserved M/B contacts) from the upper end of the data area to use as temporaries for VAR_INPUT and VAR_OUTPUT. On call, the input operands are copied into the scratch, the FB body runs against the scratch, and the output scratch is copied back to the caller's destination. VAR_IN_OUT is compiled as a pointer load; no scratch is used. Engineers must never read the scratch D-registers from HMI or from a non-IEC program because the values are only valid across the call boundary.