diff options
| author | user <user@clank> | 2026-07-15 23:44:28 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-15 23:44:28 +0200 |
| commit | de725da74db5cba0f9bfd0755269e1683423d87b (patch) | |
| tree | dc39eca9fbd290393ed51de31eee5e4bd6af8375 | |
| download | gbos-de725da74db5cba0f9bfd0755269e1683423d87b.tar.gz gbos-de725da74db5cba0f9bfd0755269e1683423d87b.tar.xz gbos-de725da74db5cba0f9bfd0755269e1683423d87b.zip | |
gbos scaffold: working cooperative microkernel (verified ABAB in emulator)
- fix ClearKernelRAM clobbering the kernel stack (only clear $C000-$CBFF)
- fix SyscallTrap clobbering DE (buffer arg) during table dispatch
- README: run via ~/dev/gbc --headless; note bring-up bugs
Diffstat (limited to '')
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Makefile | 34 | ||||
| -rw-r--r-- | README.md | 102 | ||||
| -rw-r--r-- | include/gbos.inc | 86 | ||||
| -rw-r--r-- | src/boot.asm | 90 | ||||
| -rw-r--r-- | src/hram.asm | 142 | ||||
| -rw-r--r-- | src/kdata.asm | 22 | ||||
| -rw-r--r-- | src/proc.asm | 159 | ||||
| -rw-r--r-- | src/sched.asm | 75 | ||||
| -rw-r--r-- | src/syscall.asm | 117 | ||||
| -rw-r--r-- | src/tasks.asm | 33 | ||||
| -rw-r--r-- | src/tty.asm | 15 |
12 files changed, 878 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d20801 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +*.gb +*.sav diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..23551ad --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +# gbos - Game Boy Color Unix-flavored microkernel (scaffold) +# Toolchain: RGBDS (rgbasm/rgblink/rgbfix) + +ROM := gbos.gb +BUILD := build +SRCS := $(wildcard src/*.asm) +OBJS := $(patsubst src/%.asm,$(BUILD)/%.o,$(SRCS)) + +ASFLAGS := -I include -Wall +# MBC5 + RAM + battery; -r 3 = 32 KiB cart RAM (bump for more task banks) +FIXFLAGS := -v -m MBC5+RAM+BATTERY -r 3 -p 0xFF -t GBOS + +.PHONY: all clean run + +all: $(ROM) + +$(BUILD)/%.o: src/%.asm | $(BUILD) + rgbasm $(ASFLAGS) -o $@ $< + +$(ROM): $(OBJS) + rgblink -m $(BUILD)/gbos.map -n $(BUILD)/gbos.sym -o $@ $(OBJS) + rgbfix $(FIXFLAGS) $@ + +$(BUILD): + mkdir -p $(BUILD) + +clean: + rm -rf $(BUILD) $(ROM) + +# convenience: run in an emulator if one is on PATH +run: $(ROM) + @which sameboy >/dev/null 2>&1 && sameboy $(ROM) || \ + which bgb >/dev/null 2>&1 && bgb $(ROM) || \ + echo "no emulator found (sameboy/bgb)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd6c5cf --- /dev/null +++ b/README.md @@ -0,0 +1,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()` +``` diff --git a/include/gbos.inc b/include/gbos.inc new file mode 100644 index 0000000..c22344a --- /dev/null +++ b/include/gbos.inc @@ -0,0 +1,86 @@ +; ============================================================================= +; gbos.inc - core constants, memory map, PCB layout, syscall numbers +; Target: Game Boy Color, MBC5 + RAM + BATTERY +; ============================================================================= + IF !DEF(GBOS_INC) +DEF GBOS_INC EQU 1 + +; ----------------------------------------------------------------------------- +; Hardware registers we touch (subset; full defs would come from hardware.inc) +; ----------------------------------------------------------------------------- +DEF rIF EQU $FF0F ; interrupt flags +DEF rIE EQU $FFFF ; interrupt enable +DEF rSVBK EQU $FF70 ; CGB WRAM bank select ($D000-$DFFF): 1..7 +DEF rKEY1 EQU $FF4D ; CGB speed switch +DEF rSB EQU $FF01 ; serial data +DEF rSC EQU $FF02 ; serial control +DEF rLCDC EQU $FF40 +DEF rSTAT EQU $FF41 +DEF rLY EQU $FF44 + +; ----------------------------------------------------------------------------- +; MBC5 mapper register addresses (writes to ROM space hit the mapper) +; ----------------------------------------------------------------------------- +DEF MBC5_RAM_ENABLE EQU $0000 ; write $0A to enable cart RAM +DEF MBC5_ROMB_LO EQU $2000 ; ROM bank bits 0..7 +DEF MBC5_ROMB_HI EQU $3000 ; ROM bank bit 8 +DEF MBC5_RAMB EQU $4000 ; RAM bank bits 0..3 ($A000-$BFFF) + +; ----------------------------------------------------------------------------- +; Memory map (the gbos plan) +; $0000-$3FFF ROM0 kernel core (fixed, always mapped) +; $4000-$7FFF ROMX current task TEXT (read-only program code) +; $8000-$9FFF VRAM graphics +; $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) +; $FF80-$FFFE HRAM bank-switch trampoline + context switch +; ----------------------------------------------------------------------------- +DEF TASK_RAM_BASE EQU $A000 +DEF TASK_RAM_TOP EQU $C000 ; one past end of the 8 KiB window +DEF TASK_STACK_TOP EQU $BFFF ; task stacks grow down from here + +DEF KSTACK_TOP EQU $D000 ; kernel stack top (grows down into WRAM0) + +; ----------------------------------------------------------------------------- +; Process table +; ----------------------------------------------------------------------------- +DEF MAX_PROCS EQU 8 + +; process states +DEF PS_FREE EQU 0 +DEF PS_READY EQU 1 +DEF PS_RUN EQU 2 +DEF PS_BLOCKED EQU 3 +DEF PS_ZOMBIE EQU 4 + +; PCB (process control block) field offsets +RSRESET +DEF PROC_STATE RB 1 ; PS_* +DEF PROC_PID RB 1 +DEF PROC_SP RW 1 ; saved stack pointer (into task's RAM bank) +DEF PROC_ROMB RW 1 ; text ROM bank (16-bit; MBC5 up to 511) +DEF PROC_RAMB RB 1 ; data/stack cart-RAM bank +DEF PROC_WRAMB RB 1 ; u-area SVBK bank (1..7) +DEF PROC_PARENT RB 1 +DEF PROC_EXIT RB 1 ; exit code (valid when PS_ZOMBIE) +DEF PROC_SIZE RB 0 + +; ----------------------------------------------------------------------------- +; Syscall numbers (passed in C; args in DE/HL/B per call, ret in A) +; ----------------------------------------------------------------------------- +DEF SYS_EXIT EQU 0 +DEF SYS_FORK EQU 1 +DEF SYS_READ EQU 2 +DEF SYS_WRITE EQU 3 +DEF SYS_OPEN EQU 4 +DEF SYS_CLOSE EQU 5 +DEF SYS_EXEC EQU 6 +DEF SYS_WAIT EQU 7 +DEF SYS_GETPID EQU 8 +DEF SYS_KILL EQU 9 +DEF SYS_BRK EQU 10 +DEF SYS_YIELD EQU 11 +DEF SYS_MAX EQU 12 + + ENDC diff --git a/src/boot.asm b/src/boot.asm new file mode 100644 index 0000000..513c392 --- /dev/null +++ b/src/boot.asm @@ -0,0 +1,90 @@ +; ============================================================================= +; boot.asm - cartridge header, reset vectors, kernel entry & init +; ============================================================================= +INCLUDE "include/gbos.inc" + +; ----------------------------------------------------------------------------- +; RST vectors. We reserve rst $30 as the syscall trap. +; ----------------------------------------------------------------------------- +SECTION "rst00", ROM0[$00] + ret +SECTION "rst30_syscall", ROM0[$30] + jp SyscallTrap ; user code does: ld c, SYS_x ; rst $30 + +; ----------------------------------------------------------------------------- +; Interrupt vectors +; ----------------------------------------------------------------------------- +SECTION "vblank", ROM0[$40] + reti +SECTION "stat", ROM0[$48] + reti +SECTION "timer", ROM0[$50] + jp TimerISR ; drives preemptive scheduling +SECTION "serial", ROM0[$58] + reti +SECTION "joypad", ROM0[$60] + reti + +; ----------------------------------------------------------------------------- +; Entry point. rgbfix writes the Nintendo logo + header over $104-$14F. +; ----------------------------------------------------------------------------- +SECTION "entry", ROM0[$100] + nop + jp KernelInit + ds $150 - @ ; reserve $104-$14F for logo + header (rgbfix fills) + +; ----------------------------------------------------------------------------- +; Kernel init +; ----------------------------------------------------------------------------- +SECTION "kinit", ROM0 + +KernelInit: + di + ld sp, KSTACK_TOP + + ; enable cart RAM (MBC5) + ld a, $0A + ld [MBC5_RAM_ENABLE], a + + call ClearKernelRAM + call CopyHramCode ; move context-switch trampoline into HRAM + call ProcInit ; wipe process table + + ; --- spawn the two demo tasks (text-in-ROM model) --- + ; task A: entry TaskA, RAM bank 0, u-area WRAM bank 1 + ld hl, TaskA + ld b, 0 ; RAM bank + ld c, 1 ; WRAM u-area bank + call ProcCreate + + ld hl, TaskB + ld b, 1 + ld c, 2 + call ProcCreate + + call SchedInit ; pick first task, set wCurProc + + ; interrupts: enable timer for preemption + xor a + ld [rIF], a + ; ld a, IEF_TIMER ; (timer bit) - leave masked for cooperative demo + ; ld [rIE], a + + ; hand control to the first task via a context switch-in + jp SchedRunFirst + +; ----------------------------------------------------------------------------- +ClearKernelRAM: + ; zero the kernel globals region only ($C000-$CBFF). We must NOT touch + ; $CC00-$CFFF: the kernel stack (SP=$D000) grows down into it, and our + ; own return address lives there while this routine runs. + ld hl, $C000 + ld bc, $0C00 +.loop + xor a + ld [hl+], a + dec bc + ld a, b + or c + jr nz, .loop + ret diff --git a/src/hram.asm b/src/hram.asm new file mode 100644 index 0000000..7c1910c --- /dev/null +++ b/src/hram.asm @@ -0,0 +1,142 @@ +; ============================================================================= +; hram.asm - the trampoline. +; +; The context switch writes SVBK ($D000-$DFFF) and the MBC5 RAM bank +; ($A000-$BFFF). Because a task's stack lives in the RAM window, the code +; performing the swap MUST NOT execute from any bank it is about to change and +; MUST NOT touch the stack across the swap. HRAM is never banked, so the +; switch lives here. We assemble it with HRAM addresses (LOAD) but store the +; bytes in ROM0 and copy them up at boot. +; +; Convention: to compute &(PCB + OFF) we do +; ld a,e : add OFF : ld l,a : ld a,d : adc 0 : ld h,a ; HL = DE + OFF +; ============================================================================= +INCLUDE "include/gbos.inc" + +SECTION "hram_vars", HRAM +hCurBankRAM: DS 1 ; shadow of active cart-RAM bank +hCurBankWRAM: DS 1 ; shadow of active SVBK bank +hSwitchTgt: DS 2 ; &incoming PCB (set before the pushes clobber DE) + +; ----------------------------------------------------------------------------- +; Source image (lives in ROM0); CopyHramCode blits it to HRAM at boot. +; ----------------------------------------------------------------------------- +SECTION "hram_src", ROM0 + +HramSrc: +LOAD "hram_code", HRAM +; --------------------------------------------------------------------------- +; hSwitchTo: switch from the current task (wCurProc) to the PCB in DE. +; in: DE = &incoming PCB +; Runs from HRAM. Saves/restores full context. Clobbers everything. +; --------------------------------------------------------------------------- +hSwitchTo:: + ; stash target while DE is still valid + ld a, e + ld [hSwitchTgt], a + ld a, d + ld [hSwitchTgt+1], a + + ; --- save outgoing context on its (currently-mapped) stack --- + push af + push bc + push de + push hl + + ; SP -> outgoing PCB.PROC_SP (outgoing = wCurProc, in WRAM0, always mapped) + ld hl, sp+0 ; HL = SP + ld b, h + ld c, l ; BC = SP value + ld a, [wCurProc] + add PROC_SP + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a ; HL = &outgoing.PROC_SP + ld a, c + ld [hl+], a + ld a, b + ld [hl], a + + ; --- incoming becomes current --- + ld a, [hSwitchTgt] + ld e, a + ld [wCurProc], a + ld a, [hSwitchTgt+1] + ld d, a ; DE = &incoming PCB + ld [wCurProc+1], a + + ; --- program banks for incoming task (order: RAM, WRAM, ROM) --- + ; cart-RAM bank + ld a, e + add PROC_RAMB + ld l, a + ld a, d + adc 0 + ld h, a + ld a, [hl] + ld [hCurBankRAM], a + ld [MBC5_RAMB], a + ; u-area WRAM bank via SVBK + ld a, e + add PROC_WRAMB + ld l, a + ld a, d + adc 0 + ld h, a + ld a, [hl] + ld [hCurBankWRAM], a + ld [rSVBK], a + ; text ROM bank (16-bit) + ld a, e + add PROC_ROMB + ld l, a + ld a, d + adc 0 + ld h, a + ld a, [hl+] + ld [MBC5_ROMB_LO], a + ld a, [hl] + ld [MBC5_ROMB_HI], a + + ; --- restore incoming SP (its banks are now mapped) --- + ld a, e + add PROC_SP + ld l, a + ld a, d + adc 0 + ld h, a + ld a, [hl+] + ld c, a + ld a, [hl] + ld h, a + ld l, c + ld sp, hl + + ; --- pop incoming context and resume it --- + pop hl + pop de + pop bc + pop af + ret +ENDL +HramSrcEnd: + +; ----------------------------------------------------------------------------- +; CopyHramCode - blit the LOAD'd image up to HRAM. Runs once at boot. +; ----------------------------------------------------------------------------- +SECTION "hram_copy", ROM0 + +CopyHramCode:: + ld hl, HramSrc + ld de, hSwitchTo + ld bc, HramSrcEnd - HramSrc +.loop + ld a, [hl+] + ld [de], a + inc de + dec bc + ld a, b + or c + jr nz, .loop + ret diff --git a/src/kdata.asm b/src/kdata.asm new file mode 100644 index 0000000..efc780a --- /dev/null +++ b/src/kdata.asm @@ -0,0 +1,22 @@ +; ============================================================================= +; kdata.asm - kernel globals in fixed WRAM0 (always mapped, never swapped) +; ============================================================================= +INCLUDE "include/gbos.inc" + +SECTION "kernel_bss", WRAM0[$C000] + +wProcTable:: DS MAX_PROCS * PROC_SIZE ; the process table +wCurProc:: DS 2 ; pointer to running PCB +wCurPid:: DS 1 ; running pid (reserved) +wNextPid:: DS 1 ; pid allocator +wSchedNext:: DS 1 ; round-robin cursor (slot index) +wKernelPCB:: DS PROC_SIZE ; outgoing-context holder for 1st switch + ; (kernel/idle context; never resumed) + +; scratch used by ProcCreate +wTmpEntry:: DS 2 +wTmpRamB:: DS 1 +wTmpWramB:: DS 1 +wTmpSlot:: DS 1 +wTmpPid:: DS 1 +wTmpSP:: DS 2 diff --git a/src/proc.asm b/src/proc.asm new file mode 100644 index 0000000..762b30e --- /dev/null +++ b/src/proc.asm @@ -0,0 +1,159 @@ +; ============================================================================= +; proc.asm - process table management, task creation, fork/exec (stubs) +; ============================================================================= +INCLUDE "include/gbos.inc" + +SECTION "proc", ROM0 + +; ----------------------------------------------------------------------------- +; ProcInit - table already zeroed by ClearKernelRAM (PS_FREE=0). Seed pids. +; ----------------------------------------------------------------------------- +ProcInit:: + ld a, 1 + ld [wNextPid], a + ret + +; ----------------------------------------------------------------------------- +; PcbPtr - slot -> PCB pointer. in: A=slot out: DE=&PCB. preserves BC. +; ----------------------------------------------------------------------------- +PcbPtr:: + push bc + ld hl, wProcTable + ld bc, PROC_SIZE + and a + jr z, .done +.mul + add hl, bc + dec a + jr nz, .mul +.done + ld d, h + ld e, l + pop bc + ret + +; ----------------------------------------------------------------------------- +; ProcCreate - allocate a PCB and build an initial (runnable) task. +; in: HL = task entry, B = cart-RAM bank, C = SVBK u-area bank +; out: A = pid, or 0 if the table is full +; ----------------------------------------------------------------------------- +ProcCreate:: + ; stash params + ld a, l + ld [wTmpEntry], a + ld a, h + ld [wTmpEntry+1], a + ld a, b + ld [wTmpRamB], a + ld a, c + ld [wTmpWramB], a + + ; find a free slot + ld c, 0 +.find + ld a, c + call PcbPtr ; DE = &PCB[c] + ld a, [de] + cp PS_FREE + jr z, .found + inc c + ld a, c + cp MAX_PROCS + jr c, .find + xor a ; table full + ret + +.found + ld a, c + ld [wTmpSlot], a + + ; fill fields; HL walks the PCB + ld h, d + ld l, e + ld a, PS_READY + ld [hl+], a ; PROC_STATE + ld a, [wNextPid] + ld [hl+], a ; PROC_PID + ld [wTmpPid], a + inc a + ld [wNextPid], a + inc hl ; skip PROC_SP (filled below) + inc hl + ld a, 1 + ld [hl+], a ; PROC_ROMB lo (placeholder text bank) + xor a + ld [hl+], a ; PROC_ROMB hi + ld a, [wTmpRamB] + ld [hl+], a ; PROC_RAMB + ld a, [wTmpWramB] + ld [hl+], a ; PROC_WRAMB + xor a + ld [hl+], a ; PROC_PARENT + ld [hl], a ; PROC_EXIT + + ; build the initial stack frame in the task's RAM bank + ld a, [wTmpEntry] + ld l, a + ld a, [wTmpEntry+1] + ld h, a ; HL = entry + ld a, [wTmpRamB] + ld b, a ; B = RAM bank + call ProcSetupStack ; out: HL = initial SP + ld a, l + ld [wTmpSP], a + ld a, h + ld [wTmpSP+1], a + + ; store SP into PCB.PROC_SP + ld a, [wTmpSlot] + call PcbPtr ; DE = &PCB + ld a, e + add PROC_SP + ld l, a + ld a, d + adc 0 + ld h, a ; HL = &PROC_SP + ld a, [wTmpSP] + ld [hl+], a + ld a, [wTmpSP+1] + ld [hl], a + + ld a, [wTmpPid] + ret + +; ----------------------------------------------------------------------------- +; ProcSetupStack - lay a fresh switch-in frame at the top of a task RAM bank. +; in: HL = entry, B = cart-RAM bank +; out: HL = initial SP (points at the saved-HL slot) +; The frame matches hSwitchTo's restore sequence: +; [SP+0..1]=hl [+2..3]=de [+4..5]=bc [+6]=f [+7]=a [+8..9]=entry (ret target) +; ----------------------------------------------------------------------------- +ProcSetupStack:: + ld a, b + ld [MBC5_RAMB], a ; map task's RAM bank into $A000-$BFFF + push hl ; save entry + ld hl, TASK_STACK_TOP - 9 ; frame base = $BFF6 + xor a + ld [hl+], a ; hl_lo + ld [hl+], a ; hl_hi + ld [hl+], a ; de_lo + ld [hl+], a ; de_hi + ld [hl+], a ; bc_lo + ld [hl+], a ; bc_hi + ld [hl+], a ; f + ld [hl+], a ; a + pop de ; DE = entry + ld a, e + ld [hl+], a ; entry lo (ret target) + ld a, d + ld [hl], a ; entry hi + ld hl, TASK_STACK_TOP - 9 ; initial SP + ret + +; ----------------------------------------------------------------------------- +; sys_fork / sys_exec - TODO. Plan: +; fork: alloc slot, copy parent's 8 KiB RAM bank -> child bank, dup u-area, +; patch child's saved-A to 0 (child return), parent gets child pid. +; exec: point PROC_ROMB at a flashed program image, reset RAM-bank layout +; (copy .data from ROM, zero .bss, fresh heap/stack), longjmp in. +; ----------------------------------------------------------------------------- diff --git a/src/sched.asm b/src/sched.asm new file mode 100644 index 0000000..c871f7a --- /dev/null +++ b/src/sched.asm @@ -0,0 +1,75 @@ +; ============================================================================= +; sched.asm - round-robin scheduler + yield + timer stub +; ============================================================================= +INCLUDE "include/gbos.inc" + +SECTION "sched", ROM0 + +; ----------------------------------------------------------------------------- +; SchedInit - prime the round-robin cursor so the first pick lands on slot 0. +; ----------------------------------------------------------------------------- +SchedInit:: + ld a, MAX_PROCS - 1 + ld [wSchedNext], a + ret + +; ----------------------------------------------------------------------------- +; SchedRunFirst - hand control to the first runnable task. Never returns; the +; kernel-init context is saved into wKernelPCB and simply abandoned. +; ----------------------------------------------------------------------------- +SchedRunFirst:: + ld hl, wKernelPCB + ld a, l + ld [wCurProc], a + ld a, h + ld [wCurProc+1], a + call FindNextReady ; DE = &first ready PCB + jp hSwitchTo + +; ----------------------------------------------------------------------------- +; SchedYield - cooperative switch to the next runnable task. Returns to the +; caller when this task is eventually rescheduled. +; ----------------------------------------------------------------------------- +SchedYield:: + call FindNextReady ; DE = &next PCB + call hSwitchTo + ret + +; ----------------------------------------------------------------------------- +; FindNextReady - round-robin from wSchedNext. out: DE = &PCB (updates cursor) +; Scaffold: assumes at least one PS_READY task exists. +; ----------------------------------------------------------------------------- +FindNextReady:: + ld a, [wSchedNext] + ld b, MAX_PROCS +.scan + inc a + cp MAX_PROCS + jr c, .nowrap + xor a +.nowrap + push af ; save candidate slot + call PcbPtr ; DE = &PCB[slot] (preserves BC) + ld a, [de] ; state + cp PS_READY + jr z, .found + pop af ; restore candidate slot + dec b + jr nz, .scan + ; nothing ready: fall back to slot 0 (idle behavior) + xor a + call PcbPtr + ret +.found + pop af ; A = chosen slot + ld [wSchedNext], a + call PcbPtr ; DE = &PCB + ret + +; ----------------------------------------------------------------------------- +; TimerISR - preemption hook (TODO). A real preemptive switch must save the +; interrupted context and call hSwitchTo from here. For now the demo is +; cooperative (tasks call SYS_YIELD), so we just acknowledge and return. +; ----------------------------------------------------------------------------- +TimerISR:: + reti diff --git a/src/syscall.asm b/src/syscall.asm new file mode 100644 index 0000000..71e1f2b --- /dev/null +++ b/src/syscall.asm @@ -0,0 +1,117 @@ +; ============================================================================= +; syscall.asm - the trap entry, dispatch table, and handlers. +; +; ABI: user loads C = syscall number, args in DE/B/HL, then `rst $30`. +; return value in A. ($30 vector jp's here - see boot.asm) +; ============================================================================= +INCLUDE "include/gbos.inc" + +SECTION "syscall", ROM0 + +; ----------------------------------------------------------------------------- +SyscallTrap:: + ld a, c + cp SYS_MAX + jr nc, .bad + ; index the table WITHOUT touching DE/B (those are syscall args). + ld hl, SyscallTable + ld a, c + add a ; A = C*2 (word entries; C < SYS_MAX) + add l + ld l, a + jr nc, .nocarry + inc h +.nocarry + ld a, [hl+] + ld h, [hl] + ld l, a + jp hl ; tail-call; handler's `ret` returns to user +.bad + ld a, $FF ; ENOSYS-ish + ret + +; ----------------------------------------------------------------------------- +; Dispatch table (indexed by syscall number) +; ----------------------------------------------------------------------------- +SyscallTable: + dw sys_exit ; 0 + dw sys_nosys ; 1 FORK (TODO) + dw sys_nosys ; 2 READ (TODO) + dw sys_write ; 3 WRITE + dw sys_nosys ; 4 OPEN (TODO) + dw sys_nosys ; 5 CLOSE (TODO) + dw sys_nosys ; 6 EXEC (TODO) + dw sys_nosys ; 7 WAIT (TODO) + dw sys_getpid ; 8 GETPID + dw sys_nosys ; 9 KILL (TODO) + dw sys_nosys ; 10 BRK (TODO) + dw sys_yield ; 11 YIELD + +; ----------------------------------------------------------------------------- +sys_nosys: + ld a, $FF + ret + +; ----------------------------------------------------------------------------- +; sys_write(fd=B, buf=DE, len=B?) -- scaffold: fd ignored, DE=buf, B=len. +; Writes to the serial console. Returns A = bytes written. +; ----------------------------------------------------------------------------- +sys_write: + ld a, b + or a + ret z ; len 0 + ld c, b ; C = remaining +.loop + ld a, [de] + call PutChar + inc de + dec c + jr nz, .loop + ld a, b ; return len + ret + +; ----------------------------------------------------------------------------- +; sys_getpid() -> A = current pid +; ----------------------------------------------------------------------------- +sys_getpid: + ld a, [wCurProc] + add PROC_PID + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ret + +; ----------------------------------------------------------------------------- +; sys_yield() +; ----------------------------------------------------------------------------- +sys_yield: + call SchedYield + ret + +; ----------------------------------------------------------------------------- +; sys_exit(code=B) -- mark zombie and schedule away forever. +; ----------------------------------------------------------------------------- +sys_exit: + ; mark current PCB as zombie, store exit code + ld a, [wCurProc] + ld l, a + ld a, [wCurProc+1] + ld h, a ; HL = &PCB + ld a, PS_ZOMBIE + ld [hl], a ; PROC_STATE + ; PROC_EXIT is at offset PROC_EXIT + ld a, [wCurProc] + add PROC_EXIT + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, b + ld [hl], a ; exit code + ; never return to this task + call SchedYield + ; if we somehow come back (no other ready task), spin +.halt + jr .halt diff --git a/src/tasks.asm b/src/tasks.asm new file mode 100644 index 0000000..1f84142 --- /dev/null +++ b/src/tasks.asm @@ -0,0 +1,33 @@ +; ============================================================================= +; tasks.asm - two demo userland tasks (text-in-ROM model). +; Each loops: write its letter to the console, then yield. This exercises the +; whole pipeline: syscall trap, console, and the HRAM context switch swapping +; RAM banks / SVBK between tasks. +; ============================================================================= +INCLUDE "include/gbos.inc" + +SECTION "tasks", ROM0 + +TaskA:: +.loop + ld de, .msg + ld b, 1 ; len + ld c, SYS_WRITE + rst $30 + ld c, SYS_YIELD + rst $30 + jr .loop +.msg + db "A" + +TaskB:: +.loop + ld de, .msg + ld b, 1 + ld c, SYS_WRITE + rst $30 + ld c, SYS_YIELD + rst $30 + jr .loop +.msg + db "B" diff --git a/src/tty.asm b/src/tty.asm new file mode 100644 index 0000000..d2c1234 --- /dev/null +++ b/src/tty.asm @@ -0,0 +1,15 @@ +; ============================================================================= +; tty.asm - console output over the serial port (emulator debug channel). +; A real gbos would render to VRAM; serial is a zero-VRAM stub that shows up +; in emulator serial logs (bgb/SameBoy) and on link hardware. +; ============================================================================= +INCLUDE "include/gbos.inc" + +SECTION "tty", ROM0 + +; PutChar - A = character. Push to serial data and kick a transfer. +PutChar:: + ld [rSB], a + ld a, $81 ; start transfer, internal clock + ld [rSC], a + ret |
