diff options
| author | user <user@clank> | 2026-07-18 10:10:04 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-18 10:10:17 +0200 |
| commit | 0cc9b0a8bdf774ee376937d5a6767ccdced51245 (patch) | |
| tree | 221326e7a86fa0121ae7eea1763a9f3ffa8100e6 | |
| parent | kernel: dmesg-style boot spew before init spawns (diff) | |
| download | gbos-0cc9b0a8bdf774ee376937d5a6767ccdced51245.tar.gz gbos-0cc9b0a8bdf774ee376937d5a6767ccdced51245.tar.xz gbos-0cc9b0a8bdf774ee376937d5a6767ccdced51245.zip | |
kernel: a real timer source (64 Hz tick IRQ) + uptime(1)
The kernel had no clock: TimerISR was a reti stub, IEF_TIMER masked, and
IME was never enabled - the vectors were decorative. sys_sleep just polls
DIV deltas per-process; nothing counted globally.
Now: TAC runs the hardware timer at 16384 Hz with TMA=0, so TIMA overflows
at exactly 64 Hz; TimerISR increments a monotonic 32-bit wTicks (wraps
after ~2.1 years). Scheduling stays cooperative - the ISR is transparent.
Enabling IME in a kernel written for zero interrupts needs care wherever
SP points into memory whose bank is being switched (an IRQ pushes onto SP):
- read_block/write_block map the disk bank over the $A000 window that
holds the caller's stack -> di/ei around the transfer (~2ms, well under
the 15.6ms tick period, so no tick is ever lost)
- hSwitchTo switches SVBK + cart-RAM banks under the outgoing stack ->
di on entry, ei once the incoming stack is mapped
- fork already runs on KSTACK_TOP2 (fixed WRAM) - safe as-is
- term_putc's SVBK switch only remaps $Dxxx, stacks live in $Axxx/$Cxxx
SYS_UPTIME (36) copies the counter (4B LE, di/ei so the read can't tear)
to a user buffer; libc gticks(); usr/uptime.c formats 'up [Nd] H:MM:SS'.
uptime avoids SDCC long div/shift entirely: sm83.lib modules link into
their own areas that land in the $A000 RAM window (latent build.sh trap,
documented there) - bytewise >>6 plus bounded subtraction loops instead.
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | include/gbos.inc | 8 | ||||
| -rw-r--r-- | src/blk.asm | 5 | ||||
| -rw-r--r-- | src/boot.asm | 14 | ||||
| -rw-r--r-- | src/hram.asm | 5 | ||||
| -rw-r--r-- | src/kdata.asm | 1 | ||||
| -rw-r--r-- | src/programs.asm | 7 | ||||
| -rw-r--r-- | src/sched.asm | 25 | ||||
| -rw-r--r-- | src/syscall.asm | 19 | ||||
| -rwxr-xr-x | usr/build.sh | 7 | ||||
| -rw-r--r-- | usr/gbos.h | 1 | ||||
| -rw-r--r-- | usr/libc.s | 8 | ||||
| -rw-r--r-- | usr/uptime.c | 40 |
13 files changed, 132 insertions, 10 deletions
@@ -21,7 +21,7 @@ $(BUILD)/%.o: src/%.asm | $(BUILD) # $(BUILD)/usr and INCBIN'd by programs.asm PROGS := chello echo true false uname pid cat wc head args \ ls save rm sh ps kill spin blk mkdir count ptest necho \ - wget chat netd ping nslookup dhcp irc + wget chat netd ping nslookup dhcp irc uptime CBLOBS := $(patsubst %,$(BUILD)/usr/%.bin,$(PROGS)) $(BUILD)/programs.o: $(CBLOBS) diff --git a/include/gbos.inc b/include/gbos.inc index 92d8732..246628c 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -10,6 +10,10 @@ DEF GBOS_INC EQU 1 ; ----------------------------------------------------------------------------- DEF rIF EQU $FF0F ; interrupt flags DEF rIE EQU $FFFF ; interrupt enable +DEF rTIMA EQU $FF05 ; timer counter +DEF rTMA EQU $FF06 ; timer modulo (reload) +DEF rTAC EQU $FF07 ; timer control (bit2 = enable, bits1-0 = clock) +DEF IEF_TIMER EQU %00000100 DEF rDIV EQU $FF04 ; divider register: free-running 8-bit @ 16384 Hz DEF rSVBK EQU $FF70 ; CGB WRAM bank select ($D000-$DFFF): 1..7 DEF wCurMask EQU $D589 ; term/osk: cursor underline bits OR'd into a tile's last row @@ -134,7 +138,8 @@ DEF SYS_POLLIN EQU 32 ; poll OSK for a typed char (-> A=char or 0) DEF SYS_NET EQU 33 ; socket layer (DE=&netreq -> A=result) DEF SYS_SLEEP EQU 34 ; cooperative delay (B = 1/64-second units) DEF SYS_POLLCON EQU 35 ; poll console ring (link-injected -> A=byte or 0) -DEF SYS_MAX EQU 36 +DEF SYS_UPTIME EQU 36 ; copy the 64 Hz tick counter (4B LE) to [DE] +DEF SYS_MAX EQU 37 ; ---- socket layer (SYS_NET) -------------------------------------------------- ; netreq struct (in caller RAM; DE points to it): @@ -259,5 +264,6 @@ DEF PROG_PING EQU 27 DEF PROG_NSLOOKUP EQU 28 DEF PROG_DHCP EQU 29 DEF PROG_IRC EQU 30 +DEF PROG_UPTIME EQU 31 ENDC diff --git a/src/blk.asm b/src/blk.asm index 2be961e..76561da 100644 --- a/src/blk.asm +++ b/src/blk.asm @@ -34,6 +34,8 @@ ENDM ; ----------------------------------------------------------------------------- read_block:: ld c, a + di ; the tick ISR pushes on SP, which points into + ; the $A000 window we are about to remap srl a srl a srl a @@ -54,6 +56,7 @@ read_block:: dec b jr nz, .cp restore_bank ; inline - no stack while the disk bank is mapped + ei ret ; ----------------------------------------------------------------------------- @@ -61,6 +64,7 @@ read_block:: ; ----------------------------------------------------------------------------- write_block:: ld c, a + di ; see read_block srl a srl a srl a @@ -81,6 +85,7 @@ write_block:: dec b jr nz, .cp restore_bank + ei ret ; ============================================================================= diff --git a/src/boot.asm b/src/boot.asm index c2d461a..4049f9d 100644 --- a/src/boot.asm +++ b/src/boot.asm @@ -81,11 +81,19 @@ KernelInit: call SchedInit ; pick first task, set wCurProc - ; interrupts: enable timer for preemption + ; system tick: hardware timer at 16384 Hz input, TMA=0 -> TIMA overflow + ; IRQ at 64 Hz. TimerISR just counts (wTicks); scheduling stays + ; cooperative. Only the timer bit is enabled in IE. + xor a + ld [rTIMA], a + ld [rTMA], a + ld a, %00000111 ; enable | 16384 Hz + ld [rTAC], a xor a ld [rIF], a - ; ld a, IEF_TIMER ; (timer bit) - leave masked for cooperative demo - ; ld [rIE], a + ld a, IEF_TIMER + ld [rIE], a + ei ; hand control to the first task via a context switch-in jp SchedRunFirst diff --git a/src/hram.asm b/src/hram.asm index 7c1910c..9e65982 100644 --- a/src/hram.asm +++ b/src/hram.asm @@ -31,6 +31,9 @@ LOAD "hram_code", HRAM ; Runs from HRAM. Saves/restores full context. Clobbers everything. ; --------------------------------------------------------------------------- hSwitchTo:: + di ; no tick ISR while SP and the banks under it + ; are out of sync (pushes would hit the wrong + ; bank); ei below once the incoming stack is up ; stash target while DE is still valid ld a, e ld [hSwitchTgt], a @@ -114,6 +117,8 @@ hSwitchTo:: ld sp, hl ; --- pop incoming context and resume it --- + ei ; SP + banks consistent again (takes effect + ; after the next instruction) pop hl pop de pop bc diff --git a/src/kdata.asm b/src/kdata.asm index 65a059a..b0cd71b 100644 --- a/src/kdata.asm +++ b/src/kdata.asm @@ -65,3 +65,4 @@ wPnStart:: DS 2 ; scratch for sys_progname wBootA:: DS 1 ; A at entry: console model from the boot ROM wBootB:: DS 1 ; B at entry: bit0 set = AGB (GBA in CGB mode) wBootFsFresh::DS 1 ; fs_init formatted a blank disk this boot +wTicks:: DS 4 ; monotonic 64 Hz system tick (TimerISR), LE diff --git a/src/programs.asm b/src/programs.asm index 6dbc584..7c17fb5 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -133,6 +133,9 @@ ProgDhcp: SECTION "prog_irc", ROMX[$4000], BANK[32] ProgIrc: INCBIN "build/usr/irc.bin" +SECTION "prog_uptime", ROMX[$4000], BANK[33] +ProgUptime: + INCBIN "build/usr/uptime.bin" ; ----------------------------------------------------------------------------- ; PROG_SH (bank 3) - the shell, now written in C (usr/sh.c): parses >/</| and @@ -237,6 +240,8 @@ ProgramTable:: dw ProgDhcp db LOW(BANK(ProgIrc)), HIGH(BANK(ProgIrc)) dw ProgIrc + db LOW(BANK(ProgUptime)), HIGH(BANK(ProgUptime)) + dw ProgUptime ProgramTableEnd:: ; ----------------------------------------------------------------------------- @@ -306,4 +311,6 @@ NameTable:: db PROG_DHCP db "irc", 0 db PROG_IRC + db "uptime", 0 + db PROG_UPTIME db 0 diff --git a/src/sched.asm b/src/sched.asm index a540c49..0369ff4 100644 --- a/src/sched.asm +++ b/src/sched.asm @@ -67,9 +67,28 @@ FindNextReady:: 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 - the system tick. TAC runs the timer at 16384 Hz with TMA=0, so +; TIMA overflows (and this fires) at 64 Hz: wTicks is a monotonic 32-bit +; 1/64-second counter (SYS_UPTIME reads it; wraps after ~2.1 years). +; 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, 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 diff --git a/src/syscall.asm b/src/syscall.asm index bd58cc2..e3f0b1b 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -70,6 +70,7 @@ SyscallTable: dw sys_net ; 33 NET dw sys_sleep ; 34 SLEEP dw sys_pollcon ; 35 POLLCON + dw sys_uptime ; 36 UPTIME ; ----------------------------------------------------------------------------- sys_nosys: @@ -257,6 +258,24 @@ sys_yield: ret ; ----------------------------------------------------------------------------- +; sys_uptime(buf=DE) - copy the monotonic 64 Hz tick counter (4 bytes, LE) +; into the caller's buffer. di/ei so the ISR can't split the 32-bit read. +; ----------------------------------------------------------------------------- +sys_uptime: + di + ld hl, wTicks +REPT 3 + ld a, [hl+] + ld [de], a + inc de +ENDR + ld a, [hl] + ld [de], a + ei + xor a + ret + +; ----------------------------------------------------------------------------- ; sys_sleep(B = 1/64-second units) - cooperative delay driven by the DIV timer. ; DIV (FF04) free-runs at 16384 Hz, so 256 ticks = 1/64 s. We accumulate DIV ; deltas across SchedYields (other procs run) until enough units have elapsed. diff --git a/usr/build.sh b/usr/build.sh index 93815d2..5d494cf 100755 --- a/usr/build.sh +++ b/usr/build.sh @@ -10,7 +10,12 @@ sdasgb -o "$B/crt0.rel" usr/crt0.s sdasgb -o "$B/libc.rel" usr/libc.s sdcc -msm83 -c -Iusr usr/libc.c -o "$B/libc_c.rel" sdcc -msm83 -c -Iusr "$SRC" -o "$B/prog.rel" -# link: crt0 first (=> _start at 0x4000), then libc, program, and sm83 lib +# link: crt0 first (=> _start at 0x4000), then libc, program, and sm83 lib. +# TRAP: only _CODE/_DATA have pinned bases. sm83.lib modules (long div/mod/ +# shift helpers) link into their own areas, which sdldgb places after _DATA - +# i.e. code in the $A000 RAM window ("buffer too small" from makebin, or +# garbage at runtime). Don't use long division/shifts in programs; see +# usr/uptime.c for the inlined-arithmetic workaround. sdldgb -n -m -w -j -i "$B/prog.ihx" \ -b _CODE=0x4000 -b _DATA=0xa000 \ -k "$LIBDIR" -l sm83 \ @@ -21,6 +21,7 @@ unsigned char pollcon(void); /* poll console ring (link-injected input, filled by net pumps), or 0 */ unsigned char sys_net(void *req); /* socket layer trap (see sock.h) */ void gsleep(unsigned char units); /* delay units*(1/64)s (raw trap) */ +void gticks(unsigned char buf[4]); /* 64 Hz uptime ticks, 32-bit LE */ void msleep(unsigned int ms); /* delay ~ms milliseconds */ /* helpers (c/libc.c) */ @@ -1,7 +1,7 @@ .module libc .globl _writes, _putc, _puts, _nl, _strlen, _readc, _sexit, _getpid, _getargs .globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe, _ssend, _srecv, _srecv_nb, _pollin, _pollcon - .globl _sys_net, _gsleep + .globl _sys_net, _gsleep, _gticks ;; SDCC sm83 sdcccall(1): 1st arg -> A (byte) or DE (pointer); ret A / BC. ;; rst $30 (the trap) clobbers BC/DE/HL; SDCC treats them caller-saved, so ;; wrappers need not preserve them - but we compute cleanly regardless. @@ -130,6 +130,12 @@ _gsleep: rst #0x30 ret + ;; void gticks(unsigned char buf[4] /*DE*/) - 64 Hz uptime ticks, LE +_gticks: + ld c, #36 ; SYS_UPTIME + rst #0x30 + ret + ;; unsigned char pollin(void) -> A (typed OSK char, or 0) _pollin: ld c, #32 ; SYS_POLLIN diff --git a/usr/uptime.c b/usr/uptime.c new file mode 100644 index 0000000..1062217 --- /dev/null +++ b/usr/uptime.c @@ -0,0 +1,40 @@ +#include "gbos.h" +/* uptime: time since boot, from the kernel's 64 Hz tick (SYS_UPTIME). + + No long division/shifts: SDCC pulls those from sm83.lib, whose modules + link into their own areas that land in the $A000 RAM window (see + usr/build.sh). Everything here is inlined 8/32-bit add/sub/compare: + ticks>>6 done bytewise, then day/hour/minute by bounded subtraction. */ + +static void two(unsigned int v) { /* zero-padded 2-digit field */ + if (v < 10) putc('0'); + putu(v); +} + +void main(void) { + unsigned char t[4]; + union { unsigned long l; unsigned char b[4]; } u; /* sm83 is LE */ + unsigned long sec; + unsigned int d, h, m; + + gticks(t); /* 32-bit LE tick count, 64 Hz */ + + /* seconds = ticks >> 6, composed a byte at a time (24 bits is 194 days) */ + u.b[0] = (unsigned char)((t[0] >> 6) | (t[1] << 2)); + u.b[1] = (unsigned char)((t[1] >> 6) | (t[2] << 2)); + u.b[2] = (unsigned char)((t[2] >> 6) | (t[3] << 2)); + u.b[3] = 0; + sec = u.l; + + d = 0; + while (sec >= 86400UL) { sec -= 86400UL; d++; } /* <= 194 rounds */ + h = 0; + while (sec >= 3600UL) { sec -= 3600UL; h++; } /* <= 23 rounds */ + m = 0; + while (sec >= 60UL) { sec -= 60UL; m++; } /* <= 59 rounds */ + + puts("up "); + if (d) { putu(d); puts("d "); } + putu(h); putc(':'); two(m); putc(':'); two((unsigned int)sec); + nl(); +} |
