A function block is worthwhile when it owns a promise you can test in isolation. Give a motor block a request, readiness and observed running feedback. Ask it to report whether it accepted the request and whether expected feedback arrived in time. Keep carton ownership in the sequence: a motor block cannot establish product arrival from drive feedback alone. That separation makes both pieces easier to explain.
A block should own a small promise
You have written logic for one extraction fan. Now the drawing shows eight. Copying the routine eight times feels quick, until somebody changes the start timeout in seven copies and forgets the eighth. Reuse solves that problem only if the reused block has a clear job. A large block with twenty mode switches can be harder to trust than eight simple routines.
Start with the promise: when requested and permitted, command this fan; if running feedback does not arrive within the allowed time, report a failed start. This is an ordinary equipment-control promise. It does not establish a safety function or prove that airflow exists. A contactor auxiliary contact, a drive running bit and an airflow switch describe different evidence. Choose the feedback that matches the requirement.
The machine sequence decides when extraction is needed. The fan block manages this particular fan. The physical output mapping writes the terminal. Giving each decision one owner makes the program easier to explain before it makes the program shorter.
Calculation or memory?
A function is a good home for a calculation such as converting a measured volume into an estimated fill percentage. Design it so the result depends on its explicit inputs, with no hidden changes elsewhere. You can test that calculation with a table of numbers.
A function block is the natural home for behaviour that remembers: a timer, a running request accepted earlier, a previous input used for edge detection, or a latched fault. Each instance owns its own working memory. ExtractFan and CoolingFan can use the same block definition while keeping different timers and fault histories. Beckhoff's documentation distinguishes function calls from function-block instances and their stored state; the exact declaration and visibility rules belong to the target environment. Verify the programming model.
Do not infer persistence through power loss from the word “memory.” State across cyclic calls and retained state across restart are separate design choices. We will make that distinction explicit in Types, arrays and data.
Write the interface in ordinary language
Before writing ST, describe these signals:
| Signal | Meaning |
|---|---|
| RequestRun | The equipment owner currently wants this fan running |
| PermitRun | Ordinary process conditions allow that request |
| RunningFeedback | The selected feedback currently reports running |
| ResetPulse | One deliberate request to clear an inactive fault |
| StartLimit | Maximum time allowed to establish running feedback |
| RunCommand | This block's requested output state |
| StartFailed | A remembered unsuccessful start |
Notice what is absent: no references to “Tank 3,” no direct HMI button addresses, and no writes to a global mode. Those belong outside this reusable promise. Configuration such as StartLimit must be validated before use; a negative or unsupported time value must not silently turn into an acceptable setting.
A small block, including its limits
This is a complete illustrative block definition using an IEC-style TON. It assumes ResetPulse has already been made into a one-cycle event. Library names and declaration editors vary by vendor. Integrating it requires I/O mapping, configuration validation, task setup and the machine's separate protective systems.
FUNCTION_BLOCK FB_FanStart
VAR_INPUT
RequestRun : BOOL;
PermitRun : BOOL;
RunningFeedback : BOOL;
ResetPulse : BOOL;
StartLimit : TIME := T#3s;
END_VAR
VAR_OUTPUT
RunCommand : BOOL;
StartFailed : BOOL;
END_VAR
VAR
AwaitRunning : TON;
END_VAR
IF ResetPulse AND NOT RequestRun THEN
StartFailed := FALSE;
END_IF;
RunCommand := RequestRun AND PermitRun AND NOT StartFailed;
AwaitRunning(
IN := RunCommand AND NOT RunningFeedback,
PT := StartLimit
);
IF AwaitRunning.Q THEN
StartFailed := TRUE;
RunCommand := FALSE;
END_IF;
END_FUNCTION_BLOCK
The timer is called every scan. If the run command disappears, its input becomes false and its nonretentive timing resets. Calling the timer only inside an IF RunCommand branch would hide the reset calls and make its state harder to reason about.
The final assignment removes the command in the same scan that detects timeout. Without it, the earlier expression would leave RunCommand true until the next call. Neither order is magic; they produce different traces, so select deliberately.
This small block also detects feedback loss after a successful start using the same delay. That may be an unsuitable requirement. If startup gets three seconds but running feedback loss gets half a second, use two clearly named monitors. A block is correct against its contract, not because its name sounds professional.
Follow one instance through time
At time zero, request and permission are true. Feedback is false, so the command turns on and the timer starts. At 0.7 seconds, feedback arrives. The timer input becomes false and the timer resets. At 5 seconds, the request disappears. The command turns off immediately; there is no coast-down verification in this block.
Now remove feedback permanently. At the first call when elapsed time reaches the timer preset, the fault latches and the command turns off. Leaving RequestRun true while pressing Reset does nothing. The operator must first remove the request. This avoids reset behaving as a fresh start in this particular contract. Other machines need other restart procedures, which must be designed explicitly.
Give the second fan a separate instance. Calling one instance for fan A and then again for fan B in the same scan does not create two fans. It reuses one timer and one fault memory twice. The resulting behaviour depends on call order and can be surprisingly convincing during a short test.
When abstraction starts to hurt
A block should expose useful status, not every internal variable. Exposing StartFailed helps the HMI explain a stopped fan. Allowing the HMI to write the timer's elapsed time lets another component quietly alter the block's memory.
Keep process ownership outside: which fan is duty, whether a filter needs cleaning, and whether the chamber may start production. Add a new parameter only when two real applications need different behaviour. Do not add an option merely because a future machine might exist.
Ladder uses the same idea. A vendor's reusable instruction or function-block call can encapsulate the timer and latch. Drawing contacts does not remove the need for independent instances, documented reset rules or a single output owner.
Try it
Two pumps share the block idea. Pump A must prove flow within four seconds; pump B within six. A colleague declares one instance named PumpMonitor, calls it twice, and connects its fault output to two alarms. Explain the defect. Then specify a test that demonstrates independence without waiting for every possible process condition.
Work through the answer
The block definition can be shared; the instance cannot. Declare PumpAMonitor and PumpBMonitor, each with its own timer and latch. Give each instance its own request, permission, feedback and configured delay. Map each command once.
Request both pumps. Provide A's flow feedback after one second but keep B's absent. A must remain healthy while B faults after its six-second allowance. Remove B's request and reset B. A's command, elapsed timing and status must remain unchanged. Finally reverse the roles to expose a wiring copy error. This test checks ownership, timing and reset isolation together, rather than merely proving that both outputs can turn on.
Record the block's version and that behaviour table with its tests. Reuse becomes valuable when a later programmer can understand exactly what is being reused.