Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 11

Software Debouncing for Mechanical Buttons

Recommended resources

Hello! Welcome back to our course on mastering the STM32.

In our last lesson, you successfully configured a GPIO pin as an input with an internal pull-up resistor and wrote code to control an LED with a button. As you may have noticed, however, mechanical buttons aren't perfect. Pressing and releasing the button might have caused the LED to flicker erratically. This real-world imperfection is called contact bounce.

Today, we will address this head-on. Our learning outcome is to implement a software debouncing algorithm for a mechanical button. This is a critical skill for any embedded engineer, ensuring that your system responds reliably to user input—a non-negotiable requirement in fields like automotive firmware.

We'll start by understanding the problem, explore a common but flawed solution, and then implement a robust, professional-grade debouncing algorithm using the SysTick timer we've already set up.

1. Understanding Contact Bounce

When you press a mechanical button, the metal contacts inside don't make a single, clean connection. Instead, they bounce against each other for a few microseconds or even milliseconds before settling into a stable state. From the microcontroller's perspective, which can read the pin state millions of times per second, a single press can look like dozens of rapid on-off-on transitions.

Oscilloscope Trace of a Bouncing Button Signal
This oscilloscope trace shows what a single button press looks like to a microcontroller. The yellow line is the voltage at the GPIO pin. Notice the chaotic "bouncing" as the signal transitions from HIGH to LOW and back. Our job is to filter this noise and register only one event.

To get a deeper understanding of this physical phenomenon and the various ways to solve it, the following article is an excellent starting point.

STM32 Button Debounce Code Examples [Tutorial]

The article 'STM32 Button Debounce Code Examples' from DeepBlue Embedded provides a great overview of the problem and various software solutions.

Please read the first two main sections: STM32 Button Debounce Preface: This section explains the concept of switch bounce with clear oscilloscope screenshots. STM32 Button Debouncing Techniques: This section categorizes the solutions into hardware and software, giving you a map of the different approaches. We will be focusing on the software techniques.


2. A Simple (but Flawed) Approach: Blocking Delays

The most intuitive way to debounce a button is to detect a change, wait a short period for the bouncing to stop, and then check the button's state again.

Software Debouncing Methods Flowcharts
This image shows two debouncing methods. Method 1 (left) illustrates the simple delay-based approach. We read the state, wait, and read again. While simple, it has a major drawback.

Let's look at what this would look like in code. In the while(1) loop, we could do something like this (this is for conceptual understanding, do not add this to your project):

// Conceptual pseudo-code for a bad debouncing method
if (button_is_pressed()) {
    delay_ms(20); // Wait for 20ms
    if (button_is_pressed()) {
        // Debounce successful, toggle the LED
        toggle_led();
        // Wait for button release
        while(button_is_pressed());
    }
}

The problem here is the call to delay_ms(20). This is a blocking delay. While the microcontroller is stuck in this delay, it can't do anything else—it can't read other sensors, communicate on a bus, or update other outputs. In a professional application, especially an automotive one where a missed CAN message or a delayed sensor reading could be critical, this approach is unacceptable.


3. A Professional Approach: Non-Blocking, Time-Based Debouncing

A robust solution must be non-blocking. We need to check the button's state periodically without halting the entire system. We can achieve this by using our SysTick timer as a time reference.

The core idea of this algorithm is to act like a state machine:

  1. Read the button's current state.
  2. If the current state is different from the previously recorded state, it might be the start of a bounce or a genuine press. We record the current time from our SysTick counter.
  3. We only accept the new state as "stable" if it has remained unchanged for a specific duration (e.g., 20-50 milliseconds).
  4. Once the state is confirmed as stable, we can then act upon it (like toggling an LED).

For a deep dive into the theory and best practices behind such an algorithm, Jack Ganssle's "A Guide to Debouncing" is a foundational text in the industry.

A Guide to Debouncing

This guide by Jack Ganssle is one of the most respected resources on debouncing. It provides empirical data and professional insight into designing reliable debounce routines.

Please read the following sections: Software Debouncers (starts on page 17): Focus on the 'Debounce Policy' subsection. It outlines the professional rules for creating a good debouncer (e.g., minimizing CPU overhead, ensuring quick response). A Counting Algorithm (starts on page 19): Study 'Listing 1'. This C code provides a robust, time-based algorithm that debounces both the press and release events. This is the model for what we will build. Summing up (page 22): This provides excellent practical recommendations for timer rates and debounce periods.


4. Hands-On Task: Implementing a Non-Blocking Debouncer

Now, let's refactor the button-reading code from our previous lesson to include a proper, non-blocking debouncing algorithm. We will toggle the LED on each debounced button press.

Hardware Setup:
Your hardware setup should be the same as the last lesson:

  • The onboard LED is on pin PC13.
  • A push-button is connected between pin PA0 and GND.

Software Implementation:

We will modify your existing main.c file. Ensure your system clock and SysTick timer (generating a 1ms interrupt that increments a variable) are already configured from our previous lessons.

1. Add New Global Variables:
At the top of your main.c, near your other global variables (like the SysTick counter), add the variables needed for our debouncing logic.




#include <stdint.h>

// Your existing SysTick counter
volatile uint32_t ms_ticks = 0; 

// --- NEW VARIABLES FOR DEBOUNCING ---



#define DEBOUNCE_DELAY_MS 50 // Debounce time in milliseconds

uint8_t last_button_state = 1; // Assumes button is initially not pressed (HIGH due to pull-up)
uint8_t debounced_button_state = 1;
uint32_t last_debounce_time = 0;

2. Modify Your main Function:
Replace the entire while(1) loop from the previous lesson with this new logic. The GPIO initializations for the LED (PC13) and button (PA0) remain the same.

// Inside your main function, after all initializations...

while (1)
{
    // Read the raw state of the button
    uint8_t current_button_state = (GPIOA->IDR & (1 << 0)) ? 1 : 0;

    // Check if the button state has changed
    if (current_button_state != last_button_state) {
        // Reset the debouncing timer
        last_debounce_time = ms_ticks;
    }

    // Check if the state has been stable for the debounce delay
    if ((ms_ticks - last_debounce_time) > DEBOUNCE_DELAY_MS) {
        // If the button state has changed and has been stable
        if (current_button_state != debounced_button_state) {
            debounced_button_state = current_button_state;

            // Act on the stable state change. We only care about the press (transition to LOW).
            if (debounced_button_state == 0) {
                // Button was pressed, toggle the LED.
                // Remember: PC13 is active-low.
                GPIOC->ODR ^= (1 << 13);
            }
        }
    }

    // Update the last button state for the next iteration
    last_button_state = current_button_state;

    // We can add other non-blocking tasks here in the future.
    // The loop is never halted.
}

Code Explanation:

  • current_button_state: We read the raw, potentially bouncy state of the pin in every loop iteration.
  • if (current_button_state != last_button_state): This detects any change, including bounces. Every time a change is seen, we reset our last_debounce_time to the current ms_ticks. This means we've started a new "timing window."
  • if ((ms_ticks - last_debounce_time) > DEBOUNCE_DELAY_MS): This is the core of the non-blocking check. It asks, "has it been more than 50ms since the last change was detected?" If so, the input must be stable.
  • if (current_button_state != debounced_button_state): Once we know the input is stable, we check if this stable state is a new state. This ensures we only act once per state change.
  • if (debounced_button_state == 0): We check for the transition to the "pressed" state (LOW) and toggle the LED. This logic ensures we get one toggle per press, not one per press and another per release.
  • last_button_state = current_button_state: This is crucial for detecting the start of a bounce in the next loop iteration.

Compile and flash this code. You should now find that the LED toggles cleanly once every time you press the button, no matter how quickly or slowly you press and release it. The flickering should be gone.


Conclusion

You have just implemented one of the most fundamental and necessary algorithms in embedded systems. By moving from a naive, blocking approach to a non-blocking, time-based state machine, you are thinking like a professional firmware developer.

Key Takeaways:

  • Mechanical switches produce noisy signals due to contact bounce.
  • Using blocking delays (delay_ms()) to handle debouncing is inefficient and unacceptable in responsive systems.
  • A non-blocking, timer-based algorithm allows the system to remain responsive while reliably filtering input noise.
  • The logic involves tracking the time since the last detected input change and only accepting a new state after a period of stability.

Preview of the Next Lesson:
Our current debouncing code runs inside the main while(1) loop, constantly polling the GPIO pin. While this is non-blocking, it's still not the most efficient use of CPU time. What if we could ignore the button completely until it's actually pressed? In our next lesson, we will explore the powerful concept of External Interrupts (EXTI) and the Nested Vectored Interrupt Controller (NVIC) to make our MCU react to events asynchronously, saving power and CPU cycles.