Hello! Welcome to the fourth lesson in our module on Interrupts and Exception Handling.
In our previous lessons, we've focused on the "control room" of the interrupt system—the NVIC. You've learned how to enable interrupts and how to meticulously configure their priorities and priority groupings to control which interrupt gets to run and when it can preempt another.
Now, we'll move from the control room to the "first responder" itself: the Interrupt Service Routine (ISR). Today's lesson directly addresses the learning outcome: Write an interrupt service routine (ISR) following the correct procedure for context preservation and clearing interrupt flags. We will demystify what happens under the hood when a hardware event demands the CPU's attention, a process that frameworks like AUTOSAR often abstract away. For a low-level driver development role, mastering this sequence is non-negotiable.
Let's break down the lifecycle of an interrupt.
1. The Interrupt Sequence: From Trigger to Service
When a peripheral (like a UART receiving a byte) triggers an interrupt, a precise sequence of events is initiated by the hardware.

The two most critical parts of this process from a developer's perspective are:
- Context Preservation: How does the processor save its current work so it can resume flawlessly later?
- Interrupt Acknowledgement: How do we tell the hardware, "I've got it, don't ask again for the same event"?
Let's dive into the first part.
2. Automatic Context Preservation: The Hardware's Magic
Before the processor can jump to your ISR code, it must first save the state, or context, of the code it was just running. This context includes essential CPU registers like the program counter (PC), general-purpose registers (R0-R3, R12), and the program status register (PSR).
On ARM Cortex-M processors, this is not something you program in software. It's an automatic hardware feature. When an accepted interrupt occurs, the processor hardware performs a stacking operation.
To see exactly what happens during this stacking process, watch the following segment from the 'Lecture 9: Interrupts' video. It provides a clear, step-by-step visualization of the hardware saving the processor's context.
Please watch from 13:15 to 16:31. Focus on the list of registers that are automatically pushed onto the stack and the order in which they are saved. The video also explains the 'unstacking' process that happens on return, which we'll discuss shortly.
As the video shows, the hardware automatically pushes eight registers onto the current stack: R0, R1, R2, R3, R12, LR (Link Register), PC (Program Counter), and xPSR (Program Status Register). This process is fundamental because it preserves the state of the interrupted task without any software overhead.

This hardware-based context saving is a crucial feature. It not only makes interrupt handling faster (reducing latency) but also simplifies our job as programmers. Since the hardware automatically handles saving the registers as defined by the ARM Architecture Procedure Call Standard (AAPCS), we can write our ISRs as standard C functions. You may recall our earlier discussion on the AAPCS; this is a prime example of how the hardware architecture is designed to work seamlessly with C compilers.
3. The Anatomy of an Interrupt Service Routine
Once the context is saved, the processor is in Handler Mode and the Program Counter is loaded with the starting address of your ISR. This address is fetched from the vector table, which acts as a dispatch table for all exceptions.
In practice, an ISR is a C function with a specific name, determined by the peripheral and defined in the startup files of your project (e.g., TIMER2_IRQHandler, UART0_IRQHandler). An ISR should have a void return type and take no parameters.
The body of the ISR has two primary responsibilities:
- Acknowledge the Interrupt: Clear the hardware flag that triggered the interrupt.
- Service the Request: Perform the actual work (e.g., read a byte from the UART data register, toggle a GPIO, increment a counter).
Let's focus on the first, and most critical, responsibility.
Acknowledging the Interrupt: Don't Forget This!
A peripheral requests an interrupt by setting a status flag (a "trigger flag"). This flag is "sticky"—it stays set until it is explicitly cleared.
If your ISR services the request but forgets to clear the flag, a critical failure occurs. As soon as the ISR completes, the hardware's unstacking process finishes, and the processor checks for pending interrupts again. Since the trigger flag was never cleared, the NVIC sees the interrupt as still active and immediately re-triggers the same ISR. This creates an infinite loop where the CPU does nothing but execute the same ISR over and over, effectively locking up the system.
The textbook 'Chapter 12: Interrupts' by Jonathan Valvano explains this requirement clearly and provides code examples. Pay close attention to the concept of acknowledgment.
Please read the following parts of the webpage: The introduction ('An interrupt is...') to understand the definition of a trigger and the sequence of a context switch. The paragraph starting with 'Clearing a trigger flag is called acknowledgement...'. The short section titled 'Interrupt Service Routine (ISR)'. The code snippet in 'Program 12.2' and the accompanying 'Program 12.4' to see a practical example of acknowledging a GPIO interrupt.
As you read, the key takeaway is that for almost every peripheral interrupt, the ISR must perform an action to clear the source of the interrupt.
How is a flag cleared?
This is specific to each peripheral and must be found in the microcontroller's reference manual. Common methods include:
- Write-1-to-clear (W1C): Writing a '1' to a bit in an interrupt clear register (
ICR). This is common for GPIO interrupts. - Reading/Writing a Data Register: For peripherals like UARTs or ADCs, simply reading the data register that holds the new value often clears the "data ready" interrupt flag automatically.
Here is the canonical structure of a GPIO ISR, based on the examples you just reviewed:
// ISR for GPIO Port F on a TM4C microcontroller
void GPIOPortF_Handler(void) {
// --- JOB 1: Acknowledge the interrupt ---
// Check which pin caused the interrupt (e.g., PF4) and clear its flag.
if (GPIO_PORTF_RIS_R & 0x10) { // Check Raw Interrupt Status for pin 4
GPIO_PORTF_ICR_R = 0x10; // Acknowledge by writing 1 to the clear register for pin 4
}
// --- JOB 2: Service the request ---
// Do the actual work, e.g., increment a counter.
FallingEdges = FallingEdges + 1;
}
An Exception to the Rule: SysTick
The one common exception is the SysTick timer, which is part of the ARM core itself. Its interrupt flag is cleared automatically when the ISR is entered. This is why you will not see an explicit acknowledgement in a SysTick_Handler.
4. Returning from the Interrupt
After the ISR has acknowledged the flag and completed its work, it must return. In assembly, this is done with a BX LR (Branch with Exchange to Link Register) instruction. The C compiler generates this for you when your void function reaches its end.
This instruction is special in Handler Mode. The hardware detects a special EXC_RETURN value in the LR that was placed there on interrupt entry. This signals the hardware to begin the unstacking process, where it pops the eight registers it originally saved back into the core. The PC is restored, and the interrupted program resumes exactly where it left off, usually unaware that it was ever paused.
Test your understanding!
You are writing a driver for a new ADC peripheral. The datasheet says, "An interrupt is generated when a conversion is complete. The EOC (End of Conversion) flag in the ADC_STATUS register is set to 1. Reading the ADC_DATA register will automatically clear the EOC flag."
You write the following ISR. What is wrong with it?
volatile uint16_t latest_adc_value;
void ADC_IRQHandler(void) {
// Service the request first
latest_adc_value = ADC_DATA;
// Now, acknowledge the interrupt
ADC_STATUS &= ~ADC_STATUS_EOC_Msk; // Manually clear the EOC flag
}
Show answer
The problem is the explicit acknowledgment (ADC_STATUS &= ~...). The datasheet states that simply reading the ADC_DATA register is the correct way to clear the EOC flag.
By reading ADC_DATA, the flag is already cleared. The subsequent manual clear operation is redundant and, depending on the hardware, could potentially be an invalid operation or have unintended side effects. The correct ISR is simpler:
volatile uint16_t latest_adc_value;
void ADC_IRQHandler(void) {
// Reading the data register both services the request AND acknowledges the interrupt.
latest_adc_value = ADC_DATA;
}
This highlights why reading the datasheet is absolutely critical for writing correct ISRs.
5. Practical Considerations and Debugging
In a real-world project, you don't write ISRs in a vacuum. You'll work within an SDK that provides a startup file containing the vector table and weakly-defined default handlers.
A Practical guide to ARM Cortex-M Exception Handling
The article 'A Practical guide to ARM Cortex-M Exception Handling' from Memfault's blog provides excellent examples of how this works in a real-world SDK (Nordic nRF52).
Please read these three sections: 'Setup Prep': See how the vector table is defined in an assembly file (gcc_startup_nrf52840.S) and how ISRs are declared as weak symbols. 'Triggering a Built In Exception (PendSV)': Understand the process of implementing your own handler to override the weak default and then triggering it programmatically. 'Pre-emption of an NVIC Interrupt': This example is more complex but reinforces the concepts from our last lesson while showing how to implement and trigger a peripheral ISR (POWER_CLOCK_IRQHandler). The goal is not to memorize the code, but to understand the workflow: find the default handler, write your own implementation, and then test it.
The workflow described is a standard part of bare-metal driver development and a great skill to demonstrate in an interview. You find the default while(1) handler, implement your own function with the exact same name, and the linker will automatically replace the weak default with your strong implementation.
Interview Preparation
-
Question: "Walk me through the full lifecycle of a peripheral interrupt on an ARM Cortex-M, from the hardware event to returning to the main code."
- Your Answer: "First, a hardware event sets a trigger flag in a peripheral. The NVIC detects this, and if the interrupt's priority is sufficient, it preempts the current execution. The processor hardware then automatically pushes a stack frame with 8 registers (R0-R3, R12, LR, PC, PSR) and enters Handler Mode. The PC is loaded with the ISR's address from the vector table. My ISR code then executes. Its first critical job is to acknowledge the interrupt by clearing the peripheral's trigger flag, usually by writing to a clear register or accessing a data register. Then, it services the request. Upon completion, the ISR executes a
BX LR, which triggers the hardware to pop the saved registers from the stack, restoring the original context. Execution then seamlessly resumes in the interrupted code."
- Your Answer: "First, a hardware event sets a trigger flag in a peripheral. The NVIC detects this, and if the interrupt's priority is sufficient, it preempts the current execution. The processor hardware then automatically pushes a stack frame with 8 registers (R0-R3, R12, LR, PC, PSR) and enters Handler Mode. The PC is loaded with the ISR's address from the vector table. My ISR code then executes. Its first critical job is to acknowledge the interrupt by clearing the peripheral's trigger flag, usually by writing to a clear register or accessing a data register. Then, it services the request. Upon completion, the ISR executes a
-
Question: "What is the most common and critical bug when writing an ISR for a peripheral, and what are its symptoms?"
- Your Answer: "Forgetting to acknowledge the interrupt by clearing its trigger flag. The symptom is a system lock-up. The ISR will execute, but because the flag isn't cleared, the interrupt is immediately re-triggered upon the ISR's exit. The CPU gets stuck in a loop, only ever executing that one ISR, and the main application never gets to run."
-
Question: "Who is responsible for context saving when an interrupt occurs on a Cortex-M: the hardware, the compiler, or my code?"
- Your Answer: "It's a hardware responsibility. The Cortex-M core automatically handles the stacking of key registers on interrupt entry and unstacking on exit. This is a key architectural feature that reduces latency and ensures compliance with the C procedure call standard without requiring any special keywords or compiler pragmas in the ISR itself."
Conclusion
Today you've dissected the precise sequence of events that make up an interrupt. You now understand the critical, automatic role the hardware plays in preserving context, and the equally critical role you play in writing an ISR that correctly acknowledges the interrupt source.
Key Takeaways:
- Context switching (stacking and unstacking) is an automatic hardware function on ARM Cortex-M, preserving a standard 8-register stack frame.
- An ISR is a C function whose name is mapped in the vector table. Its two jobs are to acknowledge and service.
- Acknowledging the interrupt by clearing its trigger flag is mandatory for almost all peripherals and is a common source of bugs if forgotten.
- The method for acknowledgment is peripheral-specific and must be found in the device's documentation.
- Returning from an ISR via
BX LRtriggers the automatic context restore.
Next Lesson Preview:
We've mentioned that the hardware's automatic context saving helps reduce latency. In our next lesson, we will explore the concept of interrupt latency in detail. We'll identify all the factors that contribute to the delay between a hardware event and the execution of your ISR code, and we'll look at further hardware optimizations like tail-chaining and late-arriving interrupts that are designed to minimize it.