Until now, time has existed only indirectly, as the duration consumed by the executed instructions. Almost any real application, however, needs time as a quantity it can measure and impose: a motor needs an adjustable average power, an ultrasonic sensor needs a pulse width with microsecond precision, a communication protocol needs the line sampled exactly at the middle of every bit. For all of this, the microcontroller has a dedicated hardware block, at its core a simple counter that advances on its own, in parallel with the program - the timer module.
1Purpose and structure of the course6 min
A timer counts pulses received on its clock input, with no instruction being executed by the processor for it. Once started, it advances regardless of whether the central unit is computing, waiting, or in a low-power mode - one of the few peripherals that offers a measure of time independent of what the software is doing. On an Arduino Uno board, the clock is 16 MHz, so one pulse every 62.5 ns: the number accumulated in the register translates directly into a duration.
- A handler routine must be short, with no delays, and communicates through
volatilevariables (Lecture 05) - A variable larger than one byte needs a critical section for an atomic read (Lecture 05)
- Explain the difference between the timer mode and the counter mode of the same hardware module
- Distinguish the normal, CTC, fast PWM and phase-correct PWM modes
- Calculate, from the clock frequency, the prescale factor and the compare value, the period or frequency of an interrupt or of a PWM signal
- Explain the relationship between the duty cycle and the average value of a PWM signal
- Explain why the capture unit gives a precision an ordinary interrupt routine cannot reach
2Timer or counter, and why delay() is a poor solution11 min
Timer or counter
We speak of a timer when the clock signal has a uniform distribution over time (a clock with a known period) - the accumulated number is proportional to elapsed time. We speak of a counter (event counter) when the signal comes from outside, with a random distribution - pulses from an encoder, an optical gate. The hardware is identical: on the ATmega328P, the clock-source selection bits switch the same module between the two modes - hence the official name, Timer/Counter.
Why delay() is the worst solution
delay(1000) runs a loop that repeatedly reads the millisecond counter, exiting
when it has increased by a thousand - millions of instructions spent doing nothing. Worse:
while the program waits, it cannot respond to anything else - a button pressed and released
in this interval will not be noticed, a serial character may be lost.
/* blocking: the processor is tied up for 500 ms */
void loop() {
digitalWrite(LED, HIGH); delay(500);
digitalWrite(LED, LOW); delay(500);
}
/* non-blocking: the loop runs thousands of times a second */
unsigned long lastToggle = 0;
const unsigned long PERIOD = 500;
void loop() {
unsigned long now = millis();
if (now - lastToggle >= PERIOD) {
lastToggle = now; /* the moment is stored, not accumulated */
ledState = !ledState;
digitalWrite(LED, ledState);
}
/* buttons, sensors, serial communication can be handled here */
}
millis() returns a counter incremented by a hardware timer's interrupt
routine. The difference is that the program consults time instead of waiting for it. The next
step, covered in this lecture, is to let the timer announce the program on its own, through an
interrupt, or even generate the desired signal directly at the pin, with no processor involvement at
all.3Classifying timer modules11 min
A function-oriented classification distinguishes five structures, often sharing hardware.
The plain timer counts the internal clock and resets on overflow, signaling
through a flag and/or an interrupt. The counting range is [0, 2ⁿ-1] - an 8-bit
counter at 16 MHz overflows in 16 µs, a 16-bit one in 4.096 ms, not enough to time a
second without prescaling.
The event counter is the same module, with the clock switched to an external pin - it no longer measures time, it accumulates events. Combining the two modes (a timer fixes a one-second window, a counter accumulates pulses in it) directly gives the frequency of a signal, in hertz.
The PWM generator produces, entirely in hardware, a signal with a constant frequency and an adjustable duty cycle - discussed at length below.
The watchdog timer counts continuously, independent of the program; the program is obligated to reinitialize it periodically. If it gets stuck in an infinite loop or in an interrupt it never exits, reinitialization no longer happens, and the timer generates a reset that restarts the system into a known state.
The RTC (Real-Time Clock) keeps the time and date over the long term, off a 32.768 Hz crystal (=2¹⁵, so that successive division by two, through 15 flip-flops, gives exactly one pulse per second, with no rounding error). The ATmega328P has no full RTC, but can approximate the function with Timer2 in asynchronous mode.
| Module | Resolution | Prescale factors | Dependent library functions |
|---|---|---|---|
| Timer0 | 8 bits | 1, 8, 64, 256, 1024 | millis(), micros(), delay(), analogWrite() on pins 5, 6 |
| Timer1 | 16 bits | 1, 8, 64, 256, 1024 | the Servo library, analogWrite() on pins 9, 10 |
| Timer2 | 8 bits | 1, 8, 32, 64, 128, 256, 1024 | tone(), analogWrite() on pins 3, 11 |
The Arduino core configures Timer0 with a prescale of 64 and enables its overflow interrupt,
which feeds millis(), micros() and delay(). If the
program changes Timer0's prescale for a PWM signal of a different frequency on pin 5, it indeed
obtains that frequency, but it simultaneously speeds up or slows down the millisecond counter - all
timing based on millis() becomes wrong, with no error message. The rule: on
an Arduino board, leave Timer0 alone; manual configuration experiments are done on Timer1 or
Timer2. Only Timer1 has a capture unit and 16-bit compare registers - the only one suited
for fine-grained time measurement.
4Operating modes: normal, CTC, fast PWM, phase-correct PWM11 min
The counter's behavior between two returns to zero defines the operating mode, chosen through the WGM bits in TCCRnA/TCCRnB. The difference concerns the maximum value (TOP), the counting direction and the moment the output switches.
2ⁿ-1, returns to zero and flags
the overflow. The period is fixed, determined only by the clock and the resolution - changing
it requires preloading the counter in every routine, an operation that introduces an error
dependent on interrupt latency.Fast PWM: the counter continuously runs 0 → TOP; the output is active at the start of the cycle, inactive at the match with the compare register. The counter traverses the range only once per period, so the frequency is the highest possible for a given resolution.
Phase-correct PWM: the counter rises to TOP, then falls to zero; the output toggles at every crossing of the compare value, once on each ramp. The resulting pulse is centered on the middle of the period, regardless of the duty cycle - hence the name: the phase does not shift when the pulse width changes. The price: the period becomes roughly double, the frequency is halved compared to fast PWM.
5Ways of accessing timer data9 min
There are three ways the software gets in touch with the timer module.
Directly reading the counter register
uint8_t value8 = TCNT0; /* Timer0's 8-bit counter */
uint16_t value16 = TCNT1; /* Timer1's 16-bit counter */
Does not disturb the counter - a simple snapshot. Useful for timing a code sequence (read before and after) or short timings, without overflow.
TCNT1 is read through two successive instructions (the low byte, then the high one). The hardware partially solves the problem through a buffer register: when the low byte is read, the high byte is automatically copied into a buffer, read right after - but this mechanism only works if the two reads are not separated by an interrupt that in turn accesses another 16-bit register of the same timer, because the buffer is shared. Access from the main program to a 16-bit register of Timer1, if an interrupt accesses the same timer, must be protected by temporarily disabling interrupts.
Polling the status flags
while (!(TIFR1 & (1<<TOV1))) { ; } /* empty loop: cycles are wasted */
TIFR1 = (1 << TOV1); /* clearing is done by writing 1 */
Maximum precision (a reaction within a few cycles, with no interrupt latency), but while the loop
spins, the processor can do nothing else - exactly the flaw of delay(), more visible.
Useful in short, critical sequences that cannot accept the latency of an interrupt.
Using interrupts - the preferred way
Three conditions, all mandatory: individually enabling the source (TIMSK0/1/2), the global
enable (sei()), and a routine written for the correct vector - a vector with no
routine leads to the microcontroller resetting. Three categories of timer interrupts: Overflow
(on overflow), Compare Match (on matching the compare register, evenly spaced in CTC mode) and
Input Capture (on an edge on the capture pin, with the counter automatically stored - the
final section of the lecture).
6The prescaler circuit and the basic formulas10 min
An 8-bit counter fed directly with the 16 MHz clock overflows every
256×62.5 ns = 16 µs, that is 62,500 times a second - practically unusable for
timing, and on every overflow the processor spends cycles on context. The solution: the
prescaler circuit.
The prescaler is a chain of flip-flops that successively divide the frequency by two; a multiplexer
picks the stage. The available values are of the form 2^P: for Timer0 and Timer1,
1, 8, 64, 256, 1024; for Timer2, additionally 32, 128.
T_tick = N/f_clk.CTC mode, event period:
T = N·(OCRnA+1)/f_clk, hence
f = f_clk / (N·(OCRnA+1)).Compare value needed for an imposed period:
OCRnA = f_clk/(N·f) - 1.A large factor extends the time to overflow (long timings with a small counter), but worsens the resolution: if a pulse lasts 64 clock periods, the timing cannot be distinguished more finely than 64 periods (4 µs at 16 MHz). The practical rule: the smallest prescale factor that still covers the desired interval.
ATmega328P, 16 MHz. Configure a timer for an interrupt every millisecond.
See the solution
Clock pulses in one millisecond: f_clk/f = 16×10⁶/1000 = 16000.
With no prescaling (N=1): OCRnA = 16000-1 = 15999 - does not fit in 8 bits, but fits
comfortably in Timer1's 16 bits (maximum resolution, 62.5 ns).
With an 8-bit timer, OCRnA ≤ 255: look for the smallest N with 16000/N - 1 ≤ 255, that is
N ≥ 62.5. From the list, the first that satisfies it: N = 64.
OCRnA = 16×10⁶/(64×1000) - 1 = 250 - 1 = 249 - an integer, fits in 8 bits, an exact
period, with no error. Check: 250 × 4 µs = 1000 µs. ✓
7Application: a one-millisecond time base6 min
volatile uint32_t milliseconds = 0;
void setupTimeBase(void) {
cli();
TCCR1A = 0x00;
TCCR1B = (1 << WGM12); /* CTC mode, TOP given by OCR1A */
OCR1A = 249; /* 16 MHz / (64*250) = 1000 Hz */
TCCR1B |= (1<<CS11)|(1<<CS10); /* prescaler 64: starts the counter */
TIMSK1 = (1 << OCIE1A);
TCNT1 = 0;
sei();
}
ISR(TIMER1_COMPA_vect) { milliseconds++; }
8Characteristics of periodic signals8 min
A signal x(t) is periodic, with period T, if x(t) = x(t+T) for
any t. The frequency is the number of repetitions per second: f = 1/T.
For a rectangular signal, the duty cycle is the ratio between the duration of the
active state T_on and the period: DC = (T_on/T)×100 [%]. 0% = permanently inactive, 100% =
permanently active, 50% = symmetric.
V_avg = Vcc × DC/100. In a DC motor, mechanical
inertia and the winding's inductance smooth the current - the speed is adjusted continuously through the
duty cycle. In an LED, the eye integrates light over ~20 ms - above ~100 Hz, the switching becomes
invisible (below this threshold flicker appears; that is why Arduino's default PWM frequencies, ~490 and
~980 Hz, are above the threshold).The same power reduction could be obtained with a series resistor or a transistor in linear mode, but the difference in power would turn into heat. The PWM switch is either fully open (zero current) or fully closed (nearly zero voltage drop) - the power dissipated by it is small in both states. That is why PWM regulates powers of tens or hundreds of watts with a transistor that barely gets warm. Important: pulse-width modulation does not change the amplitude, only the average value - the load needs enough inertia to average it; applied to a fast load, with no filtering, a PWM signal remains a series of full pulses.
9Fast PWM and phase-correct PWM9 min
In fast PWM, the counter continuously increases 0→TOP; the pin becomes active on the return to zero, inactive on the match with the compare register.
f_PWM = f_clk / (N·(TOP+1)).Duty cycle:
DC = (OCRnx+1)/(TOP+1) × 100%.Resolution (steps):
R = log₂(TOP+1) bits.With TOP=255, N=64, f_clk=16 MHz:
f_PWM ≈ 976.6 Hz - exactly the frequency of
analogWrite() on the Uno's pins 5 and 6, with 8 bits of resolution (256 steps).The product of the PWM frequency and the number of steps is constant, equal to
f_clk/N - a high frequency and a high resolution cannot both be obtained at once without
raising the clock or reducing the prescale.
In phase-correct PWM, the counter traverses the range twice per period (up,
down): f_PWM = f_clk / (2·N·TOP). On 8-bit AVR modules, the number of pulses per
period is 510 (=2×255) - with N=64, this gives ≈490.2 Hz, the frequency of
analogWrite() on pins 3, 9, 10, 11: roughly half of fast PWM, at the same
resolution - the price paid for fixing the phase.
10Implementing PWM and the link to analogWrite()9 min
void setupPWM2(void) {
DDRB |= (1 << PB3); /* OC2A = pin 11 on the Uno */
/* WGM21:WGM20=11 -> Fast PWM, TOP=0xFF; COM2A1:COM2A0=10 -> non-inverted */
TCCR2A = (1<<WGM21)|(1<<WGM20)|(1<<COM2A1);
TCCR2B = (1 << CS22); /* prescaler 64: 16MHz/(64*256) = 976.6 Hz */
OCR2A = 25; /* 26/256 ~ 10% duty cycle */
}
void setDutyCycle(uint8_t percent) {
OCR2A = (uint16_t)percent * 255 / 100;
}
The COM2A1:COM2A0 bits choose a non-inverted output (active from the start of the period until the match), inverted (a complementary pair, a load driven active-low), or disconnected (the pin stays an ordinary GPIO, but the module keeps counting and can still generate interrupts).
What analogWrite() does
Identifies the timer associated with the requested pin, enables the compare block (the COM bits) and writes the argument into the compare register - the name is poorly chosen: the output is not analog but digital, switching between 0 V and 5 V, only the average value varies continuously. The argument has 8 bits because the 8-bit timers' compare registers have 8 bits - a hardware limitation, not one of the function. The frequency cannot be changed through analogWrite() (it does not touch the prescale bits). Only six pins on the Uno support it, because there are only six compare outputs in hardware.
If the compare value were changed exactly when the counter had just passed it, the current pulse would be distorted, could be missing, or could extend over a whole extra period. AVR modules use buffer registers: the written value is held and transferred into the active register only at the start of a new period - the mechanism eliminates most timing errors, but not all; in modes with a variable TOP, a change at the wrong moment can still produce a cycle of the wrong duration.
11Capturing external events13 min
The reverse problem of generation: determining the exact moment an external event occurred, solved by the capture unit (input capture).
Pin ICP1 (digital pin 8 on the Uno) is continuously watched by hardware. On an edge of the configured direction, the instantaneous value of the counter is automatically copied into ICR1, the ICF1 flag is set and, if enabled, an interrupt request is generated.
The rule for choosing the prescaler: the timing error grows directly with the prescale factor -
for captures, the smallest possible prescaler that does not exceed the measurement range is chosen
(Δt_max = (2^r - 1)·N/f_clk).
The sensor emits a burst of ultrasound and activates an output line for exactly as long as the
round trip of the wave takes. Distance: d = v_sound·Δt/2, with v_sound ≈ 343 m/s.
ISR(TIMER1_CAPT_vect) {
if (TCCR1B & (1<<ICES1)) { /* rising edge */
capture1 = ICR1;
TCCR1B &= ~(1<<ICES1); /* next: falling */
} else { /* falling edge */
width = ICR1 - capture1;
TCCR1B |= (1<<ICES1);
ready = 1;
}
TIFR1 = (1 << ICF1);
}
The usual beginner solution, pulseIn(), blocks the processor for tens of milliseconds and
is affected by any interrupt occurring during that interval; with the capture unit, the measurement is
done in hardware, and the program loses only a few microseconds in the two routines.
See the resolution and range calculation (prescaler 8)
T_tick = 8/16×10⁶ = 0.5 µs. One tick corresponds to Δd = 343×0.5×10⁻⁶/2 ≈ 0.086
mm - far below the sensor's own accuracy, so temporal resolution is not the
limiting factor.
The maximum range: Δt_max = 65535×0.5 µs ≈ 32.8 ms, that is d_max ≈ 5.6 m -
above the sensor's useful range (a few meters). Prescaler 8 is suitable: needlessly good resolution,
sufficient range. With prescaler 1, the range would drop to 4.1 ms (~70 cm) - not enough.
ICR1 - capture1 under unsigned arithmetic stays correct even if the counter has
wrapped through zero between the two captures. ICF1 is explicitly cleared after switching the
edge direction, because the switching operation can itself set the flag, producing a
false interrupt right after leaving the routine.
12Frequent mistakes4 min
- "On an Arduino board I can reconfigure any timer, including Timer0, with no side effects." False - Timer0 feeds millis(), micros() and delay(). Reconfiguring its prescaler changes every timing based on millis(), with no visible error message. Manual timer configuration experiments are done on Timer1 or Timer2, not on Timer0.
- "analogWrite() gives a real analog output." False - the output switches digitally between 0 V and 5 V; only its average value, as perceived by a load with inertia, varies continuously with the duty cycle. Treat PWM as a digital signal with a variable duty cycle, not as a real analog voltage.
- "To measure the width of a pulse, I read the counter right inside the external interrupt routine triggered by the edge." False - the value read is affected by interrupt latency (finishing the current instruction, saving the context), which varies by dozens of cycles and cannot be compensated. Use the capture unit (Timer1 + ICP1), which stores the counter in hardware, exactly at the moment of the edge.
13Summary and glossary5 min
A timer is, at its core, a hardware counter fed through a prescaler from a stable clock source. Three mechanisms derive from it: comparing the counter to a threshold and clearing it automatically (CTC mode) gives exact periodic interrupts, replacing delay(); driving a pin based on the same comparison gives a PWM signal with an adjustable duty cycle, with much better efficiency than lowering the voltage with a series resistor; storing the counter on an external edge (input capture) gives exact timestamping of events, with a precision software alone cannot reach. The obtained period or frequency always results from three numbers - the clock frequency, the prescale factor, the compare value - and choosing them is a trade-off between resolution and range. Timer modules are resources shared with the Arduino libraries: any reconfiguration must be done knowing which functions, from that moment on, stop telling the truth.
14Self-check questions7 min
- Explain the difference between the timer mode and the counter mode of the same hardware module. What changes in the configuration when moving from one to the other?
- Why is delay() a waste of resources? Concretely describe what is lost during the wait.
- An 8-bit counter is fed directly with the 16 MHz clock. After how long does the overflow occur, and how many times a second would the interrupt routine be called?
- Why are the available prescale factors powers of two, not arbitrary values? What is lost with a prescale factor larger than needed?
- An interrupt is needed every 10 ms, with Timer1 at 16 MHz, in CTC mode. Calculate OCR1A for a prescale factor of 64 and check whether the result is achievable.
- Compare normal mode with CTC mode from the point of view of the precision of the generated intervals.
- A PWM signal has a period of 2 ms and an active state of 0.4 ms. Calculate the frequency, the duty cycle, and the average voltage at an amplitude of 5 V.
15Directions for further study2 min
The next lecture uses exactly the same compare mechanism of a timer, applied to the reverse problem of generation - turning a continuous analog voltage into a number: the analog-to-digital converter.
The prescaling formulas, PWM generation and event capture from this lecture become a real setup in Laboratory 04.