libc review: mem/, stdio/, fixes in sprinter-cc and FILE shim

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 09:26:10 +03:00
parent c71e249a4e
commit 527d4a6a18
17 changed files with 639 additions and 129 deletions
+1
View File
@@ -68,6 +68,7 @@ mame/
# IDEs
.vscode/
.idea/
.kilo/
# Claude Code local settings (per-machine, not for the repo)
.claude/
+15 -11
View File
@@ -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
+26 -6
View File
@@ -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,20 +278,33 @@ 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")
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#*=}"
@@ -299,6 +318,7 @@ if [[ ${#BANK_SPECS[@]} -gt 0 ]]; then
BANK_LD_FLAGS+=("-Wl-b_BANK${bank_n}=${addr}")
done
fi
fi
# 4. link → .ihx
IHX="$WORK/$(basename "$OUT" .exe).ihx"
+37
View File
@@ -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ов
+3
View File
@@ -0,0 +1,3 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := dec_test
include $(PROJ_ROOT)/examples/example.mk
+39
View File
@@ -0,0 +1,39 @@
#include <stdio.h>
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;
}
+2
View File
@@ -2,4 +2,6 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := openenv
MEMORY := big
EXTRA_FLAGS :=
include $(PROJ_ROOT)/examples/example.mk
+5
View File
@@ -12,6 +12,9 @@
* 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)
{
printf(" %s: errno=%d \"%s\"\n", label, errno, strerror(errno));
@@ -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. */
+4 -2
View File
@@ -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
+74 -8
View File
@@ -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 <stdint.h>
/* ESTEX EMM allocator wrappers. */
uint8_t mem_alloc_pages(uint8_t n); /* 0 = failure */
/* ===================================================================
* ESTEX EMM allocator wrappers
* =================================================================== */
/* 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 (always-mapped HOME, swap W3 internally). */
/* ===================================================================
* 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);
/* 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
+52
View File
@@ -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 <stdint.h>
#include <string.h>
#include <sprinter.h>
#include <sprinter_mem.h>
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);
}
+11 -10
View File
@@ -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 <stdint.h>
+58 -20
View File
@@ -14,38 +14,68 @@
#include <stdint.h>
#include <sprinter_mem.h>
/*
* 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)
;; Actually after two pushes: SP+0 = free_pages_ptr, SP+2 = total_ptr.
;; That's the layout we'll use below.
push hl ; later [SP+2] = total_ptr
push de ; TOS [SP+0] = free_pages_ptr
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
+184
View File
@@ -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 <stdio.h>
#include <stdint.h>
/* 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;
}
+37 -15
View File
@@ -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 <stdio.h>
@@ -19,17 +45,13 @@
#include <errno.h>
/* ---- 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 -------------------------------------------------*/
+68
View File
@@ -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 <stdio.h>
#include <stdint.h>
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;
}
+3 -37
View File
@@ -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 <stdio.h>
#include <stdint.h>
/* ---- 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);
}