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:
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 / 34–117 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user