Real lines almost never make one thing. The same donut machine has to run strawberry rings at 9am, choc-sprinkles at noon, and sugar minis after lunch β and the operator changing it over is not a programmer. A recipe is how you let them: a named set of setpoints you swap into the running task without touching a line of code. Pick a product below and watch the machine reconfigure itself.
The sequence never changes: advance the belt β glaze the donut β box them. What changes between products is just numbers β glaze colour, dwell time, belt speed, box count. Recipes pull those numbers out of the code and into a working struct the task reads from. Click a recipe: the line keeps running the exact same program, but the loaded setpoints change and the machine follows.
fbDonutStation( Setpoints := Active ) β the FB only ever sees Active. Loading a recipe copies one row of the set into it; the FB never knows which product it is.π Notice what did not happen: no download, no recompile, no engineer. The yellow setpoints flashed, the belt re-timed, the glaze recoloured β a 0.3 s changeover. That gap between "change a product" and "change the program" is the entire reason recipes exist.
It's three simple pieces. First a type β a STRUCT that names every parameter a product needs. Then one active instance the running code reads. Then a set: many filled-in copies, one per product, stored on the controller. Loading = copying the chosen row of the set into the active instance.
// 1 Β· the TYPE β every product is shaped like this TYPE ST_DonutRecipe : STRUCT sGlazeType : STRING; // 'strawberry' nConveyorSpeed : INT; // mm/s tGlazeDwell : TIME; // pour time rFryTemp : REAL; // Β°C setpoint bSprinkles : BOOL; nBoxCount : INT; // donuts per box END_STRUCT END_TYPE
// 2 Β· the ACTIVE copy the task actually reads VAR_GLOBAL Active : ST_DonutRecipe; // loaded set END_VAR // 3 Β· the SET β many products, stored on disk // (a file/array of ST_DonutRecipe rows) // loading is just a copy: Active := Set[ nChosenProduct ]; // from here the sequence is product-blind: IF bDonutAtStation THEN tDwell( IN:=TRUE, PT:=Active.tGlazeDwell ); END_IF
The stored set is just a table β rows are products, columns are the struct fields. This is literally what an operator sees on the HMI. Click any row to load it (it drives the live line above too):
| Product | sGlazeType | Speed | tGlazeDwell | rFryTemp | bSprinkles | nBoxCount |
|---|
On a real mixed-reference line, an operator picking from a list is the exception. The machine usually selects its own recipe from a hardware signal β and then refuses to run until a second, independent source agrees. This is the pattern worth knowing cold, because it's everywhere in automotive, pharma and food: tool ID in β barcode confirm β cycle-start.
π§ Channel 1 β the tooling tells the machine. Each reference has its own physical fixture / gripper / mould. When it's clamped in, it presents a coded tool ID β hardwired ID pins, an RFID tag, or an EtherCAT coupler the bus enumerates. The PLC reads nToolID and auto-loads that reference's recipe. The machine knows what it's set up to make β no one typed anything.
π Channel 2 β the barcode proves the part. Knowing the setup isn't enough; the part in front of the tool could be the wrong one. A BCR (barcode reader) scans the incoming part and the PLC compares sScannedRef against the loaded recipe. Agree β cycle-start enabled. Disagree β reject & alarm, before anything is processed.
Try it. Clamp a tool (it auto-loads its recipe and drives the live line up in Β§01), present a part to the reader, then hit Scan. Feed it the wrong box on purpose and watch the machine stop itself.
// the real pattern: select by tool, then VERIFY before you run CASE nToolID OF // coded tool ID read at the inputs 1: Active := Set.Strawberry; 2: Active := Set.Choc; 3: Active := Set.Sugar; ELSE bToolFault := TRUE; bCycleEnable := FALSE; // no/unknown tool β inhibit END_CASE IF bScanDone THEN // the BCR finished a read IF sScannedRef = Active.sRef THEN bCycleEnable := TRUE; // β both channels agree β run ELSE bRejectPart := TRUE; bAlarm := TRUE; // β wrong part for this tooling END_IF END_IF
This is poka-yoke β error-proofing. Two independent channels must name the same reference, or the machine won't cycle. Here's every outcome the interlock has to handle:
| Tool ID | BCR read | PLC decision | What happens |
|---|---|---|---|
| valid Β· recipe loaded | matches Active.sRef | CONFIRMED | bCycleEnable := TRUE β run the batch |
| valid Β· recipe loaded | different reference | MISMATCH | bRejectPart + bAlarm β part ejected |
| valid Β· recipe loaded | no-read / smudged | NO-READ | re-present / manual entry β never assume |
| none / unknown | β | NO TOOL | bToolFault β cycle-start inhibited |
Why bother when an operator could just pick it? Because people mis-pick, and a wrong reference is expensive β scrapped product, a line stop, a recall in regulated industries. Two-channel verification takes the human out of the critical decision and logs the scanned barcode against the batch, so traceability comes for free. Selection becomes automatic; being right becomes guaranteed.
The whole point is that recipes outlive any single download β so they are stored as files on the target, edited from the HMI, and loaded at runtime. TwinCAT gives you the Tc2_RecipeManagement library for exactly this: register the variables you want managed, then save and load named recipes that persist on disk. (On TwinCAT HMI you'd use its built-in recipe management instead β same idea, prettier front end.)
Tc2_RecipeManagement stores each named recipe as a file in the boot folder on the controller β survives power-off and re-download.Active. The 1 ms task reads it next cycle and the machine follows β no stop, no download.β οΈ Recipe β PERSISTENT. PERSISTENT/RETAIN keep the machine's one current state alive across a reboot β a single snapshot. A recipe set is a library of many named products you load on demand. Different jobs; people mix them up constantly.
β
Validate on load. A recipe is just data β and data can be wrong. Range-check every field as you copy it into Active (a 600 Β°C fryer setpoint should be refused, not fried). Recipes move responsibility to the operator, so the code must guard the limits.
Reach for recipes the moment a machine makes more than one variant, or the moment the same numbers get typed in twice. Here's what they buy you.
1 Β· Your donut line must make three products and the operators change it over themselves. What's the right structure?
2 Β· A colleague says "we already have PERSISTENT variables, so we don't need recipe management." Are they right?