Division by zero in Structured Text: what the controller actually does
Everyone guards the divisions they are worried about. The ones that fault a plant are the divisions nobody was worried about — where the divisor is a variable that is only zero in a state the code can reach on a bad day.
It is not one behavior, it is three
IEC 61131-3 does not prescribe what happens, so the answer is platform-specific — which is precisely why it is dangerous. On most CODESYS-based runtimes an integer divide by zero raises an exception and the application stops. A floating-point divide often does not: it produces inf or NaN, and that value then travels quietly into a PID setpoint, a scaling calculation, an averaging buffer. On some platforms the result is a frozen task.
The stopped controller is the good outcome. The one that keeps running with NaN in a control loop is the one you find out about from the process.
How the zero gets in
Rarely as a literal. The common shapes are these:
// 1. a variable that is never assigned anywhere
rFlow := rVolume / rSeconds; // rSeconds declared, initialised 0, never written
// 2. a counter that starts at zero
rAverage := rSum / iSamples; // first cycle: iSamples = 0
// 3. a span computed from configuration
rScaled := (rRaw - rMin) / (rMax - rMin); // rMax = rMin on an unconfigured channel
Why the compiler stays silent
A divisor is a run-time value, and the compiler reasons about types. It will reject x / 0 written as a literal on some toolchains, and that is where its help ends: x / y is valid code no matter what y will hold on Tuesday. Whether the zero is reachable is a question about the whole program, not about that line.
Guarding without cluttering the code
Wrapping every division in an IF makes the logic unreadable, and unreadable logic breeds its own bugs. Guard where the divisor comes from outside the POU — configuration, a device, an operator, a counter — and prefer a defined fallback over an exception:
IF (rMax - rMin) > 0.001 THEN
rScaled := (rRaw - rMin) / (rMax - rMin);
ELSE
rScaled := 0.0;
bScalingFault := TRUE; // visible, not silent
END_IF
A note on the tolerance
Comparing a REAL divisor against exactly 0.0 is not enough. A value of 1e-9 passes that check and still produces an overflow-sized result that behaves like a fault downstream. Compare against a threshold that means something in your units — the smallest span the instrument can actually report.
How to find them in your own project
Search the project for / and MOD, then keep only the lines where the divisor is a variable rather than a constant. For each one, answer a single question: where does this value come from, and is there a state — first cycle, device offline, channel not configured, recipe not loaded — in which it is zero? That question takes a minute per line, and it is the entire method.
Catching it automatically
PLC Lint traces divisors through the export and reports the ones that are zero-initialised and never assigned, taken from an unguarded counter, or wired to a literal zero in graphical code. Run it on your machine or check an export here.