Button debouncing and pattern matching
Published
A robust, portable, extensible method for handling button presses when interfacing software and hardware.
Button debouncing and pattern matching
Chris Lomont Nov 2022
This is a collection of notes on designing and implementing button debouncing and pattern recognition. These notes were developed from many gadgets I’ve designed and built over many years.
The result is a very flexible button framework evolved over many projects.
Features include:
- Arbitrary single- and multi- button patterns
- Tunable performance
- Easily portable
- Buttons can pull low or high electrically
It consists of four pieces:
- a low level timing interrupt to sample button pins,
- per-button debouncers to smooth out ripples,
- button objects that handle single buttons and their patterns,
- and an optional larger multi-button pattern handler.
This article collects factors useful for designing such systems as well as explains design requirements, decision reasons, and tradeoffs in the one I made. If you want a quick jump to code check out my implementation on GitHub https://github.com/ChrisLomont/ButtonDebouncer.
Let’s start at the lowest level of button implementation: button noise debouncing.
Button debouncing
Button debouncing is the act of turning noisy electrical signals from button up and down switching into crisper up and down states for device usage. This is needed since button up/down is noisy, and naïve sampling/listening to pins results in lots of false signals and poor device behavior. The following image (Wikipedia, CC0) shows such noise.
Jack Gannsle has two very good articles [1,2] detailing a lot of background and advice. I recommend you start there if you really want debouncing hardware in depth.
Here I collect useful knowledge about debouncers:
- Do not use pin change notifications, since the noise in the bouncing contacts do not meet interrupt needs on voltage or timing, and will lead to problems. Such code is brittle and prone to failure.
- Do use a timer interrupt to poll the button states.
- Design all pieces to use milliseconds (or faster), never use simple counters tied to device CPU performance or other non-time units. Delays and numbers in units of time, not in units of ’ticks’, keeps code portable and reusable.
- Jack tested 18 switches, found most had bouncing under 10ms, but a few had much longer bounces up to 157 ms! Some twin switches varied by over a factor of 2 in timing. Others had asymmetric up and down bouncing times.
- Solutions can be hardware or firmware (or a combination). A RC circuit is solid. I prefer software due to low cost, easier to tune if needed, easier to adapt in production firmware updates if parts change during production due to availability, cost, etc.
- There are hardware debouncers, such as the MC14490 (Digikey here) which handles 6 buttons at a time. These are ~$5 as of 2022, quite pricey for something you can do in software.
- Test your button!
Example articles and code
There are many articles about and examples of examples of debouncer code. Here are some
- From Hackaday 2010, a massive list of code examples [3].
- Another from Hackaday 2015, in two parts, with explanation and a derivation [4,5].
- Adafruit [6].
- Two commonly referenced pieces of code: tcleg’s implementation [7] of Jack Ganssle’s article and Kenneth Kuhn’s [8].
Unfortunately, none easily support the more complex button interactions I wanted. Most are difficult to move beyond the platforms they worked due to not being cleanly based on time, and none support a need I’ll detail below.
Timing
To design timing information, pick a unit time interval and stick to it. Make sure you can fit times into. Hardware needs sometime make it difficult to balance these needs. Tradeoffs:
- The biggest problem is time rollover, and how you test or handle it. It can lead to quite subtle bugs in commercial devices.
- Device architecture (RAM, efficient bit widths) may push you towards a counter size.
- Using floating point variables for time is often difficult or not supported in hardware, making float computations slow and large.
- Some time representation sizes:
- A 16-bit unsigned integer using milliseconds rolls over in 1 minute.
- A 32-bit unsigned integer using milliseconds rolls over in 49.7 days.
- A 32-bit unsigned integer using microseconds rolls over around 72 minutes.
- A 64-bit unsigned integer using nanoseconds rolls over in 584 years.
The main question for debouncing is what timing works? The goal is to pick two values: SAMPLE_MS
(milliseconds between pin samples) and DEBOUNCE_MS
(milliseconds of consistent reading for the debouncer to switch to button up or down). Some switches require different up and down debounce times since they are asymmetric in behavior, so you could have different DEBOUNCE_UP_MS
and DEBOUNCE_DOWN_MS
choices.
SAMPLE_MS
tradeoffs:
- lower values sample more often, which can give lower latency, but increases CPU load to handle updates.
- higher values sample less often, which gives higher latency at the benefit of decreased CPU load.
DEBOUNCE_MS
tradeoffs
- higher values handle longer debouncing switches, but increases latency.
- lower values decrease latency, but can fail on more noisy switches.
So for example, in the worst case, a debouncer will signal a button down state in (SAMPLE_MS + DEBOUNCE_DOWN_MS)
ms after the user pressed down. To simplify this note I’ll use the same value in both directions.
RAM and ROM loads are somewhat independent of these timing choices, except some debouncers use more RAM for longer buffer lengths for longer times.
Depending on project needs, latency can be a big issue. A big factor is what usage you expect, and what users of your device expect. There is a huge literature on human factors like this, and different uses need different speeds to feel nice to users. Illustrating some overall button clicking times helps guide debouncer needs:
-
A kiosk can handle a 100 ms button down delay, and not feel too terrible.
-
A musical instrument needs vastly quicker button response (order of 1-5 ms).
-
Typical typist types 100 words/minute, so about 10 chars per sec, so need 100ms button clicks for this fast, which means debouncers far below this.
-
Top typists may reach in excess of 200 English words per minute.
-
Morse code: long click 187.5 ms, short 62.5 ms.
-
Tests of clicks per second (CPS) show a 3-6 average clicking rate. Some pro gamers surpass 14 CPS.
-
According to Microsoft’s MSDN website, the default timing in Windows is 500 ms for a mouse click .
So to handle a specific rate, your debouncer needs to process an up and down in well under that rate.
Common debouncer timing choices listed online includes:
- For fast response, 1 to 5 ms works well.
- 10-20ms is good
- Overall user response of 50ms is usable, so debounce times (up and down) in the 5 to 50 ms range work.
Design summary shortlist
Summary list of choices and recommendations:
- Design with a timer interrupt polling inputs and providing debounce information.
- Use milliseconds (or faster, no need below microseconds) everywhere.
- Ensure interrupts only use atomic operations to communicate with user code to prevent rare data “tearing”.
- Decide on system needs
- Button needs: up/down only, single (maybe double and triple too) clicks, other click patterns
- Latency for needs drives interrupt and debounce bounds
- Pick the two debouncer two times in milliseconds. 1 ms interrupts and 5 ms debounce is a good start.
- Test
- System responsiveness and behavior
- CPU usage
- Iterate
Finally, leave room for error - parts often change in production or change in tolerances in usage, age, temperature, or climate.
Using a millisecond timer interrupt is a good place to place an elapsed system time counter. Putting the time in the fast interrupt instead of slower button pattern code allows more precise timing needed in many applications. Always remember that 32-bit unsigned integers overflow at 49 days, so design carefully.
Finally, the factor I mentioned above that I nearly always end up requiring: I want the debouncer to track the time a change happened, because higher button pattern requirements nearly always need to know how long something has been going on. So my debouncers create debounced up and down states and the time the change occurred. This has to be done atomically at the interrupt level to ensure user code doesn’t, for example, read a button as down that has just happened, but obtain a large time down from the previous up state. This type of error will cause all sorts of nightmares in debugging. Using volatile
in C or C++ is completely the wrong choice! std::atomic
in C++11 is the correct choice. Or use some platform specific hardware instructions to handle this - but it should be very high performance since it’s in an interrupt.
So this gets tricky.
Button click patterns
To make using buttons useful for many uses, the simple up/down from a debouncer is insufficient: you need to respond to user clicks, double clicks, and other patterns. This section provides some background and notation useful to understand and design such patterns.
Common click patterns
Events like clicks, double clicks, triple clicks, medium presses, long presses, and repeat clicks on hold down, can be defined as up/down patterns and time ranges. Here is an illustration:
|
|
Clicks are defined with desired up and down patterns (and we’ll add time ranges [low, high] in a moment). Patterns often need an end time; for example, to have both single and double clicks supported, the up state end time for single click should be decently longer than the up time between clicks in a double click to separate them.
Note some of the times (such as the various d3
values in the triple-click) need not be the same. I have simplified somewhat. The proper way to select them is to have users do them, gather enough data across many users, and design your ranges to catch 95% or 99% or whatever percent of your userbase you require. But this is costly, so simply cheat and copy numbers from this document, which work pretty well in my experience.
Empirical data on good timings can be found in the paper [9]. Some useful facts and data I extrapolated for my uses:
-
Using the same timings for up/down in single, double, triple clicks is acceptable: people click in fairly regular timings.
-
However, people do click faster on double clicks than on clicks by a little bit.
-
Timings vary as a Gaussian across users and across time, which is great, since it allows you to design for wide user behavior without needing more complex time distributions.
- Given the desired time above, a solid range is between 1 (~68%) and 2 (~95%) standard deviations from center. This is nice, since you can design using mean and standard deviation numbers for events, and design decent pattern timing ranges.
-
From the paper, and merging slight variation in components, I will simplify and use all down times
di
and up timesui
as- mean 150ms
- 120 standard deviation
- This means to catch ~68% of clicks, use $150\pm \frac{120}{2}$ ms (range [90,210]) . To catch ~95%, use $150\pm120$ ms (range[30,270]).
- This was for mouse style clicks. Press buttons may be different.
-
To complete a click and be sure you’re not starting a double click, you need the end timing
e1
to be larger than the high end of the range for up states in the middle of a multi-click. Thus set theei
larger than 150+120, say at 300. -
Medium press and long press times should be much longer, say 600 ms and 1500 ms
-
Repeat down should be longer than a click down, but can be close, so again, > 150+120, say 300 ms.
-
These choices are safe, but may seem slow. Tune as you see fit and what your hardware supports.
Complex click patterns
Some projects require more complex patterns, recognizing things including
- Button A down, then button B down, A up, B up, etc.
- “Chording”: where multiple buttons pressed (close) together
- Knock patterns on a single button, for example the “Shave and a haircut” knock, for hidden features
- The famous “Konami code” for game controllers UUDDLRLRBA
Complex patterns often need to know multiple button up and down history, timing history, use low and high time bounds, and do complex pattern recognition on these sequences. This can be costly in both memory for button history and cpu cost to evaluate patterns over and over.
If all you have is one or two button click patterns to recognize, coding them directly is reasonable. As things become more complex, hand crafting lots of timing and if/then code to recognize click types becomes extremely messy and brittle. Over many projects, hand coding each time is tedious and error prone.
There is a better way.
General pattern recognition
For supporting general patterns, there are competing goals as usual:
- Low resource (CPU, RAM, ROM)
- Flexibility and extensibility
- Low latency
- Ease of development and testing
A button pattern matcher should support:
- Simple patterns: single click, double click, triple click, N-click, Repeat Click, Medium hold, Long hold
- Timing ranges to handle knock patterns
- Easily timing tunable for varying design needs
- Able to handle single and multi-button patterns
- Able to handle or require (nearly) simultaneous presses
To recognize long, complex patterns, one solution requires buttons to store a history of up down times. As patterns become complex, this history become more costly in RAM usage. Pattern matching over long patterns is computationally costly when done at button rates.
A much better solution is to use a finite state machine (FSM) for each pattern. A FSM most simply can store a current state index in RAM, and have the rest in ROM. However I’ll augment it with a little more to support button pattern matching needs.
A button pattern Finite State Machine
Designing a finite state machine capable of recognizing the simple patterns, with multiple button matching, leads to the following system. The high level is:
- A Pattern Matcher is a collection of states with arrows from one state to another.
- Each Arrow has a pattern to match before being selected
- Each Arrow has 0 or more Actions to perform, such as notifying a click or other higher level need.
This design can be extended easily: if a pattern needs developed that the system cannot handle cleanly or easily, more pattern or action types can be added, and a relevant FSM designed.
The FSM is defined more precisely as:
|
|
Default FSMs for common buttons
Given the above, here are a few recognizers for common button patterns:
|
|
Code implementation
I have implemented this as a C++17 library at https://github.com/ChrisLomont/ButtonDebouncer. This section details design nuances and choices I made.
Features
Here is a list of features I obtained
- User settable timings for debouncer sample and debounce rate
- default 1 ms sampling, 5 ms debounce
- Arbitrary button clicking patterns and timings
- Buttons can pull high or low electrically on down state
- Built in (yet optional) patterns (to show how to make the pattern Finite State Machines)
- Single button: click-N, medium hold, long hold, repeat click
- Multi button: 1 down, 2 down, 1 up, 2 up, in two different timing requirements
- User settable timings for all pattern parameters
- Portable: write 4 functions per platform and the rest works.
ElapsedMs
gets elapsed system time in millisecondsStartDebouncerInterrupt
- start the timer interruptStopDebouncerInterrupt
- stop the timer interruptSetPinHardware
- do any per pin initialization, such as allocating/opening GPIO, setting pullups/downs, etc.
- Add/remove buttons on the fly
- Add/remove patterns on the fly
- Example systems given
- ESP32 using the ESP-IDF
- Windows Win32 for easy poking and debugging
- Small
- 4 files, simply include a header in your code, link in one C++ file
- ~800 lines total
Usage
Code usage (example follows):
- Implement 4 needed functions on your platform. See example below as well as the
ESP32.cpp
andWin32.cpp
examples. - Change button timings as needed. Defaults are usually ok.
- Make buttons.
- (Optional) Make/change patterns recognized
- (Optional) make multi-button pattern watcher
- Add patterns or use default ones for testing
- Process buttons every 20-30ms depending on latency needs
- Can check
bool IsDown()
as is - Call button
UpdatePatternMatches
to update pattern watching - Call
int Clicks(int pattern)
for each pattern index to find matches
- Can check
- Destroy buttons when done to free up resources and remove interrupt.
Example code
|
|
Goals
Here is a list of goals, some competing or even mutually exclusive, that I tried to obtain, roughly in order of importance, for reference. These drove the features above.
-
Button Up/Down debounced
-
Common patterns users expect supported as needed: Single Click, Double Click, Triple Click (Click-N ok for all), Medium hold, Long hold, Repeat click
-
Handles any number of buttons
-
Arbitrary single- and multi- button patterns for special project needs
-
Latency/error/noise tunable for needs from very low latency to very noisy buttons
-
Low latency so users don’t hate it or bang on buttons trying to get response
-
No (or low) errors
-
Easy to port to new systems and projects
-
Easily tunable for ease of use in new projects (timing of sampling, debouncing, pattern thresholds)
-
Time based for portability (milliseconds or finer grained)
-
Low resource usage as much as possible (RAM,ROM,CPU, interrupts)
-
Thread safety between interrupt and user code
-
Buttons physically can pull voltage high or low when down
-
Can add/remove buttons from system on the fly
-
Can add/remove patterns on the fly
Design Notes
Finally, here are a detailed system design notes:
The system is four main pieces:
- Timer Interrupt - on 1 ms ticks samples pins and tells debouncers the results
- Debouncers - debounce on 5 ms windows using an integrator, provide Up/Down and system elapsed ms at last change.
- Buttons - sample debouncers (20-30ms), applying info into patterns, to match single-button events
- Multi-button matcher - samples all debouncers (20-30ms), applying patterns, to match multi-button
Patterns take as inputs current button id, button up/down state, and time that state has persisted. Each runs a finite state machine to determine events of interest.
Design choices
- time as uint64_t in ms to avoid wraparound hassles and testing
- 1ms debouncer samples, 5 ms debounce
- Each time tick, the interrupt:
- Get elapsed milliseconds for system
- for each debouncer in the system:
- reads the physical input high/low
- converts to up/down as button requires
- calls debouncer->DebounceInput(isDown, elapsedMs)
- The debouncer gets an input from the interrupt and
- uses a local integrator to track up/down (if/then and +- per step)
- marks state changes atomically to prevent data tearing
- To make as portable as possible to many systems, this is done with an atomic pointer swap.
- All reads and writes go through two functions in Debouncer to enforce data consistency
- Any errors from systems not supporting features can be fixed by addressing all items at this choke point
- Buttons, updated every so often (20-30ms as user desires)
- obtains debouncer state atomically to prevent errors
- updates all patterns
- User then check each button, each pattern, for relevant clicks, other things.
Future Directions
Some ideas I was unable to implement so far, in no particular order
- “Compile” FSMs into byte code , export tables, to include in ROM for low memory systems
History
|
|
References
[1] http://www.ganssle.com/debouncing.htm
[2] http://www.ganssle.com/debouncing-pt2.htm
[3] https://hackaday.com/2010/11/09/debounce-code-one-post-to-rule-them-all/
[4] https://hackaday.com/2015/12/09/embed-with-elliot-debounce-your-noisy-buttons-part-i/
[5] https://hackaday.com/2015/12/10/embed-with-elliot-debounce-your-noisy-buttons-part-ii/
[6] https://learn.adafruit.com/make-it-switch/debouncing
[7] https://github.com/tcleg/Button_Debouncer
[8] http://www.kennethkuhn.com/electronics/debounce.c
[9] Alistair Edwards, “How many ways can you use one button? Timing data for button presses,” 2002.
Comments
Comment is disabled to avoid unwanted discussions from 'localhost:1313' on your Disqus account...