MSP PSP Stack Placement-Embedded C Programming Training Online

MSP PSP Stack Placement | ARM Cortex-M Embedded Systems

EmbeddedPathashala — ARM Cortex-M Course Home All Lectures STM32 Projects

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.

Home › ARM Cortex-M Course › Lecture 12 — MSP PSP Stack Placement

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.

MSP (Main Stack Pointer, R13 / SP_main)
• 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

Type 1: Data → Heap → Stack (middle) → Unused
RAM_END
Unused
space
STACK_START
Stack ↓
STACK_END
Heap ↑
HEAP_START
Data/.bss
RAM_START
Characteristics:
• Stack begins at a fixed STACK_START label
• 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)

Type 2: Data → Heap → Unused → Stack (top)
RAM_END = STACK_START
Stack ↓
STACK_END
Unused
space
Heap ↑
HEAP_START
Data/.bss
RAM_START
Characteristics:
• Stack starts at RAM_END (top of SRAM)
• 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
Why Type 2 is preferred: With the stack at the top of RAM, a stack overflow first consumes the unused buffer zone before it can reach heap or data segments. This gives the MPU a natural place to put a guard region at the heap top. In Type 1, the stack and heap grow toward each other with no such natural buffer.

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.

MSP / PSP SRAM Partition (1 KB each)
STACK_MSP_START (top of RAM)
MSP Stack ↓
(Handler + kernel code)
STACK_MSP_END = STACK_PSP_START
PSP Stack ↓
(Thread / task code)
STACK_PSP_END
Operation flow:
1. Reset → SP = STACK_MSP_START (hardware)
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
The boundary address (STACK_MSP_END = STACK_PSP_START) is the dividing line. An MPU guard region placed at this boundary catches MSP overflows before they corrupt PSP data, and vice versa.

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.

CONTROL register bits:
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 */
}
Why naked? If switch_sp_to_psp were a normal function, GCC might generate PUSH {LR} as a prologue. At that point SP still points to MSP. This PUSH would occur before the inline assembly body executes, meaning one word is written to MSP stack before the switch. The function would then return from PSP — and that pre-switch PUSH to MSP would leave MSP permanently misbalanced. The naked attribute prevents this entirely.

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
FreeRTOS pattern: FreeRTOS uses PendSV as its context-switch ISR. On PendSV entry, hardware switches to MSP (Handler mode). The PendSV handler saves the current task’s PSP value (via MRS R0, PSP), pushes remaining registers manually, stores the PSP in the TCB, loads the next task’s TCB, restores registers, writes its PSP via MSR PSP, R0, and returns. On exception return, hardware switches back to PSP — the new task’s stack.

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

  1. Interrupt fires while Thread code runs on PSP.
  2. Hardware stacks exception frame (R0-R3, R12, LR, PC, xPSR) onto the PSP (the current stack).
  3. Hardware switches SP to MSP and sets EXC_RETURN = 0xFFFFFFFD in LR.
  4. ISR executes entirely using MSP. It can freely PUSH/POP on MSP.

Exception return sequence

  1. ISR executes BX LR (or POP {PC}). LR = 0xFFFFFFFD.
  2. Hardware detects the magic EXC_RETURN value (bits 31:4 = 0xFFFFFFF).
  3. Bit 2 of EXC_RETURN = 1 → return to Thread mode using PSP.
  4. Hardware pops the exception frame from PSP, restoring R0-R3, R12, LR, PC, xPSR.
  5. 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

Q: Do I need to use PSP in a bare-metal (no RTOS) project?

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.

Q: What happens if I write to PSP without setting SPSEL=1?

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.

Q: Can an ISR read the task’s PSP value?

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.

Q: Why does a naked function not have a prologue/epilogue?

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.

Q: Is ISB really necessary after writing CONTROL?

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.

Q: What is a safe minimum PSP stack size?

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.

ARM MSP PSP Stack Pointer Selection CONTROL Register SPSEL Bit Naked Function ARM FreeRTOS Context Switch STM32 Stack Layout Embedded RTOS Free Embedded Course

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *