JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Reacting on Time: Interrupts and Timers

Imagine a receptionist who answers every phone call instantly — yet never once stops typing the report on her desk to check whether the phone *might* ring. That magic trick is what [[ee-interrupt|interrupts]] do for a [[ee-microcontroller|microcontroller]], and a [[ee-timer-counter|timer]] is the metronome that lets it keep perfect rhythm without watching the clock. In this guide you'll stop wasting cycles polling, and start reacting on time.

The tyranny of the polling loop

In rung 3 you read a button by *polling*: the CPU runs a loop that, millions of times per second, asks the GPIO pin "are you pressed yet? are you pressed yet?" It works, but think about what it costs. While your code is glued to that one pin, it cannot also watch a sensor, drive a motor, or update a display. And the instant your loop wanders off to do something else for 50 ms, a button press that lasts only 30 ms slips through unseen. Polling is a security guard who can only stare at one door — and blinks at the worst possible moment.

The deeper problem is *waste*. A modern MCU might tick at 48–480 MHz, but a human finger or a 10 Hz sensor speaks at a glacial pace by comparison. A polling loop burns hundreds of millions of instructions per second waiting for something that happens a handful of times. On a battery, that is wasted firmware effort turned directly into a dead cell. We need a way for the hardware to *come tell us* when something happens — so the CPU can sleep, or work, until then.

What an interrupt actually does

An interrupt is a hardware-arranged pause button on your program. When a configured event fires — a pin changes level, a timer overflows, a byte arrives on the UART — a dedicated block called the interrupt controller (on an Arm Cortex-M part, the NVIC) freezes the CPU mid-instruction-stream. It saves the program's place, looks up the address of a small function you wrote called the Interrupt Service Routine (ISR), jumps there, runs it, and then restores everything and resumes the main program exactly where it left off. The main code never even knows it was interrupted — like being teleported away to handle an emergency and teleported back into the same sentence you were typing.

  main program running ......................
     |  add r0,r1     ldr r2,[r3]   str ...   <-- normal flow
     |                     ^
  ===|=====================|=========================
     |   EVENT! (e.g. timer overflow) fires here
     v                     |
   1. NVIC saves PC, status, r0-r3 onto the stack (auto)
   2. NVIC fetches ISR address from the vector table
   3. CPU jumps to TIM2_IRQHandler()  <-- your ISR
        clear the interrupt flag
        toggle the LED
        return
   4. NVIC pops the saved registers back
   5. CPU resumes the very next instruction: str ...
  =================================================
  main program continues, none the wiser ...........
The interrupt life cycle on a Cortex-M: hardware saves and restores context around your ISR automatically.

Two numbers define whether an interrupt is useful: latency and jitter. Latency is the delay from the event to the first line of your ISR — on a Cortex-M it is famously deterministic at around 12 clock cycles, so at 48 MHz that's a quarter of a microsecond. Jitter is how much that delay *wobbles* from one event to the next, which depends on what else the CPU was doing. A motor-control loop that must sample current at a precise instant lives and dies by low, predictable latency.

The timer/counter: a metronome in silicon

If interrupts are the ears of an MCU, the timer/counter is its heartbeat. Strip away the marketing and a timer is breathtakingly simple: a register that counts up by one on every tick of a clock. That's it. All its power comes from what you wire around that counter — a *prescaler* that divides the clock down before it reaches the counter, and an *auto-reload* (or *period*) register that says "when you reach this value, wrap back to zero and raise a flag."

Those two knobs turn one fast clock into any timing you want. The math is the whole game: the timer raises an interrupt every (PSC + 1) × (ARR + 1) clock cycles. Suppose your MCU's timer runs from a 48 MHz clock and you want a tick every 1 ms (1000 Hz). You need to count 48,000 cycles per millisecond. Split that into a prescaler of 48 (the +1 makes it divide by 48, giving a 1 MHz counting clock — one count per microsecond) and a period of 1000. Clean, exact, and drift-free, because it's anchored to the crystal, not to how fast your code happens to run.

  Goal: a 1 ms (1000 Hz) timer tick from a 48 MHz clock
  --------------------------------------------------------
    f_tick = f_clk / ( (PSC+1) * (ARR+1) )
    1000   = 48e6  / ( (PSC+1) * (ARR+1) )
    => (PSC+1)*(ARR+1) = 48000

  Pick PSC+1 = 48  -> counting clock = 48e6/48 = 1 MHz (1 us/count)
       ARR+1  = 1000 -> overflow every 1000 us = 1.000 ms  exact

  So:  PSC = 47 ,  ARR = 999

  Counter:  0,1,2, ... ,998,999, [overflow! IRQ + reload], 0,1,2,...
             |<----------- 1000 counts = 1.000 ms ----------->|
Prescaler and auto-reload turn a 48 MHz crystal into a precise 1 ms heartbeat.

The same counter hardware does triple duty. Run it free and read its value to *measure* how long something took (a stopwatch). Feed it an external pulse train instead of the clock and it becomes a *counter* of events. And compare its value against a threshold on every tick to flip a pin high then low — that's exactly the PWM you built in rung 2, generated entirely in hardware while the CPU sleeps. One peripheral, three superpowers.

Worked example: a hardware-blinked LED

Let's earn the theory. The goal: blink an LED at a steady 1 Hz (on 500 ms, off 500 ms) *while the CPU is free to do real work* — say, run a slow computation or talk to a sensor. We'll configure a timer to interrupt every 1 ms, count those ticks in the ISR, and toggle the LED every 500 ticks. Notice how short the ISR is: it does the absolute minimum and gets out.

  1. Enable the clocks. Turn on the clock to the timer peripheral and to the GPIO port driving the LED — peripherals are dead silicon until they're clocked.
  2. Configure the timer. Set PSC = 47 and ARR = 999 for a 1 ms period from a 48 MHz clock, then enable the *update interrupt* (the overflow event).
  3. Wire up the NVIC. Enable the timer's interrupt line in the interrupt controller and give it a priority, so the CPU knows to listen.
  4. Start the timer and let `main()` fall into its own work loop. From here the LED runs itself.
volatile uint32_t ms_ticks = 0;   // shared with main: must be volatile

void TIM2_IRQHandler(void) {       // the ISR: runs every 1 ms
    TIM2->SR &= ~TIM_SR_UIF;       // 1. CLEAR the flag (or it re-fires!)
    ms_ticks++;                    // 2. one millisecond has passed
    if (ms_ticks % 500 == 0)       // 3. every 500 ms ...
        GPIOA->ODR ^= (1 << 5);    //    ... toggle the LED pin. Done.
}

int main(void) {
    clocks_enable();               // step 1
    TIM2->PSC = 47;                // step 2: 48 MHz / 48 = 1 MHz
    TIM2->ARR = 999;               //         1 MHz / 1000 = 1 kHz = 1 ms
    TIM2->DIER |= TIM_DIER_UIE;    //         enable update interrupt
    NVIC_EnableIRQ(TIM2_IRQn);     // step 3: let the NVIC route it
    TIM2->CR1 |= TIM_CR1_CEN;      // step 4: start counting

    while (1) {
        do_real_work();            // CPU is 100% free here.
        // The LED keeps perfect time even if this is slow,
        // because the TIMER, not this loop, defines the rhythm.
    }
}
A 1 ms timer ISR blinks an LED at 1 Hz while main() does unrelated work — the heartbeat lives in hardware.

Priorities, nesting, and the art of the short ISR

Real systems have many interrupt sources, and they won't politely take turns. A byte arrives on the UART at the same microsecond the timer overflows and the motor's fault pin trips. Who goes first? That's what priority decides. You assign each source a priority number, and the interrupt controller serves the most urgent first. On a Cortex-M, a higher-priority interrupt can even *preempt* one already running — an ISR interrupting an ISR, called nesting — so a hard-real-time safety event never waits behind a lazy housekeeping routine.

This is why the golden rule of embedded is: keep ISRs short. Every microsecond you spend in an ISR is a microsecond that every lower-priority interrupt is blocked and that jitter creeps up for everyone else. An ISR should do three things and nothing more: clear the flag, grab the time-critical data (read the sensor sample, push the byte into a buffer), and set a flag for the main loop to do the heavy lifting later. Never call a slow function, never wait, never print, never spin a delay loop inside an ISR — that's like answering the emergency phone and then reading the caller a novel while every other line rings.

  GOOD ISR (top half)            BAD ISR (does everything inline)
  ------------------------       -------------------------------
  void ADC_IRQ(void){            void ADC_IRQ(void){
    clear_flag();                  clear_flag();
    buf[i++] = ADC->DR;            float v = filter(ADC->DR); // slow!
    if(i==N) ready = 1;            printf("v=%f\n", v);       // VERY slow!
  }   // ~1 us, returns fast       delay_ms(2);               // disaster
                                 }   // blocks everything for ms
  main loop later:
    if(ready){ heavy_process(buf); ready=0; }   // do the work here
The "top half / bottom half" pattern: the ISR grabs data fast; the main loop does the heavy processing.

Pitfalls that bite everyone once

Interrupts are powerful precisely because they break the comfortable illusion that your code runs top-to-bottom. That power has sharp edges, and every embedded engineer has the scars. The good news: the failures are stereotyped, so you can recognize them on sight.

  1. The unclearned flag. The ISR fires once and then forever, hanging the system. *Always* clear the source flag, ideally as the first line.
  2. The forgotten `volatile`. Main code reads a stale cached value of a shared variable and never sees the ISR's updates. Mark every ISR-shared variable `volatile`.
  3. The race condition. Main code reads a multi-byte shared value while the ISR is half-way through writing it. Disable interrupts briefly around the read, or use an atomic access.
  4. The blocking ISR. A `delay()`, a `printf()`, or a slow loop inside an ISR starves every other interrupt and explodes your jitter. Keep it short; defer the work.
  5. Stack overflow from nesting. Deeply nested high-priority ISRs each push context onto the stack; size your stack for the worst case, not the average.

Step back and the whole picture snaps into focus. The clock is the rhythm of the chip; the timer divides that rhythm into precise, drift-free intervals; the interrupt lets real-world events and timer overflows seize the CPU's attention the instant they matter; and short ISRs plus the main loop split the labor so the system stays responsive on every front at once. With that, your microcontroller stops *waiting* and starts *reacting* — which is the entire reason it has a heartbeat.