From 527d4a6a189e0e44580eca23ccad230078c66907 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 4 Jun 2026 09:26:10 +0300 Subject: [PATCH] libc review: mem/, stdio/, fixes in sprinter-cc and FILE shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libc/mem/: • Split bank_io.c into bank_io_w3.c (existing W3 helpers, base 0xC000, port 0xE2) and bank_io_w1.c (new mirror through W1, base 0x4000, port 0xA2). Two .rel files so DCE picks only the needed group: a W3-only user pulls ~70 bytes instead of all 134. W1 variants are `--memory tiny`-only (any other mode runs code from W1 or uses W1 for the banked-code segment, swapping it crashes). • mem_alloc.c: add CF=err checks for mem_free_block and mem_get_page (were silently ignored), per-function docstrings on alloc/free/ get_page/info, drop the confused "wait wrong order" comment in mem_info. Header sprinter_mem.h gets matching per-function doc. libc/stdio/: • Add hex_print.c (hex8/hex16/hex32, ~26 bytes) and dec_print.c (dec8/dec16/dec32, ~170 bytes) ported from solid-c STDLIB.ASM. Replaces the printf("%u"/"%X") wrappers in solid_helpers.c that dragged in the 3-5 KB printf machinery. - hex* use the classic cp 10 / sbc 0x69 / daa nibble→ASCII trick; hex8 self-calls for the high nibble, hex16/hex32 tail-call hex8. - dec32 is the master routine; dec8/dec16 jump into shared entry points (__dec_entry3 / __dec_entry5). 32-bit subtract-power-of-10 keeps the high 16 bits in HL alt (shadow set). - DISCOVERY: ESTEX PUTCHAR ($5B) on our Sprinter build preserves the main register set + IX but CLOBBERS the shadow set (BC'/DE'/HL'). solid-c's original code assumed otherwise and garbled output for values ≥ 6 digits. Fix: save/restore HL alt around the RST 10 in _dec_emit_or_skip. Documented in memory/estex_putchar_abi.md. • file.c: drop stdaux/stdprn (no Sprinter printer API), change stdin/stdout/stderr fd markers to 0/-1/-2 (positive fds clash with ESTEX OPEN return values), add TODO header pointing at v2 buffered FILE rewrite (see docs/TODO.md for the Solid-C reference struct). bin/sprinter-cc: • --memory big and --memory huge now always use crt0_banked.s (was: only with --bank flags), matching docs/memory_modes_implemented.md. When the user has no --bank flags, generate a tiny stub with `const uint8_t n_banks = 0;` and assemble bank.s for _bank_pages. Without this fix, openenv with --memory big could not see the estex_file_handle symbol exported by crt0_banked. examples/openenv: • Add usage of estex_file_handle to confirm the crt0_banked startup- info is reachable. Local extern decl — keeps the symbol out of sprinter.h since it only exists in big/huge builds. examples/dec_test: • New regression test covering hex8/16/32 and dec8/16/32 across the interesting boundary values. .gitignore: add .kilo/ (editor session cache). Co-Authored-By: Claude Opus 4.7 --- .gitignore | 1 + README.md | 26 ++-- bin/sprinter-cc | 56 +++++--- docs/TODO.md | 37 ++++++ examples/dec_test/Makefile | 3 + examples/dec_test/dec_test.c | 39 ++++++ examples/openenv/Makefile | 6 +- examples/openenv/openenv.c | 5 + lib/Makefile | 6 +- libc/include/sprinter_mem.h | 92 ++++++++++++-- libc/mem/bank_io_w1.c | 52 ++++++++ libc/mem/{bank_io.c => bank_io_w3.c} | 21 +-- libc/mem/mem_alloc.c | 80 +++++++++--- libc/stdio/dec_print.c | 184 +++++++++++++++++++++++++++ libc/stdio/file.c | 52 +++++--- libc/stdio/hex_print.c | 68 ++++++++++ libc/stdio/solid_helpers.c | 40 +----- 17 files changed, 639 insertions(+), 129 deletions(-) create mode 100644 examples/dec_test/Makefile create mode 100644 examples/dec_test/dec_test.c create mode 100644 libc/mem/bank_io_w1.c rename libc/mem/{bank_io.c => bank_io_w3.c} (60%) create mode 100644 libc/stdio/dec_print.c create mode 100644 libc/stdio/hex_print.c diff --git a/.gitignore b/.gitignore index 4e0c72c..10b343b 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ mame/ # IDEs .vscode/ .idea/ +.kilo/ # Claude Code local settings (per-machine, not for the repo) .claude/ diff --git a/README.md b/README.md index 97b7770..d8a507d 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ Sprinter's address space is four 16 KB windows (W0 / W1 / W2 / W3). DSS allocat pages by program size — small programs get only one page. Pick a memory mode based on what your program needs: -| Mode | Code lives in | Banking | Use when | +| Mode | Code lives in | Banking | Use when | Note | |---|---|---|---| -| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | -| `small` | W1 (0x4100+) | no | code+data < 30 KB | -| `big` | W2 + W1 banks | yes (W1) | tiny + extra code modules | -| `huge` | W1 + W3 banks | yes (W3) | small + extra code modules | -| `manual` | user-specified | optional | special layouts | +| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | | +| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | | +| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | | +| `huge` | W1-W2 + W3 banking | yes (W3) | small + extra code modules | | +| `manual` | user-specified | optional | special layouts | Not implemented | ```sh sprinter-cc --memory small -o big.exe bigprog.c @@ -157,20 +157,24 @@ sprinter-cc -o foo.exe foo.c [more.c ...] [options] What works in v1.0: * Compile / link / pack to SprintEXE — verified on all 27 examples -* All five memory modes (tiny / small / big / huge / manual) +* Four memory modes (tiny / small / big / huge) * Graphics (both modes) with accelerator * Mouse (text + graphics cursor) * File I/O, directories, environment, time * All headers listed above Deferred to v2.0 (see `docs/TODO.md`): -* **IM2 interrupt handlers** — research complete (`docs/im2_isr_design.md`), - implementation scheduled for v2 * **Turbo-C-style BGI graphics API** — `initgraph` / `setcolor` / `circle` / `getimage` / `putimage` / etc. on top of our `gfx_*` primitives -* **Audio API** (AY-3-8910 + COVOX) — requires IM2 -* **ISA-8 slot drivers** — requires IM2 * Remaining Solid-C compatibility gaps (Phase 2/3) — see `docs/solid_c_compatibility.md` +* Manual memory mode +* Rewrite FILE\* stream API (current implementation is very primitive and doesn't use buffers) + +Deferred to v3.0: +* **IM2 interrupt handlers** — research complete (`docs/im2_isr_design.md`), + implementation scheduled for v3 +* **Audio API** (AY-3-8910 + COVOX) — requires IM2 +* **ISA-8 slot drivers** — requires IM2 (???) ## Documentation diff --git a/bin/sprinter-cc b/bin/sprinter-cc index 4aa3f7e..a5071e4 100755 --- a/bin/sprinter-cc +++ b/bin/sprinter-cc @@ -186,9 +186,15 @@ ENTRY_ADDR="${ENTRY_ADDR:-$LOAD_ADDR}" # Pick crt0 source. Memory mode drives default crt0 selection; explicit # --crt0=TYPE still wins (e.g. --crt0=minimal for tiny w/o argv). +# +# big/huge ALWAYS use crt0_banked, even without explicit --bank flags: +# big — banks in W1 (BANK_W1=1 prepended) — code stays in W2 = tiny layout +# huge — banks in W3 — code in W1 = small layout, crt0_banked auto-detects W2 +# crt0_banked also exports startup-info symbols (estex_file_handle etc.). case "$MEMORY_MODE" in - small|huge) DEFAULT_CRT0="small";; - *) DEFAULT_CRT0="default";; + small) DEFAULT_CRT0="small";; + big|huge) DEFAULT_CRT0="banked";; + *) DEFAULT_CRT0="default";; # tiny, manual esac if [[ "$CRT0_TYPE" == "default" && "$DEFAULT_CRT0" != "default" ]]; then CRT0_TYPE="$DEFAULT_CRT0" @@ -272,32 +278,46 @@ for src in "${SOURCES[@]}"; do USER_RELS+=("$rel") done -# 3. bank sources + trampoline (bank.s). In BIG mode banks live in W1 at -# low16 = 0x4000; in HUGE / default mode they live in W3 at low16 = 0xC000. +# 3. bank infrastructure — required whenever crt0_banked is in play. +# Always assemble bank.s for _bank_pages + the bcall/bjump trampolines. +# If the user did not pass any --bank, generate a tiny stub providing +# n_banks = 0 (crt0_banked.s imports this symbol unconditionally). +# In BIG mode banks live in W1 at low16 = 0x4000; in HUGE / default +# mode they live in W3 at low16 = 0xC000. BANK_RELS=() BANK_LD_FLAGS=() -if [[ ${#BANK_SPECS[@]} -gt 0 ]]; then +if [[ "$CRT0_TYPE" == "banked" ]]; then if [[ $BANK_W1 -eq 1 ]]; then BANK_LOW16=0x4000 else BANK_LOW16=0xC000 fi - # Trampoline — depends on BANK_W1 so we assemble it here, not from the lib. + # Trampoline + _bank_pages table — depend on BANK_W1, so assemble per build. BANK_TRAMP_REL="$WORK/bank_trampoline.rel" asm_runtime "$RUNTIME/bank.s" "$BANK_TRAMP_REL" BANK_RELS+=("$BANK_TRAMP_REL") - for spec in "${BANK_SPECS[@]}"; do - bank_n="${spec%%=*}" - bank_src="${spec#*=}" - rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel" - run "$SDCC" "${CC_FLAGS[@]}" \ - --codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \ - -c -o "$rel" "$bank_src" - BANK_RELS+=("$rel") - # Virtual address: bank_n in upper byte, BANK_LOW16 in low half. - addr=$(printf "0x%X" $(( (bank_n << 16) | BANK_LOW16 ))) - BANK_LD_FLAGS+=("-Wl-b_BANK${bank_n}=${addr}") - done + + if [[ ${#BANK_SPECS[@]} -eq 0 ]]; then + # User has no banks — supply n_banks=0 so crt0_banked links. + STUB_C="$WORK/_n_banks_stub.c" + STUB_REL="$WORK/_n_banks_stub.rel" + echo "const unsigned char n_banks = 0;" > "$STUB_C" + run "$SDCC" "${CC_FLAGS[@]}" -c -o "$STUB_REL" "$STUB_C" + BANK_RELS+=("$STUB_REL") + else + for spec in "${BANK_SPECS[@]}"; do + bank_n="${spec%%=*}" + bank_src="${spec#*=}" + rel="$WORK/bank${bank_n}_$(basename "$bank_src" .c).rel" + run "$SDCC" "${CC_FLAGS[@]}" \ + --codeseg "BANK${bank_n}" --constseg "BANK${bank_n}" --dataseg "BANK${bank_n}" \ + -c -o "$rel" "$bank_src" + BANK_RELS+=("$rel") + # Virtual address: bank_n in upper byte, BANK_LOW16 in low half. + addr=$(printf "0x%X" $(( (bank_n << 16) | BANK_LOW16 ))) + BANK_LD_FLAGS+=("-Wl-b_BANK${bank_n}=${addr}") + done + fi fi # 4. link → .ihx diff --git a/docs/TODO.md b/docs/TODO.md index 991ac5f..8a8a36c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -255,6 +255,43 @@ User-задаваемые ISR через Z80 IM 2 mode. Нужны для: ### Прочие крупные пункты для v2 +- [ ] **FILE API rewrite — buffered streams** — текущая реализация в + `libc/stdio/file.c` это provisional unbuffered shim (каждый fputc/fgetc + = один read/write syscall). Нужна полноценная buffered семантика + как в Solid-C: + + ```c + typedef struct { + uint flags; // +0..1 file status flags + int level; // +2..3 empty/fill level of buffer + char *curp; // +4..5 current active pointer + int fd; // +6..7 underlying low-level fd + char *buffer; // +8..9 data transfer buffer + char hold; // +10 ungetc byte if no buffer + short token; // +11..12 reserved + char dummy; // +13 reserved + } FILE; + ``` + + stdin/stdout/stderr — fd-маркеры `0 / -1 / -2`. Отрицательные для + stdout/stderr выбраны намеренно: ESTEX OPEN может вернуть positive + small fd (1, 2, …) для обычного файла → если бы stdout=1, реальный + fd=1 сталкивался бы с идентификатором. fd=0 для stdin безопасно + (ESTEX 0 не возвращает). Сами fd не передаются в syscall'ы — + диспетчеризация по флагам `_F_CONIN/_F_CONOUT`. + + Принтер-потоки (stdaux/stdprn) НЕ реализуем — Sprinter принтерной + API не имеет. + + Альтернатива — взять реализацию из third_party/solid-c (sources в + `SRC/CLIB/`); там есть готовый buffered FILE + fopen/fread/fwrite/ + fseek/setvbuf и т.д. Адаптировать к нашим open/read/write/lseek. + + При rewrite заодно решить deferred issues stdio-review: + - `fwrite` short-write должен ставить `_F_ERROR` + - `fgets(buf, 1, fp)` — стандарт говорит "empty string", мы вернули NULL + - `mode_to_flags` — break-out на '+' (cosmetic) + - [ ] **Audio API** — AY-3-8910 + COVOX через прерывания (требует IM2) - [ ] **ISA-8 slot support** — ZX-Bus карты (sound, network, etc.) — требует IM2 + чтения portов diff --git a/examples/dec_test/Makefile b/examples/dec_test/Makefile new file mode 100644 index 0000000..df693bb --- /dev/null +++ b/examples/dec_test/Makefile @@ -0,0 +1,3 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := dec_test +include $(PROJ_ROOT)/examples/example.mk diff --git a/examples/dec_test/dec_test.c b/examples/dec_test/dec_test.c new file mode 100644 index 0000000..e4dbe35 --- /dev/null +++ b/examples/dec_test/dec_test.c @@ -0,0 +1,39 @@ +#include +extern void dec8 (unsigned char); +extern void dec16(unsigned int); +extern void dec32(unsigned long); +extern void hex8 (unsigned char); +extern void hex16(unsigned int); +extern void hex32(unsigned long); + +int main(void) { + puts("hex8:"); + hex8(0x00); puts(""); + hex8(0x5A); puts(""); + hex8(0xFF); puts(""); + puts("hex16:"); + hex16(0x0000); puts(""); + hex16(0xCAFE); puts(""); + hex16(0xFFFF); puts(""); + puts("hex32:"); + hex32(0x00000000UL); puts(""); + hex32(0xDEADBEEFUL); puts(""); + hex32(0xFFFFFFFFUL); puts(""); + puts("dec8:"); + dec8(0); puts(""); + dec8(42); puts(""); + dec8(255); puts(""); + puts("dec16:"); + dec16(0); puts(""); + dec16(1234); puts(""); + dec16(65535); puts(""); + puts("dec32:"); + dec32(0UL); puts(""); + dec32(98765UL); puts(""); + dec32(123456UL); puts(""); + dec32(123456789UL); puts(""); + dec32(4294967295UL); puts(""); + puts("done — any key"); + (void)getchar(); + return 0; +} diff --git a/examples/openenv/Makefile b/examples/openenv/Makefile index 3d54cb1..4f128e4 100644 --- a/examples/openenv/Makefile +++ b/examples/openenv/Makefile @@ -1,5 +1,7 @@ # Build open_env_test.exe — uses lib/sprinter.lib in TINY memory mode. -PROJ_ROOT := $(abspath $(CURDIR)/../..) -EXAMPLE := openenv +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := openenv +MEMORY := big +EXTRA_FLAGS := include $(PROJ_ROOT)/examples/example.mk diff --git a/examples/openenv/openenv.c b/examples/openenv/openenv.c index bf8efc4..7b4bfc9 100644 --- a/examples/openenv/openenv.c +++ b/examples/openenv/openenv.c @@ -11,6 +11,9 @@ * Touches the floppy: creates TMP1.TXT and TMP2.TXT, writes a few bytes, * verifies O_CREAT / O_EXCL / O_TRUNC / O_APPEND behaviour, then deletes. */ + +extern uint8_t estex_file_handle; + static void show_errno(const char *label) { @@ -22,6 +25,8 @@ int main(void) puts("Sprinter open() + env API test"); puts(""); + printf("estex_file_handle: fd=%u\n", estex_file_handle); + /* --- open() state machine ------------------------------------- */ /* 1. O_CREAT|O_EXCL: must succeed first time, must fail on retry. */ diff --git a/lib/Makefile b/lib/Makefile index 055bdff..7c8f457 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -30,7 +30,7 @@ LIBC_C := \ libc/io/read.c libc/io/sleep.c \ libc/io/time.c libc/io/posix_time.c libc/io/unlink.c \ libc/io/stat.c \ - libc/mem/bank_io.c libc/mem/mem_alloc.c \ + libc/mem/bank_io_w3.c libc/mem/bank_io_w1.c libc/mem/mem_alloc.c \ libc/gfx/gfx_core.c libc/gfx/gfx_raw_common.c \ libc/gfx/gfx_raw_256.c libc/gfx/gfx_raw_16.c \ libc/gfx/gfx_256.c libc/gfx/gfx_16.c \ @@ -38,7 +38,9 @@ LIBC_C := \ libc/gfx/gfx_text_16.c \ libc/stdio/getchar.c libc/stdio/print_hex.c \ libc/stdio/putchar.c libc/stdio/puts.c libc/stdio/file.c \ - libc/stdio/solid_helpers.c libc/io/solid_compat.c + libc/stdio/hex_print.c libc/stdio/dec_print.c \ + libc/stdio/solid_helpers.c \ + libc/io/solid_compat.c # Runtime modules to bundle (pulled by symbol references from libc-using code). # NOTE: runtime/bank.s is NOT bundled — its trampoline depends on the banking diff --git a/libc/include/sprinter_mem.h b/libc/include/sprinter_mem.h index b2ac747..ebb8eea 100644 --- a/libc/include/sprinter_mem.h +++ b/libc/include/sprinter_mem.h @@ -3,18 +3,35 @@ * * For data that doesn't fit in window 2's heap, allocate physical pages * directly through ESTEX EMM and access them via bank_read / bank_write - * (which swap window 3 internally). + * (which swap a CPU window internally). * * mem_alloc_pages(N) — reserve N contiguous 16 KB pages, return block id. * mem_free_block(id) — release a previously-allocated block. * mem_get_page(id, i) — translate (block, page-index) to physical page. * mem_info(&total, &free) — query EMM about total/free 16 KB pages. * - * bank_load_byte / bank_store_byte — single-byte access via W3. - * bank_read / bank_write — bulk copy between far page and a near buffer. + * bank_load_byte / bank_store_byte — single-byte access through W3. + * bank_read / bank_write — bulk copy through W3. + * bank_*_w1 — same family but through W1 (for + * memory modes where code lives in + * W2 and W1 is free for data). * - * The bank_* helpers live in HOME so they are always reachable; they save - * the previous W3 page, swap to the target, do the work, restore W3. + * Each helper saves the previous window-mapping, swaps to the target + * physical page, does the access, restores the saved page. + * + * SAFETY by memory mode (-memory FLAG to sprinter-cc): + * + * `tiny` : both bank_* and bank_*_w1 are safe. + * `small`/`huge`: only bank_* (W3) is safe; bank_*_w1 CRASHES + * because code lives in W1. + * `big` : only bank_* (W3) is safe; bank_*_w1 would clobber + * the banked-code segment in W1. + * `huge` (data path through W3 banked code): bank_* (W3) is unsafe; + * avoid banking via W3 in huge programs. + * + * In short: `_w1` accessors are a `tiny`-mode optimisation, useful when + * the program is small enough to fit in W2 and wants both W1 and W3 as + * independent banked-data windows. */ #ifndef SPRINTER_MEM_H @@ -22,16 +39,65 @@ #include -/* ESTEX EMM allocator wrappers. */ -uint8_t mem_alloc_pages(uint8_t n); /* 0 = failure */ -void mem_free_block (uint8_t blk_id); -uint8_t mem_get_page (uint8_t blk_id, uint8_t idx); -void mem_info (uint16_t *total, uint16_t *free_pages); +/* =================================================================== + * ESTEX EMM allocator wrappers + * =================================================================== */ -/* Far-page accessors (always-mapped HOME, swap W3 internally). */ -uint8_t bank_load_byte (uint8_t phys_page, uint16_t off_in_window); +/* Allocate `n` contiguous 16-KB physical pages from the EMM pool. + * n : 1..255 + * ret : blk_id (1..255) on success; 0 on failure with errno set. + * The id is opaque — pass it to mem_get_page() and mem_free_block(). */ +uint8_t mem_alloc_pages(uint8_t n); + +/* Release a block previously returned by mem_alloc_pages(). + * On error errno is set (e.g. EINVAL for unknown id). Double-free is + * NOT idempotent: the second call sets errno. */ +void mem_free_block(uint8_t blk_id); + +/* Translate (block, page-index) into a physical page number suitable + * for sprinter_page_w1/w2/w3() or the bank_*() helpers below. + * blk_id: from mem_alloc_pages() + * idx : 0..(n-1) + * ret : physical page (1..255) on success; 0 on failure (errno set). */ +uint8_t mem_get_page(uint8_t blk_id, uint8_t idx); + +/* Query the EMM allocator state. Both pointers must be non-NULL. + * Cannot fail (no error path). */ +void mem_info(uint16_t *total, uint16_t *free_pages); + +/* =================================================================== + * Far-page accessors via window 3 (base 0xC000, port 0xE2) + * + * Each call saves the current W3 mapping, sets W3 to `phys_page`, does + * the access at (base + off_in_window), then restores the saved page. + * Safe in `tiny`, `small`, `big` memory modes (W3 is free for data). + * NOT safe in `huge` — banked code lives in W3 there. + * =================================================================== */ + +/* Read one byte from phys_page at offset off_in_window (0..0x3FFF). */ +uint8_t bank_load_byte(uint8_t phys_page, uint16_t off_in_window); + +/* Write byte `v` into phys_page at offset off_in_window. */ void bank_store_byte(uint8_t phys_page, uint16_t off_in_window, uint8_t v); -void bank_read (uint8_t phys_page, uint16_t off, void *dst, uint16_t n); + +/* Copy `n` bytes from phys_page[off..off+n-1] into the near buffer `dst`. */ +void bank_read(uint8_t phys_page, uint16_t off, void *dst, uint16_t n); + +/* Copy `n` bytes from near buffer `src` into phys_page[off..off+n-1]. */ void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n); +/* =================================================================== + * Far-page accessors via window 1 (base 0x4000, port 0xA2) + * + * Same semantics as the W3 family but swap W1 instead. SAFE ONLY in + * `--memory tiny` builds — in any other mode code lives in (or uses) W1 + * and swapping it mid-call will crash. See header notes above for the + * full safety matrix. + * =================================================================== */ + +uint8_t bank_load_byte_w1(uint8_t phys_page, uint16_t off_in_window); +void bank_store_byte_w1(uint8_t phys_page, uint16_t off_in_window, uint8_t v); +void bank_read_w1(uint8_t phys_page, uint16_t off, void *dst, uint16_t n); +void bank_write_w1(uint8_t phys_page, uint16_t off, const void *src, uint16_t n); + #endif diff --git a/libc/mem/bank_io_w1.c b/libc/mem/bank_io_w1.c new file mode 100644 index 0000000..f40760a --- /dev/null +++ b/libc/mem/bank_io_w1.c @@ -0,0 +1,52 @@ +/* + * bank_io_w1.c — far-page accessors that swap window 1 (port 0xA2, + * base 0x4000). Mirror of bank_io.c but through W1. + * + * SAFE ONLY in `--memory tiny` builds. In tiny mode all code and data + * fit in W2, so both W1 and W3 are free for arbitrary banked data. + * + * small / huge: code lives in W1 (HOME) → swapping W1 unmaps the + * function mid-call and crashes. + * big : banked code segment lives in W1 → swap clobbers it. + * + * Split into its own .rel so that programs using only the W3 variants + * do NOT pull these functions in (and vice versa). + */ + +#include +#include +#include +#include + +uint8_t bank_load_byte_w1(uint8_t phys_page, uint16_t off_in_window) +{ + uint8_t saved = _io_page_w1; + sprinter_page_w1(phys_page); + uint8_t v = *((volatile uint8_t *)(0x4000u + off_in_window)); + sprinter_page_w1(saved); + return v; +} + +void bank_store_byte_w1(uint8_t phys_page, uint16_t off_in_window, uint8_t v) +{ + uint8_t saved = _io_page_w1; + sprinter_page_w1(phys_page); + *((volatile uint8_t *)(0x4000u + off_in_window)) = v; + sprinter_page_w1(saved); +} + +void bank_read_w1(uint8_t phys_page, uint16_t off, void *dst, uint16_t n) +{ + uint8_t saved = _io_page_w1; + sprinter_page_w1(phys_page); + memcpy(dst, (const void *)(0x4000u + off), n); + sprinter_page_w1(saved); +} + +void bank_write_w1(uint8_t phys_page, uint16_t off, const void *src, uint16_t n) +{ + uint8_t saved = _io_page_w1; + sprinter_page_w1(phys_page); + memcpy((void *)(0x4000u + off), src, n); + sprinter_page_w1(saved); +} diff --git a/libc/mem/bank_io.c b/libc/mem/bank_io_w3.c similarity index 60% rename from libc/mem/bank_io.c rename to libc/mem/bank_io_w3.c index ac83af8..2c6292d 100644 --- a/libc/mem/bank_io.c +++ b/libc/mem/bank_io_w3.c @@ -1,15 +1,16 @@ /* - * bank_io — HOME-resident helpers to read/write a "far" page that lives - * in some physical RAM page outside the currently-mapped W3 bank. + * bank_io_w3.c — far-page accessors that swap window 3 (port 0xE2, + * base 0xC000). Save the current W3 page, set the target, do the + * byte/memcpy access, restore the saved page. * - * - We always run from HOME (window 1, always mapped), so we are free - * to swap W3 (port 0xE2) between the caller's bank and the data - * page, then restore it before returning. - * - The caller must not rely on W3 contents during the call — the swap - * is transparent to instruction-fetch (we execute from W1), and only - * this function touches W3. - * - DI/EI is NOT applied around the swap; ISRs in HOME are unaffected, - * and banked-call ISRs are not expected in our current design. + * Safe in `tiny`, `small`, `big` memory modes (W3 is free for data). + * NOT safe in `huge` — there banked code lives in W3. + * + * No DI/EI around the swap: ISRs that don't touch W3 are unaffected; + * concurrent W3 use from an ISR is unsafe. + * + * The `_w1` family (bank_io_w1.c) is the mirror through W1 and is + * `tiny`-only. See sprinter_mem.h for the full safety matrix. */ #include diff --git a/libc/mem/mem_alloc.c b/libc/mem/mem_alloc.c index e061d43..87903a2 100644 --- a/libc/mem/mem_alloc.c +++ b/libc/mem/mem_alloc.c @@ -14,38 +14,68 @@ #include #include +/* + * Allocate `n` contiguous 16-KB physical pages from the EMM pool. + * + * in: n — 1..255 (number of pages requested). + * out: blk_id (1..255) on success; 0 on failure with errno set. + * + * The returned block id is opaque — pass it to mem_get_page() to obtain + * each physical-page number, and to mem_free_block() when done. Block + * ids start at 1; id 0 is reserved as the "allocation failed" sentinel. + */ uint8_t mem_alloc_pages(uint8_t n) __naked { (void)n; __asm - ;; SDCC single-uint8 arg → A on entry. + ;; SDCC single-uint8 arg → A on entry; ESTEX GETMEM wants n in B. push ix ld b, a - ld c, #0x3D + ld c, #0x3D ; ESTEX GETMEM rst #0x10 pop ix jr c, _alloc_fail - ret + ret ; CF=0 → A = blk_id, return as uint8 in A _alloc_fail: - call __errno_set - xor a, a ; 0 = failure + call __errno_set ; CF=1 → A = ESTEX errcode + xor a, a ; return 0 = failure sentinel ret __endasm; } +/* + * Release a block previously returned by mem_alloc_pages(). + * + * in: blk_id (1..255). + * out: void; on ESTEX error errno is set (e.g. EINVAL for unknown id). + * + * Idempotent guarantees are NOT provided — freeing the same block twice + * sets errno on the second call. Caller is responsible for tracking + * ownership. + */ void mem_free_block(uint8_t blk_id) __naked { (void)blk_id; __asm ;; SDCC single-uint8 arg → A on entry. push ix - ld c, #0x3E + ld c, #0x3E ; ESTEX FREEMEM rst #0x10 pop ix - ret + ret nc ; CF=0 → success + jp __errno_set ; CF=1 → A = ESTEX errcode; tail-call helper __endasm; } +/* + * Translate a (block, page-index) pair into a physical 16-KB page number, + * suitable for OUT to PORT_PAGE_W1/W2/W3 or for bank_*() helpers. + * + * in: blk_id — id returned by mem_alloc_pages(). + * idx — 0..(n-1), where n was the count passed to alloc. + * out: physical page number (1..255) on success; + * 0 on failure with errno set (invalid block or idx out of range). + */ uint8_t mem_get_page(uint8_t blk_id, uint8_t idx) __naked { (void)blk_id; (void)idx; @@ -57,41 +87,49 @@ uint8_t mem_get_page(uint8_t blk_id, uint8_t idx) __naked ld c, #0xC4 ; BIOS EMM_GETPAGE rst #0x08 pop ix - ;; A = physical page number. Return as uint8 → A. + ret nc ; CF=0 → A = phys page (return value) + ;; CF=1 → A = errcode; set errno, return 0 as sentinel. + call __errno_set + xor a, a ret __endasm; } +/* + * Query the EMM allocator about its current state. + * + * *total ← number of 16-KB physical pages installed in the system + * *free_pages ← number currently available for allocation + * + * Both pointers must be non-NULL writeable uint16_t locations. + * No error path: ESTEX INFOMEM always succeeds. + */ void mem_info(uint16_t *total, uint16_t *free_pages) __naked { (void)total; (void)free_pages; __asm - ;; HL = total, DE = free_pages on entry. - ;; ESTEX INFOMEM clobbers everything; stash both pointers on stack. + ;; HL = total ptr, DE = free_pages ptr on entry. + ;; RST 10 clobbers both — stash on the stack across the call. push ix - push hl ; [SP+0..1] = total ptr - push de ; [SP+2..3] = free_pages ptr (wait wrong order) + push hl ; later [SP+2] = total_ptr + push de ; TOS [SP+0] = free_pages_ptr - ;; Actually after two pushes: SP+0 = free_pages_ptr, SP+2 = total_ptr. - ;; That's the layout we'll use below. - - ld c, #0x3C ; ESTEX INFOMEM → HL=total, BC=free + ld c, #0x3C ; ESTEX INFOMEM → HL = total, BC = free rst #0x10 - ;; HL = total value, BC = free value. - pop de ; DE = free_pages ptr + pop de ; DE = free_pages_ptr ld a, c ld (de), a inc de ld a, b - ld (de), a + ld (de), a ; *free_pages = BC - pop de ; DE = total ptr + pop de ; DE = total_ptr ld a, l ld (de), a inc de ld a, h - ld (de), a + ld (de), a ; *total = HL pop ix ret diff --git a/libc/stdio/dec_print.c b/libc/stdio/dec_print.c new file mode 100644 index 0000000..380cf85 --- /dev/null +++ b/libc/stdio/dec_print.c @@ -0,0 +1,184 @@ +/* + * dec_print.c — compact decimal print primitives ported from solid-c + * (third_party/solid-c/SRC/CLIB/STDLIB.ASM, modules dec8/dec16/dec32). + * + * void dec8 (uint8_t v) — no-leading-zero "0" .. "255" + * void dec16(uint16_t v) — no-leading-zero "0" .. "65535" + * void dec32(uint32_t v) — no-leading-zero "0" .. "4294967295" + * + * Algorithm: subtract-power-of-10 with running counter, then emit the + * digit unless we are still on leading zeros. 32-bit values use a + * BC/DE/HL pair plus the shadow set (exx) for the high half. + * + * Layout: dec32 is the master routine; dec8/dec16 are tiny wrappers + * that prepare state and jump to internal entry points (__dec_entry3 + * for "last 3 digits", __dec_entry5 for "last 5") — same idea as + * solid-c, sharing most of the per-digit code. + * + * ESTEX PUTCHAR ($5B) preserves IX (empirically verified) so no + * push/pop ix around the RST. + */ + +#include +#include + +/* Leading-zero suppression flag — 0 means "still skipping zeros", + * non-zero means "first non-zero digit seen, print everything from + * here on (including subsequent zeros)". */ +static uint8_t dec_flag = 0; + +void dec8(uint8_t v) __naked +{ + (void)v; + __asm + ;; A = v. + ld l, a + ld h, #0 ; HL = value + xor a, a + ld (_dec_flag), a ; reset suppress-leading-zero flag + jp __dec_entry3 + __endasm; +} + +void dec16(uint16_t v) __naked +{ + (void)v; + __asm + ;; HL = v. + exx + ld hl, #0 ; HL alt = 0 (high 16 of composite) + exx + xor a, a + ld (_dec_flag), a + jp __dec_entry5 + __endasm; +} + +void dec32(uint32_t v) __naked +{ + (void)v; + __asm + ;; HL = high16, DE = low16 on entry (SDCC HLDE). + ;; Move high16 into HL alt (shadow set), low16 into HL. + push hl + exx + pop hl ; HL alt = high16 + exx + ex de, hl ; HL = low16 + xor a, a + ld (_dec_flag), a + + ;; ---- 5 most-significant decades (1e9..1e5) ---- + ld de, #0xCA00 + exx + ld de, #0x3B9A ; 0x3B9ACA00 = 1,000,000,000 + exx + call _dec_get_d32 + + ld de, #0xE100 + exx + ld de, #0x05F5 ; 0x05F5E100 = 100,000,000 + exx + call _dec_get_d32 + + ld de, #0x9680 + exx + ld de, #0x0098 ; 0x00989680 = 10,000,000 + exx + call _dec_get_d32 + + ld de, #0x4240 + exx + ld de, #0x000F ; 0x000F4240 = 1,000,000 + exx + call _dec_get_d32 + + ld de, #0x86A0 + exx + ld de, #0x0001 ; 0x000186A0 = 100,000 + exx + call _dec_get_d32 + + __dec_entry5:: ; entered from dec16 + ld de, #10000 + exx + ld de, #0 + exx + call _dec_get_d32 + + ld de, #1000 + call _dec_get_d16 + + __dec_entry3:: ; entered from dec8 + ld de, #100 + call _dec_get_d16 + ld de, #10 + call _dec_get_d16 + + ;; Units digit — always emitted (so dec*(0) prints "0"). + ld a, l + add a, #0x30 + ld c, #0x5B + rst #0x10 + ret + + ;; ---- 32-bit: how many times DE+DE_alt fits in HL+HL_alt ---- + _dec_get_d32: + ld a, #0x2F ; 0x2F = '0' minus 1 (pre-decrement) + and a, a ; CF = 0 + _dec_get_d32_loop: + inc a + sbc hl, de ; low half + exx + sbc hl, de ; high half (with chained borrow) + exx + jp nc, _dec_get_d32_loop + ;; Overshot — restore the last good value. + add hl, de + exx + adc hl, de + exx + jr _dec_emit_or_skip + + ;; ---- 16-bit: how many times DE fits in HL ---- + _dec_get_d16: + ld a, #0x2F + and a, a + _dec_get_d16_loop: + inc a + sbc hl, de + jp nc, _dec_get_d16_loop + add hl, de + ;; Fall through to emit/skip. + + _dec_emit_or_skip: + ;; A = digit char in 0x30..0x39. If non-'0', latch the flag. + ;; Print only if flag is non-zero. + ld b, a ; save digit across the flag test + cp a, #0x30 + jr z, _dec_check_flag + ld (_dec_flag), a ; non-zero digit seen + _dec_check_flag: + ld a, (_dec_flag) + or a, a + ld a, b ; restore digit (ld does not touch flags) + ret z ; leading zero — skip print + + ;; ESTEX PUTCHAR ($5B) preserves the main register set (BC, DE, + ;; HL, IX) but CLOBBERS the shadow set (BC alt, DE alt, HL alt). + ;; The 32-bit subtract-power-of-10 loop in _dec_get_d32 keeps + ;; the high 16 bits of the running remainder in HL alt, so we + ;; save/restore HL alt around the RST. Main HL (= low 16 of + ;; remainder) survives the call untouched, no save needed. + ;; See memory/estex_putchar_abi.md. + exx + push hl + exx + ld c, #0x5B + rst #0x10 + exx + pop hl + exx + ret + __endasm; +} diff --git a/libc/stdio/file.c b/libc/stdio/file.c index cd63334..3784ca6 100644 --- a/libc/stdio/file.c +++ b/libc/stdio/file.c @@ -1,14 +1,40 @@ /* - * file.c — minimal unbuffered FILE * implementation on top of the - * POSIX-style fd I/O (open/read/write/lseek/close). + * file.c — *** PROVISIONAL *** minimal unbuffered FILE * implementation + * on top of POSIX-style fd I/O + * (open/read/write/lseek/close). * - * No buffering: each fputc/fgetc maps to one read/write syscall. For - * heavy-throughput code, prefer fread/fwrite with a sizable buffer or - * the raw fd I/O directly. + * ============================================================ + * TODO (v2): replace with a proper BUFFERED implementation. + * See docs/TODO.md / Solid-C reference layout: * - * stdin/stdout/stderr are STATIC sentinel FILEs with fd=-1 and the - * _F_CONIN/_F_CONOUT flags set; fputc/fgetc detect them and call - * putchar()/getchar() (which already do CR/LF mapping and ESTEX calls). + * typedef struct { + * uint flags; // +0..1 file status flags + * int level; // +2..3 empty/fill level of buffer + * char *curp; // +4..5 current active pointer + * int fd; // +6..7 underlying low-level fd + * char *buffer; // +8..9 data transfer buffer + * char hold; // +10 ungetc byte if no buffer + * short token; // +11..12 reserved + * char dummy; // +13 reserved + * } FILE; + * + * The current implementation maps each fputc/fgetc to one read/write + * syscall — fine for correctness checks, awful for throughput. Issues + * 3/4/5 from the stdio-review (fwrite short-write flag, fgets n=1, + * mode_to_flags break) are deferred until that rewrite. + * ============================================================ + * + * stdin/stdout/stderr are STATIC sentinel FILEs flagged with + * _F_CONIN/_F_CONOUT; fputc/fgetc detect them and call putchar() / + * getchar() which already handle CR/LF translation and ESTEX calls. + * + * Their fd fields are 0 / -1 / -2. Negative values were chosen because + * ESTEX OPEN can return small positive fds (1, 2, …) for ordinary + * files — if we marked stdout/stderr with fd=1/2 a real file could + * collide with their identifier. fd=0 for stdin is kept (POSIX-style) + * because ESTEX does not return 0. Even so, none of these fd fields + * is ever passed to a syscall — the _F_CONIN/_F_CONOUT flags drive + * the dispatch. */ #include @@ -19,17 +45,13 @@ #include /* ---- console pseudo-streams ----------------------------------------*/ -static FILE _stdin = { -1, _F_READ | _F_CONIN }; -static FILE _stdout = { -2, _F_WRITE | _F_CONOUT }; -static FILE _stderr = { -3, _F_WRITE | _F_CONOUT }; -static FILE _stdaux = { -4, _F_WRITE | _F_CONOUT }; -static FILE _stdprn = { -5, _F_WRITE | _F_CONOUT }; +static FILE _stdin = { 0, _F_READ | _F_CONIN }; +static FILE _stdout = { -1, _F_WRITE | _F_CONOUT }; +static FILE _stderr = { -2, _F_WRITE | _F_CONOUT }; FILE *const stdin = &_stdin; FILE *const stdout = &_stdout; FILE *const stderr = &_stderr; -FILE *const stdaux = &_stdaux; -FILE *const stdprn = &_stdprn; /* ---- fopen / fclose -------------------------------------------------*/ diff --git a/libc/stdio/hex_print.c b/libc/stdio/hex_print.c new file mode 100644 index 0000000..70555e3 --- /dev/null +++ b/libc/stdio/hex_print.c @@ -0,0 +1,68 @@ +/* + * hex_print.c — compact hex print primitives ported from solid-c + * (third_party/solid-c/SRC/CLIB/STDLIB.ASM, modules hex8/hex16/hex32). + * + * void hex8 (uint8_t v) — always-2-digit "00" .. "FF" + * void hex16(uint16_t v) — always-4-digit "0000" .. "FFFF" + * void hex32(uint32_t v) — always-8-digit "00000000" .. "FFFFFFFF" + * + * Each nibble is emitted via the classic Z80 `cp 10 / sbc 0x69 / daa` + * trick — 5 bytes per nibble. hex8 self-calls for the high nibble + * then falls through for the low nibble. hex16/hex32 split into two + * hex8/hex16 calls. + * + * ESTEX PUTCHAR ($5B) preserves IX (empirically verified) so we skip + * the usual push/pop ix around the RST. + */ + +#include +#include + +void hex8(uint8_t v) __naked +{ + (void)v; + __asm + ;; A = v on entry. + push af + rra + rra + rra + rra + call _hex8_digit + pop af + _hex8_digit: + and a, #0x0F + cp a, #10 + sbc a, #0x69 + daa + ld c, #0x5B + rst #0x10 + ret + __endasm; +} + +void hex16(uint16_t v) __naked +{ + (void)v; + __asm + ;; HL = v on entry. + ld a, h + push hl + call _hex8 + pop hl + ld a, l + jp _hex8 ; tail-call + __endasm; +} + +void hex32(uint32_t v) __naked +{ + (void)v; + __asm + ;; HL = high16, DE = low16 on entry (SDCC HLDE). + push de + call _hex16 + pop hl + jp _hex16 ; tail-call + __endasm; +} diff --git a/libc/stdio/solid_helpers.c b/libc/stdio/solid_helpers.c index 103a596..fb46d1b 100644 --- a/libc/stdio/solid_helpers.c +++ b/libc/stdio/solid_helpers.c @@ -1,12 +1,12 @@ /* * solid_helpers.c — small Solid-C compatibility helpers. * - * Each function maps to the standard printf/sprintf machinery already - * available from SDCC's z80.lib + our overrides. No new syscalls. + * dec8 / dec16 / dec32 / hex8 / hex16 / hex32 are in dec_hex.c (compact + * asm port from solid-c's STDLIB.ASM, ~150 bytes total — vs ~3-5 KB if + * routed through printf). This file now only holds gets(). */ #include -#include /* ---- gets — dangerous but Solid-C provides it ---------------------- */ char *gets(char *buf) @@ -25,37 +25,3 @@ char *gets(char *buf) buf[i] = 0; return buf; } - -/* ---- decimal output: use printf %u ---------------------------------- */ - -void dec8(uint8_t v) -{ - printf("%u", (unsigned)v); -} - -void dec16(uint16_t v) -{ - printf("%u", (unsigned)v); -} - -void dec32(uint32_t v) -{ - printf("%lu", (unsigned long)v); -} - -/* ---- hex output: zero-padded ---------------------------------------- */ - -void hex8(uint8_t v) -{ - printf("%02X", (unsigned)v); -} - -void hex16(uint16_t v) -{ - printf("%04X", (unsigned)v); -} - -void hex32(uint32_t v) -{ - printf("%08lX", (unsigned long)v); -}