Timers were one of those peripherals I underestimated at first — they seemed like a boring counting circuit compared to flashy things like wireless radios or displays. But the more embedded firmware I’ve written, the more I’ve realized timers are quietly doing an enormous share of the real work: generating PWM, measuring elapsed time, triggering periodic tasks, timing communication protocols, and even underpinning the RTOS scheduler itself. In this article I’ll go through what a hardware timer actually is, how it works internally, and the many roles it plays in real embedded systems.
What Is a Timer, Fundamentally?
At its core, a hardware timer is a digital counter register that increments (or decrements) automatically, once per clock pulse, completely independent of whatever the CPU is doing. The CPU can read the counter’s current value, configure how fast it counts, and set up conditions (like “interrupt me when it reaches this value”) — but once running, the timer counts on its own in hardware, without CPU intervention.
graph LR
CLK[Clock Source] --> PRE[Prescaler - divides clock]
PRE --> CNT[Counter Register]
CNT -->|Reaches ARR| OVF[Overflow / Update Event]
OVF -->|Interrupt| CPU[CPU: ISR runs]
CNT -->|Reaches CCR| CC[Compare Match Event]
Key Timer Components
- Clock source: The timer counts pulses from some clock — usually derived from the microcontroller’s system clock (or sometimes an external crystal, for accurate real-time functions).
- Prescaler: Divides the input clock down to a slower, more usable counting rate. Without a prescaler, most timer clocks would count far too fast to be useful for anything but the shortest intervals.
- Counter register (CNT): The actual incrementing (or decrementing) value.
- Auto-Reload Register (ARR): Defines the maximum count value before the counter resets (“overflows”) back to zero — this sets the timer’s period.
- Capture/Compare registers (CCR): Used for generating PWM, or for input capture (measuring the time between external events, like pulses from a rotary encoder).
Timer overflow period = (Prescaler + 1) × (ARR + 1) / Timer input clock
The Many Purposes of a Timer
1. Generating Periodic Interrupts (Task Scheduling)
Probably the most common use I have for timers is simply “run this function every N milliseconds.” This underlies everything from blinking an LED to sampling a sensor at a fixed rate to driving an RTOS scheduler’s tick.
#include "stm32f4xx_hal.h"
TIM_HandleTypeDef htim2;
void Timer2_Init_1Hz(void) {
htim2.Instance = TIM2;
htim2.Init.Prescaler = 8400 - 1; // 84 MHz / 8400 = 10 kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 10000 - 1; // 10 kHz / 10000 = 1 Hz overflow
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2);
}
void TIM2_IRQHandler(void) {
HAL_TIM_IRQHandler(&htim2);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
toggle_led(); // Runs once per second, automatically
}
}
2. Generating PWM Signals
As covered in the PWM article, timers with compare registers can toggle output pins at precisely defined points within each period, producing PWM waveforms for motor control, LED dimming, and servo positioning — entirely in hardware, without the CPU touching the output pin on every cycle.
3. Measuring Elapsed Time (Delays and Timeouts)
Rather than looping the CPU idle for a fixed delay (for(volatile int i=0;i<1000000;i++);, which wastes cycles and is inaccurate across different clock speeds/compiler optimizations), a timer-based delay reads the counter value and waits for a known number of counts to pass:
uint32_t millis(void) {
return HAL_GetTick(); // Backed by SysTick timer, incrementing every 1ms
}
void delay_ms_nonblocking_example(void) {
uint32_t start = millis();
while (1) {
if (millis() - start >= 500) {
do_periodic_thing();
start = millis();
}
do_other_work(); // CPU stays free to do other things
}
}
This pattern — capturing a timestamp, then repeatedly checking elapsed time — is fundamental to writing non-blocking, cooperative-style embedded code, and it depends entirely on having an accurate, free-running timer/counter underneath millis().
4. Input Capture (Measuring External Signal Timing)
Timers can also be configured to record their own counter value the instant an external event occurs (like a rising edge on a GPIO pin), which is how you measure the frequency or pulse width of an incoming signal — for example, decoding a rotary encoder, measuring an ultrasonic sensor’s echo pulse width, or reading an RC receiver’s PWM output.
sequenceDiagram
participant Signal as External Signal
participant Timer as Timer Counter
participant CCR as Capture Register
Signal->>Timer: Rising edge detected
Timer->>CCR: Counter value latched
Note over CCR: t1 captured
Signal->>Timer: Next rising edge
Timer->>CCR: Counter value latched
Note over CCR: t2 captured -> Period = t2 - t1
// Simplified ultrasonic (HC-SR04) echo pulse width measurement
volatile uint32_t rise_time = 0, fall_time = 0, pulse_width = 0;
void EXTI_IRQHandler(void) {
if (echo_pin_is_rising()) {
rise_time = TIM2->CNT;
} else {
fall_time = TIM2->CNT;
pulse_width = fall_time - rise_time;
float distance_cm = pulse_width / 58.0f; // speed-of-sound based constant
}
}
5. Watchdog Timers (System Reliability)
A specialized timer, the watchdog, continuously counts down and resets the microcontroller if it ever reaches zero without being periodically “refreshed” by the application — this catches software hangs or crashes and forces a recovery reboot. (Covered in full detail in the dedicated watchdog timer article.)
6. Real-Time Clock (RTC) Functions
Many microcontrollers include a dedicated RTC timer, often clocked by a separate low-power 32.768 kHz crystal, that keeps track of wall-clock time (seconds, minutes, hours, date) even while the rest of the chip is in deep sleep — essential for data logging with timestamps, scheduling, and calendar-based events.
7. Driving Communication Protocol Timing
UART baud rate generation, I2C/SPI clock generation, and RTOS tick scheduling all ultimately rely on timer/counter hardware internally, even if the peripheral driver abstracts this away from the application code.
Timer Modes
| Mode | Description |
|---|---|
| One-pulse mode | Counts once to the target value, then stops (single delay/timeout) |
| Continuous/periodic mode | Counts repeatedly, overflowing and restarting automatically |
| Input capture mode | Records counter value on external signal edges |
| Output compare/PWM mode | Toggles output pin(s) at defined counter values |
| Encoder mode | Special mode for directly decoding quadrature rotary encoder signals |
Configuring a Basic Periodic Timer on Arduino
#include <TimerOne.h> // Example library for AVR Timer1
void setup() {
Timer1.initialize(1000000); // 1,000,000 microseconds = 1 second
Timer1.attachInterrupt(onTimerTick);
}
void onTimerTick(void) {
toggle_led();
}
void loop() {
do_other_work();
}
Configuring a Timer on ESP32 (Arduino Framework)
hw_timer_t *timer = NULL;
void IRAM_ATTR onTimer() {
toggle_led();
}
void setup() {
timer = timerBegin(0, 80, true); // 80 prescaler -> 1 MHz tick (80MHz/80)
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 1000000, true); // 1,000,000 ticks = 1 second, auto-reload
timerAlarmEnable(timer);
}
void loop() {
// Main work continues independently of the timer interrupt
}
The RTOS Tick: Timers as the Heartbeat of Multitasking
As covered in the multitasking article, an RTOS’s ability to preemptively switch between tasks depends entirely on a periodic hardware timer interrupt — on ARM Cortex-M, this is almost always the dedicated SysTick timer, configured to interrupt at a fixed rate (commonly 1ms):
graph TD
SysTick[SysTick Timer - fires every 1ms] --> ISR[Tick ISR]
ISR --> Sched[RTOS Scheduler: decide next task]
Sched --> CTX[Context Switch]
CTX --> Task[Resume selected task]
Without a reliable timer generating this steady heartbeat, an RTOS simply couldn’t enforce time-sliced, preemptive scheduling.
Real-World Applications
- Blinking status LEDs / heartbeat indicators at a fixed rate to show a device is alive.
- Periodic sensor sampling (e.g., reading a temperature sensor every second).
- Debouncing buttons using a short timer-based delay to filter out mechanical switch bounce.
- Ultrasonic distance sensing, decoding pulse widths via input capture.
- Motor control loops running a PID controller update at a fixed frequency (e.g., every 1ms or 10ms).
- Communication protocol timeouts, detecting when an expected response hasn’t arrived within a defined window.
- Data logging timestamps, using an RTC timer to record when each measurement was taken.
IoT/Practical Example: Periodic Sensor Publish with a Software Timer Abstraction
typedef struct {
uint32_t interval_ms;
uint32_t last_run;
} soft_timer_t;
int soft_timer_expired(soft_timer_t *t) {
uint32_t now = millis();
if (now - t->last_run >= t->interval_ms) {
t->last_run = now;
return 1;
}
return 0;
}
soft_timer_t publish_timer = { .interval_ms = 10000, .last_run = 0 };
void loop() {
if (soft_timer_expired(&publish_timer)) {
float temp = read_temperature();
publish_to_mqtt(temp);
}
handle_other_tasks();
}
This “software timer” pattern, built on top of a single free-running hardware timer/millis() source, is extremely common — you rarely need a dozen separate hardware timers when one hardware tick source can back many independent software timers throughout your application.
Debugging and Reliability Considerations
- Prescaler/ARR math mistakes are the most common timer bug I run into — always double check the resulting frequency against the intended one, especially after changing system clock configuration, since prescaler/ARR values are relative to the timer’s input clock.
- Interrupt priority conflicts: if a timer ISR is too low priority, it can be delayed by other interrupts, causing timing jitter in PWM or sampling applications.
- Clock source changes: switching system clock configuration (e.g., enabling a PLL for higher CPU speed) changes the timer’s input clock too, unless it’s on an independently clocked domain (like an RTC on its own crystal) — this is a very easy thing to forget when optimizing power/performance later in a project.
Frequently Asked Questions
How many hardware timers does a typical microcontroller have? This varies widely — small AVR chips might have 2-3 timers, while a mid-range STM32 can have anywhere from 8 to 17+ timer peripherals of various types (basic, general-purpose, advanced, RTC), letting you dedicate different timers to PWM, scheduling, input capture, and the RTOS tick simultaneously.
What’s the difference between a timer and a counter? Functionally they’re often the same hardware block — the distinction is just what’s driving the increments. A “timer” counts a known, fixed clock source (measuring elapsed time), while a “counter” counts external events (like pulses from a sensor), though the underlying register and configuration mechanism are usually identical.
Can software delays (like busy-wait loops) replace hardware timers? For extremely simple, short delays in tiny projects, sometimes — but busy-wait delays block the CPU entirely, are imprecise (affected by compiler optimization and clock speed), and can’t run in the background while other code executes, which is why hardware timer-based approaches are strongly preferred for anything beyond the most trivial cases.
Why does my PWM frequency change unexpectedly after I changed the system clock speed? Because the timer’s input clock is usually derived from the system/peripheral clock; changing the system clock configuration (e.g., increasing CPU frequency) shifts the timer’s counting rate too, unless you recalculate and update the prescaler/ARR values accordingly.
Summary
A hardware timer is far more than a simple stopwatch — it’s a foundational peripheral that underlies periodic task scheduling, PWM generation, precise delay and timeout handling, input signal measurement, watchdog protection, real-time clock functions, and even the tick that drives RTOS preemptive multitasking. Learning to configure prescalers, auto-reload registers, and compare registers correctly unlocks an enormous amount of embedded system functionality, almost all of it running efficiently in hardware with minimal ongoing CPU involvement.
References and Further Reading
- STMicroelectronics STM32 Timer Peripheral Reference Manual — st.com
- ARM Cortex-M SysTick Timer Technical Reference — developer.arm.com
- Espressif ESP32 Timer Group Driver Documentation — docs.espressif.com
- Microchip/Atmel AVR Timer/Counter Documentation — microchip.com
- Arduino TimerOne / hardware timer libraries — arduino.cc
- FreeRTOS Tick Configuration Documentation — freertos.org