Debouncing
Ignoring a noisy button until its reading has stayed the same for a number of ticks in a row.
When you press a mechanical button, its metal contacts do not close cleanly. They touch, spring apart, touch again, and only settle after a few milliseconds; releasing the button does the same. Nobody notices that by eye, but a chart that ticks every millisecond reads it as a burst of presses. Debouncing means accepting a new reading only once it has been stable for a while.
This page builds a debouncer that counts ticks: first an attempt with a count delay that looks right and is not, then a state machine that works, then the same idea with an explicit counter, and finally a debouncer and a toggle working together in two regions. It builds on edges and uses count delays from the previous tick.
The problem
Section titled “The problem”Here is a toggle that switches on every rising edge of its input, fed with one press and one release of
a bouncy button. Each true after a false is a rising edge:
Diagram
Code
scchart BouncyToggle { input bool raw output bool light
initial state Off if raw && !pre(raw) do light = true go to On
state On if raw && !pre(raw) do light = false go to Off}| Tick | input bool raw | output bool light | Active state of the chart |
|---|
Guided tour, 17 ticks:
- raw = falseTick 1: the button is up, the light is off.
- raw = trueThe contacts touch for the first time: a rising edge, and the light goes on.
- raw = falseThey bounce apart again.
- raw = trueTouch again: another rising edge, and the light goes off.
- raw = trueContact.
- raw = falseAnother bounce.
- raw = trueContact once more: on.
- raw = trueFrom here the contacts stay closed: the press has settled.
- raw = trueHeld.
- raw = trueHeld.
- raw = trueHeld.
- raw = falseThe release starts with a bounce of its own ...
- raw = true... which is another rising edge: off.
- raw = falseReleased.
- raw = falseReleased.
- raw = falseReleased.
- raw = falseOne press and one release, four toggles: the light is back where it started.
The edge detection is correct. The input is the problem: the chart has no way to tell a bounce from a real press, unless you tell it how long a real press lasts.
The rule
Section titled “The rule”A debouncer turns the raw reading raw into a clean one, pressed:
pressedchanges only whenrawhas disagreed with it for N readings in a row.
A single disagreeing reading followed by an agreeing one is noise and is forgotten. The examples on this page use N = 3.
A first attempt with a count delay
Section titled “A first attempt with a count delay”A count delay, if 3 raw, fires in the third tick in which raw is true. That sounds like “three
readings”, so it is tempting:
Diagram
Code
scchart DebounceNaive { input bool raw output bool pressed
initial state Released if 3 raw do pressed = true go to Pressed
state Pressed if !raw do pressed = false go to Released}| Tick | input bool raw | output bool pressed | Active state of the chart |
|---|
Guided tour, 17 ticks:
- raw = falseTick 1: released.
- raw = trueFirst contact: the count delay
if 3 rawcounts its first tick. - raw = falseA bounce. The count delay does not care: it counts ticks in which
rawis true, not ticks in a row. - raw = trueSecond tick with
rawtrue. - raw = trueThird tick with
rawtrue:pressedis accepted, although the readings were true, false, true, true. Too early. - raw = falseThe next bounce releases it at once: the release side has no filter at all.
- raw = trueCounting again from scratch: one ...
- raw = true... two ...
- raw = true... three:
pressedagain. One physical press has produced two. - raw = trueHeld.
- raw = trueHeld.
- raw = falseThe release bounce releases
pressedimmediately. - raw = trueCounting: one.
- raw = falseReleased.
- raw = falseReleased.
- raw = falseReleased.
- raw = falseReleased.
It accepts the press in tick 5, after the readings true, false, true, true. A count delay counts
the ticks in which its trigger holds while its state is active; they do not have to be consecutive, and
the count only restarts when the state is entered again. So if 3 raw means “the third tick with raw
since I got here”, not “three in a row”. (This first attempt also releases on the first false reading;
that is the second bug, and the same fix cures it.)
The working debouncer
Section titled “The working debouncer”To demand readings in a row, restart the count whenever a reading disagrees. Leaving a state and coming back does exactly that, because every entry of a state restarts its count delays. So add a “maybe” state on each side:
Diagram
Code
scchart Debounce { input bool raw output bool pressed
initial state Released if raw go to MaybePressed
state MaybePressed if !raw go to Released if 2 raw do pressed = true go to Pressed
state Pressed if !raw go to MaybeReleased
state MaybeReleased if raw go to Pressed if 2 !raw do pressed = false go to Released}| Tick | input bool raw | output bool pressed | Active state of the chart |
|---|
Guided tour, 17 ticks:
- raw = falseTick 1:
Released,pressedis false. - raw = trueFirst contact:
Releasedmoves toMaybePressed. Nothing is accepted yet. - raw = falseA bounce:
if !rawgoes straight back toReleased, and the count is forgotten. - raw = trueContact:
MaybePressedagain, entered afresh. - raw = trueSecond reading in a row:
if 2 rawhas counted one tick. - raw = falseA bounce: back to
Released. - raw = trueContact:
MaybePressed. - raw = trueSecond true reading in a row.
- raw = trueThird true reading in a row:
if 2 rawcompletes, andpressedbecomes true. - raw = trueHeld:
Pressedwaits for a false reading. - raw = trueHeld.
- raw = falseThe release starts:
MaybeReleased.pressedstays true. - raw = trueA bounce on the way out: back to
Pressed, and the release count is forgotten. - raw = falseFalse:
MaybeReleased. - raw = falseSecond false reading in a row.
- raw = falseThird false reading in a row:
pressedbecomes false. - raw = falseReleased. One press, one
pressedpulse of seven ticks.
pressed. The press is accepted in tick 9, the release in tick 16.MaybePressed is entered on the first true. Every false sends the chart back to Released, which
throws the count away; the next true enters MaybePressed afresh, with a fresh count. Only if raw
stays true in the next two ticks does if 2 raw fire, in the third reading in a row. The release side is
the mirror image. Here is the press, tick by tick:
| Tick | raw |
State after | pressed |
|---|---|---|---|
| 1 | false | Released | false |
| 2 | true | MaybePressed | false |
| 3 | false | Released | false |
| 4 | true | MaybePressed | false |
| 5 | true | MaybePressed | false |
| 6 | false | Released | false |
| 7 | true | MaybePressed | false |
| 8 | true | MaybePressed | false |
| 9 | true | Pressed | true |
Why if 2 raw and not if 3 raw? The count delay is delayed: it does not count the tick in which
MaybePressed is entered, and that tick already brought the first true. So the transition fires in the
second counted tick, which is the third reading in a row. For N readings, write if N − 1 raw. With N = 2
the count delay disappears: MaybePressed needs only a plain if raw go to Pressed, because a delayed
transition already waits for the next reading.
Choosing N
Section titled “Choosing N”N has to cover the bounce. If the contacts bounce for at most b milliseconds and the chart ticks every t milliseconds, a bounce lasts up to b / t ticks, so pick N comfortably larger than that. Do not overdo it: the debouncer accepts a change in the N-th stable reading. That is N − 1 ticks after the first stable reading and N ticks after the last bounce: in the tour above, the last bounce is in tick 6, the first stable reading in tick 7, and the press is accepted in tick 9. So the user feels a delay of about N ticks. Button datasheets quote bounce times; when in doubt, measure.
The same with a counter
Section titled “The same with a counter”For a large N, say 20 readings at one tick per millisecond, write the count as a number instead of as
states. run counts how many readings in a row have disagreed with pressed:
Diagram
Code
scchart DebounceCounter { input bool raw output bool pressed const int N = 3 // readings in a row needed to accept a change int run = 0 // readings in a row that disagree with pressed
initial state Filter if raw == pressed do run = 0 go to Filter if run < N - 1 do run++ go to Filter do pressed = raw; run = 0 go to Filter}| Tick | input bool raw | output bool pressed | local int run | Active state of the chart |
|---|
Guided tour, 17 ticks:
- raw = falseTick 1 enters
Filter. Its transitions are delayed, so the first reading is compared in tick 2;runis 0. - raw = trueFirst disagreement:
runis 1. - raw = false
rawagrees again:rungoes back to 0. - raw = trueDisagrees: 1.
- raw = trueDisagrees: 2.
- raw = falseAgrees: 0.
- raw = trueDisagrees: 1.
- raw = trueDisagrees: 2.
- raw = trueDisagrees for the third tick in a row:
run < N - 1is false, so the last transition fires, setspressedtorawand restartsrun. - raw = trueAgrees.
- raw = trueAgrees.
- raw = falseDisagrees: 1.
- raw = trueAgrees: 0.
- raw = falseDisagrees: 1.
- raw = falseDisagrees: 2.
- raw = falseThird in a row:
pressedbecomes false. - raw = falseAgrees. The same result as
Debounce, tick for tick.
run climbs to 2 and falls back to 0 on every bounce; on the third disagreeing reading in a row, pressed changes.The single state Filter loops to itself in every tick, and its three transitions are tried in priority
order:
rawagrees withpressed: nothing to do, forget any run of disagreements.- It disagrees, but the run is not long enough yet:
run++. - Otherwise this is the N-th disagreeing reading in a row: accept it and start again.
The result is identical to Debounce, tick for tick, and the same chart debounces both edges. run is a
local declared at the top of the chart, not inside Filter: a variable declared in a state is
initialised again every time the state is entered, and Filter is entered in every tick, so a counter
declared inside it would never count past 1.
Debouncing, then reacting
Section titled “Debouncing, then reacting”A debouncer is most useful as a separate piece that other parts of the chart listen to. Here one region
debounces and emits a local signal press when it accepts a press; a second region toggles a light on
press:
Diagram
Code
scchart DebouncedToggle { input bool raw output bool light signal press
region Debounce { initial state Released if raw go to MaybePressed
state MaybePressed if !raw go to Released if 2 raw do press go to Pressed
state Pressed if !raw go to MaybeReleased
state MaybeReleased if raw go to Pressed if 2 !raw go to Released }
region Toggle { initial state Off if press do light = true go to On
state On if press do light = false go to Off }}| Tick | input bool raw | output bool light | Active state of region Debounce | Active state of region Toggle |
|---|
Guided tour, 17 ticks:
- raw = falseTick 1: off.
- raw = trueFirst contact:
Debouncegoes toMaybePressed. - raw = falseBounce.
- raw = trueContact.
- raw = trueContact.
- raw = falseBounce.
- raw = trueContact.
- raw = trueContact.
- raw = trueThird reading in a row:
Debounceemitspress, andToggle, which readspressin the same tick, switches the light on. - raw = trueHeld.
- raw = trueHeld.
- raw = falseRelease begins.
- raw = trueRelease bounce: back to
Pressed, nopress. - raw = falseReleased.
- raw = falseReleased.
- raw = falseThird false reading in a row:
Debouncegoes back toReleased. - raw = falseOne press, one toggle.
The two regions run in the same tick. In tick 9, Debounce emits press, and Toggle sees it in that
same tick: KIELER schedules every emission of a signal before every test of it (see
Concurrent regions). Note the order: debounce first, then
react to the debounced event. Edge detection on the raw input counts the bounces, as the first example on
this page shows.
Try it
Section titled “Try it”Slow to press, quick to release
Change the debouncer so that a press needs four readings in a row, but a release only two. To
test it, use the bouncy input of the Debounce tour above: raw true in ticks 2, 4 and 5, 7 to 11, and
13. The starter accepts the press in tick 9 and the release in tick 16; your version should accept the
press in tick 10 and the release in tick 15.
Show the starter chart
scchart Debounce { input bool raw output bool pressed
initial state Released if raw go to MaybePressed
state MaybePressed if !raw go to Released if 2 raw do pressed = true go to Pressed
state Pressed if !raw go to MaybeReleased
state MaybeReleased if raw go to Pressed if 2 !raw do pressed = false go to Released}Show a solution
scchart Debounce { input bool raw output bool pressed
initial state Released if raw go to MaybePressed
state MaybePressed if !raw go to Released if 3 raw do pressed = true go to Pressed
state Pressed if !raw go to MaybeReleased
state MaybeReleased if raw go to Pressed if !raw do pressed = false go to Released}Two buttons
The starter debounces rawA into pressedA and ignores rawB. Make it debounce rawB into pressedB
as well, independently, with the same rule. To test it: with rawB true from tick 5 to tick 8,
pressedB should be true from tick 7 to tick 10, whatever rawA does. Hint: give each button its own
region.
Show the starter chart
scchart TwoButtons { input bool rawA, rawB output bool pressedA, pressedB
initial state Released if rawA go to MaybePressed
state MaybePressed if !rawA go to Released if 2 rawA do pressedA = true go to Pressed
state Pressed if !rawA go to MaybeReleased
state MaybeReleased if rawA go to Pressed if 2 !rawA do pressedA = false go to Released}Show a solution
scchart TwoButtons { input bool rawA, rawB output bool pressedA, pressedB
region ButtonA { initial state Released if rawA go to MaybePressed
state MaybePressed if !rawA go to Released if 2 rawA do pressedA = true go to Pressed
state Pressed if !rawA go to MaybeReleased
state MaybeReleased if rawA go to Pressed if 2 !rawA do pressedA = false go to Released }
region ButtonB { initial state Released if rawB go to MaybePressed
state MaybePressed if !rawB go to Released if 2 rawB do pressedB = true go to Pressed
state Pressed if !rawB go to MaybeReleased
state MaybeReleased if rawB go to Pressed if 2 !rawB do pressedB = false go to Released }}Check your understanding
Section titled “Check your understanding”Diagram
Code
scchart Debounce { input bool raw output bool pressed
initial state Released if raw go to MaybePressed
state MaybePressed if !raw go to Released if 2 raw do pressed = true go to Pressed
state Pressed if !raw go to MaybeReleased
state MaybeReleased if raw go to Pressed if 2 !raw do pressed = false go to Released}The working debouncer, Debounce, with N = 3. What is pressed after ticks 6, 7, 12 and 13?
Inputs per tick. A value in grey was not set in that tick: it holds from an earlier one.
| Tick | raw |
|---|---|
| 1 | false |
| 2 | true |
| 3 | true |
| 4 | false |
| 5 | true |
| 6 | true |
| 7 | true |
| 8 | true |
| 9 | false |
| 10 | true |
| 11 | false |
| 12 | false |
| 13 | false |
Explanation
pressed is false after tick 6 and true after tick 7; it is still true after tick 12 and false after
tick 13. The bounce in tick 4 sent the chart back to Released, so the three true readings in a row
are those of ticks 5, 6 and 7, and the press is accepted in tick 7. On the way back, the bounce in tick
10 returns to Pressed, and the release is accepted on the third false in a row, in tick 13.
Show the answer
Answer C: In tick 6, the third tick with raw
A count delay counts the ticks in which its trigger holds while the state is active, consecutive or not,
starting after the entry tick. raw is true in ticks 2, 4 and 6, so the transition fires in tick 6. To
require readings in a row, leave the state on a disagreeing reading, so that the next entry restarts the
count.
Key ideas
- A bouncing contact produces several edges for one press. Debounce before detecting edges.
- Accept a change only after N readings in a row. Pick N from the bounce time and the tick period; the user feels a delay of about N ticks.
- A count delay counts ticks in which its trigger holds, not ticks in a row. Leaving and re-entering a state restarts its count delays, which is how the “maybe” states demand consecutive readings.
- For large N, count in a variable declared outside the looping state.
- Run the debouncer in its own region and let it emit a signal; other regions react to it in the same tick.
Next, Counters and timeouts: counting ticks to measure time, with count delays and with variables.