# 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).)