1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
# gbos
A tiny **Unix-flavored microkernel for the Game Boy Color** (MBC5 cartridge).
Not Linux, not FUZIX — a from-scratch cooperative kernel that borrows the V7
*process model* (separate read-only text + per-process data, a syscall trap,
round-robin scheduling) and squeezes it into the GBC's banked, MMU-less memory.
This is a **scaffold**, but a *working* one: it assembles into a valid 32 KiB
ROM, brings up a process table, and round-robin context-switches between two
demo tasks that each `write()` a byte and `yield()`. Verified end-to-end in the
`~/dev/gbc` emulator (headless serial console) — it streams `ABABAB...`.
## Why this shape (the hardware reality)
- No MMU, no memory protection. "Unix" here = the API/process model, not isolation.
- CPU only ever sees **64 KiB**; RAM is banked.
- **Kernel code lives in ROM** (`$0000-$3FFF`, up to 8 MiB via MBC5) — free, read-only.
- **Program text lives in ROM banks** (`$4000-$7FFF`) — the V7 "pure text" idea.
- **Per-task RW state** (data/bss/heap/stack) lives in one **8 KiB cart-RAM bank**
(`$A000-$BFFF`, MBC5, up to 128 KiB / 16 banks).
- The context switch swaps banks, so it **must** run from **HRAM** (never banked)
and never touch a swappable stack mid-swap.
## Memory map
```
$0000-$3FFF ROM0 kernel core (rst/IRQ vectors, init, sched, syscalls) - fixed
$4000-$7FFF ROMX current task TEXT (read-only program code)
$8000-$9FFF VRAM graphics (unused so far)
$A000-$BFFF SRAM current task DATA/BSS/HEAP/STACK (MBC5 RAM bank)
$C000-$CFFF WRAM0 kernel globals + kernel stack (never swapped)
$D000-$DFFF WRAMX per-process u-area (SVBK bank) (reserved, not used yet)
$FF80-$FFEE HRAM context-switch trampoline (111 bytes)
$FFEF-$FFF2 HRAM bank shadows + switch target
```
## Process control block (`include/gbos.inc`)
`STATE, PID, SP, ROMB(16b text bank), RAMB(data bank), WRAMB(u-area), PARENT, EXIT`
— 10 bytes. `MAX_PROCS = 8`.
## Syscall ABI
User loads `C` = syscall number, args in `DE`/`B`, then `rst $30`. Return in `A`.
| # | name | status |
|---|---------|--------|
| 0 | exit | ✅ marks zombie, reschedules |
| 3 | write | ✅ `DE`=buf `B`=len → serial console |
| 8 | getpid | ✅ |
| 11| yield | ✅ cooperative switch |
| 1,2,4,5,6,7,9,10 | fork/read/open/close/exec/wait/kill/brk | ⛔ `ENOSYS` |
## The context switch (`src/hram.asm`)
The crux. `hSwitchTo(DE=&incomingPCB)`:
1. push full register context onto the **outgoing** task's stack
2. save `SP` into the outgoing PCB (WRAM0, always mapped)
3. program incoming banks: **RAM bank → SVBK → ROM text bank**
4. load `SP` from the incoming PCB (its banks are now mapped)
5. pop context, `ret` into the incoming task
Runs from HRAM so changing `SVBK`/RAM-bank never pulls the rug out from `PC` or
the stack. New tasks are bootstrapped with a fake frame (`ProcSetupStack`) so the
first switch-in `ret`s straight to their entry point.
## Build
```sh
make # -> gbos.gb (RGBDS: rgbasm/rgblink/rgbfix)
make clean
```
Console output goes to the **serial port** (`tty.asm`). Easiest way to watch it
is the project emulator's headless serial console:
```sh
~/dev/gbc/build/gbc gbos.gb --headless --uncapped # prints ABABAB...
```
A real gbos would render to VRAM; serial is the zero-VRAM debug channel.
### Bring-up bugs already found & fixed (kept as cautionary tales)
- **Stack vs. BSS clear:** the kernel stack (`SP=$D000`) grows down into
`$CxFF`, so zeroing all of WRAM0 wiped the live return address. `ClearKernelRAM`
now clears only `$C000-$CBFF`, leaving the stack region alone.
- **Syscall ABI vs. dispatch:** `SyscallTrap` originally used `DE` to index the
jump table, clobbering the `write()` buffer pointer passed in `DE`. Dispatch
now indexes via `A`/`HL` only, preserving `DE`/`B` for the handler.
## Roadmap
- [ ] `fork`: copy parent's 8 KiB RAM bank → free bank, dup u-area, child returns 0
- [ ] `exec`: point `PROC_ROMB` at a flashed program, reset RAM-bank layout
- [ ] Preemptive scheduling: real context save in `TimerISR` → `hSwitchTo`
- [ ] Use the `$D000-$DFFF` u-area (SVBK) for per-process kernel state / kstack
- [ ] `brk`/heap allocator inside the task bank (heap up, stack down, collision = ENOMEM)
- [ ] A filesystem in remaining cart SRAM/flash (minix/v7-ish), paged 8 KiB at a time
- [ ] Swap whole tasks to cart flash when RAM banks are exhausted (UZI-style)
- [ ] VRAM console + keyboard/joypad `read()`
```
|