aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 2d2946ca4aecdb745303d3a8dd195e88d9cdc3ba (plain) (blame)
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# 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 |
| 1 | fork    | ✅ copies the 8 KiB bank, child returns 0 |
| 3 | write   | ✅ `DE`=buf `B`=len → serial console |
| 8 | getpid  | ✅ |
| 11| yield   | ✅ cooperative switch |
| 2,4,5,6,7,9,10 | read/open/close/exec/wait/kill/brk | ⛔ `ENOSYS` |

### fork (`sys_fork`, src/proc.asm)

The interesting syscall. It enters on the parent's user stack (which lives in
the `$A000-$BFFF` cart-RAM window), so before it can swap that window it
**switches to a kernel stack in WRAM0**. Then it:

1. allocates a free PCB slot + a fresh cart-RAM bank (bump allocator),
2. copies the parent's whole 8 KiB bank → the child bank, 256 bytes at a time
   through a WRAM bounce buffer (only one cart-RAM bank is visible at once),
3. plants a **switch-in frame** in the child at `Uafter-8` (`Uafter` = parent
   SP at entry): the copied return address is already there, so it just zeroes
   the saved `hl/de/bc/af` slots — the child resumes at the post-`fork` PC with
   `A=0`,
4. fills the child PCB (shared ROM text bank, new RAM bank, parent as PPID),
5. restores the parent stack and returns the child pid.

Verified: `init` forks a child (P/C alternate on serial); nested forks yield
three independent processes with distinct pids (`123123...`).

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

- [x] `fork`: copy parent's 8 KiB RAM bank → free bank, 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()`
```