Every embedded engineer's journey begins with blinking an LED, but professional applications demand much more sophistication. When you need multiple LEDs blinking at different precise intervals while your microcontroller handles critical tasks, you need robust solutions that don't compromise system performance.
Why this Approach Works Best
After implementing dozens of industrial control systems, I've found that hardware timers provide the perfect balance of precision and efficiency. They work like precision Swiss watches inside your microcontroller, completely independent of your main program flow.
How it Works:
Best For:
Pro Tip: I always use a base interval that's the greatest common divisor of all required blink rates. For 100ms and 360ms, 20ms works perfectly.
When to Choose This:
Implementation Insight:
Feature | Single Timer | Multiple Timers |
Resource Usage | Low | Higher |
Timing Precision | Excellent | Perfect |
Implementation Complexity | Moderate | Simple |
Scalability | Good | Limited |
Debugging Ease | Challenging | Straightforward |
The golden formula I use:
Compare_Value = (Desired_Interval * Clock_Frequency) / Prescaler - 1
For intervals exceeding timer maximums:
So, by choosing any of the approaches and considering the above points, we can implement the task.
Below are the solutions to the given task using different microcontrollers
We’re using an STM32 NUCLEO-F103RB board.
As we have to blink only two LEDs, and we have two hardware timers available to use.
We will use a simple approach of using two different hardware timers for each LED.
Circuit Diagram:
To hit 100 ms and 360 ms cleanly, pick a convenient counter tick (e.g., 1 ms), derive the prescaler from the timer input clock, then compute the auto-reload (ARR). This keeps the architecture simple, deterministic, and easy to audit.
Step 1 — Identify the timer input clock (f_TIM)
Step 2 — Choose an easy tick rate
Step 3 — Compute Prescaler (PSC)
Formula:
PSC = (f_TIM / f_tick) − 1
where f_tick = 1 kHz for a 1 ms tick.
Step 4 — Compute Period / ARR for target intervals
General formula (up-counting mode):
Update_Period = ((PSC + 1) × (ARR + 1)) / f_TIM
Rearrange for ARR when tick is 1 ms:
ARR = (Desired_ms / 1 ms) − 1 = Desired_ms − 1
Step 5 — Validate against timer width
Worked Configurations
Why this works
Project Setup in STM32CubeIDE
HAL_Init()
– Initializes the HAL library.SystemClock_Config()
– Configures the system clock.MX_USART2_UART_Init()
→ Sets up USART2.MX_GPIO_Init()
– Configures PA6 and PA7 as outputs.MX_TIM2_Init()
– Sets up TIM2 for 100 ms interrupts.MX_TIM3_Init()
– Sets up TIM3 for 360 ms interrupts.GPIO Initialization (MX_GPIO_Init)
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);
Timer Initialization
TIM2 – 100 ms Period
htim2.Instance = TIM2;
htim2.Init.Prescaler = 64000 - 1; // 1 ms tick
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 100 - 1; // 100 ms overflow
TIM3 – 360 ms Period
htim3.Instance = TIM3;
htim3.Init.Prescaler = 64000 - 1; // 1 ms tick
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 360 - 1; // 360 ms overflow
LED Pin Definitions
#define LED1_PORT GPIOA
#define LED1_PIN GPIO_PIN_6
#define LED2_PORT GPIOA
#define LED2_PIN GPIO_PIN_7
Timer Interrupt Callback
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2)
HAL_GPIO_TogglePin(LED1_PORT, LED1_PIN); // Blink LED1
if (htim->Instance == TIM3)
HAL_GPIO_TogglePin(LED2_PORT, LED2_PIN); // Blink LED2
}
Starting the Timers
HAL_TIM_Base_Start_IT(&htim2); // Enable TIM2 interrupts
HAL_TIM_Base_Start_IT(&htim3); // Enable TIM3 interrupts
Purpose: Begins counting for both timers in interrupt mode.
Main Loop
while (1) {
// All LED control handled in interrupts
}
PA6
and PA7
as outputs.TIM2
(100 ms), TIM3
(360 ms).The complete STM32CubeIDE project (including .ioc configuration, main.c, and HAL files) is available here:
📥 Download Project
We are using the ESP32 DevKit v4 development board and programming it using the Arduino IDE.
Since ESP32 has 4 hardware timers, we’ll use two of them, one for each LED, by assigning a separate timer interrupt to control each blink.
In the given code
#include <Arduino.h>
// LED pins
#define LED1_PIN 2 // LED1 toggles every 100 ms
#define LED2_PIN 4 // LED2 toggles every 360 ms
// Timer handles
hw_timer_t *timer1 = NULL;
hw_timer_t *timer2 = NULL;
// LED states
volatile bool led1State = false;
volatile bool led2State = false;
// ISR for LED1
void IRAM_ATTR onTimer1() {
led1State = !led1State;
digitalWrite(LED1_PIN, led1State);
}
// ISR for LED2
void IRAM_ATTR onTimer2() {
led2State = !led2State;
digitalWrite(LED2_PIN, led2State);
}
void setup() {
// Initialize LED pins
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
// Timer1: 1 MHz frequency, toggles LED1 every 100 ms → 100000 us
timer1 = timerBegin(1000000);
timerAttachInterrupt(timer1, &onTimer1);
timerAlarm(timer1, 100000, true, 0); // 100 ms
// Timer2: 1 MHz frequency, toggles LED2 every 360 ms → 360000 us
timer2 = timerBegin(1000000);
timerAttachInterrupt(timer2, &onTimer2);
timerAlarm(timer2, 360000, true, 0); // 360 ms
}
void loop() {
// Empty: all LED toggling handled by ISRs
}
Let's understand the important function
hw_timer_t *timerX
hw_timer_t *timer1 = NULL;
hw_timer_t *timer2 = NULL;
timerBegin(freq)
timer1 = timerBegin(1000000);
timer2 = timerBegin(1000000);
timerAttachInterrupt(timer, function)
onTimer1()
→ toggles LED1.onTimer2()
→ toggles LED2.timerAttachInterrupt(timer1, &onTimer1);
timerAttachInterrupt(timer2, &onTimer2);
So each LED has its own “callback” that runs automatically.
timerAlarm(timer, value, autoreload, count)
value
→ how many ticks before triggering (1 tick = 1 µs in given code).autoreload
→ if true, repeats forever (like periodic interrupts).count
→ initial count (usually 0).100000
ticks = 100 ms → LED1 toggles every 100 ms.360000
ticks = 360 ms → LED2 toggles every 360 ms.IRAM_ATTR
void IRAM_ATTR onTimer1() { … }
void IRAM_ATTR onTimer2() { … }
volatile
tells the compiler a variable may change unexpectedly (e.g., in an ISR), so it prevents optimization that could ignore updates.
We are using the Arduino UNO development board and programming it using the Arduino IDE.
Using tone() function:
We can use the tone() function for toggling LEDs. However, it has some limitations
tone()
function generates frequency on only 1 pin at a time.tone()
function can generate a lowest frequency of 31 Hz.So we will use Timer1
and Timer2
for toggling of pins.
Using Timers:
void loop()
.1 << WGM12
).62.5 kHz
(16MHz/256
).OCR1A=(100×(16000000/256)/1000)−1=6249
ISR (TIMER1_COMPA_vect)
toggles LED1 every 100ms.1 << WGM21
).OCR2A
must be ≤ 255
.16.32 ms
OCR2A = 155
to generate an interrupt every ~10ms, then use a software counter to count 36 interrupts to reach 360 ms.
#include <avr/io.h>
#include <avr/interrupt.h>
// Pin Definitions
#define LED1_PIN 9 // LED1 toggles every 100ms
#define LED2_PIN 10 // LED2 toggles every 360ms
// LED States
volatile bool led1State = LOW;
volatile bool led2State = LOW;
// Software counter for LED2
volatile uint8_t led2Counter = 0;
void setup() {
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
// Timer1 Configuration (CTC Mode for LED1 - 100ms interval)
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS12); // CTC Mode, Prescaler 256
OCR1A = 6249; // Corrected OCR1A for 100ms
TIMSK1 = (1 << OCIE1A); // Enable Timer1 Compare Match Interrupt
// Timer2 Configuration (CTC Mode for LED2 - ~10ms interval)
TCCR2A = (1 << WGM21); // CTC Mode
TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20); // Prescaler 1024
OCR2A = 155; // Generates an interrupt every ~10ms
TIMSK2 = (1 << OCIE2A); // Enable Timer2 Compare Match Interrupt
sei(); // Enable Global Interrupts
}
void loop() {
while (true) {
// Microcontroller remains busy in this loop continuously monitoring a critical task.
}
}
// Timer1 ISR - Toggles LED1 every 100ms
ISR(TIMER1_COMPA_vect) {
led1State = !led1State;
digitalWrite(LED1_PIN, led1State);
}
// Timer2 ISR - Toggles LED2 every 360ms using a software counter
ISR(TIMER2_COMPA_vect) {
led2Counter++;
if (led2Counter >= 36) { // 36 x 10ms = 360ms
led2Counter = 0;
led2State = !led2State;
digitalWrite(LED2_PIN, led2State);
}
}
Timer1 Configuration (for LED1 - 100ms interval)
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS12); // CTC Mode, Prescaler 256
OCR1A = 6249; // Corrected OCR1A for 100ms
TIMSK1 = (1 << OCIE1A); // Enable Timer1 Compare Match Interrupt
TCCR1A = 0: Clears Timer1 Control Register A.
TCCR1B
: Configures Timer1 in Clear Timer on Compare Match (CTC) mode with a prescaler of 256.OCR1A = 6249
: Sets the compare match value for 100 ms intervals.TIMSK1
: Enables the Timer1 Compare Match A interrupt.
Timer2 Configuration (for LED2 - ~10ms interval)
TCCR2A = (1 << WGM21); // CTC Mode
TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20); // Prescaler 1024
OCR2A = 155; // Generates an interrupt every ~10ms
TIMSK2 = (1 << OCIE2A); // Enable Timer2 Compare Match Interrupt
TCCR2A: Configures Timer2 in CTC mode.
TCCR2B
: Sets the prescaler to 1024.OCR2A = 155
: Sets the compare match value for ~10ms intervals.TIMSK2
: Enables the Timer2 Compare Match A interrupt.
ISR is an Interrupt Service Routine; it is serviced when an associated interrupt is triggered.
Timer1 ISR (for LED1)
ISR(TIMER1_COMPA_vect) {
led1State = !led1State;
digitalWrite(LED1_PIN, led1State);
}
Timer2 ISR (for LED2)
ISR(TIMER2_COMPA_vect) {
led2Counter++;
if (led2Counter >= 36) { // 36 x 10ms = 360ms
led2Counter = 0;
led2State = !led2State;
digitalWrite(LED2_PIN, led2State);
}
}
Summary
loop()
function is intentionally left empty, allowing the microcontroller to focus on handling interrupts and other tasks.Hardware setup
DSO waveform output:
GIF Of Output