Debouncing a switch in software using a PIC16F72

Discussion in 'Embedded Systems and Microcontrollers' started by b1u3sf4n09, Aug 10, 2014.

  1. b1u3sf4n09

    Thread Starter Member

    May 23, 2014
    115
    14
    Intro: It has been a long time since I have worked with microcontrollers, and I was looking at getting back into it. So, naturally, I chose a project that is way beyond my skill level and started at it. I decided to break my project into parts and write them out into individual programs, that way I could test them individually and put it all together when it was working properly.

    Problem: I'm currently on the input side of the project, which is the simplest part, basically just two switches which need to be debounced in software to ensure no erratic behavior. The test program uses an array of values that will change the output port of the micro to indicate the successful debouncing of the switch by energizing an LED on that output.

    Initially, I wrote a for loop with a specific time delay to handle debouncing a single switch, and I wrote an interrupt to flash an LED to ensure the loop was not locking up my program. My initial code worked in debouncing the switch, but locked up my program. I changed it to a while loop, and included a boolean function to prevent multiple state changes from a single button press, but this has produced erratic behavior from my circuit. For example, the ISR LED will not begin flashing until I have pressed the switch, and then it will stop flashing after a few switch presses.

    Code ( (Unknown Language)):
    1.  
    2.  
    3. /******************************************************************************/
    4. /* User Global Variable Declaration                                           */
    5. /******************************************************************************/
    6.  
    7. unsigned char LED_Number = 0;                                  // 8-bit variable
    8. const unsigned char LED_LookupTable[3] = {0x01, 0x08, 0x80};   // LED array
    9. bool switchState = false;
    10. unsigned char debounceDelay = 20;
    11.  
    12. /******************************************************************************/
    13. /* Main Program                                                               */
    14. /******************************************************************************/
    15.  
    16. void main(void)
    17. {
    18.     /* Configure the oscillator for the device */
    19.     ConfigureOscillator();
    20.  
    21.     /* Initialize I/O and Peripherals for application */
    22.     InitApp();
    23.  
    24.     while(1)
    25.     {
    26.  
    27.         // use lookup table to energize one LED based on LED_Number value
    28.         PORTC = LED_LookupTable[LED_Number];
    29.  
    30.         if (RC6 == 0 && switchState == false)     // button press loop
    31.         {
    32.             uint8_t db_cnt = 0;
    33.            
    34.             LED_Number++;                            // rotate display by 1
    35.             if (LED_Number >= 3)
    36.             {
    37.                 LED_Number = 0;                      // go back to LED 0.
    38.             }
    39.  
    40.             while(db_cnt<=debounceDelay)             // button debounce loop
    41.             {
    42.                 wait_ms(1);
    43.                 db_cnt++;
    44.                 if (RC6 == 1)                        // if button press
    45.                 {
    46.                     db_cnt = 0;
    47.                 }
    48.             }
    49.             switchState = true;
    50.         }
    51.         if (RC6 == 1)
    52.         {
    53.             switchState = false;
    54.         }
    55.     }
    56.  
    57. }
    58.  
    59.  
    I have also attached the files in a .zip file for examination. Does anyone have any suggestions as to what I can do to fix the issues I am seeing.
     
  2. gabeNC

    New Member

    Sep 24, 2014
    3
    1
  3. JohnInTX

    Moderator

    Jun 26, 2012
    2,347
    1,029
    You have a potential lockup issue in the while loop, I think:
    Code (Text):
    1.             while(db_cnt<=debounceDelay)             // button debounce loop
    2.             {
    3.                 wait_ms(1);
    4.                 db_cnt++;
    5.                 if (RC6 == 1)                        // if button press
    6.                 {
    7.                     db_cnt = 0;
    8.                 }
    9.             }
    After entering, if the switch goes back open before db_cnt is reached and stays open, it will lock up here. You could mess with the code but main is not the place to do switch debouncing. My personal preference for a few buttons is to:
    1) Make a system tik using a timer interrupt. You have this already.
    2) On each system tik, one of its jobs is to sample the switch(es) and run the debounce there.
    3) PRESENT the results to the main code for sampling.

    This code snippet illustrates one implementation - I just grabbed it from an AAC thread I was working with a guy on and deleted project-specific stuff so it may not be compilable but here it is:
    Code (Text):
    1.         //-----DEBOUNCE 1 BUTTON-----//
    2.         //A COUNTER COUNTS UP AND DOWN BETWEEN 0 AND ButtonDebK.  THE CURRENT
    3.         //STATE OF THE BUTTON IS INSPECTED AND WE SEE IF IT ACCUMULATES TO
    4.         //THE OTHER STATE.  THE OUTPUTS OF THIS ARE THEN INSPECTED BY THE
    5.         //MAIN PROGRAM TO DETERMINE WHAT TO DO.
    6.         if(BUTTON_HELD == FALSE)        //BUTTON NOT HELD
    7.         {
    8.             if(buttonOPEN)              //NO BUTTON INPUT -buttonOPEN is an IO macro  i.e. #define buttonOPEN (RB6==1)
    9.             {
    10.                 if(ButtonDebK != 0)     //IF BUTTON ACCUMULATED TIKS
    11.                     ButtonDebK--;       //DECREMENT BUTTON COUNTER
    12.                 else                    //IF NO TIKS ON BUTTON COUNTER
    13.                     ;                   //DO NOTHING
    14.             }                           //END IF(BUTTONOPEN)
    15.             else                        //IF BUTTON IS PRESSED
    16.             {
    17.                 ButtonDebK++;           //INCREMENT BUTTON COUNTER
    18.                 if(ButtonDebK == BUTTON_DEBOUNCE_TIKS)  //COUNTER REACHES DEBOUNCED STATE
    19.                 {
    20.                     BUTTON_HELD = TRUE; //SET BUTTON PRESSED STATE TO TRUE
    21.                     BUTTON_REQUEST = TRUE; //SET ONCE PER PUSH/RELEASE
    22.  
    23.                 }                       //END DEBOUNCED STATE
    24.             }                           //END BUTTON CLOSED STATE
    25.         }                               //END BUTTON_HELD == FALSE
    26.         else                            //BUTTON IS CURRENTLY HELD
    27.         {
    28.             if(buttonCLOSED)            //BUTTON IS CURRENTLY PRESSED
    29.             {
    30.                 if(ButtonDebK < BUTTON_DEBOUNCE_TIKS)   //COUNTER HAS NOT REACHED DEBOUNCED
    31.                     ButtonDebK++;       //INCREMENT BUTTON COUNTER
    32.                 else                    //COUNTER HAS REACHED DEBOUNCED
    33.                     ;                   //DO NOTHING
    34.             }                           //END BUTTON PRESSED
    35.             else                        //BUTTON NOT CURRENTLY PRESSED
    36.             {
    37.                 ButtonDebK--;           //DECREMENT BUTTON COUNTER
    38.                 if(ButtonDebK == 0)     //WHEN BUTTON COUNTER IS 0
    39.                     BUTTON_HELD = FALSE;    //SET BUTTON AS FALSE
    40.             }           //ELSE BUTTON IS OPEN
    41.         }               //ELSE BUTTON_HELD
    The code is run every 1ms (or whatever is convenient). A 1 byte counter (for each button) increments if the switch is closed and decrements if the switch is open. Boundary conditions (zero for open, some number of tiks for closed) determine when the switch is debounced TRUE (k==some count) or FALSE (k==0). The switch is debounced both ways.

    Two conditions are indicated by flags:
    1) buttonHELD - TRUE when button is pushed and debounced, FALSE when debounced open. buttonHELD may be written only by the debouncer as it is used as a state indicator - it is read-only by rules.
    2) buttonREQUEST - optional. This is a flag that is set ONCE when the switch is first debounced TRUE. Its most useful in cases when one action per push is required and eliminates extra processing on main's part. When a REQUEST is 'used' by main, main resets the REQUEST flag.

    Something to take away from all of this (even if you don't use it) is that IO should be processed in lower levels and the results (debounced switch or ADC result) passed to the routines that use them. A producer->consumer paradigm relieves the main program logic of the details of how things actually work but preserving the logic of what they do.

    Hope this helps..
    Have fun!
     
    Last edited: Sep 24, 2014
    b1u3sf4n09 likes this.
  4. b1u3sf4n09

    Thread Starter Member

    May 23, 2014
    115
    14
    Thank you for not just offering a solution to my issue, but also giving suggestions for good coding practice. These are the sorts of things I want to keep stored in the memory banks.
     
  5. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    Question for JohnInTx, if you please?

    John, while that's a nice example for debouncing a single switch, how would you handle multiple switches?
     
  6. John P

    AAC Fanatic!

    Oct 14, 2008
    1,634
    224
    People go nutty over debounce routines, but I really think it's wasted effort. All you need to do is read the input(s) in a recurring interrupt at some fixed time interval, selecting a rate that's slower than the bouncing time of the switch can ever be. Maybe 30 times a second (33msec) would be a good choice. Very often, you want a fixed timer in your code anyway, so there's no need to add an extra interrupt; if the timer runs faster than the polling rate for the button(s), just use a counter, so if you had a 1msec timer and you wanted to read the buttons at 30 times a second, count to 33 and when the counter hits that level, do the test and reset the counter to 0.

    Alternatively, you can avoid using interrupts at all; just poll the rollover flag for an internal timer. When you find it set, clear it and read the buttons. This has problems if the loop that the program is executing is very long, but often it's good enough. The key point is not to read the buttons too frequently--and it's OK because nobody can push them very fast anyway.
     
  7. JohnInTX

    Moderator

    Jun 26, 2012
    2,347
    1,029
    Good question. I guess I don't have any hard fast rules but the implementations I've used fall into a few broad categories.
    For a few switches I just replicate the code. Crude (especially on the source level) but it doesn't compile to that much code space.

    For more, I've used one debounce engine that parses an array of structures in ROM whose elements contain the port,bit to sample, whether its active high or low, and a pointer to the debounce counter/flags byte in RAM (still one RAM per switch). I've also used an array of pointers to a bunch of small functions that sample the bit and manage the RAM either as a function return value or use a passed pointer to it.

    Which particular implementation is best depends on the compiler/processor and I look at the compiler output to see which source constructs make the best code. The most I've debounced with the replicate method was maybe 8-10 in a combination of panel buttons and machine sense/limit switches. Despite the sorry-looking source, it compiled to just a few btfsc/s and incf/decf and one RAM per switch so did the job and executed quickly. The one I did using the array of structures method was elegant at the source level but (for a PIC18) but the engine was bigger due to the indirects an parameter passing and took longer to execute per switch than the brute force method. I tried it as an experiment. It worked just fine but wasn't a next-level improvement at the execution level.

    There are lots of variations on the same theme. If you are hard-pressed for interrupt service time and you haven't bogged down MAIN with lots of dumb delays, you can just set a flag that says 'run one debounce tik' and let MAIN get around to it when it can.

    As I noted, the code posted above was done for a midrange PIC and was written off the cuff for instructional purposes. It likely could be improved both at the semantic and code generation levels. As shown it generates 29 instructions using XC8-PRO for 12F629. Not wonderful but not bad maybe? considering the functionality.

    I'd never say my method is the best or for every application but I use some implementation of it every time. I like the ability to sample a switch in one cycle (by polling the _HELD flag) and also being able to consume individual pushes via the _REQUEST flag). Most programs have many occasions to use switches like this and I like it to be on a lower level with an consistent interface rather than repeated ad-hoc approaches buried in the logic of the main program, which also can be code-intensive. Just my .03

    John P has an interesting approach to debouncing as well. Something to consider.
     
    Last edited: Oct 6, 2014
  8. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    JohnInTx,

    Thank you for the explanation. I asked because there are so many examples (simple to exotic) for debouncing a single switch while examples for multiple switches are rather lacking.

    Gannsel conflates switch contact bounce and electrical noise in his Switch Debounce document and promotes the use of a software low pass filter. Unfortunately, implementing a low pass filter for multiple switches often results in rather awkward and inelegant code. If speed, code size, and overhead are important, using an eight bit wide 2-bit vertical counter as independent low pass filters for up to eight switches can be handy . In this case a debounced latch is toggled only after sampling a switch at the opposite state four times spanning three intervals. An 8-msec interval would be adaquate for a 24-msec debounce period.

    Code (Text):
    1.  
    2. ;
    3. ;  Mike McLaren's version of Scott Dattalo's vertical counter demo'
    4. ;
    5. ;  input  ___-----_____------______-_-------_-________
    6. ;  latch  _______-----_____------________---------____
    7. ;  delta  ___----_----_----__----__-_----___-_----____
    8. ;
    9. ;  vcnt0  ---_-_--_-_--_-_---_-_---_-_-_----_-_-_-----
    10. ;  vcnt1  ----__---__---__----__------__-------__-----
    11. ;
    12. ;  delta  ______-____-____-_____-_______-________-____
    13. ;  flags  ______-----_____------________---------_____
    14. ;
    15. ;
    16. ;       delta = latch ^ ~portb;         // changes, press or release
    17. ;       vcnt0 &= delta; vcnt0 = ~vcnt0; // clear or count vc bit 0
    18. ;       vcnt1 &= delta; vcnt1 ^= vcnt0; // clear or count vc bit 1
    19. ;       delta &= vcnt0 & vcnt1;         // collect counter overflows
    20. ;       latch ^= delta;                 // update switch state latch
    21. ;       delta &= latch;                 // filter out 'release' bits
    22. ;       flags ^= delta;                 // toggle flag bits for main
    23. ;
    24.         comf    PORTB,W                 ; sample active lo switches
    25.         xorwf   latch,W                 ; changes, press or release
    26.         movwf   vmask                   ; save temporarily
    27.         andwf   vcnt0,F                 ; reset inactive counters
    28.         andwf   vcnt1,F                 ; reset inactive counters
    29.         comf    vcnt0,F                 ; bump vc0 unconditionally
    30.         movf    vcnt0,W                 ;
    31.         xorwf   vcnt1,F                 ; bump vc1 (conditionally)
    32.         movf    vmask,W                 ; changes, press or release
    33.         andwf   vcnt0,W                 ; collect counter overflows
    34.         andwf   vcnt1,W                 ;   "
    35.         xorwf   latch,F                 ; update switch state latch
    36.         andwf   latch,W                 ; filter out 'release' bits
    37.         xorwf   flags,F                 ; toggle flag bits for main
    38.  
    39.  

    The method Mike Szczys posted in the Debounce Code Collection on Hackaday also uses a 2-bit vertical counter. It's practically the same as the code listed above;

    Code (Text):
    1.  
    2. ISR(TIM0_OVF_vect)  // interrupt every 10ms
    3. {
    4.    static unsigned char ct0, ct1;
    5.    unsigned char i;
    6.  
    7. // TCNT0 is where TIMER0 starts counting. This calculates a value based on
    8. // the system clock speed that will cause the timer to reach an overflow
    9. // after exactly 10ms TCNT0 = (unsigned char)(signed short)-(((F_CPU / 1024) * .01) + 0.5);
    10. // preload for 10ms interrupts
    11.  
    12.    i = key_state ^ ~KEY_PIN;    // key changed ?
    13.    ct0 = ~( ct0 & i );          // reset or count ct0
    14.    ct1 = ct0 ^ (ct1 & i);       // reset or count ct1
    15.    i &= ct0 & ct1;              // count until roll over ?
    16.    key_state ^= i;              // then toggle debounced state
    17.    key_press |= key_state & i;  // 0->1: key press detect
    18. }
    19.  

    I've come to realize, like JohnP, that including a low pass filter in any debounce method is unnecessary in all but extremely noisy environments. That said, practically any debounce method that samples switches at some decent debounce interval (8-32 msecs) works well and allows the use of relatively simple parallel switch state logic for multiple switches.

    Code (Text):
    1.  
    2. /*
    3.  *  K8LH parallel switch logic (and "new press" filter)
    4.  *
    5.  *  swnew  ___----____----_____  invert active lo switches
    6.  *  swold  ____----____----____  switch state latch
    7.  *  swnew  ___-___-___-___-____  changes, press or release
    8.  *  swnew  ___-_______-________  filter out 'release' bits
    9.  *  flags  ____--------________  toggle flag bits for main
    10.  */
    11.     swnew = ~portb;           // sample active lo switches
    12.     swnew ^= swold;           // changes, press or release
    13.     swold ^= swnew;           // update switch state latch
    14.     swnew &= swold;           // filter out 'release' bits
    15.     flags ^= swnew;           // toggle flag bits for main
    16.  
    17.  
    Code (Text):
    1.  
    2. ;
    3. ;  swnew  ___----____----_______  invert active lo switches
    4. ;  swold  ____----____----______  switch state latch
    5. ;  swnew  ___-___-___-___-______  changes, press or release
    6. ;  swnew  ___-_______-__________  filter out 'release' bits
    7. ;  flags  ____--------__________  toggle flag bits for main
    8. ;
    9.         comf    PORTB,W         ; sample active lo switches
    10.         xorwf   swold,W         ; changes, press or release
    11.         xorwf   swold,F         ; update switch state latch
    12.         andwf   swold,W         ; filter out 'release' bits
    13.         xorwf   flags,F         ; toggle flag bits for main
    14.  
    15.  
     
    Last edited: Dec 11, 2015
  9. John P

    AAC Fanatic!

    Oct 14, 2008
    1,634
    224
    Well, there you have it. We don't need to read a switch 10000 times a second just because we can! In fact if we opt for very rapid reading, it just gives us problems that we need to solve.
     
  10. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,394
    1,606
    I've never found reason to make a debounce routine any more complicated then seeing if a button (or group of buttons) have the same pattern over two subsequent readings. The separation between readings should be greater then the longest bounce, but short enough to keep the functionality lively. Nothing is more frustrating then pressing a button (however briefly) and having nothing happen. I have found 25 mS works well.

    The details of how to debounce buttons also depends on the overall architecture of the program. My projects tend to have 5 buttons and my preference is to do debouncing in an ISR and "publish" the key states in a global variable.

    Why 5 buttons? I arrange them in a pattern to suggest UP/DOWN/LEFT/RIGHT with an ENTER in the center. I find this convenient when working with an LCD display to walk a menu chain where either a sub menu or parameter is displayed, selected and modified. With the button state determined in an ISR the main code needs just test the current state. While buttons may be spread over several input ports it is more convenient to lump them into the same port, it is just less processing as one does not have to rearrange them into one variable.

    I make the ISR triggered off a timer with a 1 ms period, which gives a useful time tick. At 25 loops the buttons are tested. If the current reading matches the last reading the global key state is updated, and each time the current reading replaces the last reading.

    Here is a sample ISR. Note while taken from a working program this is untested code as several other functions have been removed:

    Code (Text):
    1.  
    2. //global
    3. unsigned char Keys = 0;          // initially celar all buttons
    4. int ticks = 0;                   // time heartbeat
    5. #define BUTTON_MASK 0b01001111   // mask for our 5 buttons
    6. #define KEY_DELAY 25             // gives 25mS delay between readings
    7.  
    8. #pragma interruptlow Heartbeat
    9. void Heartbeat (void)
    10. {
    11.   // low priority ISR
    12.   // Heartbeat is the Timer2 match handler
    13.   // it is called every 1 mS
    14.  
    15.   static unsigned char LastKeys = 0xFF;
    16.   unsigned char RawKeys;
    17.  
    18.   if (PIR1bits.TMR2IF)
    19.   {
    20.     ticks++;                  // update heartbeat
    21.     PIR1bits.TMR2IF = 0;      // cleat ISR flag
    22.     if (KeyDelay++ >= KEY_DELAY)
    23.     {
    24.       KeyDelay = 1;                        // reset after a scan
    25.       RawKeys = DATA_PORT & BUTTON_MASK;   // read current keys & mask out any unused pins
    26.       if (LastKeys != RawKeys)
    27.       {
    28.         // we have a new key pressed, save it to test when it is stable
    29.         LastKeys = RawKeys;
    30.       }
    31.       else    //   (LastKeys == RawKeys) for new button state
    32.       {
    33.         // we have a new stable pattern
    34.         Keys = RawKeys;  // copy new stable pattern into global
    35.       }
    36.     }
    37.   }
    38. }
    39.  
     
  11. MMcLaren

    Well-Known Member

    Feb 14, 2010
    759
    116
    Hi ErnieM,

    Thank you for the example but I'm curious why you believe that checking for two consecutive samples at the same state is necessary, especially when you've chosen a sample interval that you believe to be longer than the maximum switch bounce time?

    Code (Text):
    1.  
    2.       if (LastKeys != RawKeys)
    3.       {
    4.         // we have a new key pressed, save it to test when it is stable
    5.         LastKeys = RawKeys;
    6.       }
    7.       else // (LastKeys == RawKeys) for new button state
    8.       {
    9.         // we have a new stable pattern
    10.         Keys = RawKeys; // copy new stable pattern into global
    11.       }
    12.  

    Let's say you press a switch 24-msecs into the interrupt interval and you sample it 1-msec later in the interrupt routine. If you sample it while it's bouncing you may or may not detect a change in state, however, it will certainly be at a stable state when you sample it again in another 25-msecs. In effect you're able to detect the switch state change either on the first or the second sample or within 1-msec or 26-msecs after you've pressed the switch. Your double check code on the other hand would produce a key press flag either 26-msecs or 51-msecs after you pressed the switch. The code literally samples the switch a second (or third) time when it's at a known stable state. Depending on what you're doing, a 51-msec response time may seem sluggish.

    Is there an advantage to the double-check that I may be missing? Or, is this just a simplified low pass (noise) filter?

    Also, it seems you're simply passing debounced switch state data to main. How do you differentiate a "new press" state from a "still pressed" state in main? In other words how do you prevent code in main from processing a single switch press multiple times?

    Cheerful regards, Mike
     
    Last edited: Oct 7, 2014
  12. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,394
    1,606
    The double check IS the low pass filter. Personally I find the 25mS time acceptable, I've never been able to fool the computer using standard tactile switches.

    I don't do anything beyond checking if there are no buttons or one (or more) buttons down. The used could push more then 1 button down and may get some funny results, but that's what you get for messing with the machine <grin>. They really are not too serious as the processor responds to the first button and hangs till all are released.

    In my menu driven system the code is typically waiting for a button to be pressed, and occasionally waiting for one to be released. This is done with just a pair of while loops:

    Code (Text):
    1.  
    2.   while(!Keys);     // wait for key to be pressed
    3.   // now we have a press, decode it
    4.   switch (Keys)
    5.   Case Button_A
    6.     {do something}
    7.     break;
    8.   Case Button_B
    9.     {do something else}
    10.     break;
    11.   while(Keys);   // wait for key to be released
    12.  
    That's the basics. Again, my code is too interleaved to show, as all the menus are drawn in a function along with button presses, and even the while loop is done in a function that also calls other tasks (it keeps the USB interface alive for one).
     
Loading...