Interrupts let the CPU respond instantly to hardware events without polling. Today you'll write ISRs, configure the NVIC, and handle interrupt safety correctly.
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.
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).
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.
// 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
}
}
}
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.