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
- SysTick fires every millisecond.
- A counter increments inside
SysTick_Handler. - Every 10th tick, it calls
task_yield(). task_yield()sets the PendSV pending bit.- When interrupts unwind, PendSV runs, saves the old thread’s context, and restores the next one.
- 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.