POU model — PROGRAM, FUNCTION_BLOCK, FUNCTION

The three POU kinds

KindHas state?Has return value?Called viaStored where
PROGRAMYes (per program instance)NoTask schedulerResources
FUNCTION_BLOCKYes (per FB instance)NoInstance variableAnywhere a variable can live
FUNCTIONNoYes (single typed return)Direct callNowhere — call is pure

PROGRAM

The top-level executable unit. A PROGRAM is what a TASK actually runs — it’s the IEC equivalent of a “main” function, except a configuration can have many PROGRAM instances each assigned to a different task.

PROGRAM PLC_PRG
    VAR
        iCycleCount : INT := 0;
        tonStartup : TON;
    END_VAR

    iCycleCount := iCycleCount + 1;
    tonStartup(IN := TRUE, PT := T#5s);

    IF tonStartup.Q THEN
        DoMainWork();
    END_IF;
END_PROGRAM

A program’s VAR block persists across cycles like an FB’s, but unlike FBs, a PROGRAM is instantiated only via the CONFIGURATION / RESOURCE / TASK mechanism (see configuration) — you don’t myProgInst : PLC_PRG; in another POU’s VAR block.

FUNCTION_BLOCK

The stateful, reusable workhorse. An FB has:

  • VAR_INPUT — read-only inputs.
  • VAR_OUTPUT — outputs (read by the caller via the instance).
  • VAR_IN_OUT — pass-by-reference parameters.
  • VAR — internal state that persists between calls per instance.
  • VAR_TEMP — scratch that resets at each call.
FUNCTION_BLOCK FB_Counter
    VAR_INPUT
        xCU : BOOL;
        iPV : INT;
    END_VAR
    VAR_OUTPUT
        xQ : BOOL;
        iCV : INT;
    END_VAR
    VAR
        xPrevCU : BOOL;     (* edge-detector state *)
    END_VAR

    IF xCU AND NOT xPrevCU THEN
        iCV := iCV + 1;
    END_IF;
    xPrevCU := xCU;
    xQ := iCV >= iPV;
END_FUNCTION_BLOCK

The caller declares an instance and calls the instance:

VAR
    myCount : FB_Counter;     (* one persistent struct *)
END_VAR

myCount(xCU := xButton, iPV := 10);
IF myCount.xQ THEN ... END_IF;

See textual/structured-text/function-calls/fb-instance-vs-function for the full story.

FUNCTION

A pure mapping from inputs to one typed return value. No internal state. Same call with same inputs always returns the same value.

FUNCTION FC_Clamp : INT
    VAR_INPUT
        iValue : INT;
        iMin : INT;
        iMax : INT;
    END_VAR
    (* No VAR block allowed — see llm_signals *)
    VAR_TEMP
        iWork : INT;     (* this IS allowed; resets each call *)
    END_VAR

    iWork := iValue;
    IF iWork < iMin THEN iWork := iMin; END_IF;
    IF iWork > iMax THEN iWork := iMax; END_IF;
    FC_Clamp := iWork;        (* assign to function name = return value *)
END_FUNCTION

The return value is set by assigning to the function name, not via a RETURN statement. RETURN exits the function but doesn’t carry a value — the most-recent assignment to <FunctionName> is what the caller receives.

Calling a FUNCTION needs no instance — see function-calls/_index.

When to pick which

SituationPOU kind
Task that runs every cyclePROGRAM
Reusable building block with state (timer, counter, edge detector, sequencer)FUNCTION_BLOCK
Pure transformation: one or more inputs → one output, no memoryFUNCTION
Need persistent counter / accumulator inside reusable codeFUNCTION_BLOCK (a FUNCTION can’t keep state)

ForgeIEC POU types

ForgeIEC extends the IEC POU set with three “VarList” POUs that don’t have executable bodies:

  • GlobalVarList (GVL) — IEC VAR_GLOBAL block, exposed as a POU for editor convenience.
  • AnvilVarList — auto-generated per-bus-device variable list. See textual/structured-text/variables-references.
  • TempVarList — project-local scratchpad GVL.

These show up in the project tree alongside PROGRAM/FB/FUN POUs but contribute only declarations, no executable code.

IEC reference

IEC 61131-3 third edition (2013), clause 6.6.2 — “Function and function block declarations” plus clause 6.6.6 for PROGRAM.

matiec conformance

All three POU kinds implemented per the standard. The “no VAR in FUNCTION” rule is matiec’s most common rejection point for code ported from other vendor toolchains (which sometimes accept state in functions).