Elegant debouncing solution with software Schmitt trigger emulation

I am working on a project that requires me to listen to a series of very brief, very quick button presses and to decode these button press events depending on the timing between them. It has nothing to do with Morse code, but the encoding is similar: there are three groups of button presses, presses within each group are separated by 100-300 ms, groups are separated by 300-1000 ms, and a pause of >1000 ms signifies the end of the current encoding batch.

As I suspected, the tactile micro-buttons I am using are rather bouncy / noisy. I am extremely space-limited on this project and wished to avoid implementing a hardware debounce solution. So I began experimenting with software debounce algorithms. I tried various polling algorithms and interrupt algorithms, but ultimately none of these worked because it turns out that while the vast majority of my button presses generate bounce lasting less than 90 ms, sometimes the bounce lasts rather substantially more than 100 ms, and handling any bounce longer than 100 ms with a timing algorithm (whether polling- or interrupt-based) would conflict with my requirement for 100 ms resolution on the individual button presses.

Just as I was about to revisit my enclosure / packaging plan to see how I could possibly fit an RC circuit and a Schmitt trigger in there, I ran into a couple of websites describing a debouncing algorithm that emulates an RC circuit and a Schmitt trigger using only a digital pin for input.

The algorithm is a hybrid count- and time-based debouncer, and includes hysteresis -- just like a real Schmitt trigger. Here are the links to the two articles I found:

http://web.engr.oregonstate.edu/~traylor/ece473/lectures/debounce.pdf ( see page 8 )

Here is a snippet of my code implementing the algorithm:

//This is within my 'void loop()' function//

//Variables for RC-Schmitt Trigger emulator debounce algorithm
uint8_t y_old=0, temp=0;
boolean buttonPressed = false;

  //Button polling loop
    //Grab pin state
    boolean pinState = PIND & ( 1 << 2 ); //Reading pin state  directly off register (pin PD2 / digital 2)
    pinState = !pinState; // The button grounds the pin, which is otherwise pulled high, so we flip it
                          // to achieve TRUE = buttonpress

    //This is the "RC circuit" part of the emulator, a recursive low-pass filter of recent pin states
    //Equation: y_old = (pinState * 0.25) + (y_old * 0.75)
    //First we get (y_old * 0.75)
    temp = (y_old >> 2);     //Bitwise operation for (y_old / 4)
    y_old -= temp;           // y_old - (y_old/4) is equivalent to (y_old * 0.75)
    //Second we add (pinState * 0.25) to y_old, but only if pinState is 1 (since otherwise the product is 0)
        //Note that we are actually adding 63 rather than (1*0.25). This is because we are using a byte
        //to represent the pin state in the equation for the purpose of having sufficient resolution to 
        //later apply the hysteresis of the virtual Schmitt trigger. 63 is the integer result of multiplying
        //255 (maximum 8-bit number) by 0.25. 
    if(pinState) y_old += 0x3f;  //0x3f is hex for 63

    //This is the Schmitt trigger part of the algorithm. Note the hysteresis -- different thresholds for 
    //detecting button-down and button-up 
        //Thresholds are 240 (for a valid press) and 15 (for a valid release) [out of 255]
        //(These may be tuned to the particular switch/button being debounced)   
    if( (y_old > 0xf0) && (!buttonPressed) ){  //This means a real press-down has occurred
        buttonPressed = true;
        keypresses++; //For debugging, I tracked the number of detected keypresses to compare against what
                      //my finger was telling me
        PORTD |= ( 1 << 7);  //This lights an LED connected to PD7, or Arduino digital port 7
    else if( (y_old < 0x0f) && (buttonPressed) ){  //This means a real release has occurred
        buttonPressed = false;
        PORTD &= ~(1 << 7); //This turns off the LED connected to PD7 (Arduino digital port 7)
    delay(4); //The algorithm specifies that the above routine be called by a timer interrupt every 
              //4-5 ms. With the default Schmitt thresholds of 0x0f (15) and 0xf0 (240), a delay of 4 ms
              //here results in a debouncing period of approximately 50 ms (it is not exact because
              //this is not a pure timing algorithm, so depending on actual bounce, the period may vary

(For the above code to work, pin 7 (PD7) must be set to output, pin 2 (PD2) to input, and pin 2 (PD2) must be pulled up via an external or internal resistor.)

Now, I don't know how closely this code actually emulates an analog RC/Schmitt debouncing circuit, but it actually works. It works extremely well. For my scenario of multiple quick button presses, it works where all other algorithms I've tried have failed.

Hope this helps someone out!

That looks interesting, thanks!

What I have done in the past is debounce by waiting for 10 mS from the last bounce. That is, after the switch is pressed you remember the time. If another bounce happens you reset the timer (that is, remember the new time). Once 10 mS elapses and no further bounces then you consider the bouncing over. This gives a quick recovery from a short bounce, but guarantees that you will properly handle multiple bounces.

I thought that was fairly complicated.

I would do something like this:

#define BUTTON_PRESSED 200
#define BUTTON_PORT (~PIND) //active low
//reads a button
//active high
void button_read(unsigned char button) {
  static unsigned char tmp=0;

  tmp = (tmp >> 1) + (((BUTTON_PORT) & button)?(0xff>>1):0);
  if (tmp >= BUTTON_PRESSED) {tmp = 0; return button;}
  else {return 0;}

//user code
  if (button_read(1<<0)) //if button pd.0 is pressed
  //dome something

BUTTON_PORT controls which port the button is attached. BUTTON_PRESSED is the threshold value. The higher BUTTON_PRESSED, the more consecutive button presses it will require to trigger that function.

A simpler delay-based algorithm like the one you describe works well if button presses are relatively isolated in time (if the time interval between button presses will be longer than the longest possible bounce period).

But in my case, I needed to detect button presses that may come only 100-150 ms from one another. With my tactile button, the duration of the bouncing was quite variable, with some (very few, but some nonetheless) button presses generating noise for more than 100 ms. Since that exceeds the resolution with which I must detect button presses, delay-based algorithms did not work for me.

The benefit of an RC / Schmitt trigger solution (whether hardware or software-emulated) is that it constantly smooths out the state change information coming from the pin rather than imposing a fixed "blackout" period where the pin is ignored for a certain amount of time or, even less usefully, for a fixed number of state changes. The smoothing is accomplished by the RC circuit in a hardware solution, or by the following recursive algorithm in the software solution:

filteredPinstate = (newPinstate * 0.25) + (filteredPinstate * 0.75);

The algorithm only recognizes a true pin state change if the filteredPinstate rises (or falls) to a certain threshold (there is hysteresis, so there are different thresholds for rising and falling). Because this threshold is less than a logical 1 for a valid 1 to be registered, and more than a logical 0 for a 0 to be registered, the algorithm is able to recognize a true pin state change even if the pin state is still bouncing slightly.

In other words, a pin state change is recognized not when bouncing ceases completely, but when the pin BEGINS to settle down (and based on both experience and research, buttons are mostly settled within 20 or so milliseconds after a button press, with only occasional spikes occurring up to 150-200 ms after that). Thanks to the hysteresis, this does not result in an error if the button is re-pressed while some bouncing from the prior button press is going on -- so long as the bouncing from the prior button press has settled down sufficiently for the algorithm to recognize a valid button press (this is where the Schmitt trigger thresholds, 240 and 15 in my code, can be tuned).

This theory works in practice. I tried out a number of different momentary buttons (OK, I only had three different tactile switches on hand :stuck_out_tongue: ), and when the requirement is to be able to recognize button presses that are as little as 100-150 ms apart, this algorithm works flawlessly (so far). I have tried at least a dozen various delay-based or counter-based debounce solutions, and not a single one was able to eliminate all bounce while still recognizing all button presses.

I thought that was fairly complicated.

My code was heavily commented and broken down to make it easier to read. The operative portion is only about 10 lines of code. And the moderate complexity of the code yields a result that your code does not -- functional debouncing in the scenario I describe above.

In any event, it's never a good idea to debounce solely based on count of pin state changes without some sort of timeout also being implemented, especially when using polling rather than interrupts for detecting button presses, because some button presses are clean and do not generate ANY bouncing, whereas other presses of the same button can generate 2, 10, or 300 (or any arbitrary number) of false state changes. See Debouncing Contacts and Switches.

Imposing a "blackout" based on the number of pin state changes will therefore miss many button presses.

Thank you for taking the time to show it to us.
This looks very useful indeed. The comments in the code are alifesaver.

I noticed that you use direct port access and a lot of bitwise operations and integer math to speed up execution.
I wonder if you could "translate" those instructions in common code (for lack of a better term) to help newbees like me understand their operations (not actually to be replaced as that would nullify the sped advantage)

Also, how would you use it in afuncion to detect button press and release (and act on them) after the algorithm has done its magic (i would't go so far as asking for a "led blink" example using your algorithm...though I think I just did...)?

You are using it wih three buttons, but i presume this can be used with any number of buttons.
Can the algorithm detect multiple simultaneus button presses, e.g. that button 1 & 3 have been pressesd and then button 1 (but not 3) released?

Finally, why do you need a while(true) statement in the loop() portion?

Sorry if these questions look naive....

His comments largely explain:

    boolean pinState = PIND & ( 1 << 2 ); //Reading pin state  directly off register (pin PD2 / digital 2)

In other words:

  boolean pinState = digitalRead (2);

If the button itself is bouncing for over 100 ms then I fail to see how software will shorten that.

OTOH I can see how software could watch the data and say, bounce or not, it was pressed.

To that end I think that the length of false positives is more critical. If I keep polling the pin to see a continuous (in this case) LOW state for 2x longer than a false positive then I have a press. The thing is that with that method I would need to check for button let go before testing for the next press.

If I had such crappy buttons and such a close interval to hold, I would use different buttons.

I've done this one before or one very like it. It can register touch in under 20 micros, IIRC.

An alternative, in my view a better solution, is to read the pin, and accumulate the value internally until a predetermined threshold has been reached - you return a positive indicated a key has been pressed.

Something like this:

unsigned char button_read(unsigned char button) {
  static unsigned char button_accumulator = 0; //accumulate button reads
  if (digitalRead(button) == BUTTON_PRESSED) {
    button_accumulator += 1; //increment accumulator
    if (button_accumulator >= BUTTON_THRESHOLD) {
      button_accumulator = 0; //reset button_accumulator
      return 1;  //indicating that button has been pressed
  } else {
    button_accumulator = 0; //reset button accumulator
    return 0; //no button has been pressed

So it will return a 1 if successive reads are all BUTTON_PRESSED, indicating a valid button press.

I recall my favourite switch de-bouncing circuit I used a lot with discrete 7400 logic chip designs in the 70s to 90s. I would always use a SPDT switch (either momentary or toggle or slide as the application required). I would ground the common switch terminal and wire the normally open and normally closed contacts to the direct set and reset input pins of a 7474 flip-flop chip. I could then choose either the Q or Q-bar output as required as perfectly de-bounced signals, always worked perfect.

I guess a arduino function could emulate that if wired from a SPDT to two input pins? The key is that a mechanical SPDT switch can't have contact bounce happening on both terminals at the same time, so even a bouncing value means that there will be a valid change soon and no need to wait for it to settle out. If that makes sense? not sure I can work out the equievelent SPDT + flip-flop in software, but maybe someone here can? :smiley:

I stumbled upon your post searching exactely for what you presented: software-based Schmitt-Trigger.

I was always quite dissatisfied with timer-based debouncing algorithms, mostly because of the "blackout" period you mention.

I think the conceptual power of your strategy is to treat button state not as an idealized, digital, time-discrete phenomenon, but rather as a continuous phenomenon, both in state (electrical resistance ranging between zero and infinite, in a rather ill-conditioned way due to bounce) as in time.

This continous model depends on two premises: our polling should have enough time resolution (sheer processor speed should be more than enough for non-blocking code), and "vertical" resolution should be also enough, that is, variation in "state" - between zero and one - should not be too quantized, it should have enough representation bits.

Once the phenomenon is modeled, it requires only two well-chosen parameters: histeresis (that is, how far apart are the positive and negative thresholds), and slope (that is, how smoothing the filter is.

For the first order recursive filter presented, that could be achieved the following way, for example:

float factor = 0.25; // could be way lower, say 0.01
old_value = new_value*factor + old_value*(1-factor); // note the "1 - fraction" part, ensuring it sums up to 1

I would tend to do the low-pass filter like so:

uint_8 filter_sum;

loop() {
    // filter decay: new value = old value * .75 + signal * .25

    filter_sum = (filter_sum>>1) + (filter_sum>>2); // filter_sum * 0.75
    if((PIND & ( 1 << 2 )) == 0) {
        // 0x41 is 0xFF - (0x7f + 0x3f). 
        filter_sum += 0x41; // + pin * 0.25

No need to use temp variables for complex expressions: let the compiler work out how it's best done.

I also suspect that there's no need to bit-fiddle PIND when you are just reading/writing one bit, it's likely that digitalRead() is a macro or an inline function that does the same thing.

Apart from those quibbles, the underlying method is really cool.

I have simpler version of this method, where I just load the bits into a byte and treat an 'up' as the byte being FF and 'down as it being '00': waiting for the bytes to settle. I don't know how this would differ, but I suspect that your method is quicker to 'see' a button change when the button is not noisy, and is capable of dealing with a signal that is continuously noisy (one bit in four noisy), whereas mine would wait forever for a clean run of 8 bits.

Another possibility suggests itself with respect to hysteresis. What would be the effect of feeding back the previous button state? That is, if the button was down last time through the loop, add that into the low-pass filter? Say: filter = .5 * filter + .25 * signal + .25 * previous_value; ? Would this make any difference at all - is this already managed by the action of the filter itself? Or would it add hysteresis in such a way that the different threshold values become unnecessary?

The main problem with this bit of code is the use of delay(). In a similar vein, it might be nice if it would handle being called at irregular intervals.


I'm currently looking at the behavior of this algorithm, as far as I can make it out.

presses within each group are separated by 100-300 ms, groups are separated by 300-1000 ms, and a pause of >1000 ms signifies the end of the current encoding batch.

So, let's take a sequence of 1,2,3 presses, each press being 200 ms (50 samples) and separated by 200ms (50 samples), the groups being separated by 400ms (100 samples).

To simulate noise of up to 100ms (25 samples), if the button is not pressed, then the signal will chosen randomly from the last 25 "real" button states.

Now an obvious thing to see here is that if it is the case that the switch isn't bouncy when you press it down, only when you release it, then you can get a much better envelope with asymmetrical bounds on your schmitt trigger.

We can see in the graph that once the signal drops to .5, it won't rise to 15/16 again (a.k.a 0xf0). This gives us a tighter envelope.

I raised that bound to .75 before I started seeing noise in the output

As always, it all depends on exactly what is going on. If the bounciness is different to the bounciness simulated here, YMMV.

Ok! I have implemented a debounce that seems to work well using this method, and postred it over in the exhibition area. See Software debounce ~ PaulMurrayCbr.

-- EDIT --

Drat! I had a programming error: the filter was not being driven off the noisy input, so no wonder it seemed to work so neatly.

I'll go back and redo this :frowning:

Ok. I made an attempt at a software debounce, but I was feeding the raw button signal into the filter rather than the noiseified one, so it looked much better than it actually was.

With the fllter actually attempting to filter noise, it behaved poorly.

Looking at the way the filter output behaves, it seems to me that it's exactly wrong. When the output is high, a low bit has a dramatic effect on the next state - much more so than when the state is low. This is kinda backwards. What we need is for the curve to be stickier.

A simple curve that's more sticky is simply a linear count of the most recent bits - a rolling average of the last N samples. This, combined with hysteresis, will I think give a better result.

A word has 16 bits. The count of bits in a word is four bits long. So it seems that we can only count 7 samples and have enough room for the history, the rolling average, and the hysteresis bit.

So I can put the rolling average in the high nybble, the hysteresis bit somewhere in the next nybble, and store the most recent 7 samples in the low byte of the word.

The algorithm t maintain a rolling average on receiving a data bit is:

left-shift the bottom byte by 1.
put the data bit in bit 0.
if bit 7 is high, subtract 1 from the top nybble.
if bit 0 is high, add 1 to the top nybble.

So given a 'state' variable and a 'data' bit:

word state;
boolean data;

state = (state & 0xFF00) | ( (state & 0x007f)<<1 ) | data;
state = state - ((state&0x0080)<<5) + ((state&0x0001)<<12);

we then do hysteresis. Let's say that if 5 of the most recent 7 bits indicate that the state should change, change the state.

state = (state & ~0x0100 ) | (state > (state & 0x0100 ? 0x2000 : 0x4000) ? 0x0100 : 0);

I'll code it up tomorrow and see what the result looks like.

Ok, sorry about the rubbish posts - I see they have been deleted.

I have implemented this, with some tweaks, and uploaded a video to youtube. See post Software Debounce - Exhibition / Gallery - Arduino Forum for the code.

The result manages to detect button clicks even when 25% of the samples are noise. Pretty good, I think.

-- EDIT --

I have epoxied this into a library. See DebounceInput @github.