Counting cartons with CTU
A robot palletiser builds layers of six cartons. The infeed conveyor carries cartons past a photo-eye, and after the sixth one the conveyor must stop while the robot picks the layer. Somewhere in the program a number has to go up by one per carton and something has to happen at six. That is a counter, and the IEC 61131-3 up-counter is CTU.
Like the timers in the last two chapters, CTU is a function block: declare an instance, call it on every scan, read its outputs.
| Pin | Direction | Type | Meaning |
|---|---|---|---|
CU | input | BOOL | Count up — on each rising edge. |
R | input | BOOL | Reset: while TRUE, CV is forced to 0. |
PV | input | INT | Preset value: the target count. |
Q | output | BOOL | TRUE while CV >= PV. |
CV | output | INT | Current value. |
PROGRAM Main
VAR
CartonEye : BOOL; (* input: photo-eye, TRUE = carton in the beam *)
LayerReset : BOOL; (* input: robot has picked the layer *)
LayerCount : CTU;
Infeed : BOOL; (* output: infeed conveyor *)
END_VAR
LayerCount(CU := CartonEye, R := LayerReset, PV := 6);
Infeed := NOT LayerCount.Q;
END_PROGRAMToggle CartonEye on and off six times. CV goes up by one each time the eye goes from clear to blocked — not while it is blocked, and not when it clears. At six, Q goes TRUE and the conveyor stops. More cartons would still count (CV keeps going past PV; Q just stays TRUE). Switch LayerReset on and the count returns to zero for as long as it is held.
The names of the reset and load pins are the one place where vendors genuinely disagree. The standard and Siemens use R and LD. CODESYS and TwinCAT name the same pins RESET and LOAD. Studio 5000 has no CTU in Structured Text; its CTUD block uses CUEnable, CDEnable, Reset, Load, PRE and ACC. The behaviour is the same, so learn it once and look up the spelling. This page follows the standard; its engine also accepts the CODESYS spellings RESET and LOAD, so either version of the code runs here.
Count parts, not scans
The edge detection inside CTU is the whole reason to use it. A carton on a conveyor at 0.3 m/s takes about a second to pass a photo-eye. On a 10 ms task that is a hundred scans with the beam blocked. Write the obvious line and you count all hundred:
Run both side by side and hold CartonEye on for a second:
PROGRAM Main
VAR
CartonEye : BOOL;
ScanCount : DINT; (* wrong: counts scans while blocked *)
PartCount : CTU; (* right: counts rising edges *)
END_VAR
IF CartonEye THEN
ScanCount := ScanCount + 1;
END_IF;
PartCount(CU := CartonEye, R := FALSE, PV := 1000);
END_PROGRAMScanCount races; PartCount.CV goes up by one. The CTU remembers last scan's CU and counts only when it was FALSE and is now TRUE — the same rising-edge detection as the R_TRIG from chapter 8, built in.
That memory brings back the rule from the timer chapters: call every counter instance once per scan, unconditionally. If you write IF CartonEye THEN LayerCount(CU := TRUE, ...); END_IF;, the instance never sees CU go FALSE, never sees another rising edge, and stops at one.
Edges also put a ceiling on how fast you can count. The program can only see an edge if the signal is TRUE for at least one whole scan and FALSE for at least one whole scan. On a 10 ms task the theoretical maximum is 50 pulses a second; in practice, with input filters and scan jitter, plan on half that. A bottle line at 36,000 bottles an hour is 10 per second, which is fine. An encoder on a motor shaft at 1,500 rpm with 100 pulses per revolution is 2,500 per second, which is not. That job belongs to a high-speed counter: the built-in HSC inputs on an S7-1200, a counter module such as the ET 200SP TM Count, or an EtherCAT encoder terminal. The program then reads the module's count, which never misses a pulse.
Counting down with CTD
Some jobs count down from a target: a case packer that needs 24 bottles per case, a dosing station that delivers 50 tablets per bottle, a stacker that knows how many sheets are left in the magazine. The display is more useful as "7 left" than "17 done". The down-counter CTD has CD (count down on a rising edge), LD (load: copy PV into CV), PV, and outputs CV and Q, which is TRUE while CV <= 0.
PROGRAM Main
VAR
NewCase : BOOL; (* input: empty case in position — load the counter *)
BottleEye : BOOL; (* input: bottle dropped into the case *)
Remaining : CTD;
CaseFull : BOOL; (* output: index the case out *)
END_VAR
Remaining(CD := BottleEye, LD := NewCase, PV := 6);
CaseFull := Remaining.Q;
END_PROGRAMBefore you do anything, look at CaseFull: it is already TRUE. A CTD starts with CV at 0, and Q means "CV is at or below zero", so until the first load it reports done. On a real machine that is the classic first-scan bug: power up, the case is "full", and the indexer kicks an empty case out. Switch NewCase on and off to load 6, then toggle BottleEye and watch the count fall to zero.
Keep toggling after zero and the standard says CV carries on into negative numbers. Some vendor libraries stop at zero instead. Do not depend on either: load the counter when a new case arrives, and treat Q as the only end condition.
Up and down at once: CTUD
An accumulation table between a labeller and a case packer holds a few cartons so a short stop on one machine doesn't stop the other. Cartons enter past one eye and leave past another. The program needs to know how many are on the table: that is CTUD, with both CU and CD, both R and LD, and two outputs — QU (CV >= PV, full) and QD (CV <= 0, empty).
PROGRAM Main
VAR
EntryEye : BOOL; (* input: carton arrives on the table *)
ExitEye : BOOL; (* input: carton leaves to the case packer *)
TableClear : BOOL; (* input: operator has cleared the table by hand *)
Buffer : CTUD;
LabellerHold : BOOL; (* output: table full, hold the labeller *)
END_VAR
Buffer(CU := EntryEye, CD := ExitEye, R := TableClear, LD := FALSE, PV := 8);
LabellerHold := Buffer.QU;
END_PROGRAMToggle the entry eye a few times, then the exit eye, and the count tracks the table. Two details are worth knowing. If both eyes see an edge on the same scan, the standard leaves CV unchanged — one in, one out. And if R and LD are both TRUE, reset wins.
A counter like this drifts. A carton knocked off the table by hand, a double-trigger from a carton with a reflective label, a carton removed during a jam — each one leaves the count wrong for good. That is why TableClear exists, and why a good accumulation design also has a physical "table full" photo-eye near the infeed. The counter is a model of the machine, and models need a way back to reality.
Counting with a plain DINT and R_TRIG
Many experienced ST programmers never use CTU. They write the counter out:
PROGRAM Main
VAR
BottleEye : BOOL; (* input: bottle at the filler outlet *)
NextBatch : BOOL; (* input: start a new batch *)
BottleEdge : R_TRIG;
BottleCount : DINT;
BatchDone : BOOL;
END_VAR
BottleEdge(CLK := BottleEye);
IF NextBatch THEN
BottleCount := 0;
ELSIF BottleEdge.Q THEN
BottleCount := BottleCount + 1;
END_IF;
BatchDone := BottleCount >= 12;
END_PROGRAMIt is four lines longer and it buys you a lot:
- Range.
BottleCountis a 32-bit DINT, good to 2,147,483,647. A standard CTU'sCVis a 16-bit value on most platforms, which tops out at 32,767 — less than an hour at 10 parts a second. (TIA Portal lets you choose a wider data type for a counter instance; most libraries still default to 16 bits.) - Arithmetic. A two-up filler head drops two bottles per edge:
BottleCount := BottleCount + 2;. A reject removes one:- 1. A CTU can only step by one. - Priority you can read. The
IF … ELSIFsays in plain text that reset beats counting. With a CTU you have to know thatRwins. - Retention. A plain DINT is easy to declare retentive; a counter's
CVlives inside the instance (more on that below).
Use CTU when the count is small, the reset is simple and you want the block to be recognisable to a technician. Use a DINT and an R_TRIG for production totals, anything that steps by more than one, and anything that has to survive a power cut. Both count edges, which is the part that matters.
Overflow: what happens at the top
Every integer has a ceiling, and production counters reach it. What happens there depends on what you used.
A plain INT wraps. 32,767 plus one is −32,768, and the next comparison against a batch size of 500 is suddenly TRUE or FALSE in ways nobody intended. A DINT wraps the same way, just much later: at 2,147,483,647. A CTU, by contrast, stops: the standard only increments while CV is below the type's maximum, so it sits at 32,767 and Q stays TRUE.
Do the arithmetic for the counters you write. A shift counter on a 10-per-second bottle line reaches 288,000 in eight hours — too big for an INT, trivial for a DINT. A press stroke counter at 60 strokes a minute needs 31.5 million a year; a DINT lasts sixty years. A counter of 1 ms ticks in a DINT lasts 24.8 days — exactly the point where TIME, which is also a signed 32-bit millisecond count, runs out. That number shows up in real failures: a controller that faults on day 25 of uptime almost always has a 32-bit millisecond value wrapping somewhere.
What to retain across a power cut
When the supply drops and comes back, every variable returns to its initial value unless you have declared it retentive. For counters, that is a decision you must make on purpose, variable by variable.
VAR RETAIN
ShiftTotal : DINT; (* production report: must survive *)
StrokesTotal : DINT; (* press maintenance interval *)
END_VAR
VAR
LayerCount : CTU; (* rebuilt from the machine: not retained *)
CartonEdge : R_TRIG;
END_VAR
A sensible split:
- Retain totals and maintenance counts. A shift report that resets to zero at every dip of the supply is useless, and a die-change interval that forgets 40,000 strokes is dangerous.
- Don't retain counts that describe the machine's physical state unless you can trust them after a stop. Cartons on an accumulation table may have been moved by hand while the power was off. Clear the count and let the operator confirm, or rebuild it from sensors.
- Batch counts in progress are the hard case. Retain them, but on power-up make the operator confirm the half-finished batch rather than carrying on silently.
Edge detectors have a power-up quirk too. An R_TRIG starts with its memory FALSE, so a carton sitting in the beam at power-up produces a rising edge on the first scan and is counted once. Usually harmless; in a dosing count it is one tablet too many.
The syntax is per vendor. CODESYS and TwinCAT use VAR RETAIN (and VAR PERSISTENT, which also survives a program download). In TIA Portal you tick Retain per tag in the DB or for a range of memory. Logix controllers keep every tag's value through a power cycle, so you do the opposite: clear what must not survive in a first-scan routine using the S:FS bit. To retain a CTU's CV you would have to retain the whole instance — one more reason production totals are better as plain DINTs.
Next: sequences
Timers, counters and edges are the parts. A real machine strings them together: fill until the level is reached, mix for 90 seconds, drain until empty, count the batch, start again. Wire those together with flags and IFs and within a week nobody can tell which step the machine is in. Chapter 12 shows the structure that keeps sequences readable — a state variable, a CASE, and one timer that times whatever step you are in.