;; ---------------------------------------------------------------------- ;; 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