A preemptive, real-time microkernel operating system written from scratch in C and ARMv8 assembly, running bare metal (no standard library, no host OS) on a Raspberry Pi 4B. The OS drives a physical model-train layout: it tracks multiple trains in real time, plans routes across the track graph, reserves track segments to prevent collisions, resolves deadlocks automatically, and even hosts a two-train competitive game.
- Microkernel architecture — tasks, priority scheduling, and synchronous message passing (Send / Receive / Reply) with a name server, all built from scratch.
- Preemptive multitasking — hardware-timer-driven context switching and interrupt-driven UART I/O.
- Real-time train tracking — sensor attribution, exponential-moving-average velocity estimation, and a per-train velocity-based braking model that calibrates itself online.
- Route planning — Dijkstra shortest-path search over the track graph, including support for mid-route reversals.
- Track reservation & deadlock handling — look-ahead segment reservations with automatic conflict detection, yielding, and deadlock resolution.
- Interactive control & competitive game mode — multi-train random-roaming demo and a two-round scoring game with automatic route planning, switching, and traffic management.
- Languages: C (freestanding) and ARMv8-A assembly
- Target: Raspberry Pi 4B — AArch64, bare metal
- Toolchain:
aarch64-none-elfGCC cross-compiler - No standard library and no RTOS: boot, cache/MMU setup, drivers, scheduler, and IPC are all implemented from scratch.
You need an aarch64-none-elf bare-metal GCC cross toolchain. Point the build at your
toolchain install with XDIR:
make XDIR=/path/to/aarch64-none-elf TRACK=CAn image file is produced at build/kernel.img.
You can also pass other compile-time options:
make XDIR=/path/to/aarch64-none-elf TRACK=<C|D> CACHE=<n|i|d|b> OPT=<0|1> VERBOSE=<0|1> IDLE_WINDOW_TICKS=<ticks>| Option | Default | Valid values | Description |
|---|---|---|---|
TRACK |
C |
C, D |
Track layout to compile for. |
CACHE |
b |
n, i, d, b |
Cache mode: none / I-cache / D-cache / both. |
OPT |
1 |
0, 1 |
Enable (1) or disable (0) -O3 optimization. |
VERBOSE |
0 |
0, 1 |
Enable extra verbose debug compile flag (-DVERBOSE). |
IDLE_WINDOW_TICKS |
50 |
positive integer | Rolling window size for idle % display. |
- Copy the image at
build/kernel.imgonto the Raspberry Pi's boot media (or upload it through your Pi's boot / netboot setup). - Power-cycle the Raspberry Pi to boot the image.
- Enter commands at the prompt:
| Command | Description |
|---|---|
tr <train> <speed> |
Set train speed (from 0 to 14) |
| `sw <S | C>` |
rv <train> |
Reverse train (stop -> reverse -> restore speed) |
q/Q |
quit/reboot |
init |
init all switch to straight |
| `li <1 | 0>` |
goto <train> <node id> [offset+-mm] |
Make the train pick up a constant speed (fixed at speed step 8) and stops at a specific track node |
findpos <train1> [train2] [train3] [train4] |
Find positions for one or more trains |
demo <speed> <train1> [train2] [train3] [train4] [seed] |
Start demo: trains roam randomly at the given speed level |
demo stop |
Stop demo gracefully (no new missions assigned) |
demo stop force |
Stop demo immediately and reset runtime state back to startup defaults |
demo seed <u32> |
Set the RNG seed used for random route selection |
demo tune trip <mm> |
Set minimum trip distance (mm) for random route selection |
game |
Enter interactive game setup; each setup input is a bare train number and triggers one findpos before the next prompt |
<sensor> |
During game round, enter a bare sensor name (e.g. A3) to choose the human player's target |
game stop |
Stop the current game |
game status |
Show current game state and scores |
- Make sure all three trains are stopped (or in unknown state) and positioned on the track.
- Type
gameat the prompt. The system enters setup mode and the prompt changes togame>. - Enter the human player's train number as a bare number only, for example
55. - The system immediately runs one
findposbootstrap for that train. While it is locating, the prompt changes towait>. - After that train has confirmed position and stopped, the system prompts for the AI train number.
- Enter the AI train number as a bare number only, for example
13. - The system again runs
findposfor that train, waits for it to finish, then prompts for the neutral train number. - Enter the neutral train number as a bare number only, for example
14. - After the neutral train has also finished its setup
findpos, the game enters Round 1 and waits for a sensor input. - During each round, the human player types a bare sensor name (e.g.
A3) to choose a destination. - To stop a running game gracefully, type
game stop. To abort immediately, typegame stop force.
Note: while in game setup mode, train inputs are bare numbers only and other commands are blocked. Type
qto quit without launching.
- The match lasts for
2rounds. - There are
3trains on the track during the game:2scoring trains: one controlled by the human player and one controlled by the AI.1neutral train: it creates traffic pressure but never scores.
- In every round, all three trains receive exactly one destination sensor and depart in the same round.
- The player only chooses a destination sensor. The system handles route planning, reservation, switches, launch timing, deadlock handling, and yielding automatically.
- A train is considered to have reached its round target when the system determines that it has arrived and stopped there. The final target sensor does not need to physically trigger.
- Priority alternates in a fixed order across the two rounds.
- Before Round 1, the system performs one random draw to decide which scoring player has priority first.
- Round 2 gives priority to the other scoring player.
- Each scoring player therefore receives priority in exactly
1round.
- The human player secretly chooses one destination sensor.
- The AI secretly chooses one destination sensor.
- The neutral train's destination sensor is drawn publicly for that round.
- All three destinations are revealed at the same time.
- The system plans and reserves routes in this order:
- the scoring player with round priority
- the other scoring player
- the neutral train
- All three trains execute their movements within the same round.
- The round ends when:
- both scoring trains have completed their round targets, and
- the neutral train has either completed its own round target or has been redirected to a safe standby location and marked as resolved.
There is no round time limit.
- The neutral train receives one official destination sensor per round.
- Its round destination is chosen by a public draw without replacement, so the same neutral destination is not repeated within the same 2-round match.
- The neutral destination pool contains only physical sensors.
- At draw time, the neutral destination must not be the current sensor occupied by any train.
- The system should prefer reachable neutral destinations whose shortest path from the neutral train is at least
1400 mm. - If no such candidate exists in the current round, the draw may fall back to any reachable physical sensor.
- The neutral train starts the next round from wherever it stopped at the end of the previous round.
- The neutral train never scores and never consumes any sensor reward that could otherwise be earned by a scoring player.
- Only the two scoring trains can earn points.
- Points are awarded only when a scoring train physically triggers a new physical sensor.
- Logical arrival at a round target does not award points by itself.
- If a sensor was used as a final destination but did not physically trigger on arrival, it remains available for future scoring if it is physically triggered later.
- Each physical sensor can award points at most twice during the whole match:
- The first scoring player to physically trigger that sensor earns
1.5points. - The other scoring player earns
1.0point the first time they physically trigger that same sensor later.
- The first scoring player to physically trigger that sensor earns
- A player earns
0additional points for re-triggering a sensor they have already scored from. - After both scoring players have already earned from the same physical sensor, that sensor awards no further points.
- Scoring is tracked by physical sensor, not by directional node.
- The neutral train does not affect first-touch or second-touch scoring rewards.
- Any newly triggered scoring sensor during a yielding movement still counts normally.
- Between the two scoring players, the original rule still applies:
- if the later-planned scoring player still cannot complete their target after the earlier-planned scoring player has already arrived and stopped, and the earlier player's stopped train is the blocking cause, then the earlier player must yield.
- The yielding location is computed automatically by the system.
- A scoring player who yields after already completing their own target does not need to return to that target afterward.
- If a scoring player is blocked by the neutral train's stopped position, the neutral train must yield first.
- If the neutral train is the only unfinished train at the end of the round and its remaining block comes only from already-completed scoring trains, the scoring trains are not forced to yield for it.
- In that case, the system redirects the neutral train to a safe standby location and considers the neutral train resolved for the round.
- After 2 rounds, the scoring player with the higher total score wins.
- If both scoring players have the same total score, the match ends in a draw.
Co-developed with Alex Au.