We need to toggle between two LED blinking patterns using a push-button, while the main loop is busy with other tasks.
To ensure instant response and non-blocking operation, we use interrupts.
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 Timer3// In MX_GPIO_Init()
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_RESET);
/*Configure GPIO pin : PC0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
/*Configure GPIO pins : PA6 PA7 PA8 PA9 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
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);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
This configuration initializes the button on PC0 as an external interrupt input with a pull-up resistor, ensuring the line is normally HIGH and triggers an interrupt when pressed (falling edge).
The four LEDs (PA6, PA7, PA8, PA9) are configured as push-pull digital outputs, allowing each pin to drive its LED either HIGH (on) or LOW (off) with strong current capability.
#define SWITCH0_PORT GPIOC
#define SWITCH0_PIN GPIO_PIN_0
#define LED0_PORT GPIOA
#define LED0_PIN GPIO_PIN_6
#define LED1_PORT GPIOA
#define LED1_PIN GPIO_PIN_7
#define LED2_PORT GPIOA
#define LED2_PIN GPIO_PIN_9
#define LED3_PORT GPIOA
#define LED3_PIN GPIO_PIN_8
These macros provide human-readable names for the switch and LED pins, improving code clarity and maintainability.
Changing pin assignments in the future only requires modifying these definitions instead of editing multiple code sections.
// 2 pattern sets, each containing 2 frames of 4 LED states
const uint8_t patterns[2][2][4] = {
{ {1,1,1,1}, {0,0,0,0} }, // Pattern Set 0: All ON → All OFF
{ {1,0,1,0}, {0,1,0,1} } // Pattern Set 1: Alternating pattern
};
// Global control flags and counters
volatile uint8_t pattern = 0; // Current pattern set (0 or 1)
volatile uint8_t patternIndex = 0; // Current frame index (0 or 1)
volatile uint8_t buttonPressed = 0; // Flag for a validated button press
volatile uint8_t isr_call_count = 0; // Counts 10 ms ticks → 1 second cycle
volatile uint8_t debounceArmed = 1; // Prevents multiple presses during debounce
These variables manage LED patterns and button logic.
patterns[][][] defines the LED states for each frame.patternIndex switches between frames within a pattern every second.debounceArmed ensures only one press is accepted during each 20 ms debounce interval.a) LED Frame Writer
// Updates the LED outputs according to the selected pattern frame
static inline void LEDs_WriteFrame(uint8_t p, uint8_t idx)
{
HAL_GPIO_WritePin(LED0_PORT, LED0_PIN, patterns[p][idx][0] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, patterns[p][idx][1] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, patterns[p][idx][2] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED3_PORT, LED3_PIN, patterns[p][idx][3] ? GPIO_PIN_SET : GPIO_PIN_RESET);
}This function writes a frame of LED states to the GPIO pins.
It ensures all LEDs update simultaneously according to the selected pattern set and frame index.
b) External Interrupt Callback
// Triggered when PC0 detects a falling edge (button press)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == SWITCH0_PIN)
{
if (debounceArmed)
{
debounceArmed = 0;
__HAL_TIM_SET_COUNTER(&htim3, 0);
HAL_TIM_Base_Start_IT(&htim3); // Start 20 ms debounce one-shot timer
}
}
}When the button is pressed, this callback arms Timer 3 to begin a 20-ms one-shot debounce window. During this time, subsequent interrupts are ignored, preventing false triggers from mechanical bounce.
c) Timer Interrupt Handler
// Called when a timer period elapses
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
// TIM2: 10 ms periodic timer (for pattern updates)
if (htim->Instance == TIM2)
{
isr_call_count++;
if (isr_call_count >= 100 || buttonPressed) // 100×10ms = 1s
{
LEDs_WriteFrame(pattern, patternIndex);
patternIndex ^= 1; // Toggle between the two frames
isr_call_count = 0;
buttonPressed = 0;
}
}
// TIM3: 20 ms debounce timer
if (htim->Instance == TIM3)
{
HAL_TIM_Base_Stop_IT(&htim3); // Stop the one-shot timer
if (HAL_GPIO_ReadPin(SWITCH0_PORT, SWITCH0_PIN) == GPIO_PIN_RESET)
{
pattern ^= 1; // Switch to the other pattern set
buttonPressed = 1; // Force immediate LED update
}
debounceArmed = 1; // Re-arm for the next button press
}
}TIM2 (10 ms periodic):
Used for frame timing. Every 100 interrupts (≈1 s), the active LED frame toggles.
If a button press occurs, an immediate frame update happens without waiting for the 1-second mark.
TIM3 (20 ms one-shot):
Used for debouncing. After 20 ms, it checks if the button is still pressed (active-low).
If true, it confirms a valid press, toggles the pattern, and re-enables button detection.
int main(void)
{
HAL_Init(); // Initialize HAL and SysTick
SystemClock_Config(); // Configure system clock (HSI+PLL 64 MHz)
MX_GPIO_Init(); // Configure LEDs and switch pins
MX_USART2_UART_Init(); // Optional debugging UART
MX_TIM2_Init(); // 10 ms periodic timer
MX_TIM3_Init(); // 20 ms debounce one-shot timer
// Start periodic timer
HAL_TIM_Base_Start_IT(&htim2);
// Initialize LEDs with the first frame
LEDs_WriteFrame(pattern, patternIndex);
while (1)
{
// Main loop intentionally empty: fully interrupt-driven
}
}This design provides a clean, responsive, and low-CPU solution with precise timing.
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.
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"
#include "soc/gpio_struct.h" // for GPIO.out_w1ts / GPIO.out_w1tc
#define BUTTON_PIN 13 // push-button (active LOW)
static const uint8_t ledPins[4] = { 15, 16, 17, 18 }; // LEDs (all <32 → same register bank)
uint8_t timerCount = 0;
static const uint8_t patterns[2][2][4] = {
{ { 1, 1, 1, 1 }, { 0, 0, 0, 0 } }, // pattern set 0
{ { 1, 0, 1, 0 }, { 0, 1, 0, 1 } } // pattern set 1
};
volatile bool pattern = 0; // which pattern set (toggled on button)
volatile bool patternIndex = 0; // flips every update
volatile bool buttonPressed = false;
volatile bool timerFlag = true; // to arm debounce
hw_timer_t* debounceTimer = nullptr; // 20 ms one-shot
hw_timer_t* patternTimer = nullptr; // 1 s periodic
void IRAM_ATTR isButtonPress(); // GPIO ISR
void ARDUINO_ISR_ATTR onDebounceTimer(); // 20 ms one-shot
void ARDUINO_ISR_ATTR onPatternTick(); // 1 s periodic
// Build a 4-bit LED pattern from tables
static inline uint8_t makeLedBits(bool pat, bool idx) {
uint8_t b = 0;
b |= (patterns[pat][idx][0] ? 1 : 0) << 0;
b |= (patterns[pat][idx][1] ? 1 : 0) << 1;
b |= (patterns[pat][idx][2] ? 1 : 0) << 2;
b |= (patterns[pat][idx][3] ? 1 : 0) << 3;
return b;
}
// Write LED bits directly (atomic, ISR-safe)
static inline void IRAM_ATTR writeLedsMask(uint8_t bits) {
uint32_t setMask = 0, clrMask = 0;
for (int i = 0; i < 4; ++i) {
uint32_t pinMask = (1u << ledPins[i]);
if (bits & (1u << i)) setMask |= pinMask;
else clrMask |= pinMask;
}
if (setMask) GPIO.out_w1ts = setMask;
if (clrMask) GPIO.out_w1tc = clrMask;
}
void setup() {
Serial.begin(115200);
// LED setup
for (uint8_t i = 0; i < 4; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
}
// Button setup
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), isButtonPress, FALLING);
// 20 ms debounce timer: one-shot
debounceTimer = timerBegin(1'000'000); // 1 MHz base
timerAttachInterrupt(debounceTimer, &onDebounceTimer);
timerAlarm(debounceTimer, 20'000, false, 0); // 20 ms one-shot
timerStop(debounceTimer);
timerWrite(debounceTimer, 0);
// Pattern timer of 10msec
patternTimer = timerBegin(1'000'000); // 1 MHz base
timerAttachInterrupt(patternTimer, &onPatternTick);
timerAlarm(patternTimer, 10000 /*us*/, true, 0); // 10msec auto-reload
// Show initial pattern
writeLedsMask(makeLedBits(pattern, patternIndex));
}
void loop() {
// Controller is busy doing some important task
Serial.println(timerCount);
}
// Button ISR (falling edge): start debounce timer if idle
void IRAM_ATTR isButtonPress() {
if (timerFlag) {
timerFlag = false;
timerWrite(debounceTimer, 0);
timerStart(debounceTimer);
}
}
// Debounce timer ISR: confirm press and toggle pattern
void ARDUINO_ISR_ATTR onDebounceTimer() {
timerFlag = true;
if (gpio_get_level((gpio_num_t)BUTTON_PIN) == 0) {
pattern = !pattern;
buttonPressed = true;
}
timerStop(debounceTimer);
}
// toggle LEDs
void ARDUINO_ISR_ATTR onPatternTick() {
if (timerCount == 100 || buttonPressed) {
uint8_t bits = makeLedBits(pattern, patternIndex);
writeLedsMask(bits);
patternIndex = !patternIndex;
buttonPressed = false;
timerCount = 0;
} else
timerCount++;
}
1.Button Press ISR (isButtonPress())
timerFlag = true), it starts the 20-ms one-shot timer for debouncing.2. Debounce Timer ISR (onDebounceTimer())
pattern = !pattern; → toggles LED pattern set.buttonPressed = true;timerFlag = true) so it can detect the next press.3. Pattern Timer ISR (onPatternTick())
buttonPressed is true (pattern change detected) or after every 1 sec, it updates LEDs using the current pattern and pattern index.patternIndex toggles (0 ↔ 1) each second to animate LEDs alternately.GPIO.out_w1ts / GPIO.out_w1tc), ensuring ISR-safe updates.1. Debounce Timer (20 ms one-shot)
timerBegin(1000000) at 1 MHz base frequency.timerAlarm(debounceTimer, 20’000, false, 0);2 . Pattern Timer (10 ms periodic)
timerBegin(1000000); at 1 MHz.timerAlarm(patternTimer, 10’000, true, 0);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

We will implement the following approach for the task:
#define BUTTON_PIN 2 // Push button connected to pin 2 (interrupt pin)
// Pattern Array (2 sets of 2 patterns)
const int patterns[2][2][4] = {
{ { 1, 1, 1, 1 }, // pattern 1
{ 0, 0, 0, 0 } },
{ { 1, 0, 1, 0 }, // pattern 2
{ 0, 1, 0, 1 } }
};
// LED Pin Array
const uint8_t ledPins[] = { 3, 4, 5, 6 };
volatile bool pattern = 0;
volatile bool patternIndex = 0;
volatile bool buttonPressed = false;
volatile bool timerFlag = true;
volatile uint8_t isr_call_count = 0;
void setup() {
// Set all LED pins as OUTPUT
for (uint8_t i = 0; i < 4; i++) {
pinMode(ledPins[i], OUTPUT);
}
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Attach interrupt to button pin, trigger on FALLING edge (button press)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), isButtonPress, FALLING);
/* Configuring Timer 1 for 20 msec delay */
TCCR1A = 0; // Normal operation
TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); // CTC mode, prescaler = 1024
OCR1A = 312; // Set compare match value (for 20ms interrupt with 1024 prescaler)
/* Configuring Timer 2 for 10 msec delay */
TCCR2A = 0; // Normal operation
TCCR2B = (1 << WGM22) | (1 << CS22) | (1 << CS21); // CTC mode, prescaler = 1024
OCR2A = 156; // Set compare match value (for 10ms interrupt with 1024 prescaler)
TIMSK2 |= (1 << OCIE2A); // enable Timer2 Compare A interrupt
// enable global interrupt
sei();
}
void loop() {
while (true) {
// microcontroller is busy in doing critcal task countinously.
}
}
// Interrupt Service Routine (ISR) to handle button press
void isButtonPress() {
if (timerFlag == true) {
// set prescaler to 1024
TCCR1B |= (1 << CS12);
TCCR1B |= (1 << CS10);
// Enable Timer1 Compare interrupt
TIMSK1 = (1 << OCIE1A);
timerFlag = false;
}
TCNT1 = 0; // Reset timer count to 0.
}
// Timer1 interrupt service routine for 20 msec debounce delay check
ISR(TIMER1_COMPA_vect) {
timerFlag = true;
if (digitalRead(BUTTON_PIN) == LOW) {
pattern = !pattern;
buttonPressed = true;
}
// Disable Timer1 interrupt after 20ms
TIMSK1 &= ~(1 << OCIE1A); // Disable Timer1 Compare A interrupt
// Stop Timer1 by clearing prescaler bits
TCCR1B &= ~(1 << CS12);
TCCR1B &= ~(1 << CS10);
TCCR1B &= ~(1 << CS20);
}
//Timer2 to ISR is called after every 10ms delay
ISR(TIMER2_COMPA_vect) {
isr_call_count++;
// update count after every 1 sec or if button press ocurre.
if (isr_call_count == 100 || buttonPressed) {
digitalWrite(ledPins[0], patterns[pattern][patternIndex][0]);
digitalWrite(ledPins[1], patterns[pattern][patternIndex][1]);
digitalWrite(ledPins[2], patterns[pattern][patternIndex][2]);
digitalWrite(ledPins[3], patterns[pattern][patternIndex][3]);
patternIndex = !patternIndex;
isr_call_count = 0;
buttonPressed = false;
}
}
1. Button Press ISR (isButtonPress())
2. Timer1 Compare Match ISR (TIMER1_COMPA_vect())
digitalRead(BUTTON_PIN) == LOW), it marks the button press as valid (buttonPressed = true) and changes the pattern by doing pattern = !pattern; , considering the button is pressed for a straight 20 ms.3. Timer2 Compare Match ISR (TIMER2_COMPA_vect())
buttonPressed == true then LED pattern is updated based on patternIndex and pattern.patternIndex alternates between 0 and 1.= 20 msec.
= 10 msec
