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 and box count. Recipes pull those numbers out of the code and into a working struct the task reads from. Click a recipe and the line keeps running exactly the same program; only the loaded setpoints change, and the machine follows them.
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 and the glaze changed colour in a 0.3 s changeover. Recipes exist to put that gap between "change a product" and "change the program".
There are three pieces. First comes 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 a recipe means 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 a table: rows are products and columns are the struct fields. An operator sees exactly this 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 it refuses to run until a second, independent source agrees. Learn this pattern cold, because you will find it all over automotive, pharma and food plants: 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 is set up to make without anyone typing 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. If they agree, cycle-start is enabled. If they disagree, the part is rejected and an alarm raised, 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, or error-proofing. Two independent channels must name the same reference, or the machine won't cycle. The interlock has to handle every outcome in this table:
| 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 | n/a | NO TOOL | bToolFault: cycle-start inhibited |
Why bother, when an operator could just pick it? People mis-pick, and a wrong reference is expensive: scrapped product, a line stop, or 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 you get traceability without extra work. Selection becomes automatic, and being right is checked by two independent channels.
Recipes have to 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 Recipe Manager object and its 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: the same idea with a nicer front end.)
RecipeManagement stores each named recipe as a file in the runtime's file directory on the controller (the File path on the Recipe Manager's Storage tab), where it survives power-off. TwinCAT creates these files from the project's recipes when you download to the controller.Active. The 1 ms task reads it on the next cycle and the machine follows without a stop or a download.⚠️ Recipe ≠ PERSISTENT. PERSISTENT/RETAIN keep the machine's one current state alive across a reboot, as a single snapshot. A recipe set is a library of many named products you load on demand. They do different jobs, and 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 as soon as a machine makes more than one variant, or as soon as the same numbers get typed in twice. They buy you four things.
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?