Add full compiler toolchain, libc, examples and reference docs

First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.

What's in here:
  bin/sprinter-cc        — driver script invoking SDCC + linker + mkexe
  libc/                  — Sprinter-specific libc layer over ESTEX/BIOS
                           (conio, gfx, io, mem, stdio + headers)
  runtime/               — crt0 variants (default/small/banked/minimal)
                           + heap + bank trampolines
  toolchain/             — mkexe (SprintEXE packer, C + tests)
  examples/              — 30 demo programs (gfx, file I/O, env, time, …)
  lib/Makefile           — builds the libc archive (sprinter.lib)
  docs/                  — converted Sprinter manuals + asm reference samples
  third_party/           — solid-c reference compiler dump + sdcc setup script
  release_docs/          — packaging / release notes

gitignore overhaul:
  • Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
    and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
    on macOS APFS).  Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
  • Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
    INPUT fixtures, not build outputs.
  • Collapse the duplicated SDCC/C/Sdcc sections into one section per
    concern (build outputs / vendored / OS-junk).
  • Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:13:21 +03:00
parent f542608b3f
commit c71e249a4e
404 changed files with 75155 additions and 58 deletions
+106
View File
@@ -0,0 +1,106 @@
/*
* gfx_16.c — 16-colour (mode 0x82) public drawing API.
*
* Raw primitives live in gfx_raw_16.c. Single-shot wrappers do one
* W3-begin / one W3-end around their raw call; composites wrap many
* raw calls in a single begin/end pair so the W3 dance is amortised.
*
* No vertical-accelerator path: in 16-colour mode each byte spans two
* horizontal pixels, so a vertical Fill burst would also affect the
* other column's nibble — vline16 falls back to per-row RMW.
*/
#include <gfx.h>
#include <stdint.h>
extern void _gfx_w3_video_begin(void);
extern void _gfx_w3_video_end(void);
extern void _gfx_putpixel16_raw(int x, int y, uint8_t color);
extern void _gfx_hline16_raw (int x, int y, int len, uint8_t color);
extern void _gfx_vline16_raw (int x, int y, int len, uint8_t color);
extern void _gfx_clear16_raw (uint8_t color);
void gfx_clear16(uint8_t color)
{
_gfx_w3_video_begin();
_gfx_clear16_raw(color);
_gfx_w3_video_end();
}
void gfx_putpixel16(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16 || (unsigned)y >= GFX_HEIGHT_16) return;
_gfx_w3_video_begin();
_gfx_putpixel16_raw(x, y, color);
_gfx_w3_video_end();
}
void gfx_hline16(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_hline16_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_vline16(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_vline16_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_rect16(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
_gfx_w3_video_begin();
_gfx_hline16_raw(x, y, w, color);
_gfx_hline16_raw(x, y + h - 1, w, color);
if (h > 2) {
_gfx_vline16_raw(x, y + 1, h - 2, color);
_gfx_vline16_raw(x + w - 1, y + 1, h - 2, color);
}
_gfx_w3_video_end();
}
void gfx_fill_rect16(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
/* Column-major vlines — user's earlier request: 16-color rect_fill
* via vertical lines. Each column does its own per-row RMW; the
* outer begin/end wraps the whole rect so W3 is mapped once. */
_gfx_w3_video_begin();
for (int xx = 0; xx < w; xx++)
_gfx_vline16_raw(x + xx, y, h, color);
_gfx_w3_video_end();
}
void gfx_line16(int x0, int y0, int x1, int y1, uint8_t color)
{
if (y0 == y1) {
int x = x0 <= x1 ? x0 : x1;
int w = (x0 <= x1 ? x1 - x0 : x0 - x1) + 1;
gfx_hline16(x, y0, w, color);
return;
}
if (x0 == x1) {
int y = y0 <= y1 ? y0 : y1;
int h = (y0 <= y1 ? y1 - y0 : y0 - y1) + 1;
gfx_vline16(x0, y, h, color);
return;
}
/* Bresenham — single W3 setup around the whole loop. */
int dx = x1 - x0; int sx = dx < 0 ? -1 : 1; if (dx < 0) dx = -dx;
int dy = y1 - y0; int sy = dy < 0 ? -1 : 1; if (dy < 0) dy = -dy;
int err = (dx > dy ? dx : -dy) / 2;
int x = x0, y = y0;
_gfx_w3_video_begin();
for (;;) {
_gfx_putpixel16_raw(x, y, color);
if (x == x1 && y == y1) break;
int e2 = err;
if (e2 > -dx) { err -= dy; x += sx; }
if (e2 < dy) { err += dx; y += sy; }
}
_gfx_w3_video_end();
}
+118
View File
@@ -0,0 +1,118 @@
/*
* gfx_256.c — 256-colour (mode 0x81) public drawing API.
*
* Single-shot wrappers do one W3-begin / one W3-end around their raw
* call. Composites (rect, fill_rect, line) wrap a single begin/end
* around many raw calls so the W3 dance is paid once per operation,
* not once per pixel/byte.
*/
#include <gfx.h>
#include <stdint.h>
/* From gfx_raw_common.c — DI + map _gfx_bank into W3, save previous. */
extern void _gfx_w3_video_begin(void);
extern void _gfx_w3_video_end(void);
/* From gfx_raw_256.c — W3-naive primitives. */
extern void _gfx_putpixel256_raw(int x, int y, uint8_t color);
extern void _gfx_hline256_raw (int x, int y, int len, uint8_t color);
extern void _gfx_vline256_raw (int x, int y, int len, uint8_t color);
extern void _gfx_clear256_raw (uint8_t color);
void gfx_clear256(uint8_t color)
{
_gfx_w3_video_begin();
_gfx_clear256_raw(color);
_gfx_w3_video_end();
}
void gfx_putpixel256(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
_gfx_w3_video_begin();
_gfx_putpixel256_raw(x, y, color);
_gfx_w3_video_end();
}
void gfx_hline256(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_hline256_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_vline256(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_vline256_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_rect256(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
_gfx_w3_video_begin();
_gfx_hline256_raw(x, y, w, color);
_gfx_hline256_raw(x, y + h - 1, w, color);
if (h > 2) {
_gfx_vline256_raw(x, y + 1, h - 2, color);
_gfx_vline256_raw(x + w - 1, y + 1, h - 2, color);
}
_gfx_w3_video_end();
}
void gfx_fill_rect256(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
/* Pick the orientation with fewer accelerator bursts. Each burst
* paints up to 256 contiguous bytes (horizontal) or up to 256
* vertical pixels in one column.
* row-major (hlines): h × ceil(w/256) bursts
* col-major (vlines): w × ceil(h/256) bursts — for h ≤ 256 = w
* Vertical wins for tall-narrow rects; horizontal for short-wide. */
int h_bursts = h * ((w + 255) >> 8);
int v_bursts = w * ((h + 255) >> 8);
_gfx_w3_video_begin();
if (h_bursts <= v_bursts) {
for (int yy = 0; yy < h; yy++)
_gfx_hline256_raw(x, y + yy, w, color);
} else {
for (int xx = 0; xx < w; xx++)
_gfx_vline256_raw(x + xx, y, h, color);
}
_gfx_w3_video_end();
}
void gfx_line256(int x0, int y0, int x1, int y1, uint8_t color)
{
/* Orthogonal lines route to the accelerator. */
if (y0 == y1) {
int x = x0 <= x1 ? x0 : x1;
int w = (x0 <= x1 ? x1 - x0 : x0 - x1) + 1;
gfx_hline256(x, y0, w, color);
return;
}
if (x0 == x1) {
int y = y0 <= y1 ? y0 : y1;
int h = (y0 <= y1 ? y1 - y0 : y0 - y1) + 1;
gfx_vline256(x0, y, h, color);
return;
}
/* Bresenham — single W3-setup around the whole loop. */
int dx = x1 - x0; int sx = dx < 0 ? -1 : 1; if (dx < 0) dx = -dx;
int dy = y1 - y0; int sy = dy < 0 ? -1 : 1; if (dy < 0) dy = -dy;
int err = (dx > dy ? dx : -dy) / 2;
int x = x0, y = y0;
_gfx_w3_video_begin();
for (;;) {
_gfx_putpixel256_raw(x, y, color);
if (x == x1 && y == y1) break;
int e2 = err;
if (e2 > -dx) { err -= dy; x += sx; }
if (e2 < dy) { err += dx; y += sy; }
}
_gfx_w3_video_end();
}
+178
View File
@@ -0,0 +1,178 @@
/*
* gfx_core.c — Sprinter graphics: common state + setup/teardown API.
*
* This module owns the variables that select which graphics page is
* visible vs drawn into, which W3 bank is mapped during gfx writes,
* and the row-base CPU address. All mode-specific primitives (256-
* color in gfx_raw_256.c / gfx_256.c, 16-color in gfx_raw_16.c /
* gfx_16.c, text in gfx_text_*.c) read this state via extern.
*
* Public API:
* gfx_init / gfx_done — switch video mode, restore previous
* gfx_set_visible_page / get — ESTEX $54 SELPAGE wrapper + cached value
* gfx_set_draw_page / get — updates _gfx_addr_base for the new page
* gfx_set_bank / get — sets the W3 page byte (0x50..0x5F)
* gfx_wait_vsync — EI; HALT until next frame interrupt
* gfx_pal_load / gfx_pal_set — BIOS $A4 PIC_SET_PAL wrappers
*
* Shared state (extern from this file):
* _gfx_addr_base — 0xC000 for page 0, 0xC140 for page 1. Every
* mode-specific primitive uses this instead of a
* hard-coded 0xC000 so the same code targets the
* currently-selected draw page.
* _gfx_bank — read by _gfx_w3_video_begin in gfx_raw_common.c
* to map the right W3 page.
*/
#include <gfx.h>
#include <stdint.h>
/* From conio's videomode_raw.c — bypasses set_videotextmode's text-only
* validation so gfx_init can move INTO graphics modes. */
extern uint8_t _videomode_raw_get(void);
extern int _videomode_raw_set(uint8_t mode);
/* ---- Shared graphics state --------------------------------------- */
/* Cached values of the SELPAGE state. Page numbers are 0 or 1. */
static uint8_t _gfx_visible_page = 0;
static uint8_t _gfx_draw_page = 0;
/* The W3 page byte (0x50..0x5F) — see memory/sprinter_vram_transparency.md
* for the bit-meanings (0x50 normal, 0x54 temp, 0x58 transparent, 0x5C
* both). Read by _gfx_w3_video_begin in gfx_raw_common.c. */
uint8_t _gfx_bank = 0x50;
/* CPU address of column 0 in the current draw page. Each VRAM row is
* 1024 bytes wide — page 0 occupies bytes 0..319 (CPU 0xC000+), page 1
* occupies 320..639 (CPU 0xC140+), and the remaining 384 bytes hold
* mode descriptors / palette data we don't touch. Updated by
* gfx_set_draw_page; read by every primitive's raw helper. */
uint16_t _gfx_addr_base = 0xC000;
/* ---- gfx_init / gfx_done ----------------------------------------- */
uint8_t gfx_init(uint8_t mode, uint8_t page)
{
uint8_t prev = _videomode_raw_get();
_videomode_raw_set(mode);
_gfx_bank = 0x50;
gfx_set_visible_page(page);
gfx_set_draw_page(page);
return prev;
}
void gfx_done(uint8_t mode)
{
_videomode_raw_set(mode);
}
/* ---- Visible page (ESTEX $54 SELPAGE) ---------------------------- *
*
* Direct OUT to port 0xC9 bit 0 toggles the "screen mode page" register
* but doesn't notify DSS's display bookkeeping, leaving the screen in
* an inconsistent state (one of the swaps shows as black). Going
* through the syscall keeps DSS happy.
*/
void gfx_set_visible_page(uint8_t page)
{
_gfx_visible_page = page & 1;
__asm
push ix
ld a, (__gfx_visible_page)
ld b, a ; B = page 0/1
ld c, #0x54 ; ESTEX SELPAGE
rst #0x10
pop ix
__endasm;
}
uint8_t gfx_get_visible_page(void)
{
return _gfx_visible_page;
}
/* ---- Draw page --------------------------------------------------- */
void gfx_set_draw_page(uint8_t page)
{
_gfx_draw_page = page & 1;
/* Direct constants beat (0xC000 + (cond ? 0x140 : 0)) by 3 Z80
* instructions — SDCC doesn't fold the constant addition. */
_gfx_addr_base = _gfx_draw_page ? 0xC140 : 0xC000;
}
uint8_t gfx_get_draw_page(void)
{
return _gfx_draw_page;
}
/* ---- W3 bank (0x50..0x5F) ---------------------------------------- */
void gfx_set_bank(uint8_t bank)
{
_gfx_bank = bank;
}
uint8_t gfx_get_bank(void)
{
return _gfx_bank;
}
/* ---- Frame sync -------------------------------------------------- *
*
* Block until the next IM2 frame interrupt (50 Hz, programmed by DSS
* for keyboard / cursor maintenance). The Z80's HALT instruction
* sleeps the CPU until the next IRQ, which DSS handles and returns
* to the instruction after HALT — that's the start of the vertical
* retrace window, ideal for a tear-free page swap.
*/
void gfx_wait_vsync(void) __naked
{
__asm
ei
halt
ret
__endasm;
}
/* ---- Palette (BIOS $A4 PIC_SET_PAL) ----------------------------- */
static uint8_t pal_num_;
static uint8_t pal_start_;
static uint8_t pal_count_;
static uint16_t pal_data_;
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *data)
{
pal_num_ = pal_num;
pal_start_ = start;
pal_count_ = count;
pal_data_ = (uint16_t)(uintptr_t)data;
__asm
push ix
ld a, (_pal_start_)
ld e, a ; E = start
ld a, (_pal_count_)
ld d, a ; D = count (0 256)
ld hl, (_pal_data_) ; HL = data
ld b, #0xFF ; mask
ld a, (_pal_num_) ; A = palette number
ld c, #0xA4 ; BIOS PIC_SET_PAL
rst #0x08
pop ix
__endasm;
}
void gfx_pal_set(uint8_t pal_num, uint8_t idx,
uint8_t r, uint8_t g, uint8_t b)
{
uint8_t entry[4];
entry[0] = b;
entry[1] = g;
entry[2] = r;
entry[3] = 0;
gfx_pal_load(pal_num, idx, 1, entry);
}
+63
View File
@@ -0,0 +1,63 @@
/*
* gfx_font.c — bitmap-font management shared by the 256- and 16-colour
* text renderers.
*
* Format (ZX-Spectrum-compatible): 256 glyphs × 8 rows × 1 byte = 2 KB,
* INTERLEAVED row-major —
* offset = row * 256 + char_code
* so row 0 of every glyph occupies bytes 0x000..0x0FF, row 1 occupies
* 0x100..0x1FF, etc. Each row byte is MSB-first (bit 7 = leftmost px).
*
* The default source is BIOS WIN_GET_ZG (fn 0xB8) — the active system
* character generator. Programs may override with gfx_set_font().
*
* Lazy initialisation: gfx_text_256.c / gfx_text_16.c call _gfx_font_ensure
* on first use so a pure-graphics program doesn't pay the BIOS call.
*
* The font pointer and the buffer are exported to the two text renderers
* via _gfx_font_ptr (extern), which always points at valid data once
* _gfx_font_ensure has been called.
*/
#include <gfx.h>
#include <stdint.h>
#define FONT_BYTES 2048
static uint8_t _gfx_font_buf[FONT_BYTES];
const uint8_t *_gfx_font_ptr = _gfx_font_buf;
static uint8_t _gfx_font_loaded = 0;
/* BIOS WIN_GET_ZG (0xB8): DE = destination, returns 2 KB. */
static void bios_get_zg(uint8_t *dest) __naked
{
(void)dest;
__asm
push ix ; BIOS clobbers IX
ex de, hl ; DE = dest (HL was arg)
xor a ; font 0 (default)
ld c, #0xB8
rst #0x08
pop ix
ret
__endasm;
}
void gfx_load_default_font(void)
{
bios_get_zg(_gfx_font_buf);
_gfx_font_ptr = _gfx_font_buf;
_gfx_font_loaded = 1;
}
void gfx_set_font(const uint8_t *font)
{
_gfx_font_ptr = font;
_gfx_font_loaded = 1;
}
/* Called from text renderers — loads the default font on first use. */
void _gfx_font_ensure(void)
{
if (!_gfx_font_loaded) gfx_load_default_font();
}
+186
View File
@@ -0,0 +1,186 @@
/*
* gfx_raw_16.c — 16-colour (mode 0x82) raw primitives.
*
* Pixel format (verified empirically 2026-05-31): each byte at
* 0xC000+xb holds two horizontal pixels —
* high nibble (bits 7-4) = pixel at x = 2*xb (LEFT, even)
* low nibble (bits 3-0) = pixel at x = 2*xb + 1 (RIGHT, odd)
*
* Row addressing matches mode 0x81: Port_Y (0x89) = y, 320 bytes/row,
* 320 × 2 = 640 pixels.
*
* Accelerator: byte-wise — one Fill burst paints up to 256 bytes =
* 512 pixels. For a solid colour the byte value is the nibble in
* both halves: b = c | (c<<4).
*
* RMW pattern for unaligned edges & vertical lines:
* read byte at (HL); mask out target nibble; OR in new nibble; write.
* ~10 instructions per pixel — slow but unavoidable since a byte
* spans two horizontal pixels.
*
* "Raw" = W3-naive: caller wraps a sequence with one
* _gfx_w3_video_begin / _gfx_w3_video_end pair.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
/* ---- Scratch shared across asm helpers ---------------------------- */
static uint8_t g16_y;
static uint8_t g16_byte; /* combined byte: nibble | (nibble<<4) */
static uint8_t g16_nibble; /* color in correct half (low or high) */
static uint8_t g16_mask; /* mask preserving the OTHER half */
static uint8_t g16_len; /* accel block size (0 = 256) */
static uint16_t g16_addr;
/* ---- Accel horizontal Fill burst ---------------------------------- *
*
* Caller has W3 mapped and DI active. Only the block-size byte uses
* SMC. Colour is preloaded into C and shipped via `ld a, c` (0x79).
* Inserting another `ld a, #n` between LD C,C and the firing LD (HL),A
* breaks the burst — the accel FSM re-reads the immediate as a fresh
* block size.
*/
static void g16_hfill_chunk(void) __naked
{
__asm
ld a, (_g16_len)
ld (_g16_h_len_imm), a
ld a, (_g16_byte)
ld c, a
ld a, (_g16_y)
out (#0x89), a
ld hl, (_g16_addr)
ld d, d ; 0x52 set block size
ld a, #0 ; 0x3E nn length (patched)
_g16_h_len_imm = . - 1
ld c, c ; 0x49 horizontal Fill
ld a, c ; 0x79 A = colour byte
ld (hl), a ; fires accel
ld b, b ; 0x40 disable
ret
__endasm;
}
/* ---- RMW one nibble at (g16_addr, g16_y) ------------------------- */
static void g16_rmw_pixel(void) __naked
{
__asm
ld a, (_g16_y)
out (#0x89), a
ld hl, (_g16_addr)
ld a, (_g16_mask)
ld b, a ; B = preserve mask
ld a, (_g16_nibble)
ld c, a ; C = new nibble (in correct half)
ld a, (hl)
and a, b ; clear target nibble
or a, c ; OR in new
ld (hl), a
ret
__endasm;
}
/* ---- Raw primitives (W3-naive, composable) ----------------------- */
void _gfx_putpixel16_raw(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16 || (unsigned)y >= GFX_HEIGHT_16) return;
g16_y = (uint8_t)y;
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
if (x & 1) {
g16_nibble = (uint8_t)(color & 0x0F);
g16_mask = 0xF0;
} else {
g16_nibble = (uint8_t)((color & 0x0F) << 4);
g16_mask = 0x0F;
}
g16_rmw_pixel();
}
void _gfx_hline16_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)y >= GFX_HEIGHT_16) return;
if (x < 0) { len += x; x = 0; }
if (x >= GFX_WIDTH_16) return;
if (x + len > GFX_WIDTH_16) len = GFX_WIDTH_16 - x;
if (len <= 0) return;
g16_y = (uint8_t)y;
uint8_t cnib = color & 0x0F;
g16_byte = (uint8_t)(cnib | (cnib << 4));
/* Leading unaligned pixel: x odd → RIGHT half of left-most byte. */
if (x & 1) {
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
g16_nibble = cnib;
g16_mask = 0xF0;
g16_rmw_pixel();
x++;
len--;
if (len <= 0) return;
}
/* Even x; emit (len/2) full bytes via accel hfill. */
int full = len >> 1;
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
while (full > 0) {
int chunk = full > 256 ? 256 : full;
g16_len = (chunk == 256) ? 0 : (uint8_t)chunk;
g16_hfill_chunk();
full -= chunk;
g16_addr += chunk;
}
/* Trailing odd-length pixel: LEFT half of the next byte. */
if (len & 1) {
g16_nibble = (uint8_t)(cnib << 4);
g16_mask = 0x0F;
g16_rmw_pixel();
}
}
void _gfx_vline16_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16) return;
if (y < 0) { len += y; y = 0; }
if (y >= GFX_HEIGHT_16) return;
if (y + len > GFX_HEIGHT_16) len = GFX_HEIGHT_16 - y;
if (len <= 0) return;
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
if (x & 1) {
g16_nibble = (uint8_t)(color & 0x0F);
g16_mask = 0xF0;
} else {
g16_nibble = (uint8_t)((color & 0x0F) << 4);
g16_mask = 0x0F;
}
for (int i = 0; i < len; i++) {
g16_y = (uint8_t)(y + i);
g16_rmw_pixel();
}
}
/* Clear the whole 640×256 area with `color`. Row-major hfill —
* 256 rows × 2 bursts each (256+64 bytes). */
void _gfx_clear16_raw(uint8_t color)
{
g16_byte = (uint8_t)((color & 0x0F) | ((color & 0x0F) << 4));
for (int y = 0; y < GFX_HEIGHT_16; y++) {
g16_y = (uint8_t)y;
g16_addr = _gfx_addr_base;
g16_len = 0;
g16_hfill_chunk();
g16_addr = (uint16_t)(_gfx_addr_base + 0x100);
g16_len = 64;
g16_hfill_chunk();
}
}
+175
View File
@@ -0,0 +1,175 @@
/*
* gfx_raw_256.c — 256-colour (mode 0x81) raw primitives.
*
* "Raw" = W3-naive: caller must have W3 mapped to the video bank and
* interrupts disabled (typically via _gfx_w3_video_begin/end from
* gfx_raw_common.c). Composite primitives in gfx_256.c wrap one
* begin/end around many raw calls so the W3 dance is amortised across
* the whole drawing operation.
*
* Addressing (mode 0x81): pixel (x, y) lives at CPU 0xC000 + x with
* Port_Y (0x89) = y. _gfx_addr_base is 0xC000 for draw page 0 and
* 0xC140 for draw page 1 — see gfx_core.c.
*
* Accelerator opcodes (docs/converted/accel_r.txt):
* LD D,D (0x52) enter "set block size" mode; the NEXT byte is the
* length operand and MUST follow LD A immediate (0x3E).
* LD C,C (0x49) horizontal Fill mode (LD (HL),A fills n bytes)
* LD E,E (0x5B) vertical Fill mode (auto-increments Port_Y)
* LD B,B (0x40) disable accelerator
*
* The block-length byte uses SMC — we patch the immediate at runtime,
* then run the accel sequence. HOME is RAM after DSS loads us, so
* SMC inside our own .EXE is safe.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
/* ---- Scratch shared across asm helpers --------------------------- *
* Single-threaded — no reentrancy. GFX runs with interrupts off
* between _gfx_w3_video_begin and _gfx_w3_video_end. */
static uint8_t acc_color;
static uint8_t acc_y;
static uint8_t acc_len; /* 0 means 256 — the accel convention */
static uint16_t acc_addr;
/* Putpixel scratch — separate from acc_* so a putpixel inside a
* Bresenham loop doesn't trample on outer accel state. */
static uint8_t _gfx_pp_y;
static uint16_t _gfx_pp_addr;
static uint8_t _gfx_pp_color;
/* ---- inner accel bursts ------------------------------------------ *
*
* Pre: W3 mapped to the video bank, DI active. Caller wraps a sequence
* of bursts in a single begin/end pair. */
/* Horizontal fill: acc_len bytes at acc_addr on row acc_y. */
static void hfill_chunk(void) __naked
{
__asm
;; Patch the LD A,#n immediate (operand byte) with acc_len.
ld a, (_acc_len)
ld (_hfill_len_imm), a
ld a, (_acc_y)
out (#0x89), a ; Port_Y = y
;; Pre-load colour into C and dest into HL before arming accel.
ld a, (_acc_color)
ld c, a
ld hl, (_acc_addr)
;; --- ACCEL SEQUENCE ---
ld d, d ; 0x52 set block size mode
ld a, #0 ; 0x3E nn block size (nn patched above)
_hfill_len_imm = . - 1
ld c, c ; 0x49 horizontal Fill
ld a, c ; 0x79 A = colour (NOT another ld a,#n)
ld (hl), a ; fires accel; fills acc_len bytes
ld b, b ; 0x40 disable
ret
__endasm;
}
/* Vertical fill: acc_len pixels at column acc_addr, top row acc_y.
* The accel auto-increments Port_Y as it paints down the column. */
static void vfill_chunk(void) __naked
{
__asm
ld a, (_acc_len)
ld (_vfill_len_imm), a
ld a, (_acc_y)
out (#0x89), a ; starting Y
ld a, (_acc_color)
ld c, a
ld hl, (_acc_addr)
ld d, d ; 0x52 set block size
ld a, #0 ; immediate length (patched)
_vfill_len_imm = . - 1
ld e, e ; 0x5B vertical Fill
ld a, c ; A = colour
ld (hl), a ; fires accel
ld b, b ; 0x40 disable
ret
__endasm;
}
/* ---- Raw primitives (W3-naive, composable) ----------------------- *
*
* These do NO DI/W3 setup — caller wraps a sequence with one
* _gfx_w3_video_begin / _gfx_w3_video_end pair. */
void _gfx_putpixel256_raw(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
_gfx_pp_y = (uint8_t)y;
_gfx_pp_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
_gfx_pp_color = color;
__asm
ld a, (__gfx_pp_y)
out (#0x89), a
ld hl, (__gfx_pp_addr)
ld a, (__gfx_pp_color)
ld (hl), a
__endasm;
}
void _gfx_hline256_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)y >= GFX_HEIGHT) return;
if (x < 0) { len += x; x = 0; }
if (x >= GFX_WIDTH) return;
if (x + len > GFX_WIDTH) len = GFX_WIDTH - x;
if (len <= 0) return;
acc_color = color;
acc_y = (uint8_t)y;
acc_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
while (len > 0) {
int chunk = len > 256 ? 256 : len;
acc_len = (chunk == 256) ? 0 : (uint8_t)chunk;
hfill_chunk();
len -= chunk;
acc_addr += chunk;
}
}
void _gfx_vline256_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH) return;
if (y < 0) { len += y; y = 0; }
if (y >= GFX_HEIGHT) return;
if (y + len > GFX_HEIGHT) len = GFX_HEIGHT - y;
if (len <= 0) return;
/* GFX_HEIGHT = 256 so a full column is a single accel burst. */
acc_color = color;
acc_y = (uint8_t)y;
acc_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
acc_len = (len == 256) ? 0 : (uint8_t)len;
vfill_chunk();
}
/* Clear the whole 320×256 area with `color`. Row-major hfill:
* 256 rows × 2 bursts each (256-byte + 64-byte) = 512 bursts. */
void _gfx_clear256_raw(uint8_t color)
{
acc_color = color;
for (int y = 0; y < GFX_HEIGHT; y++) {
acc_y = (uint8_t)y;
acc_addr = _gfx_addr_base; /* 256-byte burst */
acc_len = 0;
hfill_chunk();
acc_addr = (uint16_t)(_gfx_addr_base + 256); /* 64-byte burst */
acc_len = 64;
hfill_chunk();
}
}
+44
View File
@@ -0,0 +1,44 @@
/*
* gfx_raw_common.c — W3 page mapping primitives shared by every gfx mode.
*
* Composite primitives (gfx_line / gfx_rect / gfx_fill_rect / ...) bracket
* their inner loop with one `_gfx_w3_video_begin()` / `_gfx_w3_video_end()`
* pair and call the `*_raw` variants inside, so the W3 dance is paid once
* per drawing operation instead of once per pixel. Single-shot wrappers
* (gfx_putpixel, gfx_hline, ...) wrap the same way for their single call.
*
* Begin disables interrupts, saves the current W3 page byte, then maps
* `_gfx_bank` (the current video bank, 0x50..0x5F). End restores the
* saved page and re-enables interrupts.
*
* NOT re-entrant — saving the previous W3 byte in a static is safe only
* because GFX runs with interrupts off between begin and end.
*/
#include <stdint.h>
extern uint8_t _gfx_bank; /* current W3 video bank (gfx_core.c) */
static uint8_t _gfx_saved_w3;
void _gfx_w3_video_begin(void) __naked
{
__asm
di
in a, (#0xE2)
ld (__gfx_saved_w3), a
ld a, (__gfx_bank)
out (#0xE2), a
ret
__endasm;
}
void _gfx_w3_video_end(void) __naked
{
__asm
ld a, (__gfx_saved_w3)
out (#0xE2), a
ei
ret
__endasm;
}
+124
View File
@@ -0,0 +1,124 @@
/*
* gfx_text_16.c — bitmap-font text rendering for mode 0x82
* (640×256×16, 4 bits per pixel).
*
* Per glyph row (8 pixels): emit 4 bytes — each byte packs two
* adjacent pixels as (LEFT<<4) | RIGHT. A precomputed 4-entry pair
* table maps the 2-bit source pattern (LEFT*2 + RIGHT) directly to
* the output byte, avoiding per-pixel mask/OR.
*
* Currently requires byte-aligned x (x must be even); odd x is
* silently ignored. Text grids are typically aligned.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
extern const uint8_t *_gfx_font_ptr;
extern void _gfx_font_ensure(void);
static uint8_t txt_y;
static uint8_t txt_row;
static uint16_t txt_addr;
/* 4-byte lookup: index = LEFT*2 + RIGHT, value = (LEFT<<4)|RIGHT. */
static uint8_t txt_pair[4];
/* Render one 8-pixel row using the pair table — 4 byte writes. */
static void render_row_16(void) __naked
{
__asm
di
in a, (#0xE2)
push af
ld a, #0x50
out (#0xE2), a
ld a, (_txt_y)
out (#0x89), a
ld hl, (_txt_addr)
ld a, (_txt_row)
ld c, a ; C = rotating source bits
ld b, #4 ; 4 output bytes
rr16_loop:
;; Extract two top bits of C as index A = LEFT*2 + RIGHT.
sla c ; CY = LEFT bit; C <<= 1
ld a, #0
adc a, a ; A = LEFT
sla c ; CY = RIGHT bit; C <<= 1
adc a, a ; A = LEFT*2 + RIGHT (0..3)
;; A = txt_pair[A]; preserve HL across the lookup.
push hl
ld e, a
ld d, #0
ld hl, #_txt_pair
add hl, de
ld a, (hl)
pop hl
ld (hl), a
inc hl
djnz rr16_loop
pop af
out (#0xE2), a
ei
ret
__endasm;
}
static void build_pair_table(uint8_t fg, uint8_t bg)
{
uint8_t f = fg & 0x0F;
uint8_t b = bg & 0x0F;
/* HIGH nibble = LEFT, LOW nibble = RIGHT (sprinter_graphics convention). */
txt_pair[0] = (uint8_t)((b << 4) | b); /* 00 BG BG */
txt_pair[1] = (uint8_t)((b << 4) | f); /* 01 BG FG */
txt_pair[2] = (uint8_t)((f << 4) | b); /* 10 FG BG */
txt_pair[3] = (uint8_t)((f << 4) | f); /* 11 FG FG */
}
void gfx_putchar16(int x, int y, char c, uint8_t fg, uint8_t bg)
{
_gfx_font_ensure();
if ((unsigned)x >= GFX_WIDTH_16 || (unsigned)y >= GFX_HEIGHT_16) return;
if (x & 1) return;
build_pair_table(fg, bg);
uint8_t cc = (uint8_t)c;
uint16_t base = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
for (int r = 0; r < 8; r++) {
int yy = y + r;
if ((unsigned)yy >= GFX_HEIGHT_16) break;
txt_y = (uint8_t)yy;
txt_addr = base;
txt_row = _gfx_font_ptr[r * 256 + cc];
render_row_16();
}
}
void gfx_text16(int x, int y, const char *s, uint8_t fg, uint8_t bg)
{
/* Build pair table once for the whole string (FG/BG don't change). */
_gfx_font_ensure();
if (x & 1) return;
build_pair_table(fg, bg);
for (; *s; s++) {
if (x >= GFX_WIDTH_16) break;
if ((unsigned)y >= GFX_HEIGHT_16) break;
uint8_t cc = (uint8_t)*s;
uint16_t base = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
for (int r = 0; r < 8; r++) {
int yy = y + r;
if ((unsigned)yy >= GFX_HEIGHT_16) break;
txt_y = (uint8_t)yy;
txt_addr = base;
txt_row = _gfx_font_ptr[r * 256 + cc];
render_row_16();
}
x += 8;
}
}
+90
View File
@@ -0,0 +1,90 @@
/*
* gfx_text_256.c — bitmap-font text rendering for mode 0x81
* (320×256×256, one byte per pixel).
*
* Per glyph row (8 pixels): emit 8 bytes — FG where the source bit is
* 1, BG where 0. Port_Y is set once per row; the row writer wraps its
* own DI / W3 save+restore so callers don't need to.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
extern const uint8_t *_gfx_font_ptr;
extern void _gfx_font_ensure(void);
/* ---- Scratch shared with the asm row writer ---------------------- */
static uint8_t txt_y;
static uint8_t txt_row; /* current glyph-row bit pattern */
static uint8_t txt_fg;
static uint8_t txt_bg;
static uint16_t txt_addr; /* VRAM address for this row */
/* Render one 8-pixel row. Self-contained DI / W3 save+restore so a
* gfx_text() can be called from any context without extra ceremony. */
static void render_row_256(void) __naked
{
__asm
di
in a, (#0xE2)
push af
ld a, #0x50
out (#0xE2), a
ld a, (_txt_y)
out (#0x89), a
ld hl, (_txt_addr)
ld a, (_txt_fg)
ld d, a
ld a, (_txt_bg)
ld e, a
ld a, (_txt_row)
ld b, #8 ; 8 pixels in this row
rr256_loop:
rla ; CY = MSB, A <<= 1
jr nc, rr256_bg
ld (hl), d ; FG
jr rr256_next
rr256_bg:
ld (hl), e ; BG
rr256_next:
inc hl
djnz rr256_loop
pop af
out (#0xE2), a
ei
ret
__endasm;
}
void gfx_putchar256(int x, int y, char c, uint8_t fg, uint8_t bg)
{
_gfx_font_ensure();
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
txt_fg = fg;
txt_bg = bg;
uint8_t cc = (uint8_t)c;
uint16_t base = (uint16_t)(_gfx_addr_base + (unsigned)x);
for (int r = 0; r < 8; r++) {
int yy = y + r;
if ((unsigned)yy >= GFX_HEIGHT) break;
txt_y = (uint8_t)yy;
txt_addr = base;
/* Interleaved layout: row r of char cc lives at font[r*256 + cc]. */
txt_row = _gfx_font_ptr[r * 256 + cc];
render_row_256();
}
}
void gfx_text256(int x, int y, const char *s, uint8_t fg, uint8_t bg)
{
for (; *s; s++) {
if (x >= GFX_WIDTH) break;
gfx_putchar256(x, y, *s, fg, bg);
x += 8;
}
}
+154
View File
@@ -0,0 +1,154 @@
/*
* conio.h — direct console I/O backed by ESTEX functions.
*
* Two-set output API following the Turbo-C convention:
*
* stdio (fast, no attr — system default colour):
* putchar(c) — one char + '\n' → CR LF translation
* puts(s) — string + trailing newline
* printf(...) — uses putchar internally
*
* conio (slower, applies g_text_attr — set via textcolor / textbackground /
* textattr / set_text_attr):
* putch(c) — one char, NO '\n' translation
* cputs(s) — string, NO trailing newline, NO '\n' translation
* (write "\r\n" yourself for line breaks)
* cprintf(...) — printf with attribute, via vsprintf+cputs internally
*
* The conio set short-circuits to the fast path when
* g_text_attr == KEEP_EXIST_ATTR — useful for "I usually want a
* specific colour but right now don't care".
*
* Other helpers (unchanged):
* kbhit / getch / getche — keyboard
* clrscr / clrscr_attr — clear screen
* gotoxy / wherex / wherey
* wrchar / rdchar — direct VRAM cell access
* get_videotextmode / set_videotextmode (text modes only — see gfx.h
* for graphics mode constants)
*
* Coordinates are 0-based to match ESTEX directly.
*/
#ifndef CONIO_H
#define CONIO_H
#include <stdint.h>
char kbhit (void);
char getch (void);
char getche(void);
char putch (char c);
char cputs (const char *s);
int cprintf(const char *fmt, ...);
void clrscr(void);
void gotoxy(uint8_t x, uint8_t y);
/* Solid-C compatibility helpers. */
#define home() gotoxy(0, 0)
#define inp(port) z80_inp(port)
#define outp(p, v) z80_outp((p), (v))
#define enable() __asm__("ei")
#define disable() __asm__("di")
uint8_t z80_inp(uint8_t port);
void z80_outp(uint8_t port, uint8_t value);
/* Read a line from the console (no echo control — uses getche).
* `buf[0]` must be the max length; on return `buf[1]` is the actual
* length and `buf[2..]` is the NUL-terminated string. */
char *cgets(char *buf);
/* Cursor query via ESTEX $53 CURSOR. Returns 0-based row/column. */
uint8_t wherex (void);
uint8_t wherey (void);
uint16_t wherexy(void); // high byte = Y, low byte = X coords.
/* Direct character/attribute screen access (ESTEX $57 / $58).
* wrchar — write char + attribute at (x, y); does NOT advance the cursor
* and does NOT interpret control characters. Useful for
* coloured text and for painting the last-column-last-row cell
* without triggering ESTEX's auto-scroll on PCHARS/PUTCHAR.
* rdchar — read both character and attribute back; returned as
* (attr<<8 | ch). */
void wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr);
uint16_t rdchar(uint8_t x, uint8_t y);
/* clrscr_attr — wipe the entire 80x32 screen using the given attribute
* byte (fill character = space). Companion to clrscr() which uses the
* default attr 0x0F (bright white on black). */
void clrscr_attr(uint8_t attr);
/* Text video-mode control (ESTEX $50 / $51). These constants and helpers
* cover ONLY text modes; graphics modes live in <gfx.h> as GFX_MODE_*.
* `set_videotextmode()` validates that the argument is a known text mode
* (so calling code that includes conio.h alone cannot accidentally switch
* the screen into graphics — that requires <gfx.h>). */
#define TEXT_MODE_40x32 0x02
#define TEXT_MODE_80x32 0x03
uint8_t get_videotextmode(void);
int set_videotextmode(uint8_t mode); /* 0 OK, -1 + errno on bad mode */
/* ------------------------------------------------------------------ *
* Text-output attribute (used by the conio set: putch / cputs / cprintf).
*
* textcolor(c) — set foreground (preserves background+blink)
* textbackground(c) — set background (preserves foreground+blink)
* textattr(a) — replace the whole attribute byte
* set_text_attr(a) — alias to textattr but returns the previous value
* get_text_attr() — read current attribute
*
* Range 0x00..0xFF is a real attribute byte; KEEP_EXIST_ATTR (0xFFFF)
* means "don't touch attributes — fall through to the fast no-attr path"
* (so putch becomes equivalent to putchar etc.).
*
* Default at startup = 0x0F (bright white on black).
*
* NOTE: stdio's putchar / puts / printf IGNORE this — they always use
* whatever ESTEX has cached for the cursor cell. Use the conio set
* (putch / cputs / cprintf) for coloured output.
* ------------------------------------------------------------------ */
#define KEEP_EXIST_ATTR 0xFFFF
void textcolor(uint8_t fg);
void textbackground(uint8_t bg);
void textattr(uint8_t attr);
int16_t set_text_attr(int16_t attr); /* returns the previous value */
int16_t get_text_attr(void);
/* Control how putch / cputs / cprintf (the WRCHAR path) treat
* control characters (< 0x20):
*
* mode = 0 (default) — interpret BS/TAB/LF/CR:
* 0x08 BS → pc_col-- (clamped at 0)
* 0x09 TAB → pc_col rounded up to next multiple of 8 (clamped 80)
* 0x0A LF → pc_row++ (clamped at 32; no glyph emitted)
* 0x0D CR → pc_col = 0
*
* mode = 1 — print EVERY character as a CP437 glyph (no
* interpretation; useful e.g. for drawing box-drawing
* characters that overlap the 0x00..0x1F range).
*
* Only affects the WRCHAR path (attr ≤ 0xFF). When attr is
* KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS rule the cursor and
* pc_raw_mode is irrelevant. */
void set_putch_raw_mode(uint8_t mode);
uint8_t get_putch_raw_mode(void);
/* Sprinter text-mode 03h attribute byte (verified via attr_probe):
* bits 0..3 = foreground (0..15)
* bits 4..6 = background (0..7)
* bit 7 = blink (toggles fg between fg-colour and bg-colour)
* Colour order is standard CGA / Borland-conio.h. Constants 0..7 are
* usable for both fg and bg; 8..15 are foreground-only. */
enum {
COLOR_BLACK = 0, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN,
COLOR_RED, COLOR_MAGENTA, COLOR_BROWN, COLOR_LIGHTGRAY,
COLOR_DARKGRAY, COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN,
COLOR_LIGHTRED, COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE
};
#define COLOR_BLINK 0x80u
#define COLOR(fg, bg) ((uint8_t)((((bg) & 0x07) << 4) | ((fg) & 0x0F)))
#endif
+51
View File
@@ -0,0 +1,51 @@
/*
* dir.h — directory iteration via ESTEX F_FIRST / F_NEXT ($19 / $1A).
*
* ffirst(pattern, &buf, attrib)
* Initialises the search. The pattern is a DOS wildcard like
* `*.TXT` or `DATA\\*`. attrib is the search mask (use FA_NORMAL
* to match plain files, OR in FA_DIREC to include subdirectories).
* Returns 0 on success (buf is populated), -1 on failure (errno set).
*
* fnext(&buf)
* Steps to the next matching entry, reusing the same buf. Returns
* 0 on success, -1 when no more files match (errno = ENOENT) or on
* other errors.
*
* The buffer fields below mirror ESTEX's layout exactly so we can hand
* it straight to the kernel. Use `buf.found_name` (NUL-terminated DOS
* "name.ext" form) to display results.
*/
#ifndef DIR_H
#define DIR_H
#include <stdint.h>
/* File attribute bits (ESTEX / DOS standard). */
#define FA_NORMAL 0x00
#define FA_RDONLY 0x01
#define FA_HIDDEN 0x02
#define FA_SYSTEM 0x04
#define FA_LABEL 0x08
#define FA_DIREC 0x10
#define FA_ARCH 0x20
/* 256-byte work buffer for ESTEX F_FIRST / F_NEXT (B=1 mode). */
typedef struct {
char name[8]; /* +0 pattern: 8-byte filename */
char ext[3]; /* +8 pattern: 3-byte extension */
uint8_t attrib; /* +11 search attribute */
char reserved[10]; /* +12 DSS internal state */
uint16_t time; /* +22 time of last write */
uint16_t date; /* +24 date of last write */
uint16_t first_cluster; /* +26 first cluster */
uint32_t size; /* +28 file size in bytes */
uint8_t found_attr; /* +32 attribute of the matched file */
char found_name[223]; /* +33 NUL-terminated "name.ext" */
} ffblk_t;
int ffirst(const char *pattern, ffblk_t *buf, uint8_t attrib);
int fnext (ffblk_t *buf);
#endif
+62
View File
@@ -0,0 +1,62 @@
/*
* errno.h — ESTEX error codes + global errno + strerror / perror.
*
* ESTEX functions report errors by setting CF=1 and returning the error
* code in A. Our libc wrappers stash that code into `errno` and return
* -1 (or 0 / NULL where the C type calls for it).
*
* Error numbers match ESTEX (so they round-trip through OS/BIOS calls
* untouched). Where the meaning lines up cleanly with POSIX we also
* expose the POSIX-style name as an alias.
*/
#ifndef ERRNO_H
#define ERRNO_H
/* Process-wide error state. Reset to 0 only by user code — libc never
* clears it. */
extern int errno;
/* Error numbers — direct ESTEX codes. */
#define EOK 0 /* No error */
#define EINVFN 1 /* Invalid function */
#define ENODRV 2 /* Invalid drive number */
#define ENOENT 3 /* File not found — POSIX */
#define ENOPATH 4 /* Path not found */
#define EBADF 5 /* Invalid handle — POSIX */
#define EMFILE 6 /* Too many open files — POSIX */
#define EEXIST 7 /* File already exists — POSIX */
#define EROFS 8 /* File is read-only — POSIX */
#define EROOTFULL 9 /* Root directory overflow */
#define ENOSPC 10 /* No free space — POSIX */
#define ENOTEMPTY 11 /* Directory not empty — POSIX */
#define EBUSY 12 /* Can't delete current directory — POSIX-ish */
#define EMEDIA 13 /* Invalid media */
#define EUNKOP 14 /* Unknown operation */
#define EISDIR 15 /* Directory exists — POSIX */
#define EINAME 16 /* Invalid filename */
#define EINVEXE 17 /* Invalid EXE file */
#define ENOEXEC 18 /* Not supported EXE — POSIX */
#define EACCES 19 /* Permission denied — POSIX */
#define ENOTREADY 20 /* Device not ready */
#define ESEEK 21 /* Seek error — POSIX (ESPIPE) */
#define ENOSECT 22 /* Sector not found */
#define ECRC 23 /* CRC error */
#define EWRPROT 24 /* Write protect */
#define EREAD 25 /* Read error */
#define EWRITE 26 /* Write error */
#define EDRVFAIL 27 /* Drive failure */
#define ENOMEM 30 /* Out of memory — POSIX */
#define EINVMEM 31 /* Invalid memory block */
#define EUNKERR 32 /* Unknown error */
/* POSIX aliases for codes ESTEX doesn't have a direct equivalent for.
* Folded onto the closest existing code so error strings stay sane. */
#define EINVAL EUNKOP /* "Invalid argument" → "Unknown operation" */
/* C99 perror / strerror surface. */
const char *strerror(int err);
void perror (const char *prefix);
#endif
+31
View File
@@ -0,0 +1,31 @@
/*
* fcntl.h — open / creat for Sprinter ESTEX.
*
* POSIX-style flag bits. The low two bits select access mode (matches
* POSIX numbering — RDONLY=0, WRONLY=1, RDWR=2 — and is translated to
* the ESTEX OPEN $11 convention inside open()).
*
* The other flags map onto ESTEX calls like this:
* O_CREAT + O_EXCL → $0B (CREATE_NEW, fails if exists)
* O_CREAT + O_TRUNC → $0A (CREATE, truncates existing)
* O_CREAT alone → try $11 (OPEN); on ENOENT fall back to $0A
* no O_CREAT → $11 (OPEN, fails if missing)
* O_APPEND → after open, $15 lseek(0, SEEK_END)
*/
#ifndef FCNTL_H
#define FCNTL_H
#define O_RDONLY 0
#define O_WRONLY 1
#define O_RDWR 2
#define O_CREAT 0x040
#define O_EXCL 0x080
#define O_TRUNC 0x200
#define O_APPEND 0x400
int open (const char *path, int flags);
int creat(const char *path, int mode); /* mode arg ignored on Sprinter */
#endif
+148
View File
@@ -0,0 +1,148 @@
/*
* gfx.h — Sprinter graphics primitives.
*
* Two main modes:
* GFX_MODE_320x256x256 (0x81) — one byte per pixel, palette of 256
* entries. API functions suffixed _256.
* GFX_MODE_640x256x16 (0x82) — 4 bits per pixel, palette of 16.
* API functions suffixed _16.
*
* Common API (no suffix) covers things that are mode-agnostic:
* gfx_init / gfx_done
* gfx_set_visible_page / gfx_set_draw_page / gfx_set_bank
* gfx_wait_vsync
* gfx_pal_load / gfx_pal_set
* gfx_load_default_font / gfx_set_font
*
* Addressing reminder:
* pixel (x, y) lives at CPU 0xC000 + (x or x/2) with Port_Y (0x89) = y;
* the gfx code maps a 16 KB VRAM page into W3 around every write.
* For double-buffering, page 1 starts 320 bytes later (0xC140).
*
* Palette: BIOS $A4 (RST 8); 4 bytes per entry — B, G, R, pad.
*/
#ifndef GFX_H
#define GFX_H
#include <stdint.h>
/* ESTEX SETVMOD codes — same values as the SETVMOD `A` register. */
#define GFX_MODE_TEXT_40x32 0x02
#define GFX_MODE_TEXT_80x32 0x03
#define GFX_MODE_320x256x256 0x81
#define GFX_MODE_640x256x16 0x82
/* Pixel dimensions of mode 0x81 (320×256, 256 colours). */
#define GFX_WIDTH 320
#define GFX_HEIGHT 256
/* Pixel dimensions of mode 0x82 (640×256, 16 colours). Each byte at
* 0xC000+x_byte holds two pixels: high nibble = LEFT (even-x), low
* nibble = RIGHT (odd-x) — see memory/sprinter_graphics.md. */
#define GFX_WIDTH_16 640
#define GFX_HEIGHT_16 256
#define GFX_COLORS_16 16
/* ---- Setup / teardown -------------------------------------------- *
*
* Switch to `mode`, returning the previous mode for restore. `page`
* (0 or 1) selects the initial graphics screen — both the visible and
* the draw page are set to it, and the W3 bank is reset to 0x50 (the
* canonical visible video page). Programs that don't double-buffer
* just pass 0. */
uint8_t gfx_init(uint8_t mode, uint8_t page);
/* Restore a previously-saved video mode. */
void gfx_done(uint8_t mode);
/* ---- Page selection (double-buffering) and bank control ---------- *
*
* The Sprinter hardware holds two graphics screens. The VISIBLE page
* is what's shown on screen; the DRAW page is where the gfx_* writes
* land. Render the next frame into the hidden page and flip when ready.
*
* "Bank" is the W3 page byte (0x50..0x5F) — bits 2,3 select
* normal/temp/transparent display modes. All gfx_* primitives map
* THIS bank into W3 before touching 0xC000+. Default is 0x50. */
void gfx_set_visible_page(uint8_t page); /* 0 or 1 */
uint8_t gfx_get_visible_page(void);
void gfx_set_draw_page(uint8_t page); /* 0 or 1 */
uint8_t gfx_get_draw_page(void);
void gfx_set_bank(uint8_t bank); /* 0x50..0x5F */
uint8_t gfx_get_bank(void);
/* Block until the next frame interrupt (50 Hz on Sprinter). Uses
* `EI; HALT` — the Z80 sleeps until the next IM2 tick that DSS programs
* for keyboard / cursor handling. Typical use:
*
* gfx_set_draw_page(hidden);
* draw_frame(...);
* gfx_wait_vsync(); // wait for vretrace
* gfx_set_visible_page(hidden); // tear-free flip
*/
void gfx_wait_vsync(void);
/* ---- 320×256×256 (mode 0x81) drawing API -------------------------- *
* Colour args are palette indices 0..255. */
void gfx_clear256 (uint8_t color);
void gfx_putpixel256 (int x, int y, uint8_t color);
void gfx_hline256 (int x, int y, int len, uint8_t color);
void gfx_vline256 (int x, int y, int len, uint8_t color);
void gfx_line256 (int x0, int y0, int x1, int y1, uint8_t color);
void gfx_rect256 (int x, int y, int w, int h, uint8_t color);
void gfx_fill_rect256(int x, int y, int w, int h, uint8_t color);
/* ---- 640×256×16 (mode 0x82) drawing API --------------------------- *
* Colour args are palette indices 0..15. The accelerator works
* byte-wise so vline16 falls back to per-row RMW (a byte spans two
* horizontal pixels). */
void gfx_clear16 (uint8_t color);
void gfx_putpixel16 (int x, int y, uint8_t color);
void gfx_hline16 (int x, int y, int len, uint8_t color);
void gfx_vline16 (int x, int y, int len, uint8_t color);
void gfx_line16 (int x0, int y0, int x1, int y1, uint8_t color);
void gfx_rect16 (int x, int y, int w, int h, uint8_t color);
void gfx_fill_rect16(int x, int y, int w, int h, uint8_t color);
/* ---- Bitmap-font text -------------------------------------------- *
* Font is 256 glyphs × 8 rows × 1 byte (ZX-Spectrum format), 2 KB.
* On first use the default system font is fetched via BIOS WIN_GET_ZG
* (fn 0xB8). gfx_set_font() lets you swap in a custom font (the
* pointer is held — keep the storage alive). */
void gfx_load_default_font(void);
void gfx_set_font(const uint8_t *font);
/* 320×256×256 text: one byte per pixel; advances x by 8 per char. */
void gfx_putchar256(int x, int y, char c, uint8_t fg, uint8_t bg);
void gfx_text256 (int x, int y, const char *s, uint8_t fg, uint8_t bg);
/* 640×256×16 text: 4 bits per pixel; x must be EVEN (byte-aligned). */
void gfx_putchar16 (int x, int y, char c, uint8_t fg, uint8_t bg);
void gfx_text16 (int x, int y, const char *s, uint8_t fg, uint8_t bg);
/* ---- Palette ----------------------------------------------------- *
* Each graphics page has its own palette page (page 0 → palette 0,
* page 1 → palette 1). For seamless double-buffering, load the same
* palette into both. */
/* Load a contiguous block of palette entries.
* pal_num: 0..3 (graphics palettes)
* start: first colour slot (0..255)
* count: number of slots (0 → 256)
* data: pointer to count entries, each formatted (B, G, R, 0). */
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *data);
/* Convenience: set one palette entry from RGB. Internally builds the
* BGR+pad triple and calls gfx_pal_load(pal_num, idx, 1, ...). */
void gfx_pal_set (uint8_t pal_num, uint8_t idx,
uint8_t r, uint8_t g, uint8_t b);
#endif
+113
View File
@@ -0,0 +1,113 @@
/*
* mouse.h — Sprinter mouse driver (RST 30h).
*
* The driver is installed in the system shell. After a successful
* mouse_init(), the driver tracks the hardware and you can query state
* or move/show/hide the cursor on demand.
*
* coordinate units:
* READ_STATE / GOTO take pixel coordinates. In text mode 03h
* (80x32) divide x by 8 and y by 8 (NOT 16) to get char-cell
* position.
*
* buttons bitmask:
* bit 0 = left, bit 1 = right
*/
#ifndef MOUSE_H
#define MOUSE_H
#include <stdint.h>
typedef struct {
uint16_t x; /* pixel coordinate */
uint16_t y; /* pixel coordinate */
uint8_t buttons; /* bit 0 = left, bit 1 = right */
} mouse_state_t;
int mouse_init (void); /* 0 OK, -1 if no driver */
void mouse_show (void);
void mouse_hide (void);
void mouse_refresh(void);
void mouse_read (mouse_state_t *st);
void mouse_goto (int x, int y);
void mouse_bounds_x(int xmin, int xmax);
void mouse_bounds_y(int ymin, int ymax);
/* Text-mode cursor shape (function $0A).
* sym_and / sym_xor — character glyph mask
* attr_and / attr_xor — attribute byte mask
* Default cursor is XOR'd inverse video at the cell. */
void mouse_text_cursor(uint8_t sym_and, uint8_t sym_xor,
uint8_t attr_and, uint8_t attr_xor);
/* ---- Graphics-mode cursor image (functions $09 / $0B) ----------- *
* The driver accepts an opaque bitmap for the cursor in graphics modes.
* Doc does not specify the byte layout — empirically size = width*height
* bytes (1 byte per pixel for 256-colour mode, presumably packed nibbles
* for 16-colour mode). hot_x / hot_y mark the "click point" within the
* cursor image (0,0 = top-left).
*/
typedef struct {
const void *image; /* bitmap data */
uint8_t width;
uint8_t height;
uint8_t hot_x;
uint8_t hot_y;
} mouse_cursor_t;
void mouse_load_cursor(const mouse_cursor_t *c);
/* Read back the current cursor.
* c->image must point to a buffer large enough to hold width*height bytes.
* width, height, hot_x, hot_y are written by the driver. */
void mouse_get_cursor(mouse_cursor_t *c);
/* ---- Sensitivity (functions $0E / $0F) ------------------------- *
* Verified empirically (2026-05-31): the value is a DIVIDER — the
* driver counts that many raw hardware steps from the mouse before
* advancing the cursor by one screen pixel. So **smaller value = more
* sensitive** (cursor moves faster), and larger value = slower cursor.
* (The doc statement "higher = less movement needed" appears to be
* inverted.) Useful starting point: 2 on both axes. */
uint8_t mouse_get_sensitivity_x(void);
uint8_t mouse_get_sensitivity_y(void);
void mouse_set_sensitivity(uint8_t horz, uint8_t vert);
/* ---- Solid-C compatibility ------------------------------------- *
* Solid-C uses shorter `ms_*` names + typed state structs. The semantics
* are identical to our `mouse_*` API. */
#define LEFT_BUTTON 1
#define RIGHT_BUTTON 2
/* MSGSTAT mirrors mouse_state_t — same field order and types. */
typedef mouse_state_t MSGSTAT;
#define ms_init mouse_init
#define ms_show mouse_show
#define ms_hide mouse_hide
#define ms_ref mouse_refresh
#define ms_xbnd mouse_bounds_x
#define ms_ybnd mouse_bounds_y
#define ms_spos mouse_goto
#define mssgpos mouse_goto
#define ms_tcur mouse_text_cursor
#define ms_scur mouse_load_cursor
#define ms_gcur mouse_get_cursor
#define ms_vmod mouse_video_mode_changed
#define ms_ssen mouse_set_sensitivity
#define msgstat(st) mouse_read((st))
/* ---- $81 CHANGE VIDEO MODE ------------------------------------- *
* Call after changing the video mode (set_videotextmode or gfx_init) so
* the driver re-syncs its internal coordinate ranges and cursor image.
*
* `mode` is the byte you switched to — TEXT_MODE_* (from <conio.h>) or
* GFX_MODE_* (from <gfx.h>), e.g. 0x03 for 80×32 text or 0x81 for
* 320×256×256. The driver REQUIRES the new mode in register A —
* passing garbage leaves the cursor mis-configured (text-mode XOR
* pattern applied in graphics, etc.). */
void mouse_video_mode_changed(uint8_t mode);
#endif
+156
View File
@@ -0,0 +1,156 @@
/*
* sprinter.h — low-level Sprinter platform definitions for C programs.
*
* Numbers and behaviour cross-checked against docs/converted/ (IvanMak.txt,
* DiskSyscalls.txt, BIOS_v3.txt, ProgrammerManual.txt).
*/
#ifndef SPRINTER_H
#define SPRINTER_H
#include <stdint.h>
/* ---- I/O ports ----------------------------------------------------- */
/* Memory window page-select ports (write 8-bit physical page number). */
#define PORT_PAGE_W0 0x82 /* window 0: 0x0000-0x3FFF (ESTEX system) */
#define PORT_PAGE_W1 0xA2 /* window 1: 0x4000-0x7FFF (HOME) */
#define PORT_PAGE_W2 0xC2 /* window 2: 0x8000-0xBFFF (stack+heap) */
#define PORT_PAGE_W3 0xE2 /* window 3: 0xC000-0xFFFF (paged) */
/* Graphics ports (used in stage 3+). */
#define PORT_RGADR 0x89 /* graphic Y / Spectrum-page selector */
#define PORT_RGMOD 0xC9 /* bit 0 = active screen page 0/1 */
/* ---- ESTEX RST 10h function numbers -------------------------------- */
/* Full list in docs/converted/DiskSyscalls.txt (DSS v1.6). */
#define ESTEX_VERSION 0x00
#define ESTEX_CHDISK 0x01
#define ESTEX_CURDISK 0x02
#define ESTEX_DSKINFO 0x03
#define ESTEX_CREATE 0x0A
#define ESTEX_CREATE_NEW 0x0B
#define ESTEX_DELETE 0x0E
#define ESTEX_RENAME 0x10
#define ESTEX_OPEN 0x11
#define ESTEX_CLOSE 0x12
#define ESTEX_READ 0x13
#define ESTEX_WRITE 0x14
#define ESTEX_MOVE_FP 0x15
#define ESTEX_ATTRIB 0x16
#define ESTEX_F_FIRST 0x19
#define ESTEX_F_NEXT 0x1A
#define ESTEX_MKDIR 0x1B
#define ESTEX_RMDIR 0x1C
#define ESTEX_CHDIR 0x1D
#define ESTEX_CURDIR 0x1E
#define ESTEX_SYSTIME 0x21
#define ESTEX_WAITKEY 0x30
#define ESTEX_SCANKEY 0x31
#define ESTEX_ECHOKEY 0x32
#define ESTEX_CTRLKEY 0x33
#define ESTEX_SETWIN 0x38
#define ESTEX_SETWIN1 0x39
#define ESTEX_SETWIN2 0x3A
#define ESTEX_SETWIN3 0x3B
#define ESTEX_INFOMEM 0x3C
#define ESTEX_GETMEM 0x3D
#define ESTEX_FREEMEM 0x3E
#define ESTEX_SETMEM 0x3F
#define ESTEX_EXEC 0x40
#define ESTEX_EXIT 0x41
#define ESTEX_WAIT 0x42
#define ESTEX_ENV 0x46 /* env API: B=0 sysenv, B=1 getenv, B=2 putenv */
#define ESTEX_SETVMOD 0x50
#define ESTEX_GETVMOD 0x51
#define ESTEX_LOCATE 0x52
#define ESTEX_CURSOR 0x53
#define ESTEX_SELPAGE 0x54
#define ESTEX_SCROLL 0x55
#define ESTEX_CLEAR 0x56
#define ESTEX_RDCHAR 0x57
#define ESTEX_WRCHAR 0x58
#define ESTEX_WINCOPY 0x59
#define ESTEX_WINREST 0x5A
#define ESTEX_PUTCHAR 0x5B
#define ESTEX_PCHARS 0x5C
#define ESTEX_PRINT 0x5F
/* ---- BIOS RST 8 function numbers ----------------------------------- */
/* Full list in docs/converted/BIOS_v3.txt (BIOS v3.00). */
#define BIOS_EMM_INFO 0xC0 /* HL=total pages, BC=free pages */
#define BIOS_EMM_INIT 0xC1
#define BIOS_EMM_ALLOC 0xC2 /* B=npages -> A=blk_id */
#define BIOS_EMM_FREE 0xC3 /* A=blk_id */
#define BIOS_EMM_GETPAGE 0xC4 /* A=blk_id, B=log -> A=physical */
#define BIOS_EMM_LIST 0xC5
#define BIOS_EMM_PORT_FOR 0xC6 /* A=window -> C=port, B=current page */
#define BIOS_EMM_NEXTPAGE 0xC7
/* ---- Inline paging intrinsics -------------------------------------- */
/*
* Each generates a single "OUT (port), A" + the load of the literal.
* Wrap in DI/EI yourself for graphics-grade timing; for one-shot setup
* the inline form is fine.
*/
__sfr __at PORT_PAGE_W0 _io_page_w0;
__sfr __at PORT_PAGE_W1 _io_page_w1;
__sfr __at PORT_PAGE_W2 _io_page_w2;
__sfr __at PORT_PAGE_W3 _io_page_w3;
static inline void sprinter_page_w0(uint8_t page) { _io_page_w0 = page; }
static inline void sprinter_page_w1(uint8_t page) { _io_page_w1 = page; }
static inline void sprinter_page_w2(uint8_t page) { _io_page_w2 = page; }
static inline void sprinter_page_w3(uint8_t page) { _io_page_w3 = page; }
/* ---- Sprinter-specific debug helpers ------------------------------ */
/* Print one byte as two uppercase hex digits via putchar(). */
void print_hex(uint8_t v);
#ifdef DEBUG_RT
/*
* Runtime diagnostics — exposed only when the program is built with
* `sprinter-cc --debug`. The flag below is set by crt0 before main().
*
* 0 — crt0 did NOT self-allocate a W2 page (tiny mode never needs to,
* and in small mode DSS itself maps W2 when the image > 16 KB).
* 1 — crt0 had to allocate and map a W2 page via ESTEX $3D + $3A
* (small mode with image ≤ 16 KB).
*/
extern uint8_t w2_self_allocated;
#endif
/* ---- Environment (ESTEX $46) -------------------------------------- */
/*
* getenv(name) — return pointer to value, or NULL if not set.
* Returned buffer is ESTEX-owned (shared static);
* copy if needed before the next getenv() call.
* putenv("name=value") — add/replace. Pass "name" with no '=' to remove.
* sysenv(buf) — copy the WHOLE environment into caller's buf,
* as a series of NUL-terminated "NAME=value"
* strings followed by an extra NUL marking end:
* "PATH=...\0SOLID=H\0\0"
* Caller is responsible for sizing buf large
* enough for the entire environment.
*
* Return values:
* getenv: pointer to value (NUL-terminated) on success,
* NULL when the variable is not present (errno unchanged),
* NULL with errno set on real error.
* putenv: 0 on success, -1 on error (errno set).
* sysenv: buf on success, (char*)-1 on error (errno set).
*/
char *getenv(const char *name);
int putenv(const char *namevalue);
char *sysenv(char *buf);
#endif /* SPRINTER_H */
+106
View File
@@ -0,0 +1,106 @@
/*
* sprinter_compat.h — Solid-C compatibility shims.
*
* Pulls in standard headers and adds aliases/macros that Solid-C
* programs expect. Programs targeting Sprinter from Solid-C source
* can `#include <sprinter_compat.h>` and most names "just work".
*/
#ifndef SPRINTER_COMPAT_H
#define SPRINTER_COMPAT_H
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
#include <mouse.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <dir.h>
#include <sprinter_exit.h> /* _exit, atexit */
/* ---- Solid-C types ----------------------------------------------- */
typedef uint8_t BYTE;
typedef uint8_t TINY;
typedef uint8_t BOOL;
typedef uint8_t STATUS;
typedef uint16_t WORD;
typedef int FD;
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#define YES 1
#define NO 0
#endif
#ifndef OK
#define OK 0
#endif
#ifndef ERROR
#define ERROR (-1)
#endif
#ifndef EXIT_SUCCESS
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
#endif
/* Solid-C's uint typedef (we expose it explicitly). Note `uint` may
* already be defined in some environments; guard. */
#ifndef _SOLID_UINT_DEFINED
#define _SOLID_UINT_DEFINED
typedef unsigned uint;
#endif
/* fpoint — 32-bit split file position (used by lseek/tell). */
typedef struct fpoint {
uint16_t low;
uint16_t high;
} f_point;
/* ---- string.h aliases -------------------------------------------- */
/* setmem(ptr, n, byte) → memset(ptr, byte, n) — arg order swapped */
#define setmem(p, n, b) memset((p), (b), (n))
/* movmem(src, dst, n) → memcpy(dst, src, n) — arg order swapped */
#define movmem(s, d, n) memcpy((d), (s), (n))
/* In-place case conversion. */
char *strlwr(char *s);
char *strupr(char *s);
/* Solid-C calls perror's table 'strerr'. */
#define strerr(n) strerror((n))
/* ---- stdlib.h additions ------------------------------------------ */
#define abort() _exit(0xFF)
#ifndef min
#define min(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef max
#define max(a, b) ((a) > (b) ? (a) : (b))
#endif
/* div() and div_t come from SDCC's <stdlib.h> (already included above). */
/* sysenv() is now a real function (libc/io/env.c) that returns the WHOLE
* environment into a caller-provided buffer. Declared in <sprinter.h>. */
/* ---- ctype.h additions ------------------------------------------- */
#define isascii(c) (((unsigned)(c)) < 128)
/* ---- io.h aliases (Solid-C fd shortcuts) ------------------------- */
#define seek(fd, off) lseek((fd), (long)(off), SEEK_SET)
#define tell(fd) ((uint16_t)lseek((fd), 0, SEEK_CUR))
#define remove(name) unlink(name)
/* ---- dir.h alias ------------------------------------------------- */
#define _ffirst ffirst
#endif
+15
View File
@@ -0,0 +1,15 @@
/*
* sprinter_exit.h — exit() / _exit() / atexit() declarations.
*
* SDCC's z80 <stdlib.h> doesn't ship these. Include this header
* (in addition to <stdlib.h>) to get them. Link with libc/io/atexit.c.
*/
#ifndef SPRINTER_EXIT_H
#define SPRINTER_EXIT_H
int atexit(void (*fn)(void));
void exit (int code);
void _exit (int code);
#endif
+37
View File
@@ -0,0 +1,37 @@
/*
* sprinter_mem.h — banking-aware page allocator and bank-data accessors.
*
* For data that doesn't fit in window 2's heap, allocate physical pages
* directly through ESTEX EMM and access them via bank_read / bank_write
* (which swap window 3 internally).
*
* mem_alloc_pages(N) — reserve N contiguous 16 KB pages, return block id.
* mem_free_block(id) — release a previously-allocated block.
* mem_get_page(id, i) — translate (block, page-index) to physical page.
* mem_info(&total, &free) — query EMM about total/free 16 KB pages.
*
* bank_load_byte / bank_store_byte — single-byte access via W3.
* bank_read / bank_write — bulk copy between far page and a near buffer.
*
* The bank_* helpers live in HOME so they are always reachable; they save
* the previous W3 page, swap to the target, do the work, restore W3.
*/
#ifndef SPRINTER_MEM_H
#define SPRINTER_MEM_H
#include <stdint.h>
/* ESTEX EMM allocator wrappers. */
uint8_t mem_alloc_pages(uint8_t n); /* 0 = failure */
void mem_free_block (uint8_t blk_id);
uint8_t mem_get_page (uint8_t blk_id, uint8_t idx);
void mem_info (uint16_t *total, uint16_t *free_pages);
/* Far-page accessors (always-mapped HOME, swap W3 internally). */
uint8_t bank_load_byte (uint8_t phys_page, uint16_t off_in_window);
void bank_store_byte(uint8_t phys_page, uint16_t off_in_window, uint8_t v);
void bank_read (uint8_t phys_page, uint16_t off, void *dst, uint16_t n);
void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n);
#endif
+102
View File
@@ -0,0 +1,102 @@
/*
* stdio.h — extends SDCC's z80 stdio with a minimal FILE * stream API.
*
* The bottom of the stack is the existing POSIX-style fd I/O (open/
* read/write/close/lseek). FILE * is a thin struct wrapping a fd plus
* a few flag bits. No buffering — every fread/fwrite/fputc maps 1:1
* to a syscall. Sprinter's I/O is already char-oriented at the ESTEX
* level, so this stays minimal.
*
* stdin / stdout / stderr are predefined "virtual" FILE * pointers
* that talk to the console via putchar()/getchar() (NOT through a fd).
* That keeps printf-routed output going through our CR/LF mapping.
*/
#ifndef STDIO_H
#define STDIO_H
#include <stdarg.h>
#include <stddef.h> /* size_t */
#include <stdint.h>
#ifndef EOF
#define EOF (-1)
#endif
/* ---- printf family (linked from SDCC's z80.lib) -------------------- */
int printf (const char *, ...);
int sprintf(char *, const char *, ...);
int vprintf(const char *, va_list);
int vsprintf(char *, const char *, va_list);
/* puts / putchar / getchar — overridden by our libc to use ESTEX. */
int puts (const char *);
int putchar(int);
int getchar(void);
/* ---- FILE * (minimal, unbuffered) ---------------------------------- */
/* Internal layout — opaque to user. fd == -1 marks the console
* pseudo-streams (stdin/stdout/stderr). */
typedef struct __FILE {
int fd; /* underlying POSIX fd, or -1 for console */
uint8_t flags; /* see _F_* below */
} FILE;
#define _F_READ 0x01
#define _F_WRITE 0x02
#define _F_APPEND 0x04
#define _F_EOF 0x10
#define _F_ERROR 0x20
#define _F_CONIN 0x40 /* console pseudo-stream — uses getchar() */
#define _F_CONOUT 0x80 /* console pseudo-stream — uses putchar() */
extern FILE *const stdin;
extern FILE *const stdout;
extern FILE *const stderr;
/* fseek whence — same numeric values as SEEK_SET/CUR/END in unistd.h. */
#ifndef SEEK_SET
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
#endif
FILE *fopen (const char *path, const char *mode);
int fclose(FILE *fp);
int fflush(FILE *fp);
int fputc (int c, FILE *fp);
int fgetc (FILE *fp);
int fputs (const char *s, FILE *fp);
char *fgets (char *buf, int n, FILE *fp);
size_t fread (void *ptr, size_t size, size_t nmemb, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp);
int fseek (FILE *fp, long off, int whence);
long ftell (FILE *fp);
void rewind(FILE *fp);
int feof (FILE *fp);
int ferror(FILE *fp);
void clearerr(FILE *fp);
/* Aliases to fputc/fgetc — POSIX says they may be macros. */
#define putc(c, fp) fputc(c, fp)
#define getc(fp) fgetc(fp)
/* Read line from stdin into buf until '\n' or EOF; '\n' is not stored.
* Returns buf, or NULL on EOF with empty input. Solid-C/POSIX semantic.
* Note: dangerous, no length check — caller must size buf appropriately. */
char *gets(char *buf);
/* Solid-C decimal/hex helpers — print uint as decimal/hex without args. */
void hex8 (uint8_t v); /* prints two hex digits */
void hex16(uint16_t v); /* prints four hex digits */
void hex32(uint32_t v); /* prints eight hex digits */
void dec8 (uint8_t v); /* prints up to 3 decimal digits, no padding */
void dec16(uint16_t v); /* prints up to 5 decimal digits */
void dec32(uint32_t v); /* prints up to 10 decimal digits */
#endif
+42
View File
@@ -0,0 +1,42 @@
/*
* sys/stat.h — POSIX-style stat() / fstat() over ESTEX file metadata.
*
* stat(path, &st) queries via ESTEX F_FIRST ($19) using the exact
* name; gets size, attrib, and DOS-format date/time
* that we expand to a struct stat.
* fstat(fd, &st) queries an open handle: size via lseek(SEEK_END)
* trick + date/time via ESTEX GET_D_T ($17).
*
* Sprinter has no inodes / owners / groups, so st_ino / st_uid / st_gid
* are absent. S_ISREG and S_ISDIR are the only meaningful mode tests.
*/
#ifndef SYS_STAT_H
#define SYS_STAT_H
#include <stdint.h>
#include <time.h> /* time_t */
/* File mode bits — POSIX subset (octal as per convention). */
#define S_IFMT 0170000 /* file-type mask */
#define S_IFREG 0100000 /* regular file */
#define S_IFDIR 0040000 /* directory */
#define S_IRUSR 0000400 /* owner read */
#define S_IWUSR 0000200 /* owner write */
#define S_IXUSR 0000100 /* owner execute */
#define S_IRWXU 0000700
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
struct stat {
uint16_t st_mode; /* file type + perms */
uint32_t st_size; /* size in bytes */
time_t st_mtime; /* last-mod time (Unix epoch) */
};
int stat (const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
#endif
+70
View File
@@ -0,0 +1,70 @@
/*
* time.h — system clock access via ESTEX $21 / $22.
*
* getdatetime() — read current date/time into a datetime_t.
* setdatetime() — set system clock from a datetime_t.
* Returns 0 on success, -1 with errno set on failure.
*
* Date/time is the raw Sprinter clock — no epoch conversion, no time_t.
*/
#ifndef TIME_H
#define TIME_H
#include <stdint.h>
typedef struct {
uint8_t day; /* 1..31 */
uint8_t month; /* 1..12 */
uint16_t year; /* full year, e.g. 2026 */
uint8_t hour; /* 0..23 */
uint8_t minute; /* 0..59 */
uint8_t second; /* 0..59 */
uint8_t dow; /* day of week, 1-based: 1=Sun..7=Sat — see DOW_* */
} datetime_t;
/* Sprinter ESTEX SYSTIME dow encoding (verified empirically):
* 1-based, week starts on Sunday — matches DOS INT 21h AH=2A+1. */
#define DOW_SUN 1
#define DOW_MON 2
#define DOW_TUE 3
#define DOW_WED 4
#define DOW_THU 5
#define DOW_FRI 6
#define DOW_SAT 7
void getdatetime(datetime_t *dt);
int setdatetime(const datetime_t *dt);
/* ------------------------------------------------------------------
* POSIX <time.h> API — implemented by SDCC's z80.lib (time.rel etc.).
* We provide RtcRead() in libc/io/time.c which bridges to getdatetime().
*
* Layout of struct tm matches SDCC's z80 ABI exactly (__TIME_UNSIGNED=1
* variant): tm_sec/min/hour/mday/mon/wday/isdst/hundredth are 1 byte,
* tm_year/tm_yday are 16-bit ints. Total 12 bytes.
* ------------------------------------------------------------------ */
struct tm {
unsigned char tm_sec; /* 0..60 */
unsigned char tm_min; /* 0..59 */
unsigned char tm_hour; /* 0..23 */
unsigned char tm_mday; /* 1..31 */
unsigned char tm_mon; /* 0..11 (POSIX, not Sprinter native!) */
int tm_year; /* years since 1900 */
unsigned char tm_wday; /* 0..6 (Sunday=0, POSIX) */
int tm_yday; /* 0..365 */
unsigned char tm_isdst;
unsigned char tm_hundredth; /* SDCC extension; we leave 0 */
};
typedef unsigned long time_t;
time_t time (time_t *t);
struct tm *gmtime (time_t *timep);
struct tm *localtime(time_t *timep);
time_t mktime (struct tm *timeptr);
char *asctime (struct tm *timeptr);
char *ctime (time_t *timep);
#endif
+39
View File
@@ -0,0 +1,39 @@
/*
* unistd.h — POSIX-style file descriptor I/O for Sprinter ESTEX.
*
* Each call maps to a single ESTEX RST 10h function:
* read → $13 write → $14
* close → $12 unlink → $0E
*
* On error every call returns -1 (the ESTEX error code is not exposed yet;
* a future errno mechanism will surface it).
*/
#ifndef UNISTD_H
#define UNISTD_H
#include <stddef.h> /* size_t */
/* lseek whence values (POSIX). */
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
int read (int fd, void *buf, size_t n);
int write(int fd, const void *buf, size_t n);
int close(int fd);
int unlink(const char *path);
long lseek(int fd, long offset, int whence);
/* Block the calling task for `seconds` seconds (50 Hz IRQ-based timer). */
void sleep(unsigned int seconds);
/* Directory operations (ESTEX $1B-$1E). All return 0 on success and -1
* with errno set on failure; getcwd returns the buffer on success or NULL.
* size is ignored — ESTEX always wants a 256-byte buffer. */
int mkdir (const char *path);
int rmdir (const char *path);
int chdir (const char *path);
char *getcwd(char *buf, size_t size);
#endif
+40
View File
@@ -0,0 +1,40 @@
/*
* _errno_set — set `errno` from an ESTEX error code (0..255 in A).
*
* Replaces the inline pattern
* ld (_errno), a
* xor a, a
* ld (_errno+1), a ; 7 bytes per error path
* with a single
* call __errno_set ; 3 bytes per error path
*
* Saves ~4 bytes at every libc error handler that converts an ESTEX
* code into the C-side `errno`. Helper itself is 7 bytes; with 10+
* error paths in our libc the size win is net-positive.
*
* ABI:
* in: A = ESTEX error code (0..255)
* out: HL = A (zero-extended); errno fully overwritten so a prior
* large value (e.g. errno = -1) can't leak its high byte.
* clobbers: HL, AF flags. Caller must not depend on HL afterwards.
*
* Defensive 16-bit store — see chat 2026-06-02: if anyone ever assigns
* errno via C (`errno = -1`), the high byte becomes 0xFF, and a partial
* 8-bit write here would leave that 0xFF in place. Always writing the
* full word keeps errno honest regardless of who set it last.
*/
void _errno_set(unsigned char code) __naked
{
(void)code;
__asm
;; __sdcccall(1): single uint8_t arg already in A.
;; Write the two bytes separately so HL/BC/DE/IX/IY remain
;; untouched. Only A is clobbered: it is the input register,
;; and ABI does not require preserving it across a void call.
ld (_errno), a ; low byte = code
xor a, a
ld (_errno+1), a ; high byte = 0
ret
__endasm;
}
+57
View File
@@ -0,0 +1,57 @@
/*
* atexit + exit + _exit.
*
* atexit(fn) — register fn to be called at normal termination (max 8).
* exit(code) — run the atexit chain in LIFO, then ESTEX EXIT.
* _exit(code) — POSIX raw exit: skip the chain, go straight to ESTEX.
*
* These functions own termination entirely; crt0.s only does inline RST
* 10h #41 when main returns without an explicit exit(). That path skips
* the atexit chain — programs that need handlers should call exit() at
* the bottom of main (or return through a wrapper).
*/
#include <stdlib.h>
#include <sprinter_exit.h>
#define ATEXIT_MAX 8
static void (*atexit_stack[ATEXIT_MAX])(void);
static int atexit_top = 0;
int atexit(void (*fn)(void))
{
if (atexit_top >= ATEXIT_MAX) {
return -1;
}
atexit_stack[atexit_top++] = fn;
return 0;
}
/* exit() — runs the chain, then performs the raw ESTEX EXIT. */
void exit(int code)
{
while (atexit_top > 0) {
void (*fn)(void) = atexit_stack[--atexit_top];
if (fn) {
fn();
}
}
_exit(code); /* falls into the inline-asm raw exit below */
}
/* _exit() — POSIX raw termination, no atexit chain. */
void _exit(int code) __naked
{
(void)code;
__asm
;; HL = code (single int arg).
ld a, l
ld b, a
ld c, #0x41 ; ESTEX EXIT
rst #0x10
;; Should not return.
1$: halt
jr 1$
__endasm;
}
+629
View File
@@ -0,0 +1,629 @@
/*
* conio.c — console I/O wrappers around ESTEX kbd/screen syscalls.
*
* $30 WAITKEY — blocking read, returns scan / ASCII / modifiers
* $31 SCANKEY — non-blocking poll
* $32 ECHOKEY — blocking read + auto-echo to the screen
* $52 LOCATE — set cursor to (D=row, E=col)
* $56 CLEAR — fill a window with (A=char, B=attr)
* $5B PUTCHAR — write single character (CR/LF/scroll handled by ESTEX)
*
* Every RST 10h is bracketed with push/pop IX (caller's frame pointer).
*/
#include <conio.h>
#include <stdint.h>
#include <errno.h>
/* Forward extern — definition is further down (after putch/cputs which
* reference it from asm by linker-symbol name). */
extern int16_t g_text_attr;
char kbhit(void) __naked
{
__asm
push ix
ld c, #0x33 ; ESTEX CTRLKEY peeks without consuming
rst #0x10
pop ix
;; A=0 no key waiting; non-zero there is one.
or a, a
ret z
ld a, #0x01
ret
__endasm;
}
char getch(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY (no echo)
rst #0x10
pop ix
;; ESTEX returns ASCII in E (and copy in A)
ret
__endasm;
}
char getche(void) __naked
{
__asm
push ix
ld c, #0x32 ; ESTEX ECHOKEY (echo to console)
rst #0x10
pop ix
;; ESTEX returns ASCII in E (and copy in A)
ret
__endasm;
}
/* ---- putch / cputs: Turbo-C conio convention ---------------------- *
* Both APPLY the current text attribute (g_text_attr). When attr is
* KEEP_EXIST_ATTR (>0xFF), they short-circuit to the FAST stdio path
* (putchar / puts-like raw PCHARS).
*
* No '\n' to CR LF translation here — Turbo-C cputs/putch require the
* caller to use "\r\n" explicitly. Stdio puts/putchar do translate.
*/
static uint8_t pc_ch = 0;
static uint8_t pc_attr = 0;
static uint8_t pc_row = 0;
static uint8_t pc_col = 0;
/* Controls how _raw_putch treats control characters (< 0x20):
* 0 (default) — BS/TAB/LF/CR are interpreted (no glyph output);
* other chars print as glyphs via WRCHAR.
* 1 — all characters print as glyphs, no interpretation.
*
* Only takes effect on the WRCHAR (attr ≤ 0xFF) path. When
* g_text_attr is KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS handle
* cursor and control chars — pc_raw_mode is irrelevant. */
static uint8_t pc_raw_mode = 0;
void set_putch_raw_mode(uint8_t mode) { pc_raw_mode = mode; }
uint8_t get_putch_raw_mode(void) { return pc_raw_mode; }
/* ---- Internal helpers ------------------------------------------- */
/* Read current cursor into pc_row / pc_col via ESTEX CURSOR ($53). */
static void _get_cursor(void) __naked
{
__asm
push ix
ld c, #0x53 ; ESTEX CURSOR
rst #0x10
ld a, d
ld (_pc_row), a
ld a, e
ld (_pc_col), a
pop ix
ret
__endasm;
}
/* Move cursor to (pc_col, pc_row) via ESTEX LOCATE ($52). */
static void _set_cursor(void) __naked
{
__asm
push ix
ld a, (_pc_row)
ld d, a
ld a, (_pc_col)
ld e, a
ld c, #0x52 ; ESTEX LOCATE
rst #0x10
pop ix
ret
__endasm;
}
/* ESTEX PUTCHAR ($5B) — fast no-attr path; ESTEX handles CR/LF/scroll
* and cursor itself. Used when g_text_attr = KEEP_EXIST_ATTR. */
static char _bios_putchar(char ch) __naked
{
(void)ch;
__asm
;; c in A. push af stashes it across the RST (which clobbers A).
;; push ix ; PUTCHAR не меняет IX
push af
ld c, #0x5B ; ESTEX PUTCHAR
rst #0x10
pop af
;; pop ix
ret
__endasm;
}
/* Raw putch: low-level WRCHAR-based output at (pc_col, pc_row) using
* the given attribute byte (caller has already verified that the high
* byte of g_text_attr is zero — this function takes only the low byte).
* Updates pc_col / pc_row per pc_raw_mode:
*
* pc_raw_mode == 0: BS/TAB/LF/CR are INTERPRETED:
* 0x08 BS → pc_col-- (if not already 0)
* 0x09 TAB → pc_col rounded up to next multiple of 8 (capped 80)
* 0x0A LF → pc_row++ (capped at 32; no glyph)
* 0x0D CR → pc_col = 0
* other → WRCHAR + pc_col++
*
* pc_raw_mode == 1: ALL characters print as glyphs via WRCHAR
* + pc_col++ (including 0x08, 0x09, 0x0A, 0x0D — they render as
* their CP437 glyphs).
*
* WRCHAR itself is suppressed when pc_col ≥ 80 or pc_row ≥ 32 (off-
* screen) — coordinates [0..79] × [0..31] only.
*
* Does NOT call CURSOR / LOCATE — caller is expected to fetch cursor
* once before a sequence of _raw_putch calls and write it back once
* after, so we pay the BIOS overhead per OPERATION instead of per CHAR. */
/* Mode-0 worker: interprets BS/TAB/LF/CR, outputs other chars as glyphs. */
static void _raw_putch_raw0(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch in A, attr in L.
;; Dispatch on control chars while A still holds ch (cp does not
;; modify A). B/C only get loaded on the output path so the
;; ctrl-char paths are cheaper.
cp #0x08
jr z, _rp0_bs
cp #0x09
jr z, _rp0_tab
cp #0x0A
jr z, _rp0_lf
cp #0x0D
jr z, _rp0_cr
;; Anything else (printable or unrecognised ctrl) glyph.
ld c, a ; C = ch (save before A is clobbered)
ld a, (_pc_row)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row (ESTEX WRCHAR convention)
ld a, (_pc_col)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
_rp0_bs:
ld a, (_pc_col)
or a, a
ret z ; already at col 0 no change
dec a
ld (_pc_col), a
ret
_rp0_tab:
ld a, (_pc_col)
and #0xF8 ; floor to mult of 8
add a, #8 ; next mult of 8
cp #80
jr c, _rp0_tab_store
ld a, #80 ; cap at off-screen right
_rp0_tab_store:
ld (_pc_col), a
ret
_rp0_lf:
ld a, (_pc_row)
cp #32
ret nc ; already at bottom edge
inc a
ld (_pc_row), a
ret
_rp0_cr:
xor a, a
ld (_pc_col), a
ret
__endasm;
}
/* Mode-1 worker: every byte goes through WRCHAR as a glyph. */
static void _raw_putch_raw1(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch in A, attr in L.
ld c, a ; C = ch (save)
ld a, (_pc_row)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row
ld a, (_pc_col)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
__endasm;
}
/* PCHARS (no attr) — used by cputs when KEEP_EXIST_ATTR is in effect. */
static void _cputs_pchars(const char *s) __naked
{
(void)s;
__asm
push ix
ld c, #0x5C ; ESTEX PCHARS
rst #0x10
pop ix
ret
__endasm;
}
/* ---- Public putch / cputs --------------------------------------- *
*
* KEEP_EXIST_ATTR (high byte != 0) → fast PUTCHAR/PCHARS through
* ESTEX, which manages its own cursor.
*
* Otherwise → fetch cursor ONCE via CURSOR ($53), run one or many
* _raw_putch calls, write cursor back ONCE via LOCATE ($52). This
* folds the per-char CURSOR/LOCATE pair from the old design into a
* single pair per operation. */
char putch(char ch) __naked
{
(void)ch;
__asm
;; A = ch on entry; char return A.
ld (_pc_ch), a ; stash c (for both return and re-load)
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _putch_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker ABI (2 char/uint8 args): ch in A, attr in L.
ld a, (_g_text_attr) ; A = low byte = attr
ld l, a ; L = attr
ld a, (_pc_raw_mode)
or a, a ; Z = (mode == 0)
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
jr nz, _putch_use_raw1
call __raw_putch_raw0
jr _putch_after_raw
_putch_use_raw1:
call __raw_putch_raw1
_putch_after_raw:
call __set_cursor
ld a, (_pc_ch) ; return value
ret
_putch_fast:
ld a, (_pc_ch)
call __bios_putchar ; __bios_putchar keeps AF
ret
__endasm;
}
char cputs(const char *s) __naked
{
(void)s;
__asm
;; HL = s on entry; char return A.
;; NULL-check: cputs(NULL) return 0 immediately.
ld a, h
or a, l
ret z
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _cputs_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker walks the string via DE; HL is used to carry attr in L
;; across iterations (RST 10 inside worker clobbers it, so we
;; push/pop hl around each call).
ex de, hl ; DE = s
ld a, (_g_text_attr) ; A = attr (low byte)
ld l, a ; L = attr (worker ABI: arg2 in L)
;; Pick worker once based on pc_raw_mode; IX = function pointer.
ld a, (_pc_raw_mode)
or a, a
jr z, _cputs_use_raw0
ld ix, #__raw_putch_raw1
jr _cputs_loop
_cputs_use_raw0:
ld ix, #__raw_putch_raw0
_cputs_loop:
ld a, (de)
or a, a
jr z, _cputs_loop_end
inc de
;; Z80 has no "call (ix)" emulate via push-of-ret + jp (ix).
push de ; save string pointer
push hl ; save attr (in L)
ld de, #_cputs_after_worker
push de ; push return address
jp (ix) ; "call" worker
_cputs_after_worker:
pop hl
pop de
jr _cputs_loop
_cputs_loop_end:
call __set_cursor
xor a, a ; return 0
ret
_cputs_fast:
call __cputs_pchars
xor a, a ; return 0
ret
__endasm;
}
void clrscr(void) __naked
{
__asm
ld a, #0x0F
jp _clrscr_attr
__endasm;
}
void clrscr_attr(uint8_t attr) __naked
{
(void)attr;
__asm
push ix
;; SDCC __sdcccall(1): uint8_t 1st arg is in A.
ld b, a ; B = attribute (mode-fill colour)
ld de, #0x0000 ; top-left
ld hl, #0x2050 ; H=32 rows, L=80 cols
ld a, #0x20 ; space fill
ld c, #0x56 ; ESTEX CLEAR
rst #0x10
pop ix
ret
__endasm;
}
void gotoxy(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
;; __sdcccall(1) 2 uint8 args: x in A, y in L.
;; ESTEX LOCATE ($52) wants: D = row, E = col.
push ix
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld c, #0x52
rst #0x10
pop ix
ret
__endasm;
}
uint8_t wherex(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
ld a, e
ret
__endasm;
}
uint8_t wherey(void) __naked
{
__asm
push ix
ld c, #0x53
rst #0x10
pop ix
ld a, d
ret
__endasm;
}
uint16_t wherexy(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
ret
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
* SDCC __sdcccall(1): x in A, y in L (2 uint8 → A, L); ch and attr
* packed and pushed on the stack as a single 16-bit value (caller does
* `ld hl, #(attr<<8)|ch; push hl`). Layout after CALL:
* [SP+0..1] = return address
* [SP+2] = ch (low half of pushed pair)
* [SP+3] = attr (high half)
* Void return → callee-pops the 2 stack-arg bytes via `pop bc` + jp (iy).
*/
void wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr) __naked
{
(void)x; (void)y; (void)ch; (void)attr;
__asm
pop iy ; return address
pop bc ; C = ch, B = attr
push ix
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld a, c ; A = ch
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
jp (iy)
__endasm;
}
/* rdchar(int x, int y) → (attr << 8) | ch */
uint16_t rdchar(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
push ix
ld d, l ; D = row
ld e, a ; E = col
ld c, #0x57 ; ESTEX RDCHAR
rst #0x10
;; A = ch, B = attr
ld d, b ; high byte attr
ld e, a ; low byte ch
pop ix
ret
__endasm;
}
/* Public text-mode video API — defined here so it's pulled in with the
* rest of conio. The raw setters/getters live in videomode_raw.c so
* pure graphics programs can pick them up without conio's other
* dependencies. */
extern uint8_t _videomode_raw_get(void);
extern int _videomode_raw_set(uint8_t mode);
uint8_t get_videotextmode(void)
{
return _videomode_raw_get();
}
int set_videotextmode(uint8_t mode)
{
/* Refuse anything that isn't a known text mode — otherwise a stray
* GFX_MODE_* value could swap the screen out from under text I/O. */
if (mode != TEXT_MODE_40x32 && mode != TEXT_MODE_80x32) {
errno = EINVAL;
return -1;
}
return _videomode_raw_set(mode);
}
/* ---- text attribute state ----------------------------------------
* g_text_attr is owned by conio.c now (Turbo-C-style: stdio putchar/puts
* are fast and attribute-free; only conio's putch/cputs/cprintf apply
* the attribute). Default = 0x0F (bright white on black).
*
* 0x00..0xFF — real attribute (4-bit FG | 3-bit BG | 1-bit blink)
* KEEP_EXIST_ATTR (0xFFFF) — putch/cputs fall back to fast no-attr path */
int16_t g_text_attr = 0x0F;
int16_t set_text_attr(int16_t attr)
{
int16_t prev = g_text_attr;
g_text_attr = attr;
return prev;
}
int16_t get_text_attr(void)
{
return g_text_attr;
}
/* ---- Turbo-C-style palette helpers --------------------------------
* textcolor / textbackground touch only their nibble; the other nibble
* (and the blink bit) are preserved. textattr replaces the whole byte. */
void textcolor(uint8_t fg)
{
/* If we were KEEP_EXIST_ATTR, switch to a real attr first. */
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
g_text_attr = (int16_t)((cur & 0xF0) | (fg & 0x0F));
}
void textbackground(uint8_t bg)
{
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
/* Background uses 3 bits (4..6); preserve blink (bit 7) too. */
g_text_attr = (int16_t)((cur & 0x8F) | ((bg & 0x07) << 4));
}
void textattr(uint8_t attr)
{
g_text_attr = (int16_t)attr;
}
/* ---- Solid-C compatibility ---------------------------------------- */
/* Direct port I/O. Z80 has 256 IN/OUT ports; we wrap the Z80 IN/OUT
* opcodes with a stable C API. Names match Solid-C / MS-DOS Turbo-C. */
uint8_t z80_inp(uint8_t port) __naked
{
(void)port;
__asm
;; SDCC __sdcccall(1): single uint8_t arg in A; uint8_t return in A.
ld c, a
in a, (c)
ret
__endasm;
}
void z80_outp(uint8_t port, uint8_t value) __naked
{
(void)port; (void)value;
__asm
;; __sdcccall(1): 2 uint8 args arg1 in A, arg2 in L.
ld c, a ; C = port
out (c), l ; out (port), value
ret
__endasm;
}
/* cgets — Solid-C / Turbo-C style line input.
* buf[0] = max characters (in)
* buf[1] = actual count (out)
* buf[2..] = chars + NUL
* Returns &buf[2]. */
char *cgets(char *buf)
{
uint8_t maxlen = (uint8_t)buf[0];
uint8_t n = 0;
while (n < maxlen) {
int ch = getche();
if (ch == '\n' || ch == '\r') {
putch('\r'); putch('\n');
break;
}
if (ch == 8) { /* backspace */
if (n > 0) { n--; }
continue;
}
buf[2 + n] = (char)ch;
n++;
}
buf[1] = (char)n;
buf[2 + n] = 0;
return &buf[2];
}
+28
View File
@@ -0,0 +1,28 @@
/*
* cprintf — printf for the conio output set. Formats into a static
* buffer with vsprintf (from SDCC's stdlib), then emits via cputs which
* applies the current text attribute per character.
*
* No '\n' to CR LF translation — Turbo-C convention: callers write
* "\r\n" explicitly in the format string for line breaks.
*
* Not reentrant (single static buffer) but Z80 single-threaded is fine.
*/
#include <conio.h>
#include <stdarg.h>
#include <stdio.h>
#define CPRINTF_BUF_SIZE 256
static char cp_buf[CPRINTF_BUF_SIZE];
int cprintf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
int n = vsprintf(cp_buf, fmt, ap);
va_end(ap);
cputs(cp_buf);
return n;
}
+68
View File
@@ -0,0 +1,68 @@
/*
* ffirst / fnext — directory iteration via ESTEX $19 / $1A.
*
* ESTEX F_FIRST ($19):
* HL = pattern, DE = buffer, A = attribute mask, B = format (0/1)
* CF = err / A = error code
* ESTEX F_NEXT ($1A):
* DE = same buffer
* CF = err / A = error code
*
* We always use format B=1 — 256-byte buffer with NUL-terminated DOS
* "name.ext" name at offset 33.
*
* ABI note: ffirst takes uint8_t as its 3rd arg. SDCC pushes a *single*
* byte for that (via `push af; inc sp`), not two — so the callee must
* pop ret-addr (2 bytes) AND consume the attr byte (`inc sp`) on the way
* out. Naively `pop bc` would over-eat into the caller's frame.
*/
#include <dir.h>
int ffirst(const char *pattern, ffblk_t *buf, uint8_t attrib) __naked
{
(void)pattern; (void)buf; (void)attrib;
__asm
;; On entry: HL = pattern, DE = buf, [SP+0..1] = ret, [SP+2] = attr.
ld iy, #2
add iy, sp
ld a, 0 (iy) ; A = attr (read without disturbing SP)
push ix
ld bc, #0x0119 ; ESTEX F_FIRST; format: 1 = DOS "name.ext" layout
rst #0x10
pop ix
pop hl ; HL = return address
inc sp ; consume the 1-byte attr
jr c, _ff_err
ld de, #0
jp (hl)
_ff_err:
call __errno_set
ld de, #-1
jp (hl)
__endasm;
}
int fnext(ffblk_t *buf) __naked
{
(void)buf;
__asm
;; HL = buf on entry; ESTEX F_NEXT wants buf in DE.
push ix
ex de, hl
ld c, #0x1A ; ESTEX F_NEXT
rst #0x10
pop ix
jr c, _fnext_err
ld de, #0
ret
_fnext_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
+93
View File
@@ -0,0 +1,93 @@
/*
* getenv / putenv via ESTEX ENVIRON ($46).
*
* ESTEX $46 subfn 1 (getenv):
* in: HL = name (ASCIIZ), DE = output buffer (caller-owned)
* out: CF=0 + A != 0 → variable found, value written into [DE..end-1]
* CF=0 + A == 0 → variable not present (note: this is the
* opposite of what DiskSyscalls.txt v1.6 docs
* claim — verified empirically and against
* solid-c's IO.ASM:763-766 implementation)
* CF=1 → error, A = code
* DE on exit points at one past the last byte written
*
* ESTEX $46 subfn 2 (putenv):
* in: HL = "NAME=value" (NUL-terminated)
* out: CF=1 / A = error code on failure
*
* We hand back a pointer into a private 128-byte buffer for getenv().
* Caller must copy the bytes before the next getenv() call if they
* need to outlive it.
*/
#include <stdint.h>
#include <sprinter.h>
static char env_buf[128];
char *getenv(const char *name) __naked
{
(void)name;
__asm
push ix
;; HL = name on entry; we need to also load DE = env_buf.
ld de, #_env_buf
ld bc, #0x0146 ; ESTEX ENVIRON; subfn: getenv
rst #0x10
pop ix
jr c, _getenv_err
or a, a
jr Z, _getenv_miss
ld de, #_env_buf
ret
_getenv_err:
call __errno_set
_getenv_miss:
ld de, #0
ret
__endasm;
}
int putenv(const char *namevalue) __naked
{
(void)namevalue;
__asm
push ix
ld bc, #0x0246 ; ESTEX ENVIRON; subfn: setenv
rst #0x10
pop ix
jr c, _putenv_err
ld de, #0
ret
_putenv_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX $46 subfn 0 (sysenv):
* in: HL = caller-owned buffer
* out: caller's buffer is filled with one NUL-terminated "NAME=value"
* per env var, then a trailing extra NUL marks the end:
* "PATH=...\0SOLID=H\0\0"
* return: buf on success, -1 on error (errno set).
* Buffer must be large enough for the whole environment.
*/
char *sysenv(char *buf) __naked
{
(void)buf;
__asm
push ix
push hl ; stash buffer pointer (= return value)
ld bc, #0x0046 ; ESTEX ENVIRON; subfn 0 = sysenv
rst #0x10
pop de ; DE = buffer pointer
pop ix
ret nc ; success: DE already has buf
;; CF=1 A = ESTEX error code.
call __errno_set
ld de, #-1
ret
__endasm;
}
+77
View File
@@ -0,0 +1,77 @@
/*
* errno.c — strerror / perror over the SDCC-provided `errno` global.
*
* The message table mirrors the one in solid-c's IO.ASM (we kept the
* English wording for grep-ability). ESTEX returns codes 0..32 in the
* meaningful range; anything beyond gets "Unknown error".
*
* Note: we deliberately do NOT define `_errno` here — SDCC's
* z80.lib/errno.rel provides it (a single int in _DATA), and our libc
* wrappers (read.c, open.c, etc.) just assign to `errno`. This
* removes the "multiple definition of _errno" link warning.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
/*
* Stored verbatim — pointers in the lookup table cost 2 bytes each plus
* the message bytes themselves. Sentinel "" entries pad out gaps so
* indexing stays direct.
*/
static const char *const messages[] = {
/* 0 */ "No error",
/* 1 */ "Invalid function",
/* 2 */ "Invalid drive number",
/* 3 */ "File not found",
/* 4 */ "Path not found",
/* 5 */ "Invalid handle",
/* 6 */ "Too many open files",
/* 7 */ "File already exists",
/* 8 */ "File is read-only",
/* 9 */ "Root directory overflow",
/* 10 */ "No free space",
/* 11 */ "Directory not empty",
/* 12 */ "Can't delete current directory",
/* 13 */ "Invalid media",
/* 14 */ "Unknown operation",
/* 15 */ "Directory exists",
/* 16 */ "Invalid filename",
/* 17 */ "Invalid EXE file",
/* 18 */ "Not supported EXE file",
/* 19 */ "Access denied",
/* 20 */ "Device not ready",
/* 21 */ "Seek error",
/* 22 */ "Sector not found",
/* 23 */ "CRC error",
/* 24 */ "Write protect",
/* 25 */ "Read error",
/* 26 */ "Write error",
/* 27 */ "Drive failure",
/* 28 */ "RESERVED",
/* 29 */ "RESERVED",
/* 30 */ "Out of memory",
/* 31 */ "Invalid memory block",
/* 32 */ "Unknown error",
};
const int ESTEX_MAX_ERR = sizeof(messages) / sizeof(messages[0]) - 1;
const char *strerror(int err)
{
if (err < 0 || err > ESTEX_MAX_ERR) {
err = EUNKERR;
}
return messages[err];
}
void perror(const char *prefix)
{
if (prefix && *prefix) {
fputs(prefix, stderr);
fputs(": ", stderr);
}
fputs(strerror(errno), stderr);
fputs("\r\n", stderr);
}
+88
View File
@@ -0,0 +1,88 @@
/*
* fsdir.c — directory operations via ESTEX:
* $1B MKDIR — create directory (HL = path)
* $1C RMDIR — remove empty directory (HL = path)
* $1D CHDIR — change current directory (HL = path)
* $1E CURDIR — read current directory path (HL = 256-byte buffer)
*
* All four return CF=1 + A=error on failure. We surface that as errno
* with a -1 (or NULL for getcwd) return value, matching POSIX.
*/
#include <unistd.h>
#include <errno.h>
int mkdir(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x1B ; ESTEX MKDIR; HL already = path
rst #0x10
pop ix
jr c, _mk_err
ld de, #0
ret
_mk_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
int rmdir(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x1C ; ESTEX RMDIR; HL already = path
rst #0x10
pop ix
jr c, _rm_err
ld de, #0
ret
_rm_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
int chdir(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x1D ; ESTEX CHDIR; HL already = path
rst #0x10
pop ix
jr c, _cd_err
ld de, #0
ret
_cd_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
char *getcwd(char *buf, size_t size) __naked
{
(void)buf; (void)size;
__asm
;; HL = buf, DE = size (ignored ESTEX always wants 256 bytes).
push ix
push hl ; preserve buf across RST
ld c, #0x1E ; ESTEX CURDIR
rst #0x10
pop hl ; restore buf
pop ix
jr c, _gc_err
ex de, hl ; return buf via DE (SDCC ptr return)
ret
_gc_err:
call __errno_set
ld de, #0 ; NULL on error
ret
__endasm;
}
+71
View File
@@ -0,0 +1,71 @@
/*
* lseek — 32-bit file position via ESTEX MOVE_FP ($15).
*
* ESTEX MOVE_FP: A=handle, B=whence (0=SET, 1=CUR, 2=END),
* HL=offset high16, IX=offset low16
* → HL:IX = new absolute position, CF=err
*
* SDCC __sdcccall(1) on z80 for `long lseek(int fd, long offset, int whence)`:
* - fd → HL (1st 16-bit arg in register)
* - offset → stack as two 16-bit words (low first, then high)
* - whence → stack (top after offset)
* - 32-bit return: DE = low16, HL = high16
* - caller-pops (caller-side `pop af; pop af; pop af` after the call)
*
* IX is saved (caller frame pointer).
*/
#include <unistd.h>
long lseek(int fd, long offset, int whence) __naked
{
(void)fd; (void)offset; (void)whence;
__asm
push ix ; save caller-side IX
;; Layout after push:
;; SP+0..1 = saved IX
;; SP+2..3 = ret addr
;; SP+4..5 = offset low16
;; SP+6..7 = offset high16
;; SP+8..9 = whence
ld a, l ; A = fd low byte (was in HL)
;; Walk through 5 consecutive stack bytes via HL cheaper than
;; IY-indexed because `ld r,(hl); inc hl` (2B/13T per byte) beats
;; `ld r, n(iy)` (3B/19T) for sequential reads.
ld hl, #4
add hl, sp ; HL offset_low
ld e, (hl)
inc hl
ld d, (hl) ; DE = offset_low
push de
pop ix ; IX = offset_low
inc hl
ld e, (hl)
inc hl
ld d, (hl) ; DE = offset_high
inc hl
ld b, (hl) ; B = whence (low byte)
ex de, hl ; HL = offset_high (ESTEX wants it here)
ld c, #0x15 ; ESTEX MOVE_FP
rst #0x10
jr c, _lseek_err
;; Returns HL:IX = new position. Convert to SDCC long return (DE:HL).
push ix
pop de ; DE = low16 (was IX)
;; HL already has high16
pop ix ; restore caller-side IX
ret
_lseek_err:
call __errno_set
ld hl, #0xFFFF
ld de, #0xFFFF ; long -1
pop ix
ret
__endasm;
}
+305
View File
@@ -0,0 +1,305 @@
/*
* mouse.c — Sprinter mouse driver wrappers (RST 30h).
*
* All calls use the same pattern as ESTEX (push/pop IX around the RST)
* since the driver doesn't promise to preserve registers either.
*/
#include <mouse.h>
/* Scratch for READ_STATE — RST 30h clobbers HL/DE/A so we can't keep the
* state pointer in HL across the call. Explicit `= 0` so SDCC reserves
* real BSS storage — see memory/sdcc_static_storage_gotcha.md. */
static uint16_t mb_x = 0, mb_y = 0;
static uint8_t mb_buttons = 0;
int mouse_init(void) __naked
{
__asm
push ix
ld c, #0x00 ; INITIALIZATION
rst #0x30 ; MOUSE
pop ix
jr c, _mi_err
ld de, #0
ret
_mi_err:
ld de, #-1
ret
__endasm;
}
void mouse_show(void) __naked
{
__asm
push ix
ld c, #0x01 ; SHOW MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_hide(void) __naked
{
__asm
push ix
ld c, #0x02 ; HIDE MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_refresh(void) __naked
{
__asm
push ix
ld c, #0x83 ; MOUSE REFRESH
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_read(mouse_state_t *st) __naked
{
(void)st;
__asm
;; HL = state ptr on entry.
push ix
push hl ; stash ptr across RST
ld c, #0x03 ; READ MOUSE STATE
rst #0x30 ; MOUSE
;; Returns: A=buttons, HL=x, DE=y (CF=err but we ignore here)
ld (_mb_x), hl
ld (_mb_y), de
ld (_mb_buttons), a
pop hl ; restore state ptr
pop ix
;; Copy scratch *st. Struct layout: x(2), y(2), buttons(1).
ld de, (_mb_x)
ld (hl), e
inc hl
ld (hl), d
inc hl
ld de, (_mb_y)
ld (hl), e
inc hl
ld (hl), d
inc hl
ld a, (_mb_buttons)
ld (hl), a
ret
__endasm;
}
void mouse_goto(int x, int y) __naked
{
(void)x; (void)y;
__asm
;; HL = x, DE = y.
push ix
ld c, #0x04 ; GOTO MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_bounds_x(int xmin, int xmax) __naked
{
(void)xmin; (void)xmax;
__asm
;; HL = xmin, DE = xmax.
push ix
ld c, #0x08 ; HORZ BOUNDS
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
void mouse_bounds_y(int ymin, int ymax) __naked
{
(void)ymin; (void)ymax;
__asm
;; HL = ymin, DE = ymax.
push ix
ld c, #0x07 ; VERT BOUNDS
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
/* ---- $09 LOAD CURSOR + $0B RETURN CURSOR ---------------------- */
/* Scratch for the IX-passing convention — RST 30h takes the bitmap
* pointer in IX, so we have to set it up explicitly.
*
* Initialised to 0 so SDCC reserves real BSS storage — uninitialised
* `static uint8_t` declarations can coalesce to a single address and
* stomp on each other. See memory/sdcc_static_storage_gotcha.md. */
static uint16_t mc_image = 0;
static uint8_t mc_width = 0;
static uint8_t mc_height = 0;
static uint8_t mc_hot_x = 0;
static uint8_t mc_hot_y = 0;
/* Saved struct pointer for mouse_get_cursor. SDCC __sdcccall(1) passes
* `c` in HL, then stashes it in DE around the inline asm. Our asm
* clobbers DE (driver returns hot_y/hot_x in D/E) so the post-asm
* `c->width = ...` writes would otherwise land at a garbage address.
* We park the pointer in BSS instead so SDCC re-fetches it from
* memory after the asm. */
static mouse_cursor_t *mc_dest = 0;
void mouse_load_cursor(const mouse_cursor_t *c)
{
/* Copy fields out of the C struct into our scratch globals so the
* asm side has well-known names. */
mc_image = (uint16_t)(uintptr_t)c->image;
mc_width = c->width;
mc_height = c->height;
mc_hot_x = c->hot_x;
mc_hot_y = c->hot_y;
__asm
push ix
ld ix, (_mc_image)
ld a, (_mc_height)
ld h, a
ld a, (_mc_width)
ld l, a
ld a, (_mc_hot_y)
ld d, a
ld a, (_mc_hot_x)
ld e, a
ld b, #0
ld c, #0x09 ; LOAD MOUSE CURSOR
rst #0x30 ; MOUSE
pop ix
__endasm;
}
void mouse_get_cursor(mouse_cursor_t *c)
{
mc_dest = c; /* park ptr in BSS */
mc_image = (uint16_t)(uintptr_t)c->image;
__asm
push ix
ld ix, (_mc_image) ; IX = bitmap buffer from caller
ld c, #0x0B ; RETURN CURSOR
rst #0x30 ; mouse driver, NOT ESTEX
;; Returns: H=height, L=width, D=hot_y, E=hot_x.
ld a, h
ld (_mc_height), a
ld a, l
ld (_mc_width), a
ld a, d
ld (_mc_hot_y), a
ld a, e
ld (_mc_hot_x), a
pop ix
__endasm;
/* Re-fetch the struct pointer from BSS — `c` (kept in DE by SDCC
* around the inline asm) was clobbered by the RST 30h above. */
mouse_cursor_t *p = mc_dest;
p->width = mc_width;
p->height = mc_height;
p->hot_x = mc_hot_x;
p->hot_y = mc_hot_y;
}
/* ---- $0E / $0F SENSITIVITY ------------------------------------ */
/* GET returns H=vert, L=horz in HL. We expose the two halves as
* separate getters so the simple "uint8_t" return ABI works cleanly. */
static int ms_query(void) __naked
{
__asm
push ix
ld c, #0x0E ; GET SENSITIVITY
rst #0x30 ; MOUSE
ld d, h
ld e, l
pop ix
ret
__endasm;
}
uint8_t mouse_get_sensitivity_x(void)
{
return (uint8_t)(ms_query() & 0xFF); /* E = horz */
}
uint8_t mouse_get_sensitivity_y(void)
{
return (uint8_t)(ms_query() >> 8); /* D = vert */
}
void mouse_set_sensitivity(uint8_t horz, uint8_t vert) __naked
{
(void)horz; (void)vert;
/* Pack into HL: H=vert, L=horz. */
__asm
push ix
ld h, l
ld l, a
ld c, #0x0F ; SET SENSITIVITY
rst #0x30 ; MOUSE
pop ix
__endasm;
}
/* ---- $81 CHANGE VIDEO MODE ------------------------------------ */
/* SDCC __sdcccall(1): single uint8_t arg arrives in A. */
void mouse_video_mode_changed(uint8_t mode) __naked
{
(void)mode;
__asm
push ix
;; A already holds the mode byte (from SDCC ABI).
ld c, #0x81 ; CHANGE VIDEO MODE
rst #0x30 ; MOUSE
pop ix
ret
__endasm;
}
/* CURSOR_TEXT_MODES ($0A):
* B = 0
* H = AND symbol mask L = XOR symbol mask
* D = AND attribute mask E = XOR attribute mask
*
* SDCC __sdcccall(1) gives us:
* sym_and in L (low byte of HL arg)
* sym_xor in E (low byte of DE arg)
* attr_and at [SP+2]
* attr_xor at [SP+3]
*/
void mouse_text_cursor(uint8_t sym_and, uint8_t sym_xor,
uint8_t attr_and, uint8_t attr_xor) __naked
{
(void)sym_and; (void)sym_xor; (void)attr_and; (void)attr_xor;
__asm
;; SDCC __sdcccall(1) for 4×uint8_t args:
;; arg1 sym_and A
;; arg2 sym_xor L
;; arg3 attr_and stack low byte (packed into HL.L on caller, push HL)
;; arg4 attr_xor stack high byte (HL.H pushed by caller)
pop iy ; return address
pop bc ; C = attr_and (low), B = attr_xor (high)
push ix
;; Target: H=sym_and, L=sym_xor, D=attr_and, E=attr_xor, B=0
ld h, a ; H = sym_and (from A)
; L already holds sym_xor
ld d, c ; D = attr_and
ld e, b ; E = attr_xor
ld b, #0
ld c, #0x0A ; CURSOR TEXT MODE
rst #0x30 ; MOUSE
pop ix
jp (iy)
__endasm;
}
+158
View File
@@ -0,0 +1,158 @@
/*
* open / creat / close — ESTEX file-handle primitives.
*
* The interesting part is the open() flag state machine. We expose POSIX
* flag bits (O_RDONLY/O_WRONLY/O_RDWR + O_CREAT/O_EXCL/O_TRUNC/O_APPEND)
* and dispatch onto the three ESTEX entry points:
*
* $11 OPEN — open existing file
* $0A CREATE — create (truncate if exists), return new handle
* $0B CREATE_NEW — create only if file does not exist
*
* Three private __naked wrappers do the raw RST 10h calls; the public
* open() / creat() / close() are plain C orchestrators.
*
* IX is saved across every RST 10h. Failures set errno and return -1.
*/
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/* ---- raw ESTEX wrappers ---------------------------------------------- */
/* ESTEX $11 OPEN: A=mode (1=R, 2=W, 0=R/W), HL=path → A=handle, CF=err. */
static int _estex_open_raw(const char *path, int posix_mode) __naked
{
(void)path; (void)posix_mode;
__asm
push ix
;; HL = path, DE = posix_mode. Translate to ESTEX numbering.
ld a, e
and a, #0x03
ld c, #1
or a, a
jr Z, _oopen_real
ld c, #2
dec a
jr Z, _oopen_real
ld c, #0
_oopen_real:
ld a, c
ld c, #0x11 ; ESTEX OPEN
rst #0x10
pop ix
jr c, _oopen_err
ld e, a
ld d, #0
ret
_oopen_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX $0A CREATE: A=attr, HL=path → A=handle, CF=err.
* Truncates an existing file. */
static int _estex_create_raw(const char *path) __naked
{
(void)path;
__asm
push ix
xor a, a ; A = 0 (normal attribute)
ld c, #0x0A
rst #0x10
pop ix
jr c, _ocreat_err
ld e, a
ld d, #0
ret
_ocreat_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX $0B CREATE_NEW: A=attr, HL=path → A=handle, CF=err.
* Fails (errno=EEXIST) if file already exists. */
static int _estex_create_new_raw(const char *path) __naked
{
(void)path;
__asm
push ix
xor a, a
ld c, #0x0B
rst #0x10
pop ix
jr c, _ocreatn_err
ld e, a
ld d, #0
ret
_ocreatn_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ---- public surface --------------------------------------------------- */
int open(const char *path, int flags)
{
int fd;
if (flags & O_CREAT) {
if (flags & O_EXCL) {
/* Must not already exist. */
fd = _estex_create_new_raw(path);
} else if (flags & O_TRUNC) {
/* Always create or truncate. */
fd = _estex_create_raw(path);
} else {
/* Open if it exists, otherwise create. */
fd = _estex_open_raw(path, flags);
if (fd < 0 && errno == ENOENT) {
fd = _estex_create_raw(path);
}
}
} else {
fd = _estex_open_raw(path, flags);
}
if (fd < 0) {
return -1;
}
if (flags & O_APPEND) {
/* Position at end of file so future writes append. */
(void)lseek(fd, 0L, SEEK_END);
}
return fd;
}
int creat(const char *path, int mode)
{
(void)mode; /* Sprinter has no per-file permission bits */
return open(path, O_WRONLY | O_CREAT | O_TRUNC);
}
int close(int fd) __naked
{
(void)fd;
__asm
push ix
ld a, l
ld c, #0x12
rst #0x10
pop ix
jr c, _oclose_err
ld de, #0
ret
_oclose_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
+143
View File
@@ -0,0 +1,143 @@
/*
* posix_time.c — minimal POSIX <time.h> implementation on top of
* getdatetime() (ESTEX SYSTIME $21).
*
* SDCC's z80.lib bundles time/localtime/mktime AND _RtcRead in a
* single time.rel module, so the user can't override _RtcRead from a
* separate object — overriding triggers a "multiple definition"
* linker error. We sidestep that by implementing the whole POSIX time
* API ourselves; the linker then never pulls SDCC's time.rel.
*
* The epoch is Unix (1970-01-01 00:00:00). No timezone support —
* gmtime and localtime are identical. No DST.
*/
#include <stdio.h>
#include <time.h>
static const unsigned char mdays[12] = {
31,28,31,30,31,30,31,31,30,31,30,31
};
static const char *const dnames[7] = {
"Sun","Mon","Tue","Wed","Thu","Fri","Sat"
};
static const char *const mnames[12] = {
"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"
};
static int is_leap(unsigned int y)
{
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
/* Days elapsed from 1970-01-01 to (y, 1, 1). */
static unsigned long year_days(unsigned int y)
{
unsigned long d = 0;
for (unsigned int i = 1970; i < y; i++)
d += is_leap(i) ? 366 : 365;
return d;
}
/* Days from Jan 1 to month start (1-based month input). */
static unsigned int month_days(unsigned int y, unsigned int m)
{
unsigned int d = 0;
for (unsigned int i = 0; i < m - 1; i++) d += mdays[i];
if (m > 2 && is_leap(y)) d++;
return d;
}
time_t time(time_t *t)
{
datetime_t dt;
getdatetime(&dt);
unsigned long days = year_days(dt.year)
+ month_days(dt.year, dt.month)
+ (dt.day - 1);
time_t epoch = days * 86400UL
+ (unsigned long)dt.hour * 3600UL
+ (unsigned long)dt.minute * 60UL
+ dt.second;
if (t) *t = epoch;
return epoch;
}
/* localtime and gmtime share one static buffer — caller copies if
* needed across further calls (matches POSIX behaviour). */
static struct tm tm_buf;
struct tm *gmtime(time_t *timep)
{
unsigned long sec = *timep;
tm_buf.tm_sec = (unsigned char)(sec % 60); sec /= 60;
tm_buf.tm_min = (unsigned char)(sec % 60); sec /= 60;
tm_buf.tm_hour = (unsigned char)(sec % 24); sec /= 24;
/* sec is now days since 1970-01-01 (Thursday). */
tm_buf.tm_wday = (unsigned char)((4 + sec) % 7);
/* find year */
unsigned int y = 1970;
unsigned long days = sec;
while (days >= (unsigned long)(is_leap(y) ? 366 : 365)) {
days -= is_leap(y) ? 366 : 365;
y++;
}
tm_buf.tm_year = (int)y - 1900;
tm_buf.tm_yday = (int)days;
/* find month/day */
unsigned int m = 0;
while (m < 12) {
unsigned int dim = mdays[m] + ((m == 1) && is_leap(y) ? 1u : 0u);
if (days < dim) break;
days -= dim;
m++;
}
tm_buf.tm_mon = (unsigned char)m;
tm_buf.tm_mday = (unsigned char)(days + 1);
tm_buf.tm_isdst = 0;
tm_buf.tm_hundredth = 0;
return &tm_buf;
}
struct tm *localtime(time_t *timep)
{
return gmtime(timep); /* no timezone */
}
time_t mktime(struct tm *tm)
{
unsigned int y = (unsigned int)(tm->tm_year + 1900);
unsigned long days = year_days(y)
+ month_days(y, (unsigned int)tm->tm_mon + 1)
+ (unsigned int)(tm->tm_mday - 1);
time_t epoch = days * 86400UL
+ (unsigned long)tm->tm_hour * 3600UL
+ (unsigned long)tm->tm_min * 60UL
+ tm->tm_sec;
/* Backfill wday/yday so callers can inspect them. */
tm->tm_wday = (unsigned char)((4 + days) % 7);
tm->tm_yday = (int)month_days(y, (unsigned int)tm->tm_mon + 1)
+ (tm->tm_mday - 1);
return epoch;
}
/* "Day Mon DD HH:MM:SS YYYY\n" — 25 chars + NUL. */
static char asctime_buf[26];
char *asctime(struct tm *tm)
{
sprintf(asctime_buf, "%s %s %2d %02d:%02d:%02d %d\n",
dnames[tm->tm_wday % 7],
mnames[tm->tm_mon % 12],
tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec,
tm->tm_year + 1900);
return asctime_buf;
}
char *ctime(time_t *timep)
{
return asctime(localtime(timep));
}
+67
View File
@@ -0,0 +1,67 @@
/*
* read / write — bulk transfer through ESTEX file handles.
*
* ESTEX READ ($13) / WRITE ($14):
* A = handle, HL = buffer, DE = byte count
* → DE = bytes actually transferred, CF = err with code in A.
*
* SDCC __sdcccall(1) for 3-arg int functions uses callee-pops for the
* stack-passed argument; this implementation mirrors the pattern used
* by SDCC's own z80.lib _memset.
*
* On error: sets errno, returns -1.
*/
#include <unistd.h>
int read(int fd, void *buf, size_t n) __naked
{
(void)fd; (void)buf; (void)n;
__asm
pop iy ; IY = return address
pop bc ; BC = n (stack arg)
ld a, l ; A = handle
ex de, hl ; HL = buf
ld d, b
ld e, c ; DE = n
push ix
push iy ; preserve return addr across RST
ld c, #0x13 ; ESTEX READ
rst #0x10
pop iy
pop ix
jr c, _read_err
;; DE already holds count read.
jp (iy)
_read_err:
call __errno_set
ld de, #-1
jp (iy)
__endasm;
}
int write(int fd, const void *buf, size_t n) __naked
{
(void)fd; (void)buf; (void)n;
__asm
pop iy
pop bc
ld a, l
ex de, hl
ld d, b
ld e, c
push ix
push iy
ld c, #0x14
rst #0x10
pop iy
pop ix
jr c, _write_err
jp (iy)
_write_err:
call __errno_set
ld de, #-1
jp (iy)
__endasm;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* sleep — block for N seconds using the 50 Hz frame interrupt.
*
* Sprinter ISR fires 50 times per second. `halt` parks the CPU until
* the next IRQ, so 50 halts = ~1 second of wall clock. This is the
* same trick solid-c uses in IO.ASM:285.
*
* Note: requires interrupts to be enabled (they are by default — ESTEX
* sets up IM 1 with the frame ISR before our program runs).
*/
#include <unistd.h>
void sleep(unsigned int seconds) __naked
{
(void)seconds;
__asm
inter:
;; HL = seconds on entry (SDCC single int arg).
ld a, h
or a, l
ret Z ; sleep(0) return immediately
ld b, #50 ; 50 halts per second (50 Hz interrupt)
inner:
halt
djnz inner
dec hl
jr inter
__endasm;
}
+29
View File
@@ -0,0 +1,29 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strlwr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'A' && *p <= 'Z') *p += 'a' - 'A';
p++;
}
return s;
}
char *strupr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'a' && *p <= 'z') *p -= 'a' - 'A';
p++;
}
return s;
}
/* div() comes from SDCC's z80.lib. */
+190
View File
@@ -0,0 +1,190 @@
/*
* stat.c — POSIX stat() and fstat() over ESTEX metadata.
*
* fstat(fd, &st) -> ESTEX GET_D_T ($17) for mtime + lseek/SEEK_END
* for size.
* stat(path, &st) -> open(O_RDONLY) + fstat() + close. This works
* for any regular file; directory paths fail at
* open() — F_FIRST-based stat had unreliable
* semantics for exact filenames.
*
* Sprinter / DSS doesn't track POSIX owner/group/inode, so we synth a
* minimal mode (S_IFREG | rw user perm).
*/
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <dir.h>
/* ESTEX GET_D_T: A=fd, C=$17 → D=day, E=month, IX=year, H=hour, L=min,
* B=sec. CF=1 + A=errcode on failure.
*
* Writes directly into *out using `ex (sp), hl` to swap the saved out
* pointer with HL=hour:min after RST — no static scratch needed.
* `out->dow` is left untouched (GET_D_T doesn't return it). */
static int get_dt_for_handle(int fd, datetime_t *out) __naked
{
(void)fd; (void)out;
__asm
;; __sdcccall(1): fd in HL (low byte), out in DE.
push ix ; save caller IX
push de ; stash out pointer
ld a, l ; A = fd
ld c, #0x17 ; ESTEX GET_D_T
rst #0x10
jr c, _gdt_err
;; D=day E=month IX=year H=hour L=min B=sec
ex (sp), hl ; TOS<->HL: HL=out, TOS=hour:min
ld (hl), d ; +0 day
inc hl
ld (hl), e ; +1 month
inc hl
push ix ; year onto stack
pop de ; DE = year
ld (hl), e ; +2 year low
inc hl
ld (hl), d ; +3 year high
inc hl
pop de ; D=hour E=min (from earlier ex (sp))
ld (hl), d ; +4 hour
inc hl
ld (hl), e ; +5 min
inc hl
ld (hl), b ; +6 sec (+7 dow left untouched)
pop ix ; restore caller IX
ld de, #0
ret
_gdt_err:
pop hl ; discard stashed out pointer
pop ix ; restore caller IX
call __errno_set
ld de, #-1
ret
__endasm;
}
int fstat(int fd, struct stat *buf)
{
/* Size via lseek trick. */
long cur = lseek(fd, 0L, SEEK_CUR);
if (cur < 0) return -1;
long end = lseek(fd, 0L, SEEK_END);
if (end < 0) return -1;
(void)lseek(fd, cur, SEEK_SET);
buf->st_size = (uint32_t)end;
/* Date/time via ESTEX. */
datetime_t ft;
if (get_dt_for_handle(fd, &ft) < 0) return -1;
{
struct tm tm;
tm.tm_sec = ft.second;
tm.tm_min = ft.minute;
tm.tm_hour = ft.hour;
tm.tm_mday = ft.day;
tm.tm_mon = (unsigned char)(ft.month - 1);
tm.tm_year = (int)ft.year - 1900;
tm.tm_isdst = 0;
tm.tm_hundredth = 0;
buf->st_mtime = mktime(&tm);
}
buf->st_mode = S_IFREG | S_IRUSR | S_IWUSR;
return 0;
}
/* Convert ESTEX DOS-style date+time to time_t epoch (used by stat()
* when going through F_FIRST for directory entries). */
static time_t dos_to_epoch(uint16_t date, uint16_t dtime)
{
struct tm tm;
tm.tm_sec = (unsigned char)((dtime & 0x1F) << 1);
tm.tm_min = (unsigned char)((dtime >> 5) & 0x3F);
tm.tm_hour = (unsigned char)((dtime >> 11) & 0x1F);
tm.tm_mday = (unsigned char)(date & 0x1F);
tm.tm_mon = (unsigned char)(((date >> 5) & 0x0F) - 1);
tm.tm_year = (int)((date >> 9) & 0x7F) + 80; /* DOS year base = 1980 */
tm.tm_isdst = 0;
tm.tm_hundredth = 0;
return mktime(&tm);
}
/* Returns 1 if path is "." or "..", else 0. Reads at most 3 bytes;
* NULL-safe. ~28 bytes / 34117 T-states depending on input. */
static char is_dot_or_dotdot(const char *path) __naked
{
(void)path;
__asm
ld a, h
or a, l
jr Z, _idd_fail ; NULL 0
ld a, (hl)
sub a, #0x2E
jr NZ, _idd_fail ; path[0] != '.'
inc hl
ld a, (hl)
or a, a
jr Z, _idd_ok ; ".\0" 1
sub a, #0x2E
jr NZ, _idd_fail ; path[1] not '.' and not '\0'
inc hl
ld a, (hl)
or a, a
jr Z, _idd_ok ; "..\0" 1
_idd_fail:
xor a, a
ret
_idd_ok:
ld a, #1
ret
__endasm;
}
int stat(const char *path, struct stat *buf)
{
/* Regular file: open + fstat. */
int fd = open(path, O_RDONLY);
if (fd >= 0) {
int r = fstat(fd, buf);
close(fd);
return r;
}
int saved = errno;
/* Try ffirst directly — works for ordinary subdirectories. */
ffblk_t ffb;
if (ffirst(path, &ffb, FA_DIREC) == 0 && (ffb.found_attr & FA_DIREC)) {
buf->st_size = ffb.size;
buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
buf->st_mode = S_IFDIR | S_IRWXU;
return 0;
}
/* Verified 2026-05-29: ESTEX F_FIRST rejects bare "." and ".." with
* EINAME (16), same as open(). But they DO appear in the "*.*"
* directory listing with FA_DIREC. Iterate to find them. */
if (is_dot_or_dotdot(path)) {
if (ffirst("*.*", &ffb, FA_DIREC) == 0) {
do {
if (strcmp(ffb.found_name, path) == 0) {
buf->st_size = ffb.size;
buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
buf->st_mode = S_IFDIR | S_IRWXU;
return 0;
}
} while (fnext(&ffb) == 0);
}
/* Last-resort synthetic entry — FS variant didn't expose them. */
buf->st_mode = S_IFDIR | S_IRWXU;
buf->st_size = 0;
buf->st_mtime = 0;
return 0;
}
errno = saved;
return -1;
}
+93
View File
@@ -0,0 +1,93 @@
/*
* time.c — getdatetime / setdatetime via ESTEX SYSTIME ($21) / SETTIME ($22).
*
* $21 SYSTIME → D=day E=month IX=year H=hour L=min B=sec C=dow
* $22 SETTIME D=day E=month IX=year H=hour L=min B=sec → CF=err / A=errcode
*
* Struct layout (datetime_t, 8 bytes):
* +0 day, +1 month, +2..3 year, +4 hour, +5 min, +6 sec, +7 dow
*
* Both routines write/read *dt directly — no static scratch. The key
* trick in getdatetime is `ex (sp), hl` after RST: HL holds hour:min,
* stack TOS holds dt — one byte swaps them, then we walk the struct
* via HL. setdatetime uses IX as the struct pointer (re-loaded with
* year just before RST since that's what SETTIME expects).
*/
#include <time.h>
#include <errno.h>
void getdatetime(datetime_t *dt) __naked
{
(void)dt;
__asm
push ix ; save caller IX (BIOS clobbers it)
push hl ; stash dt across RST (clobbers HL)
ld c, #0x21 ; ESTEX SYSTIME
rst #0x10
;; Returns: D=day E=month IX=year H=hour L=min B=sec C=dow
;; ex (sp), hl: TOS<->HL. Now HL = dt, TOS = hour:min stash.
ex (sp), hl
ld (hl), d ; +0 day
inc hl
ld (hl), e ; +1 month
inc hl
push ix ; year stack
pop de ; DE = year
ld (hl), e ; +2 year low
inc hl
ld (hl), d ; +3 year high
inc hl
pop de ; D = hour, E = min (from earlier ex(sp))
ld (hl), d ; +4 hour
inc hl
ld (hl), e ; +5 min
inc hl
ld (hl), b ; +6 sec
inc hl
ld (hl), c ; +7 dow
pop ix ; restore caller IX
ret
__endasm;
}
int setdatetime(const datetime_t *dt) __naked
{
(void)dt;
__asm
push ix ; save caller IX
push hl
pop ix ; IX = dt
ld d, (hl) ; +0 D = day
inc hl
ld e, (hl) ; +1 E = month
inc hl
ld c, (hl) ; +2 year low
inc hl
ld b, (hl) ; +3 year high (BC->IX)
inc hl
push bc
ld b, (hl) ; +4 H = hour
inc hl
ld c, (hl) ; +5 L = min (BC->HL)
inc hl
push bc
ld b, (hl) ; +6 B = sec
pop hl
pop ix
ld c, #0x22 ; ESTEX SETTIME
rst #0x10
pop ix ; restore caller IX
jr c, _st_err2
ld de, #0
ret
_st_err2:
call __errno_set
ld de, #-1
ret
__endasm;
}
+24
View File
@@ -0,0 +1,24 @@
/*
* unlink — remove a file via ESTEX DELETE ($0E).
* HL = ASCIIZ path; CF=err with code in A. Sets errno on failure.
*/
#include <unistd.h>
int unlink(const char *path) __naked
{
(void)path;
__asm
push ix
ld c, #0x0E ; ESTEX DELETE (HL = file name, A = attribure file)
rst #0x10
pop ix
jr c, _unlink_err
ld de, #0
ret
_unlink_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
+46
View File
@@ -0,0 +1,46 @@
/*
* videomode_raw.c — low-level ESTEX SETVMOD / GETVMOD ($50 / $51).
*
* Plain getters/setters with NO mode-class validation. Used by both
* conio (text-validated public API) and gfx (graphics modes). Lives
* in its own .c so a pure graphics program does not pull in the entire
* conio module to switch modes.
*
* Public conio functions in conio.c wrap these with a text-mode check;
* gfx_init / gfx_done in gfx_core.c call them directly.
*/
#include <stdint.h>
#include <errno.h>
uint8_t _videomode_raw_get(void) __naked
{
__asm
push ix
ld c, #0x51 ; ESTEX GETVMOD
rst #0x10
pop ix
;; uint8_t returns in A ESTEX already put mode there.
ret
__endasm;
}
int _videomode_raw_set(uint8_t mode) __naked
{
(void)mode;
__asm
;; SDCC __sdcccall(1) passes uint8_t in A leave it there.
push ix
ld bc, #0x0050 ; ESTEX SETVMOD (B=0 (page), C=0x50)
rst #0x10
jr c, _vmr_err
ld de, #0
pop ix
ret
_vmr_err:
call __errno_set
ld de, #-1
pop ix
ret
__endasm;
}
+51
View File
@@ -0,0 +1,51 @@
/*
* bank_io — HOME-resident helpers to read/write a "far" page that lives
* in some physical RAM page outside the currently-mapped W3 bank.
*
* - We always run from HOME (window 1, always mapped), so we are free
* to swap W3 (port 0xE2) between the caller's bank and the data
* page, then restore it before returning.
* - The caller must not rely on W3 contents during the call — the swap
* is transparent to instruction-fetch (we execute from W1), and only
* this function touches W3.
* - DI/EI is NOT applied around the swap; ISRs in HOME are unaffected,
* and banked-call ISRs are not expected in our current design.
*/
#include <stdint.h>
#include <string.h>
#include <sprinter.h>
#include <sprinter_mem.h>
uint8_t bank_load_byte(uint8_t phys_page, uint16_t off_in_window)
{
uint8_t saved = _io_page_w3;
sprinter_page_w3(phys_page);
uint8_t v = *((volatile uint8_t *)(0xC000u + off_in_window));
sprinter_page_w3(saved);
return v;
}
void bank_store_byte(uint8_t phys_page, uint16_t off_in_window, uint8_t v)
{
uint8_t saved = _io_page_w3;
sprinter_page_w3(phys_page);
*((volatile uint8_t *)(0xC000u + off_in_window)) = v;
sprinter_page_w3(saved);
}
void bank_read(uint8_t phys_page, uint16_t off, void *dst, uint16_t n)
{
uint8_t saved = _io_page_w3;
sprinter_page_w3(phys_page);
memcpy(dst, (const void *)(0xC000u + off), n);
sprinter_page_w3(saved);
}
void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n)
{
uint8_t saved = _io_page_w3;
sprinter_page_w3(phys_page);
memcpy((void *)(0xC000u + off), src, n);
sprinter_page_w3(saved);
}
+99
View File
@@ -0,0 +1,99 @@
/*
* mem_alloc_pages / mem_free_block / mem_get_page / mem_info — ESTEX EMM
* wrappers for explicit 16 KB-page allocation.
*
* ESTEX $3C INFOMEM → HL=total pages, BC=free pages
* ESTEX $3D GETMEM B=npages → A=block id, CF=err
* ESTEX $3E FREEMEM A=block id → CF=err
* BIOS $C4 EMM_GETPAGE A=blk, B=idx → A=physical page CF=err
*
* Pattern: every RST 10h / RST 8 is bracketed with push/pop IX because
* ESTEX/BIOS clobber it and the C caller uses it as a frame pointer.
*/
#include <stdint.h>
#include <sprinter_mem.h>
uint8_t mem_alloc_pages(uint8_t n) __naked
{
(void)n;
__asm
;; SDCC single-uint8 arg A on entry.
push ix
ld b, a
ld c, #0x3D
rst #0x10
pop ix
jr c, _alloc_fail
ret
_alloc_fail:
call __errno_set
xor a, a ; 0 = failure
ret
__endasm;
}
void mem_free_block(uint8_t blk_id) __naked
{
(void)blk_id;
__asm
;; SDCC single-uint8 arg A on entry.
push ix
ld c, #0x3E
rst #0x10
pop ix
ret
__endasm;
}
uint8_t mem_get_page(uint8_t blk_id, uint8_t idx) __naked
{
(void)blk_id; (void)idx;
__asm
;; 2-arg uint8/uint8: blk_id A, idx L.
push ix
ld b, l ; BIOS wants idx in B
;; A still has blk_id
ld c, #0xC4 ; BIOS EMM_GETPAGE
rst #0x08
pop ix
;; A = physical page number. Return as uint8 A.
ret
__endasm;
}
void mem_info(uint16_t *total, uint16_t *free_pages) __naked
{
(void)total; (void)free_pages;
__asm
;; HL = total, DE = free_pages on entry.
;; ESTEX INFOMEM clobbers everything; stash both pointers on stack.
push ix
push hl ; [SP+0..1] = total ptr
push de ; [SP+2..3] = free_pages ptr (wait wrong order)
;; Actually after two pushes: SP+0 = free_pages_ptr, SP+2 = total_ptr.
;; That's the layout we'll use below.
ld c, #0x3C ; ESTEX INFOMEM HL=total, BC=free
rst #0x10
;; HL = total value, BC = free value.
pop de ; DE = free_pages ptr
ld a, c
ld (de), a
inc de
ld a, b
ld (de), a
pop de ; DE = total ptr
ld a, l
ld (de), a
inc de
ld a, h
ld (de), a
pop ix
ret
__endasm;
}
+267
View File
@@ -0,0 +1,267 @@
/*
* file.c — minimal unbuffered FILE * implementation on top of the
* POSIX-style fd I/O (open/read/write/lseek/close).
*
* No buffering: each fputc/fgetc maps to one read/write syscall. For
* heavy-throughput code, prefer fread/fwrite with a sizable buffer or
* the raw fd I/O directly.
*
* stdin/stdout/stderr are STATIC sentinel FILEs with fd=-1 and the
* _F_CONIN/_F_CONOUT flags set; fputc/fgetc detect them and call
* putchar()/getchar() (which already do CR/LF mapping and ESTEX calls).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/* ---- console pseudo-streams ----------------------------------------*/
static FILE _stdin = { -1, _F_READ | _F_CONIN };
static FILE _stdout = { -2, _F_WRITE | _F_CONOUT };
static FILE _stderr = { -3, _F_WRITE | _F_CONOUT };
static FILE _stdaux = { -4, _F_WRITE | _F_CONOUT };
static FILE _stdprn = { -5, _F_WRITE | _F_CONOUT };
FILE *const stdin = &_stdin;
FILE *const stdout = &_stdout;
FILE *const stderr = &_stderr;
FILE *const stdaux = &_stdaux;
FILE *const stdprn = &_stdprn;
/* ---- fopen / fclose -------------------------------------------------*/
/* Translate a fopen() mode string to the open() flags subset our
* libc/io/open.c understands. Supported: r, w, a, with optional "+"
* and trailing "b" (binary — we ignore as all I/O is binary).
*/
static int mode_to_flags(const char *mode, uint8_t *file_flags)
{
if (!mode || !*mode) {
errno = EINVAL;
return -1;
}
int oflags = 0;
uint8_t ff = 0;
char base = *mode;
int plus = 0;
for (const char *p = mode + 1; *p; p++) {
if (*p == '+') plus = 1;
/* 'b' and 't' are ignored — all I/O is binary on Sprinter. */
}
switch (base) {
case 'r':
oflags = plus ? O_RDWR : O_RDONLY;
ff = _F_READ | (plus ? _F_WRITE : 0);
break;
case 'w':
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC;
ff = _F_WRITE | (plus ? _F_READ : 0);
break;
case 'a':
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND;
ff = _F_WRITE | _F_APPEND | (plus ? _F_READ : 0);
break;
default:
errno = EINVAL;
return -1;
}
*file_flags = ff;
return oflags;
}
FILE *fopen(const char *path, const char *mode)
{
uint8_t ff;
int oflags = mode_to_flags(mode, &ff);
if (oflags < 0) return NULL;
int fd = open(path, oflags);
if (fd < 0) return NULL;
FILE *fp = (FILE *)malloc(sizeof(FILE));
if (!fp) {
int saved = errno;
close(fd);
errno = saved ? saved : ENOMEM;
return NULL;
}
fp->fd = fd;
fp->flags = ff;
return fp;
}
int fclose(FILE *fp)
{
if (!fp) {
errno = EBADF;
return EOF;
}
/* Don't close stdin/stdout/stderr. */
if (fp == &_stdin || fp == &_stdout || fp == &_stderr) {
return 0;
}
int r = close(fp->fd);
free(fp);
return r < 0 ? EOF : 0;
}
int fflush(FILE *fp)
{
/* Unbuffered — nothing to flush. */
(void)fp;
return 0;
}
/* ---- char-at-a-time -------------------------------------------------*/
int fputc(int c, FILE *fp)
{
if (!fp) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONOUT) {
return putchar(c);
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
uint8_t ch = (uint8_t)c;
if (write(fp->fd, &ch, 1) != 1) {
fp->flags |= _F_ERROR;
return EOF;
}
return (int)ch;
}
int fgetc(FILE *fp)
{
if (!fp) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONIN) {
return getchar();
}
if (!(fp->flags & _F_READ)) { errno = EBADF; return EOF; }
uint8_t ch;
int r = read(fp->fd, &ch, 1);
if (r == 0) { fp->flags |= _F_EOF; return EOF; }
if (r < 0) { fp->flags |= _F_ERROR; return EOF; }
return (int)ch;
}
/* ---- string-at-a-time ----------------------------------------------*/
int fputs(const char *s, FILE *fp)
{
if (!fp || !s) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONOUT) {
while (*s) {
if (putchar((unsigned char)*s++) == EOF) return EOF;
}
return 0;
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
size_t n = strlen(s);
int w = write(fp->fd, s, (uint16_t)n);
if (w < 0 || (size_t)w != n) {
fp->flags |= _F_ERROR;
return EOF;
}
return 0;
}
char *fgets(char *buf, int n, FILE *fp)
{
if (!buf || n < 2 || !fp) return NULL;
int i = 0;
while (i < n - 1) {
int c = fgetc(fp);
if (c == EOF) {
if (i == 0) return NULL;
break;
}
buf[i++] = (char)c;
if (c == '\n') break;
}
buf[i] = '\0';
return buf;
}
/* ---- block-at-a-time -----------------------------------------------*/
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp)
{
if (!ptr || !fp) return 0;
if (size == 0 || nmemb == 0) return 0;
if (fp->flags & _F_CONIN) {
/* line-buffered console read — not very useful but functional. */
char *p = (char *)ptr;
size_t total = size * nmemb;
for (size_t i = 0; i < total; i++) {
int c = getchar();
if (c == EOF) return i / size;
p[i] = (char)c;
}
return nmemb;
}
if (!(fp->flags & _F_READ)) { errno = EBADF; return 0; }
size_t total = size * nmemb;
int r = read(fp->fd, ptr, (uint16_t)total);
if (r < 0) { fp->flags |= _F_ERROR; return 0; }
if ((size_t)r < total) fp->flags |= _F_EOF;
return (size_t)r / size;
}
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp)
{
if (!ptr || !fp) return 0;
if (size == 0 || nmemb == 0) return 0;
if (fp->flags & _F_CONOUT) {
const char *p = (const char *)ptr;
size_t total = size * nmemb;
for (size_t i = 0; i < total; i++) {
if (putchar((unsigned char)p[i]) == EOF) return i / size;
}
return nmemb;
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return 0; }
size_t total = size * nmemb;
int w = write(fp->fd, ptr, (uint16_t)total);
if (w < 0) { fp->flags |= _F_ERROR; return 0; }
return (size_t)w / size;
}
/* ---- positioning ---------------------------------------------------*/
int fseek(FILE *fp, long off, int whence)
{
if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) {
errno = EBADF;
return -1;
}
long r = lseek(fp->fd, off, whence);
if (r < 0) return -1;
fp->flags &= (uint8_t)~_F_EOF;
return 0;
}
long ftell(FILE *fp)
{
if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) {
errno = EBADF;
return -1L;
}
return lseek(fp->fd, 0L, SEEK_CUR);
}
void rewind(FILE *fp)
{
if (!fp) return;
fseek(fp, 0L, SEEK_SET);
fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
}
/* ---- status --------------------------------------------------------*/
int feof (FILE *fp) { return fp && (fp->flags & _F_EOF) ? 1 : 0; }
int ferror(FILE *fp) { return fp && (fp->flags & _F_ERROR) ? 1 : 0; }
void clearerr(FILE *fp) {
if (fp) fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
}
+29
View File
@@ -0,0 +1,29 @@
/*
* getchar via ESTEX RST 10h.
*
* ESTEX 0x30 (WAITKEY): blocks until a key, returns
* A = scan code, D = position code, E = ASCII,
* C = mode flags, B = shift flags.
*
* IX is preserved (RST 10h clobbers it; callers rely on it as frame pointer).
*/
#include <stdio.h>
int getchar(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY
rst #0x10
pop ix
ld a, e ; E = ASCII (already the low byte of our return DE)
or a, a
jr Z, no_ascii
ld d, #0
ret
no_ascii:
ld de, #-1
ret
__endasm;
}
+15
View File
@@ -0,0 +1,15 @@
/*
* print_hex — print a single byte as two uppercase hex digits.
*
* No printf yet; this is what bare-metal debug looks like in stage 3.
*/
#include <stdio.h>
#include <sprinter.h>
void print_hex(uint8_t v)
{
static const char digits[] = "0123456789ABCDEF";
putchar(digits[(v >> 4) & 0x0F]);
putchar(digits[v & 0x0F]);
}
+42
View File
@@ -0,0 +1,42 @@
/*
* putchar — emit one character via ESTEX PUTCHAR ($5B).
*
* Turbo-C convention: this stdio.h function is the FAST path with NO
* attribute control. Whatever ESTEX has cached for the cursor cell is
* used (typically the shell's default colour). Translates '\n' to
* CR LF for C-string semantics.
*
* For coloured output, use putch() / cputs() / cprintf() from <conio.h>
* — those honour textattr / g_text_attr at the cost of being ~10× slower.
*
* SDCC __sdcccall(1): char arg in L (low byte of HL=int). Returns the
* char in DE (SDCC int return).
*/
#include <stdio.h>
int putchar(int c) __naked
{
(void)c;
__asm
ld a, l ; SDCC __sdcccall(1) int HL
push ix
cp #0x0A
jr nz, _pc_emit
ld a, #0x0D ; CR before LF
push af
ld c, #0x5B
rst #0x10
pop af
ld a, #0x0A
_pc_emit:
push af
ld c, #0x5B
rst #0x10
pop af
pop ix
ld e, a
ld d, #0
ret
__endasm;
}
+69
View File
@@ -0,0 +1,69 @@
/*
* puts — C99 fputs(s, stdout) + '\n'.
*
* Turbo-C convention: stdio's `puts` is the FAST path with NO attribute
* control — backed by ESTEX PCHARS ($5C). Cursor cell attributes are
* whatever ESTEX has cached (usually the shell's default).
*
* For coloured output use cputs() / cprintf() from <conio.h>.
*
* Implementation notes:
* - PCHARS does NOT translate '\n' to CR LF, so we copy the string
* into a static buffer expanding each '\n' to CR LF, then append
* the trailing CR LF before the NUL.
* - Avoid trailing PUTCHAR after PCHARS — empirically that sometimes
* drops the next char. Embed the line ending inside the PCHARS
* buffer instead.
* - Strings longer than the buffer fall back to per-char putchar so
* we never silently truncate.
*/
#include <stdio.h>
#include <stdint.h>
#define PUTS_BUF_SIZE 256 /* body bytes before CR expansion */
static char puts_buf[PUTS_BUF_SIZE + 3]; /* +3 for trailing CR LF NUL */
static void pchars(const char *s) __naked
{
(void)s;
__asm
push ix
ld c, #0x5C
rst #0x10
pop ix
ret
__endasm;
}
int puts(const char *s)
{
uint16_t n = 0;
uint16_t i = 0;
while (s[i] && n < PUTS_BUF_SIZE - 1) {
char c = s[i++];
if (c == '\n') {
puts_buf[n++] = '\r';
puts_buf[n++] = '\n';
} else {
puts_buf[n++] = c;
}
}
if (s[i]) {
/* Overflow — char-by-char fallback so we never truncate. */
for (uint16_t k = 0; s[k]; k++)
putchar((unsigned char)s[k]);
putchar('\n');
return 0;
}
puts_buf[n++] = '\r';
puts_buf[n++] = '\n';
puts_buf[n] = 0;
pchars(puts_buf);
return 0;
}
+61
View File
@@ -0,0 +1,61 @@
/*
* solid_helpers.c — small Solid-C compatibility helpers.
*
* Each function maps to the standard printf/sprintf machinery already
* available from SDCC's z80.lib + our overrides. No new syscalls.
*/
#include <stdio.h>
#include <stdint.h>
/* ---- gets — dangerous but Solid-C provides it ---------------------- */
char *gets(char *buf)
{
int i = 0;
int c;
for (;;) {
c = getchar();
if (c == EOF) {
if (i == 0) return 0;
break;
}
if (c == '\n' || c == '\r') break;
buf[i++] = (char)c;
}
buf[i] = 0;
return buf;
}
/* ---- decimal output: use printf %u ---------------------------------- */
void dec8(uint8_t v)
{
printf("%u", (unsigned)v);
}
void dec16(uint16_t v)
{
printf("%u", (unsigned)v);
}
void dec32(uint32_t v)
{
printf("%lu", (unsigned long)v);
}
/* ---- hex output: zero-padded ---------------------------------------- */
void hex8(uint8_t v)
{
printf("%02X", (unsigned)v);
}
void hex16(uint16_t v)
{
printf("%04X", (unsigned)v);
}
void hex32(uint32_t v)
{
printf("%08lX", (unsigned long)v);
}