# Chapter 4: The Preemptive Scheduler
:::{admonition} What this chapter answers: why do other apps keep running while one does heavy work?
:class: tip
On a PC, you can leave a heavy computation running and the other apps keep
working. The OS forcibly switches the running program on every timer
interrupt. In this chapter you build that same mechanism yourself.
The difference from a PC is that the UNO R4 has only one CPU core. A PC has
several, so even if one heavy job uses up a core, the others are free. With one
core, if nothing switches, everything really does stop. You can watch it stop
in {ref}`ch04-break` at the end of this chapter.
:::
## Sample code
- Directory: `docs/os-on-arduino/code/04_scheduler`
```bash
uv run pio run -d 04_scheduler -t upload
uv run pio device monitor -b 115200
```
## Source code
The sketch for this chapter. Click a file name to expand. The code is mirrored automatically from the manuscript repository, so what you see here is always current.
:::{dropdown} `src/main.cpp` — 135 lines
:icon: code
[Open on GitHub](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/04_scheduler/src/main.cpp)
```{literalinclude} ../../../../code/04_scheduler/src/main.cpp
:language: cpp
:linenos:
```
:::
:::{dropdown} `include/os_kernel.h` — 79 lines
:icon: code
[Open on GitHub](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/04_scheduler/include/os_kernel.h)
```{literalinclude} ../../../../code/04_scheduler/include/os_kernel.h
:language: cpp
:linenos:
```
:::
:::{dropdown} `src/os_kernel.cpp` — 287 lines
:icon: code
[Open on GitHub](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/04_scheduler/src/os_kernel.cpp)
```{literalinclude} ../../../../code/04_scheduler/src/os_kernel.cpp
:language: cpp
:linenos:
```
:::
## On the board
```{figure} ../_static/ch04_preemption.gif
:name: fig-ch04-preemption
:width: 100%
`Count` keeps appearing exactly once a second while `Heavy` is interrupted in between. It is printed by the `Serial` task (ID 3); `Current task` is that task's own ID, so it is always 3. Recorded on a real Arduino UNO R4 WiFi.
```
```{raw} html
The L LED (LED1, every 0.1 s) and the left half of the matrix (LED2, every 0.5 s) keep blinking even while the Heavy task runs its long computation. The right half toggles each time Heavy finishes one round. (Filmed with a webcam at 30 fps.)
```
## In the simulator
Without the board, the same firmware running in the {doc}`simulator <../getting-started/simulator>` looks like this.
```{figure} ../_static/sim_ch04_blink.gif
:name: fig-ch04-sim
:width: 80%
L (LED1) toggles every 100 ms and the left half of the matrix (LED2) every 500 ms; the right half flips each time the busy `Heavy` task finishes a round. While `Heavy` keeps the CPU busy, L and the left half keep their rhythm. (Recorded in the simulator.)
```
(ch04-break)=
## Break it
What happens if you change just one place in working code?
**Predict the result before you change it**, then open the answer.
Build the modified code and
{ref}`drop the resulting firmware.elf into the browser version `
to try it without a board. No flashing needed. To undo the change, run
`git checkout -- 04_scheduler`.
```bash
uv run pio run -d 04_scheduler
# drop 04_scheduler/.pio/build/uno_r4_wifi/firmware.elf into the browser version
```
:::{note}
The outputs in the answers were captured by actually running the modified code
in QEMU, the same emulator the browser version uses. They have not been checked
on the real board yet; where it stops and the numbers in a dump may differ there.
:::
### 1. Make the time slice one second
Change one line in `include/os_kernel.h`. Tasks now switch every second
instead of every 10 ms.
```diff
-#define TIME_SLICE_MS 10
+#define TIME_SLICE_MS 1000
```
**Predict:** what happens to the L LED blinking and the `Count` line printed every second?
:::{dropdown} Answer
After `Starting preemptive scheduler...`, **nothing more is printed.**
The L LED does not blink either.
```text
Tasks created:
ID 0: idle
ID 1: LED1
ID 2: LED2
ID 3: Serial
ID 4: Heavy
Starting preemptive scheduler...
```
The SysTick counter is 24 bits wide, so it can count at most 16,777,216 clocks
at a time. One second at 48 MHz is 48,000,000 clocks, which does not fit.
When `SysTick_Config()` is given a value that does not fit, it returns 1
without starting the timer
([`SysTick_Config()` in CMSIS `core_cm4.h`](https://github.com/ARM-software/CMSIS_5/blob/5.9.0/CMSIS/Core/Include/core_cm4.h#L2022-L2037);
the limit is [`SysTick_LOAD_RELOAD_Msk`](https://github.com/ARM-software/CMSIS_5/blob/5.9.0/CMSIS/Core/Include/core_cm4.h#L787) = `0xFFFFFF`).
`os_start()` does not check that return value, so no interrupt ever arrives,
and the idle task, chosen first, sleeps in `__WFI()` forever.
In QEMU, 349 ms still runs and 350 ms (16,800,000 clocks) stops.
At 100 ms it keeps running, but the `Count` timestamps move in 100 ms steps, like `[3100ms]`.
**To fix it:** if `SysTick_Config()` returns anything other than 0, print an error and stop.
:::
### 2. Take task switching out of SysTick
In `SysTick_Handler()` in `src/os_kernel.cpp`, comment out the call to `os_schedule()`.
Time still advances, but the timer interrupt no longer switches tasks.
```diff
g_system_ticks += TIME_SLICE_MS;
- os_schedule();
+ // os_schedule();
```
**Predict:** a task that calls `os_sleep()` still switches there. Which task keeps running?
:::{dropdown} Answer
Again, **nothing is printed** after `Starting preemptive scheduler...`.
The first task to run is the idle task, ID 0. Idle never calls `os_sleep()`;
it is a loop that only waits for interrupts in `__WFI()`, so it never gives up
the CPU on its own. Once `os_sleep()` is the only way to switch, idle keeps the
CPU and no other task ever runs.
The cooperative switching in Chapter 3 ({doc}`ch03`) relied on each task
calling `os_yield()` itself. A single task that does not keep that promise
stops everything, and that is the problem preemption solves.
:::
### 3. Stop saving registers R4–R11
In `PendSV_Handler()`, replace the instructions that push and pop R4–R11
with ones that only move the stack pointer. The stack layout stays the same;
only the contents are no longer saved or restored.
```diff
- " STMDB R0!, {R4-R11} \n"
+ " SUB R0, R0, #32 \n"
...
- " LDMIA R0!, {R4-R11} \n"
+ " ADD R0, R0, #32 \n"
```
**Predict:** the CPU pushes R0–R3, PC and a few others automatically when it
enters the exception. What happens if the remaining eight are not saved?
:::{dropdown} Answer
It prints the first `Count` line and then **stops with a fault.**
The dump below comes from the fault handler bundled with the Arduino core (CmBacktrace).
```text
Starting preemptive scheduler...
[10ms] Count: 0 | Current task: 3
Firmware name: PROJECT_NAME, compiled on: Sep 28 2026
Fault on interrupt or bare metal(no OS) environment
(stack dump omitted)
=================== Registers information ====================
R0 : 000000f0 R1 : e000ed00 R2 : e000ed00 R3 : 10000800
R12: 00000001 LR : 00006981 PC : 00004860 PSR: 41000000
==============================================================
Usage fault is caused by attempts to switch to an invalid state (e.g., ARM)
```
R4–R11 are the registers a function promises to leave unchanged across a call.
The compiler keeps loop values and addresses in them while calling other
functions. If they are not saved, the task switched to picks up the previous
task's values as they are. Here a corrupted value was used as a jump target,
and the CPU stopped trying to switch to an invalid state (the last line).
Where it stops depends on which value gets corrupted and how.
:::
## Pitfalls
(Collected from reader questions — ask via
[GitHub Issues](https://github.com/iory/build-your-own-arduino-rtos/issues).)