UNIT 13 · LESSON 3 OF 6

Tasks, Context Switching, and Scheduling

What if the scheduler could stop the long job in the middle, run the sampler, and resume the long job exactly where it was, without the long job being written any differently?

INTERACTIVEPreemptive and cooperative scheduling of the same tasks
A timeline of three periodic tasks under preemptive or cooperative schedulingA (high)B (mid)C (low)05101520msA: worst response 1 ms, deadline 5 msmetB: worst response 4 ms, deadline 10 msmetC: worst response 15 ms, deadline 20 msmetCPU load 80 %. All deadlines met.
A timeline of three periodic tasks under preemptive or cooperative schedulingA (high)B (mid)C (low)05101520msA: worst response 1 ms, deadline 5 msmetB: worst response 4 ms, deadline 10 msmetC: worst response 15 ms, deadline 20 msmetCPU load 80 %. All deadlines met.

Try this

Scheduler
Priorities
C takes
A 1 ms, B 4 ms, C 15 ms: all deadlines met.

Three periodic tasks, each with its deadline at the end of its period: A (1 ms every 5 ms), B (3 ms every 10 ms) and C (every 20 ms). With preemption, a task that becomes ready runs at once if it is more urgent than the running one; cooperatively, it waits until the running task gives up the CPU. Priorities by period (shortest period most urgent, “rate monotonic”) or reversed. Red bars finish after their deadline.

What you will be able to do
  • Describe a task’s states (running, ready, blocked, suspended) and what moves a task between them.
  • Predict a preemptive fixed-priority schedule and compare it with the cooperative one for the same tasks.
  • Explain how the FreeRTOS Cortex-M0+ port performs a context switch with PendSV and what it saves on each task’s stack.
  • Explain time slicing between tasks of equal priority and when it happens.
  • Assign priorities by period (rate monotonic) and recognise when a reversed assignment misses deadlines.
Before you start
  • Cooperative scheduling and its worst-case delay (lesson 2).
  • Exception entry, the stacked frame and EXC_RETURN (unit 6, lesson 4; unit 9, lesson 2).
Steps in this lesson
  1. Tasks
  2. Preemptive, fixed-priority scheduling
  3. Time slicing between equal priorities
  4. How the switch happens on a Cortex-M0+
  5. Worked example: the 6 ms task
  6. Common misconceptions

The puzzle

Lesson 2 ended with a job that cannot be split: a 30 ms library call, and a sensor that must be sampled every 2 ms. With cooperative scheduling the sensor waits 30 ms. What if the scheduler could stop the long job in the middle, run the sampler, and resume the long job exactly where it was, without the long job being written any differently?

STEP 1

Tasks

An RTOS lets you write each activity as a task: an ordinary C function, usually an endless loop, with its own stack and a priority. Because each task has its own stack, it can be stopped anywhere, in the middle of a function call chain, and resumed later with every local variable intact. That is what protothreads (lesson 2) cannot do.

void sampler(void *arg) {                        /* a FreeRTOS task */
    TickType_t last = xTaskGetTickCount();
    for (;;) {
        read_sensor();
        xTaskDelayUntil(&last, pdMS_TO_TICKS(2));  /* previous wake time + 2 ms (configTICK_RATE_HZ = 1000) */
    }
}

At any moment each task is in one state. FreeRTOS names them running (on the CPU; one per core), ready (could run, waiting for the CPU), blocked (waiting for time to pass or for an event: a queue, a semaphore, a notification) and suspended (taken out of scheduling explicitly). A blocked task uses no CPU time at all. When nothing is ready, the RTOS runs its idle task, at priority 0, where the CPU can sleep.

STEP 2

Preemptive, fixed-priority scheduling

The scheduler’s rule is short: the highest-priority ready task runs. It is applied whenever something changes, which Zephyr calls reschedule points: a task blocks, a task becomes ready (a semaphore is given, a delay expires on a tick), or an interrupt handler returns after making a task ready. If a task more urgent than the running one becomes ready, the running task is preempted at once, in the middle of whatever it was doing.

↑ This step uses the figure at the top of the page.

Priority numbering is not portable: in FreeRTOS a higher number is more urgent (the idle task is 0), in Zephyr a lower number is more urgent and negative numbers are cooperative threads, and in the NVIC a lower number is more urgent (unit 9, lesson 3). Interrupts still preempt every task, whatever its priority, unless they are masked.

Choosing priorities. For periodic tasks with deadlines at the end of their periods, a good and simple rule is rate monotonic: the shorter the period, the higher the priority. It is not about importance: a critical task with a long period can safely run at a low priority if the analysis (lesson 6) shows it still meets its deadline, and a short, frequent task at a low priority can miss its deadlines while longer tasks run.

STEP 3

Time slicing between equal priorities

Tasks of the same priority do not preempt each other. With preemption on and time slicing enabled (FreeRTOS configUSE_TIME_SLICING, which defaults to 1), the tick interrupt requests a switch whenever another task of the running task’s priority is ready, so equal-priority tasks take turns, one tick each. Zephyr can do the same for preemptible threads with a configurable slice. Time slicing shares the CPU among equals; it does nothing for latency between different priorities.

STEP 4

How the switch happens on a Cortex-M0+

On Cortex-M, the FreeRTOS port performs every task switch in the PendSV exception, which it sets to the lowest priority (it writes 255 into PendSV’s field of the SHPR3 register), and it gives the tick (SysTick) the same lowest priority. A switch is requested by making PendSV pending: taskYIELD() does it from task code, and the tick handler does it when the tick made a switch necessary. Because PendSV has the lowest priority, the switch runs only after every other handler has finished; when another handler pended it, the core goes straight on into it by tail-chaining (unit 9, lesson 2).

INTERACTIVEA context switch, word by word
The stacks of two tasks during a FreeRTOS context switch on a Cortex-M0+task A’s stack (high → low address)A’s own localsxPSRPCLRR12R3R2R1R0R11R10R9R8R7R6R5R4EXC_RETURN← TCBtask B’s stack (high → low address)B’s own localsxPSRPCLRR12R3R2R1R0R11R10R9R8R7R6R5R4EXC_RETURN← TCBswitching: exception entry and PendSV_Handlerstep 2 of 5PendSV_Handler reads PSP, stores EXC_RETURN and R4–R11 below the hardware frame (9words, 36 bytes), and saves the new stack top in A’s TCB.teal: stacked by the hardware (8 words) · orange: stacked by PendSV_Handler (9 words)
The stacks of two tasks during a FreeRTOS context switch on a Cortex-M0+task A stackA’s own localsxPSRPCLRR12R3R2R1R0R11R10R9R8R7R6R5R4EXC_RETURN← TCBtask B stackB’s own localsxPSRPCLRR12R3R2R1R0R11R10R9R8R7R6R5R4EXC_RETURN← TCBswitching: exception entry and PendSV_Handlerstep 2 of 5PendSV_Handler reads PSP, stores EXC_RETURN andR4–R11 below the hardware frame (9 words, 36 bytes),and saves the new stack top in A’s TCB.teal: stacked by the hardware (8 words) · orange:stacked by PendSV_Handler (9 words)
Step 2: switching from A to B; 17 words (68 bytes) saved per suspended task.

On a Cortex-M0+ running FreeRTOS (the generic ARM_CM0 port on FreeRTOS main), switching tasks is an exception return with a different stack. The hardware saves and restores eight registers; the port’s PendSV handler saves the other eight (R4–R11) with EXC_RETURN and swaps the stack pointer. Each suspended task keeps 17 words (68 bytes) of context on its own stack; the RP2040 port saves 16 (plus 4 with divider saving). A task that has never run gets a fake frame built by pxPortInitialiseStack() “as it would be created by a context switch interrupt”, so its first switch-in starts the task function.

On exception entry, the core pushes 8 registers (xPSR, PC, LR, R12, R3–R0) onto the running task’s stack, which is the process stack (PSP). The port’s PendSV_Handler saves the rest: it moves the stack pointer down by 36 bytes, stores EXC_RETURN and R4–R11 there, and writes the new stack top into the task’s control block (TCB). It calls vTaskSwitchContext() with interrupts masked, which picks the highest-priority ready task, then loads that task’s saved stack top, restores its R4–R11, points PSP at its hardware frame and returns. The exception return pops the other 8 registers, and the new task continues where it was stopped.

With the generic FreeRTOS ARM_CM0 port, every suspended task therefore holds 17 words, 68 bytes, of saved registers on its own stack (without a floating-point unit); the RP2040 port the pico-sdk builds saves R4–R11 without EXC_RETURN, 16 words (64 bytes), plus 4 words if it also saves the hardware divider, on top of its own worst-case stack use. A new task is started the same way: pxPortInitialiseStack() builds a fake frame whose PC is the task function, whose R0 is its parameter, and whose LR is an error trap (prvTaskExitError) in case the function ever returns.

STEP 5

Worked example: the 6 ms task

Three tasks have deadlines equal to their periods: A needs 1 ms every 5 ms, B 3 ms every 10 ms, C 6 ms every 20 ms. The load is

U=15+310+620=0.8U = \frac{1}{5} + \frac{3}{10} + \frac{6}{20} = 0.8

Preemptive, rate monotonic (A highest): A runs 0–1, B 1–4, C 4–5. A is released at 5 and preempts C: A 5–6, C 6–10 (5 ms done). At 10, A and B are released: A 10–11, B 11–14, then C’s last millisecond 14–15. Worst responses: A 1 ms, B 4 ms, C 15 ms, all within their periods. Cooperatively, C starts at 4 and keeps the CPU until 10: A, released at 5, starts at 10 and finishes at 11, a response of 6 ms, past its 5 ms deadline. With reversed priorities (C highest), A waits behind C and B even with preemption: A finishes at 10.

The cost of preemption is one context switch per preemption and three stacks instead of one: at least 68 bytes of saved context for each suspended task, plus each task’s own deepest call chain and a margin.

MYTHS AND FACTS

Common misconceptions

The most important task gets the highest priority

Priorities control latency; assign them by deadline or period, then check importance through analysis.

A higher-priority task runs only when the lower one yields

That is cooperative scheduling; preemption switches at once, at any instruction.

Equal priorities preempt each other

They share the CPU only through time slicing at the tick, or by blocking and yielding.

Context switches are free

Each saves and restores about 17 words on a Cortex-M0+ plus the scheduler’s decision, and each task needs its own stack.

Check yourself

Answer in your head, then open the card.

A task of priority 3 is running in FreeRTOS when an interrupt handler gives a semaphore that a priority-5 task is waiting for. What happens when the handler returns, with preemption enabled?

The priority-5 task becomes ready. xSemaphoreGiveFromISR reports that a more urgent task was woken, so the handler requests a switch before it exits (portYIELD_FROM_ISR); PendSV runs after the handler and the priority-5 task runs. The priority-3 task stays ready and resumes later exactly where it was.

How many bytes of saved registers does each suspended task hold on its stack in the FreeRTOS Cortex-M0+ port, and where do they come from?

68 bytes: 32 bytes (8 words) stacked by the core on exception entry and 36 bytes (9 words: EXC_RETURN and R4–R11) stored by PendSV_Handler.

Why does the port give PendSV the lowest exception priority?

So that the context switch runs only after all other interrupt handlers have finished and switches tasks only on the way back to thread level. Other handlers can preempt it, except during the short section where it masks interrupts to pick the next task.

Two FreeRTOS tasks of equal priority are always ready. With time slicing on and a 1 ms tick, how do they share the CPU? And with time slicing off?

They alternate, switching at each tick. With time slicing off, the running one keeps the CPU until it blocks or yields, or until a higher-priority task preempts it (after which the other may be resumed instead).

Sources (5)
  1. FreeRTOS Kernel, portable/GCC/ARM_CM0/portasm.c (PendSV_Handler) — without the MPU: mrs r0, psp; subs r0, r0, #36 “Make space for LR and the remaining registers on the stack”; str r0, [r1] “Save the new top of stack in TCB”; stmia r0!, {r3-r7} (r3 = LR/EXC_RETURN) and r8–r11 via r4–r7; cpsid i; bl vTaskSwitchContext; cpsie i; restore, msr psp, r0; bx r3
  2. FreeRTOS Kernel, portable/GCC/ARM_CM0/port.c — vPortYield(): “Set a PendSV to request a context switch” (portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT); SysTick_Handler pends PendSV when xTaskIncrementTick() returns true; PendSV and SysTick priorities set to portMIN_INTERRUPT_PRIORITY (255) in SHPR3; pxPortInitialiseStack() “Simulate[s] the stack frame as it would be created by a context switch interrupt”: xPSR 0x01000000, PC = task function, LR = prvTaskExitError, R0 = pvParameters, EXC_RETURN 0xfffffffd
  3. FreeRTOS Kernel, tasks.c and include/task.h — taskSELECT_HIGHEST_PRIORITY_TASK finds “the highest priority queue that contains ready tasks” and uses listGET_OWNER_OF_NEXT_ENTRY “so the tasks of the same priority get an equal share of the processor time”; “Tasks of equal priority to the currently running task will share processing time (time slice) if preemption is on” and configUSE_TIME_SLICING is not 0 (it defaults to 1 in FreeRTOS.h); eTaskState: eRunning, eReady, eBlocked, eSuspended, eDeleted; tskIDLE_PRIORITY is 0
  4. FreeRTOS Kernel, include/semphr.h (xSemaphoreGiveFromISR) — sets “*pxHigherPriorityTaskWoken to pdTRUE if giving the semaphore caused a task to unblock, and the unblocked task has a priority higher than the currently running task … then a context switch should be requested before the interrupt is exited”
  5. Zephyr Project documentation, doc/kernel/services/scheduling/index.rst and threads/index.rst — “The kernel's scheduler selects the highest priority ready thread”; among equals, “the one that has been waiting longest”; reschedule points include k_sem_give, “return to thread context after processing an interrupt” and k_yield; thread priorities: a lower number is a higher priority, negative values are cooperative threads