Add mdview markdown viewer, reorganize tests/examples and libc layout
- Split tests/ (libc feature tests) and examples/ (real apps); shared
app.mk in repo root, was examples/example.mk
- libc/io/* split into libc/{conio,env,errno,file,mouse,string,sys,
time,video}/ — clearer module boundaries
- New examples/mdview/: markdown viewer (Phases 1-5 + light nested
lists). Headers (H1-H4), HR, ulist/olist/quote with nesting via
leading spaces, fenced code blocks, inline emphasis (bold/italic/
underscore/code), wrap/unwrap mode with soft wrap (F2), horizontal
pan (← →) with '>' truncation indicator
- libc additions: scroll() in conio (ESTEX SCROLL), strlwr/strupr,
gets() test
- Makefile updates across tests/ for the new shared app.mk path
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* _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;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,629 +0,0 @@
|
||||
/*
|
||||
* 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];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
-305
@@ -1,305 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* 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. */
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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