0
0
Embedded Cprogramming~15 mins

Writing an ISR (Interrupt Service Routine) in Embedded C - Deep Dive

Choose your learning style9 modes available
Overview - Writing an ISR (Interrupt Service Routine)
What is it?
An Interrupt Service Routine (ISR) is a special function in embedded C that runs automatically when a specific hardware event happens. It temporarily pauses the main program to quickly handle the event, like a button press or a timer tick. After the ISR finishes, the main program continues where it left off. ISRs help embedded systems respond fast to important signals without wasting time checking for them constantly.
Why it matters
Without ISRs, embedded programs would have to keep checking for events in a slow, wasteful way called polling. This wastes power and slows down the system. ISRs let the system react immediately and efficiently, making devices like microwaves, cars, and phones work smoothly and safely. They are essential for real-time responsiveness in embedded systems.
Where it fits
Before learning ISRs, you should understand basic C programming, functions, and how microcontrollers work with hardware like timers and input pins. After mastering ISRs, you can learn advanced topics like interrupt priorities, nested interrupts, and real-time operating systems (RTOS) that manage many ISRs together.
Mental Model
Core Idea
An ISR is like a fire alarm that instantly interrupts your normal activities to handle an emergency, then lets you resume what you were doing.
Think of it like...
Imagine you are reading a book at home, and suddenly the fire alarm rings. You stop reading immediately, take action to check the fire, then return to your book once it's safe. The ISR is the fire alarm, the main program is your reading, and the emergency is the hardware event.
Main Program Loop ──────────────┐
                                │
Hardware Event Occurs ──> Interrupt Triggered
                                │
                        ┌───────▼───────┐
                        │   ISR Runs    │
                        └───────┬───────┘
                                │
                        Resume Main Program
Build-Up - 7 Steps
1
FoundationUnderstanding Interrupt Basics
🤔
Concept: Learn what interrupts are and why they are useful in embedded systems.
An interrupt is a signal from hardware that tells the processor to stop what it is doing and run a special function. This helps the system react quickly to events like button presses or sensor signals without waiting or checking repeatedly.
Result
You understand that interrupts allow immediate attention to important events, improving efficiency.
Knowing interrupts exist changes how you design embedded programs from slow polling loops to fast, event-driven responses.
2
FoundationBasic ISR Syntax in Embedded C
🤔
Concept: Learn how to write a simple ISR function in embedded C using the correct syntax.
In embedded C, an ISR is a function with a special name or attribute depending on the microcontroller and compiler. For example, using GCC for ARM Cortex-M, you write: void TIM2_IRQHandler(void) { // ISR code here } This function runs automatically when the TIM2 timer interrupt occurs.
Result
You can write a basic ISR function that the system calls on an interrupt.
Understanding the special function signature and naming is key to linking hardware events to your code.
3
IntermediateClearing Interrupt Flags Inside ISR
🤔Before reading on: Do you think the ISR automatically resets the interrupt signal, or do you need to do it manually? Commit to your answer.
Concept: Learn that ISRs must clear the interrupt flag to prevent repeated triggers.
When an interrupt happens, a flag is set in hardware to signal the event. Inside the ISR, you must clear this flag manually, or the ISR will keep running repeatedly. For example: void TIM2_IRQHandler(void) { if (TIM2->SR & TIM_SR_UIF) { // Check update interrupt flag TIM2->SR &= ~TIM_SR_UIF; // Clear flag // Handle timer event } } This stops the interrupt from firing again immediately.
Result
Your ISR runs once per event and does not get stuck in a loop.
Knowing to clear flags prevents infinite interrupt loops, a common bug in embedded programming.
4
IntermediateKeeping ISRs Short and Fast
🤔Before reading on: Should ISRs contain long, complex code or just quick tasks? Commit to your answer.
Concept: Learn why ISRs must be brief and avoid heavy processing.
ISRs pause the main program and block other interrupts of the same or lower priority. Long ISRs cause delays and missed events. Best practice is to do minimal work in the ISR, like setting a flag or reading data, then handle complex tasks in the main loop. Example: volatile int data_ready = 0; void EXTI0_IRQHandler(void) { EXTI->PR |= EXTI_PR_PR0; // Clear interrupt data_ready = 1; // Signal main loop } int main() { while (1) { if (data_ready) { data_ready = 0; // Process data here } } }
Result
Your system remains responsive and stable under interrupts.
Understanding ISR speed requirements helps avoid system freezes and lost interrupts.
5
IntermediateUsing Volatile Variables with ISRs
🤔
Concept: Learn why variables shared between ISRs and main code must be declared volatile.
The compiler optimizes code by assuming variables don't change unexpectedly. But ISRs can change variables anytime. Declaring shared variables as volatile tells the compiler not to optimize them away. Example: volatile int flag = 0; void ISR(void) { flag = 1; } int main() { while (!flag) { // wait } } Without volatile, the compiler might optimize the loop incorrectly.
Result
Your program correctly detects changes made by ISRs.
Knowing volatile prevents subtle bugs where main code misses ISR updates.
6
AdvancedHandling Nested and Prioritized Interrupts
🤔Before reading on: Can ISRs interrupt other ISRs, or do they always run one at a time? Commit to your answer.
Concept: Learn how interrupt priorities and nesting work to manage multiple simultaneous events.
Microcontrollers often support interrupt priorities. Higher priority interrupts can interrupt lower priority ISRs. This nesting allows urgent events to be handled immediately. You configure priorities in hardware registers and enable nesting. For example, in ARM Cortex-M: NVIC_SetPriority(TIM2_IRQn, 1); // Higher priority NVIC_SetPriority(EXTI0_IRQn, 2); // Lower priority Enabling nesting requires setting special bits in the CPU. This system ensures critical events are never delayed by less important ones.
Result
Your system can handle multiple interrupts efficiently without losing important signals.
Understanding nested interrupts helps design robust real-time systems that prioritize urgent tasks.
7
ExpertAvoiding Common ISR Pitfalls and Side Effects
🤔Before reading on: Do you think ISRs can safely call any function or use any variable? Commit to your answer.
Concept: Learn the subtle dangers of ISRs, like reentrancy, shared resource conflicts, and calling non-reentrant functions.
ISRs run asynchronously and can interrupt main code or other ISRs. Calling functions that are not safe for interrupts (like printf or malloc) can cause crashes or data corruption. Also, accessing shared variables without protection can cause race conditions. Best practices include: - Avoid complex library calls inside ISRs - Use atomic operations or disable interrupts briefly when accessing shared data - Keep ISRs minimal and delegate work Example of unsafe code: void ISR(void) { printf("Interrupt happened\n"); // Unsafe } Safe approach: volatile int event_flag = 0; void ISR(void) { event_flag = 1; } int main() { if (event_flag) { event_flag = 0; printf("Interrupt happened\n"); } }
Result
Your system avoids crashes and unpredictable behavior caused by ISR misuse.
Knowing ISR limitations prevents subtle bugs that are hard to debug in embedded systems.
Under the Hood
When a hardware event occurs, the microcontroller sets an interrupt flag and signals the CPU. The CPU finishes the current instruction, saves its state (like program counter and registers) on the stack, and jumps to the ISR address. The ISR runs, handling the event. After completion, the CPU restores the saved state and resumes the main program. This context switching is very fast but requires careful management of shared resources and flags.
Why designed this way?
This design allows immediate response to hardware events without wasting CPU time on constant checking. Saving and restoring state ensures the main program continues seamlessly. Alternatives like polling waste power and slow response. The interrupt system balances speed and complexity, enabling real-time embedded control.
┌─────────────┐
│ Hardware    │
│ Event       │
└──────┬──────┘
       │ Interrupt Signal
       ▼
┌─────────────┐
│ CPU         │
│ Saves State │
│ Jumps to ISR│
└──────┬──────┘
       │ Runs ISR Code
       ▼
┌─────────────┐
│ ISR         │
│ Clears Flag │
│ Handles Event│
└──────┬──────┘
       │ Return from ISR
       ▼
┌─────────────┐
│ CPU         │
│ Restores    │
│ State       │
└──────┬──────┘
       │ Resumes Main Program
       ▼
Myth Busters - 4 Common Misconceptions
Quick: Do you think an ISR can run forever without affecting the main program? Commit to yes or no.
Common Belief:ISRs can run long or even forever without problems because they run separately from the main program.
Tap to reveal reality
Reality:Long-running ISRs block the main program and other interrupts, causing system freezes or missed events.
Why it matters:Ignoring ISR length can make your device unresponsive or cause critical events to be lost.
Quick: Do you think variables changed inside an ISR are always immediately visible to the main program? Commit to yes or no.
Common Belief:Variables changed in ISRs automatically update in the main program without special handling.
Tap to reveal reality
Reality:Without declaring variables as volatile, the compiler may optimize away checks, causing the main program to miss changes made by ISRs.
Why it matters:This leads to bugs where the main program waits forever or uses stale data.
Quick: Can you safely call any standard library function like printf inside an ISR? Commit to yes or no.
Common Belief:You can call any function inside an ISR just like in normal code.
Tap to reveal reality
Reality:Many functions are not reentrant or thread-safe and can cause crashes or deadlocks if called inside ISRs.
Why it matters:Calling unsafe functions inside ISRs can cause unpredictable system behavior and hard-to-find bugs.
Quick: Do you think the hardware automatically clears interrupt flags after ISR execution? Commit to yes or no.
Common Belief:The hardware clears interrupt flags automatically, so you don't need to do it in the ISR.
Tap to reveal reality
Reality:Most hardware requires the ISR to clear the interrupt flag manually to stop repeated interrupts.
Why it matters:Failing to clear flags causes the ISR to run repeatedly, freezing the system.
Expert Zone
1
Some microcontrollers support tail-chaining, where the CPU switches directly between ISRs without restoring main context, improving performance.
2
Using atomic operations or disabling interrupts briefly in main code prevents race conditions when accessing shared variables with ISRs.
3
Stack usage in ISRs is critical; deep call chains or large local variables can cause stack overflow, crashing the system.
When NOT to use
ISRs are not suitable for heavy processing or blocking operations like waiting for input. Instead, use ISRs only to signal events and handle complex tasks in the main loop or a real-time operating system (RTOS) task.
Production Patterns
In production, ISRs often set flags or push data to lock-free queues, while main tasks process data. Interrupt priorities are carefully assigned to ensure critical events preempt less important ones. Debugging tools and hardware trace help analyze ISR timing and performance.
Connections
Event-driven Programming
ISRs are a hardware-level example of event-driven programming where code runs in response to events.
Understanding ISRs helps grasp how software frameworks handle events asynchronously, improving responsiveness.
Operating System Signals
ISRs are similar to OS signals that interrupt processes to handle urgent tasks.
Knowing ISRs clarifies how OS signals work and why signal handlers must be quick and safe.
Emergency Response Systems
ISRs function like emergency responders who interrupt normal activities to handle urgent situations.
This cross-domain view shows how interrupt handling balances normal work with urgent needs, a principle in many fields.
Common Pitfalls
#1Writing long code inside the ISR causing system freezes.
Wrong approach:void ISR(void) { // Complex processing for (int i = 0; i < 100000; i++) { // heavy computation } clear_interrupt_flag(); }
Correct approach:volatile int event_flag = 0; void ISR(void) { clear_interrupt_flag(); event_flag = 1; } int main() { if (event_flag) { event_flag = 0; // heavy processing here } }
Root cause:Misunderstanding that ISRs block other code and must be short.
#2Not declaring shared variables as volatile, causing stale reads.
Wrong approach:int flag = 0; void ISR(void) { flag = 1; } int main() { while (flag == 0) { // wait } }
Correct approach:volatile int flag = 0; void ISR(void) { flag = 1; } int main() { while (flag == 0) { // wait } }
Root cause:Ignoring compiler optimizations that assume variables don't change unexpectedly.
#3Calling non-reentrant functions like printf inside ISR causing crashes.
Wrong approach:void ISR(void) { printf("Interrupt occurred\n"); clear_interrupt_flag(); }
Correct approach:volatile int interrupt_occurred = 0; void ISR(void) { clear_interrupt_flag(); interrupt_occurred = 1; } int main() { if (interrupt_occurred) { interrupt_occurred = 0; printf("Interrupt occurred\n"); } }
Root cause:Not knowing which functions are safe to call inside ISRs.
Key Takeaways
An ISR is a special function that runs immediately when hardware signals an event, pausing the main program.
ISRs must be short, fast, and clear their interrupt flags to avoid system freezes and repeated triggers.
Variables shared between ISRs and main code must be declared volatile to ensure correct behavior.
Calling complex or non-reentrant functions inside ISRs can cause crashes; delegate heavy work to the main program.
Understanding interrupt priorities and nesting helps build responsive and reliable embedded systems.