MSP PSP Stack Placement
Understand the two physical stack pointers in ARM Cortex-M processors, how SRAM is partitioned between MSP and PSP, the two stack layout strategies for embedded applications, and how to switch the active stack pointer using naked C functions and MSR/MRS assembly instructions.
Two Physical Stack Pointers in Cortex-M
Every ARM Cortex-M processor contains two separate, independent stack pointer registers: the Main Stack Pointer (MSP, also called SP_main) and the Process Stack Pointer (PSP, also called SP_process). At any given moment the processor exposes only one of them as the architectural “SP” (R13), but both registers exist in silicon and hold their values simultaneously.
• Default stack pointer after every reset
• Always active in Handler mode (exception/ISR execution)
• Used in Thread mode when CONTROL[1] (SPSEL) = 0
• Initialised automatically by hardware from vector table word 0 (address 0x00000000)
PSP (Process Stack Pointer, SP_process)
• Only usable in Thread mode
• Activated when CONTROL[1] = 1
• Must be initialised manually by software before use
• Never active during exception handling — hardware always switches to MSP on IRQ entry
This dual-pointer design was motivated by real-time operating systems. In an RTOS each task (thread) has its own PSP-backed stack. The RTOS itself and all interrupt handlers share the MSP-backed stack. This physical separation makes it impossible for a misbehaving task to overflow into ISR stack space, improving fault isolation without any software overhead.
| Property | MSP | PSP |
|---|---|---|
| Register alias | R13 when SPSEL=0 or in Handler mode | R13 when SPSEL=1 in Thread mode |
| Active in Handler mode? | Always | Never |
| Active in Thread mode? | When CONTROL[1]=0 (default after reset) | When CONTROL[1]=1 |
| Reset initialisation | Hardware reads from 0x00000000 | Undefined — software must set it |
| RTOS role | Kernel / ISR stack | Per-task stack |
| Read/write in assembly | MRS Rn, MSP / MSR MSP, Rn |
MRS Rn, PSP / MSR PSP, Rn |
Stack Placement in SRAM: Type 1 vs Type 2
The linker script determines where the stack lives in SRAM. There are two common arrangements for embedded applications, each with different trade-offs for overflow detection and memory utilisation.
Type 1 — Stack in the Middle of RAM
• Stack grows downward toward STACK_END
• Heap grows upward from HEAP_START
• Unused space sits above the stack
• Stack overflow risks colliding with heap
• Linker script defines STACK_START explicitly
• SP is initialised to STACK_START at reset
• Good for fixed-size stack (known upper bound)
Type 2 — Stack at the Top of RAM (Recommended)
• Stack grows downward into unused space
• Unused space acts as natural buffer zone
• Stack overflow stops at heap top (protected)
• SP is simply initialised to RAM_END
• STM32 default linker script uses this layout
• Easier to reason about overflow conditions
• Preferred for most embedded applications
STM32F411 default linker script (Type 2)
/* From the default STM32F411 linker script (STM32F411RETx_FLASH.ld) */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* = 0x20000000 + 0x20000 = 0x20020000 */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
}
/* SP is initialised to _estack = 0x20020000 (one past the last RAM byte) */
/* The vector table word 0 contains this value, loaded by hardware at reset */
On STM32F411 with 128 KB SRAM, RAM runs from 0x20000000 to 0x2001FFFF. The initial SP is set to 0x20020000 — one word past the top of RAM. The first PUSH decrements SP to 0x2001FFFC and writes to that address, which is valid SRAM.
Partitioning SRAM for MSP and PSP
In bare-metal applications, the MSP uses the full SRAM stack region. When introducing an RTOS or a bare-metal dual-stack design, SRAM is split between the two stack pointers. A common partition gives each stack an equal fixed size.
2. Startup code runs in Thread mode using MSP
3. Before entering user code, software:
a. Loads PSP = STACK_PSP_START
b. Sets CONTROL[1] = 1 (switch to PSP)
c. Executes ISB (flush pipeline)
4. Thread code now runs using PSP
5. On any IRQ: hardware switches to MSP
6. ISR runs entirely on MSP stack
7. IRQ return: hardware switches back to PSP
MSP/PSP summary — 4 key rules
- There are exactly 2 physical stack pointer registers in every Cortex-M processor (MSP and PSP).
- MSP is the default after reset; it is used by all exception/IRQ handlers and by Thread mode code when CONTROL[1]=0.
- PSP is an alternate stack pointer, available only in Thread mode, typically used for application tasks in RTOS environments.
- After power-up, hardware automatically initialises MSP by reading word 0 of the vector table (address 0x00000000). PSP is not initialised by hardware — software must set it before switching.
Switching Between MSP and PSP
The active stack pointer in Thread mode is controlled by bit 1 (SPSEL) of the CONTROL register. Writing CONTROL[1]=1 makes the processor use PSP as SP in Thread mode. This must be done carefully — the switch takes effect after the pipeline is flushed, and SP must point to valid stack memory before the switch.
Bit 0 (nPRIV): 0 = privileged Thread mode, 1 = unprivileged Thread mode
Bit 1 (SPSEL): 0 = use MSP in Thread mode, 1 = use PSP in Thread mode
Bit 2 (FPCA): FPU context active (Cortex-M4F only)
Handler mode always uses MSP regardless of SPSEL.
Method 1: Direct assembly with MSR/MRS
/* Read current CONTROL value, set SPSEL, write back, flush */
void switch_to_psp_asm(uint32_t psp_top)
{
uint32_t ctrl;
/* Step 1: initialise PSP before switching to it */
__asm volatile ("MSR PSP, %0" : : "r"(psp_top) : "memory");
/* Step 2: set SPSEL bit in CONTROL */
__asm volatile ("MRS %0, CONTROL" : "=r"(ctrl));
ctrl |= (1U << 1); /* SPSEL = 1 → use PSP */
__asm volatile (
"MSR CONTROL, %0\n\t"
"ISB\n\t" /* instruction sync barrier — required after CONTROL write */
: : "r"(ctrl) : "memory"
);
/* From this point SP (R13) refers to PSP */
}
Method 2: Naked function in C
A naked function is a C function decorated with __attribute__((naked)). The compiler emits no prologue (PUSH) or epilogue (POP PC) for naked functions — only the inline assembly body appears. This is essential for SP-switching code because a normal C function’s prologue would PUSH to the stack using whatever SP is currently active, potentially corrupting either MSP or PSP data during the switch.
/* Naked function: compiler generates NO prologue/epilogue */
__attribute__((naked)) void switch_sp_to_psp(void)
{
__asm volatile (
/* R0 contains the first argument (psp_value) on entry */
"MSR PSP, R0 \n\t" /* load PSP with the provided stack top */
"MRS R0, CONTROL \n\t" /* read CONTROL */
"ORR R0, R0, #0x02 \n\t" /* set bit 1 (SPSEL) */
"MSR CONTROL, R0 \n\t" /* write CONTROL */
"ISB \n\t" /* flush pipeline */
"BX LR \n\t" /* return — now running on PSP */
);
}
/* Caller: */
#define PSP_STACK_SIZE 1024U
static uint8_t psp_stack[PSP_STACK_SIZE] __attribute__((aligned(8)));
void setup_psp(void)
{
uint32_t psp_top = (uint32_t)(psp_stack + PSP_STACK_SIZE);
switch_sp_to_psp(psp_top);
/* All code from here uses PSP */
}
Method 3: Reading current PSP/MSP values
static inline uint32_t get_msp(void)
{
uint32_t val;
__asm volatile ("MRS %0, MSP" : "=r"(val));
return val;
}
static inline uint32_t get_psp(void)
{
uint32_t val;
__asm volatile ("MRS %0, PSP" : "=r"(val));
return val;
}
static inline void set_msp(uint32_t val)
{
__asm volatile ("MSR MSP, %0" : : "r"(val) : "memory");
}
static inline void set_psp(uint32_t val)
{
__asm volatile ("MSR PSP, %0" : : "r"(val) : "memory");
}
Full bare-metal dual-stack initialisation
/* linker symbols from STM32 ld file */
extern uint32_t _estack; /* top of RAM = initial MSP */
/* Dedicate bottom 1 KB of the stack region to PSP */
#define MSP_SIZE 1024U
#define PSP_SIZE 1024U
/* In the linker script, _estack = RAM_END.
MSP uses RAM_END down to (RAM_END - MSP_SIZE).
PSP uses (RAM_END - MSP_SIZE) down to (RAM_END - MSP_SIZE - PSP_SIZE). */
static const uint32_t MSP_START = (uint32_t)&_estack;
static const uint32_t PSP_START = (uint32_t)&_estack - MSP_SIZE;
__attribute__((naked)) void init_psp_stack(void)
{
__asm volatile (
"LDR R0, =PSP_START\n\t" /* load PSP_START address */
"LDR R0, [R0] \n\t" /* dereference to get the value */
"MSR PSP, R0 \n\t" /* set PSP */
"MRS R0, CONTROL \n\t"
"ORR R0, R0, #0x02 \n\t" /* SPSEL = 1 */
"MSR CONTROL, R0 \n\t"
"ISB \n\t"
"BX LR \n\t"
);
}
int main(void)
{
init_psp_stack();
/* Thread code now uses PSP.
MSP is reserved for fault handlers and ISRs. */
while (1) { /* application loop */ }
}
Mode, Privilege, and SP Selection Table
| Processor Mode | CONTROL[0] (nPRIV) | CONTROL[1] (SPSEL) | Active SP | Typical Use |
|---|---|---|---|---|
| Thread (after reset) | 0 (privileged) | 0 | MSP | Startup / bare-metal main() |
| Thread (RTOS task) | 1 (unprivileged) | 1 | PSP | FreeRTOS / RTX task code |
| Thread (privileged + PSP) | 0 | 1 | PSP | Bare-metal dual-stack setups |
| Handler (any IRQ/fault) | N/A (always priv.) | Ignored | MSP always | ISRs, SVC, PendSV, SysTick |
Automatic SP Switch on Exception Entry and Exit
One of the most elegant Cortex-M hardware features is automatic, transparent MSP/PSP switching on exception entry and return. The programmer’s RTOS code doesn’t have to manually switch stacks for IRQ servicing.
Exception entry sequence
- Interrupt fires while Thread code runs on PSP.
- Hardware stacks exception frame (R0-R3, R12, LR, PC, xPSR) onto the PSP (the current stack).
- Hardware switches SP to MSP and sets EXC_RETURN = 0xFFFFFFFD in LR.
- ISR executes entirely using MSP. It can freely PUSH/POP on MSP.
Exception return sequence
- ISR executes BX LR (or POP {PC}). LR = 0xFFFFFFFD.
- Hardware detects the magic EXC_RETURN value (bits 31:4 = 0xFFFFFFF).
- Bit 2 of EXC_RETURN = 1 → return to Thread mode using PSP.
- Hardware pops the exception frame from PSP, restoring R0-R3, R12, LR, PC, xPSR.
- Processor resumes Thread code execution on PSP exactly where it left off.
| EXC_RETURN Value | Return to | SP after return |
|---|---|---|
0xFFFFFFF1 |
Handler mode | MSP |
0xFFFFFFF9 |
Thread mode | MSP |
0xFFFFFFFD |
Thread mode | PSP ← most common in RTOS |
Frequently Asked Questions
No. For bare-metal projects without tasks, MSP alone is sufficient. You only need PSP when you want to isolate stack usage between kernel/ISR code and application thread code — most commonly when adding an RTOS or building a custom task scheduler.
PSP gets the value you wrote, but Thread mode continues using MSP (because SPSEL=0). The PSP write is harmless — it just doesn’t take effect yet. When you later set SPSEL=1, the PSP value you previously wrote becomes active immediately.
Yes. Even though the ISR runs on MSP, the PSP register still holds the Thread mode stack pointer. The ISR can read it with MRS R0, PSP. This is exactly what FreeRTOS’s PendSV handler does to find and save the current task’s stack frame.
The compiler normally generates PUSH {LR} at the start of any non-leaf function and POP {PC} at the end to save/restore the link register. With __attribute__((naked)), the compiler trusts you to handle everything in inline assembly. This is necessary for SP-switching functions where any stack access before the switch could corrupt the wrong stack.
Yes. The ARM architecture specification requires an ISB (Instruction Synchronisation Barrier) after any write to CONTROL. Without it, instructions already in the pipeline may execute using the old CONTROL state. The ISB flushes the pipeline so all subsequent instructions see the new SPSEL value.
At minimum: the deepest call stack (all callee-saved pushes) + one exception frame (32 bytes without FPU, 104 bytes with FPU) + a safety margin. In practice, 512 bytes is a reasonable bare minimum for a simple task; 1–4 KB is typical in FreeRTOS applications.

2 Comments