Linked Units
An application is rarely made of a single Unit. As soon as there is more than one, some units usually have to call into others: a Module unit that orchestrates a whole line has to start the Loading unit, wait for Handling to place a part, and run a Treatment station - a Handling unit that needs to move something into a Treatment station has to align, lock and eventually halt that station itself.
A linked unit is nothing more than a reference to another unit, addressed through that unit's own interface (i.e. IHandling, ITreatmentStation, ...) instead of its concrete implementation. This keeps a unit decoupled from the concrete units it depends on - a sequence only ever sees the interface it needs - while still giving it full access to the standard IUnit methods (RunAutomaticAsync, HaltAsync, HomingAsync, StopAsync, TryLock, IsLocked, ...) and whatever additional, application-specific methods that unit's interface exposes.
Note
Module is used throughout this page purely because it is a good example of a unit that links many others. The pattern is completely generic - any unit may declare linked units, including units that are themselves linked to by other units. There is no restriction on how deep or in which direction units may reference each other, application design determines this, not the framework.
Wiring linked units
Linked units are grouped in a dedicated container function block, following the naming convention <UnitName>LinkedUnit. Like every other piece of Equipment, it lives in the unit's _Equipment folder, extends the unit's own container base class (which in turn extends ObjectContainer) and is exposed as the Unit member of <UnitName>Equipment:
FUNCTION_BLOCK ModuleEquipment EXTENDS ModuleContainer
VAR_INPUT
Io : ModuleIo(_parent);
Actuator : ModuleActuator(_parent);
Axis : ModuleAxis(_parent);
Fieldbus : ModuleFieldbus(_parent);
Unit : ModuleLinkedUnit(_parent); // < container for all units that Module talks to
END_VAR
ModuleLinkedUnit itself simply lists one member per unit that should be reachable, typed with that unit's own interface, and assigns it in its implementation from the corresponding global instance (see Application Structure for where units are instantiated, e.g. in ZModuleProgram):
/// This function block is a container for all linked units that are used by this unit.
/// Linked units can be addressed through their individual interface.
FUNCTION_BLOCK ModuleLinkedUnit EXTENDS ModuleContainer
VAR_INPUT
Handling : IHandling;
Loading : ILoading;
Unloading : IUnloading;
Treatment : ARRAY[0..(ModuleTreatmentStation.Count - 1)] OF ITreatmentStation;
{attribute '__ZwPlcUnitEquipmentDeclaration__'} // used by Zeugwerk Creator for code generation, do not remove
END_VAR
IF _isInitialized THEN
RETURN;
END_IF
Handling := ZModuleProgram.Handling;
Loading := ZModuleProgram.Loading;
Unloading := ZModuleProgram.Unloading;
FOR _i := 0 TO (ModuleTreatmentStation.Count - 1)
DO
CASE _i OF
ModuleTreatmentStation.Station1: Treatment[_i] := ZModuleProgram.TreatmentStation1;
ModuleTreatmentStation.Station2: Treatment[_i] := ZModuleProgram.TreatmentStation2;
END_CASE
END_FOR
{attribute '__ZwPlcUnitEquipmentImplementation__'} // used by Zeugwerk Creator for code generation, do not remove
_isInitialized := TRUE;
Zeugwerk Creator scaffolds an (empty) <UnitName>LinkedUnit for every unit by default - see, for instance, QuickstartLinkedUnit in the Quickstart Tutorial. Filling it in with the units your application actually needs is a manual, application-specific step, just like adding a new userdefined state.
Using a linked unit from a sequence
A unit's base Sequence function block (i.e. ModuleSequence) holds a reference to the linked unit container, wired up in FB_init alongside the other equipment references:
FUNCTION_BLOCK ModuleSequence EXTENDS ZApplication.Sequence
VAR
{attribute 'hide'}
_parent : REFERENCE TO ModuleUnit;
_io : REFERENCE TO ModuleIo;
_unit : REFERENCE TO ModuleLinkedUnit;
END_VAR
METHOD FB_init : BOOL
VAR_INPUT
bInitRetains : BOOL;
bInCopyCode : BOOL;
unit : REFERENCE TO ModuleUnit;
END_VAR
_parent REF= unit;
_io REF= unit._equipment.Io;
_unit REF= unit._equipment.Unit; // < wires up the linked unit container
Every state sequence of Module can now simply call into a linked unit like any other object, using the same Async-suffixed methods described in Userdefined States:
ModuleStep.AutomaticStartTreatment:
IF _step.OnEntry() THEN
_unit.Treatment[_station].RunAutomaticAsync(startToken:=THIS^, unit:=0);
END_IF
IF Halting THEN
_step.SetNext(ModuleStep.AutomaticMoveToHomePosition);
ELSIF _unit.Treatment[_station].Error THEN
Abort(_unit.Treatment[_station].ErrorMessage());
ELSIF NOT _unit.Treatment[_station].Busy THEN
_step.SetNext(ModuleStep.AutomaticNext);
END_IF
The unit parameter of every Async method (see IUnit) is the calling unit, usually _parent or, as above, 0 if locking is not required for this particular call. It is used by TryLock internally to make sure that two units are not accidentally trying to control the same linked unit at once - passing _parent claims the lock for the duration of the call, so that, for example, another unit that also links to the same Treatment station cannot start it concurrently.
Halting a linked unit
As explained in Halt, passing the calling sequence itself (THIS^) as the startToken of an Async call automatically informs the called object about a halt request - a linked unit is no exception. If Module's sequence starts a Treatment station with startToken:=THIS^ and Module is then halted, the treatment station's active sequence becomes aware of the halt request too and may react on its own by setting Halting := TRUE and, if it configured one, resuming later from its own Milestone - exactly as described for a single unit.
Being informed does not mean a linked unit is forced to stop immediately - just like within a single unit, the receiving sequence decides itself whether and when it checks Halting. This is useful, because a linked unit sometimes has to keep running for a well-defined reason even while the caller wants to halt - for instance, a treatment step that must run for its full, fixed duration before it is safe to leave a station. On top of the automatic propagation, HaltAsync can be called explicitly at exactly the point in time the caller decides it is safe, which additionally drives the linked unit's own UnitStateMachine into its dedicated Halt state:
ModuleStep.TreatCarryOutTreatment:
IF _step.OnEntry() THEN
_unit.Treatment[_station].RunAutomaticAsync(startToken:=THIS^, unit:=_parent);
_timer.WaitAsync(duration:=_treatmentTime);
END_IF
Await(_timer, ModuleStep.TreatHaltUnit);
ModuleStep.TreatHaltUnit:
IF _step.OnEntry() THEN
_unit.Treatment[_station].HaltAsync(startToken:=THIS^, unit:=_parent);
END_IF
Await(_unit.Treatment[_station], ModuleStep.TreatLeaveUnit);
Here, Module's own Halting flag may already be TRUE while it is still waiting for the treatment timer, but the treatment station's automatic sequence simply does not check Halting while carrying out the treatment, so it keeps running for the full, fixed _treatmentTime regardless. Only once the timer has elapsed does Module explicitly halt the linked unit and wait (Await) for it to acknowledge the halt before leaving the station.
When to use linked units
Reach for a linked unit whenever a sequence needs to call another unit that is not one of its own equipment objects. A few rules of thumb:
- Keep the linked unit typed to the smallest interface that is actually needed (
IHandling,ILoading, ...) rather than a concrete unit type - this is what keeps units testable and exchangeable in isolation. - Any unit may both be a linked unit of others and have linked units of its own - there is no requirement to arrange units in a strict hierarchy, although most applications end up with one or two orchestrating units (often named
Module) at the top that link to most others.