Add full compiler toolchain, libc, examples and reference docs
First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.
What's in here:
bin/sprinter-cc — driver script invoking SDCC + linker + mkexe
libc/ — Sprinter-specific libc layer over ESTEX/BIOS
(conio, gfx, io, mem, stdio + headers)
runtime/ — crt0 variants (default/small/banked/minimal)
+ heap + bank trampolines
toolchain/ — mkexe (SprintEXE packer, C + tests)
examples/ — 30 demo programs (gfx, file I/O, env, time, …)
lib/Makefile — builds the libc archive (sprinter.lib)
docs/ — converted Sprinter manuals + asm reference samples
third_party/ — solid-c reference compiler dump + sdcc setup script
release_docs/ — packaging / release notes
gitignore overhaul:
• Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
on macOS APFS). Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
• Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
INPUT fixtures, not build outputs.
• Collapse the duplicated SDCC/C/Sdcc sections into one section per
concern (build outputs / vendored / OS-junk).
• Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; bank.s — Sprinter banked-call trampolines for SDCC __banked functions.
|
||||
;;
|
||||
;; ABI (verified empirically with SDCC 4.5, see memory/sdcc_banking.md):
|
||||
;; On entry to ___sdcc_bcall_ehl:
|
||||
;; E = bank id (1..15), HL = target address (low 16 bits).
|
||||
;; Args already on stack, pushed by the caller right-to-left.
|
||||
;; Stack picture once the trampoline finishes set-up and jp(hl) fires:
|
||||
;; SP+0..1 = bjump return (callee's "ret address")
|
||||
;; SP+2 = saved physical page (1 byte)
|
||||
;; SP+3..4 = bcall caller return
|
||||
;; SP+5.. = args ← __banked callees read from here
|
||||
;;
|
||||
;; The 3-byte spacer between callee ret and args is the contract SDCC bakes
|
||||
;; into its codegen — touching it breaks all __banked calls.
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module bank
|
||||
|
||||
.globl _bank_pages
|
||||
.globl ___sdcc_bcall_ehl
|
||||
.globl ___sdcc_bjump_ehl
|
||||
|
||||
;; Choose between W3 (HUGE mode, default) and W1 (BIG mode, sprinter-cc
|
||||
;; prepends `BANK_W1 = 1`). Keep in sync with crt0_banked.s.
|
||||
.ifdef BANK_W1
|
||||
BANK_PORT = 0xA2
|
||||
.else
|
||||
BANK_PORT = 0xE2
|
||||
.endif
|
||||
|
||||
;; HOME area — always mapped, so the trampoline is reachable from any bank.
|
||||
.area _CODE
|
||||
|
||||
___sdcc_bcall_ehl::
|
||||
;; The 3-byte spacer between bcall-ret and args (1-byte saved page
|
||||
;; + 2-byte bjump return address) is baked into SDCC's __banked
|
||||
;; codegen — callees access their args at fixed offsets from SP.
|
||||
;; We MUST keep that contract intact.
|
||||
;;
|
||||
;; The old `pop af; out (n), a` restore sequence clobbered A,
|
||||
;; dropping uint8_t return values. We use the `OUT (C), r` form
|
||||
;; (port via C, byte via B) so A is preserved. BC is scratch in
|
||||
;; SDCC __sdcccall(1), so clobbering it here is harmless.
|
||||
in a, (#BANK_PORT) ; A = current bank-window page
|
||||
push af ; push A:F (2 bytes)
|
||||
inc sp ; keep only the A byte (1-byte spacer)
|
||||
call ___sdcc_bjump_ehl
|
||||
;; Callee returned. A may hold a uint8_t return value.
|
||||
dec sp ; widen back to 2 bytes
|
||||
pop bc ; B = saved page (C = whatever F was)
|
||||
ld c, #BANK_PORT ; C = bank-window select port
|
||||
out (c), b ; OUT (port), B — A and F untouched
|
||||
ret ; back to bcall caller
|
||||
|
||||
___sdcc_bjump_ehl::
|
||||
push hl ; preserve target address
|
||||
ld d, #0 ; D:E = 0:bank_id
|
||||
ld hl, #_bank_pages
|
||||
add hl, de ; HL = &_bank_pages[bank_id]
|
||||
ld a, (hl) ; A = physical page for this bank
|
||||
pop hl ; restore target address
|
||||
out (#BANK_PORT), a ; map bank into banking window
|
||||
jp (hl) ; tail-jump to callee
|
||||
|
||||
;; Per-bank physical page table. Index = bank id (1-based; [0] is unused).
|
||||
;; Filled at startup by crt0_banked.s.
|
||||
.area _DATA
|
||||
_bank_pages::
|
||||
.ds 16
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; crt0.s — Sprinter ESTEX C runtime startup, with argv parsing.
|
||||
;;
|
||||
;; What's special vs crt0_minimal.s:
|
||||
;; After gsinit and before calling main, we read the ESTEX command-line
|
||||
;; from the startup prefix (IX+0 = length, IX+1.. = ASCIIZ bytes) and
|
||||
;; tokenize it in place into a static argv[] table. argc → HL, argv → DE
|
||||
;; when we then `call _main`, which matches SDCC's __sdcccall(1) two-arg
|
||||
;; ABI. Programs declared as `int main(void)` simply ignore the input
|
||||
;; registers — no harm done — so this is the default crt0.
|
||||
;;
|
||||
;; Entry contract (per ESTEX EXEC):
|
||||
;; IX -> startup prefix:
|
||||
;; IX-3 = open file handle (only if EXE header.loader > 0)
|
||||
;; IX-2 = memory block id (released automatically on EXIT)
|
||||
;; IX-1 = current process level
|
||||
;; IX+0 = command-line length (1 byte)
|
||||
;; IX+1 = command-line bytes (ASCIIZ, up to 127 chars)
|
||||
;;
|
||||
;; Memory map: see compiler_approach memory file.
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module crt0
|
||||
.globl _main
|
||||
|
||||
;; Linker-emitted symbols (resolved at link time).
|
||||
.globl s__INITIALIZER
|
||||
.globl l__INITIALIZER
|
||||
.globl s__INITIALIZED
|
||||
.globl s__BSS
|
||||
.globl l__BSS
|
||||
|
||||
;; Tunables — match the constants in argv parsing below.
|
||||
ARGV_MAX_ARGS = 16 ; including argv[0]
|
||||
ARGV_BUF_BYTES = 128 ; cmdline buffer size (ESTEX max + 1)
|
||||
|
||||
;; ___sdcc_heap_end (upper bound of the malloc free-list) lives in a
|
||||
;; separate per-program file (runtime/heap_top.s) so it can be regenerated
|
||||
;; with a custom value without re-assembling crt0. The default is set in
|
||||
;; that file (0xBB00); sprinter-cc may emit a different value when
|
||||
;; `--stack-size N` is given.
|
||||
|
||||
;; =========================================================================
|
||||
;; AREA ORDERING — emitted up-front so the linker walks them in this order.
|
||||
;; =========================================================================
|
||||
.area _HOME
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
|
||||
.area _DATA
|
||||
.area _INITIALIZED
|
||||
.area _BSEG
|
||||
.area _BSS
|
||||
.area _HEAP
|
||||
|
||||
;; =========================================================================
|
||||
;; Entry point — first instruction in _CODE, hence at --code-loc (0x4100).
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
_start::
|
||||
ld sp, #0xBFFE
|
||||
ld (_estex_startup_ix), ix ; save IX prefix pointer
|
||||
|
||||
call gsinit
|
||||
|
||||
.ifdef DEBUG_RT
|
||||
;; tiny mode never self-allocates a W2 page (CODE+DATA both live in W2,
|
||||
;; which DSS itself maps for us). Publish 0 to the diagnostic flag —
|
||||
;; must run AFTER gsinit since BSS is zeroed there and we want a
|
||||
;; deterministic value visible to user code from main() onward.
|
||||
xor a
|
||||
ld (_w2_self_allocated), a
|
||||
.endif
|
||||
|
||||
;; Parse the ESTEX command-line into argv[]; populates _argc, _argv.
|
||||
call parse_argv
|
||||
|
||||
;; Replace argv[0] (empty string placeholder) with the basename of the
|
||||
;; running .EXE. Safe to skip if ESTEX APPINFO fails.
|
||||
call get_progname
|
||||
|
||||
;; Load argc/argv per SDCC __sdcccall(1): arg1 → HL, arg2 → DE.
|
||||
ld hl, (_argc)
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; SDCC's int return → DE. Low byte is the exit code.
|
||||
ld a, e
|
||||
ld b, a
|
||||
ld c, #0x41 ; ESTEX EXIT
|
||||
rst #0x10
|
||||
;; Should not return; halt loop just in case.
|
||||
1$: halt
|
||||
jr 1$
|
||||
|
||||
;; =========================================================================
|
||||
;; parse_argv — tokenize ESTEX cmdline into argv[].
|
||||
;;
|
||||
;; The prefix: (IX+0) = length; (IX+1...) = ASCIIZ bytes.
|
||||
;; We copy up to ARGV_BUF_BYTES-1 chars into our own buffer (so we can
|
||||
;; overwrite separators with NUL), strip leading whitespace (DSS quirk),
|
||||
;; then walk through tokens. argv[0] is set to an empty string because
|
||||
;; ESTEX doesn't pass the program name in the prefix.
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
parse_argv::
|
||||
;; Copy cmdline body into argv_buf, NUL-terminate.
|
||||
ld hl, (_estex_startup_ix)
|
||||
ld a, (hl) ; A = cmdline length
|
||||
cp #ARGV_BUF_BYTES
|
||||
jr c, len_ok
|
||||
ld a, #ARGV_BUF_BYTES-1
|
||||
len_ok:
|
||||
ld c, a
|
||||
ld b, #0 ; BC = number of bytes to copy
|
||||
inc hl ; HL = body start (= IX+1)
|
||||
ld de, #argv_buf
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, after_copy
|
||||
ldir ; DE = one past last copied byte
|
||||
after_copy:
|
||||
xor a
|
||||
ld (de), a ; NUL-terminate
|
||||
|
||||
;; Skip leading whitespace.
|
||||
ld hl, #argv_buf
|
||||
strip_ws:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, no_args
|
||||
cp #0x20 ; ' '
|
||||
jr Z, strip_more
|
||||
cp #0x09 ; tab
|
||||
jr nz, have_first_token
|
||||
strip_more:
|
||||
inc hl
|
||||
jr strip_ws
|
||||
|
||||
no_args:
|
||||
;; Only argv[0] = "" — no real arguments.
|
||||
ld hl, #empty_str
|
||||
ld (argv_array), hl
|
||||
ld hl, #0
|
||||
ld (argv_array+2), hl ; argv[1] = NULL
|
||||
ld hl, #1
|
||||
ld (_argc), hl
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
have_first_token:
|
||||
;; HL points at start of first real argument.
|
||||
;; argv[0] = empty string (placeholder for program name).
|
||||
ld de, #empty_str
|
||||
ld (argv_array), de
|
||||
|
||||
;; B = current argc (start at 1). Use B because HL/DE are busy.
|
||||
ld b, #1
|
||||
|
||||
token_loop:
|
||||
;; Bound check: argc < ARGV_MAX_ARGS
|
||||
ld a, b
|
||||
cp #ARGV_MAX_ARGS
|
||||
jr nc, tokens_done
|
||||
|
||||
;; argv[argc] = HL. Compute slot address = argv_array + argc*2.
|
||||
push hl
|
||||
ld a, b
|
||||
add a, a ; A = argc * 2
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ld hl, #argv_array
|
||||
add hl, de ; HL = &argv[argc]
|
||||
pop de ; DE = current token pointer
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
ex de, hl ; HL = token pointer again
|
||||
|
||||
inc b ; argc++
|
||||
|
||||
;; Advance past token (non-whitespace).
|
||||
walk_token:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, tokens_done
|
||||
cp #0x20
|
||||
jr Z, end_of_token
|
||||
cp #0x09
|
||||
jr Z, end_of_token
|
||||
inc hl
|
||||
jr walk_token
|
||||
|
||||
end_of_token:
|
||||
;; Replace separator with NUL.
|
||||
xor a
|
||||
ld (hl), a
|
||||
inc hl
|
||||
|
||||
;; Skip extra whitespace.
|
||||
skip_ws:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, tokens_done
|
||||
cp #0x20
|
||||
jr Z, skip_more
|
||||
cp #0x09
|
||||
jr Z, skip_more
|
||||
jr token_loop
|
||||
skip_more:
|
||||
inc hl
|
||||
jr skip_ws
|
||||
|
||||
tokens_done:
|
||||
;; argv[argc] = NULL terminator.
|
||||
ld a, b
|
||||
add a, a
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ld hl, #argv_array
|
||||
add hl, de
|
||||
ld (hl), #0
|
||||
inc hl
|
||||
ld (hl), #0
|
||||
;; Publish argc (16-bit, zero-extended from B).
|
||||
ld a, b
|
||||
ld (_argc), a
|
||||
xor a
|
||||
ld (_argc+1), a
|
||||
;; Publish argv = &argv_array[0]
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
;; The empty string used as argv[0] placeholder — lives in code.
|
||||
empty_str:
|
||||
.db 0
|
||||
|
||||
;; =========================================================================
|
||||
;; get_progname — fetch the running program's full path via ESTEX APPINFO
|
||||
;; (subfn 2, $47) and set argv[0] to the basename portion.
|
||||
;;
|
||||
;; ESTEX APPINFO: A=err, C=$47, B=subfn, HL=buf → A=err, CF=1 on error.
|
||||
;; subfn 2 = full app path, e.g. "A:\PROGRAMS\ARGV_TES.EXE"
|
||||
;;
|
||||
;; On error or when no separator is found, we leave argv[0] as the empty
|
||||
;; string placeholder that parse_argv set.
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
get_progname::
|
||||
push ix ; ESTEX clobbers IX
|
||||
ld hl, #progname_buf
|
||||
ld b, #2 ; subfn 2 = app_path
|
||||
ld c, #0x47
|
||||
rst #0x10
|
||||
pop ix
|
||||
jr c, gpn_skip ; APPINFO failed → keep empty argv[0]
|
||||
|
||||
;; First make sure the buffer is null-terminated (it should already be).
|
||||
;; Then scan forward to find the end-of-string, then scan backwards to
|
||||
;; the last directory separator ('\' or ':').
|
||||
ld hl, #progname_buf
|
||||
gpn_find_end:
|
||||
ld a, (hl)
|
||||
or a, a
|
||||
jr Z, gpn_at_end
|
||||
inc hl
|
||||
jr gpn_find_end
|
||||
gpn_at_end:
|
||||
;; HL points at the NUL. Walk backwards looking for '\' or ':'. Stop
|
||||
;; if we reach the start of the buffer — then the whole path is the
|
||||
;; basename.
|
||||
ld de, #progname_buf
|
||||
gpn_scan_back:
|
||||
ld a, h
|
||||
cp a, d
|
||||
jr nz, gpn_dec
|
||||
ld a, l
|
||||
cp a, e
|
||||
jr Z, gpn_set ; reached buffer start
|
||||
gpn_dec:
|
||||
dec hl
|
||||
ld a, (hl)
|
||||
cp #0x5C ; backslash
|
||||
jr Z, gpn_after_sep
|
||||
cp #0x3A ; colon (drive separator)
|
||||
jr Z, gpn_after_sep
|
||||
jr gpn_scan_back
|
||||
|
||||
gpn_after_sep:
|
||||
inc hl ; skip past the separator
|
||||
gpn_set:
|
||||
ld (argv_array), hl ; argv[0] = basename
|
||||
gpn_skip:
|
||||
ret
|
||||
|
||||
;; =========================================================================
|
||||
;; Runtime data (HOME's _DATA / _BSS)
|
||||
;; =========================================================================
|
||||
.area _DATA
|
||||
_estex_startup_ix::
|
||||
.ds 2
|
||||
|
||||
_argc::
|
||||
.ds 2
|
||||
_argv::
|
||||
.ds 2
|
||||
|
||||
.ifdef DEBUG_RT
|
||||
;; Runtime diagnostic: 0 = no extra W2 page was self-allocated by crt0
|
||||
;; (DSS gave us what we needed); 1 = crt0 had to allocate W2 itself.
|
||||
;; Only present when sprinter-cc is invoked with --debug.
|
||||
_w2_self_allocated::
|
||||
.ds 1
|
||||
.endif
|
||||
|
||||
.area _BSS
|
||||
argv_buf:
|
||||
.ds ARGV_BUF_BYTES
|
||||
argv_array:
|
||||
.ds (ARGV_MAX_ARGS + 1) * 2 ; +1 for trailing NULL pointer
|
||||
progname_buf:
|
||||
.ds 128 ; ESTEX APPINFO app_path target
|
||||
|
||||
;; =========================================================================
|
||||
;; gsinit — copy _INITIALIZER -> _INITIALIZED, then zero _BSS.
|
||||
;; =========================================================================
|
||||
.area _GSINIT
|
||||
gsinit::
|
||||
ld bc, #l__INITIALIZER
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_bss
|
||||
ld de, #s__INITIALIZED
|
||||
ld hl, #s__INITIALIZER
|
||||
ldir
|
||||
gsinit_bss:
|
||||
ld bc, #l__BSS
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld hl, #s__BSS
|
||||
ld (hl), #0
|
||||
dec bc
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld d, h
|
||||
ld e, l
|
||||
inc de
|
||||
ldir
|
||||
gsinit_done:
|
||||
|
||||
.area _GSFINAL
|
||||
ret
|
||||
@@ -0,0 +1,459 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; crt0_banked.s — Sprinter ESTEX C runtime startup with bank loading.
|
||||
;;
|
||||
;; Same entry contract as crt0.s, plus the EXE was packaged with
|
||||
;; `loader > 0` so the file handle survives in (IX-3).
|
||||
;;
|
||||
;; Sequence:
|
||||
;; 1. Boot stack in W1 (CODE area). We can't yet touch W2 because
|
||||
;; small images (<16 KB) get only the W1 page from DSS — W2 reads
|
||||
;; as 0xFF and ignores writes. This mirrors crt0_small.s.
|
||||
;; 2. Detect via IN A,(#0xC2) whether DSS already mapped W2. If not,
|
||||
;; ESTEX $3D GETMEM + $3A SETWIN2 maps a fresh page (BIOS $C4 + OUT
|
||||
;; would require SP in W2, which we don't have yet).
|
||||
;; 3. SP = 0xBFFE (now W2 is real RAM)
|
||||
;; 4. Stash file handle + ESTEX block id from prefix
|
||||
;; 5. GETMEM(_n_banks) → block id, then for each bank i in 1..n:
|
||||
;; BIOS_EMM_GETPAGE → phys page → _bank_pages[i]
|
||||
;; OUT (#0xE2), phys ; map into W3
|
||||
;; ESTEX READ 16384 bytes → 0xC000 ; load bank from file
|
||||
;; 6. CLOSE file
|
||||
;; 7. Standard gsinit + call _main + ESTEX EXIT
|
||||
;;
|
||||
;; _n_banks MUST be defined as `const uint8_t n_banks = N;` — we read it
|
||||
;; before gsinit, so a plain initialized `uint8_t` would still hold 0
|
||||
;; (the initializer hasn't been copied yet) and bank loading would be
|
||||
;; silently skipped, leaving window 3 mapped to a garbage page.
|
||||
;; For zero banks, link crt0.s instead.
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module crt0_banked
|
||||
.globl _main
|
||||
.globl _n_banks
|
||||
.globl _bank_pages
|
||||
|
||||
.globl s__INITIALIZER
|
||||
.globl l__INITIALIZER
|
||||
.globl s__INITIALIZED
|
||||
.globl s__BSS
|
||||
.globl l__BSS
|
||||
|
||||
;; argv parsing tunables — mirror crt0.s.
|
||||
ARGV_MAX_ARGS = 16
|
||||
ARGV_BUF_BYTES = 128
|
||||
BOOT_STACK_BYTES = 32 ; enough for a few ESTEX RST 10h calls
|
||||
|
||||
;; ----- Banking-window parameters ----------------------------------------
|
||||
;; Choose between W3 (default = HUGE mode) and W1 (BIG mode, prepended
|
||||
;; BANK_W1 = 1 at assembly time by sprinter-cc). These constants steer
|
||||
;; the bank-loading IO port and the address where each bank lives in
|
||||
;; the program's virtual memory once mapped in.
|
||||
.ifdef BANK_W1
|
||||
BANK_PORT = 0xA2 ; W1 page register
|
||||
BANK_LOAD_ADDR = 0x4000 ; banks at 0x4000-0x7FFF when mapped
|
||||
.else
|
||||
BANK_PORT = 0xE2 ; W3 page register
|
||||
BANK_LOAD_ADDR = 0xC000 ; banks at 0xC000-0xFFFF when mapped
|
||||
.endif
|
||||
|
||||
;; ----- Area declaration order ---------------------------------------------
|
||||
.area _HOME
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
|
||||
.area _DATA
|
||||
.area _INITIALIZED
|
||||
.area _BSEG
|
||||
.area _BSS
|
||||
.area _HEAP
|
||||
|
||||
;; =========================================================================
|
||||
;; Entry point — first instruction in _CODE, hence at --code-loc (0x4100).
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
|
||||
_start::
|
||||
;; ----- Step 1: switch to W1 boot stack (W2 may be unmapped). -------
|
||||
ld sp, #boot_stack_top
|
||||
push ix ; stash IX in W1 (W2 not yet safe)
|
||||
|
||||
;; ----- Step 2: detect / allocate W2. ------------------------------
|
||||
;; Port 0xC2 = W2 page register. 0xFF means DSS hasn't mapped W2
|
||||
;; (program ≤ 16 KB). Any other value means W2 is real RAM and we
|
||||
;; must NOT replace it (DSS may have stored bytes there).
|
||||
in a, (#0xC2)
|
||||
cp #0xFF
|
||||
jr nz, w2_already_mapped
|
||||
;; ESTEX $3D GETMEM — alloc 1 page; A = block id, CF = err
|
||||
ld b, #1
|
||||
ld c, #0x3D
|
||||
rst #0x10
|
||||
jp c, abort_fault
|
||||
;; ESTEX $3A SETWIN2 — A = block id, B = page idx → map into W2
|
||||
ld b, #0
|
||||
ld c, #0x3A
|
||||
rst #0x10
|
||||
jp c, abort_fault
|
||||
.ifdef DEBUG_RT
|
||||
ld a, #1 ; flag: we self-allocated W2
|
||||
jr w2_join
|
||||
.endif
|
||||
|
||||
w2_already_mapped:
|
||||
.ifdef DEBUG_RT
|
||||
xor a ; flag: DSS already gave us W2
|
||||
w2_join:
|
||||
.endif
|
||||
pop ix ; restore IX
|
||||
ld sp, #0xBFFE ; canonical stack top (W2)
|
||||
.ifdef DEBUG_RT
|
||||
push af ; preserve flag across gsinit/bank load
|
||||
.endif
|
||||
|
||||
;; Stash startup prefix info from IX (asxxxx syntax: disp(ix)).
|
||||
ld a, -3 (ix)
|
||||
ld (_estex_file_handle), a
|
||||
ld a, -2 (ix)
|
||||
ld (_estex_block_id), a
|
||||
ld (_estex_startup_ix), ix
|
||||
|
||||
;; ---- Allocate _n_banks pages via ESTEX GETMEM ----
|
||||
ld a, (_n_banks)
|
||||
or a, a
|
||||
jr Z, skip_bank_load ; nothing to allocate
|
||||
|
||||
ld b, a
|
||||
ld c, #0x3D ; ESTEX GETMEM
|
||||
rst #0x10
|
||||
jp c, abort_fault ; CF=1 → out of memory
|
||||
ld (_bank_block_id), a ; remember block id
|
||||
|
||||
;; ---- For each bank i in 0..n-1 ----
|
||||
ld b, #0 ; B = page index inside block
|
||||
load_bank_loop:
|
||||
ld a, (_n_banks)
|
||||
cp b
|
||||
jr Z, load_bank_done ; B == n_banks → finished
|
||||
|
||||
;; --- Get physical page for slot B from BIOS EMM ---
|
||||
push bc ; save loop counter
|
||||
ld a, (_bank_block_id)
|
||||
ld c, #0xC4 ; BIOS EMM_GETPAGE
|
||||
rst #0x08
|
||||
pop bc
|
||||
jp c, abort_fault
|
||||
|
||||
;; A = physical page. Store in _bank_pages[B+1].
|
||||
push af ; save A (the phys page)
|
||||
push bc
|
||||
ld c, b
|
||||
inc c ; C = B+1 (1-based index)
|
||||
ld b, #0
|
||||
ld hl, #_bank_pages
|
||||
add hl, bc
|
||||
pop bc
|
||||
pop af
|
||||
ld (hl), a ; _bank_pages[B+1] = phys
|
||||
|
||||
;; Map this page into the banking window (W1 for BIG, W3 for HUGE).
|
||||
out (#BANK_PORT), a
|
||||
|
||||
;; ESTEX READ: A=handle, HL=buf, DE=count, C=0x13
|
||||
push bc ; save loop counter across RST
|
||||
ld a, (_estex_file_handle)
|
||||
ld hl, #BANK_LOAD_ADDR
|
||||
ld de, #0x4000 ; 16384 bytes (one full bank)
|
||||
ld c, #0x13 ; ESTEX READ
|
||||
rst #0x10
|
||||
pop bc
|
||||
jp c, abort_fault
|
||||
|
||||
inc b
|
||||
jr load_bank_loop
|
||||
|
||||
load_bank_done:
|
||||
;; Close the executable file — banks are in RAM now.
|
||||
ld a, (_estex_file_handle)
|
||||
ld c, #0x12 ; ESTEX CLOSE
|
||||
rst #0x10
|
||||
;; Ignore CF from close.
|
||||
|
||||
skip_bank_load:
|
||||
;; ---- Standard SDCC init path ----
|
||||
call gsinit
|
||||
.ifdef DEBUG_RT
|
||||
pop af
|
||||
ld (_w2_self_allocated), a
|
||||
.endif
|
||||
|
||||
;; Parse cmdline into argv[] (BSS now zeroed by gsinit), then fix
|
||||
;; argv[0] to the .EXE basename via APPINFO. Both routines fail
|
||||
;; gracefully (argv[0] stays empty if APPINFO returns CF=1).
|
||||
call parse_argv
|
||||
call get_progname
|
||||
|
||||
;; SDCC __sdcccall(1): main(argc, argv) → arg1=HL, arg2=DE.
|
||||
ld hl, (_argc)
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; main returned: int return is in DE per SDCC 4.5 __sdcccall(1).
|
||||
ld a, e
|
||||
jr exit_with_a
|
||||
|
||||
_exit::
|
||||
;; Userspace _exit(int code) — code in HL (single-arg ABI).
|
||||
ld a, l
|
||||
|
||||
exit_with_a:
|
||||
ld b, a
|
||||
ld c, #0x41 ; ESTEX EXIT
|
||||
rst #0x10
|
||||
1$: halt
|
||||
jr 1$
|
||||
|
||||
abort_fault:
|
||||
;; Bail out with a sentinel exit code so failures are visible in the
|
||||
;; emulator / shell.
|
||||
ld b, #0xFE
|
||||
ld c, #0x41
|
||||
rst #0x10
|
||||
1$: halt
|
||||
jr 1$
|
||||
|
||||
;; =========================================================================
|
||||
;; parse_argv / get_progname — identical copy of crt0.s. When we build
|
||||
;; libsprinter.lib these two routines will be factored into argv.s and
|
||||
;; linked once for all crt0 variants.
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
parse_argv::
|
||||
ld hl, (_estex_startup_ix)
|
||||
ld a, (hl)
|
||||
cp #ARGV_BUF_BYTES
|
||||
jr c, len_ok
|
||||
ld a, #ARGV_BUF_BYTES-1
|
||||
len_ok:
|
||||
ld c, a
|
||||
ld b, #0
|
||||
inc hl
|
||||
ld de, #argv_buf
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, after_copy
|
||||
ldir
|
||||
after_copy:
|
||||
xor a
|
||||
ld (de), a
|
||||
|
||||
ld hl, #argv_buf
|
||||
strip_ws:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, no_args
|
||||
cp #0x20
|
||||
jr Z, strip_more
|
||||
cp #0x09
|
||||
jr nz, have_first_token
|
||||
strip_more:
|
||||
inc hl
|
||||
jr strip_ws
|
||||
|
||||
no_args:
|
||||
ld hl, #empty_str
|
||||
ld (argv_array), hl
|
||||
ld hl, #0
|
||||
ld (argv_array+2), hl
|
||||
ld hl, #1
|
||||
ld (_argc), hl
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
have_first_token:
|
||||
ld de, #empty_str
|
||||
ld (argv_array), de
|
||||
ld b, #1
|
||||
|
||||
token_loop:
|
||||
ld a, b
|
||||
cp #ARGV_MAX_ARGS
|
||||
jr nc, tokens_done
|
||||
push hl
|
||||
ld a, b
|
||||
add a, a
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ld hl, #argv_array
|
||||
add hl, de
|
||||
pop de
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
ex de, hl
|
||||
inc b
|
||||
|
||||
walk_token:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, tokens_done
|
||||
cp #0x20
|
||||
jr Z, end_of_token
|
||||
cp #0x09
|
||||
jr Z, end_of_token
|
||||
inc hl
|
||||
jr walk_token
|
||||
|
||||
end_of_token:
|
||||
xor a
|
||||
ld (hl), a
|
||||
inc hl
|
||||
|
||||
skip_ws:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, tokens_done
|
||||
cp #0x20
|
||||
jr Z, skip_more
|
||||
cp #0x09
|
||||
jr Z, skip_more
|
||||
jr token_loop
|
||||
skip_more:
|
||||
inc hl
|
||||
jr skip_ws
|
||||
|
||||
tokens_done:
|
||||
ld a, b
|
||||
add a, a
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ld hl, #argv_array
|
||||
add hl, de
|
||||
ld (hl), #0
|
||||
inc hl
|
||||
ld (hl), #0
|
||||
ld a, b
|
||||
ld (_argc), a
|
||||
xor a
|
||||
ld (_argc+1), a
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
empty_str:
|
||||
.db 0
|
||||
|
||||
;; =========================================================================
|
||||
;; Boot stack — reserved in _CODE (W1, real RAM). Used only during the
|
||||
;; first few instructions of _start, before W2 is known-mapped. After we
|
||||
;; switch SP to 0xBFFE this memory is just dead bytes in HOME.
|
||||
;; =========================================================================
|
||||
boot_stack:
|
||||
.ds BOOT_STACK_BYTES
|
||||
boot_stack_top:
|
||||
|
||||
get_progname::
|
||||
push ix
|
||||
ld hl, #progname_buf
|
||||
ld b, #2
|
||||
ld c, #0x47
|
||||
rst #0x10
|
||||
pop ix
|
||||
jr c, gpn_skip
|
||||
|
||||
ld hl, #progname_buf
|
||||
gpn_find_end:
|
||||
ld a, (hl)
|
||||
or a, a
|
||||
jr Z, gpn_at_end
|
||||
inc hl
|
||||
jr gpn_find_end
|
||||
gpn_at_end:
|
||||
ld de, #progname_buf
|
||||
gpn_scan_back:
|
||||
ld a, h
|
||||
cp a, d
|
||||
jr nz, gpn_dec
|
||||
ld a, l
|
||||
cp a, e
|
||||
jr Z, gpn_set
|
||||
gpn_dec:
|
||||
dec hl
|
||||
ld a, (hl)
|
||||
cp #0x5C
|
||||
jr Z, gpn_after_sep
|
||||
cp #0x3A
|
||||
jr Z, gpn_after_sep
|
||||
jr gpn_scan_back
|
||||
|
||||
gpn_after_sep:
|
||||
inc hl
|
||||
gpn_set:
|
||||
ld (argv_array), hl
|
||||
gpn_skip:
|
||||
ret
|
||||
|
||||
;; =========================================================================
|
||||
;; Runtime data
|
||||
;; =========================================================================
|
||||
.area _DATA
|
||||
_estex_startup_ix::
|
||||
.ds 2
|
||||
_estex_file_handle::
|
||||
.ds 1
|
||||
_estex_block_id::
|
||||
.ds 1
|
||||
_bank_block_id::
|
||||
.ds 1
|
||||
_argc::
|
||||
.ds 2
|
||||
_argv::
|
||||
.ds 2
|
||||
|
||||
.area _BSS
|
||||
argv_buf:
|
||||
.ds ARGV_BUF_BYTES
|
||||
argv_array:
|
||||
.ds (ARGV_MAX_ARGS + 1) * 2
|
||||
progname_buf:
|
||||
.ds 128
|
||||
|
||||
.ifdef DEBUG_RT
|
||||
;; Runtime diagnostic — same semantics as in crt0.s / crt0_small.s.
|
||||
_w2_self_allocated::
|
||||
.ds 1
|
||||
.endif
|
||||
|
||||
;; =========================================================================
|
||||
;; gsinit — same as crt0.s (copy INITIALIZER, zero BSS)
|
||||
;; =========================================================================
|
||||
.area _GSINIT
|
||||
gsinit::
|
||||
ld bc, #l__INITIALIZER
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_bss
|
||||
ld de, #s__INITIALIZED
|
||||
ld hl, #s__INITIALIZER
|
||||
ldir
|
||||
gsinit_bss:
|
||||
ld bc, #l__BSS
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld hl, #s__BSS
|
||||
ld (hl), #0
|
||||
dec bc
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld d, h
|
||||
ld e, l
|
||||
inc de
|
||||
ldir
|
||||
gsinit_done:
|
||||
|
||||
.area _GSFINAL
|
||||
ret
|
||||
@@ -0,0 +1,110 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; crt0_minimal.s — Sprinter ESTEX C runtime startup, no argv parsing.
|
||||
;;
|
||||
;; This is the opt-out variant for programs where every byte counts.
|
||||
;; The DEFAULT runtime is runtime/crt0.s which also parses argv from the
|
||||
;; ESTEX command line and passes argc/argv to main().
|
||||
;;
|
||||
;; To use this instead of the default:
|
||||
;; crt0.rel: $(PROJ_ROOT)/runtime/crt0_minimal.s
|
||||
;; $(SDASZ80) -o $@ $<
|
||||
;;
|
||||
;; Otherwise identical to crt0.s — saves IX, calls gsinit, calls main
|
||||
;; with no arguments, then EXIT via ESTEX $41.
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module crt0_minimal
|
||||
.globl _main
|
||||
|
||||
;; Linker-emitted symbols for areas (resolved at link time).
|
||||
.globl s__INITIALIZER
|
||||
.globl l__INITIALIZER
|
||||
.globl s__INITIALIZED
|
||||
.globl s__BSS
|
||||
.globl l__BSS
|
||||
|
||||
;; =========================================================================
|
||||
;; AREA ORDERING — emitted up-front so the linker walks them in this order.
|
||||
;; =========================================================================
|
||||
.area _HOME
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
|
||||
.area _DATA
|
||||
.area _INITIALIZED
|
||||
.area _BSEG
|
||||
.area _BSS
|
||||
.area _HEAP
|
||||
|
||||
;; =========================================================================
|
||||
;; Entry point — first instruction in _CODE, hence at --code-loc (0x4100).
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
_start::
|
||||
;; ESTEX already set SP from the EXE header, but make it explicit.
|
||||
ld sp, #0xBFFE
|
||||
|
||||
;; Save startup prefix (IX) for argv/exit-code helpers.
|
||||
ld (_estex_startup_ix), ix
|
||||
|
||||
;; Run SDCC global initialisers (concatenated into _GSINIT).
|
||||
call gsinit
|
||||
|
||||
;; Hand off to user main.
|
||||
call _main
|
||||
|
||||
;; SDCC 4.5 __sdcccall(1): int return is in DE, so low byte of main()'s
|
||||
;; return is in E. (See memory/sdcc_z80_abi.md.)
|
||||
;; main returned — terminate via ESTEX EXIT directly. We do NOT
|
||||
;; expose a _exit symbol; programs that need exit() / atexit() must
|
||||
;; link libc/io/atexit.c.
|
||||
ld a, e
|
||||
ld b, a
|
||||
ld c, #0x41 ; ESTEX EXIT
|
||||
rst #0x10
|
||||
1$: halt
|
||||
jr 1$
|
||||
|
||||
;; =========================================================================
|
||||
;; Runtime data
|
||||
;; =========================================================================
|
||||
.area _DATA
|
||||
_estex_startup_ix::
|
||||
.ds 2
|
||||
|
||||
;; =========================================================================
|
||||
;; gsinit — copy _INITIALIZER -> _INITIALIZED, then zero _BSS.
|
||||
;; SDCC appends per-unit init code between this label and the ret in _GSFINAL.
|
||||
;; =========================================================================
|
||||
.area _GSINIT
|
||||
gsinit::
|
||||
;; --- Copy _INITIALIZER -> _INITIALIZED if non-empty ---
|
||||
ld bc, #l__INITIALIZER
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_bss
|
||||
ld de, #s__INITIALIZED
|
||||
ld hl, #s__INITIALIZER
|
||||
ldir
|
||||
gsinit_bss:
|
||||
;; --- Zero _BSS if non-empty ---
|
||||
ld bc, #l__BSS
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld hl, #s__BSS
|
||||
ld (hl), #0
|
||||
dec bc
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld d, h
|
||||
ld e, l
|
||||
inc de
|
||||
ldir
|
||||
gsinit_done:
|
||||
|
||||
.area _GSFINAL
|
||||
ret
|
||||
@@ -0,0 +1,395 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; crt0_small.s — Sprinter ESTEX C runtime startup for SMALL memory mode.
|
||||
;;
|
||||
;; Layout (small mode):
|
||||
;; CODE at 0x4100 (W1)
|
||||
;; DATA chained after code by the linker (W1, possibly spills into W2)
|
||||
;; STACK at 0xBFFE (top of W2, valid after W2 page is mapped)
|
||||
;; HEAP from end-of-BSS up to ___sdcc_heap_end (default 0xBB00)
|
||||
;;
|
||||
;; Size auto-detect:
|
||||
;; For programs < ~14 KB, DSS allocates exactly ONE 16 KB page (W1). W2
|
||||
;; is "page #FF" (read=0xFF, writes ignored) and we must allocate &
|
||||
;; map a page ourselves. For programs that already span > 16 KB (the
|
||||
;; linker emitted bytes above 0x7FFF), DSS allocates TWO pages on its
|
||||
;; own — W2 already RAM — and we MUST NOT call SETWIN2, or we'd swap
|
||||
;; the DSS-loaded code page out of W2.
|
||||
;;
|
||||
;; We auto-detect by writing a test byte at 0xBFFF (well above stack
|
||||
;; pushes) and reading back. Match → W2 is RAM, skip allocation.
|
||||
;; Mismatch → W2 is page #FF, allocate via ESTEX $3D + $3A.
|
||||
;;
|
||||
;; Chicken-and-egg:
|
||||
;; DSS sets SP from the EXE header (0xBFFE) — in unmapped W2 for small
|
||||
;; programs. Pushes silently fail; first `call` returns to 0xFFFF and
|
||||
;; crashes. Boot stack lives in HOME (W1, real RAM) until W2 is ready.
|
||||
;;
|
||||
;; Why ESTEX $3A SETWIN2 not BIOS $C4 + OUT (0xC2):
|
||||
;; BIOS calls require SP in W2 (0x8000..0xBFFF) — exactly what we don't
|
||||
;; have yet. SETWIN2 is ESTEX, works with W1 stack.
|
||||
;;
|
||||
;; Entry contract — same as crt0.s.
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module crt0_small
|
||||
.globl _main
|
||||
|
||||
;; Linker-emitted symbols (resolved at link time).
|
||||
.globl s__INITIALIZER
|
||||
.globl l__INITIALIZER
|
||||
.globl s__INITIALIZED
|
||||
.globl s__BSS
|
||||
.globl l__BSS
|
||||
|
||||
;; Tunables — mirror crt0.s.
|
||||
ARGV_MAX_ARGS = 16
|
||||
ARGV_BUF_BYTES = 128
|
||||
BOOT_STACK_BYTES = 32 ; enough for 2-3 nested RST 10h/RST 8 calls
|
||||
|
||||
;; =========================================================================
|
||||
;; AREA ORDERING.
|
||||
;; =========================================================================
|
||||
.area _HOME
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
|
||||
.area _DATA
|
||||
.area _INITIALIZED
|
||||
.area _BSEG
|
||||
.area _BSS
|
||||
.area _HEAP
|
||||
|
||||
;; =========================================================================
|
||||
;; Entry point — must be FIRST in _CODE so it lands at --code-loc (0x4100).
|
||||
;; W2 is unmapped at this instant — no `call` allowed until we point SP at
|
||||
;; boot_stack_top (in W1, declared just below).
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
_start::
|
||||
;; Switch to the boot stack in W1. The DSS-set SP=0xBFFE may point
|
||||
;; into unmapped W2 — pushing there silently fails and the first call
|
||||
;; would return to 0xFFFF. Boot stack lives in HOME (W1, real RAM).
|
||||
ld sp, #boot_stack_top
|
||||
|
||||
;; The DSS startup-prefix pointer is in IX. Stash it on the boot
|
||||
;; stack — _estex_startup_ix is in W2 which may not be RAM yet.
|
||||
push ix
|
||||
|
||||
;; ----- Step 1: detect whether W2 is already mapped. -----------------
|
||||
;; Port 0xC2 is the W2 page register (0x82/A2/C2/E2 = W0/W1/W2/W3).
|
||||
;; Reading it returns the page number currently selected for W2; page
|
||||
;; 0xFF is the "unmapped" sentinel (read=0xFF / write-ignored). A
|
||||
;; non-0xFF value means DSS already allocated and mapped a real RAM
|
||||
;; page for W2 — this happens for programs whose HOME image extends
|
||||
;; past 0x7FFF (DSS allocates as many pages as the image size demands).
|
||||
in a, (#0xC2)
|
||||
cp #0xFF
|
||||
jr nz, w2_already_mapped
|
||||
|
||||
;; ----- Step 2a: W2 unmapped → allocate one page and map it. --------
|
||||
;; ESTEX $3D GETMEM: B = npages → A = block id, CF = err.
|
||||
ld b, #1
|
||||
ld c, #0x3D
|
||||
rst #0x10
|
||||
jr c, w2_alloc_fail
|
||||
;; A = block id of the freshly-allocated 1-page block.
|
||||
|
||||
;; ESTEX $3A SETWIN2: A = block id, B = page idx → maps to W2. This
|
||||
;; is an ESTEX (RST 10h) call, not BIOS (RST 8) — BIOS would require
|
||||
;; SP in W2, which is exactly what we don't have yet.
|
||||
ld b, #0 ; page 0 of the 1-page block
|
||||
ld c, #0x3A
|
||||
rst #0x10
|
||||
jr c, w2_alloc_fail
|
||||
.ifdef DEBUG_RT
|
||||
ld a, #1 ; flag: we self-allocated W2
|
||||
jr w2_join
|
||||
.endif
|
||||
|
||||
w2_already_mapped:
|
||||
.ifdef DEBUG_RT
|
||||
xor a ; flag: DSS already gave us W2
|
||||
w2_join:
|
||||
.endif
|
||||
;; ----- Step 3: switch to the canonical SP at the top of W2. --------
|
||||
;; Pop IX back from the boot stack first.
|
||||
pop ix
|
||||
ld sp, #0xBFFE
|
||||
.ifdef DEBUG_RT
|
||||
;; Save the self-alloc flag across gsinit (which clobbers BC/DE/HL and
|
||||
;; zeroes BSS). SP is in W2 now, push is safe.
|
||||
push af
|
||||
.endif
|
||||
|
||||
;; ----- Step 5: save IX prefix in its canonical W2 slot. -------------
|
||||
ld (_estex_startup_ix), ix
|
||||
|
||||
;; ----- Step 6: standard gsinit + argv flow (identical to crt0.s). --
|
||||
call gsinit
|
||||
.ifdef DEBUG_RT
|
||||
pop af
|
||||
ld (_w2_self_allocated), a
|
||||
.endif
|
||||
call parse_argv
|
||||
call get_progname
|
||||
|
||||
ld hl, (_argc)
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; SDCC int return in DE. Low byte = exit code.
|
||||
ld a, e
|
||||
ld b, a
|
||||
ld c, #0x41 ; ESTEX EXIT
|
||||
rst #0x10
|
||||
1$: halt
|
||||
jr 1$
|
||||
|
||||
;; -------------------------------------------------------------------------
|
||||
;; W2 allocation failed — there's not much we can do without DATA/BSS.
|
||||
;; Try to abort via ESTEX EXIT with a non-zero code (no return).
|
||||
;; -------------------------------------------------------------------------
|
||||
w2_alloc_fail:
|
||||
ld b, #0xFE ; arbitrary failure code
|
||||
ld c, #0x41 ; ESTEX EXIT
|
||||
rst #0x10
|
||||
2$: halt
|
||||
jr 2$
|
||||
|
||||
;; =========================================================================
|
||||
;; Boot stack — reserved in HOME (W1, real RAM). Used only during the
|
||||
;; first few instructions of _start, before W2 is mapped. After we switch
|
||||
;; SP to 0xBFFE the boot stack memory is just dead bytes in HOME.
|
||||
;; =========================================================================
|
||||
boot_stack:
|
||||
.ds BOOT_STACK_BYTES
|
||||
boot_stack_top:
|
||||
|
||||
;; =========================================================================
|
||||
;; parse_argv / get_progname / runtime data / gsinit — same as crt0.s.
|
||||
;; Kept inline rather than factored out to keep crt0 build self-contained.
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
parse_argv::
|
||||
ld hl, (_estex_startup_ix)
|
||||
ld a, (hl)
|
||||
cp #ARGV_BUF_BYTES
|
||||
jr c, len_ok
|
||||
ld a, #ARGV_BUF_BYTES-1
|
||||
len_ok:
|
||||
ld c, a
|
||||
ld b, #0
|
||||
inc hl
|
||||
ld de, #argv_buf
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, after_copy
|
||||
ldir
|
||||
after_copy:
|
||||
xor a
|
||||
ld (de), a
|
||||
|
||||
ld hl, #argv_buf
|
||||
strip_ws:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, no_args
|
||||
cp #0x20
|
||||
jr Z, strip_more
|
||||
cp #0x09
|
||||
jr nz, have_first_token
|
||||
strip_more:
|
||||
inc hl
|
||||
jr strip_ws
|
||||
|
||||
no_args:
|
||||
ld hl, #empty_str
|
||||
ld (argv_array), hl
|
||||
ld hl, #0
|
||||
ld (argv_array+2), hl
|
||||
ld hl, #1
|
||||
ld (_argc), hl
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
have_first_token:
|
||||
ld de, #empty_str
|
||||
ld (argv_array), de
|
||||
ld b, #1
|
||||
|
||||
token_loop:
|
||||
ld a, b
|
||||
cp #ARGV_MAX_ARGS
|
||||
jr nc, tokens_done
|
||||
|
||||
push hl
|
||||
ld a, b
|
||||
add a, a
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ld hl, #argv_array
|
||||
add hl, de
|
||||
pop de
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
ex de, hl
|
||||
|
||||
inc b
|
||||
|
||||
walk_token:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, tokens_done
|
||||
cp #0x20
|
||||
jr Z, end_of_token
|
||||
cp #0x09
|
||||
jr Z, end_of_token
|
||||
inc hl
|
||||
jr walk_token
|
||||
|
||||
end_of_token:
|
||||
xor a
|
||||
ld (hl), a
|
||||
inc hl
|
||||
|
||||
skip_ws:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, tokens_done
|
||||
cp #0x20
|
||||
jr Z, skip_more
|
||||
cp #0x09
|
||||
jr Z, skip_more
|
||||
jr token_loop
|
||||
skip_more:
|
||||
inc hl
|
||||
jr skip_ws
|
||||
|
||||
tokens_done:
|
||||
ld a, b
|
||||
add a, a
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ld hl, #argv_array
|
||||
add hl, de
|
||||
ld (hl), #0
|
||||
inc hl
|
||||
ld (hl), #0
|
||||
ld a, b
|
||||
ld (_argc), a
|
||||
xor a
|
||||
ld (_argc+1), a
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
empty_str:
|
||||
.db 0
|
||||
|
||||
.area _CODE
|
||||
get_progname::
|
||||
push ix
|
||||
ld hl, #progname_buf
|
||||
ld b, #2
|
||||
ld c, #0x47
|
||||
rst #0x10
|
||||
pop ix
|
||||
jr c, gpn_skip
|
||||
|
||||
ld hl, #progname_buf
|
||||
gpn_find_end:
|
||||
ld a, (hl)
|
||||
or a, a
|
||||
jr Z, gpn_at_end
|
||||
inc hl
|
||||
jr gpn_find_end
|
||||
gpn_at_end:
|
||||
ld de, #progname_buf
|
||||
gpn_scan_back:
|
||||
ld a, h
|
||||
cp a, d
|
||||
jr nz, gpn_dec
|
||||
ld a, l
|
||||
cp a, e
|
||||
jr Z, gpn_set
|
||||
gpn_dec:
|
||||
dec hl
|
||||
ld a, (hl)
|
||||
cp #0x5C
|
||||
jr Z, gpn_after_sep
|
||||
cp #0x3A
|
||||
jr Z, gpn_after_sep
|
||||
jr gpn_scan_back
|
||||
|
||||
gpn_after_sep:
|
||||
inc hl
|
||||
gpn_set:
|
||||
ld (argv_array), hl
|
||||
gpn_skip:
|
||||
ret
|
||||
|
||||
;; =========================================================================
|
||||
;; Runtime data — same layout as crt0.s. Lives in DATA/BSS which the
|
||||
;; linker places in W2 (--data-loc 0x8000).
|
||||
;; =========================================================================
|
||||
.area _DATA
|
||||
_estex_startup_ix::
|
||||
.ds 2
|
||||
|
||||
_argc::
|
||||
.ds 2
|
||||
_argv::
|
||||
.ds 2
|
||||
|
||||
.area _BSS
|
||||
argv_buf:
|
||||
.ds ARGV_BUF_BYTES
|
||||
argv_array:
|
||||
.ds (ARGV_MAX_ARGS + 1) * 2
|
||||
progname_buf:
|
||||
.ds 128
|
||||
|
||||
.ifdef DEBUG_RT
|
||||
;; Runtime diagnostic: 0 = DSS already mapped W2 for us (program > 16 KB,
|
||||
;; or some other case); 1 = crt0 had to allocate and map W2 itself via
|
||||
;; ESTEX $3D + $3A. Only present when sprinter-cc is invoked with --debug.
|
||||
_w2_self_allocated::
|
||||
.ds 1
|
||||
.endif
|
||||
|
||||
;; =========================================================================
|
||||
;; gsinit — copy _INITIALIZER -> _INITIALIZED, then zero _BSS. Identical
|
||||
;; to crt0.s; runs AFTER W2 is mapped so writes to _INITIALIZED/_BSS land.
|
||||
;; =========================================================================
|
||||
.area _GSINIT
|
||||
gsinit::
|
||||
ld bc, #l__INITIALIZER
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_bss
|
||||
ld de, #s__INITIALIZED
|
||||
ld hl, #s__INITIALIZER
|
||||
ldir
|
||||
gsinit_bss:
|
||||
ld bc, #l__BSS
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld hl, #s__BSS
|
||||
ld (hl), #0
|
||||
dec bc
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, gsinit_done
|
||||
ld d, h
|
||||
ld e, l
|
||||
inc de
|
||||
ldir
|
||||
gsinit_done:
|
||||
|
||||
.area _GSFINAL
|
||||
ret
|
||||
@@ -0,0 +1,48 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; heap.s — Sprinter heap definition. Replaces SDCC's z80.lib heap.rel
|
||||
;; with a DYNAMIC heap that auto-sizes to whatever space is left between
|
||||
;; end-of-BSS and a fixed upper bound near the top of W2.
|
||||
;;
|
||||
;; Old design reserved a fixed `.ds 14000` after BSS, which overflowed
|
||||
;; the stack at 0xBFFE in tiny mode and crashed once allocations crossed
|
||||
;; that boundary. Even a static 8 KB would fail once CODE+DATA grew.
|
||||
;;
|
||||
;; New design:
|
||||
;; ___sdcc_heap = label at start of _HEAP area (= end of _BSS,
|
||||
;; placed by the linker)
|
||||
;; ___sdcc_heap_end = absolute equate at HEAP_TOP (= 0xBB00 default).
|
||||
;; malloc uses `&___sdcc_heap_end` as the upper
|
||||
;; bound of the free list.
|
||||
;;
|
||||
;; Heap size = HEAP_TOP - end_of_BSS. Grows when code shrinks, shrinks
|
||||
;; when code grows. Falls to 0 (malloc returns NULL) if BSS grows past
|
||||
;; HEAP_TOP — safe, no crash.
|
||||
;;
|
||||
;; The gap (BSS_end..HEAP_TOP) is real RAM that simply isn't tracked by
|
||||
;; the linker — it doesn't need to be, since nothing else is placed in
|
||||
;; that range (the area-ordering in crt0.s ends with _HEAP).
|
||||
;;
|
||||
;; Reserve 0xBB00..0xBFFE (= 1278 bytes) for the stack — enough for
|
||||
;; deep printf and most recursion. Override with -DHEAP_TOP=0xNNNN.
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module sprinter_heap
|
||||
|
||||
.globl ___sdcc_heap_init
|
||||
|
||||
;; NOTE: ___sdcc_heap_end is defined in crt0.s (per-program crt0).
|
||||
;; We need it as an absolute equate (since malloc takes &symbol),
|
||||
;; and the value must be configurable per program via -DHEAP_TOP=N
|
||||
;; (or its derivative --stack-size). Putting it in crt0 means the
|
||||
;; library can stay HEAP_TOP-agnostic.
|
||||
|
||||
;; gsinit hook — calls into z80.lib's malloc.rel to wire up the
|
||||
;; free-list at startup (after _BSS is zeroed).
|
||||
.area _GSINIT
|
||||
call ___sdcc_heap_init
|
||||
|
||||
;; Heap start label — _HEAP follows _BSS in the area-ordering chain,
|
||||
;; so ___sdcc_heap = first byte after BSS. No bytes reserved here;
|
||||
;; the heap memory is the gap to HEAP_TOP (defined in crt0).
|
||||
.area _HEAP
|
||||
___sdcc_heap::
|
||||
@@ -0,0 +1,22 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; heap_top.s — defines the absolute upper bound of the heap.
|
||||
;;
|
||||
;; This file is built PER-PROGRAM (not bundled into sprinter.lib) so the
|
||||
;; value of ___sdcc_heap_end can be overridden without re-assembling the
|
||||
;; library or the user's crt0. sprinter-cc may regenerate this file in
|
||||
;; its working dir with a custom value when `--stack-size N` is passed;
|
||||
;; otherwise the default below applies.
|
||||
;;
|
||||
;; malloc takes &___sdcc_heap_end as the upper bound of the free-list,
|
||||
;; so the symbol's address (== the equate value) IS the heap ceiling.
|
||||
;;
|
||||
;; 0xBFFE — stack top (init SP in crt0)
|
||||
;; 0xBB00 — heap top (default) → ~1278 bytes reserved for the stack
|
||||
;; ... — heap grows up to here from end-of-BSS
|
||||
;; end-of-BSS — heap start
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module sprinter_heap_top
|
||||
|
||||
___sdcc_heap_end = 0xBB00
|
||||
.globl ___sdcc_heap_end
|
||||
Reference in New Issue
Block a user