We have to detect single, double, and long-press events from the push button Control LED1 and LED2 based on the press events while the main loop is busy with other tasks.
To ensure instant response and non-blocking operation, we use interrupts.
To differentiate between single, double, and long presses, specific timing windows are used.
Timing Windows for button press detection are as follows
After 500 ms (button released):
After 500 ms (button still held):
When a mechanical button is pressed, it often produces unwanted rapid ON/OFF transitions (bounces) due to contact vibration.
To ensure a clean and stable reading, debouncing logic introduces a short time delay before validating the press.
Process
Thus, a button press is confirmed only after a stable 50-ms signal is observed.
So, by selecting a proper resistor, LED, and push-button switch correctly, we can implement the task.
Below are the solutions to the given task using different microcontrollers
We’re using an STM32 NUCLEO-F103RB board, which runs at a 3.3V logic level.
Circuit Diagram

Project Setup in STM32CubeIDE:
HAL_Init() → Initializes the HAL library.SystemClock_Config() → Configures system clock.MX_GPIO_Init() → Configures GPIO pins.MX_TIM2_Init() → Configures Timer2MX_TIM3_Init() → Configures Timer3LONG_PRESS_MS) is intuitive./* In MX_GPIO_Init() */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);
/* PC0 as EXTI on Rising+Falling, with Pull-Up */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
/* PA6, PA7 as push-pull outputs */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* EXTI0 interrupt */
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);This config sets PC0 as an interrupt input on both edges with pull-up (idle HIGH).
PA6/PA7 are push-pull outputs for LEDs.
#define BTN_GPIO_Port GPIOC
#define BTN_Pin GPIO_PIN_0
#define LED1_GPIO_Port GPIOA
#define LED1_Pin GPIO_PIN_6
#define LED2_GPIO_Port GPIOA
#define LED2_Pin GPIO_PIN_7Clear, single-source pin mapping improves maintainability.
// Timing constants (ms)
#define DEBOUNCE_MS 50u
#define LONG_PRESS_MS 1000u
#define DOUBLE_GAP_MS 500u
// Button state (modified in ISRs)
static volatile uint8_t debouncing = 0;
static volatile GPIO_PinState last_stable_level = GPIO_PIN_SET; // idle HIGH
static volatile uint32_t press_time_ms = 0;
static volatile uint8_t short_press_count = 0;debouncing locks out re-entry while the debounce one-shot runs.last_stable_level tracks the debounced level.press_time_ms measures press duration.short_press_count counts clicks within the double-click window.static void TIM_OneShot_Arm(TIM_HandleTypeDef *htim, uint32_t duration_ms)
{
__HAL_TIM_DISABLE_IT(htim, TIM_IT_UPDATE);
__HAL_TIM_SET_COUNTER(htim, 0);
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE);
__HAL_TIM_SET_AUTORELOAD(htim, duration_ms);
__HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);
HAL_TIM_Base_Start(htim);
}
static void TIM_OneShot_Disarm(TIM_HandleTypeDef *htim)
{
HAL_TIM_Base_Stop(htim);
__HAL_TIM_DISABLE_IT(htim, TIM_IT_UPDATE);
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE);
}With the 1 kHz base, ARR = duration_ms directly.
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin != BTN_Pin) return;
if (debouncing) return;
debouncing = 1;
HAL_NVIC_DisableIRQ(EXTI0_IRQn); // mask EXTI during debounce
__HAL_TIM_SET_AUTORELOAD(&htim2, DEBOUNCE_MS);
TIM_OneShot_Arm(&htim2, DEBOUNCE_MS);
}Start TIM2 (50 ms) and mask EXTI to ignore bounce edges.
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2) { // Debounce finished
TIM_OneShot_Disarm(&htim2);
GPIO_PinState stable = HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin);
if (stable != last_stable_level) {
on_button_state_change(stable);
last_stable_level = stable;
}
debouncing = 0;
__HAL_GPIO_EXTI_CLEAR_IT(BTN_Pin);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
else if (htim->Instance == TIM3) { // Double-click window over
TIM_OneShot_Disarm(&htim3);
if (short_press_count == 1) do_single_press_action();
else if (short_press_count >= 2) do_double_press_action();
short_press_count = 0;
}
}static void on_button_state_change(GPIO_PinState new_level)
{
if (new_level == GPIO_PIN_RESET) {
// Press (active-LOW)
press_time_ms = HAL_GetTick();
} else {
// Release
uint32_t dur = HAL_GetTick() - press_time_ms;
if (dur >= LONG_PRESS_MS) {
do_long_press_action();
short_press_count = 0;
TIM_OneShot_Disarm(&htim3); // cancel pending double
} else {
short_press_count++;
__HAL_TIM_SET_AUTORELOAD(&htim3, DOUBLE_GAP_MS);
TIM_OneShot_Disarm(&htim3);
TIM_OneShot_Arm(&htim3, DOUBLE_GAP_MS);
}
}
}static void do_single_press_action(void)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); // Single -> toggle LED1
}
static void do_double_press_action(void)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); // Double -> toggle LED2
}
static void do_long_press_action(void)
{
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); // Long -> both OFF
}Simple, visible outcomes tied to each gesture.
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_TIM2_Init();
MX_TIM3_Init();
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
// Initialize last stable level (pull-up idle expected = HIGH)
last_stable_level = HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin);
// Ensure EXTI0 is enabled (also enabled in MX_GPIO_Init)
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
while (1) {
// Fully interrupt-driven; no polling or blocking delays here.
}
}Be cautious when selecting EXTI pins. Lines EXTI5–9 and EXTI10–15 share the same interrupt vector.
The complete STM32CubeIDE project (including .ioc configuration, main.c, and HAL drivers) is available here:
📥 Download Project
We are using the ESP32 DevKitC v4 development board and programming it using the Arduino IDE.
To implement the given task, we are going to use the GPIO and timer interrupt.
Note: Avoid using GPIOs 34–39 for push-buttons while using ESP32 because they do not support pull-up/down resistors internally.
Circuit Connection


#include <Arduino.h>
#include "driver/gpio.h"
//Pin Definitions
#define LED1_PIN 5
#define LED2_PIN 18 // safe GPIO
#define BUTTON_PIN 27 // active-LOW button with internal pull-up
//Global State
volatile bool isT1running = false;
volatile bool isT2running = false;
volatile int clicksCount = 0;
volatile int t1Counter = 0;
volatile uint8_t led1State = 0;
volatile uint8_t led2State = 0;
// ESP32 Hardware Timers
hw_timer_t* timer1 = nullptr; // 500 ms periodic window
hw_timer_t* timer2 = nullptr; // 50 ms one-shot debounce
static const uint32_t TIMER_FREQ_HZ = 1000000UL; // 1 MHz → 1 µs tick
static const uint32_t T1_PERIOD_US = 500000; // 500 ms
static const uint32_t T2_DELAY_US = 50000; // 50 ms
// IRAM-Safe Helper Functions
inline void IRAM_ATTR setLed(uint8_t pin, uint8_t level) {
gpio_set_level((gpio_num_t)pin, level);
}
inline void IRAM_ATTR toggleLed(volatile uint8_t& s, uint8_t pin) {
s ^= 1;
gpio_set_level((gpio_num_t)pin, s);
}
inline int IRAM_ATTR readButton() {
return gpio_get_level((gpio_num_t)BUTTON_PIN); // LOW = pressed
}
// GPIO Interrupt Service Routine — Button Press Handler
void IRAM_ATTR onButtonPress() {
// Start 500 ms window timer if first click
if (!isT1running && clicksCount == 0) {
timerRestart(timer1);
timerStart(timer1);
isT1running = true;
}
// Start 50 ms debounce one-shot if not running
if (!isT2running) {
timerRestart(timer2);
timerStart(timer2);
isT2running = true;
}
}
// Timer2 ISR — 50 ms Debounce (One-Shot)
void IRAM_ATTR onDebounceTimer() {
if (readButton() == 0 && isT1running) { // still pressed after 50 ms
clicksCount++;
}
timerStop(timer2);
isT2running = false;
}
// Timer1 ISR — 500 ms Click Window Logic
void IRAM_ATTR onClickWindowTimer() {
t1Counter++;
if (t1Counter == 1) { // 500 ms after first press
if (clicksCount > 1) {
// Double-click → toggle LED2
toggleLed(led2State, LED2_PIN);
t1Counter = 3; // skip further checks
} else if (readButton() == 1 && clicksCount == 1) {
// Single click (released by 500 ms) → toggle LED1
toggleLed(led1State, LED1_PIN);
t1Counter = 3;
}
} else if (t1Counter == 2) { // 1.0 s long-press check
if (readButton() == 0 && clicksCount == 1) {
// Long press → turn both LEDs OFF
led1State = 0;
setLed(LED1_PIN, 0);
led2State = 0;
setLed(LED2_PIN, 0);
}
t1Counter = 3;
}
// Stop timer after sequence completes
if (t1Counter >= 3) {
timerStop(timer1);
isT1running = false;
clicksCount = 0;
t1Counter = 0;
}
}
void setup() {
// Configure LEDs
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
digitalWrite(LED1_PIN, LOW);
digitalWrite(LED2_PIN, LOW);
// Configure button input
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Low-level GPIO setup for ISR-safe operations
gpio_set_direction((gpio_num_t)LED1_PIN, GPIO_MODE_OUTPUT);
gpio_set_direction((gpio_num_t)LED2_PIN, GPIO_MODE_OUTPUT);
gpio_set_direction((gpio_num_t)BUTTON_PIN, GPIO_MODE_INPUT);
//Create timers using migrated v3 API
timer1 = timerBegin(TIMER_FREQ_HZ);
timer2 = timerBegin(TIMER_FREQ_HZ);
// Attach ISRs (v3 API — no edge parameter)
timerAttachInterrupt(timer1, &onClickWindowTimer);
timerAttachInterrupt(timer2, &onDebounceTimer);
// Configure alarms (auto-enabled)
timerAlarm(timer1, (uint64_t)T1_PERIOD_US, /*autoreload=*/true, 0);
timerAlarm(timer2, (uint64_t)T2_DELAY_US, /*autoreload=*/false, 0);
// Start both stopped
timerStop(timer1);
timerStop(timer2);
//GPIO Interrupt using attachInterrupt()
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonPress, FALLING);
}
void loop() {
while (true) {
// MCU busy with important background work
// All user interaction handled by ISRs
}
}
isT1running and isT2running: Flags to check if Timer1 (500 ms window) or Timer2 (50 ms debounce) is active.clicksCount: Counts button clicks.t1Counter: Counts 500 ms timeouts to identify single, double, or long press.led1State, led2State: Store LED ON/OFF states for toggling.attachInterrupt() triggers onButtonPress() on the falling edge (button press).onButtonPress() – Triggered when the button is pressed.onDebounceTimer() – Runs after 50 ms; if button still pressed, increments clicksCount.onClickWindowTimer() – Every 500 ms:We are using the Arduino UNO development board and programming it using the Arduino IDE.
We will use external and timer interrupts for button detection.
Circuit Connection


#define LED1_PIN 5 // Pin for LED1
#define LED2_PIN 6 // Pin for LED2
#define BUTTON_PIN 2 // Pin for the button
volatile bool isT1running = false; // Flag to indicate if Timer1 is running
volatile bool isT2running = false; // Flag to indicate if Timer2 is running
volatile int clicksCount = 0; // Counter for button clicks
volatile int t1Counter = 0; // Counter for Timer1 compare events
volatile int t2Counter = 0; // Counter for Timer2 compare events
void setup() {
pinMode(LED1_PIN, OUTPUT); // Set LED1 pin as output
pinMode(LED2_PIN, OUTPUT); // Set LED2 pin as output
pinMode(BUTTON_PIN, INPUT_PULLUP); // Set button pin as input with pull-up resistor
// Configure Timer1 for a 500ms timeout
TCCR1A = 0; // Set Timer1 to Normal mode
TCCR1B = 0; // Stop Timer1
OCR1A = 31249; // Set compare value for 500ms
TCNT1 = 0; // Reset Timer1 counter
TIMSK1 = (1 << OCIE1A); // Enable Timer1 Compare Match A interrupt
// Configure Timer2 for debounce logic
TCCR2A = (1 << WGM21); // Set Timer2 to CTC mode
TCCR2B = 0; // Stop Timer2
OCR2A = 255; // Set compare value for debounce duration
TCNT2 = 0; // Reset Timer2 counter
TIMSK2 = (1 << OCIE2A); // Enable Timer2 Compare Match A interrupt
// Configure external interrupt for the button (INT0)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), ISR_INT0, FALLING);
// Enable global interrupts
sei();
}
void loop() {
while (true) {
//In this loop Microcontroller is busy in monitoring and performing important tasks constantly.
}
}
// Interrupt Service Routine for button press (INT0)
void ISR_INT0() {
// If Timer1 is not running and no clicks are counted yet, start Timer1
if (!isT1running && clicksCount == 0) {
TCCR1B = (1 << WGM12) | (1 << CS12); // Start Timer1
isT1running = true;
}
// Start Timer2 for debounce logic if it's not running
if (!isT2running) {
TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20); // Start Timer2
isT2running = true;
}
}
// Interrupt Service Routine for Timer2 (Debounce logic)
ISR(TIMER2_COMPA_vect) {
t2Counter++;
if (t2Counter >= 3) { // After debounce duration (~3 cycles)
if (digitalRead(BUTTON_PIN) == LOW && isT1running) {
clicksCount++; // Increment click count if button is still pressed
}
TCCR2B = 0; // Stop Timer2
isT2running = false; // Reset debounce flag
t2Counter = 0; // Reset Timer2 counter
}
}
// Interrupt Service Routine for Timer1 (Click detection logic)
ISR(TIMER1_COMPA_vect) {
t1Counter++;
if (t1Counter == 1) { // First timeout (500ms after button press)
if (clicksCount > 1) {
digitalWrite(LED2_PIN, !digitalRead(LED2_PIN)); // Toggle LED2 on double-click
t1Counter = 3; // Skip further checks
} else if (digitalRead(BUTTON_PIN) == HIGH && clicksCount == 1) {
digitalWrite(LED1_PIN, !digitalRead(LED1_PIN)); // Toggle LED1 on single click
t1Counter = 3; // Skip further checks
}
} else if (t1Counter == 2) { // Second timeout (long press detected)
if (digitalRead(BUTTON_PIN) == LOW && clicksCount == 1) {
digitalWrite(LED1_PIN, LOW); // Turn off LED1
digitalWrite(LED2_PIN, LOW); // Turn off LED2
}
t1Counter = 3; // Ensure Timer1 stops
}
if (t1Counter >= 3) { // Stop Timer1 after logic completion
TCCR1B = 0; // Stop Timer1
isT1running = false; // Reset Timer1 running flag
clicksCount = 0; // Reset click counter
t1Counter = 0; // Reset Timer1 counter
}
}T1running and isT2running: Flags to track if Timer1 and Timer2 are active.clicksCount: Counts the number of button clicks.t1Counter and t2Counter: Counters for Timer1 and Timer2 interrupts loops. BUTTON_PIN is set as an input with a internal pull-up resistor.OCR1A is set to 31249 for 500 ms.OCR2A is set to 255 for 16 ms.sei().loop() performing important tasks and all the logic is handled by interrupts.Button Press ISR (ISR_INT0)
Timer2 ISR (Debounce Logic)
clicksCount is incremented.Timer1 ISR (Click Detection Logic)
clicksCount > 1, it's a double-click: toggle LED2.clicksCount == 1 and the button is released, it's a single click: toggle LED1.clicksCount == 1, it's a long press: turn off both LEDs.