What’s Software Debouncing, Anyway?

When a switch is pressed or released, its internal metal contacts do not transition cleanly between states, instead, they rapidly oscillate—making and breaking contact multiple times within milliseconds—before settling into a stable position. This phenomenon, known as bouncing, can cause microcontrollers to misinterpret a single press as multiple inputs, leading to erratic or unintended behavior.

The following pictures clearly demonstrate this phenomenon:

ideal switch
Expected behavior
bouncy switch
Actual behavior

The switch may continue to chatter for several milliseconds before it finally settles.

There are several hardware techniques that effectively eliminate switch bouncing, including SR latches and RC circuits combined with Schmitt trigger buffers. These solutions work wonderfully, ensuring a single clean transition with each button press. Jack Ganssle has written an excellent article that dives deep into these implementations.

The main drawback, however, is the need for additional components and the precious PCB real estate they consume.

The software method for handling bouncing is what we refer to as software debouncing which is the cost-effective alternative to hardware-based solutions.


The Software Debouncing Approach

The concept of software debouncing is quite straightforward. You read the input, wait a short period to allow any bouncing to settle, then read it again and compare. If the second reading matches the first, you accept it as valid. If not, you discard it as noise.

The code could be as simple as this:

            
        #include <stdbool.h>

        bool GPIO_ReadPin(const volatile uint8_t* port, const uint8_t pin_mask) {
            return ((*port & pin_mask) != 0) ? true : false;
        }

        void main(void){
            SYSTEM_Initialize();
            while(1){
                bool state=GPIO_ReadPin(&PORTB, _PORTB_RB1_MASK);
                __delay_ms(50);
                bool new_state=GPIO_ReadPin(&PORTB, _PORTB_RB1_MASK);
                
                if (new_state==state) state=new_state;
                
                if (state){
                    //button is pressed
                }

            }
        }
            
        

Let’s polish it a bit and make the code more modular:

button.h

            
        #include <stdbool.h>
        #include <stdint.h>
        
        #define DEBOUNCE_TIME_MS 50

        typedef struct {
            const volatile uint8_t* port;
            const uint8_t pin_mask;
            bool state;
        } button_t;

        void BUTTON_Update(button_t* btn);
        bool BUTTON_isPressed(const button_t *btn);
            
        

button.c

            
        #include <stdbool.h>
        #include <stdint.h>
        #include <button.h>

        static bool GPIO_ReadPin(const volatile uint8_t* port, const uint8_t pin_mask) {
            return ((*port & pin_mask) != 0) ? true : false;
        }

        void BUTTON_Update(button_t* btn) {
            bool state = GPIO_ReadPin(btn->port, btn->pin_mask);
            
            __delay_ms(DEBOUNCE_TIME_MS);
            
            bool new_state = GPIO_ReadPin(btn->port, btn->pin_mask);
            
            if (state == new_state) {
                btn->state = new_state;
            }
            
        }

        bool BUTTON_isPressed(const button_t *btn){
            return btn->state;
        }

            
        

 

main.c

            
        #include <stdbool.h>
        #include <button.h>

        button_t buttons[] = {
            {&PORTB, _PORTB_RB1_MASK, false}, 
            {&PORTB, _PORTB_RB2_MASK, false}
        };

        void main(void){
            SYSTEM_Initialize();

            button_t *btn1=&buttons[0];
            button_t *btn2=&buttons[1];

            while (1) {
                BUTTON_Update(btn1);
                BUTTON_Update(btn2);


                if (BUTTON_isPressed(btn1)) {
                    // Button 1 is pressed
                }

                if (BUTTON_isPressed(btn2)) {
                    // Button 2 is pressed
                }

            }

        }
            
        

 

Although this works fine, it has a major flaw: The blocking nature of the delay function limits system responsiveness.

By leveraging a hardware timer that ticks at regular intervals, typically every 1 ms, you can track elapsed time without halting program execution, thus allow the microcontroller to remain responsive.

Instead of waiting inside a delay loop, you store the timestamp of the last detected change in button state. If the raw input remains stable for a predefined debounce period (e.g. 50 ms), the new state is accepted as valid.

This approach ensures that transient noise is filtered out while maintaining system responsiveness at just the cost of the interrupt initialization overhead. Just ensure that the timer is configured with the appropriate prescaler so that it generates interrupts every 1 ms.

button.h

            
        #include <stdbool.h>
        #include <stdint.h>
        #define DEBOUNCE_TICKS 50

        typedef struct {
            volatile const uint8_t* port;
            const uint8_t pin_mask;
            bool state;               
            uint8_t debounce_counter; 
        } button_t;

        void BUTTON_Update(button_t* btn);
        bool BUTTON_isPressed(const button_t *btn);
            
        

button.c

            
        #include <stdbool.h>
        #include <stdint.h>
        #include "button.h"
        
        extern volatile uint8_t timer_ticks;
        
        static bool GPIO_ReadPin(const volatile uint8_t* port, const uint8_t pin_mask) {
            return ((*port & pin_mask) != 0);
        }

        void BUTTON_Update(button_t* btn) {
            bool raw_state = GPIO_ReadPin(btn->port, btn->pin_mask);

            if (raw_state != btn->state) {
                if ((uint8_t)(timer_ticks - btn->debounce_counter) >= DEBOUNCE_TICKS) {
                    btn->state = raw_state;
                }
            } else {
                btn->debounce_counter = timer_ticks;
            }

        }

        bool BUTTON_isPressed(const button_t *btn){
            return btn->state;
        }

            
        

 

main.c

            
        #include <stdint.h>
        #include <stdbool.h>
        #include "button.h"

        volatile uint8_t timer_ticks;

        button_t buttons[] = {
            {&PORTB, _PORTB_RB1_MASK, false, 0 }, 
            {&PORTB, _PORTB_RB2_MASK, false, 0 }
        };

        //Supposing a PIC 18F MCU
        void __interrupt() ISR(void) {
            if(PIE3bits.TMR0IE == 1 && PIR3bits.TMR0IF == 1)
            {
                TMR0_ISR();
                TMR0_Reload();
                PIR3bits.TMR0IF = 0;
            }            
        }

        void main(void){
            SYSTEM_Initialize();
            INTERRUPT_GlobalInterruptEnable();

            button_t *btn1=&buttons[0];
            button_t *btn2=&buttons[1];

            while (1) {
                BUTTON_Update(btn1);
                BUTTON_Update(btn2);


                if (BUTTON_isPressed(btn1)) {
                    // Button 1 is pressed
                }

                if (BUTTON_isPressed(btn2)) {
                    // Button 2 is pressed
                }

            }

        }