SysTick Setup & Context Switch Stack Frames-Embedded C Course Online

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

← PREV_LECNEXT_LEC →

Free Embedded C Programming Course

SysTick Setup & Context Switch Stack Frames

Calculate the SysTick reload value for a precise 1 ms tick, then trace the full T1-to-T2 context switch through both hardware-created (SF1) and software-created (SF2) stack frames.

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

1 — Quick Recap: The Two Stack Frames

A context switch creates exactly two stack frames on the outgoing task’s stack and destroys two frames on the incoming task’s stack. Understanding which frame is created by hardware and which by software is the key to writing a correct PendSV handler.

▶

Stack Frame 1 (SF1)

Hardware Exception Frame

Created automatically by the CPU on every exception entry. Contains: xPSR, PC, LR, r12, r3, r2, r1, r0 — 8 registers, 32 bytes. Destroyed automatically by the CPU on EXC_RETURN.

🖊

Stack Frame 2 (SF2)

Software Context Frame

Created manually by the PendSV handler in assembly. Contains: r11, r10, r9, r8, r7, r6, r5, r4 — 8 registers, 32 bytes. Destroyed manually by the PendSV handler on restore.

💾

PSP in TCB

Saved Stack Pointer

After both frames are pushed, the updated PSP is saved into the task’s TCB entry. On restore, the TCB value is loaded back into PSP so the CPU knows where to unstack from.

Complete Suspended Task Stack (lower address = top of stack)
PSP
▲ PSP saved in TCB TCB
+4
r4 SF2
+8
r5 SF2
+12
r6 SF2
+16
r7 SF2
+20
r8 SF2
+24
r9 SF2
+28
r10 SF2
+32
r11 SF2
+36
r0 SF1
+40
r1 SF1
+44
r2 SF1
+48
r3 SF1
+52
r12 SF1
+56
LR SF1
+60
PC (resume) SF1
+64
xPSR SF1
↓
… local variables / call frames …
SF1 — 32 bytes Created by CPU hardware on exception entry
SF2 — 32 bytes Created by PendSV handler in assembly
Total — 64 bytes Complete 16-register context

SF2 (manual, purple) sits on top of SF1 (hardware, yellow). PSP in TCB points to the r4 slot at the very top.

2 — SysTick Reload Value Calculation (16 MHz Clock)

The SysTick timer is a simple 24-bit down-counter. It starts at a reload value, counts down to zero, fires an interrupt, reloads, and repeats. To get a 1 ms interrupt on a 16 MHz processor you need to find the correct reload value.

The Mathematics

Processor Clock

16,000,000

Hz (16 MHz)

÷

Desired Frequency

1,000

Hz (1 kHz = 1 ms period)

=

Reload Value

16,000

counts per tick

Step-by-Step Time Resolution

1

1 ÷ 16,000,000 = 0.0625 μs

One SysTick clock cycle at 16 MHz takes 62.5 nanoseconds (0.0625 microseconds)

2

0.0625 μs × 16 = 1 μs

16 counts produce a 1 microsecond delay

3

0.0625 μs × 16,000 = 1 ms

16,000 counts produce a 1 millisecond delay — this is the reload value

General formula:
Reload Value = (Processor Clock Hz ÷ Desired Tick Hz) − 1

The −1 accounts for the fact that SysTick fires when the counter reaches zero, not when it wraps. With a reload of 15,999 the counter counts from 15,999 → 0 (16,000 steps total), giving exactly 1 ms.

Reload Values for Common Clocks

MCU / Clock SpeedDesired PeriodReload ValueFormula
STM32F0 @ 16 MHz1 ms15,999(16,000,000 / 1,000) − 1
STM32F4 @ 84 MHz1 ms83,999(84,000,000 / 1,000) − 1
STM32F4 @ 168 MHz1 ms167,999(168,000,000 / 1,000) − 1
STM32F7 @ 216 MHz1 ms215,999(216,000,000 / 1,000) − 1
Any clockAny period(Clock / TickHz) − 1CMSIS: SysTick_Config(SystemCoreClock / TickHz)

Maximum reload value: SysTick is a 24-bit counter, so the maximum reload value is 0xFFFFFF = 16,777,215. On a 168 MHz processor, the lowest possible tick frequency is 168,000,000 / 16,777,215 ≈ 10 Hz (100 ms minimum). For longer periods you must use a general-purpose timer instead.

3 — Configuring SysTick in C

CMSIS (recommended)

C

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

#define TICK_HZ   1000U   /* desired tick frequency: 1 kHz = 1 ms period */

void systick_init(void)
{
    /*
     * SysTick_Config() does three things:
     *   1. Writes the reload register: SYST_RVR = (SystemCoreClock / TICK_HZ) - 1
     *   2. Sets the current value register to zero: SYST_CVR = 0
     *   3. Enables SysTick with processor clock source + interrupt enabled
     *
     * SystemCoreClock is a CMSIS global set by SystemInit() to the actual
     * core clock after PLL configuration (e.g. 16,000,000 or 168,000,000).
     */
    uint32_t reload = SystemCoreClock / TICK_HZ - 1;

    if (reload > 0xFFFFFF) {
        /* Reload value exceeds 24-bit counter — tick period too long */
        while (1);  /* trap: use a GPTimer instead */
    }

    SysTick_Config(SystemCoreClock / TICK_HZ);

    /* Priority: SysTick highest, PendSV lowest — critical for correct operation */
    NVIC_SetPriority(SysTick_IRQn, 0);
    NVIC_SetPriority(PendSV_IRQn,  0xFF);
}

Manual registers

C

#include <stdint.h>

/* SysTick register base address (same on all Cortex-M) */
#define SYST_CSR   (*(volatile uint32_t *)0xE000E010U) /* Control & Status  */
#define SYST_RVR   (*(volatile uint32_t *)0xE000E014U) /* Reload Value Reg  */
#define SYST_CVR   (*(volatile uint32_t *)0xE000E018U) /* Current Value Reg */

/* CSR bits */
#define SYST_ENABLE    (1U << 0)   /* enable counter          */
#define SYST_TICKINT   (1U << 1)   /* enable exception        */
#define SYST_CLKSOURCE (1U << 2)   /* 1 = processor clock     */

#define PROCESSOR_CLOCK_HZ  16000000U
#define TICK_HZ             1000U

void systick_init_manual(void)
{
    /* 1. Disable SysTick while configuring */
    SYST_CSR = 0;

    /* 2. Set reload value: counts from this value down to 0, then fires */
    SYST_RVR = (PROCESSOR_CLOCK_HZ / TICK_HZ) - 1;
    /*         = (16,000,000 / 1,000)  - 1  =  15,999                      */

    /* 3. Clear current value to avoid immediate fire on enable */
    SYST_CVR = 0;

    /* 4. Enable counter | enable interrupt | use processor clock */
    SYST_CSR = SYST_ENABLE | SYST_TICKINT | SYST_CLKSOURCE;
}

Key registers: SYST_RVR is the reload (the value loaded when counter hits 0). SYST_CVR is the live counter (write any value to clear it to 0). SYST_CSR bit 2 selects the clock source: 1 = processor clock, 0 = external reference clock.

SysTick_Handler

C

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

/*
 * SysTick_Handler fires every 1 ms.
 *
 * IMPORTANT: This handler must NOT perform any context switch itself.
 * Doing so risks a UsageFault (INVPC) if a peripheral IRQ fires during
 * a task's time slot (see previous lesson for full explanation).
 *
 * The only job here is:
 *   1. Advance the scheduler's task index
 *   2. Set PENDSVSET to defer the actual switch to PendSV
 */
void SysTick_Handler(void)
{
    /* Move the scheduler index to the next task (round-robin) */
    update_next_task();

    /* Request context switch via PendSV — fires after all ISRs complete */
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

Keep SysTick_Handler short. Every microsecond spent in SysTick_Handler is time stolen from all tasks. The handler shown above runs in under 10 clock cycles. Never call printf, read sensors, or perform any blocking operation inside it.

4 — Phase 1: Hardware Creates SF1 on T1’s Stack

When SysTick fires while T1 is running in Thread mode, the CPU automatically creates Stack Frame 1 (SF1) on T1’s PSP stack before transferring control to SysTick_Handler. No code you write causes this — it is entirely automatic.

A

T1 running in Thread mode — PSP is active

T1 is executing its task code. The PSP register points to the top of T1’s current stack (local variables, call frames). The CPU is in Thread mode, unprivileged, using PSP.

B

SysTick fires — CPU begins exception entry

The SysTick counter reaches zero. The CPU detects the pending exception and begins the exception entry sequence before executing the next instruction in T1.

C

CPU pushes SF1 onto T1’s PSP stack — automatically

The processor pushes 8 registers in a fixed order: r0, r1, r2, r3, r12, LR, PC, xPSR. The PSP decrements by 32 bytes. This is the hardware-created Stack Frame 1.

D

CPU switches to Handler mode — MSP now active

LR is loaded with EXC_RETURN (0xFFFFFFFD). The CPU enters Handler mode, switches to the MSP, and jumps to SysTick_Handler. T1’s PSP still points to the top of SF1.

T1 Stack — BEFORE SysTick
PSP
▲ T1’s SP (running)
↓
free stack space
low
local vars / frames
►
T1 Stack — AFTER SF1 pushed
new PSP
▲ T1’s PSP (top of SF1)
+4
r0 SF1
+8
r1 SF1
+12
r2 SF1
+16
r3 SF1
+20
r12 SF1
+24
LR SF1
+28
PC ← T1 resumes SF1
+32
xPSR SF1
↓
local vars / frames

SF1 is pushed by hardware. PSP decrements by 32 bytes. SysTick_Handler runs on MSP (not PSP).

5 — Phase 2: Software Saves SF2 on T1’s Stack (PendSV)

SysTick_Handler updates the scheduler index and pends PendSV, then exits. PendSV fires next (at lowest priority, after all other ISRs). Inside PendSV, the programmer’s code creates Stack Frame 2 (SF2) by pushing r4–r11 manually, then saves T1’s updated PSP into the TCB.

Code

@ PendSV_Handler — Phase 2: save T1's context (SF2)
@ At entry: CPU is in Handler mode using MSP
@ T1's PSP still points to the top of SF1 (r0 slot)

    mrs   r0, psp           @ r0 = T1's current PSP (top of SF1)
    isb

    @ Push r4–r11 BELOW SF1, building SF2 on T1's stack
    @ STMDB: Decrement Before each store — stack grows downward
    @ After this: r0 points to r4 (top of SF2 = top of complete frame)
    stmdb r0!, {r4-r11}

    @ Save the updated PSP (pointing to top of SF2) into T1's TCB
    @ This is the value we will need to RESTORE T1 in the future
    push  {lr}              @ preserve EXC_RETURN before BL overwrites LR
    bl    save_current_psp  @ save_current_psp(r0) — stores r0 in TCB[current]
    bl    update_next_task  @ advance round-robin index to T2
    pop   {lr}              @ restore EXC_RETURN
T1 Stack — After SF1 (start of PendSV)
PSP
▲ PSP = top of SF1 (r0)
+4
r0 SF1
+8
r1 SF1
…
r2, r3, r12, LR, PC, xPSR SF1
►
T1 Stack — After SF2 pushed (saved to TCB)
new PSP
▲ PSP → saved to TCB[T1]
+4
r4 SF2
+8
r5 SF2
+12
r6 SF2
+16
r7 SF2
+20
r8 SF2
+24
r9 SF2
+28
r10 SF2
+32
r11 SF2
+36
r0 SF1
…
r1, r2, r3, r12, LR, PC, xPSR SF1

STMDB pushes r4–r11 below SF1. The new PSP (pointing to r4) is stored in TCB[T1]. T1’s context is now completely saved.

6 — Phase 3: Software Restores SF2 from T2’s Stack

Now PendSV loads T2’s saved PSP from the TCB and pops the 8 callee-saved registers from T2’s stack. After this, r4–r11 hold T2’s values and the PSP is updated to point to the top of T2’s SF1 (the hardware frame), ready for the CPU to unstack automatically.

Code

@ PendSV_Handler — Phase 3: restore T2's context (SF2)

    push  {lr}
    bl    get_next_psp      @ r0 = TCB[T2].psp — points to T2's r4 (top of SF2)
    pop   {lr}

    @ Pop r4–r11 from T2's stack (SF2)
    @ LDMIA: Increment After each load — stack shrinks upward
    @ r4 is loaded first (lowest address), r11 last
    @ After this: r0 points to T2's SF1 (r0 slot of T2's hardware frame)
    ldmia r0!, {r4-r11}

    @ Write the updated PSP — now pointing to T2's SF1
    @ When PendSV returns with BX LR, the CPU will unstack SF1 from here
    msr   psp, r0
    isb

    @ EXC_RETURN in LR = 0xFFFFFFFD → Thread mode, PSP, no FP
    @ CPU will automatically pop SF1 (r0–r3, r12, LR, PC, xPSR) from PSP
    bx    lr
T2 Stack — Before PendSV restores (T2 was suspended earlier)
TCB PSP
▲ loaded from TCB[T2]
+4
r4 SF2 ← pop
+8
r5 SF2 ← pop
+12
r6–r11 SF2 ← pop
+36
r0 SF1
…
r1, r2, r3, r12, LR, PC, xPSR SF1
►
T2 Stack — After LDMIA (r4–r11 restored)
new PSP
▲ PSP → top of T2’s SF1
+4
r0 SF1
+8
r1 SF1
+12
r2 SF1
+16
r3 SF1
+20
r12 SF1
+24
LR SF1
+28
PC ← T2 resumes here SF1
+32
xPSR SF1

LDMIA pops SF2 (r4–r11) and advances PSP to point at T2’s SF1. The hardware will unstack SF1 automatically on exception return.

7 — Phase 4: Hardware Unstacks SF1 — T2 Runs

The final step requires no code at all. When PendSV executes BX LR with LR = 0xFFFFFFFD, the CPU detects the EXC_RETURN pattern and automatically pops T2’s SF1 from the PSP stack, restoring all eight hardware-saved registers. T2 resumes exactly where it was interrupted.

T2 Stack — unstacking SF1
PendSV executes
BX LR
↓
CPU detects EXC_RETURN
0xFFFFFFFD
↓
Bit 2 = 1
Use PSP for unstack
↓
Pop r0,r1,r2,r3,
r12,LR,PC,xPSR
from T2’s PSP
↓
T2 runs at restored PC
Thread mode, PSP active
PSP
▲ PSP before unstack
+4
r0 popped ✓
+8
r1 popped ✓
+12
r2 popped ✓
+16
r3 popped ✓
+20
r12 popped ✓
+24
LR popped ✓
+28
PC → T2 resumes ✓
+32
xPSR popped ✓
new PSP
▲ PSP after unstack
↓
T2 local vars / frames

Hardware unstacking: CPU pops SF1 from T2’s PSP, advances PSP by 32 bytes, jumps to T2’s saved PC. T2 runs as if never interrupted.

The complete T1→T2 switch is done in four phases:
HW pushes SF1 on T1 → SW pushes SF2 on T1, saves PSP in TCB → SW pops SF2 from T2, loads PSP from TCB → HW pops SF1 from T2, resumes T2.

8 — Complete Scheduler Code Walk-Through

main.c

C

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

void task1(void) { while(1) { printf("[T1] running\n"); for(volatile int i=0;i<50000;i++); } }
void task2(void) { while(1) { printf("[T2] running\n"); for(volatile int i=0;i<50000;i++); } }
void task3(void) { while(1) { printf("[T3] running\n"); for(volatile int i=0;i<50000;i++); } }
void task4(void) { while(1) { printf("[T4] running\n"); for(volatile int i=0;i<50000;i++); } }

int main(void)
{
    /* Step 1: Initialise scheduler stacks and TCBs */
    sched_init(task1, task2, task3, task4);

    /* Step 2: Start 1 ms SysTick, set PendSV to lowest priority */
    systick_init();

    /* Step 3: Switch Thread mode to use PSP, launch task1 */
    sched_launch();   /* never returns */

    return 0;
}

scheduler.c

C

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

#define TASK_COUNT   4
#define STACK_SIZE   1024U      /* bytes per task stack */
#define SRAM_END     (0x20000000U + (128U*1024U))

/* Stack tops for each task (stacks grow downward from these addresses) */
static const uint32_t stk_top[TASK_COUNT] = {
    SRAM_END,
    SRAM_END - STACK_SIZE,
    SRAM_END - 2*STACK_SIZE,
    SRAM_END - 3*STACK_SIZE
};

static uint32_t tcb_psp[TASK_COUNT]; /* saved PSP for each task */
static uint8_t  current = 0;

/* Build fake SF1 + SF2 on each task's stack so PendSV can first-restore */
static void build_stack(uint8_t idx, void(*fn)(void))
{
    uint32_t *sp = (uint32_t *)stk_top[idx];

    /* SF1 — hardware exception frame (fake values, PC points to task fn) */
    *--sp = 0x01000000;   /* xPSR  — Thumb bit set         */
    *--sp = (uint32_t)fn;  /* PC    — task entry            */
    *--sp = 0xFFFFFFFD;   /* LR    — EXC_RETURN           */
    *--sp = 0;             /* r12                          */
    *--sp = 0;             /* r3                           */
    *--sp = 0;             /* r2                           */
    *--sp = 0;             /* r1                           */
    *--sp = 0;             /* r0                           */

    /* SF2 — manually saved callee registers (zeros for new task) */
    *--sp = 0; /* r11 */
    *--sp = 0; /* r10 */
    *--sp = 0; /* r9  */
    *--sp = 0; /* r8  */
    *--sp = 0; /* r7  */
    *--sp = 0; /* r6  */
    *--sp = 0; /* r5  */
    *--sp = 0; /* r4  */

    tcb_psp[idx] = (uint32_t)sp; /* PSP = top of SF2 = top of r4 slot */
}

void sched_init(void(*t1)(void),void(*t2)(void),void(*t3)(void),void(*t4)(void))
{
    void(*fns[TASK_COUNT])(void) = {t1, t2, t3, t4};
    for (int i = 0; i < TASK_COUNT; i++) build_stack(i, fns[i]);
}

void systick_init(void)
{
    SysTick_Config(SystemCoreClock / 1000U); /* 1 ms tick */
    NVIC_SetPriority(SysTick_IRQn, 0x00);
    NVIC_SetPriority(PendSV_IRQn,  0xFF);
}

void update_next_task(void) { current = (current + 1) % TASK_COUNT; }
uint32_t get_next_psp(void)         { return tcb_psp[current]; }
void save_current_psp(uint32_t psp) {
    uint8_t prev = (current + TASK_COUNT - 1) % TASK_COUNT;
    tcb_psp[prev] = psp;
}

void SysTick_Handler(void)
{
    update_next_task();
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

void sched_launch(void)
{
    __asm volatile(
        "ldr  r0, =tcb_psp    \n"
        "ldr  r0, [r0]        \n"   /* PSP of task 0 */
        "msr  psp, r0         \n"
        "mov  r0, #0x02       \n"   /* CONTROL: SPSEL=1 */
        "msr  control, r0     \n"
        "isb                  \n"
        "ldm  sp!, {r4-r11}   \n"   /* pop SF2 of task 0 */
        "pop  {r0-r3,r12,lr}  \n"   /* pop partial SF1 */
        "pop  {pc}            \n"   /* jump to task entry */
        : : : "memory"
    );
}

pendsv_handler.s

C

    .syntax unified
    .thumb

    .extern save_current_psp   @ void(uint32_t)
    .extern get_next_psp       @ uint32_t(void)
    .extern update_next_task   @ void(void)

    .global PendSV_Handler
    .type   PendSV_Handler, %function

PendSV_Handler:

    @ ─── SAVE T1 (outgoing) ─────────────────────────────────────
    @ SF1 already on T1's PSP stack (hardware did it on SysTick entry)
    @ We only push SF2 (r4-r11)

    mrs   r0, psp               @ r0 = T1's PSP (top of SF1)
    isb
    stmdb r0!, {r4-r11}         @ push SF2: r4-r11 below SF1
                                @ r0 now = top of complete saved frame

    push  {lr}                  @ save EXC_RETURN
    bl    save_current_psp      @ TCB[T1].psp = r0  ← T1 fully saved
    pop   {lr}

    @ ─── RESTORE T2 (incoming) ──────────────────────────────────
    @ Load T2's PSP from TCB, pop SF2 (r4-r11), set PSP to T2's SF1 top

    push  {lr}
    bl    get_next_psp          @ r0 = TCB[T2].psp (top of T2's SF2)
    pop   {lr}

    ldmia r0!, {r4-r11}         @ pop SF2: r4-r11 restored to T2's values
                                @ r0 now = top of T2's SF1

    msr   psp, r0               @ PSP = top of T2's SF1
    isb

    @ BX LR with EXC_RETURN = 0xFFFFFFFD:
    @ CPU automatically pops SF1 from T2's PSP:
    @   r0, r1, r2, r3, r12, LR, PC, xPSR  ← hardware unstack
    @ T2 resumes at its saved PC in Thread mode
    bx    lr

Output

Expected UART Output

// Tasks interleave every 1 ms — each gets ~1 ms slice
[T1] running
[T2] running
[T3] running
[T4] running
[T1] running
[T2] running
[T3] running
[T4] running
// Cycle repeats every 4 ms (4 × 1 ms)

If all 4 tasks print in round-robin order your scheduler is working. If only T1 prints, PendSV is not firing — check that PENDSVSET is set in SysTick_Handler and PendSV priority is lower than SysTick (0xFF vs 0x00).

🧠 Quick Knowledge Check

1. What is the SysTick reload value for a 1 ms tick on a 16 MHz processor?

  • a) 16,000,000
  • b) 15,999
  • c) 1,000
  • d) 16,000
Check Answer
Correct answer: b) 15,999. (16,000,000 / 1,000) − 1 = 15,999. The −1 is because the counter fires at zero, so it counts 15,999 → 0 (16,000 steps = 1 ms).

2. What registers are in Stack Frame 1 (SF1)?

  • a) r4, r5, r6, r7, r8, r9, r10, r11
  • b) r0, r1, r2, r3, r12, LR, PC, xPSR
  • c) All 16 registers
  • d) PC, xPSR, and SP only
Check Answer
Correct answer: b) r0, r1, r2, r3, r12, LR, PC, xPSR. SF1 is the hardware exception frame: r0–r3, r12, LR, PC, xPSR. Created automatically by the CPU on exception entry.

3. Who creates Stack Frame 2 (SF2) and when?

  • a) The CPU hardware, on exception entry
  • b) The SysTick_Handler C function
  • c) The PendSV handler assembly code, using STMDB to push r4–r11
  • d) The linker script at compile time
Check Answer
Correct answer: c) The PendSV handler assembly code, using STMDB to push r4–r11. SF2 is created by the programmer inside PendSV_Handler using STMDB r0!, {r4-r11}. The hardware only creates SF1.

4. After saving T1 and before restoring T2, what value is stored in TCB[T1]?

  • a) The original PSP before any exception (top of T1’s local variables)
  • b) The PSP after SF1 was pushed (top of r0 in SF1)
  • c) The PSP after SF2 was pushed (top of r4 in SF2)
  • d) The address of T1’s task function
Check Answer
Correct answer: c) The PSP after SF2 was pushed (top of r4 in SF2). After STMDB pushes r4–r11 (SF2), r0 points to the r4 slot — the very top of the complete saved context. This value is saved to TCB[T1].

5. How does the CPU know to unstack SF1 from T2’s PSP (not from MSP) when PendSV returns?

  • a) PendSV writes a flag to the NVIC before returning
  • b) The EXC_RETURN value in LR (0xFFFFFFFD) encodes bit 2 = 1 which means “use PSP”
  • c) The CPU always unstacks from PSP on PendSV return
  • d) The MSR PSP instruction tells the CPU to use PSP
Check Answer
Correct answer: b) The EXC_RETURN value in LR (0xFFFFFFFD) encodes bit 2 = 1 which means “use PSP”. EXC_RETURN = 0xFFFFFFFD has bit 2 set (PSP). When the CPU sees BX LR with this value, it unstacks from PSP, returns to Thread mode, and T2 resumes at the saved PC.

6. Why must SYST_CVR be cleared to zero before enabling SysTick?

  • a) To prevent the counter from wrapping around immediately and firing an interrupt before the first expected tick
  • b) To reset the processor clock
  • c) The CVR register controls the interrupt priority
  • d) It is not necessary; the hardware clears it on startup
Check Answer
Correct answer: a) To prevent the counter from wrapping around immediately and firing an interrupt before the first expected tick. If CVR contains a random value at startup, SysTick could fire after only a few counts rather than after a full 1 ms period. Clearing CVR to 0 ensures the first tick fires at the correct time.
Image 1 — SysTick Calculation “Number line showing 16 MHz ÷ 1000 = 16,000 counts, with 0.0625 μs per count, 1 μs = 16 counts, 1 ms = 16,000 counts”

Alt: “SysTick reload value calculation 16MHz ARM Cortex-M 1ms tick embedded C”
Image 2 — SF1 + SF2 Stack Frames “T1 stack showing SF2 (purple, r4–r11) stacked below SF1 (yellow, r0–r3 r12 LR PC xPSR), with PSP arrow at top pointing to r4”

Alt: “ARM Cortex-M context switch SF1 SF2 stack frames PendSV handler embedded C”
Image 3 — Four-Phase Context Switch “Four-panel diagram: HW pushes SF1 on T1 → SW pushes SF2 on T1 → SW pops SF2 from T2 → HW pops SF1 from T2”

Alt: “ARM Cortex-M four phase context switch T1 to T2 SysTick PendSV bare metal”

Frequently Asked Questions

Does SysTick_Config() subtract 1 from the reload value automatically?

Yes. The CMSIS SysTick_Config(ticks) function writes ticks - 1 to the SYST_RVR register. So when you call SysTick_Config(SystemCoreClock / 1000) it writes 15,999 to SYST_RVR for a 16 MHz clock.

If you configure the registers manually, you must subtract 1 yourself: SYST_RVR = (CLOCK / TICK_HZ) - 1;. Forgetting the -1 means your timer fires one count late every tick, accumulating a small but measurable timing error.

Can I use a different timer instead of SysTick for the RTOS tick?

Yes, but SysTick is strongly preferred because it is part of the Cortex-M core standard — it exists on every Cortex-M device without any device-specific configuration. FreeRTOS, Zephyr, and most commercial RTOS products use SysTick as the default tick source.

You might choose a GPTimer instead if: (1) you need a tick period longer than the SysTick maximum (about 100 ms at 168 MHz), (2) you need the scheduler to work with the processor in low-power tickless idle mode, or (3) your SysTick is already used for another purpose (unusual but possible in complex systems).

What is the exact order in which SF1 registers are pushed to the stack?

The Cortex-M architecture reference manual specifies this exact order (from highest to lowest stack address, i.e., r0 is at the lowest address):

Highest address (first pushed): xPSR → PC → LR → r12 → r3 → r2 → r1 → r0 (lowest address, PSP points here)

This means when you read [PSP + 0] you get r0, [PSP + 4] gives r1, and [PSP + 28] gives PC (the resume address). This is why the SVC handler trick of reading the SVC number from ((uint8_t *)frame[6])[-2] works — frame[6] is PC, and the SVC instruction is 2 bytes before it.

After sched_launch(), how does the very first task start without a real exception frame?

The fake stack frames built by build_stack() solve this. When sched_launch() sets PSP to tcb_psp[0] (which points to the r4 slot of task 0’s fake SF2), it then manually pops SF2 and SF1 using regular POP instructions.

The last instruction is POP {pc}, which loads the PC with the task’s entry function address — jumping directly into the task. From this point on, SysTick fires every 1 ms and creates a real SF1, and PendSV creates a real SF2. The fake frames are only needed for the very first launch.

What is the total RAM overhead of this 4-task scheduler?

Task stacks: 4 × 1 KB = 4 KB
Scheduler (MSP) stack: 1 KB
TCB array (PSP values): 4 × 4 bytes = 16 bytes
Code (scheduler + PendSV): ≈ 100–200 bytes in flash
Total stack RAM: 5 KB

On an STM32F407 with 192 KB of SRAM, this is about 2.6% overhead — negligible. Each additional task costs 1 KB of stack plus 4 bytes of TCB. You could run 180+ tasks before hitting the RAM limit (though 4–20 is typical for practical designs).

Why is it important that SysTick has a higher priority than PendSV?

If PendSV had the same or higher priority than SysTick, PendSV could preempt SysTick before it finishes. At that point, the scheduler’s task index might not yet be updated, leading to the wrong task being switched in.

More critically, if a peripheral IRQ (at some intermediate priority) fires during a task’s time slot, and PendSV has a higher priority than that IRQ, PendSV would preempt the ISR and attempt to return to Thread mode while the ISR is still on the exception stack — causing a UsageFault (INVPC). Setting PendSV to the absolute lowest priority (0xFF) prevents all these problems: PendSV only runs when there is nothing else pending.

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

← PREV_LECNEXT_LEC →

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

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