; ============================================================================= ; 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. ; ; PRESERVES ALL REGISTERS, on both paths. The switch path always did, because ; hSwitchTo saves and restores the full context - but the "nobody else is ; runnable" path used to `ret c` straight out of FindNextReady, which leaves ; HL pointing into wProcTable (via PcbPtr). A caller holding a pointer in HL ; across the call then walked the process table instead of its own variable. ; ; That is what warm-rebooted a link-less gbos every ~11s: with no DHCP server, ; the boot-time `dhcp` spun in net_op_recv's timeout loop and, with the shell ; blocked on tty input, every yield took the no-switch path - so `inc hl / ; inc [hl]` incremented PCB bytes instead of wNetTO until a saved return ; address rolled to $0100 and the cart restarted. It only ever bit with the ; network absent, which is why it hid for so long. ; ----------------------------------------------------------------------------- SchedYield:: push af push bc push de push hl call FindNextReady ; DE = &next PCB, CF if none runnable jr c, .none ; nobody else ready: keep running current call hSwitchTo ; (returns here when we are rescheduled; the ; pushes above sit on our own stack) .none pop hl pop de pop bc pop af ret ; ----------------------------------------------------------------------------- ; FindNextReady - round-robin from wSchedNext. ; out: DE = &PCB and CF clear on success; CF set if nothing is PS_READY. ; ----------------------------------------------------------------------------- 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 scf ; nothing runnable ret .found pop af ; A = chosen slot ld [wSchedNext], a call PcbPtr ; DE = &PCB and a ; clear carry ret ; ----------------------------------------------------------------------------- ; TimerISR - the system tick. TAC runs the timer at 16384 Hz with TMA=0 - ; but the CPU is in DOUBLE-SPEED mode, so the input clock is really 32768 Hz ; and this fires at 128 Hz. A toggle halves that back down: wTicks stays a ; monotonic 32-bit 1/64-second counter (SYS_UPTIME reads it; wraps ~2.1 yr). ; Scheduling stays cooperative - this ISR is deliberately transparent (all ; registers preserved, fixed-WRAM only), so it can interrupt anything, ; including kernel code mid-syscall. Preemption would hook in here later. ; ----------------------------------------------------------------------------- TimerISR:: push af push hl ld hl, wTickTog ld a, [hl] xor 1 ld [hl], a jr nz, .done ; first IRQ of the 128 Hz pair: count nothing ld hl, wTicks inc [hl] jr nz, .done ; no carry out of byte 0 inc hl inc [hl] jr nz, .done inc hl inc [hl] jr nz, .done inc hl inc [hl] .done pop hl pop af reti