Batch Control
ISA-88 Batch Control & Recipe Management in Structured Text: Complete Implementation Guide
Build a complete ISA-88 batch control system — from recipe parameter tables and procedural state machines to unit allocation and phase logic in Structured Text.
What Is ISA-88 (S88)?
ISA-88 (also known as S88 or IEC 61512) is the international standard for batch process control. It provides a consistent framework for designing, programming, and operating batch manufacturing systems — from pharmaceutical reactors to food processing lines.
Why ISA-88 Matters for PLC Programmers
The ISA-88 Models
ISA-88 defines three key models that work together:
1. Physical Model (Equipment Hierarchy)
Enterprise
└── Site
└── Area
└── Process Cell
└── Unit ← (This is where PLC logic lives)
├── Equipment Module (EM)
│ └── Control Module (CM)
└── Equipment Module
└── Control Module
In PLC terms:
2. Procedural Model (What Happens)
Procedure (= complete batch recipe)
└── Unit Procedure (= what happens in one unit)
└── Operation (= a major step: Charge, React, Discharge)
└── Phase (= smallest executable action)
Examples: Open_Valve, Heat_To_Temp,
Agitate, Transfer_Material
3. Recipe Model (Product Definition)
| Recipe Type | Scope | Example |
|---|---|---|
| General | Company-wide master | "Vanilla Extract v2.1" |
| Site | Adapted for one factory | Adjusted for local water quality |
| Master | For specific equipment | Maps to Reactor-01 capabilities |
| Control | Active batch instance | "Batch #20240315-001" running now |
Recipe Data Structure in Structured Text
Defining Recipe Parameters
TYPE RecipeHeader :
STRUCT
RecipeID : INT;
RecipeName : STRING(40);
Version : STRING(10);
ProductCode : STRING(20);
TotalPhases : INT;
BatchSize : REAL; // kg or liters
BatchSizeUnit : STRING(10); // 'kg', 'L', 'gal'
END_STRUCT;
END_TYPETYPE PhaseParameter :
STRUCT
PhaseID : INT; // Sequential phase number
PhaseName : STRING(30); // 'CHARGE_WATER', 'HEAT', 'MIX'
PhaseType : INT; // 1=Charge, 2=Heat, 3=Mix, 4=React, 5=Transfer
// Setpoints
TargetTemp : REAL; // °C
TargetPressure: REAL; // bar
TargetVolume : REAL; // Liters
TargetSpeed : REAL; // RPM (agitator)
HoldTime : TIME; // Duration at setpoint
// Material
MaterialCode : STRING(20);
MaterialAmount: REAL;
MaterialUnit : STRING(10); // 'kg', 'L'
// Limits
TempHiLimit : REAL;
TempLoLimit : REAL;
PressHiLimit : REAL;
END_STRUCT;
END_TYPE
Recipe Storage Table
PROGRAM RecipeManager
VAR
// Recipe database — holds up to 20 recipes, each with up to 15 phases
RecipeHeaders : ARRAY[1..20] OF RecipeHeader;
RecipePhases : ARRAY[1..20, 1..15] OF PhaseParameter;
// Active recipe
ActiveRecipeID : INT := 0;
ActiveHeader : RecipeHeader;
ActivePhases : ARRAY[1..15] OF PhaseParameter;
// Batch tracking
BatchNumber : STRING(20);
BatchStartTime : DATE_AND_TIME;
CurrentPhaseIdx : INT := 0;
TotalPhases : INT := 0;
RecipeLoaded : BOOL := FALSE;
RecipeValid : BOOL := FALSE;
END_VAR// ── Load Recipe by ID ──
IF LoadRecipeCmd AND ActiveRecipeID >= 1 AND ActiveRecipeID <= 20 THEN
ActiveHeader := RecipeHeaders[ActiveRecipeID];
TotalPhases := ActiveHeader.TotalPhases;
FOR i := 1 TO TotalPhases DO
ActivePhases[i] := RecipePhases[ActiveRecipeID, i];
END_FOR;
RecipeLoaded := TRUE;
RecipeValid := (TotalPhases > 0) AND (ActiveHeader.BatchSize > 0.0);
CurrentPhaseIdx := 0;
END_IF;
END_PROGRAM
The ISA-88 State Machine
Every phase in ISA-88 follows a standard state model. This is the heart of batch control:
┌──────────┐
┌────►│ IDLE │◄─── Reset
│ └────┬─────┘
│ │ Start
│ ┌────▼─────┐
│ │ RUNNING │◄─── Restart
│ └──┬───┬───┘
│ │ │ Hold
│ Done │ ▼
│ ┌──┘ ┌──────────┐
│ │ │ HOLDING │
│ │ └────┬─────┘
│ │ │ Held
│ │ ┌────▼─────┐
│ │ │ HELD │───► Restart (back to RUNNING)
│ │ └──────────┘
│ │
│ ▼
│ ┌──────────┐ ┌──────────┐
│ │COMPLETING│─────►│ COMPLETE │
│ └──────────┘ └────┬─────┘
│ │ Reset
└─────────────────────────┘
Any state ──► STOPPING ──► STOPPED
Any state ──► ABORTING ──► ABORTED
Implementing the State Machine in Structured Text
TYPE BatchPhaseState : (
PHASE_IDLE,
PHASE_RUNNING,
PHASE_COMPLETE,
PHASE_HOLDING,
PHASE_HELD,
PHASE_STOPPING,
PHASE_STOPPED,
PHASE_ABORTING,
PHASE_ABORTED
);
END_TYPEFUNCTION_BLOCK FB_BatchPhase
VAR_INPUT
CMD_Start : BOOL;
CMD_Hold : BOOL;
CMD_Restart : BOOL;
CMD_Stop : BOOL;
CMD_Abort : BOOL;
CMD_Reset : BOOL;
PhaseComplete: BOOL; // Set TRUE when phase logic finishes
END_VAR
VAR_OUTPUT
State : BatchPhaseState;
StateCode : INT; // Numeric state for HMI display
IsActive : BOOL; // TRUE when RUNNING
IsDone : BOOL; // TRUE when COMPLETE
InHold : BOOL; // TRUE when HELD
END_VAR
VAR
PrevState : BatchPhaseState;
END_VAR
PrevState := State;
CASE State OF
PHASE_IDLE:
IsActive := FALSE;
IsDone := FALSE;
InHold := FALSE;
IF CMD_Start THEN
State := PHASE_RUNNING;
END_IF;
PHASE_RUNNING:
IsActive := TRUE;
IF CMD_Abort THEN
State := PHASE_ABORTING;
ELSIF CMD_Stop THEN
State := PHASE_STOPPING;
ELSIF CMD_Hold THEN
State := PHASE_HOLDING;
ELSIF PhaseComplete THEN
State := PHASE_COMPLETE;
END_IF;
PHASE_HOLDING:
IsActive := FALSE;
// Perform hold actions (ramp down, safe state)
State := PHASE_HELD; // Transition immediately or after hold logic
PHASE_HELD:
InHold := TRUE;
IF CMD_Abort THEN
State := PHASE_ABORTING;
ELSIF CMD_Stop THEN
State := PHASE_STOPPING;
ELSIF CMD_Restart THEN
InHold := FALSE;
State := PHASE_RUNNING;
END_IF;
PHASE_COMPLETE:
IsActive := FALSE;
IsDone := TRUE;
IF CMD_Reset THEN
IsDone := FALSE;
State := PHASE_IDLE;
END_IF;
PHASE_STOPPING:
IsActive := FALSE;
// Perform stop actions (close valves, stop agitator)
State := PHASE_STOPPED;
PHASE_STOPPED:
IF CMD_Reset THEN
State := PHASE_IDLE;
END_IF;
PHASE_ABORTING:
IsActive := FALSE;
// Emergency actions (dump, vent, de-energize)
State := PHASE_ABORTED;
PHASE_ABORTED:
IF CMD_Reset THEN
State := PHASE_IDLE;
END_IF;
END_CASE;
// Numeric state code for HMI
CASE State OF
PHASE_IDLE: StateCode := 0;
PHASE_RUNNING: StateCode := 1;
PHASE_COMPLETE: StateCode := 2;
PHASE_HOLDING: StateCode := 3;
PHASE_HELD: StateCode := 4;
PHASE_STOPPING: StateCode := 5;
PHASE_STOPPED: StateCode := 6;
PHASE_ABORTING: StateCode := 7;
PHASE_ABORTED: StateCode := 8;
END_CASE;
END_FUNCTION_BLOCK
Phase Logic Examples
Phase: Charge Material
FUNCTION_BLOCK FB_Phase_Charge
VAR_INPUT
Execute : BOOL;
TargetVolume : REAL; // Liters to charge
MaterialValve : BOOL; // Valve feedback (open confirmed)
END_VAR
VAR_OUTPUT
Done : BOOL;
ValveCmd : BOOL; // Open inlet valve
ActualVolume : REAL;
END_VAR
VAR
FlowTotalizer : REAL := 0.0;
Charging : BOOL := FALSE;
END_VARIF Execute AND NOT Done THEN
IF FlowTotalizer < TargetVolume THEN
ValveCmd := TRUE;
Charging := TRUE;
// In real PLC, FlowTotalizer comes from a flow meter
// Simulated here:
IF MaterialValve THEN
FlowTotalizer := FlowTotalizer + 0.5; // 0.5 L per scan
END_IF;
ELSE
ValveCmd := FALSE;
Charging := FALSE;
Done := TRUE;
END_IF;
ELSIF NOT Execute THEN
Done := FALSE;
FlowTotalizer := 0.0;
ValveCmd := FALSE;
Charging := FALSE;
END_IF;
END_FUNCTION_BLOCK
Phase: Heat to Temperature
FUNCTION_BLOCK FB_Phase_HeatToTemp
VAR_INPUT
Execute : BOOL;
TargetTemp : REAL; // °C
ActualTemp : REAL; // From sensor
Tolerance : REAL; // ±°C for "at setpoint"
HoldTime : TIME; // Time to hold at setpoint
END_VAR
VAR_OUTPUT
Done : BOOL;
HeatingOn : BOOL;
AtSetpoint : BOOL;
RemainingHold : TIME;
END_VAR
VAR
HoldTimer : TON;
ReachedTemp : BOOL := FALSE;
END_VARIF Execute AND NOT Done THEN
// Check if temperature is within tolerance
AtSetpoint := ABS(ActualTemp - TargetTemp) <= Tolerance;
// Control heating element
IF ActualTemp < (TargetTemp - Tolerance) THEN
HeatingOn := TRUE;
ELSIF ActualTemp > (TargetTemp + Tolerance) THEN
HeatingOn := FALSE; // Overshoot — turn off
END_IF;
// Hold timer — counts while at setpoint
HoldTimer(IN := AtSetpoint, PT := HoldTime);
RemainingHold := HoldTime - HoldTimer.ET;
IF HoldTimer.Q THEN
HeatingOn := FALSE;
Done := TRUE;
END_IF;
ELSIF NOT Execute THEN
Done := FALSE;
HeatingOn := FALSE;
AtSetpoint := FALSE;
HoldTimer(IN := FALSE, PT := T#0s);
END_IF;
END_FUNCTION_BLOCK
Phase: Agitate / Mix
FUNCTION_BLOCK FB_Phase_Agitate
VAR_INPUT
Execute : BOOL;
TargetSpeed : REAL; // RPM
Duration : TIME; // How long to agitate
END_VAR
VAR_OUTPUT
Done : BOOL;
MotorCmd : BOOL;
SpeedSetpoint : REAL; // To VFD
Elapsed : TIME;
END_VAR
VAR
RunTimer : TON;
END_VARIF Execute AND NOT Done THEN
MotorCmd := TRUE;
SpeedSetpoint := TargetSpeed;
RunTimer(IN := TRUE, PT := Duration);
Elapsed := RunTimer.ET;
IF RunTimer.Q THEN
MotorCmd := FALSE;
SpeedSetpoint := 0.0;
Done := TRUE;
END_IF;
ELSIF NOT Execute THEN
Done := FALSE;
MotorCmd := FALSE;
SpeedSetpoint := 0.0;
RunTimer(IN := FALSE, PT := T#0s);
END_IF;
END_FUNCTION_BLOCK
Batch Sequencer — Orchestrating Phases
The sequencer walks through recipe phases in order, advancing when each phase completes:
PROGRAM BatchSequencer
VAR
// Recipe data
ActivePhases : ARRAY[1..15] OF PhaseParameter;
TotalPhases : INT := 5;
CurrentPhase : INT := 0;
// Phase instances
PhaseState : FB_BatchPhase;
ChargePhase : FB_Phase_Charge;
HeatPhase : FB_Phase_HeatToTemp;
AgitatePhase : FB_Phase_Agitate;
// Commands
BatchStart : BOOL := FALSE;
BatchHold : BOOL := FALSE;
BatchStop : BOOL := FALSE;
BatchAbort : BOOL := FALSE;
// Status
BatchRunning : BOOL := FALSE;
BatchComplete : BOOL := FALSE;
PhaseComplete : BOOL := FALSE;
// Process feedback
ActualTemp : REAL;
ValveFeedback : BOOL;
END_VAR// ── State machine for current phase ──
PhaseState(
CMD_Start := BatchStart AND (CurrentPhase > 0),
CMD_Hold := BatchHold,
CMD_Restart := NOT BatchHold,
CMD_Stop := BatchStop,
CMD_Abort := BatchAbort,
CMD_Reset := FALSE,
PhaseComplete := PhaseComplete
);
// ── Start batch ──
IF BatchStart AND NOT BatchRunning AND CurrentPhase = 0 THEN
CurrentPhase := 1;
BatchRunning := TRUE;
BatchComplete := FALSE;
PhaseComplete := FALSE;
END_IF;
// ── Execute current phase based on type ──
IF BatchRunning AND PhaseState.IsActive THEN
CASE ActivePhases[CurrentPhase].PhaseType OF
1: // CHARGE
ChargePhase(
Execute := TRUE,
TargetVolume := ActivePhases[CurrentPhase].TargetVolume,
MaterialValve := ValveFeedback
);
PhaseComplete := ChargePhase.Done;
2: // HEAT
HeatPhase(
Execute := TRUE,
TargetTemp := ActivePhases[CurrentPhase].TargetTemp,
ActualTemp := ActualTemp,
Tolerance := 2.0,
HoldTime := ActivePhases[CurrentPhase].HoldTime
);
PhaseComplete := HeatPhase.Done;
3: // MIX / AGITATE
AgitatePhase(
Execute := TRUE,
TargetSpeed := ActivePhases[CurrentPhase].TargetSpeed,
Duration := ActivePhases[CurrentPhase].HoldTime
);
PhaseComplete := AgitatePhase.Done;
END_CASE;
END_IF;
// ── Advance to next phase ──
IF PhaseComplete AND PhaseState.IsDone THEN
IF CurrentPhase < TotalPhases THEN
CurrentPhase := CurrentPhase + 1;
PhaseComplete := FALSE;
// Reset phase FBs
ChargePhase(Execute := FALSE, TargetVolume := 0.0, MaterialValve := FALSE);
HeatPhase(Execute := FALSE, TargetTemp := 0.0, ActualTemp := 0.0, Tolerance := 0.0, HoldTime := T#0s);
AgitatePhase(Execute := FALSE, TargetSpeed := 0.0, Duration := T#0s);
ELSE
BatchComplete := TRUE;
BatchRunning := FALSE;
CurrentPhase := 0;
END_IF;
END_IF;
END_PROGRAM
Example Recipe: Simple Chemical Mixing
Here's how a real recipe maps to the data structures:
Recipe: "Cleaning Solution Batch v1.2"
Product Code: CLN-001
Batch Size: 500 LPhase 1: CHARGE_WATER
- Type: Charge
- Material: DI_WATER
- Amount: 400 L
Phase 2: HEAT_WATER
- Type: Heat
- Target: 60°C
- Hold: 0 min (just reach temp)
Phase 3: ADD_CHEMICAL_A
- Type: Charge
- Material: CHEM_A
- Amount: 50 L
Phase 4: MIX
- Type: Agitate
- Speed: 200 RPM
- Duration: 15 min
Phase 5: ADD_CHEMICAL_B
- Type: Charge
- Material: CHEM_B
- Amount: 50 L
Phase 6: REACT
- Type: Heat
- Target: 75°C
- Hold: 30 min
Phase 7: COOL_AND_TRANSFER
- Type: Heat (cooling)
- Target: 25°C
- Hold: 0 min
Loading This Recipe in ST
// Recipe #1: Cleaning Solution
RecipeHeaders[1].RecipeID := 1;
RecipeHeaders[1].RecipeName := 'Cleaning Solution v1.2';
RecipeHeaders[1].ProductCode := 'CLN-001';
RecipeHeaders[1].TotalPhases := 7;
RecipeHeaders[1].BatchSize := 500.0;
RecipeHeaders[1].BatchSizeUnit := 'L';// Phase 1: Charge Water
RecipePhases[1,1].PhaseID := 1;
RecipePhases[1,1].PhaseName := 'CHARGE_WATER';
RecipePhases[1,1].PhaseType := 1;
RecipePhases[1,1].MaterialCode := 'DI_WATER';
RecipePhases[1,1].MaterialAmount := 400.0;
RecipePhases[1,1].MaterialUnit := 'L';
// Phase 2: Heat Water
RecipePhases[1,2].PhaseID := 2;
RecipePhases[1,2].PhaseName := 'HEAT_WATER';
RecipePhases[1,2].PhaseType := 2;
RecipePhases[1,2].TargetTemp := 60.0;
RecipePhases[1,2].HoldTime := T#0s;
RecipePhases[1,2].TempHiLimit := 65.0;
// Phase 3: Add Chemical A
RecipePhases[1,3].PhaseID := 3;
RecipePhases[1,3].PhaseName := 'ADD_CHEMICAL_A';
RecipePhases[1,3].PhaseType := 1;
RecipePhases[1,3].MaterialCode := 'CHEM_A';
RecipePhases[1,3].MaterialAmount := 50.0;
RecipePhases[1,3].MaterialUnit := 'L';
// Phase 4: Mix
RecipePhases[1,4].PhaseID := 4;
RecipePhases[1,4].PhaseName := 'MIX';
RecipePhases[1,4].PhaseType := 3;
RecipePhases[1,4].TargetSpeed := 200.0;
RecipePhases[1,4].HoldTime := T#15m;
// Phase 5: Add Chemical B
RecipePhases[1,5].PhaseID := 5;
RecipePhases[1,5].PhaseName := 'ADD_CHEMICAL_B';
RecipePhases[1,5].PhaseType := 1;
RecipePhases[1,5].MaterialCode := 'CHEM_B';
RecipePhases[1,5].MaterialAmount := 50.0;
RecipePhases[1,5].MaterialUnit := 'L';
// Phase 6: React
RecipePhases[1,6].PhaseID := 6;
RecipePhases[1,6].PhaseName := 'REACT';
RecipePhases[1,6].PhaseType := 2;
RecipePhases[1,6].TargetTemp := 75.0;
RecipePhases[1,6].HoldTime := T#30m;
RecipePhases[1,6].TempHiLimit := 80.0;
// Phase 7: Cool and Transfer
RecipePhases[1,7].PhaseID := 7;
RecipePhases[1,7].PhaseName := 'COOL_TRANSFER';
RecipePhases[1,7].PhaseType := 2;
RecipePhases[1,7].TargetTemp := 25.0;
RecipePhases[1,7].HoldTime := T#0s;
Best Practices for ISA-88 in PLCs
1. Keep Recipes Out of PLC Code
Never hardcode recipe parameters. Use recipe tables (arrays of structs) or download from an MES/batch server. This lets operators create new products without an engineer.
2. One Phase = One Function Block
Each phase type (Charge, Heat, Mix, Transfer) should be a reusable FB. The sequencer just passes parameters from the recipe to the FB.
3. Always Implement Hold/Restart
Operators need to pause batches for sampling, maintenance, or alarms. The ISA-88 state machine guarantees a safe hold → restart path.
4. Log Everything
Record phase transitions, setpoints, actuals, and operator actions for batch reports. In regulated industries, this is legally required (21 CFR Part 11).
5. Unit Allocation
If you have multiple identical units (Reactor A, B, C), the batch system should dynamically allocate units based on availability — not hardcode equipment.
Summary
ISA-88 batch control transforms complex, multi-step manufacturing processes into organized, reusable, and auditable PLC programs. The key insight is separation: recipes define what to make (parameters), while equipment phases define how to make it (control logic). This separation means a single PLC program can manufacture hundreds of different products just by loading different recipe parameters. Combined with the standard state machine (Idle → Running → Complete, with Hold and Abort paths), ISA-88 gives both engineers and operators a predictable, safe framework for batch manufacturing.