In ARM Cortex-M architectures such as Cortex-M3, Cortex-M4, Cortex-M7, and Cortex-M33, stack management is a fundamental mechanism that enables exception handling, context switching, and real-time operating system (RTOS) operation.
One of the unique features of the ARM Cortex-M core is its support for two independent stack pointers: the Main Stack Pointer (MSP) and the Process Stack Pointer (PSP). This dual-stack design provides a clean separation between privileged system code and unprivileged application tasks, significantly improving system reliability, security, and real-time behavior
In this blog post, we will explore the difference between MSP and PSP (MSP vs PSP), how the processor switches between them, and when to use each. We will also look at practical examples and common use cases like RTOS integration.
π What Is a Stack Pointer?
The Stack Pointer (SP) is a CPU register (typically R13 in ARM architecture) that tracks the top of the stack in memory. The stack memory is used to store return addresses, local variables, and CPU context during interrupts.
In ARM Cortex-M cores, there are two stack pointers:
- MSP (Main Stack Pointer): The default stack pointer used after reset and during exceptions.
- PSP (Process Stack Pointer): An alternate stack pointer used optionally in thread (normal) mode.
MSP (Main Stack Pointer):
β Characteristics:
- Used by default after reset.
- Automatically used during exceptions and interrupts.
- Stored in the MSP register.
- Suitable for applications not using RTOS.
How to read the current value of SP into a variable:
There are many ways to read stack pointers. Here are some examples.
π§ Example 1:
uint32_t mspValue = __get_MSP();
π What it does:
- Reads the Main Stack Pointer (MSP) register directly.
- Provided by CMSIS and works across all ARM Cortex-M cores.
β Pros:
- β Portable
- β Safe and clear
- β Supported by all toolchains that use CMSIS
β Note:
This reads only MSP, regardless of whether the CPU is using PSP or MSP at that moment.
π§ Example 2:
register uint32_t sp;
asm volatile ("MOV %0, SP" : "=r" (sp));
π What it does:
- Reads the currently active stack pointer (could be MSP or PSP depending on CONTROL.SPSEL).
- Uses GCC-style inline assembly.
β Pros:
- β Reads the active SP (not just MSP)
- β Useful in debugging, low-level runtime code
β Cons:
- β Not portable to compilers like IAR/Keil (syntax is GCC-specific)
- β Less readable for beginners
- β Compiler may optimize unexpectedly without volatile
π§ Example 2:
If you are using an RTOS or custom thread model and want to determine which SP is active:
uint32_t active_sp;
asm volatile ("MOV %0, SP" : "=r" (active_sp));
uint32_t control = __get_CONTROL();
if (control & (1 << 1))
{
// Using PSP
} else
{
// Using MSP
}
How to set the current value of SP into a variable:
π§ Example 1: (Recommended)
__set_MSP(app_sp)
β Pros:
- β Portable across ARM compilers (GCC, IAR, Keil) using CMSIS
- β Safe and well-documented
- β Compiler understands its semantics (can optimize, prevent reordering)
- β Readable and intention-clear to other developers
β Use Cases:
- Setting MSP at boot/startup
- Jumping to another image or firmware
- Context switch (if you’re building a scheduler)
π§ Example 1:
asm volatile ("MOV SP, %0" :: "r" (sp_val));
β Pros:
- β Works fine if you’re careful
- β Gives you full control at the assembly level
β Cons:
- β Not portable across compilers (inline assembly syntax differs between GCC, IAR, etc.)
- β Harder to maintain and read
- β Compiler might not fully understand side effects unless marked volatile
- β Easy to misuse in timing-critical or ISR code
β Safe Use Cases for Setting MSP:
1. Early Startup (Bootloader / Reset Handler):
At the very beginning (e.g., in _start() or Reset_Handler), it’s safe to do:
__set_MSP(initial_stack_addr);
Often used in bootloaders or when jumping to a new image.
2. Before Jumping to Another Application:
If you load an application (e.g., from QSPI Flash) and want to hand over execution:
typedef void (*app_entry_t)(void);
void jump_to_app(uint32_t app_sp, uint32_t app_entry_addr)
{
__disable_irq(); // Disable interrupts
__set_MSP(app_sp); // Set new stack pointer
app_entry_t app = (app_entry_t)app_entry_addr;
app(); // Jump to application
}
3. Context Switch in Bare-Metal Task Scheduler:
If you’re writing your own context switcher:
__set_MSP(task->stack_top);
But be careful: you must save and restore full context manually.
π« When NOT to Set MSP:
- β In an interrupt handler β use the existing stack
- β While in Thread mode without switching away from MSP first
- β If using RTOS β kernel manages it internally
PSP (Process Stack Pointer)
β Characteristics:
- Manually enabled via the CONTROL register.
- Used only in Thread mode (i.e., normal application execution).
- Commonly used in RTOS environments for per-task stacks.
- Stored in the PSP register.
π§ Example Use Case:
// 1. Set the PSP (Process Stack Pointer) to point to the new task's stack __set_PSP(new_task_stack_pointer); // 2. Set CONTROL register to switch from MSP to PSP // Bit 1 of CONTROL = 1 means use PSP instead of MSP __set_CONTROL(__get_CONTROL() | (1 << 1)); // 3. Execute an Instruction Synchronization Barrier (ISB) // Ensures the change to CONTROL takes effect immediately __ISB();
π§ Explanation of Each Line:
1.β __set_PSP(new_task_stack_pointer);
-
- This sets the Process Stack Pointer (PSP) to the new taskβs stack.
- This is useful in multitasking or context switching where each task has its own stack.
2. β __set_CONTROL(__get_CONTROL() | (1 << 1));
-
- This reads the current CONTROL register, sets bit 1 to 1, and writes it back.
- CONTROL[1] = 1 switches the stack pointer used by the processor from MSP (Main Stack Pointer) to PSP (Process Stack Pointer).
- This is crucial for running unprivileged threads or tasks with their own stacks.
3. β __ISB();
-
- The Instruction Synchronization Barrier ensures that the processor flushes the pipeline and refetches instructions.
- This is necessary because the processor may still execute instructions assuming the old stack pointer unless explicitly synchronized.
π Switching Between MSP and PSP:
ARM Cortex-M uses the CONTROL register bit 1 to select the active stack pointer in Thread mode:
| CONTROL[1] Value | Stack Pointer Used |
|---|---|
0 |
MSP |
1 |
PSP |
Note: In Handler mode (i.e., interrupts), the processor always uses MSP regardless of CONTROL register settings.
MSP vs PSP in ARM Cortex-M:
To clearly understand when and how to use MSP and PSP in Cortex-M architecture, the following table offers a deep technical comparison. It highlights operational behavior, usage in different modes, security implications, and typical use cases. Whether you’re working on a bare-metal system or designing an RTOS kernel, this breakdown will help you choose the right stack pointer for your needs.
| Aspect | MSP (Main Stack Pointer) | PSP (Process Stack Pointer) |
|---|---|---|
| Full Name | Main Stack Pointer | Process Stack Pointer |
| Hardware Register | MSP register | PSP register |
| Default Usage | Used by default after reset and during all exception/interrupt handling | Not used by default; must be explicitly enabled via CONTROL register |
| Thread Mode Usage | Used in Thread mode unless CONTROL.SPSEL is set to 1 | Can be used in Thread mode only when CONTROL.SPSEL = 1 |
| Handler Mode Usage | Always used in Handler mode (interrupt context) | Not used in Handler mode β Cortex-M cores hardwire MSP usage for exceptions |
| Context Switching | Used to store ISR context frame (automatic stacking by hardware) | Used for task-level context in RTOS environments during thread execution |
| CONTROL Register Dependency | Irrelevant; MSP is used if CONTROL.SPSEL = 0 or in all exceptions regardless | Activated when CONTROL.SPSEL = 1; processor switches to PSP for Thread mode |
| Privileged Mode Behavior | Fully usable in both Privileged and Unprivileged modes | Also usable in both modes, but more often used in Unprivileged user-space applications |
| Stack Switching | Occurs automatically upon entering/exiting exceptions | Needs manual stack switching (e.g., in RTOS context switch routines) |
| Security Domain | Shares stack with kernel and system exception handlers | Enables stack separation between kernel (MSP) and user-level tasks (PSP) |
| Memory Layout Consideration | Often placed at top of SRAM and grows downward; must be sized to handle worst-case ISR nesting | Each task has its own PSP assigned in task control block (TCB); memory must be preallocated for each task |
| Stack Frame on Exception Entry | Exception stack frame is always pushed onto MSP (automatic by hardware) | Stack frame for PSP-based threads is manually handled by RTOS during task context switch |
| Usage in RTOS | Kernel, scheduler, and ISR handlers use MSP | User tasks and threads use PSP |
| Security Risk | Using MSP for both kernel and user space increases risk of stack corruption | PSP enables safe stack isolation between tasks and privileged execution context |
| Initialization Responsibility | Automatically initialized from vector table (initial SP value at address 0x0000_0000) | Must be manually initialized before enabling PSP (__set_PSP()) |
| Reset Behavior | Loaded from the initial stack pointer value defined at vector address [0x00000000] | Reset to zero; invalid until set by software |
| Performance | Slightly better for exception handling (direct usage, no extra instructions) | No performance overhead when correctly configured; switching involves a few instructions |
| Typical Usage Scenarios | – Bare-metal systems – Interrupt handlers – Bootloader code – Kernel context |
– RTOS task execution – User-level applications – Secure stack contexts (TrustZone) |
| Toolchain Support | Fully supported by all ARM compilers, CMSIS, and debuggers | Fully supported but requires explicit setup; usage depends on RTOS or custom task switcher logic |
| Debugging Insight | Can be viewed via debugger as MSP | Viewed via debugger as PSP; debugger must be aware of which stack is active in Thread mode |
| Stack Traceability | Easier to debug exception traces since MSP is always used for faults | May complicate thread-level stack traces if RTOS is not symbol-aware |
| TrustZone Compatibility | In TrustZone-enabled cores (e.g., Cortex-M33), MSP has secure and non-secure variants | PSP also has secure/non-secure banks; used in NS application while MSP is used in secure monitor |
| Manual Set Capability | β Can be set with __set_MSP(addr) but should only be done in startup, bootloader, or safe context switch | β Designed to be set at runtime using __set_PSP(addr); safe and standard in RTOS for per-task stack switching |