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