⚙️ MicrOS Internals: Yielding with SysTick and PendSV

In the last article, we looked at how PendSV swaps thread contexts using prebuilt stack frames. Now it’s time to give our kernel a proper yield mechanism — so threads can give up the CPU voluntarily, or automatically via a timer tick.


The Yield API

MicrOS provides task_yield() as the official way to request a context switch.
It simply sets the PendSV pending bit in the System Control Block:

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

#define SCB_ICSR   (*(volatile uint32_t *)0xE000ED04)
#define PENDSVSET  (1UL << 28)

void task_yield(void) {
    SCB_ICSR = PENDSVSET;   // trigger PendSV
}

This is the entire API: calling it doesn’t immediately switch, but ensures PendSV will run as soon as possible.


Yielding from SysTick

To demonstrate preemption, let’s connect SysTick with task_yield().
Instead of switching every single tick, we’ll yield every 10 ticks:

#include "sched.h"
#include "cmsis_device.h"   // CMSIS header for SysTick

static volatile int tick_count = 0;

void SysTick_Handler(void) {
    tick_count++;
    if (tick_count >= 10) {
        tick_count = 0;
        task_yield();       // request context switch
    }
}

void system_init(void) {
    SysTick->LOAD  = (SystemCoreClock / 1000) - 1; // 1 ms period
    SysTick->VAL   = 0;
    SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                     SysTick_CTRL_TICKINT_Msk   |
                     SysTick_CTRL_ENABLE_Msk;
}

This way, tasks get time slices of 10 ms each before the scheduler rotates them.


Priority Setup

PendSV must be lowest priority to prevent it from preempting ISRs. Configure it once during boot:

#define SCB_SHPR3   (*(volatile uint32_t *)0xE000ED20)

void pend_sv_init(void) {
    SCB_SHPR3 |= (0xFFU << 16);  // PendSV priority = lowest
}

A Test Example

Two simple tasks alternating every 10 ms slice:

#include "thread.h"
#include "sched.h"

void taskA(void *arg) {
    while (1) {
        // pretend work
        for (volatile int i = 0; i < 100000; i++);
    }
}

void taskB(void *arg) {
    while (1) {
        // pretend work
        for (volatile int i = 0; i < 100000; i++);
    }
}

int main(void) {
    pend_sv_init();
    system_init();

    micros_thread_create(0, taskA, 0);
    micros_thread_create(1, taskB, 0);

    // start first thread
    micros_current = 0;
    __set_PSP((uint32_t)micros_threads[0].sp);
    task_yield();   // manually kick off PendSV

    while (1) {}
}

In QEMU, you can watch registers or add printf() in each task to see them swap every ~10 ms.


Why This Works

  1. SysTick fires every millisecond.
  2. A counter increments inside SysTick_Handler.
  3. Every 10th tick, it calls task_yield().
  4. task_yield() sets the PendSV pending bit.
  5. When interrupts unwind, PendSV runs, saves the old thread’s context, and restores the next one.
  6. Control resumes in the other task.

Takeaway

With less than 50 lines of code, we established preemptive multitasking driven by task_yield() and SysTick. Tasks don’t need to know about the scheduler — they automatically rotate based on time slices.

This is the building block for:

  • round-robin scheduling,
  • priority-based preemption, and eventually
  • sleep/wake mechanisms.
Grzegorz Grzęda
Grzegorz Grzęda

At G2Labs, Grzegorz Grzęda develops IoT and embedded platforms while building MicrOS in public and sharing hands-on tutorials.

Articles: 10

Newsletter Updates

Enter your email address below and subscribe to our newsletter