Interrupts and Timers

An interrupt is a hardware signal that preempts normal code execution to run an Interrupt Service Routine (ISR). Timers are peripherals that count clock cycles, generating interrupts at precise intervals. Together they enable event-driven embedded systems without busy-waiting.

Why It Matters

Polling wastes CPU cycles. Interrupts let the MCU sleep or do useful work until an event occurs — a button press, a byte received, a timer expiring. Timers are the backbone of PWM output, periodic sampling, watchdogs, and RTOS tick generation.

How It Works

Interrupt Vector Table

The vector table is an array of function pointers at the start of Flash. Each entry corresponds to one interrupt source. On Cortex-M, the first two entries are special:

Address     Entry              Handler
0x0000_0000 Initial SP         (stack pointer value, not a function)
0x0000_0004 Reset              Reset_Handler
0x0000_0008 NMI                NMI_Handler
0x0000_000C HardFault          HardFault_Handler
...
0x0000_0058 EXTI0              EXTI0_IRQHandler       <- GPIO PA0 interrupt
0x0000_00A0 TIM2               TIM2_IRQHandler        <- Timer 2
0x0000_00D0 USART1             USART1_IRQHandler      <- UART receive

The vector table can be relocated via the SCBVTOR register (useful for bootloaders that jump to application code at a different Flash offset).

NVIC (Nested Vectored Interrupt Controller)

Every Cortex-M has an NVIC that manages all peripheral interrupts:

  Peripheral IRQ --> ┌──────┐    ┌─────┐
  Peripheral IRQ --> │ NVIC │--> │ CPU │ <- runs highest-priority pending ISR
  Peripheral IRQ --> └──────┘    └─────┘
                       ^
              Priority registers
            (0 = highest priority)
  • Priority grouping: Cortex-M splits the priority byte into preempt priority (determines nesting) and sub-priority (breaks ties). Configured via NVIC_SetPriorityGrouping().
  • Nesting: A higher-priority interrupt can preempt a running lower-priority ISR. The preempted ISR resumes when the higher-priority one returns.
  • Tail chaining: If another interrupt is pending when an ISR finishes, the CPU skips the full context restore/save and jumps directly to the next ISR (6 cycles vs 12).

ISR Best Practices

  1. Keep ISRs short. Set a flag or push data to a queue, do heavy work in main() or an RTOS task.
  2. No blocking calls. Never printf, malloc, delay, or take a mutex inside an ISR.
  3. Use volatile for any variable shared between ISR and main code. See Memory-Mapped IO.
  4. Clear the interrupt flag inside the ISR, or it fires again immediately.
  5. Avoid read-modify-write on shared data. Use atomic operations or disable interrupts briefly.
// Pattern: ISR sets flag, main loop does work
volatile uint8_t data_ready = 0;
 
void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        rx_buffer[rx_idx++] = USART1->DR;   // read clears RXNE flag
        data_ready = 1;
    }
}
 
int main(void) {
    while (1) {
        if (data_ready) {
            data_ready = 0;
            process_data(rx_buffer);
        }
        __WFI();  // sleep until next interrupt (saves power)
    }
}

Timers

A timer counts clock pulses. When the count reaches the auto-reload value, it generates an update event (interrupt, DMA request, or output toggle).

                    Prescaler           Counter
PCLK (84 MHz) --> [  / (PSC+1)  ] --> [0 -> ARR] --> Update Event (interrupt)
                                           |
                                       Compare --> Output Compare / PWM

Frequency calculation:

Timer frequency = PCLK / ((PSC + 1) * (ARR + 1))

Example: 1 ms periodic interrupt from an 84 MHz APB1 clock:

// TIM2 on APB1 = 84 MHz (STM32F4 with APB1 prescaler /4, timers x2)
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
 
TIM2->PSC = 84 - 1;       // 84 MHz / 84 = 1 MHz tick
TIM2->ARR = 1000 - 1;     // 1 MHz / 1000 = 1 kHz = 1 ms period
TIM2->DIER |= TIM_DIER_UIE;   // enable update interrupt
TIM2->CR1  |= TIM_CR1_CEN;    // start counting
 
NVIC_SetPriority(TIM2_IRQn, 3);
NVIC_EnableIRQ(TIM2_IRQn);
 
void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;   // clear flag
        tick_count++;
    }
}

PWM Generation

PWM uses the output compare function. The timer counts 0 to ARR. While counter < CCR, the output is HIGH; above CCR, it is LOW.

ARR = 999
CCR = 700

Output:  HIGH                      LOW
         ========================..........
         0                   700       999

Duty cycle = CCR / (ARR + 1) = 700/1000 = 70%
Frequency  = PCLK / ((PSC+1) * (ARR+1))
// PWM on PA5 (TIM2_CH1, AF1)
GPIOA->MODER |= (2 << (5 * 2));       // alternate function
GPIOA->AFR[0] |= (1 << (5 * 4));      // AF1 = TIM2
 
TIM2->PSC  = 84 - 1;                  // 1 MHz tick
TIM2->ARR  = 1000 - 1;                // 1 kHz PWM frequency
TIM2->CCR1 = 700;                     // 70% duty cycle
TIM2->CCMR1 = (6 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE;  // PWM mode 1
TIM2->CCER |= TIM_CCER_CC1E;          // enable channel 1 output
TIM2->CR1  |= TIM_CR1_CEN;            // start

SysTick Timer

24-bit down-counter built into every Cortex-M core. Typically used for a 1 ms system tick:

// Configure SysTick for 1 ms at 168 MHz
SysTick->LOAD = 168000 - 1;       // 168 MHz / 168000 = 1 kHz
SysTick->VAL  = 0;                // clear current value
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk  // use processor clock
              | SysTick_CTRL_TICKINT_Msk     // enable interrupt
              | SysTick_CTRL_ENABLE_Msk;     // start
 
volatile uint32_t sys_ticks = 0;
void SysTick_Handler(void) { sys_ticks++; }
void delay_ms(uint32_t ms) {
    uint32_t start = sys_ticks;
    while (sys_ticks - start < ms);
}

Watchdog Timer

The Independent Watchdog (IWDG) resets the MCU if software hangs. It runs from a separate low-speed oscillator (LSI, ~32 kHz) and must be “fed” periodically:

IWDG->KR  = 0x5555;       // unlock registers
IWDG->PR  = 4;            // prescaler /64
IWDG->RLR = 500;          // reload value -> ~1 second timeout
IWDG->KR  = 0xCCCC;       // start watchdog
 
// In main loop:
IWDG->KR = 0xAAAA;        // feed the dog (reset counter)