/* * 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 #include #include /* 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; } /* getkey — like getch() but exposes BOTH the ASCII value and the * positional scan code, so callers can distinguish extended keys * (arrows, F1..F12, PgUp/PgDn, Home/End, Ins/Del — all of which carry * ASCII == 0 from ESTEX) from plain ASCII keys. * * return = (scan << 8) | ascii * * For plain keys: ascii holds the character, scan holds the positional * code (bit 7 set when Ctrl/Alt/Shift is held). * For extended keys: ascii == 0, scan identifies the key (see KEY_* in * ). */ uint16_t getkey(void) __naked { __asm push ix ld c, #0x30 ; ESTEX WAITKEY: A=ASCII, D=scan, E=ASCII rst #0x10 pop ix ld e, a ; E = ASCII (defensive: A is the canonical copy) ret ; __sdcccall(1) returns uint16_t in DE __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 scroll(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t direction, uint8_t clear) __naked { (void)x; (void)y; (void)w; (void)h; (void)direction; (void)clear; __asm pop iy ; return address ld d, l ; D = row (y) ld e, a ; E = col (x) pop hl ; H = heigth(h), L = width(w) pop bc ; C = direction, B = clear ld a, b ; A = clear(B) ld b, c ; B = direction(C) push ix ld c, #0x55 ; ESTEX SCROLL rst #0x10 pop ix jp (iy) __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]; }