c71e249a4e
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>
396 lines
10 KiB
ArmAsm
396 lines
10 KiB
ArmAsm
;; ----------------------------------------------------------------------
|
|
;; 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
|