Pages: [1]   Go Down
Author Topic: Elegant debouncing solution with software Schmitt trigger emulation  (Read 3581 times)
0 Members and 1 Guest are viewing this topic.
0
Offline Offline
Newbie
*
Karma: 2
Posts: 7
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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://www.edn.com/file/13370-70705di.pdf
http://web.engr.oregonstate.edu/~traylor/ece473/lectures/debounce.pdf ( see page 8 )

Here is a snippet of my code implementing the algorithm:

Code:
//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
  while(true){
     
    //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
              //somewhat
}

(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!
Logged

Global Moderator
Melbourne, Australia
Offline Offline
Brattain Member
*****
Karma: 511
Posts: 19367
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
Logged

http://www.gammon.com.au/electronics

Please post technical questions on the forum - not to me by personal message. Thanks a lot.

Offline Offline
Edison Member
*
Karma: 116
Posts: 2205
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I thought that was fairly complicated.

I would do something like this:

Code:
#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.
Logged

0
Offline Offline
Newbie
*
Karma: 2
Posts: 7
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.

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:

Code:
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  smiley-razz ), 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 http://www.ganssle.com/debouncing.htm.

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

Maryland, USA
Offline Offline
Jr. Member
**
Karma: 0
Posts: 79
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Cactusjack,
   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....


Logged

There are three kind of people in the world: Those who can count, and those who can't

Global Moderator
Melbourne, Australia
Offline Offline
Brattain Member
*****
Karma: 511
Posts: 19367
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

His comments largely explain:

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

In other words:

Code:
  boolean pinState = digitalRead (2);
Logged

http://www.gammon.com.au/electronics

Please post technical questions on the forum - not to me by personal message. Thanks a lot.

Pittsburgh, PA, USA
Offline Offline
Faraday Member
**
Karma: 99
Posts: 4837
I learn a bit every time I visit the forum.
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
http://www.arduino.cc/playground/Code/CapacitiveSensor

Logged

I find it harder to express logic in English than in Code.
Sometimes an example says more than many times as many words.

Offline Offline
Edison Member
*
Karma: 116
Posts: 2205
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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:

Code:
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.
Logged

Left Coast, CA (USA)
Offline Offline
Brattain Member
*****
Karma: 362
Posts: 17308
Measurement changes behavior
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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-grin
Lefty
Logged

Pages: [1]   Go Up
Jump to: