Embedded
Timers
A hardware timer is a counter built into the chip that keeps ticking on its own, independent of whatever the CPU is doing — and can take an action (fire an interrupt, flip a pin, capture a value) the instant it reaches a target count. It's how firmware gets precise timing without burning CPU cycles in a busy-wait loop.
- What it is
- A hardware counter, clocked independently of the CPU, that can trigger an interrupt, toggle a pin, or capture a value when it reaches a target count.
- Mental model
- A stopwatch built into the chip — it keeps counting in the background and can tap the CPU on the shoulder, or wiggle a pin by itself, without anyone watching it.
- Key numbers
- Counter width (8/16/32-bit), a prescaler that divides the input clock, and a reload/period register (ARR) that sets where it wraps.
- Reach for it when
- You need a fixed-rate event, a precise delay, a PWM output, or to measure the timing of an external signal — all without polling.
Why not just a software delay loop?
A for loop that counts to some number and calls it "a
delay" ties up the CPU for the entire wait, can't do anything else
while it waits, and drifts with clock speed, compiler optimization
level, and whatever else changes about the build. A hardware timer
solves all three: it counts using dedicated silicon while the CPU is
completely free to do other work, it only interrupts the CPU at the
exact moment something needs to happen, and its timing is derived
directly from a known clock source instead of "however many loop
iterations happened to take."
Anatomy of a hardware timer
Strip away the vendor-specific register names and every hardware timer is built from the same handful of parts, chained together:
- Clock source
- The input clock feeding the timer — often the CPU's own bus clock, sometimes a separate low-power oscillator for timers that need to keep running in sleep modes.
- Prescaler (PSC)
- Divides the input clock down before it reaches the counter — without it, most timers would wrap around far too fast to be useful for anything but the shortest intervals.
- Counter (CNT)
- The actual register that increments (or on some timers, decrements) once per prescaled tick. Reading it tells you the timer's current value at that instant.
- Auto-reload register (ARR)
- The "period" register — the value the counter counts up to before wrapping back to zero. This is what turns a free-running counter into a fixed-frequency generator.
- Compare/capture register (CCR)
- A second, independently-set value the counter is compared against mid-cycle — the basis for PWM duty cycle and for timestamping an external edge (input capture).
The frequency a timer actually produces follows directly from these parts:
f_timer = f_clk / ((PSC + 1) * (ARR + 1))
The +1 on both terms trips almost everyone up at least
once — both the prescaler and the reload register are typically
zero-indexed in hardware (a PSC of 0 means "don't divide at all," not
"divide by zero"), so the actual divisor is always one more than the
register value.
Timer modes
- One-shot
- Counts once from zero to ARR, fires its event, then stops — used for a single delayed action ("do this in exactly 200 µs, once").
- Periodic (up-counting)
- Counts to ARR, fires its event, automatically resets to zero, and keeps going — the default mode for anything that needs to happen at a fixed rate.
- Output compare / PWM
- Drives a physical output pin based on comparing CNT against a CCR value each cycle — see below.
- Input capture
- Instead of driving a pin, the timer latches its current CNT value into a CCR the instant an edge arrives on an input pin — see Input capture.
Generating PWM with a timer
PWM output is just the compare step from the anatomy diagram wired to
a pin instead of (or in addition to) an interrupt: the pin goes high
when the counter resets to zero, and drops low the instant the
counter reaches the compare value (CCR). Because the counter's period
is fixed by ARR, the fraction of each cycle spent high — the
duty cycle — is just CCR / (ARR + 1),
set purely by changing one register with no CPU involvement once
configured.
This is the exact same mechanism behind using PWM as a poor man's DAC — a fixed-frequency square wave whose average voltage, after filtering (or just the physical inertia of a motor or the persistence of vision of an LED), tracks the duty cycle.
Step by step: configuring a timer in C
Exact register names differ by vendor (this reads like a generic Cortex-M/STM32-style timer; an 8-bit AVR or a PIC will use different register names for the same five ideas), but the sequence is always the same shape:
- Decide the target frequency or period. Say we want a periodic interrupt every 1 ms (1 kHz), and the timer's input clock is 84 MHz.
-
Solve for PSC and ARR. Pick a prescaler that gets
the post-divide clock to a convenient round number, then let ARR
set the actual period:
84,000,000 / (83 + 1) = 1,000,000ticks/second (1 µs per tick), thenARR = 999gives exactly 1000 ticks → 1 ms per period. - Enable the timer's clock. On most MCUs the timer peripheral is clock-gated off by default to save power — nothing else works until this bit is set.
- Write PSC and ARR, then enable the update interrupt and unmask it in the interrupt controller (see Interrupts for what that unmasking step is actually doing).
- Start the timer by setting its enable bit — from this point the CPU is completely uninvolved until the ISR fires.
- Write the ISR: clear the interrupt flag first (skipping this is the single most common timer bug — see Gotchas), then do the minimum work needed and return.
// Target: 1 kHz periodic interrupt from an 84 MHz timer clock.
// f_timer = f_clk / ((PSC+1) * (ARR+1))
// 1000 Hz = 84,000,000 / ((83+1) * (999+1))
#define TIMER_PSC_VALUE 83 // 84 MHz / 84 = 1 MHz tick rate (1 us/tick)
#define TIMER_ARR_VALUE 999 // 1000 ticks per period = 1 ms period
void timer_init_1khz(void) {
TIMx_CLOCK_ENABLE(); // step 3: power the peripheral
TIMx->PSC = TIMER_PSC_VALUE; // step 4: prescaler
TIMx->ARR = TIMER_ARR_VALUE; // step 4: period
TIMx->CNT = 0; // start counting from zero
TIMx->DIER |= TIM_DIER_UIE; // step 4: enable "update" interrupt
NVIC_EnableIRQ(TIMx_IRQn); // step 4: unmask it in the NVIC
TIMx->CR1 |= TIM_CR1_CEN; // step 5: start the timer
}
volatile uint32_t ms_ticks;
void TIMx_IRQHandler(void) {
TIMx->SR &= ~TIM_SR_UIF; // step 6: MUST clear the flag first
ms_ticks++; // step 6: keep it short — see Interrupts
}
Most vendor HALs (STM32 HAL, NXP MCUXpresso, Microchip Harmony…) wrap exactly this sequence behind a config-struct-and-init-function API — learning the raw register version once makes every HAL's timer API make sense immediately, instead of feeling like a pile of unexplained struct fields.
Input capture: measuring a signal
Run the same hardware in reverse: instead of the timer driving a pin, an external signal edge (rising, falling, or both) tells the timer to latch its current CNT value into a capture register, without any CPU involvement or jitter from software reaction time. Two captures on successive edges, subtracted, give the period of an incoming signal directly in timer ticks — the standard way to measure the frequency of an unknown signal or the pulse width of something like an ultrasonic distance sensor's echo pin.
Real-world applications
- Motor control PWM — the output-compare mechanism above, typically running at several kHz, is how firmware sets motor speed or LED brightness continuously without CPU involvement per cycle.
- RTOS tick — a dedicated hardware timer (SysTick on Cortex-M) firing at a fixed rate is what actually drives an RTOS scheduler's time-slicing and delay/timeout accounting.
- Non-blocking delays — reading a free-running timer's CNT value and comparing elapsed ticks replaces
delay()-style busy-waits with code that can do other things while "waiting." - Ultrasonic/time-of-flight sensing — input capture on an echo pin is the standard way to measure a pulse's width precisely, converting it to distance.
- Communication baud-rate generation — UART, and often SPI/I2C clock generation internally, are built on the same divide-down-a-clock idea as a general-purpose timer.
Gotchas
- Forgetting to clear the interrupt (update) flag inside the ISR is the single most common timer bug — depending on the core, the same interrupt can either refuse to fire again or fire continuously in a tight loop the moment it returns.
- Off-by-one on ARR: the period is
(ARR + 1)ticks, notARRticks, because the counter includes zero — a timer "set to 999" for 1000 ticks is correct, not a bug. - A prescaler/reload combination that doesn't divide the input clock evenly produces a frequency that's close but not exact — fine for a blink rate, not fine for anything timing-critical like a communication baud rate.
- Two peripherals (say, a PWM channel and an application timing tick) sharing one physical timer instance will fight over ARR, since there's only one reload value per timer — use a different timer instance, or a different channel of a multi-channel timer, per independent use.
- Measuring a long pulse with input capture can silently wrap the counter if the pulse outlasts one full period — either use a wider counter, or also count overflow-interrupt events and add them into the elapsed time.