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. That is 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.
What the standard says a counter does
The standard defines the three counters by short pieces of code rather than by timing diagrams, so their behaviour at the edges is fixed in a way the timers' is not. Paraphrased in ST, a CTU does this on every call:
(* What a CTU does on each call, written out. CU_Rise is CU AND NOT the
value CU had on the previous call; PVmax is the largest INT, 32767. *)
IF R THEN
CV := 0;
ELSIF CU_Rise AND (CV < PVmax) THEN
CV := CV + 1;
END_IF;
Q := CV >= PV;
Four facts fall out of those lines, and each answers a question that comes up at commissioning.
Reset wins. While R is TRUE the count is held at zero, and an edge on CU during the reset is lost. When R drops on a scan where the eye is already blocked, that carton is not counted either, because there is no new rising edge.
The count stops at the top. CV < PVmax means a CTU sits at 32,767 instead of wrapping, which is gentler than plain integer arithmetic (see below).
Q is a comparison, evaluated on every call. Lower PV below the current count and Q goes TRUE on the next call; raise it and Q goes FALSE again. Nothing is latched.
PV plays no part in the counting. A CTU with PV := 0 has Q TRUE from the first scan, which is occasionally the explanation for a conveyor that refuses to start after a recipe download left the batch size at zero.
The down-counter is the mirror image: LD copies PV into CV, a rising edge on CD subtracts one while CV is above the type's minimum, and Q is CV <= 0. The up-down counter combines both, with R checked first, then LD, and a rule for the scan on which both inputs see an edge, covered below.
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 more details. 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 every model needs a way back to reality.
The counters on other platforms
The pins are the same idea everywhere, but the data types under them differ more than the names do, and the type decides where a counter runs out.
| Platform | Block or instruction | Count type | Notes |
|---|---|---|---|
| IEC 61131-3, 2nd edition | CTU, CTD, CTUD | INT | R, LD |
| IEC 61131-3, 3rd edition | adds CTU_DINT, CTU_LINT, CTU_UDINT, CTU_ULINT and the same for CTD/CTUD | as named | typed variants of the same blocks |
| CODESYS | CTU, CTD, CTUD (Standard library) | WORD, 0 to 65,535 | RESET, LOAD; the CODESYS help notes that WORD departs from the standard's INT |
| Siemens S7-1200 / S7-1500 | CTU, CTD, CTUD | chosen on the instruction box: Int, DInt and others | R, LD |
| Rockwell Logix ladder | CTU and CTD on one COUNTER tag | DINT | .PRE, .ACC, .DN, .OV, .UN; cleared with a RES instruction |
| Rockwell Logix ST / FBD | CTUD on an FBD_COUNTER tag | DINT | .CUEnable, .CDEnable, .Reset, .Load |
| Siemens S7-300 / S7-400 S5 counters | S_CU, S_CD, S_CUD | 0 to 999 | the old counter family; avoid in new code |
Rockwell's ladder counters work differently from the IEC blocks in one respect you need to know before you read someone else's program. There is no up-down counter instruction in ladder: you put a CTU and a CTD on two rungs that address the same COUNTER tag, and both act on its ACC. The tag also carries overflow and underflow bits. Count past 2,147,483,647 and .OV is set; count down past −2,147,483,648 and .UN is set. Nothing stops the program, so a program that cares has to look at those bits.
The CODESYS WORD type has a consequence of its own: CV cannot go negative, and the CODESYS CTD only counts down while CV is above zero. It stops at zero where the standard's INT version would go on to −1, −2 and below, which is the vendor difference the CTD section warns about. For wider counts the 64-bit CODESYS library adds LCTU, LCTD and LCTUD.
How fast you can count
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.
The rate is only half of it. For each part, the question is how long the sensor is on and how long it is off, and both must be longer than one scan plus the input filter. Work it out for the actual parts:
| Part | Length in the beam | Gap to the next | Belt speed | Beam blocked | Beam clear |
|---|---|---|---|---|---|
| Carton | 300 mm | 200 mm | 0.5 m/s | 600 ms | 400 ms |
| Bottle, neck-high eye | 25 mm (neck) | 55 mm | 1.0 m/s | 25 ms | 55 ms |
| Bottle cap on a cap chute | 38 mm | 0 mm (touching) | 0.4 m/s | 95 ms | almost none |
The carton is easy on any task. The bottle is fine on a 10 ms task, with 25 ms blocked and 55 ms clear, and marginal on a 20 ms task once a 3 ms input filter is added. The caps cannot be counted at all with that eye position, because touching caps leave no gap: the beam stays blocked as long as the chute is full. That is a sensor problem, not a program problem, and no amount of code fixes it. The usual answers are a different viewing angle, so the eye sees the dip between rounded edges, or a mechanical spacer that opens a gap.
Measure before you calculate when you can. Most CPUs let you trace an input against time (the trace function in TIA Portal and in CODESYS, the trend in Studio 5000) and a few minutes of trace at full line speed shows you the shortest on and off times the program will ever see.
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.
The range is the first gain. BottleCount is a 32-bit DINT, good to 2,147,483,647. A standard CTU's CV is a 16-bit INT, which tops out at 32,767, less than an hour at 10 parts a second; the CODESYS version uses a WORD and gets to 65,535, under two hours. TIA Portal lets you choose a wider data type for a counter instance; most libraries still default to 16 bits.
Arithmetic is the second. 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.
The priority is visible. The IF … ELSIF says in plain text that reset beats counting. With a CTU you have to know that R wins.
And retention is simpler. A plain DINT is easy to declare retentive, while a counter's CV lives 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 decides whether the number is right.
Gate the count, not the sensor
Most counts should only happen while the machine is doing what makes the count meaningful: count cartons while the conveyor runs in automatic, not while a fitter jogs it to clear a jam; count strokes while the press cycles, not while it is inched for a die change. The obvious way to write that is to AND the condition into the sensor signal before the edge detector. It counts some cartons twice.
The rule generalises: the edge detector should see the physical signal, debounced if necessary, and the conditions belong on the decision to count. A debounced, gated count looks like this:
PROGRAM Main
VAR
Running : BOOL; (* input: capper running in automatic *)
CapEye : BOOL; (* input: raw photo-eye on the cap chute *)
EyeStable : TON;
CapEdge : R_TRIG;
Caps : DINT; (* output: caps counted *)
END_VAR
EyeStable(IN := CapEye, PT := T#200ms); (* debounce the sensor first *)
CapEdge(CLK := EyeStable.Q); (* then find the edge *)
IF CapEdge.Q AND Running THEN (* then decide whether it counts *)
Caps := Caps + 1;
END_IF;
END_PROGRAMTurn Running on and hold CapEye for longer than 200 ms: one count. Flick it briefly and nothing happens. Turn Running off while the eye is held, then on again: no extra count. The three steps run in the same order in every good counting routine: filter, edge, decision.
A field story: the shift that made too many cases
A case packer's production report showed the late shift packing about 4 % more cases per hour than the day shift, on the same product at the same line speed. Management took it as a good result for a while. Then the warehouse counted pallets and found the late shift's figures did not add up.
The count came from a diffuse photo-eye at the packer's discharge, counted with an R_TRIG and a DINT, exactly as in this chapter. The program was fine. The cases were not: the late shift ran a promotional pack with a gloss-laminated panel, and the eye, mounted level with that panel, sometimes saw a reflection off it as the case passed and switched off and on again for a few milliseconds in the middle of the case. On the day shift's plain board it never did.
A trace of the input at line speed showed the double pulses within ten minutes. The fix was in the sensor (a retro-reflective eye with a polarising filter, which ignores glossy surfaces, mounted at a different height), plus a 50 ms debounce on the count as a second line of defence. The lesson for the program side is to make the count checkable. The report now also logs cases per pallet from the palletiser's own count, and the two numbers are compared every shift.
Rates from counts
Operators want to know how fast the line is running, and a counter gives you that almost for free: count over a known window and divide. The window must be measured, not assumed, and a TON's ET measures it:
PROGRAM Main
VAR
BottleEye : BOOL; (* input: photo-eye at the filler outlet *)
BottleEdge : R_TRIG;
Total : DINT;
Window : TON;
WindowMs : DINT;
LastTotal : DINT;
PerMinute : DINT; (* output: bottles per minute, for the HMI *)
END_VAR
BottleEdge(CLK := BottleEye);
IF BottleEdge.Q THEN
Total := Total + 1;
END_IF;
Window(IN := TRUE, PT := T#1h);
IF Window.ET >= T#10s THEN
WindowMs := TIME_TO_DINT(Window.ET);
PerMinute := (Total - LastTotal) * 60000 / WindowMs;
LastTotal := Total;
Window(IN := FALSE); (* restart the window *)
END_IF;
END_PROGRAMToggle BottleEye steadily for ten seconds and PerMinute updates. The rate comes from the difference between two totals, so the running total is never reset, and the division uses the window's measured length in milliseconds. Notice the order of the arithmetic: multiply first, then divide. (Total - LastTotal) / WindowMs * 60000 in integer arithmetic gives zero, because 50 divided by 10,000 is 0 before it is ever multiplied.
The method is not exact. The scan on which the window restarts belongs to neither window's time, so on this page's 100 ms task the rate reads about 1 % high, and the count in a window can be one more or one fewer depending on where the edges fall. Bottles toggled every 200 ms read 300 or 306 a minute here. For an HMI display that is plenty. For an efficiency report, use the shift's totals and the controller's clock, with the arithmetic done once per shift.
Choose the window from the rate. At 600 bottles a minute, a 10 s window holds 100 bottles and the display is steady to 1 %. At 6 cartons a minute it holds one, and the display jumps between 0 and 12. A slow line needs a longer window, or a different method: time the gap between parts and compute the rate from that.
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 a Siemens TIME, which is also a signed 32-bit millisecond count, runs out. (On CODESYS and TwinCAT, where TIME is unsigned, the same limit arrives at 49.7 days.) Those numbers show up in real failures: a controller that faults or misbehaves on day 25 or day 50 of uptime almost always has a 32-bit millisecond value wrapping somewhere.
Counters as diagnostics
Not every counter counts product. Some of the most useful counters in a program count things going wrong, because a fault that happens once is a nuisance and a fault that happens forty times a shift is a maintenance job.
Retry counters are the commonest. A pick-and-place gripper whose vacuum does not make on the first attempt may well make it on the second; a reject flap that does not prove its position might move on the next try. Letting the machine retry a small number of times keeps it running through the occasional miss, and counting the retries stops it from trying forever on a real failure. The count resets on success, so it measures failures in a row.
Event counters sit beside alarms and count how often a condition occurs, latched or not. The chattering low-level switch from chapter 9 and the dying silo probe from chapter 10 are both caught this way: count the rising edges of the raw signal, and alarm when the count in an hour is higher than the process can explain. Store the counts in a form maintenance can read, such as events per shift on an HMI page, and you will find more failing sensors with it than with any alarm.
And life counters track wear: valve strokes, contactor operations, gripper cycles. A pneumatic valve or a contactor has a rated life in operations from its maker's data sheet, and a counter compared with a fraction of that figure raises a "service due" message before the part fails in production. Those counters must be retained and must never reset on a download, which brings us to the next section.
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
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 without a word.
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, which is one more reason production totals are better as plain DINTs.
Downloads are the other power cut
A power cut is not the only event that resets values. A program download can do it too, and it is the one that catches people, because it happens at a convenient moment chosen by an engineer rather than by the supply.
On a Logix controller, a full download writes the tag values stored in the offline project into the controller. A stroke counter that has been counting on the machine for six months goes back to whatever value was in the project file when it was last saved, unless someone uploads first or the counter lives somewhere the download does not touch. In TIA Portal, changing the structure of a data block forces a reinitialisation of that DB on download unless you have set aside memory reserve for it, and the counts in it return to their start values. CODESYS and TwinCAT keep PERSISTENT variables through a download, and lose ordinary RETAIN ones in some download situations: check what your version does before you rely on it.
The defence is the same everywhere. Keep life and production totals in one place that changes rarely (a dedicated DB in TIA Portal, a separate persistent list in TwinCAT), back them up before every download, and log them somewhere outside the PLC, such as the SCADA historian or MES, so that a lost value can be restored from a record rather than guessed.
Reviewing counter code
When you read a counting routine, follow one part through it. Where does the sensor signal come from, and has anyone looked at a trace of it at full line speed? Is there a debounce, and does its preset fit the shortest gap between parts? Is the edge detected on the physical signal, with the gating applied afterwards? What type holds the count, and when will it run out at the highest rate the machine can reach? What resets it, and is there exactly one place that does? What happens to it at power-up and at a download?
Then look for the second source of truth. Every count that people act on (batch size, pallet layers, totals in a report) should be checkable against something else: a weight, a palletiser's own count, a full-table eye, a pallet count in the warehouse. A counter with no cross-check is a number that will be believed long after it went wrong.
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.