diff options
Diffstat (limited to 'src')
| -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 |
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 |
