Chapter 11 / 24 · Part 3 · Time and counting

Counters

Count parts and events with CTU, CTD and CTUD, reset them deliberately, and count edges instead of scans.

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.

PinDirectionTypeMeaning
CUinputBOOLCount up — on each rising edge.
RinputBOOLReset: while TRUE, CV is forced to 0.
PVinputINTPreset value: the target count.
QoutputBOOLTRUE while CV >= PV.
CVoutputINTCurrent 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: CartonEye, LayerReset.

Toggle 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: CartonEye.

ScanCount 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: NewCase, BottleEye.

Before 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: EntryEye, ExitEye, TableClear.

Toggle 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: BottleEye, NextBatch.

It is four lines longer and it buys you a lot:

  • Range. BottleCount is a 32-bit DINT, good to 2,147,483,647. A standard CTU's CV is 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 … ELSIF says in plain text that reset beats counting. With a CTU you have to know that R wins.
  • Retention. A plain DINT is easy to declare retentive; 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 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.

Practise this chapter