Worked Examples

Four complete programs: a conveyor sorter, tank level control, a batching sequence and a bottle-packing line.

How to read these examples

Each example gives the machine, the I/O list, the logic (in ladder or IEC Structured Text), and the numbers worked out. The code is written in vendor-neutral IEC style; to use it, create the tags in your software, map the I/O, and translate the instructions (see the platform tables in How PLCs Work and Ladder Logic in Practice). All of them assume that stops are wired normally closed and that emergency stops are hardwired outside the PLC.

Example 1: a conveyor sorter

Boxes arrive on a belt. Tall boxes must be pushed off onto a side chute by a diverter cylinder 600 mm after the sensors; short boxes continue. The belt moves at 0.3 m/s.

belt0.3 m/sshorttallPE1 photoeye (I0.0)PE2 high eye (I0.1): sees tall boxes onlydetection to diverter: 600 mmdiverter cylinder (Q0.1)tall chuteshort boxes continueDelay from detection to diverter = 0.6 m ÷ 0.3 m/s = 2.0 s (a TON of T#2s, or a bit shifted through a register each scan).
The sorter layout: two sensors at the infeed, a diverter 600 mm downstream.
TagAddress (example)TypeMeaning
Start_PBI0.0BOOLStart button (normally open)
Stop_OKI0.1BOOLStop button, wired normally closed
EStop_OKI0.2BOOLEmergency-stop status (hardwired circuit also cuts power)
PE1I0.3BOOLPhotoeye across the belt: any box
PE2I0.4BOOLHigh photoeye: on only for tall boxes
Divert_Home / Divert_OutI0.5 / I0.6BOOLCylinder end sensors
Belt_RunQ0.0BOOLBelt contactor
DivertQ0.1BOOLDiverter valve
Track(memory)DWORDShift register: one bit per 100 ms tick of belt travel

The numbers

  • Delay from detection to the diverter: 0.6 m ÷ 0.3 m/s = 2.0 s.
  • A single timer cannot do the job, because the next box arrives before the first reaches the diverter (boxes are 0.4 m apart, one every 1.33 s, less than the 2.0 s delay). The program must remember several boxes at once.
  • Shift register: shift a bit every 100 ms. A box detected now travels for 2.0 ÷ 0.1 = 20 ticks before it reaches the diverter. Position resolution: 0.3 m/s × 0.1 s = 30 mm per tick.
  • Capacity: boxes 0.3 m long with 0.1 m gaps make a pitch of 0.4 m: 45.0 boxes per minute at most.
  • The diverter fires when the front edge of a tall box reaches it and stays out for 0.5 s (a TP pulse); the box is 1.0 s long on the belt, so the cylinder pushes it away as it passes.
Rung 1: run the belt (seal-in)Start_PBI0.0Belt_RunQ0.0Stop_OKI0.1EStop_OKI0.2Belt_RunQ0.0Rung 2: a tall box has arrived (PE1 rising edge while PE2 is on)PPE1I0.3PE2I0.4New_TallM0.0Rung 3: the divert pulse starts when the bit reaches the diverter (stage 20)At_DiverterM0.1TPIN = rungPT = T#500msQ > DivertDivert_Pulse
Ladder for the belt, the tall-box detection and the divert pulse. The tracking is done in the shift-register code below.
(* Called every scan *)
(* 1. a 100 ms tick *)
Tick_T(IN := Belt_Run AND NOT Tick_T.Q, PT := T#100ms);
Tick := Tick_T.Q;                                   (* TRUE for one scan every 100 ms *)

(* 2. shift the tracking register once per tick (only while the belt runs) *)
IF Tick THEN
    Track := SHL(Track, 1);                          (* every box moves one stage *)
END_IF;

(* 3. record a new tall box at stage 0 *)
IF New_Tall THEN
    Track := Track OR 16#00000001;
END_IF;

(* 4. stage 20 is the diverter position: 20 ticks = 2.0 s after detection *)
At_Diverter := (Track AND 16#00100000) <> 0;
IF At_Diverter THEN
    Track := Track AND NOT 16#00100000;              (* consume it *)
END_IF;
Divert_Pulse(IN := At_Diverter, PT := T#500ms);      (* hold the valve 0.5 s *)
Divert := Divert_Pulse.Q;

(* 5. clear the register when the belt stops, so that old boxes are forgotten *)
IF NOT Belt_Run THEN Track := 0; END_IF;
  • Make the tracking depend on the belt, not on time. If the belt slows or stops, boxes must not move on in the register; a belt encoder (a pulse every few mm) is better than a timer for accuracy.
  • Verify the fire. Use the Divert_Out sensor: if the cylinder does not extend within 1 s, raise a fault.
  • Test with the diverter disconnected first, then with a few boxes, and tune the delay by a few ticks until the pusher hits the box.

Example 2: tank level control

A tank is filled by a pump from a supply and emptied by the process. The pump must start when the level is low and stop when it is high, with alarms, protection against running dry, and two pumps that share the work.

80 % pump stops40 % pump startsLT1014–20 mALSHH: high-high float switchLSLL: low-low (pump dry run)P101supplypump (Q0.5)outlet valve XV102Tank 1.5 m diameter, 3.0 m measuring range. LT101 4–20 mA gives 0–100 % level.
The tank with a 4–20 mA level transmitter, two float switches, and the pump and outlet valve.
TagAddress (example)TypeMeaning
LT101_RawIW64INTLevel transmitter, 0–27 648 for 4–20 mA
Level_Pct(memory)REALScaled level, 0–100 % of 3 m
LSLL_OKI0.0BOOLLow-low float switch (true when the level is above it)
LSHHI0.1BOOLHigh-high float switch (true when the level is at it)
Auto / Reset_PBI0.2 / I0.3BOOLAuto mode switch, alarm reset
Pump1, Pump2Q0.0, Q0.1BOOLPump contactors (through interposing relays)
Lead(memory)INTWhich pump leads: 1 or 2
Alarm_High, Alarm_Low, Fault(memory)BOOLLatched alarms

The numbers

  • Volume between start and stop: a 1.5 m diameter tank, from 40 % to 80 % of a 3 m range, is a height of 1.2 m: π × 0.75² × 1.2 = 2 121 L.
  • The pump delivers 20 L/s and the process draws 5 L/s. Fill time: 2 121 ÷ 15 = 141 s. Draw-down time with the pump off: 2 121 ÷ 5 = 424 s.
  • A cycle takes 565 s: 6.4 starts per hour. Motors are usually limited to 6–10 starts per hour, so the hysteresis band is about right; a narrower band would start the pump too often.
  • Alarms: low at 30 %, high at 85 %, plus the float switches as independent trips at the extremes (the transmitter can fail).
(* 1. scale the level: 4-20 mA = 0-27648 counts = 0-100 % *)
Level_Pct := INT_TO_REAL(LT101_Raw) / 27648.0 * 100.0;
Sensor_Fault := (LT101_Raw < -1382) OR (LT101_Raw > 29030);     (* wire break or overrange *)

(* 2. hysteresis: start below 40 %, stop above 80 % *)
IF Auto AND NOT Fault AND NOT Sensor_Fault THEN
    IF Level_Pct < 40.0 THEN Call_Pump := TRUE; END_IF;
    IF Level_Pct > 80.0 THEN Call_Pump := FALSE; END_IF;
ELSE
    Call_Pump := FALSE;
END_IF;

(* 3. alternate the lead pump every time the call drops out *)
Call_R(CLK := Call_Pump);
Call_F(CLK := Call_Pump);
IF Call_F.Q THEN Lead := 3 - Lead; END_IF;                 (* 1 <-> 2 *)

(* 4. protection: dry-run trip and high-high trip *)
IF NOT LSLL_OK THEN Fault := TRUE; END_IF;
IF LSHH         THEN Alarm_HH := TRUE; END_IF;
IF Reset_PB AND LSLL_OK AND NOT LSHH THEN Fault := FALSE; Alarm_HH := FALSE; END_IF;

(* 5. outputs *)
Pump1 := Call_Pump AND (Lead = 1) AND NOT Fault AND NOT LSHH;
Pump2 := Call_Pump AND (Lead = 2) AND NOT Fault AND NOT LSHH;
Alarm_Low  := Level_Pct < 30.0;
Alarm_High := Level_Pct > 85.0;
  • Hysteresis keeps the pump from chattering around one level. The two-compare ladder version is on Timers, Counters and Data.
  • Lag pump. Start the second pump if the level keeps rising above 90 % with the lead running, or if the lead pump trips.
  • Run-time and starts. Add a run-hour counter for each pump and a start counter, for maintenance.
  • The float switches protect against a failed transmitter: they trip independently and are wired normally closed.

Example 3: a batching sequence

A skid mixes a recipe: 100 L of water measured by a flow meter, 5 L of an ingredient, 120 s of mixing, then draining to the next stage. The flow meter gives 200 pulses per liter.

Water supplyFTflow meter 200 pulses/LXV1 water (Q0.0)IngredientXV2 ingredient (Q0.1)Mix tank150 LM1agitator (Q0.2)XV3 discharge (Q0.3)to fillingLSL empty (I0.4)Recipe: 100 L of water,5 L of ingredient, mix 120 s.100 L = 20 000 pulses.5 L = 1 000 pulses.
The batching skid: water and ingredient valves, a flow meter, a mix tank with an agitator, and a discharge valve.

The numbers

  • Pulse targets: water 100 L × 200 = 20 000 pulses; ingredient 5 L × 200 = 1 000 pulses. A 32-bit counter holds these easily; a 16-bit counter (65 535 maximum) would overflow on a larger batch.
  • Early closing (pre-act): at 100 L/min (1.67 L/s), a valve that closes over 0.5 s lets about 0.42 L through after the command (half the flow for half the time, 1.67 × 0.5 ÷ 2). That is about 83 pulses, so close the valve at 19 917 pulses to land on 100 L.
  • Times: water 60 s, ingredient 30 s (at 10 L/min), mix 120 s, discharge 126 s (105 L at 50 L/min): a batch takes about 336 s, 10.7 batches per hour, 1 125 L/h.
  • Flow-meter check: if no pulses arrive within 5 s of opening the valve, the meter, the valve or the supply has failed: go to Fault.
CASE State OF
  0:  (* idle *)
      XV1 := FALSE; XV2 := FALSE; M1 := FALSE; XV3 := FALSE;
      IF Start AND LSL_Empty AND NOT Fault THEN
          Pulses := 0; State := 10;                  (* clear the pulse count on entry *)
      END_IF;
  10: (* water: close early by the pre-act amount *)
      XV1 := TRUE;
      IF Pulses >= Water_Target - PreAct THEN XV1 := FALSE; Water_Done := TRUE; State := 20; Pulses := 0; END_IF;
      NoFlow(IN := XV1 AND (Pulses = 0), PT := T#5s);  IF NoFlow.Q THEN State := 90; END_IF;
  20: (* ingredient *)
      XV2 := TRUE;
      IF Pulses >= Ing_Target - PreAct2 THEN XV2 := FALSE; State := 30; END_IF;
  30: (* mix *)
      M1 := TRUE;  MixT(IN := TRUE, PT := T#120s);
      IF MixT.Q THEN M1 := FALSE; MixT(IN := FALSE); State := 40; END_IF;
  40: (* discharge until the tank is empty *)
      XV3 := TRUE;
      IF LSL_Empty THEN XV3 := FALSE; State := 0; Batches := Batches + 1; END_IF;
  90: (* fault: everything off *)
      XV1 := FALSE; XV2 := FALSE; M1 := FALSE; XV3 := FALSE;
      IF Reset AND NOT Fault_Cause THEN State := 0; END_IF;
END_CASE;
  • Read the pulse counter from a high-speed input (flow at 100 L/min is 333 pulses per second, beyond a normal input). Use a counter in a periodic task.
  • Weight-based batching works the same way with a load cell and the scaled weight in place of the pulse count.
  • Store recipes in retentive data (an array or a structure per recipe), and have the HMI select the recipe number; the program reads targets from it.
  • Log each batch (the actual volumes, times and the operator) for traceability.

Example 4: a bottle-packing line

Bottles pass a fill check, a reject pusher and a counting eye into cases of 24. The program counts good and rejected bottles, signals a full case, removes bottles that fail the fill check, and shows the line state.

150 bottles/minfill check (I0.3)reject pusher (Q0.2)reject bincount eye (I0.0)case24 bottlesCase fill time: 24 ÷ 150 per min = 9.6 s. Bottle spacing: one every 0.4 s.Counters: CTU Good_Count (PV 24, reset on case full), CTU Reject_Count, a running total in a DINT.Reject rate % = Reject_Count ÷ (Good + Reject) × 100, shown on the HMI.
The packing line: fill check, reject pusher and bin, count eye and the case.

The numbers

  • At 150 bottles per minute a bottle passes every 0.4 s. A case of 24 fills in 24 × 0.4 = 9.6 s.
  • Reject delay: the pusher is 3 bottle positions after the fill check: 3 × 0.4 = 1.2 s, or 12 ticks of 100 ms in a shift register (the same method as the sorter).
  • Over an 8-hour shift: 150 × 480 = 72 000 bottles (3 000 cases). With a 0.8 % reject rate, 576 are rejected and 71 424 are good: 2 976 full cases.
  • Counter size. 72 000 bottles per shift exceeds the 32 767 limit of a 16-bit INT: use a DINT for the totals.
Count bottles (one count per rising edge) and flag a full case at 24Bottle_SensorI0.7CTUCU = rungR = ResetPV = 24Q > Case_FullCase_CounterReset the count when the case has been moved onCase_FullCase_Counter.QCase_GoneI1.0ResetM1.0
The case counter (as on Timers, Counters and Data): a CTU counts good bottles and a reset rung clears it when the case leaves.
(* good and rejected bottle counts *)
Fill_Fail_R(CLK := Fill_Check_Fail);                 (* edge: this bottle failed *)
IF Fill_Fail_R.Q THEN Reject_Track := Reject_Track OR 1; END_IF;
IF Tick THEN Reject_Track := SHL(Reject_Track, 1); END_IF;
Reject_Now := (Reject_Track AND 16#00001000) <> 0;       (* stage 12 = 1.2 s *)
IF Reject_Now THEN Reject_Track := Reject_Track AND NOT 16#00001000; Reject_Count := Reject_Count + 1; END_IF;
Reject_Pusher(IN := Reject_Now, PT := T#300ms);       (* push for 0.3 s *)

Count_R(CLK := Count_Eye);                            (* bottle reached the case *)
IF Count_R.Q THEN Good_Count := Good_Count + 1; Total_Good := Total_Good + 1; END_IF;
Case_Full := Good_Count >= 24;
IF Case_Full AND Case_Gone THEN Good_Count := 0; Cases := Cases + 1; END_IF;

(* reject rate shown on the HMI *)
IF (Total_Good + Reject_Count) > 0 THEN
    Reject_Pct := DINT_TO_REAL(Reject_Count) / DINT_TO_REAL(Total_Good + Reject_Count) * 100.0;
END_IF;

(* line state *)
Starved(IN := Line_Run AND NOT Bottle_Seen, PT := T#5s);       (* no bottles for 5 s *)
Blocked(IN := Case_Full AND NOT Case_Gone, PT := T#10s);       (* no case for 10 s *)
IF Blocked.Q THEN Line_State := 3; ELSIF Starved.Q THEN Line_State := 2; ELSIF Line_Run THEN Line_State := 1; ELSE Line_State := 0; END_IF;
  • Totals in DINT, shift counts retentive or logged to a database, so that a power cut does not lose the day.
  • Reject rate and states are cheap to add and very useful: they tell you whether the line is starved (upstream problem), blocked (downstream problem) or rejecting too many.
  • Verify the pusher: a sensor on the reject bin confirms that the bottle left; a bad bottle that continues is a customer complaint.
  • Counters need edges and filters. A bottle that wobbles in the beam gives two counts; filter the eye (a 10–20 ms on-delay) and check against a manual count.

Adapting an example to your platform

StepWhat to do
1. TagsCreate tags with the same names and the right types (BOOL, DINT, REAL, TIME) in your software
2. I/OMap each input and output to your hardware addresses, and check sinking or sourcing
3. InstructionsReplace the IEC names with your platform’s: timers, edge instructions, SHL/shift, MOD, INT_TO_REAL
4. UnitsCheck timer units (ms, 10 ms or 100 ms) and the analog count range of your card
5. TasksRun counters, tick timers and PID in a periodic task if your platform has one
6. SimulateTest in a simulator, force the inputs, and walk through every state and fault before the real machine
7. CommissionTest with the load disconnected, then with it; back up the working program

Related

FAQ

Can I use these programs on my PLC?

Use them as a pattern. The logic is vendor-neutral, but you must create the tags, map the I/O, and translate the instruction names and units for your platform. Test everything in a simulator and on the machine with the load disconnected before running it for real.

Why a shift register instead of a timer for tracking boxes?

A timer handles one item at a time. When boxes are closer together than the delay, several are in transit at once, and a shift register keeps one bit per position for all of them.

How do I handle the belt speed changing?

Drive the tracking from a belt encoder (shift once per fixed distance) instead of a time tick. Then the register follows the belt, whatever its speed.

Why close the valve early in batching?

The valve takes time to close and the liquid keeps moving, so closing at the target overshoots. Closing slightly early (the pre-act) lands on the target; adjust it from the measured error of several batches.

Do these examples include safety?

They include sensible interlocks and trips, but not a safety system. Emergency stops, guarding and any hazard need a risk assessment and a proper safety circuit; see PLC Troubleshooting, Safety and Documentation.

References

  • IEC 61131-3 (data types, standard function blocks TON, TP, R_TRIG, F_TRIG, SHL).
  • ISA-88 / IEC 61512 (batch control), ISA-TR88.00.02 (PackML).
  • Flow meter and level transmitter datasheets for K-factors and ranges; valve closing times from the valve maker.
  • Petruzella, F., Programmable Logic Controllers, McGraw-Hill; Bolton, W., Programmable Logic Controllers, Newnes.

Last reviewed: October 2026. Instruction names, addresses and data sizes differ by manufacturer: confirm against your controller’s manual, and keep safety functions independent of standard PLC logic.