PendSV Exception & Context Switching on ARM Cortex-M3/M4-Embedded Systems Training Institute in Hyderabad

ARM Cortex-M Programming
Free Embedded C Programming Course

PendSV Exception & Context Switching on ARM Cortex-M3/M4

Understand how PendSV works, why it is the cornerstone of every bare-metal RTOS, and how to build a round-robin task scheduler from scratch in C.

▶ ARM Cortex-M3 / M4📚 Intermediate Level🚀 Free Course✎ Hands-On C Code

1 — SVC Handler Exercise: Four Arithmetic Operations

In the previous lesson you learned how to write and invoke an SVC (SuperVisor Call) handler. This exercise puts that knowledge into practice: the thread-mode application passes two operands through the exception stack frame, uses the SVC number to select an operation, and reads the result back.

Why use SVC for arithmetic? In a real RTOS, a task running in unprivileged Thread mode cannot directly access certain hardware resources. SVC is the clean, hardware-enforced gate through which unprivileged code requests a privileged service. This exercise mimics that pattern exactly.

SVC Service Numbers

SVC NumberOperationExpressionLevel
36Additionresult = op1 + op2Basic
37Subtractionresult = op1 - op2Basic
38Multiplicationresult = op1 * op2Intermediate
39Divisionresult = op1 / op2Guard needed

How Operands Are Passed via the Stack Frame

When the CPU takes any exception it automatically pushes eight registers onto the active stack. We exploit those saved registers — especially r0 and r1 — to pass operands to the SVC handler without any global variables.

Cortex-M exception stack frame — values pushed by hardware on SVC entry (stack grows downward)
SP+28
xPSR (Program Status Register)
SP+24
PC (Return Address — points past the SVC instruction)
SP+20
LR (Link Register)
SP+16
r12
SP+12
r3
SP+8
r2
SP+4
r1 — operand 2 (op2)
SP+0
r0 — operand 1 (op1) / result written back here

The SVC number is encoded in the lower byte of the SVC instruction itself, found 2 bytes before the saved PC.

Complete Implementation

main.c (caller)

main.c — Thread-mode caller

#include <stdint.h>
#include <stdio.h>

#define SVC_ADD  36
#define SVC_SUB  37
#define SVC_MUL  38
#define SVC_DIV  39

/*
 * Passes op1 into r0 and op2 into r1, then fires SVC #num.
 * The handler writes the result into the saved r0 slot in the
 * stack frame, so the CPU restores it into r0 on return.
 */
static inline int32_t svc_call(uint8_t num, int32_t op1, int32_t op2)
{
    int32_t result;
    __asm volatile(
        "mov r0, %1    \n"
        "mov r1, %2    \n"
        "svc %3        \n"
        "mov %0, r0    \n"
        : "=r"(result)
        : "r"(op1), "r"(op2), "I"(num)
        : "r0", "r1", "cc"
    );
    return result;
}

int main(void)
{
    int32_t a = 20, b = 4;

    printf("Add : %d + %d = %d\n", a, b, svc_call(SVC_ADD, a, b));
    printf("Sub : %d - %d = %d\n", a, b, svc_call(SVC_SUB, a, b));
    printf("Mul : %d * %d = %d\n", a, b, svc_call(SVC_MUL, a, b));
    printf("Div : %d / %d = %d\n", a, b, svc_call(SVC_DIV, a, b));

    while(1);
}

SVC Handler

svc_handler.c — Privileged handler

#include <stdint.h>

/*
 * svc_frame[] maps directly to the exception stack frame:
 *   [0]=r0  [1]=r1  [2]=r2  [3]=r3
 *   [4]=r12 [5]=LR  [6]=PC  [7]=xPSR
 *
 * The SVC number is the low byte of the 2-byte Thumb SVC instruction
 * located immediately before the saved PC.
 */
void SVC_Handler_Main(uint32_t *svc_frame)
{
    uint8_t  svc_num = ((uint8_t *)svc_frame[6])[-2];
    int32_t  op1     = (int32_t)svc_frame[0];
    int32_t  op2     = (int32_t)svc_frame[1];
    int32_t  result  = 0;

    switch (svc_num) {
        case 36: result = op1 + op2;                          break;
        case 37: result = op1 - op2;                          break;
        case 38: result = op1 * op2;                          break;
        case 39: result = (op2 != 0) ? op1 / op2 : 0;       break;
        default:                                               break;
    }

    /* Write result into saved r0 — caller receives it on return */
    svc_frame[0] = (uint32_t)result;
}

/* Naked trampoline: determine which stack was active, pass it to C */
__attribute__((naked)) void SVC_Handler(void)
{
    __asm volatile(
        "tst   lr, #4          \n"  /* EXC_RETURN bit[2]: 0=MSP, 1=PSP */
        "ite   eq               \n"
        "mrseq r0, msp          \n"
        "mrsne r0, psp          \n"
        "b     SVC_Handler_Main \n"
    );
}

Key trick: EXC_RETURN in LR tells us which stack (MSP or PSP) was active when the SVC fired. We always read the correct frame regardless of privilege level.

Expected Output

Expected Serial/UART Output

// UART console — 115200 baud
Add : 20 + 4 = 24
Sub : 20 – 4 = 16
Mul : 20 * 4 = 80
Div : 20 / 4 = 5

Test tip: Set b = 0 and verify the division guard returns 0 instead of triggering a HardFault from divide-by-zero.

2 — What Is the PendSV Exception?

PendSV (Pendable Service Call) is exception type 14 on every ARM Cortex-M processor. Unlike hardware interrupts which are triggered by peripherals, PendSV is triggered entirely by software — by setting a single bit in the Interrupt Control and State Register (ICSR).

🔧
Trigger Mechanism

Software writes bit 28 (PENDSVSET) of SCB->ICSR. The exception is held pending until the CPU is ready to take it.

🌟
Priority

Always configured to the lowest possible priority (0xFF / 255) so it runs only after every other pending exception has been serviced.

⚖
Primary Use

Deferred context switching. The scheduler pends PendSV; the actual register save/restore happens inside the PendSV handler, safely after all ISRs finish.

🔄
Secondary Use

Bottom-half interrupt processing — split time-critical ISR work from the slower “bottom half” that runs at low priority in PendSV.

Exception Priority Landscape

ARM Cortex-M exception priorities — lower numeric value means higher priority
NMI
-2
Fixed, non-maskable
HardFault
-1
Fixed
SysTick
e.g. 0x00
High — scheduler runs here
IRQ (ISRs)
0x40 to 0xC0
Configurable per peripheral
PendSV
0xFF (lowest)
Always last — perfect for context switch

How to Trigger PendSV in C

Pend the PendSV exception from software

#include "stm32f4xx.h"  /* or CMSIS core_cm4.h */

void scheduler_init(void)
{
    /* Set PendSV to the absolute lowest priority at startup */
    NVIC_SetPriority(PendSV_IRQn, 0xFF);
}

/* Call this from inside any ISR to request a context switch */
void request_context_switch(void)
{
    /* Set PENDSVSET bit[28] in ICSR.
       PendSV will fire after the current exception returns. */
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

3 — How Context Switching Works

Context switching is the act of saving the CPU register state of the running task and restoring the register state of the next task so that each task appears to have its own private processor. The Cortex-M hardware handles part of this automatically; the PendSV handler does the rest.

What Must Be Saved and Restored?

Auto-saved by hardware on exception entry

xPSR, PC, LR, r12, r3, r2, r1, r0

Pushed onto the task’s Process Stack (PSP) automatically before the PendSV handler runs.

Manually saved by PendSV handler

r4, r5, r6, r7, r8, r9, r10, r11

These callee-saved registers are NOT pushed by hardware. The PendSV assembly stub must push and pop them explicitly.

Normal Operation: SysTick + PendSV Timeline

Round-robin context switch — SysTick triggers the decision, PendSV performs the switch
Task A
Task A runs
►
SysTick
SysTick fires
►
Scheduler
Pick next, pend PendSV
►
PendSV
Save A, Restore B
►
Task B
Task B runs
►
SysTick
SysTick fires
►
PendSV
Save B, Restore A
►
Task A
Task A resumes

Step-by-Step Sequence (with an IRQ in the middle)

  1. Task A runs in Thread mode (PSP active) The CPU executes Task A’s code using the Process Stack Pointer.
  2. SysTick fires — hardware auto-saves 8 registers The CPU pushes xPSR, PC, LR, r12, r3, r2, r1, r0 onto Task A’s PSP and jumps to SysTick_Handler.
  3. Scheduler selects the next task, pends PendSV SysTick_Handler updates the TCB pointer then writes SCB->ICSR |= PENDSVSET. It does NOT touch any task registers.
  4. SysTick returns — any pending IRQ runs first Because PendSV has the lowest priority, any peripheral interrupt that was waiting runs to completion before PendSV is taken.
  5. PendSV handler runs — performs the actual switch The handler reads the PSP, pushes r4-r11 of Task A, saves the updated PSP into TCB-A, loads PSP from TCB-B, then pops r4-r11 of Task B.
  6. PendSV returns with EXC_RETURN = 0xFFFFFFFD Hardware pops r0-r3, r12, LR, PC, xPSR from Task B’s stack. Task B resumes exactly where it was interrupted last time.

4 — Why Switching Inside SysTick Causes a UsageFault

A tempting shortcut is to perform the entire context switch directly inside the SysTick handler. This approach breaks badly whenever any peripheral interrupt fires during a task time slot.

Danger: If SysTick attempts to return to Thread mode while a peripheral ISR is still present on the exception stack, the Cortex-M raises a UsageFault (INVPC) — an invalid attempt to return to Thread mode from a nested exception context.

Context switching without PendSV — IRQ mid-slot causes UsageFault
Task A
Task A
►
IRQ fires
ISR starts
►
SysTick
SysTick preempts ISR
►
Switch attempt
SysTick tries Thread return
►
FAULT
UsageFault INVPC

PendSV solves this: the switch is deferred until the processor is genuinely back at the base exception level with an empty exception stack. PendSV then exits cleanly to Thread mode.

StrategyInterrupt-safe?ComplexityRecommended?
Switch inside SysTick handlerNo — UsageFault riskLowNo
Switch in PendSV at lowest priorityYes — always safeMediumYes

5 — Offloading Interrupt Processing with PendSV

Long-running ISR work starves other interrupts and reduces system responsiveness. The solution — used in Linux, FreeRTOS, and every industrial RTOS — is to split the ISR into two halves:

First Half — Top Half (ISR)

Time-Critical Work

Runs inside the ISR at high priority. Acknowledges the hardware, reads raw data into a buffer, then pends PendSV and exits immediately.

Second Half — Bottom Half (PendSV)

Time-Consuming Work

Runs inside PendSV at the lowest priority. Processes the buffered data, runs CRC checks, parses packets, calls application-level logic.

Bottom-half deferred processing — ISR0 exits fast, PendSV handles the heavy work later
Task A
Task A
►
IRQ#0
ISR0 top half — fast
►
IRQ#1
ISR1 not delayed
►
PendSV
Bottom half — heavy work
►
Task A
Task A resumes

Top-half / bottom-half UART processing pattern

#include <stdint.h>
#include "ring_buffer.h"

static ring_buf_t uart_rx_buf;

/* TOP HALF — runs at UART ISR priority, must finish in microseconds */
void USART2_IRQHandler(void)
{
    uint8_t byte = USART2->DR & 0xFF;
    ring_buf_put(&uart_rx_buf, byte);

    /* Schedule bottom half — PendSV fires after all ISRs complete */
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

/* BOTTOM HALF — runs in PendSV at lowest priority, can take its time */
void PendSV_Handler(void)
{
    uint8_t byte;
    while (ring_buf_get(&uart_rx_buf, &byte)) {
        protocol_process_byte(byte);
    }
}

6 — Implementing a Round-Robin Scheduler

A round-robin scheduler gives every task an equal time slice and cycles through them in a fixed circular order. No task can monopolise the CPU; each one gets its turn regardless of what it is doing. It is the simplest preemptive scheduling algorithm and is an ideal first RTOS to build from scratch.

Plan: SysTick fires every 1 ms, advances the current-task pointer, and pends PendSV. The PendSV assembly handler saves r4-r11 of the outgoing task, switches the PSP, and restores r4-r11 of the incoming task. Hardware handles the remaining eight registers on exception exit.

Task Control Block (TCB) — Data Structure

scheduler.h

#ifndef SCHEDULER_H
#define SCHEDULER_H

#include <stdint.h>

#define MAX_TASKS   4
#define STACK_SIZE  512   /* 512 words = 2 KB per task */

typedef enum { TASK_READY = 0, TASK_RUNNING, TASK_BLOCKED } task_state_t;

typedef struct {
    uint32_t     *psp;                    /* saved process stack pointer */
    task_state_t  state;
    uint32_t      id;
    uint32_t      stack[STACK_SIZE];      /* private stack storage       */
} tcb_t;

void scheduler_init(void);
void task_create(uint32_t id, void (*entry)(void));
void scheduler_start(void);
void task_exit_hook(void);

#endif

scheduler.c

scheduler.c — init, task creation, SysTick handler

#include "scheduler.h"
#include "stm32f4xx.h"

static tcb_t   tcb[MAX_TASKS];
static uint8_t task_count = 0;
static uint8_t current    = 0;

/* Exported symbols — PendSV assembly reads these */
tcb_t *current_tcb = NULL;
tcb_t *next_tcb    = NULL;

/*
 * Build a fake exception stack frame so PendSV can "restore" a brand-new task
 * the same way it restores any previously interrupted task.
 *
 * Stack layout after init (grows downward, top of stack is highest address):
 *   ... [r11][r10][r9][r8][r7][r6][r5][r4]  ← manually-saved by PendSV
 *       [r0] [r1] [r2][r3][r12][LR][PC][xPSR] ← auto-saved by hardware
 */
static void init_task_stack(tcb_t *t, void (*entry)(void))
{
    uint32_t *sp = &t->stack[STACK_SIZE];   /* top of stack */

    /* Hardware exception frame */
    *--sp = 0x01000000;             /* xPSR — Thumb bit set, required! */
    *--sp = (uint32_t)entry;         /* PC   — task entry function      */
    *--sp = (uint32_t)task_exit_hook;/* LR   — if task ever returns     */
    *--sp = 0x00000000;             /* r12                             */
    *--sp = 0x00000003;             /* r3                              */
    *--sp = 0x00000002;             /* r2                              */
    *--sp = 0x00000001;             /* r1                              */
    *--sp = 0x00000000;             /* r0                              */

    /* Manually-saved frame (PendSV pushes/pops r4-r11) */
    *--sp = 0x00000011;  /* r11 */
    *--sp = 0x00000010;  /* r10 */
    *--sp = 0x00000009;  /* r9  */
    *--sp = 0x00000008;  /* r8  */
    *--sp = 0x00000007;  /* r7  */
    *--sp = 0x00000006;  /* r6  */
    *--sp = 0x00000005;  /* r5  */
    *--sp = 0x00000004;  /* r4  */

    t->psp = sp;
}

void task_create(uint32_t id, void (*entry)(void))
{
    tcb_t *t = &tcb[task_count++];
    t->id    = id;
    t->state = TASK_READY;
    init_task_stack(t, entry);
}

void scheduler_init(void)
{
    NVIC_SetPriority(PendSV_IRQn,  0xFF); /* PendSV: lowest */
    NVIC_SetPriority(SysTick_IRQn, 0x00); /* SysTick: highest configurable */
    SysTick_Config(SystemCoreClock / 1000); /* 1 ms time slice */
}

/* SysTick: only decides which task is next, never touches registers */
void SysTick_Handler(void)
{
    current_tcb = &tcb[current];
    current     = (current + 1) % task_count;
    next_tcb    = &tcb[current];

    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

void task_exit_hook(void)
{
    /* Tasks should never return. Spin here if one does. */
    while(1);
}

/* Launch the scheduler — loads PSP of task 0 and switches to Thread mode */
void scheduler_start(void)
{
    current_tcb = &tcb[0];
    __asm volatile(
        "msr  psp, %0         \n"  /* set PSP to task 0's initial SP */
        "mov  r0, #0x03       \n"  /* CONTROL: SPSEL=1(PSP), nPRIV=1 */
        "msr  control, r0     \n"
        "isb                  \n"
        "ldm  %0, {r4-r11}    \n"  /* pop manually-saved regs        */
        "add  %0, %0, #32     \n"  /* advance past r4-r11 (8x4 bytes) */
        "msr  psp, %0         \n"
        "pop  {r0-r3,r12,lr}  \n"
        "pop  {pc}            \n"  /* jump to task entry             */
        : : "r"(current_tcb->psp) : "memory"
    );
}

pendsv_handler.s

ARM Assembly

    .syntax unified
    .thumb

    .extern current_tcb     @ pointer-to-pointer: running task TCB
    .extern next_tcb        @ pointer-to-pointer: next task TCB

    .global PendSV_Handler
    .type   PendSV_Handler, %function

PendSV_Handler:

    @ ── 1. Save outgoing task context ─────────────────────────────
    mrs   r0, psp               @ r0 = PSP of outgoing task
    isb

    @ Push callee-saved registers onto the task stack
    stmdb r0!, {r4-r11}         @ store r4-r11, decrement r0

    @ Persist updated PSP into the TCB
    ldr   r2, =current_tcb
    ldr   r1, [r2]              @ r1 = current_tcb pointer
    str   r0, [r1, #0]          @ current_tcb->psp = updated PSP

    @ ── 2. Restore incoming task context ──────────────────────────
    ldr   r3, =next_tcb
    ldr   r1, [r3]              @ r1 = next_tcb pointer
    ldr   r0, [r1, #0]          @ r0 = next_tcb->psp

    @ Pop callee-saved registers from the new task's stack
    ldmia r0!, {r4-r11}         @ load r4-r11, increment r0

    @ Restore PSP to point at the hardware-saved frame
    msr   psp, r0
    isb

    @ Update current_tcb to reflect the new running task
    str   r1, [r2, #0]          @ current_tcb = next_tcb

    @ ── 3. Return to Thread mode using PSP ─────────────────────────
    @ EXC_RETURN = 0xFFFFFFFD:
    @   bits[3:0] = 1101
    @   bit[4]=1  → no FP extension
    @   bit[3]=1  → return to Thread mode
    @   bit[2]=1  → use PSP
    @   bit[1]=0  → reserved
    @   bit[0]=1  → must be 1
    ldr   lr, =0xFFFFFFFD
    bx    lr

EXC_RETURN = 0xFFFFFFFD is the magic value that tells the CPU to exit exception mode, return to unprivileged Thread mode, and use the PSP for the stack. Any other value will corrupt the CPU state or trigger a fault.

tasks.c + main.c

tasks.c + main.c — four concurrent tasks

#include <stdio.h>
#include <stdint.h>
#include "scheduler.h"

static void delay(volatile uint32_t n) { while(n--); }

void task1(void)          /* LED blink */
{
    while(1) {
        printf("[Task 1] LED toggle\n");
        delay(100000);
    }
}

void task2(void)          /* counter */
{
    uint32_t count = 0;
    while(1) {
        printf("[Task 2] count = %lu\n", ++count);
        delay(200000);
    }
}

void task3(void)          /* sensor read */
{
    while(1) {
        printf("[Task 3] sensor read\n");
        delay(150000);
    }
}

void task4(void)          /* heartbeat */
{
    while(1) {
        printf("[Task 4] heartbeat\n");
        delay(80000);
    }
}

int main(void)
{
    scheduler_init();

    task_create(1, task1);
    task_create(2, task2);
    task_create(3, task3);
    task_create(4, task4);

    scheduler_start();   /* never returns */
    return 0;
}

// Serial output — tasks interleave every 1 ms
[Task 1] LED toggle
[Task 2] count = 1
[Task 3] sensor read
[Task 4] heartbeat
[Task 1] LED toggle
[Task 2] count = 2
…

Build & Flash

Compile and flash to STM32F411 Nucleo board

# Prerequisites: sudo apt install gcc-arm-none-eabi openocd

arm-none-eabi-gcc \
  -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
  -O2 -g -Wall \
  -T STM32F411CEUX_FLASH.ld \
  main.c scheduler.c tasks.c pendsv_handler.s \
  startup_stm32f411xe.s \
  -o scheduler_demo.elf

# Create binary for flashing
arm-none-eabi-objcopy -O binary scheduler_demo.elf scheduler_demo.bin

# Flash via OpenOCD (STLink)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
  -c "program scheduler_demo.elf verify reset exit"

# Monitor UART output
minicom -b 115200 -D /dev/ttyACM0

Verify it works: All four tasks should print their messages in a cyclic pattern. If only one task prints, check that PendSV_Handler is correctly linked and that PendSV priority is set lower than SysTick.

🧠 Quick Knowledge Check

1. What register must you write to trigger PendSV from software?

  1. NVIC_ISER — Interrupt Set Enable Register
  2. SCB->ICSR — bit 28 (PENDSVSET)
  3. SysTick->CTRL
  4. SCB->SHPR3
Check Answer

Correct answer: B — SCB->ICSR — bit 28 (PENDSVSET)

SCB->ICSR bit 28 (PENDSVSET) is the software trigger for PendSV.

2. Why is PendSV configured with the lowest priority in an RTOS?

  1. To make context switches faster
  2. So it only runs after all other pending exceptions complete, making the return to Thread mode always safe
  3. Because it cannot be preempted once started
  4. To reduce interrupt latency for peripherals
Check Answer

Correct answer: B — So it only runs after all other pending exceptions complete, making the return to Thread mode always safe

Lowest priority ensures PendSV fires only when the exception stack is empty, preventing the INVPC UsageFault.

3. Which registers does Cortex-M hardware save automatically on exception entry?

  1. r4, r5, r6, r7, r8, r9, r10, r11
  2. r0, r1, r2, r3, r12, LR, PC, xPSR
  3. All 16 general-purpose registers
  4. Only PC and xPSR
Check Answer

Correct answer: B — r0, r1, r2, r3, r12, LR, PC, xPSR

The hardware exception frame is r0-r3, r12, LR, PC, xPSR. The PendSV handler manually saves r4-r11.

4. What EXC_RETURN value returns the CPU to Thread mode using the PSP?

  1. 0xFFFFFFF1 — Handler mode, MSP
  2. 0xFFFFFFF9 — Thread mode, MSP
  3. 0xFFFFFFFD — Thread mode, PSP
  4. 0x00000000 — reset value
Check Answer

Correct answer: C — 0xFFFFFFFD — Thread mode, PSP

0xFFFFFFFD = Thread mode + PSP, no FP extension. This is the standard EXC_RETURN for task return in an RTOS.

5. Why do we set xPSR = 0x01000000 in the fake task stack frame?

  1. To enable all interrupts in the new task
  2. To set the Thumb bit (T bit = bit 24), which must always be 1 on Cortex-M
  3. To indicate that the task has the highest priority
  4. To reset the carry and overflow flags
Check Answer

Correct answer: B — To set the Thumb bit (T bit = bit 24), which must always be 1 on Cortex-M

Bit 24 (T bit) of xPSR must always be 1 on Cortex-M. A T=0 state triggers a UsageFault (INVSTATE) on the next instruction.

Image 1 “ARM Cortex-M exception priority levels — horizontal bar chart from NMI at top to PendSV at bottom”

Alt text: “ARM Cortex-M exception priority diagram for embedded C programming course”
Image 2 “Round-robin task scheduler timeline with SysTick and PendSV — four coloured task bars”

Alt text: “Round-robin scheduler context switch timeline ARM Cortex-M4 bare-metal C”
Image 3 “Cortex-M exception stack frame diagram showing r0-r3, r12, LR, PC, xPSR positions”

Alt text: “ARM Cortex-M SVC exception stack frame layout embedded C programming”

Frequently Asked Questions

What is PendSV and why is it needed for an RTOS?

PendSV (Pendable Service Call) is ARM Cortex-M exception number 14, triggered entirely by software via the ICSR register. You need it because doing a context switch directly inside a higher-priority handler (like SysTick) is unsafe when other interrupt requests (IRQs) are present.

If SysTick tries to return to Thread mode while a peripheral ISR is still stacked, the CPU raises a UsageFault (INVPC). By setting PendSV to the lowest priority and only pending it from the scheduler, the actual context switch always happens after the entire exception stack is clear, making the return to Thread mode always safe.

What is the difference between MSP and PSP on ARM Cortex-M?

Cortex-M has two stack pointers. The Main Stack Pointer (MSP) is used by exception handlers and kernel code. The Process Stack Pointer (PSP) is used by task code running in Thread mode when the SPSEL bit of the CONTROL register is set to 1.

Separating stacks prevents a buggy task from overwriting kernel stack memory. An RTOS startup routine sets SPSEL=1 so all tasks use PSP. Exception handlers automatically switch to MSP regardless of SPSEL, so the OS stack is never corrupted by task activity.

How is round-robin different from priority-based scheduling?

Round-robin gives every task an equal time slice and cycles through them in order. No task can starve another. It is simple to implement and great for learning.

Priority scheduling (used by FreeRTOS, Zephyr, RTEMS) assigns each task a priority level. Higher-priority tasks preempt lower-priority ones and run first. Lower-priority tasks only run when all higher-priority tasks are blocked or sleeping. This is better for real systems with mixed latency requirements (e.g. a fast motor control loop at high priority and a display update loop at low priority).

Why do we build a fake stack frame when creating a new task?

The PendSV handler restores a task by popping r4-r11 from its process stack, then returning via EXC_RETURN which causes the hardware to automatically pop r0-r3, r12, LR, PC, and xPSR.

A brand-new task has never been interrupted — there is no real saved state. By pushing a carefully crafted fake frame at task creation (with PC pointing to the task entry function, xPSR with the Thumb bit set, and LR pointing to a clean-up hook), the PendSV handler can start a new task using exactly the same restore path it uses for all other tasks. No special “first-time” code is needed.

Can I use this scheduler on any Cortex-M microcontroller?

Yes, with minor adjustments. The core mechanism — SysTick + PendSV + PSP/MSP separation — is standardised across all Cortex-M0, M0+, M3, M4, M7, and M33 devices.

Device-specific parts include the linker script, startup file, clock setup, and UART/printf routing. The CMSIS register headers abstract most differences, so porting usually means swapping the BSP layer while keeping all scheduler logic identical. For production use, consider building on top of FreeRTOS or Zephyr instead, but building it yourself first is the best way to understand it.

How does PendSV bottom-half processing relate to Linux softirq and workqueues?

The concept is identical. Linux uses the same top-half / bottom-half pattern for interrupt handling:

Top half (hard IRQ) — runs in hardware interrupt context, must be extremely fast, cannot sleep. It acknowledges the hardware and schedules a bottom half using tasklet_schedule() or queue_work().

Bottom half (tasklet / workqueue) — runs at a lower software priority, can do heavier processing. Tasklets run in softirq context; workqueues run in a kernel thread and can sleep.

PendSV on bare-metal Cortex-M is the simplest possible implementation of this widely-used pattern. Understanding it at the hardware level gives you deep insight into how Linux and every RTOS handles deferred work.

What is the SVC exception and when should I use it?

SVC (SuperVisor Call) is exception type 11. It is the only way for code running in unprivileged Thread mode to request a privileged service from the OS kernel.

Use SVC when you want to enforce a privilege boundary — for example, a user task needs to enable an interrupt, access a protected peripheral, or call a kernel API. The SVC number (0-255) selects which service to invoke, and operands are passed through r0-r3 or the exception stack frame.

In simple bare-metal projects without privilege separation you rarely need SVC, but it is essential in any system that uses the MPU (Memory Protection Unit) to isolate tasks.

EmbeddedPathashala — Embedded Systems Programming on ARM Cortex-M3/M4

1 Comment

Leave a Reply

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