Chapter 24 / 32 · Part 6 · Professional ST

Capstone: a bottle filling line

Build a complete filling-line controller in ST — modes, sequence, timers, counts, alarms and tests — then plan what to learn next.

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: read the specification, budget the time, build and test the modes, the plant model, the sequence, the fill, and the fault path, and only then write the whole cell.

The specification, as the customer's process engineer would hand it to you:

ItemValue
Product500 g fill, accepted between 495 g and 515 g
ClampPneumatic, 300 ms to close (no feedback switch on this machine)
FillSingle-speed valve; a normal fill takes about 2 s
Settle500 ms after the valve closes before the weight is judged
Fill timeout3 s: if the target is not reached, the supply has failed
ModesOff, Manual (jog the clamp and the valve), Auto

And the I/O list:

SignalDirectionMeaning
SelManual, SelAutoInputTwo contacts of a three-position key switch
EStopOKInputStatus from the safety relay
BottleInPlaceInputPhoto-eye under the nozzle
JogClamp, JogFillInputManual push buttons
AckInputAlarm acknowledge
Weight_gInputLoad cell, already scaled to grams
ClampValve, FillValveOutputSolenoid valves

On the real machine Weight_g comes from a weighing terminal: a Siemens SIWAREX module, a Beckhoff EL3356 load-cell terminal, or a Mettler Toledo transmitter over PROFINET or EtherNet/IP. On these pages it comes from a plant model, exactly as chapter 23 showed.

Reading the specification

Two lines of that table deserve a closer look before any code, because they come from outside the machine.

The tolerance band is lopsided on purpose: 5 g under the nominal 500 g, 15 g over. Pre-packed goods sold by weight in the EU follow the "e-mark" rules of Directive 76/211/EEC, and the UK has the same scheme in its Weights and Measures (Packaged Goods) Regulations 2006. The rules are known as the three packers' rules. The average content of a batch must not be less than the nominal quantity. Only a small proportion of packages may be short by more than the tolerable negative error (TNE). And no package may be short by more than twice the TNE. For a 500 g pack the TNE is 15 g, so a 495 g bottle is legal, but a filler that aims at exactly 500 g and scatters evenly either side of it is betting the average on luck. Process engineers set the target at or a little above nominal and reject well before the legal limit. The 495 g lower limit here is that decision written down; it is not a number you are free to change in code.

The 3 s fill timeout is the other number to understand rather than copy. A normal fill takes about 2 s. The timeout must be long enough that a slow but healthy fill (a nearly empty supply tank, a cold, thicker product) never trips it, and short enough that a real supply failure is caught before the operator notices the machine has stopped. I start at about fifty per cent above the normal time and let commissioning measurements of the slowest good fill decide the final value.

The time budget

Before writing a step, add up how long one bottle takes. It tells you what the line can produce, and it gives every timer in the program a context:

StepTimeWhere the number comes from
Bottle arrives and stopsabout 700 msConveyor speed and the bottle pitch, measured at SAT
Clamp300 msCylinder stroke at the set air pressure
Fillabout 2,000 ms500 g at about 250 g/s supply
Settle500 msLoad cell and product ringing after the valve shuts
Releaseunder 100 msClamp opening, overlaps with the next index
One bottleabout 3.5 sabout 17 bottles per minute, about 1,000 per hour

A customer who needs 6,000 bottles per hour, 100 per minute, needs six of these heads working in parallel, or a faster fill. That is why real in-line fillers have rows of nozzles and rotary fillers have dozens: the fill time is set by physics and product, so throughput comes from parallel heads. It is also why the program in this chapter should be one function block per head on a real project; the last section comes back to that.

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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: SelManual, SelAuto, EStopOK, JogClamp, JogFill.

Switch 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, because 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.

EStopOK forcing the mode to Off is the standard program's reaction to the safety circuit, so that nothing tries to run while the safety relay has removed power. It is not the emergency stop. The stop itself is done by the safety relay, which on this machine cuts the supply to the valve solenoids directly, so the clamp and fill valve go to their de-energised state whatever this program does. That also makes the de-energised state of each valve a mechanical design decision. A fill valve must close without air or power. Whether the clamp opens or holds is a question for the mechanical designer, and the answer goes into the I/O list next to the output.

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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: FillValve, BottleInPlace, Flow_g_scan.

Open FillValve and the weight climbs 25 g per scan, 500 g in 2 s. The model has one knob worth playing with: 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 and 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 4 shows the two-speed fill and how the controller learns the in-flight amount.

The model also skips the tare. A real scale weighs the bottle too, and an empty bottle is not zero grams. A weighing terminal reports a gross weight (everything on the platform) and a net weight (gross minus a stored tare), and the controller tares once per bottle: after the clamp has closed and the reading is stable, before the fill valve opens. Most weighing terminals report a "stable" or "standstill" flag when the reading has stopped moving within a set band, and that flag, not a fixed delay, is the honest way to know when to tare and when to judge. On these pages the Settle timer stands in for it.

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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: AutoSel, BottleInPlace.

Switch 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.

Waiting on a sensor brings its own failure, and it is the one I see most often on commissioning. As a bottle leaves, the photo-eye can flicker: the beam catches the curved shoulder, the label edge or a drip on the glass, and BottleInPlace goes FALSE, TRUE, FALSE within 30 ms. The sequence sees the first FALSE, goes to WaitBottle, sees the TRUE on the next scan and clamps the bottle that is already leaving. The symptom is a clamp that snaps shut on a moving bottle a few times a shift, and a watch window will never show you why. The fix is to require the bottle to be gone for a time, not for a scan:

(* Release: the bottle must be gone for 200 ms, not for one scan *)
GoneT(IN := Step = Release AND NOT BottleInPlace, PT := T#200ms);
...
    Release:
        IF GoneT.Q THEN
            Step := WaitBottle;
        END_IF;

That listing is a fragment, not a live program. The 200 ms is the kind of number that gets a line in the block header, because someone will otherwise remove it as "unnecessary delay" when they try to speed the machine up.

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.

Two speeds and the product in flight

The single-speed fill in this cell stops when the scale reads 500 g. On a real filler, product is still falling between the nozzle and the bottle at that moment, and it lands after the valve has shut. That amount is called the in-flight (weighing controllers also call it the spill or the preact). A fast fill has a lot of product in flight, so real fillers do two things about it: they slow down near the target with a second, fine feed, and they shut the valve early by the amount they expect to be in flight, learning that amount from the result of each fill.

The program below runs a stream of bottles on its own. The model adds 25 g per scan on coarse feed and 5 g per scan on fine, and when the fine valve shuts, InFlight_g more lands in the bottle. After each bottle has settled, the controller compares the final weight with the target and moves its cutoff by half the error.

PROGRAM Main
VAR CONSTANT
    TARGET_G      : REAL := 500.0;
    FINE_AT_G     : REAL := 450.0;   (* change from coarse to fine feed here *)
    COARSE_G_SCAN : REAL := 25.0;    (* plant: coarse feed per 100 ms scan *)
    FINE_G_SCAN   : REAL := 5.0;     (* plant: fine feed per scan *)
    LEARN_GAIN    : REAL := 0.5;     (* correct half of each error *)
    PREACT_MAX_G  : REAL := 40.0;    (* never cut off earlier than this *)
    SETTLE_SCANS  : INT  := 5;
END_VAR
VAR
    InFlight_g  : REAL := 12.0;   (* plant: product still falling when the valve shuts *)
    Learn       : BOOL := TRUE;   (* input: adapt the cutoff after each bottle *)
    Step        : INT;            (* 0 coarse, 1 fine, 2 settle *)
    SettleCount : INT;
    CoarseValve : BOOL;           (* output *)
    FineValve   : BOOL;           (* output *)
    NextBottle  : BOOL;           (* to the model: index the next empty bottle *)
    Preact_g    : REAL;           (* learned in-flight: cut off this much early *)
    LastFinal_g : REAL;
    Bottle      : INT;
    Weight_g    : REAL;           (* from the scale model *)
    WasFeeding  : BOOL;
END_VAR

(* ===== Controller ===== *)
NextBottle := FALSE;
CASE Step OF
    0:  IF Weight_g >= FINE_AT_G THEN
            Step := 1;
        END_IF;
    1:  IF Weight_g >= TARGET_G - Preact_g THEN
            Step := 2;
            SettleCount := 0;
        END_IF;
    2:  SettleCount := SettleCount + 1;
        IF SettleCount >= SETTLE_SCANS THEN
            LastFinal_g := Weight_g;
            IF Learn THEN
                Preact_g := LIMIT(0.0, Preact_g + LEARN_GAIN * (Weight_g - TARGET_G), PREACT_MAX_G);
            END_IF;
            Bottle     := Bottle + 1;
            NextBottle := TRUE;
            Step       := 0;
        END_IF;
END_CASE;

CoarseValve := Step = 0;
FineValve   := Step = 0 OR Step = 1;

(* ===== Plant model: two-speed feed, in-flight lands on shut-off ===== *)
IF CoarseValve THEN
    Weight_g := Weight_g + COARSE_G_SCAN;
ELSIF FineValve THEN
    Weight_g := Weight_g + FINE_G_SCAN;
END_IF;
IF WasFeeding AND NOT FineValve THEN
    Weight_g := Weight_g + InFlight_g;
END_IF;
WasFeeding := FineValve;
IF NextBottle THEN
    Weight_g := 0.0;
END_IF;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: InFlight_g, Learn.

Let it run and watch LastFinal_g. The first bottle shuts off at 500 g and ends at 512 g: the full in-flight is overfill. The controller moves its cutoff 6 g earlier, the next bottle ends at 507 g, and within four or five bottles the result settles at about 500 g, moving a few grams either side. It cannot do better than that, because the fine feed adds 5 g per scan: the resolution of the fine feed and the scan time set the accuracy of the fill. That is why fine feeds are slow, and why fillers that need gram accuracy run the cutoff in a fast task or in the weighing terminal itself.

Now type 30 into InFlight_g, as if the supply pressure had been raised. One bottle overfills and the learning walks the cutoff back over the next few. Switch Learn off and change it again: every bottle now carries the whole error, and a change of supply pressure on a Monday morning turns into a shift of rejects. Weighing controllers built for filling usually offer this correction themselves; whether it runs in the terminal or in your ST, put a limit on it (PREACT_MAX_G here), so that one bad reading, such as a bottle knocked during settle, cannot make the next fill stop absurdly early.

Stage 5: counts, quality and the fault

Four 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, which 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.

The fault path on its own

Here it is 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: BottleInPlace, Flow_g_scan, Ack.

With 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.

From one head to a real project

The cell you just wrote is one long program body, which is the right shape for learning and the wrong shape for a machine with six heads. On a real project the same logic is split the way chapter 21 described:

PieceWhat it holdsInstances
FB_FillHeadStep, timers, fill with cutoff and in-flight learning, judging, counts for one headone per nozzle
FB_AlarmDelay, latch, acknowledge for one alarm (chapter 22)one per alarm per head
Mode programKey switch, safety status, the mode ENUM for the whole fillerone
Line interfaceA structure with the filler's ready, running, faulted and production counts for the upstream and downstream machinesone
HMI interfaceRecipe target, tolerances, counters, alarm list, manual commandsone

In TIA Portal, FB_FillHead becomes an FB with its timers as multi-instances, called six times from an FB_Filler with six multi-instance heads. In Studio 5000 it becomes an Add-On Instruction with one backing tag per head, called from a filler program. In CODESYS or TwinCAT it is a FUNCTION_BLOCK with an array of six instances and a FOR loop. The recipe values (target, tolerances, timeout) become parameters that the HMI writes and the block clamps, as chapter 30 shows, and the counts move to retained memory.

The plant cases move with it. The fourteen cases of the last challenge become the test suite of FB_FillHead, run against one instance in a simulator, and a shorter set runs against the whole filler to check that six heads share the supply, the modes and the alarms correctly. On site, the order is the one the design book describes: check every input and output with the machine safe, dry-cycle without product, fill with water and weigh the results by hand, and only then run product. Keep the weights from the water trial; they are the first evidence that the cutoff and the in-flight learning work on the real valve.

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, and you have seen what a real filler adds on top: a tare, a second feed speed and a cutoff that learns.

There is more to learn, and this site has a path for each direction.

  • The rest of this course. Parts 7 and 8 go past the capstone: bits and shifts, global variables and tasks, pointers, dates and the rarer types, then the patterns every real machine needs: HMI handshakes, recipes, PLCopen motion and part tracking.
  • 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 130-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 as well as syntax. The design book covers how to structure whole machines: sequences, interlocks, alarm philosophies and the decisions behind them.
  • Reference. Keep the ST reference open while you work: every operator, data type, standard function and function block, with examples.

The best next step 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.

Practise this chapter