The Cortex-M interrupt stack is a key part of how interrupts are handled on Cortex-M processors. It provides a dedicated stack for storing context during interrupt handling so that the main stack is not disrupted. Understanding how the interrupt stack works is important for Cortex-M firmware developers to properly leverage interrupts and write interrupt handlers.
What is the Interrupt Stack?
The interrupt stack is a dedicated region of memory used specifically for storing context during interrupt handling. When an interrupt occurs, the processor automatically pushes registers like the program counter, LR (link register), and PS (processor status) onto this dedicated stack. It switches to using the interrupt stack so that the main stack remains undisturbed.
The interrupt stack is set up by configuring the MSP (main stack pointer) register. The region of memory for the interrupt stack must be allocated somewhere in RAM. The starting address of this region is written to the MSP on processor reset. Then whenever an interrupt occurs, the processor will push context onto the interrupt stack pointed to by MSP.
Having a separate interrupt stack prevents corruption of the main stack during interrupt handling. If interrupts used the main stack, unexpected changes would occur from pushing all the context registers. With the dedicated interrupt stack, the main stack remains pristine.
Interrupt Stack Frame
Each interrupt handler has a stack frame on the interrupt stack. This stack frame stores the complete context that needs preserving during the handler execution. The stack grows downward in memory. So each new interrupt handler pushes its stack frame onto the top of the interrupt stack.
The stack frame for Cortex-M interrupt handlers contains the following stored registers in order from bottom to top:
- R0
- R1
- R2
- R3
- R12
- LR (R14)
- PC (R15)
- xPSR
The xPSR register contains the combined APSR, EPSR and IPSR processor status flags. Storing these registers preserves the complete CPU, execution, and interrupt state.
When the interrupt handler finishes, it restores context by popping the stack frame off the interrupt stack back into these registers. This returns the system to its pre-interrupt execution state seamlessly.
Interrupt Stack Size
Properly sizing the interrupt stack memory region is important. It must be large enough to handle the expected maximum nested interrupt conditions. The factors that determine needed stack size are:
- Number of bytes needed per stack frame
- Max number of concurrent interrupt handlers
- Stack alignment requirements
For Cortex-M, each stack frame consumes 40 bytes for the 8 saved registers. Stack alignment requires 8-byte alignment. So for n concurrent interrupt handlers, the total interrupt stack size should be:
Stack Size = 8 * n + 40 * n
As an example, allowing for 3 concurrent interrupt handlers would require:
Stack Size = 8 * 3 + 40 * 3 = 144 bytes
So the interrupt stack should be set to a 144 byte region of memory in this case. The MSP register would point to the start of this region.
Setting Up the Interrupt Stack
Using the interrupt stack requires properly initializing MSP and stacking the initial context during reset. This is done in Cortex-M firmware with the following steps:
- Define interrupt stack region in RAM
- Initialize MSP to point to start of stack region
- Enable interrupts via CPSIE bit in PRIMASK
- Push initial context onto stack
- Set stack limit register if used
The vector table setup would include defining the stack region. For example with GCC: #define IRQ_STACK_SIZE 144 static uint32_t irqStack[IRQ_STACK_SIZE]; void Default_Handler(); void Reset_Handler() { // Initialize stack pointer __set_MSP((uint32_t)irqStack + sizeof(irqStack)); // Other initialization // Push initial context __asm volatile( “mov r0, %0\n” “mov r1, %1\n” “mov r2, %2\n” “mov r3, %3\n” “push {r0-r3}\n” :: “r” (other_regs), “r” (other_regs), “r” (other_regs), “r” (other_regs) ); }
This stacks the initial context so interrupt handling can save it properly. The stack limit register would then be set to the end of the stack region to enable overflow checking.
Configuring Interrupt Priority
Properly configuring interrupt priority levels is important to manage interrupt nesting on the stack. Cortex-M supports the following priority configuration:
- Set priority grouping for preemption and sub-priority bits
- Assign priority level 0 – 255 for each interrupt
- Set base priority mask for preemption
The NVIC_SetPriorityGrouping() function configures the split between preemption and sub-priority bits. For example, setting to NVIC_PRIORITYGROUP_4 would allocate the 4 MSBs for preemption priority and the 4 LSBs for sub-priority.
Then the NVIC_SetPriority() function assigns the specific priority level for each interrupt. Lower numbers indicate higher priority. So priority 5 would preempt priority 7.
The base priority mask enables tailoring which priority levels are allowed to preempt others. Setting PRIMASK below the minimum desired preemption level prevents lower priorities from interrupting.
With this priority configuration, the highest priority eligible pending interrupt will run when interrupts are enabled. The priority schemes support effective sharing and preemption of the interrupt stack.
Stacked Interrupt Behavior
When multiple interrupts occur, their stack frames are pushed in priority order. So higher priority interrupts stack on top of lower priority ones. When the running interrupt handler exits, the stack unwinds by one frame by popping back the registers.
This returns context to the preempted interrupt handler so it can finish executing. When no more handlers are stacked, the last exiting handler restores the base context.
If interrupts remain enabled, the next highest priority pending handler will then run. This concurrency and preemption management is all facilitated cleanly by the dedicated interrupt stack.
One key detail is that the stacked handlers run to completion – there is no preemption within a single interrupt handler’s execution. This prevents messy re-entrancy and recursion issues.
Overflow and Preemption
The interrupt stack can overflow if too many concurrent interrupts occur or the stack size is undersized. This causes a Hard Fault exception signaling the stack overflow error.
To prevent overflow, the stack size must account for the worst case handler concurrency. The stack limit register also provides an overflow breakpoint that can trigger early.
Preemption of low priority handlers can be another issue. Important interrupt handlers may require temporarily blocking preemption using PRIMASK or BASEPRI level masking.
This prevents unwanted high priority interrupts from disrupting sensitive code regions. So managing preemption and stack usage are key to robust interrupt handling.
Role in Thread Mode
The interrupt stack’s role changes when using Cortex-M Thread Mode. In Thread Mode, the main stack pointer MSP is banked per thread. But the interrupt stack pointer remains global.
So all threads share the same interrupt stack. The interrupt stack frame still only includes context from the thread that was interrupted.
Saving and restoring the interrupted thread’s stack pointer happens via the CONTROL register rather than on the stack itself. But otherwise the interrupt stack’s purpose remains providing dedicated stack space for interrupt context.
In Thread Mode, the interrupt stack essentially forms the boundary between thread context and shared interrupt context.
Interrupts in OS Context
Using an RTOS with Cortex-M can change how the interrupt stack is utilized. Many RTOS kernels manage their own interrupt handling.
A common technique is to not allow direct interrupt handling in thread context. Instead, pending interrupts will trigger scheduling and context switches.
The interrupt stack may then only be used within RTOS kernel code, not thread context. So the RTOS kernel takes full control of servicing interrupts and the stack.
This removes the need for application threads to have any interrupt stack space or handling. The RTOS kernel uses the interrupt stack internally on behalf of the application.
So in an RTOS context, the developer just needs to properly size the interrupt stack based on expected kernel interrupts. The stack allows the kernel to service interrupts without disturbing application thread stacks.
Optimizing Interrupt Latency
The Cortex-M interrupt stack enables low interrupt latency by reducing context saving steps. But additional optimization is possible via:
- Tail chaining handlers – avoid stack if servicing back-to-back
- Lazy stacking – selectively push only essential registers
- Stack overflow breakpoint – catch errors early
- Stack size tuning – balance needs and overhead
Tail chaining executes the next highest priority handler directly without stacking once the current handler finishes. This avoids stack operations.
Lazy stacking opts to manually stack only essential registers depending on handler needs. Unneeded registers are left untouched.
The stack overflow breakpoint can trigger prior to a hard fault on deep stack usage. This enables catching issues before corruption.
And tuning the stack size balances providing enough space for concurrency against keeping the region small for efficiency.
So while the interrupt stack already enables efficient interrupt handling, additional optimizations can further reduce latency.
Summary
The Cortex-M interrupt stack is vital for robust interrupt processing and low-latency preemption. Keeping interrupt context off the main stack prevents corruption. Tailored priority schemes manage concurrency. And the stack allows for efficient nested interrupt handling.
Understanding the stack structure, configuration, size requirements, and usage is important for Cortex-M firmware developers. Properly leveraging the interrupt stack facilitates building responsive real-time embedded systems.