Four operators run most machines
At the end of chapter 1 you wrote Horn := DoorOpen AND NOT MaintKey; and it read like the rule it implemented: the horn sounds when the door is open and the key is not on. That is the whole idea of this chapter. A large part of any machine program is boolean logic — whether a motor may start, whether a valve may open, whether a press may close — and good boolean logic in ST reads aloud like the sentence in the functional specification.
There are four operators:
NOT Ais TRUE when A is FALSE. In ladder, a normally-closed contact.A AND Bis TRUE only when both are TRUE. Contacts in series.&is an accepted alternative spelling.A OR Bis TRUE when at least one is TRUE. Contacts in parallel.A XOR B(exclusive or) is TRUE when exactly one is TRUE — when the two disagree. Ladder has no single contact for it.
Their complete behaviour fits in one table:
| A | B | A AND B | A OR B | A XOR B | NOT A |
|---|---|---|---|---|---|
| FALSE | FALSE | FALSE | FALSE | FALSE | TRUE |
| FALSE | TRUE | FALSE | TRUE | TRUE | TRUE |
| TRUE | FALSE | FALSE | TRUE | TRUE | FALSE |
| TRUE | TRUE | TRUE | TRUE | FALSE | FALSE |
Flip the two inputs below through all four combinations and check every row:
PROGRAM Main
VAR
A, B : BOOL; (* inputs *)
AndQ : BOOL;
OrQ : BOOL;
XorQ : BOOL;
NandQ : BOOL; (* NOT (A AND B) *)
END_VAR
AndQ := A AND B;
OrQ := A OR B;
XorQ := A XOR B;
NandQ := NOT (A AND B);
END_PROGRAMA boolean expression is re-evaluated on every scan, exactly like the arithmetic of the previous chapter. AndQ is not "set" when both inputs come on and "reset" later; it simply is A AND B, recomputed ten times a second here. You will not need an IF for most of the logic in this chapter, and the programs are better for it.
Precedence: NOT, then AND, then XOR, then OR
When you combine operators, IEC 61131-3 applies them in a fixed order: NOT binds most tightly, then AND, then XOR, then OR. Comparisons such as > and = bind more tightly than all of the boolean operators except NOT. That is why Level > 80.0 AND PumpRunning works without parentheses.
So A OR B AND C means A OR (B AND C), the same way 1 + 2 * 3 means 1 + (2 * 3). Thinking of AND as multiplication and OR as addition is the old trick for remembering it, and it is exact: with TRUE as 1 and FALSE as 0, AND behaves like multiplication.
The seal-in: start, stop and memory in one line
The most famous circuit in industrial control is the motor starter with a seal-in (or holding) contact. Press Start and the contactor pulls in; an auxiliary contact on the contactor itself bridges the Start button, so the motor keeps running after you let go. Press Stop and the circuit breaks. In ST the whole thing is one expression in which the output appears on both sides:
PROGRAM Main
VAR
Start : BOOL; (* input: green push button, NO contact *)
StopOK : BOOL := TRUE; (* input: red push button, NC contact - TRUE when NOT pressed *)
OverloadOK : BOOL := TRUE; (* input: motor overload relay, NC - TRUE when healthy *)
Motor : BOOL; (* output: contactor coil *)
END_VAR
Motor := (Start OR Motor) AND StopOK AND OverloadOK;
END_PROGRAMRead it aloud: the motor runs if (Start is pressed or it is already running) and Stop is not pressed and the overload is healthy. Press Start and release it; the motor stays on, because on the next scan Motor is still TRUE from the scan before and holds itself in. Switch StopOK off (the button pressed) and it drops out; switch it back on and the motor stays off until you press Start again. Hold Start and press Stop together: Stop wins, because it is outside the parentheses. That priority is the point of the shape. Stop must always win.
The variable Motor is doing something new here: it is reading its own previous value. Because variables keep their value between scans, the scan-to-scan memory comes for free. Chapter 8 covers latches, SR and RS blocks and the question of what a program should remember; the seal-in is the version you will see in every motor starter.
Fail-safe: why stop buttons are normally closed
Look at the declarations again. StopOK is TRUE when the button is not pressed. That is not a quirk of this example; it is how stop circuits are wired everywhere. A stop button uses a normally-closed (NC) contact, so current flows through it when all is well and stops flowing when it is pressed. The PLC input sees 24 V as "OK to run".
The reason is what happens when something breaks. Wires come loose, terminals corrode, connectors get pulled during maintenance. With an NC contact, a broken wire looks exactly like a pressed button: the input goes FALSE and the machine stops. With a normally-open (NO) stop button wired to an input meaning "stop pressed", a broken wire looks like nobody is pressing it, and you only find out when someone needs to stop the machine and cannot.
The program below runs two identical conveyors, one with an NO stop button and one with an NC button. CableCut simulates the wire to both buttons coming off its terminal:
PROGRAM Main
VAR
Start : BOOL; (* input: start button (both conveyors) *)
StopPressed : BOOL; (* input: the operator presses stop *)
CableCut : BOOL; (* input: the stop cable comes loose *)
InStopNO : BOOL; (* what the PLC terminal sees with an NO button *)
InStopNC : BOOL; (* what the PLC terminal sees with an NC button *)
ConveyorNO : BOOL; (* output: wired the wrong way *)
ConveyorNC : BOOL; (* output: wired fail-safe *)
END_VAR
(* The field wiring: a cut cable gives 0 V either way *)
InStopNO := StopPressed AND NOT CableCut;
InStopNC := NOT StopPressed AND NOT CableCut;
ConveyorNO := (Start OR ConveyorNO) AND NOT InStopNO;
ConveyorNC := (Start OR ConveyorNC) AND InStopNC;
END_PROGRAMStart both conveyors and stop them with StopPressed: both behave. Now start them, switch CableCut on and try to stop them. ConveyorNC stopped the moment the cable came off. ConveyorNO keeps running and cannot be stopped from the button at all. The same thinking applies to every signal whose loss should mean "stop": guard door switches, pressure-OK switches, overloads, low-level cut-outs. Name such inputs for their healthy state — StopOK, GuardClosed, LubePressureOK — so the logic reads correctly and nobody inverts them by mistake.
Interlocks and permissives that read aloud
Two words appear in every functional specification. A permissive is a condition that must be true before something may start. An interlock is a condition that stops something, or prevents it, whenever it is true — usually to protect equipment. In code both end up as booleans in the same expression, and the art is to make that expression readable by the process engineer who wrote the specification.
The way to do it is to give every condition a name, and to build the final command from names rather than raw inputs:
PROGRAM Main
VAR
RunRequest : BOOL; (* input: auto sequence wants the pump *)
SuctionValveOpen : BOOL; (* input: limit switch on suction valve *)
TankHighLevel : BOOL; (* input: high-level switch in the receiving tank *)
PumpFault : BOOL; (* input: drive fault *)
LowSuctionPressure : BOOL; (* input: pressure switch, pump would run dry *)
StartPermissive : BOOL;
RunInterlock : BOOL;
TransferPump : BOOL; (* output *)
END_VAR
StartPermissive := SuctionValveOpen AND NOT TankHighLevel;
RunInterlock := PumpFault OR LowSuctionPressure OR TankHighLevel;
TransferPump := RunRequest AND StartPermissive AND NOT RunInterlock;
END_PROGRAMThe last line now reads: run the transfer pump when it is requested, it is permitted and it is not interlocked. Anyone can check that against the specification. The two lines above it each answer one question, and each named variable shows up in the online watch, so when the pump will not start the maintenance technician can see which condition is missing instead of decoding a twelve-term expression. On a real HMI those intermediate booleans are the "start conditions" screen.
Notice that TankHighLevel appears in both. That is deliberate: it blocks a start and stops a running pump. Duplication of this kind is fine; what you must avoid is a condition that appears in neither because everyone assumed it was in the other.
De Morgan's laws: two ways to say the same thing
Two identities let you rewrite any expression with NOT in front of parentheses:
| This | is the same as |
|---|---|
NOT (A AND B) | NOT A OR NOT B |
NOT (A OR B) | NOT A AND NOT B |
In words: "not (both)" is "at least one is missing"; "not (either)" is "neither". They matter because the same rule often comes to you phrased one way and reads better the other way. "The line may run if no zone has a fault" is NOT (Zone1Fault OR Zone2Fault OR Zone3Fault), which is also NOT Zone1Fault AND NOT Zone2Fault AND NOT Zone3Fault — three NC contacts in series, which is exactly how an electrician would wire it.
De Morgan is also how you check an inversion. Suppose Ready := GuardClosed AND AirOK; and you need the alarm for "not ready". The tempting NotReady := NOT GuardClosed AND AirOK; is wrong: NOT binds to GuardClosed only, so this is TRUE only when the guard is open and the air is fine. You want NOT (GuardClosed AND AirOK), which De Morgan turns into the readable NOT GuardClosed OR NOT AirOK — the guard is open, or the air is gone. When you invert a rule, invert every term and swap AND with OR. Missing one of those two steps is the common bug.
Voting and discrepancy
Where a single sensor failing would be dangerous or expensive, plants install three and vote. A 2-out-of-3 (2oo3) trip fires when at least two of the three agree: one faulty switch can neither trip the plant on its own nor prevent a genuine trip. There is no special operator for it; you list the pairs:
Trip := (A AND B) OR (A AND C) OR (B AND C);
XOR is the natural operator for the other half of the job: noticing that redundant signals disagree. ChannelA XOR ChannelB is TRUE exactly when two channels that should match do not. In a real plant you would only alarm the discrepancy after it has persisted for a few hundred milliseconds — switches never change at precisely the same scan — and that delay is a timer, coming in chapter 9. Because XOR sits between AND and OR in the precedence order, which almost nobody remembers, always wrap it in parentheses.
Bit masks on WORDs
The same operators work on WORD and DWORD values, bit by bit: AND keeps the bits set in both, OR combines them, NOT inverts all sixteen. This is how you test several flags in a packed status word at once. A mask is a constant with ones in the positions you care about:
PROGRAM Main
VAR
AlarmWord : WORD := 16#0000; (* input: 16 alarm bits from a remote valve terminal *)
TripMask : WORD := 16#00FF; (* bits 0-7 stop the line, bits 8-15 are warnings *)
TripBits : WORD;
LineTrip : BOOL;
Warning : BOOL;
END_VAR
TripBits := AlarmWord AND TripMask;
LineTrip := TripBits <> 0;
Warning := (AlarmWord AND NOT TripMask) <> 0;
END_PROGRAMType 4 into AlarmWord (bit 2) and the line trips. Type 256 (bit 8, 16#0100) and you get a warning only. Type 260 and you get both. The mask documents the classification in one constant instead of sixteen OR terms. Keep masks for exactly this kind of packed data from drives, valve terminals and protocol registers; for your own signals, separate named BOOL variables are clearer.
What to take into the next chapter
AND, OR, NOT and XOR are enough to write most of a machine's protective logic, and an expression assigned every scan is often clearer than any IF. NOT binds first, then AND, then XOR, then OR — and you write the parentheses anyway. The seal-in (Start OR Motor) AND StopOK keeps its stop conditions outside the parentheses so Stop always wins. Stops and other "loss means stop" signals are wired normally-closed and named for their healthy state. Permissives and interlocks get names of their own so the final line reads like the specification, and De Morgan tells you how to invert a rule without missing a term.
Expressions compute a value every scan. Sometimes, though, the program has to choose between doing one thing and doing another — open the valve in one way in manual and another in auto, or do nothing at all. That is the job of IF, ELSIF and ELSE, and the next chapter starts with the trap that catches everyone who first moves logic like this seal-in into an IF.