Packaging
PLC Conveyor Sorting System: Complete Structured Text Tutorial with Sensor Logic & Diverter Control
Program a multi-lane conveyor sorting system from scratch — with photoelectric sensor logic, product tracking arrays, pneumatic diverter timing, and reject bin management in Structured Text.
Conveyor Sorting System Overview
Automated sorting systems are the backbone of logistics, food processing, packaging, and manufacturing. A typical system uses sensors to identify products, then activates diverters (pneumatic pushers, pop-up wheels, or tilt trays) to route items to the correct lane.
System Architecture
┌─── Lane 1 (Small)
│
Feed ──► Scanner ──►├─── Lane 2 (Medium)
Belt Station │
├─── Lane 3 (Large)
│
└─── Lane 4 (Reject)
Components We'll Program
| Component | I/O Type | Description |
|---|---|---|
| Main conveyor motor | DO | VFD-controlled belt |
| Infeed photoeye | DI | Detects product arrival |
| Height sensor | AI | Measures product height |
| Weight scale | AI | In-motion weighing |
| Barcode scanner | Serial/Ethernet | Product identification |
| Diverter 1-3 | DO | Pneumatic pusher solenoids |
| Diverter confirm 1-3 | DI | Pusher extended feedback |
| Lane full sensors 1-4 | DI | Downstream backup detection |
| E-Stop | DI | Emergency stop circuit |
| Stack light | DO × 3 | Green/Yellow/Red status |
Data Structures
Product Tracking
TYPE ProductInfo :
STRUCT
ID : INT;
Barcode : STRING(20);
Height : REAL; // mm
Weight : REAL; // grams
Category : INT; // 1=Small, 2=Medium, 3=Large, 0=Unknown
TargetLane : INT; // 1-3, 4=reject
Position : REAL; // mm from scanner station
Sorted : BOOL;
Valid : BOOL; // Tracking slot in use
END_STRUCT;
END_TYPETYPE DiverterStatus :
STRUCT
SolenoidCmd : BOOL;
Extended : BOOL;
Retracted : BOOL;
CycleCount : DINT; // Maintenance tracking
FaultFlag : BOOL;
END_STRUCT;
END_TYPE
TYPE SystemCounters :
STRUCT
TotalProducts : DINT;
SortedLane1 : DINT;
SortedLane2 : DINT;
SortedLane3 : DINT;
Rejected : DINT;
Throughput : REAL; // Products per minute
END_STRUCT;
END_TYPE
Product Detection & Classification
FUNCTION_BLOCK FB_ProductClassifier
VAR_INPUT
InfeedSensor : BOOL;
HeightRaw : INT; // Analog 0-27648
WeightRaw : INT;
BarcodeReady : BOOL;
BarcodeData : STRING(20);
END_VAR
VAR_OUTPUT
NewProduct : BOOL;
ProductData : ProductInfo;
END_VAR
VAR
PrevSensor : BOOL := FALSE;
RisingEdge : BOOL;
Height_mm : REAL;
Weight_g : REAL;
NextID : INT := 1;
SmallMaxHeight : REAL := 100.0;
SmallMaxWeight : REAL := 500.0;
MediumMaxHeight : REAL := 250.0;
MediumMaxWeight : REAL := 2000.0;
END_VARRisingEdge := InfeedSensor AND NOT PrevSensor;
PrevSensor := InfeedSensor;
NewProduct := FALSE;
IF RisingEdge THEN
Height_mm := (INT_TO_REAL(HeightRaw) / 27648.0) * 500.0;
Weight_g := (INT_TO_REAL(WeightRaw) / 27648.0) * 5000.0;
ProductData.ID := NextID;
NextID := NextID + 1;
IF NextID > 32767 THEN NextID := 1; END_IF;
ProductData.Height := Height_mm;
ProductData.Weight := Weight_g;
ProductData.Position := 0.0;
ProductData.Sorted := FALSE;
ProductData.Valid := TRUE;
IF Height_mm <= SmallMaxHeight AND Weight_g <= SmallMaxWeight THEN
ProductData.Category := 1;
ProductData.TargetLane := 1;
ELSIF Height_mm <= MediumMaxHeight AND Weight_g <= MediumMaxWeight THEN
ProductData.Category := 2;
ProductData.TargetLane := 2;
ELSIF Height_mm > MediumMaxHeight OR Weight_g > MediumMaxWeight THEN
ProductData.Category := 3;
ProductData.TargetLane := 3;
ELSE
ProductData.Category := 0;
ProductData.TargetLane := 4;
END_IF;
IF BarcodeReady THEN
ProductData.Barcode := BarcodeData;
END_IF;
NewProduct := TRUE;
END_IF;
END_FUNCTION_BLOCK
Product Position Tracking
Products move along the conveyor at belt speed. We track each product's position and fire diverters when they reach the correct station:
PROGRAM ProductTracker
VAR
TrackBuffer : ARRAY[0..31] OF ProductInfo;
BeltSpeed : REAL := 500.0; // mm/s
ScanInterval : REAL := 0.01; // 10ms PLC scan
Diverter1_Pos : REAL := 2000.0; // mm from scanner
Diverter2_Pos : REAL := 3500.0;
Diverter3_Pos : REAL := 5000.0;
TriggerWindow : REAL := 50.0; // mm tolerance
FireDiverter1 : BOOL := FALSE;
FireDiverter2 : BOOL := FALSE;
FireDiverter3 : BOOL := FALSE;
i : INT;
TargetPos : REAL;
END_VARFOR i := 0 TO 31 DO
IF TrackBuffer[i].Valid AND NOT TrackBuffer[i].Sorted THEN
TrackBuffer[i].Position := TrackBuffer[i].Position + (BeltSpeed * ScanInterval);
CASE TrackBuffer[i].TargetLane OF
1: TargetPos := Diverter1_Pos;
2: TargetPos := Diverter2_Pos;
3: TargetPos := Diverter3_Pos;
ELSE
TargetPos := 6000.0;
END_CASE;
IF ABS(TrackBuffer[i].Position - TargetPos) <= TriggerWindow THEN
CASE TrackBuffer[i].TargetLane OF
1: FireDiverter1 := TRUE;
2: FireDiverter2 := TRUE;
3: FireDiverter3 := TRUE;
END_CASE;
TrackBuffer[i].Sorted := TRUE;
END_IF;
IF TrackBuffer[i].Position > 7000.0 THEN
TrackBuffer[i].Valid := FALSE;
END_IF;
END_IF;
END_FOR;
END_PROGRAM
Pneumatic Diverter Control with Fault Detection
FUNCTION_BLOCK FB_DiverterControl
VAR_INPUT
Fire : BOOL;
ExtendedFB : BOOL;
RetractedFB : BOOL;
LaneFullSensor : BOOL;
Enable : BOOL;
END_VAR
VAR_OUTPUT
SolenoidOut : BOOL;
Fault : BOOL;
Busy : BOOL;
CycleCount : DINT;
END_VAR
VAR
State : INT := 0;
ExtendTimer : TON;
HoldTimer : TON;
RetractTimer : TON;
ExtendTimeout : TIME := T#500ms;
HoldTime : TIME := T#300ms;
RetractTimeout : TIME := T#500ms;
END_VARFault := FALSE;
IF NOT Enable THEN
SolenoidOut := FALSE;
State := 0;
Busy := FALSE;
RETURN;
END_IF;
CASE State OF
0: // IDLE
SolenoidOut := FALSE;
Busy := FALSE;
IF Fire AND NOT LaneFullSensor THEN
State := 1;
Busy := TRUE;
CycleCount := CycleCount + 1;
END_IF;
1: // EXTENDING
SolenoidOut := TRUE;
ExtendTimer(IN := TRUE, PT := ExtendTimeout);
IF ExtendedFB THEN
ExtendTimer(IN := FALSE, PT := T#0s);
State := 2;
ELSIF ExtendTimer.Q THEN
Fault := TRUE;
SolenoidOut := FALSE;
ExtendTimer(IN := FALSE, PT := T#0s);
State := 0;
END_IF;
2: // HOLD
SolenoidOut := TRUE;
HoldTimer(IN := TRUE, PT := HoldTime);
IF HoldTimer.Q THEN
SolenoidOut := FALSE;
HoldTimer(IN := FALSE, PT := T#0s);
State := 3;
END_IF;
3: // RETRACTING
SolenoidOut := FALSE;
RetractTimer(IN := TRUE, PT := RetractTimeout);
IF RetractedFB THEN
RetractTimer(IN := FALSE, PT := T#0s);
State := 0;
ELSIF RetractTimer.Q THEN
Fault := TRUE;
RetractTimer(IN := FALSE, PT := T#0s);
State := 0;
END_IF;
END_CASE;
END_FUNCTION_BLOCK
Throughput Monitoring
FUNCTION_BLOCK FB_ThroughputMonitor
VAR_INPUT
ProductDetected : BOOL;
ResetCounters : BOOL;
END_VAR
VAR_OUTPUT
ProductsPerMin : REAL;
ProductsPerHour : REAL;
TotalCount : DINT;
PeakRate : REAL;
END_VAR
VAR
WindowCounts : ARRAY[0..59] OF INT;
WindowIndex : INT := 0;
SecondTimer : TON;
WindowTotal : INT;
CurrentSecCount : INT := 0;
PrevDetected : BOOL := FALSE;
i : INT;
END_VARIF ProductDetected AND NOT PrevDetected THEN
CurrentSecCount := CurrentSecCount + 1;
TotalCount := TotalCount + 1;
END_IF;
PrevDetected := ProductDetected;
SecondTimer(IN := TRUE, PT := T#1s);
IF SecondTimer.Q THEN
WindowCounts[WindowIndex] := CurrentSecCount;
CurrentSecCount := 0;
WindowIndex := WindowIndex + 1;
IF WindowIndex > 59 THEN WindowIndex := 0; END_IF;
SecondTimer(IN := FALSE, PT := T#0s);
WindowTotal := 0;
FOR i := 0 TO 59 DO
WindowTotal := WindowTotal + WindowCounts[i];
END_FOR;
ProductsPerMin := INT_TO_REAL(WindowTotal);
ProductsPerHour := ProductsPerMin * 60.0;
IF ProductsPerMin > PeakRate THEN
PeakRate := ProductsPerMin;
END_IF;
END_IF;
IF ResetCounters THEN
TotalCount := 0;
PeakRate := 0.0;
FOR i := 0 TO 59 DO WindowCounts[i] := 0; END_FOR;
END_IF;
END_FUNCTION_BLOCK
Complete Sorting System Main Program
PROGRAM ConveyorSortingSystem
VAR
Classifier : FB_ProductClassifier;
Diverter : ARRAY[1..3] OF FB_DiverterControl;
Throughput : FB_ThroughputMonitor;
ConveyorMotor : BOOL := FALSE;
InfeedPhotoeye : BOOL;
HeightSensor : INT;
WeightSensor : INT;
EStop : BOOL;
DiverterFB_Ext : ARRAY[1..3] OF BOOL;
DiverterFB_Ret : ARRAY[1..3] OF BOOL;
LaneFull : ARRAY[1..4] OF BOOL;
Light_Green : BOOL;
Light_Yellow : BOOL;
Light_Red : BOOL;
SystemRunning : BOOL := FALSE;
AnyDiverterFault: BOOL := FALSE;
AnyLaneFull : BOOL := FALSE;
i : INT;
END_VARIF EStop THEN
SystemRunning := FALSE;
ConveyorMotor := FALSE;
Light_Red := TRUE;
Light_Green := FALSE;
Light_Yellow := FALSE;
RETURN;
END_IF;
ConveyorMotor := SystemRunning;
Classifier(
InfeedSensor := InfeedPhotoeye,
HeightRaw := HeightSensor,
WeightRaw := WeightSensor
);
Throughput(ProductDetected := Classifier.NewProduct);
FOR i := 1 TO 3 DO
Diverteri;
END_FOR;
AnyDiverterFault := Diverter[1].Fault OR Diverter[2].Fault OR Diverter[3].Fault;
AnyLaneFull := LaneFull[1] OR LaneFull[2] OR LaneFull[3] OR LaneFull[4];
Light_Green := SystemRunning AND NOT AnyDiverterFault AND NOT AnyLaneFull;
Light_Yellow := SystemRunning AND (AnyLaneFull OR AnyDiverterFault);
Light_Red := NOT SystemRunning OR AnyDiverterFault;
END_PROGRAM
Performance Optimization Tips
1. Encoder-Based Tracking
Use a conveyor encoder instead of estimating position from belt speed. Encoders account for belt slip and VFD speed changes:
Product.Position := Product.Position +
(INT_TO_REAL(EncoderDelta) * mmPerPulse);
2. Diverter Pre-Trigger Compensation
Pneumatic cylinders take 50-150ms to extend. Fire the diverter early to compensate:
TriggerPos := DiverterPos - (BeltSpeed * 0.100); // 100ms advance
3. Gap Control
Ensure minimum spacing between products so diverters can retract:
MinGap_mm := 0.800 * BeltSpeed; // 800ms cycle × speed
IF GapToNextProduct < MinGap_mm THEN
SlowInfeedConveyor := TRUE;
END_IF;
Summary
A conveyor sorting system brings together nearly every PLC programming skill: sensor edge detection for product arrival, analog scaling for height/weight measurement, array-based tracking for following products along the belt, state machines for diverter control with fault detection, and throughput monitoring for production KPIs. The modular approach — separate FBs for classification, tracking, diverter control, and monitoring — makes the system maintainable and testable from simple 2-lane systems to complex multi-tier sorting operations.