Feel the difference first
No definitions yet. Three experiments, three instincts: a FUNCTION forgets, a FUNCTION_BLOCK remembers, and OOP earns its keep only when variants multiply. Everything else in this workshop hangs off what you feel in the next five minutes.
① The amnesia experiment
A conveyor must alarm if the photo-eye stays blocked for 3 seconds. Below, the same jam check is written both ways (the FB version also resets its timer when the eye clears). Jam the belt and watch which one can actually count.
scan #14rule: alarm if blocked > 3 s
𝑓 written as a FUNCTION
1FUNCTION F_JamCheck : BOOL
2VAR
3 tBlocked : REAL; // local — REBORN AT 0 EVERY CALL
4END_VAR
5IF bEyeBlocked THEN
6 tBlocked := tBlocked + 0.1;
7END_IF
8F_JamCheck := tBlocked > 3.0;
tBlocked » 0.0 s (then forgotten)
alarm » FALSE, forever
alarm » FALSE, forever
🧱 written as a FUNCTION_BLOCK
1FUNCTION_BLOCK FB_JamCheck
2VAR
3 tBlocked : REAL; // INSTANCE memory — survives the scan
4END_VAR
5IF bEyeBlocked THEN
6 tBlocked := tBlocked + 0.1;
7ELSE
8 tBlocked := 0.0;
9END_IF
10bJam := tBlocked > 3.0;
tBlocked » 0.0 s
alarm » FALSE
alarm » FALSE
🧠
Nearly the same logic: only the FB has an ELSE that resets the timer, and the FUNCTION would gain nothing from one. The FUNCTION's
tBlocked is born at zero on every single scan: it adds 0.1 once and then dies, so it can never measure time. The FUNCTION_BLOCK's variable lives in its instance and keeps accumulating, scan after scan. That one difference, amnesia versus memory, is what the F-vs-FB decision comes down to.② The instance factory
An FB is more than "an F with memory". It is a blueprint you stamp copies from, so one FB gives four motors four independent lives:
1FUNCTION_BLOCK FB_Motor
2VAR_INPUT bStart, bStop : BOOL; END_VAR
3VAR_OUTPUT bRunning : BOOL;
4 rHours : REAL; // each instance keeps its OWN hours
5END_VAR
1VAR
2 mConveyor : FB_Motor;
3 mMixer : FB_Motor;
4 mPump1 : FB_Motor;
5 mPump2 : FB_Motor;
6END_VAR
7mConveyor(bStart := …); mMixer(bStart := …);
8mPump1(bStart := …); mPump2(bStart := …);
⚙️
mConveyor
124.600 h
STOPPED⚙️
mMixer
87.100 h
STOPPED⚙️
mPump1
301.900 h
STOPPED⚙️
mPump2
12.300 h
STOPPED🏭
The FB is a blueprint; each declaration stamps out an instance with its own private memory. Start
mPump1 and watch only ITS hours climb. You get four motors with four separate states and one piece of code to test and maintain. That is why plants standardise on FB libraries: fix a bug once and every motor in the factory gets the fix.③ The step-up: when OOP starts paying rent
FBs scale beautifully until the same kind of thing comes in many variants. Drag the variant count up, feel the IF-tree rot, and then refactor it.
sensor variants the machine must support:3
1(* every caller repeats this dance: *)
2IF eSensorType = ANALOG_420 THEN
3 rLevel := F_Scale(iRaw, 0.0, 100.0);
4 bValid := rLoopmA > 3.6;
5ELSIF eSensorType = IOLINK THEN
6 rLevel := fbIoLink.rValue;
7 bValid := fbIoLink.bOk;
8ELSIF eSensorType = SIMULATED THEN
9 rLevel := fbSim.rValue; bValid := TRUE;
10END_IF
caller code size » 26 lines
touched on every new variant » EVERY caller, EVERY time
touched on every new variant » EVERY caller, EVERY time
supported variants » analog 4-20 mA · IO-Link digital · simulated (FAT test)
🧬
OOP shows up when a family of variants makes plain FBs painful, and being fancy has nothing to do with it. The interface freezes the conversation ("
Read(), IsValid()") so callers and variants stop knowing about each other. Two variants? The IF-tree is honestly fine. Five and growing? The interface pays rent.