aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-15 23:44:28 +0200
committeruser <user@clank>2026-07-15 23:44:28 +0200
commitde725da74db5cba0f9bfd0755269e1683423d87b (patch)
treedc39eca9fbd290393ed51de31eee5e4bd6af8375 /src
downloadgbos-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 'src')
-rw-r--r--src/boot.asm90
-rw-r--r--src/hram.asm142
-rw-r--r--src/kdata.asm22
-rw-r--r--src/proc.asm159
-rw-r--r--src/sched.asm75
-rw-r--r--src/syscall.asm117
-rw-r--r--src/tasks.asm33
-rw-r--r--src/tty.asm15
8 files changed, 653 insertions, 0 deletions
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