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 SCB→VTOR 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
- Keep ISRs short. Set a flag or push data to a queue, do heavy work in
main()or an RTOS task. - No blocking calls. Never
printf,malloc,delay, or take a mutex inside an ISR. - Use
volatilefor any variable shared between ISR and main code. See Memory-Mapped IO. - Clear the interrupt flag inside the ISR, or it fires again immediately.
- 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; // startSysTick 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)Related
- GPIO and Digital IO — EXTI interrupts on pin edges
- UART SPI I2C — peripheral interrupts for data transfer
- RTOS Fundamentals — SysTick drives the RTOS scheduler tick
- Bare Metal vs RTOS — ISR+flag pattern is the bridge between bare metal and RTOS
- Concurrency and Synchronization — same race condition problems exist in ISR context
- Scheduling — timer interrupts are the mechanism behind preemptive scheduling