Day 3 of 5
⏱ ~60 minutes
Firmware Development in 5 Days — Day 3

Interrupts

Interrupts let the CPU respond instantly to hardware events without polling. Today you'll write ISRs, configure the NVIC, and handle interrupt safety correctly.

How Interrupts Work

When a hardware event occurs (button press, timer overflow, UART byte received), the CPU: saves current state (PC, registers pushed to stack), looks up the ISR address in the vector table, executes the ISR, returns to the interrupted code via RETURN_FROM_EXCEPTION. This is transparent to your main code — it just sees time passing. The key rule: ISRs must be short and fast. Never block, never call printf, never allocate memory in an ISR.

NVIC: Nested Vectored Interrupt Controller

The NVIC manages interrupt priorities (0=highest on ARM Cortex-M). Configure with: NVIC_SetPriority(IRQn, priority), NVIC_EnableIRQ(IRQn). Priorities enable nesting: a high-priority ISR can interrupt a lower-priority one. Beware: if two peripherals share priority and interrupt simultaneously, order is undefined. Use lower numbers for time-critical ISRs (fault detection, safety shutdown) and higher numbers for non-critical ones (UART logging).

Volatile and Atomic Operations

Variables shared between ISRs and main code must be declared volatile — this prevents the compiler from caching them in a register. But volatile alone doesn't make operations atomic. A 32-bit write on ARM Cortex-M3 is atomic for aligned addresses. A 64-bit value or non-aligned write is not atomic — disable interrupts briefly with __disable_irq()/__enable_irq() when reading/writing multi-word values from main code.

c
// Interrupt-driven GPIO and SysTick
#include "stm32f1xx.h"

// Volatile: shared between ISR and main
volatile uint32_t systick_ms = 0;
volatile uint8_t  button_pressed = 0;
volatile uint32_t press_count = 0;

// SysTick ISR — runs every 1ms
void SysTick_Handler(void) {
    systick_ms++;  // atomic on ARM for aligned 32-bit
}

// EXTI0 ISR — fires on PA0 rising edge (button)
void EXTI0_IRQHandler(void) {
    if (EXTI->PR & EXTI_PR_PR0) {  // confirm pending flag
        press_count++;
        button_pressed = 1;
        EXTI->PR = EXTI_PR_PR0;    // clear pending flag (write 1)
    }
}

uint32_t millis(void) {
    return systick_ms;
}

void delay(uint32_t ms) {
    uint32_t start = millis();
    while ((millis() - start) < ms);
}

void init_systick(void) {
    // 1ms tick at 72MHz: reload = 72000 - 1
    SysTick_Config(72000);
}

void init_button_interrupt(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
    // PA0 as input floating
    GPIOA->CRL &= ~(0xF << 0);
    GPIOA->CRL |=  (0x4 << 0);
    // Route PA0 to EXTI0
    AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;
    // Enable EXTI0, rising edge trigger
    EXTI->IMR  |= EXTI_IMR_MR0;
    EXTI->RTSR |= EXTI_RTSR_TR0;
    NVIC_SetPriority(EXTI0_IRQn, 1);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

int main(void) {
    init_systick();
    init_button_interrupt();
    uart1_init(9600);  // from Day 2

    while (1) {
        if (button_pressed) {
            button_pressed = 0;  // atomic clear
            uart1_puts("PRESS!
");
        }
        // Toggle LED every 500ms using millis()
        static uint32_t last = 0;
        if (millis() - last >= 500) {
            last = millis();
            GPIOC->ODR ^= (1 << 13);  // toggle PC13
        }
    }
}
💡
Always clear the interrupt pending flag in the ISR. If you don't, the ISR re-enters immediately after returning — creating an infinite loop of ISR calls. For EXTI, write 1 to the pending register bit to clear it (ARM uses write-1-to-clear semantics for many status registers).
📝 Day 3 Exercise
Build an Interrupt-Driven Encoder Counter
  1. Connect a rotary encoder to PA0 (A) and PA1 (B). Configure EXTI0 for both rising and falling edges on PA0.
  2. In the ISR, read the state of PA1 to determine direction: PA1=HIGH when PA0 rises = clockwise; PA1=LOW = counterclockwise.
  3. Maintain a volatile int32_t position counter. Increment on clockwise, decrement on counterclockwise.
  4. In main(), print the position over UART whenever it changes (use a local copy and compare to detect change).
  5. Add velocity measurement: track the time between pulses (using SysTick) to calculate RPM.

Day 3 Summary

  • ISRs must be short: no blocking, no printf, no malloc — just set a flag or write to a buffer
  • NVIC priorities: lower number = higher priority; configure time-critical ISRs with priority 0–2
  • Shared variables between ISR and main: always volatile; multi-word values: disable interrupts during access
  • Clear interrupt pending flags inside ISR — failure to do so causes immediate ISR re-entry
Challenge

Implement a software UART transmitter using a timer interrupt. Configure TIM3 to fire at the baud rate frequency (e.g., 9600Hz). In the ISR, output each bit of the current byte being transmitted to a GPIO pin. Handle start bit, 8 data bits (LSB first), and stop bit. Test by connecting to a serial terminal. This is how bit-banged UART works — you'll never take hardware UART for granted again.

Finished this lesson?