Add full compiler toolchain, libc, examples and reference docs

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>
This commit is contained in:
2026-06-03 16:13:21 +03:00
parent f542608b3f
commit c71e249a4e
404 changed files with 75155 additions and 58 deletions
+40
View File
@@ -0,0 +1,40 @@
/*
* _errno_set — set `errno` from an ESTEX error code (0..255 in A).
*
* Replaces the inline pattern
* ld (_errno), a
* xor a, a
* ld (_errno+1), a ; 7 bytes per error path
* with a single
* call __errno_set ; 3 bytes per error path
*
* Saves ~4 bytes at every libc error handler that converts an ESTEX
* code into the C-side `errno`. Helper itself is 7 bytes; with 10+
* error paths in our libc the size win is net-positive.
*
* ABI:
* in: A = ESTEX error code (0..255)
* out: HL = A (zero-extended); errno fully overwritten so a prior
* large value (e.g. errno = -1) can't leak its high byte.
* clobbers: HL, AF flags. Caller must not depend on HL afterwards.
*
* Defensive 16-bit store — see chat 2026-06-02: if anyone ever assigns
* errno via C (`errno = -1`), the high byte becomes 0xFF, and a partial
* 8-bit write here would leave that 0xFF in place. Always writing the
* full word keeps errno honest regardless of who set it last.
*/
void _errno_set(unsigned char code) __naked
{
(void)code;
__asm
;; __sdcccall(1): single uint8_t arg already in A.
;; Write the two bytes separately so HL/BC/DE/IX/IY remain
;; untouched. Only A is clobbered: it is the input register,
;; and ABI does not require preserving it across a void call.
ld (_errno), a ; low byte = code
xor a, a
ld (_errno+1), a ; high byte = 0
ret
__endasm;
}
+57
View File
@@ -0,0 +1,57 @@
/*
* atexit + exit + _exit.
*
* atexit(fn) — register fn to be called at normal termination (max 8).
* exit(code) — run the atexit chain in LIFO, then ESTEX EXIT.
* _exit(code) — POSIX raw exit: skip the chain, go straight to ESTEX.
*
* These functions own termination entirely; crt0.s only does inline RST
* 10h #41 when main returns without an explicit exit(). That path skips
* the atexit chain — programs that need handlers should call exit() at
* the bottom of main (or return through a wrapper).
*/
#include <stdlib.h>
#include <sprinter_exit.h>
#define ATEXIT_MAX 8
static void (*atexit_stack[ATEXIT_MAX])(void);
static int atexit_top = 0;
int atexit(void (*fn)(void))
{
if (atexit_top >= ATEXIT_MAX) {
return -1;
}
atexit_stack[atexit_top++] = fn;
return 0;
}
/* exit() — runs the chain, then performs the raw ESTEX EXIT. */
void exit(int code)
{
while (atexit_top > 0) {
void (*fn)(void) = atexit_stack[--atexit_top];
if (fn) {
fn();
}
}
_exit(code); /* falls into the inline-asm raw exit below */
}
/* _exit() — POSIX raw termination, no atexit chain. */
void _exit(int code) __naked
{
(void)code;
__asm
;; HL = code (single int arg).
ld a, l
ld b, a
ld c, #0x41 ; ESTEX EXIT
rst #0x10
;; Should not return.
1$: halt
jr 1$
__endasm;
}
+629
View File
@@ -0,0 +1,629 @@
/*
* conio.c — console I/O wrappers around ESTEX kbd/screen syscalls.
*
* $30 WAITKEY — blocking read, returns scan / ASCII / modifiers
* $31 SCANKEY — non-blocking poll
* $32 ECHOKEY — blocking read + auto-echo to the screen
* $52 LOCATE — set cursor to (D=row, E=col)
* $56 CLEAR — fill a window with (A=char, B=attr)
* $5B PUTCHAR — write single character (CR/LF/scroll handled by ESTEX)
*
* Every RST 10h is bracketed with push/pop IX (caller's frame pointer).
*/
#include <conio.h>
#include <stdint.h>
#include <errno.h>
/* Forward extern — definition is further down (after putch/cputs which
* reference it from asm by linker-symbol name). */
extern int16_t g_text_attr;
char kbhit(void) __naked
{
__asm
push ix
ld c, #0x33 ; ESTEX CTRLKEY peeks without consuming
rst #0x10
pop ix
;; A=0 no key waiting; non-zero there is one.
or a, a
ret z
ld a, #0x01
ret
__endasm;
}
char getch(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY (no echo)
rst #0x10
pop ix
;; ESTEX returns ASCII in E (and copy in A)
ret
__endasm;
}
char getche(void) __naked
{
__asm
push ix
ld c, #0x32 ; ESTEX ECHOKEY (echo to console)
rst #0x10
pop ix
;; ESTEX returns ASCII in E (and copy in A)
ret
__endasm;
}
/* ---- putch / cputs: Turbo-C conio convention ---------------------- *
* Both APPLY the current text attribute (g_text_attr). When attr is
* KEEP_EXIST_ATTR (>0xFF), they short-circuit to the FAST stdio path
* (putchar / puts-like raw PCHARS).
*
* No '\n' to CR LF translation here — Turbo-C cputs/putch require the
* caller to use "\r\n" explicitly. Stdio puts/putchar do translate.
*/
static uint8_t pc_ch = 0;
static uint8_t pc_attr = 0;
static uint8_t pc_row = 0;
static uint8_t pc_col = 0;
/* Controls how _raw_putch treats control characters (< 0x20):
* 0 (default) — BS/TAB/LF/CR are interpreted (no glyph output);
* other chars print as glyphs via WRCHAR.
* 1 — all characters print as glyphs, no interpretation.
*
* Only takes effect on the WRCHAR (attr ≤ 0xFF) path. When
* g_text_attr is KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS handle
* cursor and control chars — pc_raw_mode is irrelevant. */
static uint8_t pc_raw_mode = 0;
void set_putch_raw_mode(uint8_t mode) { pc_raw_mode = mode; }
uint8_t get_putch_raw_mode(void) { return pc_raw_mode; }
/* ---- Internal helpers ------------------------------------------- */
/* Read current cursor into pc_row / pc_col via ESTEX CURSOR ($53). */
static void _get_cursor(void) __naked
{
__asm
push ix
ld c, #0x53 ; ESTEX CURSOR
rst #0x10
ld a, d
ld (_pc_row), a
ld a, e
ld (_pc_col), a
pop ix
ret
__endasm;
}
/* Move cursor to (pc_col, pc_row) via ESTEX LOCATE ($52). */
static void _set_cursor(void) __naked
{
__asm
push ix
ld a, (_pc_row)
ld d, a
ld a, (_pc_col)
ld e, a
ld c, #0x52 ; ESTEX LOCATE
rst #0x10
pop ix
ret
__endasm;
}
/* ESTEX PUTCHAR ($5B) — fast no-attr path; ESTEX handles CR/LF/scroll
* and cursor itself. Used when g_text_attr = KEEP_EXIST_ATTR. */
static char _bios_putchar(char ch) __naked
{
(void)ch;
__asm
;; c in A. push af stashes it across the RST (which clobbers A).
;; push ix ; PUTCHAR не меняет IX
push af
ld c, #0x5B ; ESTEX PUTCHAR
rst #0x10
pop af
;; pop ix
ret
__endasm;
}
/* Raw putch: low-level WRCHAR-based output at (pc_col, pc_row) using
* the given attribute byte (caller has already verified that the high
* byte of g_text_attr is zero — this function takes only the low byte).
* Updates pc_col / pc_row per pc_raw_mode:
*
* pc_raw_mode == 0: BS/TAB/LF/CR are INTERPRETED:
* 0x08 BS → pc_col-- (if not already 0)
* 0x09 TAB → pc_col rounded up to next multiple of 8 (capped 80)
* 0x0A LF → pc_row++ (capped at 32; no glyph)
* 0x0D CR → pc_col = 0
* other → WRCHAR + pc_col++
*
* pc_raw_mode == 1: ALL characters print as glyphs via WRCHAR
* + pc_col++ (including 0x08, 0x09, 0x0A, 0x0D — they render as
* their CP437 glyphs).
*
* WRCHAR itself is suppressed when pc_col ≥ 80 or pc_row ≥ 32 (off-
* screen) — coordinates [0..79] × [0..31] only.
*
* Does NOT call CURSOR / LOCATE — caller is expected to fetch cursor
* once before a sequence of _raw_putch calls and write it back once
* after, so we pay the BIOS overhead per OPERATION instead of per CHAR. */
/* Mode-0 worker: interprets BS/TAB/LF/CR, outputs other chars as glyphs. */
static void _raw_putch_raw0(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch in A, attr in L.
;; Dispatch on control chars while A still holds ch (cp does not
;; modify A). B/C only get loaded on the output path so the
;; ctrl-char paths are cheaper.
cp #0x08
jr z, _rp0_bs
cp #0x09
jr z, _rp0_tab
cp #0x0A
jr z, _rp0_lf
cp #0x0D
jr z, _rp0_cr
;; Anything else (printable or unrecognised ctrl) glyph.
ld c, a ; C = ch (save before A is clobbered)
ld a, (_pc_row)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row (ESTEX WRCHAR convention)
ld a, (_pc_col)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
_rp0_bs:
ld a, (_pc_col)
or a, a
ret z ; already at col 0 no change
dec a
ld (_pc_col), a
ret
_rp0_tab:
ld a, (_pc_col)
and #0xF8 ; floor to mult of 8
add a, #8 ; next mult of 8
cp #80
jr c, _rp0_tab_store
ld a, #80 ; cap at off-screen right
_rp0_tab_store:
ld (_pc_col), a
ret
_rp0_lf:
ld a, (_pc_row)
cp #32
ret nc ; already at bottom edge
inc a
ld (_pc_row), a
ret
_rp0_cr:
xor a, a
ld (_pc_col), a
ret
__endasm;
}
/* Mode-1 worker: every byte goes through WRCHAR as a glyph. */
static void _raw_putch_raw1(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch in A, attr in L.
ld c, a ; C = ch (save)
ld a, (_pc_row)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row
ld a, (_pc_col)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
__endasm;
}
/* PCHARS (no attr) — used by cputs when KEEP_EXIST_ATTR is in effect. */
static void _cputs_pchars(const char *s) __naked
{
(void)s;
__asm
push ix
ld c, #0x5C ; ESTEX PCHARS
rst #0x10
pop ix
ret
__endasm;
}
/* ---- Public putch / cputs --------------------------------------- *
*
* KEEP_EXIST_ATTR (high byte != 0) → fast PUTCHAR/PCHARS through
* ESTEX, which manages its own cursor.
*
* Otherwise → fetch cursor ONCE via CURSOR ($53), run one or many
* _raw_putch calls, write cursor back ONCE via LOCATE ($52). This
* folds the per-char CURSOR/LOCATE pair from the old design into a
* single pair per operation. */
char putch(char ch) __naked
{
(void)ch;
__asm
;; A = ch on entry; char return A.
ld (_pc_ch), a ; stash c (for both return and re-load)
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _putch_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker ABI (2 char/uint8 args): ch in A, attr in L.
ld a, (_g_text_attr) ; A = low byte = attr
ld l, a ; L = attr
ld a, (_pc_raw_mode)
or a, a ; Z = (mode == 0)
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
jr nz, _putch_use_raw1
call __raw_putch_raw0
jr _putch_after_raw
_putch_use_raw1:
call __raw_putch_raw1
_putch_after_raw:
call __set_cursor
ld a, (_pc_ch) ; return value
ret
_putch_fast:
ld a, (_pc_ch)
call __bios_putchar ; __bios_putchar keeps AF
ret
__endasm;
}
char cputs(const char *s) __naked
{
(void)s;
__asm
;; HL = s on entry; char return A.
;; NULL-check: cputs(NULL) return 0 immediately.
ld a, h
or a, l
ret z
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _cputs_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker walks the string via DE; HL is used to carry attr in L
;; across iterations (RST 10 inside worker clobbers it, so we
;; push/pop hl around each call).
ex de, hl ; DE = s
ld a, (_g_text_attr) ; A = attr (low byte)
ld l, a ; L = attr (worker ABI: arg2 in L)
;; Pick worker once based on pc_raw_mode; IX = function pointer.
ld a, (_pc_raw_mode)
or a, a
jr z, _cputs_use_raw0
ld ix, #__raw_putch_raw1
jr _cputs_loop
_cputs_use_raw0:
ld ix, #__raw_putch_raw0
_cputs_loop:
ld a, (de)
or a, a
jr z, _cputs_loop_end
inc de
;; Z80 has no "call (ix)" emulate via push-of-ret + jp (ix).
push de ; save string pointer
push hl ; save attr (in L)
ld de, #_cputs_after_worker
push de ; push return address
jp (ix) ; "call" worker
_cputs_after_worker:
pop hl
pop de
jr _cputs_loop
_cputs_loop_end:
call __set_cursor
xor a, a ; return 0
ret
_cputs_fast:
call __cputs_pchars
xor a, a ; return 0
ret
__endasm;
}
void clrscr(void) __naked
{
__asm
ld a, #0x0F
jp _clrscr_attr
__endasm;
}
void clrscr_attr(uint8_t attr) __naked
{
(void)attr;
__asm
push ix
;; SDCC __sdcccall(1): uint8_t 1st arg is in A.
ld b, a ; B = attribute (mode-fill colour)
ld de, #0x0000 ; top-left
ld hl, #0x2050 ; H=32 rows, L=80 cols
ld a, #0x20 ; space fill
ld c, #0x56 ; ESTEX CLEAR
rst #0x10
pop ix
ret
__endasm;
}
void gotoxy(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
;; __sdcccall(1) 2 uint8 args: x in A, y in L.
;; ESTEX LOCATE ($52) wants: D = row, E = col.
push ix
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld c, #0x52
rst #0x10
pop ix
ret
__endasm;
}
uint8_t wherex(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
ld a, e
ret
__endasm;
}
uint8_t wherey(void) __naked
{
__asm
push ix
ld c, #0x53
rst #0x10
pop ix
ld a, d
ret
__endasm;
}
uint16_t wherexy(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
ret
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
* SDCC __sdcccall(1): x in A, y in L (2 uint8 → A, L); ch and attr
* packed and pushed on the stack as a single 16-bit value (caller does
* `ld hl, #(attr<<8)|ch; push hl`). Layout after CALL:
* [SP+0..1] = return address
* [SP+2] = ch (low half of pushed pair)
* [SP+3] = attr (high half)
* Void return → callee-pops the 2 stack-arg bytes via `pop bc` + jp (iy).
*/
void wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr) __naked
{
(void)x; (void)y; (void)ch; (void)attr;
__asm
pop iy ; return address
pop bc ; C = ch, B = attr
push ix
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld a, c ; A = ch
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
jp (iy)
__endasm;
}
/* rdchar(int x, int y) → (attr << 8) | ch */
uint16_t rdchar(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
push ix
ld d, l ; D = row
ld e, a ; E = col
ld c, #0x57 ; ESTEX RDCHAR
rst #0x10
;; A = ch, B = attr
ld d, b ; high byte attr
ld e, a ; low byte ch
pop ix
ret
__endasm;
}
/* Public text-mode video API — defined here so it's pulled in with the
* rest of conio. The raw setters/getters live in videomode_raw.c so
* pure graphics programs can pick them up without conio's other
* dependencies. */
extern uint8_t _videomode_raw_get(void);
extern int _videomode_raw_set(uint8_t mode);
uint8_t get_videotextmode(void)
{
return _videomode_raw_get();
}
int set_videotextmode(uint8_t mode)
{
/* Refuse anything that isn't a known text mode — otherwise a stray
* GFX_MODE_* value could swap the screen out from under text I/O. */
if (mode != TEXT_MODE_40x32 && mode != TEXT_MODE_80x32) {
errno = EINVAL;
return -1;
}
return _videomode_raw_set(mode);
}
/* ---- text attribute state ----------------------------------------
* g_text_attr is owned by conio.c now (Turbo-C-style: stdio putchar/puts
* are fast and attribute-free; only conio's putch/cputs/cprintf apply
* the attribute). Default = 0x0F (bright white on black).
*
* 0x00..0xFF — real attribute (4-bit FG | 3-bit BG | 1-bit blink)
* KEEP_EXIST_ATTR (0xFFFF) — putch/cputs fall back to fast no-attr path */
int16_t g_text_attr = 0x0F;
int16_t set_text_attr(int16_t attr)
{
int16_t prev = g_text_attr;
g_text_attr = attr;
return prev;
}
int16_t get_text_attr(void)
{
return g_text_attr;
}
/* ---- Turbo-C-style palette helpers --------------------------------
* textcolor / textbackground touch only their nibble; the other nibble
* (and the blink bit) are preserved. textattr replaces the whole byte. */
void textcolor(uint8_t fg)
{
/* If we were KEEP_EXIST_ATTR, switch to a real attr first. */
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
g_text_attr = (int16_t)((cur & 0xF0) | (fg & 0x0F));
}
void textbackground(uint8_t bg)
{
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
/* Background uses 3 bits (4..6); preserve blink (bit 7) too. */
g_text_attr = (int16_t)((cur & 0x8F) | ((bg & 0x07) << 4));
}
void textattr(uint8_t attr)
{
g_text_attr = (int16_t)attr;
}
/* ---- Solid-C compatibility ---------------------------------------- */
/* Direct port I/O. Z80 has 256 IN/OUT ports; we wrap the Z80 IN/OUT
* opcodes with a stable C API. Names match Solid-C / MS-DOS Turbo-C. */
uint8_t z80_inp(uint8_t port) __naked
{
(void)port;
__asm
;; SDCC __sdcccall(1): single uint8_t arg in A; uint8_t return in A.
ld c, a
in a, (c)
ret
__endasm;
}
void z80_outp(uint8_t port, uint8_t value) __naked
{
(void)port; (void)value;
__asm
;; __sdcccall(1): 2 uint8 args arg1 in A, arg2 in L.
ld c, a ; C = port
out (c), l ; out (port), value
ret
__endasm;
}
/* cgets — Solid-C / Turbo-C style line input.
* buf[0] = max characters (in)
* buf[1] = actual count (out)
* buf[2..] = chars + NUL
* Returns &buf[2]. */
char *cgets(char *buf)
{
uint8_t maxlen = (uint8_t)buf[0];
uint8_t n = 0;
while (n < maxlen) {
int ch = getche();
if (ch == '\n' || ch == '\r') {
putch('\r'); putch('\n');
break;
}
if (ch == 8) { /* backspace */
if (n > 0) { n--; }
continue;
}
buf[2 + n] = (char)ch;
n++;
}
buf[1] = (char)n;
buf[2 + n] = 0;
return &buf[2];
}
+28
View File
@@ -0,0 +1,28 @@
/*
* cprintf — printf for the conio output set. Formats into a static
* buffer with vsprintf (from SDCC's stdlib), then emits via cputs which
* applies the current text attribute per character.
*
* No '\n' to CR LF translation — Turbo-C convention: callers write
* "\r\n" explicitly in the format string for line breaks.
*
* Not reentrant (single static buffer) but Z80 single-threaded is fine.
*/
#include <conio.h>
#include <stdarg.h>
#include <stdio.h>
#define CPRINTF_BUF_SIZE 256
static char cp_buf[CPRINTF_BUF_SIZE];
int cprintf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
int n = vsprintf(cp_buf, fmt, ap);
va_end(ap);
cputs(cp_buf);
return n;
}
+68
View File
@@ -0,0 +1,68 @@
/*
* ffirst / fnext — directory iteration via ESTEX $19 / $1A.
*
* ESTEX F_FIRST ($19):
* HL = pattern, DE = buffer, A = attribute mask, B = format (0/1)
* CF = err / A = error code
* ESTEX F_NEXT ($1A):
* DE = same buffer
* CF = err / A = error code
*
* We always use format B=1 — 256-byte buffer with NUL-terminated DOS
* "name.ext" name at offset 33.
*
* ABI note: ffirst takes uint8_t as its 3rd arg. SDCC pushes a *single*
* byte for that (via `push af; inc sp`), not two — so the callee must
* pop ret-addr (2 bytes) AND consume the attr byte (`inc sp`) on the way
* out. Naively `pop bc` would over-eat into the caller's frame.
*/
#include <dir.h>
int ffirst(const char *pattern, ffblk_t *buf, uint8_t attrib) __naked
{
(void)pattern; (void)buf; (void)attrib;
__asm
;; On entry: HL = pattern, DE = buf, [SP+0..1] = ret, [SP+2] = attr.
ld iy, #2
add iy, sp
ld a, 0 (iy) ; A = attr (read without disturbing SP)
push ix
ld bc, #0x0119 ; ESTEX F_FIRST; format: 1 = DOS "name.ext" layout
rst #0x10
pop ix
pop hl ; HL = return address
inc sp ; consume the 1-byte attr
jr c, _ff_err
ld de, #0
jp (hl)
_ff_err:
call __errno_set
ld de, #-1
jp (hl)
__endasm;
}
int fnext(ffblk_t *buf) __naked
{
(void)buf;
__asm
;; HL = buf on entry; ESTEX F_NEXT wants buf in DE.
push ix
ex de, hl
ld c, #0x1A ; ESTEX F_NEXT
rst #0x10
pop ix
jr c, _fnext_err
ld de, #0
ret
_fnext_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
+93
View File
@@ -0,0 +1,93 @@
/*
* getenv / putenv via ESTEX ENVIRON ($46).
*
* ESTEX $46 subfn 1 (getenv):
* in: HL = name (ASCIIZ), DE = output buffer (caller-owned)
* out: CF=0 + A != 0 → variable found, value written into [DE..end-1]
* CF=0 + A == 0 → variable not present (note: this is the
* opposite of what DiskSyscalls.txt v1.6 docs
* claim — verified empirically and against
* solid-c's IO.ASM:763-766 implementation)
* CF=1 → error, A = code
* DE on exit points at one past the last byte written
*
* ESTEX $46 subfn 2 (putenv):
* in: HL = "NAME=value" (NUL-terminated)
* out: CF=1 / A = error code on failure
*
* We hand back a pointer into a private 128-byte buffer for getenv().
* Caller must copy the bytes before the next getenv() call if they
* need to outlive it.
*/
#include <stdint.h>
#include <sprinter.h>
static char env_buf[128];
char *getenv(const char *name) __naked
{
(void)name;
__asm
push ix
;; HL = name on entry; we need to also load DE = env_buf.
ld de, #_env_buf
ld bc, #0x0146 ; ESTEX ENVIRON; subfn: getenv
rst #0x10
pop ix
jr c, _getenv_err
or a, a
jr Z, _getenv_miss
ld de, #_env_buf
ret
_getenv_err:
call __errno_set
_getenv_miss:
ld de, #0
ret
__endasm;
}
int putenv(const char *namevalue) __naked
{
(void)namevalue;
__asm
push ix
ld bc, #0x0246 ; ESTEX ENVIRON; subfn: setenv
rst #0x10
pop ix
jr c, _putenv_err
ld de, #0
ret
_putenv_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX $46 subfn 0 (sysenv):
* in: HL = caller-owned buffer
* out: caller's buffer is filled with one NUL-terminated "NAME=value"
* per env var, then a trailing extra NUL marks the end:
* "PATH=...\0SOLID=H\0\0"
* return: buf on success, -1 on error (errno set).
* Buffer must be large enough for the whole environment.
*/
char *sysenv(char *buf) __naked
{
(void)buf;
__asm
push ix
push hl ; stash buffer pointer (= return value)
ld bc, #0x0046 ; ESTEX ENVIRON; subfn 0 = sysenv
rst #0x10
pop de ; DE = buffer pointer
pop ix
ret nc ; success: DE already has buf
;; CF=1 A = ESTEX error code.
call __errno_set
ld de, #-1
ret
__endasm;
}
+77
View File
@@ -0,0 +1,77 @@
/*
* errno.c — strerror / perror over the SDCC-provided `errno` global.
*
* The message table mirrors the one in solid-c's IO.ASM (we kept the
* English wording for grep-ability). ESTEX returns codes 0..32 in the
* meaningful range; anything beyond gets "Unknown error".
*
* Note: we deliberately do NOT define `_errno` here — SDCC's
* z80.lib/errno.rel provides it (a single int in _DATA), and our libc
* wrappers (read.c, open.c, etc.) just assign to `errno`. This
* removes the "multiple definition of _errno" link warning.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
/*
* Stored verbatim — pointers in the lookup table cost 2 bytes each plus
* the message bytes themselves. Sentinel "" entries pad out gaps so
* indexing stays direct.
*/
static const char *const messages[] = {
/* 0 */ "No error",
/* 1 */ "Invalid function",
/* 2 */ "Invalid drive number",
/* 3 */ "File not found",
/* 4 */ "Path not found",
/* 5 */ "Invalid handle",
/* 6 */ "Too many open files",
/* 7 */ "File already exists",
/* 8 */ "File is read-only",
/* 9 */ "Root directory overflow",
/* 10 */ "No free space",
/* 11 */ "Directory not empty",
/* 12 */ "Can't delete current directory",
/* 13 */ "Invalid media",
/* 14 */ "Unknown operation",
/* 15 */ "Directory exists",
/* 16 */ "Invalid filename",
/* 17 */ "Invalid EXE file",
/* 18 */ "Not supported EXE file",
/* 19 */ "Access denied",
/* 20 */ "Device not ready",
/* 21 */ "Seek error",
/* 22 */ "Sector not found",
/* 23 */ "CRC error",
/* 24 */ "Write protect",
/* 25 */ "Read error",
/* 26 */ "Write error",
/* 27 */ "Drive failure",
/* 28 */ "RESERVED",
/* 29 */ "RESERVED",
/* 30 */ "Out of memory",
/* 31 */ "Invalid memory block",
/* 32 */ "Unknown error",
};
const int ESTEX_MAX_ERR = sizeof(messages) / sizeof(messages[0]) - 1;
const char *strerror(int err)
{
if (err < 0 || err > ESTEX_MAX_ERR) {
err = EUNKERR;
}
return messages[err];
}
void perror(const char *prefix)
{
if (prefix && *prefix) {
fputs(prefix, stderr);
fputs(": ", stderr);
}
fputs(strerror(errno), stderr);
fputs("\r\n", stderr);
}
+88
View File
@@ -0,0 +1,88 @@
/*
* fsdir.c — directory operations via ESTEX:
* $1B MKDIR — create directory (HL = path)
* $1C RMDIR — remove empty directory (HL = path)
* $1D CHDIR — change current directory (HL = path)
* $1E CURDIR — read current directory path (HL = 256-byte buffer)
*
* All four return CF=1 + A=error on failure. We surface that as errno
* with a -1 (or NULL for getcwd) return value, matching POSIX.
*/
#include <unistd.h>
#include <errno.h>
int mkdir(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x1B ; ESTEX MKDIR; HL already = path
rst #0x10
pop ix
jr c, _mk_err
ld de, #0
ret
_mk_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
int rmdir(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x1C ; ESTEX RMDIR; HL already = path
rst #0x10
pop ix
jr c, _rm_err
ld de, #0
ret
_rm_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
int chdir(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x1D ; ESTEX CHDIR; HL already = path
rst #0x10
pop ix
jr c, _cd_err
ld de, #0
ret
_cd_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
char *getcwd(char *buf, size_t size) __naked
{
(void)buf; (void)size;
__asm
;; HL = buf, DE = size (ignored ESTEX always wants 256 bytes).
push ix
push hl ; preserve buf across RST
ld c, #0x1E ; ESTEX CURDIR
rst #0x10
pop hl ; restore buf
pop ix
jr c, _gc_err
ex de, hl ; return buf via DE (SDCC ptr return)
ret
_gc_err:
call __errno_set
ld de, #0 ; NULL on error
ret
__endasm;
}
+71
View File
@@ -0,0 +1,71 @@
/*
* lseek — 32-bit file position via ESTEX MOVE_FP ($15).
*
* ESTEX MOVE_FP: A=handle, B=whence (0=SET, 1=CUR, 2=END),
* HL=offset high16, IX=offset low16
* → HL:IX = new absolute position, CF=err
*
* SDCC __sdcccall(1) on z80 for `long lseek(int fd, long offset, int whence)`:
* - fd → HL (1st 16-bit arg in register)
* - offset → stack as two 16-bit words (low first, then high)
* - whence → stack (top after offset)
* - 32-bit return: DE = low16, HL = high16
* - caller-pops (caller-side `pop af; pop af; pop af` after the call)
*
* IX is saved (caller frame pointer).
*/
#include <unistd.h>
long lseek(int fd, long offset, int whence) __naked
{
(void)fd; (void)offset; (void)whence;
__asm
push ix ; save caller-side IX
;; Layout after push:
;; SP+0..1 = saved IX
;; SP+2..3 = ret addr
;; SP+4..5 = offset low16
;; SP+6..7 = offset high16
;; SP+8..9 = whence
ld a, l ; A = fd low byte (was in HL)
;; Walk through 5 consecutive stack bytes via HL cheaper than
;; IY-indexed because `ld r,(hl); inc hl` (2B/13T per byte) beats
;; `ld r, n(iy)` (3B/19T) for sequential reads.
ld hl, #4
add hl, sp ; HL offset_low
ld e, (hl)
inc hl
ld d, (hl) ; DE = offset_low
push de
pop ix ; IX = offset_low
inc hl
ld e, (hl)
inc hl
ld d, (hl) ; DE = offset_high
inc hl
ld b, (hl) ; B = whence (low byte)
ex de, hl ; HL = offset_high (ESTEX wants it here)
ld c, #0x15 ; ESTEX MOVE_FP
rst #0x10
jr c, _lseek_err
;; Returns HL:IX = new position. Convert to SDCC long return (DE:HL).
push ix
pop de ; DE = low16 (was IX)
;; HL already has high16
pop ix ; restore caller-side IX
ret
_lseek_err:
call __errno_set
ld hl, #0xFFFF
ld de, #0xFFFF ; long -1
pop ix
ret
__endasm;
}
+305
View File
@@ -0,0 +1,305 @@
/*
* mouse.c — Sprinter mouse driver wrappers (RST 30h).
*
* All calls use the same pattern as ESTEX (push/pop IX around the RST)
* since the driver doesn't promise to preserve registers either.
*/
#include <mouse.h>
/* Scratch for READ_STATE — RST 30h clobbers HL/DE/A so we can't keep the
* state pointer in HL across the call. Explicit `= 0` so SDCC reserves
* real BSS storage — see memory/sdcc_static_storage_gotcha.md. */
static uint16_t mb_x = 0, mb_y = 0;
static uint8_t mb_buttons = 0;
int mouse_init(void) __naked
{
__asm
push ix
ld c, #0x00 ; INITIALIZATION
rst #0x30 ; MOUSE
pop ix
jr c, _mi_err
ld de, #0
ret
_mi_err:
ld de, #-1
ret
__endasm;
}
void mouse_show(void) __naked
{
__asm
push ix
ld c, #0x01 ; SHOW MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_hide(void) __naked
{
__asm
push ix
ld c, #0x02 ; HIDE MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_refresh(void) __naked
{
__asm
push ix
ld c, #0x83 ; MOUSE REFRESH
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_read(mouse_state_t *st) __naked
{
(void)st;
__asm
;; HL = state ptr on entry.
push ix
push hl ; stash ptr across RST
ld c, #0x03 ; READ MOUSE STATE
rst #0x30 ; MOUSE
;; Returns: A=buttons, HL=x, DE=y (CF=err but we ignore here)
ld (_mb_x), hl
ld (_mb_y), de
ld (_mb_buttons), a
pop hl ; restore state ptr
pop ix
;; Copy scratch *st. Struct layout: x(2), y(2), buttons(1).
ld de, (_mb_x)
ld (hl), e
inc hl
ld (hl), d
inc hl
ld de, (_mb_y)
ld (hl), e
inc hl
ld (hl), d
inc hl
ld a, (_mb_buttons)
ld (hl), a
ret
__endasm;
}
void mouse_goto(int x, int y) __naked
{
(void)x; (void)y;
__asm
;; HL = x, DE = y.
push ix
ld c, #0x04 ; GOTO MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_bounds_x(int xmin, int xmax) __naked
{
(void)xmin; (void)xmax;
__asm
;; HL = xmin, DE = xmax.
push ix
ld c, #0x08 ; HORZ BOUNDS
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_bounds_y(int ymin, int ymax) __naked
{
(void)ymin; (void)ymax;
__asm
;; HL = ymin, DE = ymax.
push ix
ld c, #0x07 ; VERT BOUNDS
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
/* ---- $09 LOAD CURSOR + $0B RETURN CURSOR ---------------------- */
/* Scratch for the IX-passing convention — RST 30h takes the bitmap
* pointer in IX, so we have to set it up explicitly.
*
* Initialised to 0 so SDCC reserves real BSS storage — uninitialised
* `static uint8_t` declarations can coalesce to a single address and
* stomp on each other. See memory/sdcc_static_storage_gotcha.md. */
static uint16_t mc_image = 0;
static uint8_t mc_width = 0;
static uint8_t mc_height = 0;
static uint8_t mc_hot_x = 0;
static uint8_t mc_hot_y = 0;
/* Saved struct pointer for mouse_get_cursor. SDCC __sdcccall(1) passes
* `c` in HL, then stashes it in DE around the inline asm. Our asm
* clobbers DE (driver returns hot_y/hot_x in D/E) so the post-asm
* `c->width = ...` writes would otherwise land at a garbage address.
* We park the pointer in BSS instead so SDCC re-fetches it from
* memory after the asm. */
static mouse_cursor_t *mc_dest = 0;
void mouse_load_cursor(const mouse_cursor_t *c)
{
/* Copy fields out of the C struct into our scratch globals so the
* asm side has well-known names. */
mc_image = (uint16_t)(uintptr_t)c->image;
mc_width = c->width;
mc_height = c->height;
mc_hot_x = c->hot_x;
mc_hot_y = c->hot_y;
__asm
push ix
ld ix, (_mc_image)
ld a, (_mc_height)
ld h, a
ld a, (_mc_width)
ld l, a
ld a, (_mc_hot_y)
ld d, a
ld a, (_mc_hot_x)
ld e, a
ld b, #0
ld c, #0x09 ; LOAD MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
__endasm;
}
void mouse_get_cursor(mouse_cursor_t *c)
{
mc_dest = c; /* park ptr in BSS */
mc_image = (uint16_t)(uintptr_t)c->image;
__asm
push ix
ld ix, (_mc_image) ; IX = bitmap buffer from caller
ld c, #0x0B ; RETURN CURSOR
rst #0x30 ; mouse driver, NOT ESTEX
;; Returns: H=height, L=width, D=hot_y, E=hot_x.
ld a, h
ld (_mc_height), a
ld a, l
ld (_mc_width), a
ld a, d
ld (_mc_hot_y), a
ld a, e
ld (_mc_hot_x), a
pop ix
__endasm;
/* Re-fetch the struct pointer from BSS — `c` (kept in DE by SDCC
* around the inline asm) was clobbered by the RST 30h above. */
mouse_cursor_t *p = mc_dest;
p->width = mc_width;
p->height = mc_height;
p->hot_x = mc_hot_x;
p->hot_y = mc_hot_y;
}
/* ---- $0E / $0F SENSITIVITY ------------------------------------ */
/* GET returns H=vert, L=horz in HL. We expose the two halves as
* separate getters so the simple "uint8_t" return ABI works cleanly. */
static int ms_query(void) __naked
{
__asm
push ix
ld c, #0x0E ; GET SENSITIVITY
rst #0x30 ; MOUSE
ld d, h
ld e, l
pop ix
ret
__endasm;
}
uint8_t mouse_get_sensitivity_x(void)
{
return (uint8_t)(ms_query() & 0xFF); /* E = horz */
}
uint8_t mouse_get_sensitivity_y(void)
{
return (uint8_t)(ms_query() >> 8); /* D = vert */
}
void mouse_set_sensitivity(uint8_t horz, uint8_t vert) __naked
{
(void)horz; (void)vert;
/* Pack into HL: H=vert, L=horz. */
__asm
push ix
ld h, l
ld l, a
ld c, #0x0F ; SET SENSITIVITY
rst #0x30 ; MOUSE
pop ix
__endasm;
}
/* ---- $81 CHANGE VIDEO MODE ------------------------------------ */
/* SDCC __sdcccall(1): single uint8_t arg arrives in A. */
void mouse_video_mode_changed(uint8_t mode) __naked
{
(void)mode;
__asm
push ix
;; A already holds the mode byte (from SDCC ABI).
ld c, #0x81 ; CHANGE VIDEO MODE
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
/* CURSOR_TEXT_MODES ($0A):
* B = 0
* H = AND symbol mask L = XOR symbol mask
* D = AND attribute mask E = XOR attribute mask
*
* SDCC __sdcccall(1) gives us:
* sym_and in L (low byte of HL arg)
* sym_xor in E (low byte of DE arg)
* attr_and at [SP+2]
* attr_xor at [SP+3]
*/
void mouse_text_cursor(uint8_t sym_and, uint8_t sym_xor,
uint8_t attr_and, uint8_t attr_xor) __naked
{
(void)sym_and; (void)sym_xor; (void)attr_and; (void)attr_xor;
__asm
;; SDCC __sdcccall(1) for 4×uint8_t args:
;; arg1 sym_and A
;; arg2 sym_xor L
;; arg3 attr_and stack low byte (packed into HL.L on caller, push HL)
;; arg4 attr_xor stack high byte (HL.H pushed by caller)
pop iy ; return address
pop bc ; C = attr_and (low), B = attr_xor (high)
push ix
;; Target: H=sym_and, L=sym_xor, D=attr_and, E=attr_xor, B=0
ld h, a ; H = sym_and (from A)
; L already holds sym_xor
ld d, c ; D = attr_and
ld e, b ; E = attr_xor
ld b, #0
ld c, #0x0A ; CURSOR TEXT MODE
rst #0x30 ; MOUSE
pop ix
jp (iy)
__endasm;
}
+158
View File
@@ -0,0 +1,158 @@
/*
* open / creat / close — ESTEX file-handle primitives.
*
* The interesting part is the open() flag state machine. We expose POSIX
* flag bits (O_RDONLY/O_WRONLY/O_RDWR + O_CREAT/O_EXCL/O_TRUNC/O_APPEND)
* and dispatch onto the three ESTEX entry points:
*
* $11 OPEN — open existing file
* $0A CREATE — create (truncate if exists), return new handle
* $0B CREATE_NEW — create only if file does not exist
*
* Three private __naked wrappers do the raw RST 10h calls; the public
* open() / creat() / close() are plain C orchestrators.
*
* IX is saved across every RST 10h. Failures set errno and return -1.
*/
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/* ---- raw ESTEX wrappers ---------------------------------------------- */
/* ESTEX $11 OPEN: A=mode (1=R, 2=W, 0=R/W), HL=path → A=handle, CF=err. */
static int _estex_open_raw(const char *path, int posix_mode) __naked
{
(void)path; (void)posix_mode;
__asm
push ix
;; HL = path, DE = posix_mode. Translate to ESTEX numbering.
ld a, e
and a, #0x03
ld c, #1
or a, a
jr Z, _oopen_real
ld c, #2
dec a
jr Z, _oopen_real
ld c, #0
_oopen_real:
ld a, c
ld c, #0x11 ; ESTEX OPEN
rst #0x10
pop ix
jr c, _oopen_err
ld e, a
ld d, #0
ret
_oopen_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX $0A CREATE: A=attr, HL=path → A=handle, CF=err.
* Truncates an existing file. */
static int _estex_create_raw(const char *path) __naked
{
(void)path;
__asm
push ix
xor a, a ; A = 0 (normal attribute)
ld c, #0x0A
rst #0x10
pop ix
jr c, _ocreat_err
ld e, a
ld d, #0
ret
_ocreat_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX $0B CREATE_NEW: A=attr, HL=path → A=handle, CF=err.
* Fails (errno=EEXIST) if file already exists. */
static int _estex_create_new_raw(const char *path) __naked
{
(void)path;
__asm
push ix
xor a, a
ld c, #0x0B
rst #0x10
pop ix
jr c, _ocreatn_err
ld e, a
ld d, #0
ret
_ocreatn_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ---- public surface --------------------------------------------------- */
int open(const char *path, int flags)
{
int fd;
if (flags & O_CREAT) {
if (flags & O_EXCL) {
/* Must not already exist. */
fd = _estex_create_new_raw(path);
} else if (flags & O_TRUNC) {
/* Always create or truncate. */
fd = _estex_create_raw(path);
} else {
/* Open if it exists, otherwise create. */
fd = _estex_open_raw(path, flags);
if (fd < 0 && errno == ENOENT) {
fd = _estex_create_raw(path);
}
}
} else {
fd = _estex_open_raw(path, flags);
}
if (fd < 0) {
return -1;
}
if (flags & O_APPEND) {
/* Position at end of file so future writes append. */
(void)lseek(fd, 0L, SEEK_END);
}
return fd;
}
int creat(const char *path, int mode)
{
(void)mode; /* Sprinter has no per-file permission bits */
return open(path, O_WRONLY | O_CREAT | O_TRUNC);
}
int close(int fd) __naked
{
(void)fd;
__asm
push ix
ld a, l
ld c, #0x12
rst #0x10
pop ix
jr c, _oclose_err
ld de, #0
ret
_oclose_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
+143
View File
@@ -0,0 +1,143 @@
/*
* posix_time.c — minimal POSIX <time.h> implementation on top of
* getdatetime() (ESTEX SYSTIME $21).
*
* SDCC's z80.lib bundles time/localtime/mktime AND _RtcRead in a
* single time.rel module, so the user can't override _RtcRead from a
* separate object — overriding triggers a "multiple definition"
* linker error. We sidestep that by implementing the whole POSIX time
* API ourselves; the linker then never pulls SDCC's time.rel.
*
* The epoch is Unix (1970-01-01 00:00:00). No timezone support —
* gmtime and localtime are identical. No DST.
*/
#include <stdio.h>
#include <time.h>
static const unsigned char mdays[12] = {
31,28,31,30,31,30,31,31,30,31,30,31
};
static const char *const dnames[7] = {
"Sun","Mon","Tue","Wed","Thu","Fri","Sat"
};
static const char *const mnames[12] = {
"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"
};
static int is_leap(unsigned int y)
{
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
/* Days elapsed from 1970-01-01 to (y, 1, 1). */
static unsigned long year_days(unsigned int y)
{
unsigned long d = 0;
for (unsigned int i = 1970; i < y; i++)
d += is_leap(i) ? 366 : 365;
return d;
}
/* Days from Jan 1 to month start (1-based month input). */
static unsigned int month_days(unsigned int y, unsigned int m)
{
unsigned int d = 0;
for (unsigned int i = 0; i < m - 1; i++) d += mdays[i];
if (m > 2 && is_leap(y)) d++;
return d;
}
time_t time(time_t *t)
{
datetime_t dt;
getdatetime(&dt);
unsigned long days = year_days(dt.year)
+ month_days(dt.year, dt.month)
+ (dt.day - 1);
time_t epoch = days * 86400UL
+ (unsigned long)dt.hour * 3600UL
+ (unsigned long)dt.minute * 60UL
+ dt.second;
if (t) *t = epoch;
return epoch;
}
/* localtime and gmtime share one static buffer — caller copies if
* needed across further calls (matches POSIX behaviour). */
static struct tm tm_buf;
struct tm *gmtime(time_t *timep)
{
unsigned long sec = *timep;
tm_buf.tm_sec = (unsigned char)(sec % 60); sec /= 60;
tm_buf.tm_min = (unsigned char)(sec % 60); sec /= 60;
tm_buf.tm_hour = (unsigned char)(sec % 24); sec /= 24;
/* sec is now days since 1970-01-01 (Thursday). */
tm_buf.tm_wday = (unsigned char)((4 + sec) % 7);
/* find year */
unsigned int y = 1970;
unsigned long days = sec;
while (days >= (unsigned long)(is_leap(y) ? 366 : 365)) {
days -= is_leap(y) ? 366 : 365;
y++;
}
tm_buf.tm_year = (int)y - 1900;
tm_buf.tm_yday = (int)days;
/* find month/day */
unsigned int m = 0;
while (m < 12) {
unsigned int dim = mdays[m] + ((m == 1) && is_leap(y) ? 1u : 0u);
if (days < dim) break;
days -= dim;
m++;
}
tm_buf.tm_mon = (unsigned char)m;
tm_buf.tm_mday = (unsigned char)(days + 1);
tm_buf.tm_isdst = 0;
tm_buf.tm_hundredth = 0;
return &tm_buf;
}
struct tm *localtime(time_t *timep)
{
return gmtime(timep); /* no timezone */
}
time_t mktime(struct tm *tm)
{
unsigned int y = (unsigned int)(tm->tm_year + 1900);
unsigned long days = year_days(y)
+ month_days(y, (unsigned int)tm->tm_mon + 1)
+ (unsigned int)(tm->tm_mday - 1);
time_t epoch = days * 86400UL
+ (unsigned long)tm->tm_hour * 3600UL
+ (unsigned long)tm->tm_min * 60UL
+ tm->tm_sec;
/* Backfill wday/yday so callers can inspect them. */
tm->tm_wday = (unsigned char)((4 + days) % 7);
tm->tm_yday = (int)month_days(y, (unsigned int)tm->tm_mon + 1)
+ (tm->tm_mday - 1);
return epoch;
}
/* "Day Mon DD HH:MM:SS YYYY\n" — 25 chars + NUL. */
static char asctime_buf[26];
char *asctime(struct tm *tm)
{
sprintf(asctime_buf, "%s %s %2d %02d:%02d:%02d %d\n",
dnames[tm->tm_wday % 7],
mnames[tm->tm_mon % 12],
tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec,
tm->tm_year + 1900);
return asctime_buf;
}
char *ctime(time_t *timep)
{
return asctime(localtime(timep));
}
+67
View File
@@ -0,0 +1,67 @@
/*
* read / write — bulk transfer through ESTEX file handles.
*
* ESTEX READ ($13) / WRITE ($14):
* A = handle, HL = buffer, DE = byte count
* → DE = bytes actually transferred, CF = err with code in A.
*
* SDCC __sdcccall(1) for 3-arg int functions uses callee-pops for the
* stack-passed argument; this implementation mirrors the pattern used
* by SDCC's own z80.lib _memset.
*
* On error: sets errno, returns -1.
*/
#include <unistd.h>
int read(int fd, void *buf, size_t n) __naked
{
(void)fd; (void)buf; (void)n;
__asm
pop iy ; IY = return address
pop bc ; BC = n (stack arg)
ld a, l ; A = handle
ex de, hl ; HL = buf
ld d, b
ld e, c ; DE = n
push ix
push iy ; preserve return addr across RST
ld c, #0x13 ; ESTEX READ
rst #0x10
pop iy
pop ix
jr c, _read_err
;; DE already holds count read.
jp (iy)
_read_err:
call __errno_set
ld de, #-1
jp (iy)
__endasm;
}
int write(int fd, const void *buf, size_t n) __naked
{
(void)fd; (void)buf; (void)n;
__asm
pop iy
pop bc
ld a, l
ex de, hl
ld d, b
ld e, c
push ix
push iy
ld c, #0x14
rst #0x10
pop iy
pop ix
jr c, _write_err
jp (iy)
_write_err:
call __errno_set
ld de, #-1
jp (iy)
__endasm;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* sleep — block for N seconds using the 50 Hz frame interrupt.
*
* Sprinter ISR fires 50 times per second. `halt` parks the CPU until
* the next IRQ, so 50 halts = ~1 second of wall clock. This is the
* same trick solid-c uses in IO.ASM:285.
*
* Note: requires interrupts to be enabled (they are by default — ESTEX
* sets up IM 1 with the frame ISR before our program runs).
*/
#include <unistd.h>
void sleep(unsigned int seconds) __naked
{
(void)seconds;
__asm
inter:
;; HL = seconds on entry (SDCC single int arg).
ld a, h
or a, l
ret Z ; sleep(0) return immediately
ld b, #50 ; 50 halts per second (50 Hz interrupt)
inner:
halt
djnz inner
dec hl
jr inter
__endasm;
}
+29
View File
@@ -0,0 +1,29 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strlwr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'A' && *p <= 'Z') *p += 'a' - 'A';
p++;
}
return s;
}
char *strupr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'a' && *p <= 'z') *p -= 'a' - 'A';
p++;
}
return s;
}
/* div() comes from SDCC's z80.lib. */
+190
View File
@@ -0,0 +1,190 @@
/*
* stat.c — POSIX stat() and fstat() over ESTEX metadata.
*
* fstat(fd, &st) -> ESTEX GET_D_T ($17) for mtime + lseek/SEEK_END
* for size.
* stat(path, &st) -> open(O_RDONLY) + fstat() + close. This works
* for any regular file; directory paths fail at
* open() — F_FIRST-based stat had unreliable
* semantics for exact filenames.
*
* Sprinter / DSS doesn't track POSIX owner/group/inode, so we synth a
* minimal mode (S_IFREG | rw user perm).
*/
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <dir.h>
/* ESTEX GET_D_T: A=fd, C=$17 → D=day, E=month, IX=year, H=hour, L=min,
* B=sec. CF=1 + A=errcode on failure.
*
* Writes directly into *out using `ex (sp), hl` to swap the saved out
* pointer with HL=hour:min after RST — no static scratch needed.
* `out->dow` is left untouched (GET_D_T doesn't return it). */
static int get_dt_for_handle(int fd, datetime_t *out) __naked
{
(void)fd; (void)out;
__asm
;; __sdcccall(1): fd in HL (low byte), out in DE.
push ix ; save caller IX
push de ; stash out pointer
ld a, l ; A = fd
ld c, #0x17 ; ESTEX GET_D_T
rst #0x10
jr c, _gdt_err
;; D=day E=month IX=year H=hour L=min B=sec
ex (sp), hl ; TOS<->HL: HL=out, TOS=hour:min
ld (hl), d ; +0 day
inc hl
ld (hl), e ; +1 month
inc hl
push ix ; year onto stack
pop de ; DE = year
ld (hl), e ; +2 year low
inc hl
ld (hl), d ; +3 year high
inc hl
pop de ; D=hour E=min (from earlier ex (sp))
ld (hl), d ; +4 hour
inc hl
ld (hl), e ; +5 min
inc hl
ld (hl), b ; +6 sec (+7 dow left untouched)
pop ix ; restore caller IX
ld de, #0
ret
_gdt_err:
pop hl ; discard stashed out pointer
pop ix ; restore caller IX
call __errno_set
ld de, #-1
ret
__endasm;
}
int fstat(int fd, struct stat *buf)
{
/* Size via lseek trick. */
long cur = lseek(fd, 0L, SEEK_CUR);
if (cur < 0) return -1;
long end = lseek(fd, 0L, SEEK_END);
if (end < 0) return -1;
(void)lseek(fd, cur, SEEK_SET);
buf->st_size = (uint32_t)end;
/* Date/time via ESTEX. */
datetime_t ft;
if (get_dt_for_handle(fd, &ft) < 0) return -1;
{
struct tm tm;
tm.tm_sec = ft.second;
tm.tm_min = ft.minute;
tm.tm_hour = ft.hour;
tm.tm_mday = ft.day;
tm.tm_mon = (unsigned char)(ft.month - 1);
tm.tm_year = (int)ft.year - 1900;
tm.tm_isdst = 0;
tm.tm_hundredth = 0;
buf->st_mtime = mktime(&tm);
}
buf->st_mode = S_IFREG | S_IRUSR | S_IWUSR;
return 0;
}
/* Convert ESTEX DOS-style date+time to time_t epoch (used by stat()
* when going through F_FIRST for directory entries). */
static time_t dos_to_epoch(uint16_t date, uint16_t dtime)
{
struct tm tm;
tm.tm_sec = (unsigned char)((dtime & 0x1F) << 1);
tm.tm_min = (unsigned char)((dtime >> 5) & 0x3F);
tm.tm_hour = (unsigned char)((dtime >> 11) & 0x1F);
tm.tm_mday = (unsigned char)(date & 0x1F);
tm.tm_mon = (unsigned char)(((date >> 5) & 0x0F) - 1);
tm.tm_year = (int)((date >> 9) & 0x7F) + 80; /* DOS year base = 1980 */
tm.tm_isdst = 0;
tm.tm_hundredth = 0;
return mktime(&tm);
}
/* Returns 1 if path is "." or "..", else 0. Reads at most 3 bytes;
* NULL-safe. ~28 bytes / 34117 T-states depending on input. */
static char is_dot_or_dotdot(const char *path) __naked
{
(void)path;
__asm
ld a, h
or a, l
jr Z, _idd_fail ; NULL 0
ld a, (hl)
sub a, #0x2E
jr NZ, _idd_fail ; path[0] != '.'
inc hl
ld a, (hl)
or a, a
jr Z, _idd_ok ; ".\0" 1
sub a, #0x2E
jr NZ, _idd_fail ; path[1] not '.' and not '\0'
inc hl
ld a, (hl)
or a, a
jr Z, _idd_ok ; "..\0" 1
_idd_fail:
xor a, a
ret
_idd_ok:
ld a, #1
ret
__endasm;
}
int stat(const char *path, struct stat *buf)
{
/* Regular file: open + fstat. */
int fd = open(path, O_RDONLY);
if (fd >= 0) {
int r = fstat(fd, buf);
close(fd);
return r;
}
int saved = errno;
/* Try ffirst directly — works for ordinary subdirectories. */
ffblk_t ffb;
if (ffirst(path, &ffb, FA_DIREC) == 0 && (ffb.found_attr & FA_DIREC)) {
buf->st_size = ffb.size;
buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
buf->st_mode = S_IFDIR | S_IRWXU;
return 0;
}
/* Verified 2026-05-29: ESTEX F_FIRST rejects bare "." and ".." with
* EINAME (16), same as open(). But they DO appear in the "*.*"
* directory listing with FA_DIREC. Iterate to find them. */
if (is_dot_or_dotdot(path)) {
if (ffirst("*.*", &ffb, FA_DIREC) == 0) {
do {
if (strcmp(ffb.found_name, path) == 0) {
buf->st_size = ffb.size;
buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
buf->st_mode = S_IFDIR | S_IRWXU;
return 0;
}
} while (fnext(&ffb) == 0);
}
/* Last-resort synthetic entry — FS variant didn't expose them. */
buf->st_mode = S_IFDIR | S_IRWXU;
buf->st_size = 0;
buf->st_mtime = 0;
return 0;
}
errno = saved;
return -1;
}
+93
View File
@@ -0,0 +1,93 @@
/*
* time.c — getdatetime / setdatetime via ESTEX SYSTIME ($21) / SETTIME ($22).
*
* $21 SYSTIME → D=day E=month IX=year H=hour L=min B=sec C=dow
* $22 SETTIME D=day E=month IX=year H=hour L=min B=sec → CF=err / A=errcode
*
* Struct layout (datetime_t, 8 bytes):
* +0 day, +1 month, +2..3 year, +4 hour, +5 min, +6 sec, +7 dow
*
* Both routines write/read *dt directly — no static scratch. The key
* trick in getdatetime is `ex (sp), hl` after RST: HL holds hour:min,
* stack TOS holds dt — one byte swaps them, then we walk the struct
* via HL. setdatetime uses IX as the struct pointer (re-loaded with
* year just before RST since that's what SETTIME expects).
*/
#include <time.h>
#include <errno.h>
void getdatetime(datetime_t *dt) __naked
{
(void)dt;
__asm
push ix ; save caller IX (BIOS clobbers it)
push hl ; stash dt across RST (clobbers HL)
ld c, #0x21 ; ESTEX SYSTIME
rst #0x10
;; Returns: D=day E=month IX=year H=hour L=min B=sec C=dow
;; ex (sp), hl: TOS<->HL. Now HL = dt, TOS = hour:min stash.
ex (sp), hl
ld (hl), d ; +0 day
inc hl
ld (hl), e ; +1 month
inc hl
push ix ; year stack
pop de ; DE = year
ld (hl), e ; +2 year low
inc hl
ld (hl), d ; +3 year high
inc hl
pop de ; D = hour, E = min (from earlier ex(sp))
ld (hl), d ; +4 hour
inc hl
ld (hl), e ; +5 min
inc hl
ld (hl), b ; +6 sec
inc hl
ld (hl), c ; +7 dow
pop ix ; restore caller IX
ret
__endasm;
}
int setdatetime(const datetime_t *dt) __naked
{
(void)dt;
__asm
push ix ; save caller IX
push hl
pop ix ; IX = dt
ld d, (hl) ; +0 D = day
inc hl
ld e, (hl) ; +1 E = month
inc hl
ld c, (hl) ; +2 year low
inc hl
ld b, (hl) ; +3 year high (BC->IX)
inc hl
push bc
ld b, (hl) ; +4 H = hour
inc hl
ld c, (hl) ; +5 L = min (BC->HL)
inc hl
push bc
ld b, (hl) ; +6 B = sec
pop hl
pop ix
ld c, #0x22 ; ESTEX SETTIME
rst #0x10
pop ix ; restore caller IX
jr c, _st_err2
ld de, #0
ret
_st_err2:
call __errno_set
ld de, #-1
ret
__endasm;
}
+24
View File
@@ -0,0 +1,24 @@
/*
* unlink — remove a file via ESTEX DELETE ($0E).
* HL = ASCIIZ path; CF=err with code in A. Sets errno on failure.
*/
#include <unistd.h>
int unlink(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x0E ; ESTEX DELETE (HL = file name, A = attribure file)
rst #0x10
pop ix
jr c, _unlink_err
ld de, #0
ret
_unlink_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
+46
View File
@@ -0,0 +1,46 @@
/*
* videomode_raw.c — low-level ESTEX SETVMOD / GETVMOD ($50 / $51).
*
* Plain getters/setters with NO mode-class validation. Used by both
* conio (text-validated public API) and gfx (graphics modes). Lives
* in its own .c so a pure graphics program does not pull in the entire
* conio module to switch modes.
*
* Public conio functions in conio.c wrap these with a text-mode check;
* gfx_init / gfx_done in gfx_core.c call them directly.
*/
#include <stdint.h>
#include <errno.h>
uint8_t _videomode_raw_get(void) __naked
{
__asm
push ix
ld c, #0x51 ; ESTEX GETVMOD
rst #0x10
pop ix
;; uint8_t returns in A ESTEX already put mode there.
ret
__endasm;
}
int _videomode_raw_set(uint8_t mode) __naked
{
(void)mode;
__asm
;; SDCC __sdcccall(1) passes uint8_t in A leave it there.
push ix
ld bc, #0x0050 ; ESTEX SETVMOD (B=0 (page), C=0x50)
rst #0x10
jr c, _vmr_err
ld de, #0
pop ix
ret
_vmr_err:
call __errno_set
ld de, #-1
pop ix
ret
__endasm;
}