TIAN Industrial Automation
EN
All articles

Debouncing digital inputs without lying to your PLC

Mechanical contacts chatter. If you trust a raw input edge, you will count one button press as five. Here is how to debounce in PLC logic — properly, with numbers you can defend.

A limit switch closes. For about 10 milliseconds, the contact doesn’t close so much as argue with itself — open, closed, open, closed — before it settles. Your PLC scans fast enough to see every one of those bounces. So the part that should register one detection registers four, the counter drifts, and three weeks later someone files a ticket that says “count is wrong sometimes.”

This is the most common bug I get called for that isn’t really a bug. It’s physics, and you have to design around it.

What bouncing actually looks like

Here’s the signal you think you’re getting versus the one the hardware delivers:

Timing diagram: a bouncing raw input on top, a clean debounced signal below, separated by a debounce window t_d

The top trace is the raw input — clean intent, ugly reality. The bottom trace is what you want: a single, stable transition that only commits after the signal has held its new state for a debounce window t_d.

A debounce timer doesn’t make the signal cleaner. It makes your logic refuse to believe the signal until it has stopped lying.

Pick a debounce time you can defend

Don’t copy t_d = 50ms from a forum post. Measure, or look it up, then add margin.

Contact typeTypical bounceSane t_d
Snap-action limit switch1–5 ms10–20 ms
Pushbutton (panel)5–20 ms30–50 ms
Relay contact1–10 ms20 ms
Reed switchup to 1 ms5–10 ms

The rule: t_d must be longer than the worst-case bounce but shorter than the fastest real event you need to catch. If a part can pass the sensor every 80 ms, a 50 ms debounce is already eating most of your margin. That tension is the whole design — write the two numbers down before you pick a timer.

The pattern, in structured text

This is an on-delay timer (TON) gated by the input. The output only goes true once the raw input has been continuously true for t_d:

// Debounce a single digital input.
// xRaw      : BOOL  - raw field input
// xClean    : BOOL  - debounced result
// tDebounce : TIME  - hold time, e.g. T#20ms

debounceTimer(IN := xRaw, PT := tDebounce);
xClean := debounceTimer.Q;

That handles the rising edge. The catch: it debounces the transition to true but trusts the falling edge instantly. For symmetric debounce — both edges filtered — track the last stable state and only update it when the timer expires:

// Symmetric debounce: filter BOTH edges.
debounceTimer(IN := (xRaw <> xStable), PT := tDebounce);

IF debounceTimer.Q THEN
    xStable := xRaw;     // signal held its new state long enough — commit
END_IF

xClean := xStable;

The timer runs only while the raw input disagrees with the committed state. The moment they agree again — bounce settled — the timer resets on its own.

Why not just do it in hardware?

Sometimes you should. An RC filter or a Schmitt-trigger input card debounces before the signal ever reaches the scan. Reach for hardware when:

  • The event is faster than your scan cycle (you’ll miss it in software regardless).
  • You’re counting high-speed pulses — use a dedicated high-speed counter input, not ladder.
  • The line is electrically noisy and you want the filtering close to the source.

For everything else, software debounce wins because it’s visible, adjustable, and documented in the same place as the logic that depends on it.

Things that will still bite you

  1. Debouncing an already-clean signal. Encoder channels, drive status words, and networked I/O are already conditioned. Adding a debounce timer just adds latency and hides real faults.
  2. One global t_d for every input. A 50 ms estop debounce is a safety problem. A 5 ms debounce on a chattery panel button is useless. Tune per signal class.
  3. Forgetting the timer is per-instance. Reusing one TON across multiple inputs in a loop means they fight over the same accumulated time. Instantiate one timer per signal.

The one-line version

Decide t_d from the worst bounce and the fastest real event, debounce both edges, instantiate one timer per input, and never debounce a signal that was already clean. Do that and the count stops being “wrong sometimes.”