
Interrupt Service Routines (ISRs) are critical for real-time embedded systems, but they are also a common source of stack-related bugs. Stack overflow in an ISR can lead to silent data corruption, hard faults, or system crashes that are difficult to trace because the overflow often corrupts the return address or other stack frames. This article explores the root causes of ISR stack overflow, presents practical solutions, and provides verification techniques to ensure your interrupt handlers are stack-safe.
Unlike regular functions, ISRs execute in an asynchronous context and often have limited stack space allocated. When an interrupt occurs, the processor saves its context (registers, return address) onto the stack. The ISR then uses the same stack for its local variables, function calls, and any nested interrupts. Several factors contribute to stack exhaustion:
The Cortex-M processor, for example, uses the main stack pointer (MSP) for exceptions and interrupts by default. If the main stack is also used by threads in an RTOS, the available space for ISR execution is reduced further. Without proper stack overflow detection, an overflow can corrupt the stack pointer, return address, or saved registers, leading to unpredictable behavior.
+------------------------------------------------------------------+| MAIN STACK (MSP) |+------------------------------------------------------------------+| Task Stack Frame (if RTOS) | HIGH ADDRESS || ----------------------------------- | || Guard Region (MPU) | <-- Stack limit || ----------------------------------- | || ISR Context (auto-saved by HW) | || - xPSR, PC, LR, R12, R3-R0 | || ----------------------------------- | || ISR Local Variables | || - buffer[256], fft_buffer[1024] | || ----------------------------------- | || Nested ISR Context (if nesting) | || - Additional auto-saved registers | || ----------------------------------- | || Nested ISR Local Variables | LOW ADDRESS || | (stack grows down) |+------------------------------------------------------------------+
The most effective strategy is to minimize the work done inside the ISR. An ISR should only perform time-critical tasks such as:
All processing, logging, or complex calculations should be deferred to the main loop or a dedicated task. This reduces the stack footprint of the ISR to a minimum.
Large arrays or structures should not be declared as local variables in an ISR. Instead, use:
For example, instead of:
void UART_IRQHandler(void) {uint8_t buffer[256]; // Large local buffer// ... process data}
use a static buffer:
static uint8_t uart_rx_buffer[256];void UART_IRQHandler(void) {// ... use uart_rx_buffer}
Note: Static variables are not reentrant; if the ISR can be nested or interrupted by another instance of the same ISR, this approach is unsafe. In such cases, consider using a double-buffering scheme or allocating buffer space per ISR instance.
Function calls increase stack usage due to return address, parameters, and register saving. Avoid calling non-trivial functions from an ISR. If a function call is necessary, ensure it is lightweight and does not itself call other functions that consume significant stack space.
Use compiler-provided stack analysis tools to estimate the maximum stack usage of your ISR. For example:
-fstack-usage generates a .su file showing stack usage per function.Additionally, enable runtime stack checking if your microcontroller or RTOS supports it. For instance:
configCHECK_FOR_STACK_OVERFLOW to detect overflows via stack watermark checking.Determine the worst-case stack usage for your ISR, considering:
Add a safety margin (e.g., 20-30%) to the calculated worst-case usage and configure the stack size accordingly. In an RTOS, this may mean increasing the stack size for the system stack or the specific ISR stack (if separate stacks are used).
void TIM2_IRQHandler(void) {if (TIM2->SR & TIM_SR_UIF) {// Clear interrupt flagTIM2->SR &= ~TIM_SR_UIF;// Large local array on stackuint32_t fft_buffer[1024]; // 4KB on stack!// Process data (calls multiple functions)arm_rfft_fast_f32(&fft_handler, fft_buffer, fft_output, 0);arm_cmplx_mag_f32(fft_output, magnitude, 1024);float peak = arm_max_f32(magnitude, 1024);// Store result (if stack hasn't overflowed yet)latest_peak = peak;}}
This ISR allocates a 4KB buffer on the stack, which is likely to exceed the available stack space on most microcontrollers. Additionally, the function calls to the CMSIS-DSP library may push further onto the stack, increasing the risk of overflow.
// Static buffer shared with main loop (ping-pong buffering)static uint16_t adc_buffer[2][BUFFER_SIZE];static uint8_t buffer_index = 0;void ADC_IRQHandler(void) {if (ADC->SR & ADC_SR_EOC) {// Clear interrupt flagADC->SR &= ~ADC_SR_EOC;// Store sample in buffer (minimal stack usage)adc_buffer[buffer_index][sample_index++] = ADC->DR;// Check if buffer is fullif (sample_index >= BUFFER_SIZE) {sample_index = 0;buffer_index ^= 1; // Ping-pong switch// Signal main loop to process buffer (sets a flag or uses RTOS event)adc_buffer_ready = true;}}}// Main loop processes buffer when flag is setint main(void) {// ... initializationwhile (1) {if (adc_buffer_ready) {adc_buffer_ready = false;process_adc_buffer(adc_buffer[buffer_index ^ 1]); // Process the other buffer}// ... other background tasks}}
This ISR only performs the time-critical task of storing ADC samples and signaling the main loop. The heavy processing (process_adc_buffer) is done in the main loop, keeping the ISR stack usage minimal.
Use your compiler’s stack analysis tool to verify the maximum stack usage of each ISR. For GCC, compile with -fstack-usage and inspect the generated .su file. Ensure the reported stack usage for each ISR is well below the allocated stack size.
Enable hardware or runtime stack overflow detection:
configCHECK_FOR_STACK_OVERFLOW to 1 or 2 and provide a stack overflow hook function to catch overflows.To verify your ISR under worst-case conditions:
0xDEADBEEF) at the stack limit and verify it remains unchanged after stress testing.Compile with -fstack-usage and examine the .su file:
ISR.o: .text.TIM2_IRQHandler 0x123 456
The number 456 is the stack usage in bytes for the ISR. Compare this to your allocated stack size (e.g., 1024 bytes) to ensure sufficient margin.
Preventing stack overflow in ISRs requires a combination of design discipline, static analysis, and runtime verification. By keeping ISRs short, avoiding large local variables, limiting function calls, and ensuring adequate stack allocation, you can eliminate this class of bugs from your embedded firmware. Always verify your ISR stack usage with the tools available in your toolchain and test under worst-case interrupt scenarios to guarantee reliable operation.
Quick Links
Legal Stuff





