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