Shared Equipment
Equipment objects (I/O, actuators, axes, fieldbus slaves, ...) usually belong to exactly one unit and are declared inside that unit's _Equipment folder. Sometimes, however, a piece of equipment or a service is genuinely shared by several units, for example a single EtherCAT bus master, a temperature sensor whose reading several units care about, or a mutex protecting a resource that multiple units access. This page describes how that sharing is modeled in a Zeugwerk Application.
The pattern: declare once in ZModuleProgram, inject into every unit that needs it
Instead of duplicating the object per unit, a shared object is declared exactly once, directly in ZModuleProgram, and then handed to the units that need it, either as a constructor parameter or through a dedicated setter method on the unit's interface.
EthercatMaster and EthercatSyncUnit
EthercatMaster observes and controls a single bus master instance (device): it scans slave devices during booting, monitors the device state while idle, performs periodic synchronization, and accepts external state requests. EthercatSyncUnit does the same for a group of slaves that belong to one EtherCAT sync unit, giving access to WcState, slave count, and fault statistics. Since a physical bus master is a single resource, both are typically instantiated once in ZModuleProgram (or in an infrastructure/orchestrator unit, see Multi-Unit Applications) and passed by reference to whichever units need to react to bus state.
Mutex
Mutex implements a simple locking mechanism for two related purposes:
- Mutual exclusion across PLC tasks, e.g. thread-safe access to a shared resource such as a logger.
- Ownership of an object across multiple cycles, e.g. making sure only one caller can move an axis at a time.
A Mutex shared between units is declared once and injected wherever TryLock/Unlock is needed to serialize access to the underlying resource.
Example: one temperature slave, several units
In a real multi-unit application (Ploetzeneder CPT4), a single EL3208 EtherCAT temperature slave is physically wired once, but its readings are needed by three different units (two rinsing baths and an infrastructure unit). The slave is declared once in the equipment of an orchestrating Module unit:
// ModuleFieldbus.TcPOU
EL3208ECSlave : ZEquipment.EthercatSlave(_parent, initialState := ZCore.EthercatSlaveState.Undefined);
and then injected into every unit that needs it through a dedicated interface method, called once during boot:
// ModuleSequenceBoot.TcPOU
_unit.DeminWaterRinsingBath.SetWaterTemperatureECSlave(_fieldbus.EL3208ECSlave);
_unit.Infrastructure.SetTemperatureECSlave(_fieldbus.EL3208ECSlave);
_unit.RoWaterRinsingBath.SetWaterTemperatureECSlave(_fieldbus.EL3208ECSlave);
Each receiving unit exposes a small UnitInterface method that simply forwards the slave reference to its own (otherwise perfectly normal) I/O object:
METHOD SetWaterTemperatureECSlave
VAR_INPUT
ecSlave : REFERENCE TO ZEquipment.EthercatSlave;
END_VAR
_equipment.Io.WaterTemperature.SetEthercatSlave(ecSlave);
This way, each of the three units still owns and reads its own analog input object as usual; only the underlying EtherCAT slave (the physical resource) is shared. _unit here refers to that unit's linked unit container, since sharing equipment across units builds on the same cross-unit references used for linked units in general.
Safety
Unlike Logging, Alarming, or EtherCAT handling, there is no dedicated Safety function block in the framework. Safety-relevant behavior (emergency stop buttons, door interlocks, safety PLC handshakes, ...) is application-specific and is built from ordinary digital I/O equipment plus regular sequence logic; it is only "shared" in the sense that it is usually evaluated once and then queried by every unit whose sequences must not run while a safety condition is violated.
A common way to structure this is to wire the safety-related I/O into one orchestrating unit and expose a gating check on its interface, which other units and sequences call before starting or continuing motion. For example, in Ploetzeneder CPT4, the Module unit wires several digital inputs (E-stop buttons, door interlocks) and periodically evaluates them:
// ModuleUnit.TcPOU, called every cycle
ObserveSafety();
and exposes the current safety status through an interface method that other units can query:
METHOD SafetyElementOk : BOOL
VAR_INPUT
element : ModuleSafetyElement;
END_VAR
SafetyElementOk := TRUE;
IF ((element = ModuleSafetyElement.All) OR_ELSE (element = ModuleSafetyElement.GeneralStop)) AND_THEN
NOT _equipment.Io.SafetyGeneralStopOk.Enabled OR_ELSE
_equipment.Unit.Loading.SafetyDoorInterlockEnabled OR_ELSE
_equipment.Unit.Unloading.SafetyDoorInterlockEnabled
THEN
SafetyElementOk := FALSE;
END_IF
IF ((element = ModuleSafetyElement.All) OR_ELSE (element = ModuleSafetyElement.EmergencyStop)) AND_THEN
NOT _equipment.Io.SafetyEmergencyStopOk.Enabled
THEN
SafetyElementOk := FALSE;
END_IF
A sequence that must not start while safety is violated then simply checks this before running, and can halt or stop the unit when a safety condition drops out while running:
IF NOT _parent.SafetyElementOk(element := ModuleSafetyElement.All) THEN
Abort('Safety check failed, automatic sequence can not be started!');
END_IF
This is a convention, not a framework requirement: the exact shape of ModuleSafetyElement, which unit owns the safety I/O, and how many gating checks exist are all specific to the application.