aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-18 10:10:04 +0200
committeruser <user@clank>2026-07-18 10:10:17 +0200
commit0cc9b0a8bdf774ee376937d5a6767ccdced51245 (patch)
tree221326e7a86fa0121ae7eea1763a9f3ffa8100e6
parentkernel: dmesg-style boot spew before init spawns (diff)
downloadgbos-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--Makefile2
-rw-r--r--include/gbos.inc8
-rw-r--r--src/blk.asm5
-rw-r--r--src/boot.asm14
-rw-r--r--src/hram.asm5
-rw-r--r--src/kdata.asm1
-rw-r--r--src/programs.asm7
-rw-r--r--src/sched.asm25
-rw-r--r--src/syscall.asm19
-rwxr-xr-xusr/build.sh7
-rw-r--r--usr/gbos.h1
-rw-r--r--usr/libc.s8
-rw-r--r--usr/uptime.c40
13 files changed, 132 insertions, 10 deletions
diff --git a/Makefile b/Makefile
index 9a7875b..fbdf10a 100644
--- a/Makefile
+++ b/Makefile
@@ -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 \
diff --git a/usr/gbos.h b/usr/gbos.h
index 2350665..5fd897a 100644
--- a/usr/gbos.h
+++ b/usr/gbos.h
@@ -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) */
diff --git a/usr/libc.s b/usr/libc.s
index 5b176d9..2366cd8 100644
--- a/usr/libc.s
+++ b/usr/libc.s
@@ -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();
+}