The machine
This chapter builds one complete controller: a single-head filling station on a bottling line. Bottles arrive on a conveyor and stop under the nozzle. A pneumatic neck clamp holds each one on a load cell, a fill valve opens, product runs in until the scale reads the target weight, the weight settles, the bottle is judged good or reject, the clamp opens and the conveyor carries it away.
It is a small machine, but it has everything a real one has: modes, a sequence, time, quantities, quality decisions, a fault that can happen every shift, and an operator who has to acknowledge it. Every chapter of this course has given you one of the pieces. Here you assemble them, one stage at a time, with tests at each stage — the way you would on a real project.
The specification, as the customer's process engineer would hand it to you:
| Item | Value |
|---|---|
| Product | 500 g fill, accepted between 495 g and 515 g |
| Clamp | Pneumatic, 300 ms to close (no feedback switch on this machine) |
| Fill | Single-speed valve; a normal fill takes about 2 s |
| Settle | 500 ms after the valve closes before the weight is judged |
| Fill timeout | 3 s: if the target is not reached, the supply has failed |
| Modes | Off, Manual (jog the clamp and the valve), Auto |
And the I/O list:
| Signal | Direction | Meaning |
|---|---|---|
SelManual, SelAuto | Input | Two contacts of a three-position key switch |
EStopOK | Input | Status from the safety relay |
BottleInPlace | Input | Photo-eye under the nozzle |
JogClamp, JogFill | Input | Manual push buttons |
Ack | Input | Alarm acknowledge |
Weight_g | Input | Load cell, already scaled to grams |
ClampValve, FillValve | Output | Solenoid valves |
On the real machine Weight_g comes from a weighing terminal — a Siemens SIWAREX module, a Beckhoff EL3356 load-cell terminal, a Mettler Toledo transmitter over PROFINET or EtherNet/IP. On these pages it comes from a plant model, exactly as chapter 23 showed.
Stage 1: modes with an ENUM
Start with the mode, because everything else depends on it. Three positions, so an ENUM (chapter 15) says exactly what is meant, and a watch table shows Manual instead of 1.
A three-position key switch gives two contacts. Neither closed is Off. One closed is Manual or Auto. Both closed cannot happen on a healthy switch — so if it does, the switch is broken, and the safe interpretation is Off. And whatever the switch says, no healthy safety circuit means Off.
TYPE E_Mode :
(
Off,
Manual,
Auto
);
END_TYPE
PROGRAM Main
VAR
SelManual : BOOL; (* input: key switch, manual contact *)
SelAuto : BOOL; (* input: key switch, auto contact *)
EStopOK : BOOL := TRUE; (* input: safety relay healthy *)
JogClamp : BOOL; (* input: manual push button *)
JogFill : BOOL; (* input: manual push button *)
Mode : E_Mode;
ClampValve : BOOL; (* output *)
FillValve : BOOL; (* output *)
END_VAR
(* Decide the mode: anything doubtful is Off *)
IF NOT EStopOK OR (SelManual AND SelAuto) THEN
Mode := Off;
ELSIF SelAuto THEN
Mode := Auto;
ELSIF SelManual THEN
Mode := Manual;
ELSE
Mode := Off;
END_IF;
(* Command: manual jogs only; Auto comes in stage 3 *)
ClampValve := Mode = Manual AND JogClamp;
FillValve := Mode = Manual AND JogFill AND JogClamp; (* never fill an unclamped bottle *)
END_PROGRAMSwitch SelManual on and try the jog buttons: the fill valve only opens while the clamp button is also held. That interlock is the kind of thing a maintenance technician will thank you for — manual mode is where people get creative. Now switch SelAuto on as well, simulating a broken switch, and both outputs drop.
Notice that Mode is assigned on every scan from the inputs, by an IF with an ELSE on every path. There is no way for it to stick.
Stage 2: a scale you can test against
The sequence will open the fill valve until the weight reaches the target, so before writing it you need something that produces a weight. The model is the same shape as the tank in chapter 23: while the valve is open, the weight rises by the flow per scan; when the bottle leaves the scale, the weight returns to zero.
PROGRAM Main
VAR
FillValve : BOOL; (* from the controller *)
BottleInPlace : BOOL := TRUE; (* from the photo-eye *)
Flow_g_scan : REAL := 25.0; (* supply: 250 g/s at 100 ms scans *)
Weight_g : REAL; (* to the controller *)
END_VAR
(* ===== Plant model: the scale ===== *)
IF FillValve THEN
Weight_g := Weight_g + Flow_g_scan;
END_IF;
IF NOT BottleInPlace THEN
Weight_g := 0.0; (* empty scale; the next bottle starts at zero *)
END_IF;
END_PROGRAMOpen FillValve and the weight climbs 25 g per scan, 500 g in 2 s. The model has a knob that matters: Flow_g_scan. Set it to 60 to simulate a supply pressure that is too high, and the fill will overshoot. Set it to 0 to simulate an empty product tank, and the fill will never finish. Those two numbers are the test cases that the final cell must survive.
A real filler is not this tidy. Product in flight between the valve and the bottle keeps landing after the valve closes, the load cell rings for a few hundred milliseconds when the clamp closes, and most production fillers use a two-speed valve — coarse to about 90 % of the target, then a slow dribble — so the in-flight error is small. The structure of the controller does not change for any of that; only the fill step gets more detailed.
Stage 3: the fill sequence
The sequence is a state machine (chapter 12) with an ENUM for the step: wait for a bottle, clamp, fill, settle, release. Each step has one job and a clear exit condition. Timed steps get their own TON, called unconditionally every scan with IN := Step = …, so the timer resets by itself the moment the sequence leaves the step.
TYPE E_Step :
(
WaitBottle,
Clamp,
Fill,
Settle,
Release
);
END_TYPE
PROGRAM Main
VAR CONSTANT
TARGET_G : REAL := 500.0;
END_VAR
VAR
AutoSel : BOOL; (* input: Auto selected *)
BottleInPlace : BOOL; (* input: photo-eye *)
Step : E_Step;
ClampT : TON;
SettleT : TON;
ClampValve : BOOL; (* output *)
FillValve : BOOL; (* output *)
Weight_g : REAL; (* from the scale model *)
END_VAR
ClampT(IN := Step = Clamp, PT := T#300ms);
SettleT(IN := Step = Settle, PT := T#500ms);
CASE Step OF
WaitBottle:
IF AutoSel AND BottleInPlace THEN
Step := Clamp;
END_IF;
Clamp:
IF ClampT.Q THEN
Step := Fill;
END_IF;
Fill:
IF Weight_g >= TARGET_G THEN
Step := Settle;
END_IF;
Settle:
IF SettleT.Q THEN
Step := Release;
END_IF;
Release:
IF NOT BottleInPlace THEN
Step := WaitBottle;
END_IF;
END_CASE;
(* Command: each output is a function of the step *)
ClampValve := Step = Clamp OR Step = Fill OR Step = Settle;
FillValve := Step = Fill;
(* ===== Plant model: the scale ===== *)
IF FillValve THEN
Weight_g := Weight_g + 25.0;
END_IF;
IF NOT BottleInPlace THEN
Weight_g := 0.0;
END_IF;
END_PROGRAMSwitch on AutoSel and BottleInPlace and watch one cycle: clamp for 300 ms, fill to 500 g, settle, release. The sequence then sits in Release until you switch BottleInPlace off — the conveyor taking the bottle away — and back on for the next one. Step through it with the pause button; every transition happens on exactly one scan.
Two design choices are worth copying. The outputs are not written inside the CASE; they are computed after it, from the step, in one line each. Ask "when is the clamp closed?" and the answer is a single readable expression. And Release waits for the bottle to leave rather than for a timer. Waiting on a real condition means a stalled conveyor leaves the machine waiting safely instead of clamping the same bottle twice.
Stage 4: fill to weight, and when it never gets there
The fill step's exit condition looks trivial: stop when the weight reaches the target. Two things go wrong with it on real machines, and both are classics from chapter 23.
The first is equality. The scale delivers 25 g per scan today; after the supply pressure is adjusted, it delivers 30. A condition written Weight_g = TARGET_G never sees exactly 500.
The second is the fill that never finishes. A product tank runs dry, a valve sticks, a nozzle blocks. The weight stops rising and the sequence waits in Fill forever with the valve open. Every step that waits for the process needs a timeout: a TON on the step, set comfortably above the normal duration (here 3 s for a 2 s fill), whose expiry sends the sequence to a fault step. Without one, "the machine stopped and nothing is on the screen" is the only symptom anyone gets.
Stage 5: counts, quality and the fault
Three things remain before the cell is complete.
Counting. At the end of Settle, the weight is judged once against the tolerance band — 495 to 515 g — and either GoodCount or RejectCount goes up by one, inside the transition, as the prediction above showed. On a real line a reject also fires a pusher or a diverter further downstream; this cell just counts. The counts are the values you would declare VAR RETAIN on a real controller, so a power cut does not wipe the shift's production figures. The run latches and the step are not.
The fault step. When the fill timer expires, the sequence goes to Faulted. The fill valve closes — that is its safe state. The clamp stays closed: a half-filled bottle released onto a moving conveyor falls over and jams the capper, so holding it is safer. FaultAlarm is on while the sequence is faulted.
Acknowledge and restart. The operator fixes the supply and presses Ack. The sequence does not resume filling. The half-filled bottle counts as a reject, the clamp opens, and the sequence waits in Release for the bottle to be taken away, then for a new one. Nothing restarts on its own, and the bottle that caused the fault never gets a second, unmeasured fill (chapter 22 explains why).
Leaving Auto. If the key switch leaves Auto during a cycle, the step returns to WaitBottle at once, which closes both valves. The one exception is Faulted: a fault must be acknowledged even if someone turns the key, otherwise switching to Off and back would be a way to make a fault disappear unread.
Here is the fault path on its own, cut down to the three steps that matter, so you can provoke it before you build it into the cell:
TYPE E_Step :
(
Fill,
Faulted,
Release
);
END_TYPE
PROGRAM Main
VAR CONSTANT
TARGET_G : REAL := 500.0;
END_VAR
VAR
BottleInPlace : BOOL := TRUE; (* input: photo-eye *)
Flow_g_scan : REAL := 0.0; (* plant: 0 = product tank empty *)
Ack : BOOL; (* input *)
Step : E_Step;
FillT : TON;
FillValve : BOOL; (* output *)
ClampValve : BOOL; (* output *)
FaultAlarm : BOOL;
RejectCount : INT;
Weight_g : REAL;
END_VAR
FillT(IN := Step = Fill, PT := T#3s);
CASE Step OF
Fill:
IF Weight_g >= TARGET_G THEN
Step := Release;
ELSIF FillT.Q THEN
Step := Faulted; (* the supply has failed *)
END_IF;
Faulted:
IF Ack THEN
RejectCount := RejectCount + 1; (* once, in the transition *)
Step := Release;
END_IF;
Release:
IF NOT BottleInPlace THEN
Weight_g := 0.0;
Step := Fill; (* next bottle; the real cell waits and clamps first *)
END_IF;
END_CASE;
FaultAlarm := Step = Faulted;
FillValve := Step = Fill AND BottleInPlace;
ClampValve := Step = Fill OR Step = Faulted; (* hold the bottle while faulted *)
(* ===== Plant model ===== *)
IF FillValve THEN
Weight_g := Weight_g + Flow_g_scan;
END_IF;
END_PROGRAMWith Flow_g_scan at 0, FillT.ET runs to 3 s and the step goes to Faulted: valve shut, clamp held, alarm on. Holding Ack down does not count the reject twice, because the count sits in the transition and the step has already moved on. Switch BottleInPlace off and on to start the next bottle, type 25 into Flow_g_scan, and the fill completes normally in 2 s.
That is everything. Before you write the last challenge, read its cases. They are the specification: one good bottle, one overfilled bottle from a high-pressure supply, one supply failure with acknowledge, and an interruption in the middle of a fill.
Where to go next
You have built a controller that a real filling line could run on: modes that fail safe, a sequence with a timeout on every wait, quantities judged with tolerances rather than equality, counts made once per bottle, a fault step with a deliberate safe state, an acknowledge, and no automatic restart. You tested it the way commissioning engineers test — with plant cases and a model of the machine.
There is more to learn, and this site has a path for each direction:
- Object-oriented ST. Modern CODESYS, TwinCAT and TIA Portal projects organise equipment with methods, interfaces and inheritance. The F · FB · OOP track starts from the function blocks you already know and goes up to IEC 61131-3 third-edition object orientation.
- Practice at volume. The 123-level game takes you from a lamp on a button to full line-safety cells, each graded with the same plant-case bench you used here. The later levels include a bottle filler, a first-out annunciator and a fail-safe shutdown that will feel familiar.
- Design, not just syntax. The design book covers how to structure whole machines: sequences, interlocks, alarm philosophies and the decisions behind them.
- Look it up. Keep the ST reference open while you work: every operator, data type, standard function and function block, with examples.
The best next step, though, is a real program. Take a machine you know — a conveyor at work, a coffee machine, a garage door — write its plant cases first, then its controller, and make every case pass.