Skip to main content

Welcome to a Grasp lesson

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

Lesson 15

Debounced Button Interrupt for LED Control

Recommended resources

Hello! Welcome to the final lesson of our module on system core components.

In our previous lessons, we've built a strong foundation. We started by configuring system clocks and using the SysTick timer for precise delays. We then moved to handling user input, configuring GPIOs, implementing a software debounce algorithm, and finally, diving deep into the world of interrupts. Most recently, you learned how to write a safe and efficient Interrupt Service Routine (ISR) using the "set a flag and exit" pattern.

Today, we will synthesize all these skills to achieve this lesson's learning outcome: to build a project that controls an LED's state using a debounced button press via an interrupt. This is a classic and essential task for any embedded engineer. Mastering this will not only complete your understanding of this module's topics but also equip you with a robust solution for handling user interfaces in your future projects, including the automotive systems you're interested in.

1. The Challenge: Combining Interrupts and Mechanical Buttons

In our last lesson, we built an ISR that efficiently set a flag. However, if you were to connect that code to a real mechanical button, you would notice erratic behavior. A single, clean press from your finger can look like a series of rapid, chaotic presses to the microcontroller. This phenomenon is called "contact bounce" or "button bounce."

Oscilloscope View of Button Debouncing
This oscilloscope capture shows the electrical reality of a button press. Instead of one clean drop from high to low, the signal "bounces" between states for a short period. Each of these bounces can trigger an interrupt if not handled correctly.

Since each of these bounces can trigger our EXTI interrupt, the microcontroller might think the button was pressed 5, 10, or even 20 times in a few milliseconds. Our simple flag-based ISR is not equipped to handle this.


2. Debouncing within an Interrupt Context: The Right Way

Your first instinct might be to add a delay inside the ISR to wait for the bouncing to stop. As we established in the last lesson, this is a critical mistake. Placing delays or any long-running code in an ISR can cripple a system by increasing latency and preventing other, more critical interrupts from running.

The professional solution is to use time to our advantage. We need a way to ignore the "noisy" interrupts that happen right after an initial press. The logic is as follows:

  1. When an interrupt occurs, we record the current time.
  2. We compare this time to the time of the last known valid press.
  3. If a sufficient amount of time (e.g., 50 milliseconds) has passed, we accept this new interrupt as a valid press.
  4. If not, we dismiss it as noise from the same press and do nothing.

This approach keeps the ISR extremely fast while effectively filtering out the bounce. The SysTick timer, which you've already configured to generate a 1ms time base, is the perfect tool for this timekeeping.

To see this technique implemented, let's watch a clear and concise video guide. While this video uses the STM32 HAL (Hardware Abstraction Layer), the core principle is identical to what we will implement with bare-metal register access.

STM32 External Interrupt (EXTI) | Button debounce interrupt and LED Toggle |

This video from CircuitGator HQ provides an excellent demonstration of the time-based debouncing technique within an interrupt handler. Pay close attention to the logic inside the HAL_GPIO_EXTI_Callback function.

Watch the video, focusing on these segments: 0:07 - 1:01: A good overview of what an external interrupt is. 2:18 - 3:23: Introduction to the debouncing problem and the declaration of currentTime and previousTime variables. This is the core setup. 3:23 - 5:33: Implementation of the debouncing logic. Notice how HAL_GetTick() is used to check the time elapsed since the last valid press. This logic is what we will adapt.

As you saw, the key logic is if (currentTime - previousTime > debounce_delay). This is the elegant solution we will now build ourselves.


3. From HAL to Bare-Metal: Adapting the Logic

The video uses HAL functions, but we can easily translate the concepts to our bare-metal register access approach:

HAL Function/Variable Our Bare-Metal Equivalent
HAL_GPIO_EXTI_Callback(...) EXTI1_IRQHandler() in stm32f1xx_it.c (or whichever handler matches your pin)
HAL_GetTick() A volatile uint32_t global variable that you increment inside SysTick_Handler()
HAL_GPIO_TogglePin(...) GPIOC->ODR ^= (1 << 13);
GPIO_PIN_5 The bit position of your button, e.g., (1 << 1) for pin PB1.
__HAL_GPIO_EXTI_CLEAR_IT(...) `EXTI->PR

4. Your Turn: Building the Debounced Interrupt Project

It's time to put everything together. We will configure your STM32F103 Blue Pill board to toggle the onboard LED (PC13) every time you press an external button (connected to PB1), complete with robust debouncing.

Step 1: Project Setup in STM32CubeIDE

  1. Create a new project for your STM32F103C8T6.
  2. System Core -> SYS: Set Debug to Serial Wire.
  3. System Core -> RCC: Set High Speed Clock (HSE) to Crystal/Ceramic Resonator.
  4. Pinout & Configuration:
    • Click on pin PC13 and set it to GPIO_Output.
    • Click on pin PB1 and set it to GPIO_EXTI1.
  5. Configure GPIO_EXTI1:
    • Go to System Core -> GPIO.
    • Select PB1. In the configuration panel, set GPIO Pull-up/Pull-down to Pull-up. This is crucial because your button will connect the pin to ground, and the pull-up keeps the line high otherwise.
  6. Configure the Interrupt:
    • Go to System Core -> NVIC.
    • Enable the EXTI line1 interrupt.
  7. Clock Configuration:
    • Go to the Clock Configuration tab.
    • Select HSE as the clock source and set the HCLK to 72 MHz. The tool will solve the PLL settings for you.
  8. Project Manager: Give your project a name and generate the code.

Step 2: Hardware Connection

  • Connect the onboard LED on your Blue Pill board. It's already wired to pin PC13.
  • Connect one leg of your push-button to pin PB1.
  • Connect the other leg of the push-button to a GND pin on the board.

Step 3: Writing the Code

Open your main.c file.

First, add the necessary global variables in the USER CODE BEGIN PV section. We need a counter for SysTick, a variable to hold the time of the last press, and a constant for our debounce delay.

/* USER CODE BEGIN PV */
volatile uint32_t g_systick_ms_counter = 0;
uint32_t g_last_press_time = 0;
const uint32_t DEBOUNCE_DELAY_MS = 50; // 50ms debounce time
/* USER CODE END PV */

Next, we need a way to increment our g_systick_ms_counter. The HAL_Init() function already configures SysTick to generate an interrupt every millisecond and defines a weak HAL_SYSTICK_Callback function. We can implement our own version. Add this to the USER CODE BEGIN 4 section.

/* USER CODE BEGIN 4 */
void HAL_SYSTICK_Callback(void)
{
    g_systick_ms_counter++;
}
/* USER CODE END 4 */

Now for the main event. We need to implement the interrupt handler callback. The HAL framework routes all EXTI interrupts to a single callback, HAL_GPIO_EXTI_Callback. We will implement this function to perform our debouncing logic. Add this code to the USER CODE BEGIN 4 section as well.

/* USER CODE BEGIN 4 */
// ... existing HAL_SYSTICK_Callback ...

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    // Check if the interrupt is from our button pin (PB1)
    if(GPIO_Pin == GPIO_PIN_1) 
    {
        uint32_t current_time = g_systick_ms_counter;

        // Check if enough time has passed since the last valid press
        if ((current_time - g_last_press_time) > DEBOUNCE_DELAY_MS)
        {
            // This is a valid press, so toggle the LED
            HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);

            // Update the last press time to the current time
            g_last_press_time = current_time;
        }
        // If not enough time has passed, we do nothing and ignore the bounce.
    }
}
/* USER CODE END 4 */

Finally, your main() function's while(1) loop will be empty. This is the beauty of an interrupt-driven design—the CPU is free to do other things (or sleep to save power) while waiting for an event.

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

Compile the code and flash it to your board. Press the button. You should see the LED toggle cleanly with each distinct press, no matter how much you try to make it bounce.


Conclusion

Congratulations! You have successfully built a robust, professional-grade button handler. This project is the culmination of everything you've learned in this module, combining clock configuration, timer-based delays, GPIO I/O, and advanced interrupt handling into a practical and reliable solution.

Key Takeaways:

  • Mechanical switches are electrically noisy and require a debouncing strategy.
  • The preferred method for debouncing within an interrupt is time-based filtering, not blocking delays.
  • The SysTick timer provides an excellent, low-overhead time base for implementing this filter.
  • An interrupt-driven design is highly efficient, freeing the CPU from polling and allowing it to perform other tasks.

Preview of the Next Lesson:

Having mastered the core system components, we are now ready to explore more advanced peripheral control. In the next module, we will dive into Advanced Timers, PWM, and Signal Measurement. You will learn how to generate variable-duty-cycle signals to control things like LED brightness and motor speed (PWM), and how to use timers to precisely measure the properties of incoming signals, a key skill for interfacing with many sensors, including the ultrasonic sensor in your kit.