Skip to content

Compose FB and SSFC Components

This guide covers the practical steps for wiring child components — both FUNCTION_BLOCK and SSFC — into parent FUNCTION_BLOCK, SSFC, and PROGRAM containers. It assumes you already know how to write individual components; the focus here is on the connection patterns.


Wire an FB inside a FUNCTION_BLOCK or PROGRAM (WIRING block)

The canonical way to host child components is with a top-level WIRING … END_WIRING block. The WIRING block declares the structural graph of the POU: which child instances receive which parent inputs, and which parent outputs receive which child outputs.

FUNCTION_BLOCK TemperatureController
  VAR_INPUT
    measurement : MATHREAL;
    setpoint    : MATHREAL;
    enable      : BOOL;
  END_VAR
  VAR_OUTPUT
    output : MATHREAL;
  END_VAR
  VAR
    pid : PID(Kp := 2.0, Ki := 0.1);
  END_VAR

  WIRING
    pid(
      measurement := measurement,
      setpoint    := setpoint,
      enable      := enable
    );
    output := pid.output;
  END_WIRING
END_FUNCTION_BLOCK

The same pattern applies to PROGRAM:

PROGRAM Pasteurizer
  VAR_INPUT
    temperature : MATHREAL;
    setpoint    : MATHREAL;
    start       : BOOL;
    level_high  : BOOL;
  END_VAR
  VAR_OUTPUT
    heater_command : MATHREAL;
    valve_fill     : BOOL;
  END_VAR
  VAR
    sequence : BatchPasteurizer(HOLD_TIME := T#30s);
    pid      : TemperaturePID;
  END_VAR

  WIRING
    sequence(start := start, level_high := level_high, ...);
    pid(enable := sequence.heater, actual := temperature, ...);

    heater_command := pid.Output;
    valve_fill     := sequence.valve_fill;
  END_WIRING
END_PROGRAM

WIRING has two syntactic forms:

Form Syntax What it does
Child-input binding inst(Port := expr, ...); Wires expression values to child VAR_INPUT ports
Output forwarding output := source; Forwards a named VAR_OUTPUT to a child output or local

Output forwarding RHS must be a simple reference (local variable, instance output path, or VAR_INPUT passthrough) — not an arithmetic expression. Computed values belong in IMPLEMENTATION.

A PROGRAM that is a pure composition needs no IMPLEMENTATION block at all.

Key points:

  • PARAMETER values (Kp, Ki) are bound at declaration time in VAR, not in WIRING.
  • Every VAR_INPUT port of the child must be wired. Omitting a port is a static error.
  • Output forwarding entries make the output visible in the schema and in tooling; they are equivalent to a named alias.
  • Read child output ports via dot notation: pid.output, sequence.valve_fill.

Common mistakes at this step:

  • Binding a PARAMETER in WIRING instead of in the VAR declaration — PARAMETER is not a wiring target.
  • Placing computed logic (IF … THEN …, arithmetic) in WIRING — move it to IMPLEMENTATION.
  • Omitting the output forwarding entries for VAR_OUTPUT — the schema will be missing outputs.

Wire an FB inside an SSFC

A child FUNCTION_BLOCK is wired in one WIRING block at the SSFC's own level — not inside a state. WIRING is declarative: the binding holds in every scan. What a state controls is the value wired to the child, not whether the wiring exists.

State-dependent values come from the sequence itself. Give the SSFC an output that says what it wants, set it in the states, and wire that output to the child:

SSFC PasteurizerSequence
  VAR_INPUT
    abort : BOOL;
  END_VAR
  VAR_OUTPUT
    heat_on : BOOL;      (* what the sequence WANTS, not how it is achieved *)
  END_VAR

  INITIAL STATE Idle
    EN: heat_on := FALSE;
    TRANSITION TO Heating WHEN NOT abort;
  END_STATE

  STATE Heating
    EN: heat_on := TRUE;
    EX: heat_on := FALSE;
    TRANSITION TO Idle WHEN abort;
  END_STATE
END_SSFC

The composition root wires the two together:

PROGRAM Plant
VAR_INPUT
    temp   : INT;
    target : INT;
    abort  : BOOL;
END_VAR
VAR_OUTPUT
    heater_command : INT;
END_VAR
VAR
    seq : PasteurizerSequence;
    pid : PID;
END_VAR
WIRING
    seq(abort := abort);
    pid(
      measurement := temp,
      setpoint    := target,
      enable      := seq.heat_on
    );
    heater_command := pid.output;
END_WIRING
END_PROGRAM

Key points:

  • The SSFC says what it wants (heat_on), the composition root says how it is delivered (pid.enable). That split is what lets either side change without touching the other.
  • The pid instance runs in every scan. enable := seq.heat_on controls the PID's internal enable logic; it does not gate whether pid executes.
  • heater_command := pid.output reads an output port directly. Wiring only covers VAR_INPUT ports.
  • A state's activity marker Heating.X is readable, but prefer a named output. X couples the composition to the sequence's internal state names; an output is a promise the sequence makes, and renaming a state then costs nothing.
  • Identifiers are case-insensitive, so an output heating and a state Heating collide. The compiler says so, at the declaration.

Common mistakes at this step:

  • Putting WIRING inside a STATE. There is no per-state wiring: the parser accepts WIRING at block level only. A child that must behave differently per state is wired once, to a value the states set.
  • Placing the child call in a state action (EN/DU/EX) instead of in WIRING. Action blocks assign to the SSFC's own variables; they do not wire child instances.

Compose an SSFC inside an SSFC

Declare the child in VAR and wire its input ports in the parent's one WIRING block — the same rule as for a FUNCTION_BLOCK. Elapsed time is runtime-owned: the child SSFC tracks scan duration from the runtime clock independently, and no timestep is forwarded.

A child that should only run in part of the sequence is told so through a value, not through where the wiring sits:

SSFC OuterSequence
  VAR_INPUT
    start : BOOL;
    abort : BOOL;
  END_VAR
  VAR_OUTPUT
    done : BOOL;
  END_VAR
  VAR
    inner : InnerSequence;
  END_VAR

  WIRING
    inner(
      start := Running.X,     (* active only while Running *)
      abort := abort
    );
  END_WIRING

  INITIAL STATE Idle
    TRANSITION TO Running WHEN start;
  END_STATE

  STATE Running
    DU: done := inner.done;
    TRANSITION TO Idle WHEN inner.done;
  END_STATE
END_SSFC

Star-wiring for same-name passthrough

To forward parent ports to a child without writing out each binding, use inst* (star-wiring). This is shorthand for wiring every VAR_INPUT port of the child that has a matching name and compatible type in the parent scope.

VAR
  valve : ValveController;
END_VAR

WIRING
  valve*;   (* forwards temp, pressure, enable from parent to valve
               if valve has VAR_INPUT ports named temp, pressure, enable *)
END_WIRING

Rules:

  • Only ports where the name and type match exactly are wired by *. A name match with a type mismatch is a static error.
  • Ports with a name mismatch are not wired — they are silently skipped by * and must be wired explicitly alongside the star.
  • inst* and explicit port bindings can appear in the same WIRING block. Explicit bindings take precedence over any port * would have matched.

When to use star-wiring vs. explicit wiring:

Use inst* for boilerplate passthrough — parent and child were designed to match. Use explicit wiring when the mapping is non-trivial, the names differ, or the binding expression needs a transformation (e.g., enable := Heating.X).

Common mistakes at this step:

  • Assuming * wires output ports back to the parent. Star-wiring only covers VAR_INPUT on the child.
  • Using * when the parent has renamed a port (e.g., parent has temperature, child expects temp). In that case the port is not matched and must be wired explicitly.

Diagnosing common errors

Error Cause Fix
Unmatched port A VAR_INPUT port of the child is not wired at the call site or WIRING block Wire the missing port explicitly
Type mismatch A wired expression has the wrong type for the port Check the port type in the child's VAR_INPUT declaration
Missing WIRING in some states The same child instance is wired in one state's WIRING block but absent from another reachable state Add a WIRING block to the other states, or move the instance to a top-level WIRING block if the binding is state-independent
Expression in output forwarding Output forwarding RHS is not a simple reference Move the computation to IMPLEMENTATION and forward the derived variable
PARAMETER wired at call site A PARAMETER port appears in the WIRING block or FB call Move the binding to the VAR declaration: inst : ChildFB(Kp := 2.0)

See also

  • Why FB and SSFC are equal-rank components: concepts/component-model.md
  • Normative WIRING specification: reference/language.md §10
  • PROGRAM root POU: reference/language.md §3.6
  • Normative WIRING/IMPLEMENTATION separation: docs/adr-pg12-wiring-composition.md