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>
460 lines
12 KiB
ArmAsm
460 lines
12 KiB
ArmAsm
;; ----------------------------------------------------------------------
|
|
;; 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
|