I have written a simple round-robin scheduler (available on GitHub) for the ARM Cortex-M0 (ARMv6-M) CPU to understand the context switch mechanism. This article is a short summary of its principle.

The same approach is used by many RTOSes and is well described in The Definitive Guide to ARM® Cortex®-M0 and Cortex-M0+ Processors by Joseph Yiu.

Background

The processor has two separate stack pointer which can be accessed through banked SP register: Main Stack Pointer (MSP) which is the default one after startup and Process Stack Pointer (PSP) which can be optionally used.

The processor supports multiple modes:

The default mode is the Privileged Thread Mode . It is possible to switch stack to PSP in this mode

. It is possible to switch stack to PSP in this mode The Privileged mode can be switched to Unprivileged Thread Mode which has certain register and memory access restrictions

which has certain register and memory access restrictions Exceptions and interrupts are handled in the Handler Mode which uses the MSP stack.

In this application, tasks run in the Unprivileged Thread Mode with PSP and kernel runs in the Handler Mode with MSP. This allows stack separation between the kernel and tasks (which simplifies the context switch procedure) and prevents tasks from accessing important registers and affecting the kernel.

Context Switch

The context switch happens in an interrupt handler. Once an interrupt occurs, the NVIC hardware automatically stacks an exception frame (registers xPSR , PC , LR , r12 and r3-r0 ) onto the Process Stack (PSP) and branches to the interrupt handler routine in Handler Mode (which uses the Main Stack).

The context switch routine has to:

Manually stack remaining registers r4-r11 on the Process Stack

on the Process Stack Save current task’s PSP to memory

Load next task’s stack pointer and assign it to PSP

Manually unstack registers r4-r11

Call bx 0xfffffffD which makes the processor switch to Unprivileged Handler Mode, unstack next task’s exception frame and continue on its PC .

Exception frame saved by the NVIC hardware onto stack:

Registers saved by the software:

Performing the Context Switch

The context switch could be performed by the SysTick_Handler with a SysTick timer configured to fire interrupts periodically:

This approach would however not work with other interrupts (peripheral interrupt for example). The SysTick_Handler would stack registers affected by the peripheral IRQ handler and unstack task’s registers, resulting in undefined behavior of both tasks and peripheral interrupt handler:

The solution is simple - the SysTick_Handler with the highest priority only selects the next task to be run and triggers PendSV interrupt. The PendSV_Handler with the lowest priority performs the actual context switch once all interrupt requests with higher priority have been handled:

The PendSV_Handler is written in pure assembly. The code relies on a fact that the task’s stack pointer is the first element of the os_task_t structure - the structure’s address corresponds to the address of its first element according to C language specification.

Task Initialization

Each task is defined by its handler function and stack. The initialization phase of task’s stack must ensure that the first 64 bytes (16 words) form a valid exception frame. It is neccessary to store at least the default value of three registers:

xPSR to 0x01000000 (the defaul value)

to (the defaul value) PC to the handler function

to the handler function LR to a function to be called when the handler function finishes (otherwise the CPU would jump to invalid location, causing HardFault or undefined behavior)

The actual function os_task_init stores values for registers r0-r12 as well for debugging purposes.

Startup

The startup phase has to configure SysTick and PendSV interrupt levels, initialize the SysTick timer to fire interrupts periodically and start the first task.

As the microcontroller starts in Privileged Thread Mode with Main Stack it is neccessary to switch to Unprivileged mode with Process Stack. This is done by writing to the CONTROL register followed by ISB instruction.

The os_start function is written without inline assembly thanks to functions and intrinsics provided by the CMSIS library.

Example

An example runs three tasks (which are switched every second). All tasks blink the onboard LED with different frequency.

The example can be run on STM32F030R8 Nucleo board and requires STM32Cube software pack which has to be present in the lib directory.

The provided Makefile requires GCC compiler and OpenOCD. See README for more information about compilation and flashing.

Compatibility

The SysTick timer and Privileged mode are optional features of the ARMv6-M architecture. They are however supported by vast majority of microcontrollers.

The code relies on standard CMSIS library by ARM which is usually distributed by microcontroller vendors. The library provides functions and intrinsics for accessing features of the ARM Cortex-M core.