ALearning Material
Naively, to react to an event you poll: loop forever asking "is the button pressed yet?" This wastes CPU and can miss fast events while you're busy elsewhere.
The naive way to react to an event is to poll. Loop forever asking 'is it pressed yet?', which wastes the CPU and can miss a fast event while you're busy doing something else. Interrupts invert that relationship: you tell the hardware 'when event X happens, immediately pause everything and run this small function, then resume', so the CPU does useful work (or sleeps) until the event actually occurs.
That power comes with strict rules, and getting them wrong produces baffling bugs. An interrupt
service routine must be short (flag it and get out) because the rest of the system is frozen
while it runs. And any variable shared between the ISR and your main code must be volatile, or
the compiler may cache a stale copy in a register and never notice the ISR changed it. Interrupts,
short ISRs, and volatile are the trio that make event-driven firmware both responsive and
correct.
Interrupts invert this. You tell the hardware "when event X happens, immediately pause whatever you're doing and run this small function (an ISR, Interrupt Service Routine), then resume." The CPU does useful work (or sleeps) until the event actually occurs.
volatile bool pressed = false; // shared with the ISR -> must be volatile
void buttonISR() { // runs the instant the pin changes
pressed = true; // do the minimum; flag and exit
}
void setup() {
attachInterrupt(BUTTON_PIN, buttonISR, FALLING); // trigger on HIGH->LOW
}
void loop() {
if (pressed) { // main code reacts when convenient
pressed = false;
toggleLED();
}
}
ISR rules (critical):
- Keep ISRs short. Set a flag, copy a value, exit. No delays, no heavy work. The
rest of the system is frozen while the ISR runs.
- Shared variables must be volatile. It tells the compiler the value can change
outside normal flow (in the ISR), so it always re-reads it from memory instead of
caching it in a register.
Timers are hardware counters that tick from a clock. They enable precise timing without blocking: - Generate a periodic interrupt (e.g. "interrupt me every 1 ms") → the basis of fixed-rate control loops on bare metal. - Produce PWM (Pulse-Width Modulation). A square wave whose duty cycle (% of time HIGH) sets average power. 25% duty ≈ 25% brightness/speed. This is how MCUs dim LEDs and drive motors.
delay() vs timers. delay(1000) blocks the whole CPU for a second, nothing
else runs. Real embedded code uses timer interrupts or a non-blocking "check the
clock" pattern (millis()), so multiple things happen "at once."
Why it exists. A robot must react the instant something happens (a limit switch, an encoder edge, a deadline) while still doing other work. Polling wastes the CPU and misses fast events; interrupts and timers are how bare-metal code reacts immediately and keeps precise time without blocking.
Mental model. Polling is repeatedly opening the oven to check the food. An interrupt is a timer bell that calls you the instant it's done, so you can do other chores meanwhile. A PWM duty cycle is like rapidly flicking a light switch. Flick it on 30% of the time and it looks ~30% as bright.
Common misunderstandings.
- "Do the real work inside the ISR." Keep ISRs tiny. Set a flag and exit; the whole system is frozen while an ISR runs.
- "A normal variable shared with an ISR is fine." It must be
volatile, or the compiler may cache a stale copy and never see the ISR's change. - "
delay(1000)is a harmless pause." It blocks the entire CPU for a second. Use timers /millis()so other things keep running.
Connections. The "flag in the ISR, react in the loop" pattern is the seed of RTOS deferred-interrupt processing (Turn 2); volatile is the bare-metal cousin of C++'s std::atomic (concurrency); and timer-driven periodic interrupts are the fixed-rate loop from Python Turn 1. PWM duty cycle is how a PID output drives a real motor.
BImmediate Active Recall
QUERYPolling vs interrupt. What's the core difference?
REVEAL
Polling repeatedly checks for an event, wasting CPU and risking misses; an interrupt lets hardware call your code the moment the event happens, so the CPU is free until then.
QUERYTwo golden rules for writing an ISR?
REVEAL
Keep it as short as possible (flag-and-exit, no delays/heavy work), and mark any variable shared with the main code as volatile.
QUERYWhat is PWM duty cycle and what does it control?
REVEAL
The percentage of each period the signal is HIGH; it sets the average power delivered, e.g. LED brightness or motor speed.
QUERYWhy is delay(1000) bad in responsive embedded code?
REVEAL
delay(1000) bad in responsive embedded code?It blocks the CPU entirely for the duration, so nothing else (other tasks, checks) can run; use timer interrupts / non-blocking millis() patterns instead.
CConceptual Questions
Answer each in your own words in the box, then reveal the model answer to compare. These ask why, not how, and your answers are saved.
Why are interrupts better than polling for reacting to a button or sensor event?
REVEAL MODEL ANSWER
Polling loops repeatedly asking 'has it happened yet?', burning CPU cycles and potentially missing a brief event while the code is busy elsewhere. An interrupt lets the CPU do other work or sleep, and the hardware diverts to the ISR the instant the event occurs, so it never misses the edge and wastes no cycles waiting. Efficiency plus guaranteed capture.
Why must an ISR be kept short, flag and exit?
REVEAL MODEL ANSWER
While the ISR runs, the rest of the system is effectively frozen: the main code is paused and other interrupts may be blocked. Doing heavy work or calling delays inside an ISR starves everything else and can cause missed events or sluggish response. The correct pattern is to do the minimum (set a flag or copy a value) and exit, handling the real work back in the main loop.
Why must a variable shared between an ISR and main code be declared volatile?
REVEAL MODEL ANSWER
volatile tells the compiler the value can change outside the normal program flow (inside the ISR), so it must re-read it from memory every time instead of caching it in a register. Without volatile, the main loop may keep using a stale cached copy and never see the ISR's update. A classic 'it never reacts' bug.
DPractice Problems
P1 (easy). You need an LED to blink while also reading a sensor continuously.
Should you use delay() or a timer/millis() approach? Why?
P2 (medium). A flag set in an ISR is sometimes ignored by the main loop even though the interrupt fired. What keyword was likely forgotten, and why does it matter?
P3 (harder). You want a motor at ~40% speed via PWM at 1 kHz. Conceptually, what duty cycle, and what does raising the frequency (not duty) change?
Solutionsclick to reveal
P1. A timer/millis() non-blocking approach. delay() would freeze the CPU
during each blink interval, halting sensor reads; checking elapsed time lets both run
"simultaneously."
P2. volatile. Without it the compiler may cache the variable in a register and
never see the ISR's update, so the main loop reads a stale value. volatile forces a
fresh read from memory each time.
P3. ~40% duty cycle (HIGH 40% of each period). Raising the frequency keeps the same average power but switches faster. Reducing audible whine/flicker and affecting motor smoothness/heating, without changing speed (duty does that).
EFeynman Exercise
Explain interrupts to a beginner with the oven-bell analogy: why waiting by the oven (polling) wastes your time, and how a bell (interrupt) frees you. Then explain PWM by describing how flicking a switch on and off very fast can make a light look half as bright, and what "duty cycle" names in that picture.
REVEAL MODEL ANSWER
Polling is like getting up to check the front door every ten seconds in case someone's there. You get nothing else done and might still miss a quick knock. An interrupt is a doorbell: you go about your work and only stop, briefly, the instant it rings, then carry on. The rule is to answer the bell fast and get back to work, if you stand at the door chatting (a long ISR), the phone rings, the kettle boils, and everything else in the house grinds to a halt while you're stuck there.
FError Analysis Framework
- Heavy ISRs. Why: doing work in the ISR. Recognise: system stutters, missed interrupts. Avoid: flag-and-exit; process in main loop.
- Missing
volatile. Why: unaware of compiler caching. Recognise: main loop ignores ISR updates. Avoid:volatileon all ISR-shared data. - Blocking with
delay(). Why: simplicity. Recognise: unresponsive system. Avoid: timers /millis()non-blocking patterns. - Confusing duty and frequency. Why: both "PWM knobs." Recognise: changing frequency to set speed. Avoid: duty = power, frequency = switching rate.
GMini Challenge
Your main loop does while(!pressed){} where pressed is set in a button ISR, but it never exits
even when you press the button. What keyword is missing, and why does adding it fix the problem?
REVEAL MODEL ANSWER
pressed must be declared volatile. Without it, the compiler is allowed to read pressed
once into a register at the top of the loop and reuse that cached value forever, so it never sees
the ISR write the new value to memory. The loop spins on a stale copy. Marking it volatile
forces a fresh read from memory on every check, so the loop sees the ISR's update and exits when
the button is pressed.
Quiz Check
A quick auto-graded check, separate from the recall cards above. Your score is pooled with the recall cards into this module's Mastery score, and completing this lesson requires the quiz submitted with pooled mastery at 80% or above.