gfx_wait_vsync() и sleep() раньше предполагали, что КАЖДОЕ прерывание на векторе 0xFF — кадровый тик; с CBL/клавиатурой на том же векторе это уже не так. gfx_wait_vsync(): вместо halt — polling бита 5 порта 0xFE (реальная позиция луча, см. MAME kbd_fe_r), доступного пока включён CBL bit7 порта 0x004E. Разделяемое владение портом с CBL через _cbl_port_ref/unref (тот же ref-counting паттерн, что у IM2-таблицы) — cbl_close() возвращает "немой" режим вместо полного выключения, если gfx ещё держит ссылку. Fallback на halt при таймауте. sleep()/delayms(): калиброванный busy-wait по духу delayms.asm вместо подсчёта halt-пробуждений. Калибровка одна на кадр (не на секунду — не переполняет uint16_t и не требует умножения/32-бит арифметики), общий движок libc/time/_sleep_calib.c для обеих функций. Fallback на старое поведение при EBUSY (фрейм-хук занят другим irq_install()). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
22 KiB
libc — справочник API (2026-07-06)
Сводка по заголовкам: сигнатура + одна строка + особенности ABI. Детали дизайна: docs/libc-headers.md (контракт затенения SDCC), docs/file-buffering-design.md (FILE*), docs/solid_c_compatibility.md.
Общие соглашения:
- ошибки: возврат -1/NULL/EOF +
errno(код DSS as-is, см. errno.h); - SDCC
__sdcccall(1): 1-й аргумент HL (8-битный — A), 2-й — DE, остальные на стеке; int/указатель возвращается в DE; - строки для BIOS-вызовов (rst 8) должны лежать в #4000–#BFFF;
- стек при любых ESTEX/BIOS-вызовах — в W2 (обеспечено crt0).
<stdio.h> — полная замена SDCC (контракт: printf-семейство из z80.lib)
Из SDCC z80.lib: printf sprintf vprintf vsprintf.
Консоль (ESTEX, без атрибутов — быстрый путь; цветной вывод — conio):
| Сигнатура | Описание |
|---|---|
int putchar(int c) |
символ через PUTCHAR $5B; '\n'→CR LF |
int getchar(void) |
блокирующий WAITKEY $30, ASCII |
char puts(const char *s) |
строка + '\n' (посимвольно через putchar) |
char *gets(char *buf) |
строка с консоли, без контроля длины |
void dec8/dec16/dec32(v) |
десятичная печать без ведущих нулей |
void hex8/hex16/hex32(v) |
hex-печать фиксированной ширины |
FILE* (буферизованный, вариант B+ — единый ленивый буфер BUFSIZ=512
на чтение/запись с автопереключением; таблица OPEN_MAX=8 слотов;
exit() сбрасывает всё через atexit; ошибки записи отложенные —
проверять fclose):
| Сигнатура | Описание |
|---|---|
FILE *fopen(path, mode) |
"r/w/a" + '+', 'b/t' игнорируются |
FILE *fdopen(fd, mode) |
завернуть готовый fd (закрывать fclose!) |
FILE *freopen(path, mode, fp) |
переоткрыть тот же FILE* |
int fclose(FILE*) / void fclosall(void) |
сброс+закрытие / все потоки |
int fflush(FILE*) |
сброс записи / откат readahead; NULL = все |
int fgetc/fputc(...) |
горячий путь на asm; getc/putc — макро-алиасы |
char *fgets(buf, n, fp) |
до '\n' (сохраняется); блочный LDI-сканер |
int fputs(s, fp) |
без '\n'; через fwrite |
size_t fread/fwrite(p, sz, n, fp) |
блоки ≥ 512 идут мимо буфера |
int ungetc(c, fp) |
1 байт putback (и на stdin) |
int fseek(fp, off, whence) / long ftell(fp) |
ftell без побочных эффектов |
void rewind(fp) |
fseek(0) + сброс EOF/ERROR |
int fgetpos/fsetpos(fp, &pos) |
fpos_t = long |
int feof/ferror(fp), void clearerr(fp) |
флаги потока |
int fprintf/vfprintf(fp, fmt, ...) |
vsprintf в статический буфер 256 |
int scanf/fscanf/sscanf(...) |
%d %u %x %o %c %s, l, ширина, %*, %% |
int rename(old, new) |
ESTEX RENAME $10 |
stdin/stdout/stderr — консольные псевдопотоки (fd 0/-1/-2), не
буферизуются; freopen на них меняет только FILE*-операции (printf
идёт в ESTEX напрямую).
<stdlib.h> — include_next + добавки
Из SDCC: malloc/free/calloc/realloc (heap в W2), atoi/atol/strtol/
strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv, exit-типы.
Наше: int16_t min(a,b), int16_t max(a,b) (функции, как в Solid-C).
<string.h> — include_next + добавки
Из SDCC: mem*/str* полностью. Наше: char *strlwr/strupr(char *s) —
in-place регистр, латиница + кириллица CP866.
<time.h> — полная замена (struct tm в SDCC-ABI, __TIME_UNSIGNED=1)
| Сигнатура | Описание |
|---|---|
void getdatetime(datetime_t*) |
RTC как есть (ESTEX SYSTIME $21) |
int setdatetime(const datetime_t*) |
установка RTC ($22) |
time_t time(time_t*) |
Unix-эпоха из RTC |
mktime/gmtime/localtime/asctime/ctime |
POSIX поверх RTC (без TZ) |
datetime_t: day/month/year(полный)/hour/minute/second/dow (1=Вс).
<unistd.h>, <fcntl.h> — fd-уровень (манипуляторы DSS)
| Сигнатура | Описание |
|---|---|
int open(path, flags) |
O_RDONLY/WRONLY/RDWR + O_CREAT/TRUNC/EXCL/APPEND (ESTEX $11/$0A/$0B) |
int creat(path, mode) |
open(W |
int read/write(fd, buf, n) |
ESTEX $13/$14. Квирк WRITE: DE-возврат ненадёжен, успех = CF=0&A=0 (см. memory/estex_write_de_quirk) |
int close(fd) |
ESTEX $12 |
long lseek(fd, off, whence) |
32-битная позиция (MOVE_FP $15) |
int unlink(path) |
удалить (DELETE $0E) |
int isatty(fd) |
fd <= 0 (файловые манипуляторы DSS с 1) |
int mkdir/rmdir/chdir(path) |
ESTEX $1B/$1C/$1D |
char *getcwd(buf, size) |
буфер 256 байт, size игнорируется |
void sleep(seconds) |
калиброванный busy-wait (см. ниже) |
void delayms(uint16_t ms) |
то же, гранулярность — миллисекунды (см. ниже) |
Лимит: 8 одновременных манипуляторов; 9-й OPEN вешает DSS — libc отказывает сама (EMFILE, предохранитель _fd_guard).
sleep() (2026-07-07, редизайн): старая версия считала halt-
пробуждения (50 = 1 c), предполагая, что КАЖДОЕ прерывание — кадровый
тик; с CBL/клавиатурой на векторе 0xFF это уже не так (CBL прерывает
намного чаще кадра — sleep() возвращался бы раньше срока). Теперь:
лениво, один раз калибруется кратковременным irq_install() против
РЕАЛЬНОГО кадрового тика (трамплин зовёт хук только на настоящих
кадровых прерываниях — клавиатура/CBL уходят в свои ветки раньше),
считая, сколько итераций тесного цикла умещается в один КАДР (не в
секунду — калибровка на секунду переполняла uint16_t, был баг:
sleep(5) отрабатывал быстрее секунды, найден пользователем на
реальном прогоне и исправлен). sleep(seconds) — вложенный цикл:
внешний по секундам, внутренний ровно 50 раз калиброванный busy-wait —
без единого прерывания и БЕЗ умножения/32-битной арифметики (по духу
docs/samples/delayms.asm, который тоже калибрует на 1 мс, а не на
1 с). Погрешность 5-10% (калибровочный и рабочий циклы не тактово-
идентичны). Fallback на старое "50 halt" поведение, если фрейм-хук уже
занят другим +580 Б) — тянет весь модуль irq_install()-клиентом (EBUSY).
Cтоит дороже по размеру (irq
(install/remove/трамплин/IM2-таблицу), даже если программа больше
ничего из irq не использует.
delayms(ms) — тот же движок (libc/time/_sleep_calib.c), общий с
sleep(): калибровка одна на двоих (первый вызов ЛЮБОЙ из функций
калибрует, вторая просто использует готовое). Производная величина
"итераций на 1 мс" — одно 16-битное деление (__divuint, НЕ
__mullong) на константу 20, вычисляется один раз при калибровке;
delayms(ms) — простой цикл ms раз, без умножения вообще. Fallback
при EBUSY — грубый (одно кадровое halt), точной альтернативы для
миллисекундной гранулярности без калибровки нет.
Квирк общий для sleep()/delayms(): калибровка сама стоит ~20-40 мс
(синхронизация на границу кадра + сам замер) — ПЕРВЫЙ вызов ЛЮБОЙ из
двух функций в программе превысит запрошенное время на эту величину;
для delayms() с маленьким ms это заметно (delayms(5) на первом
вызове может растянуться на ~25-45 мс). Все последующие вызовы точны.
Оставлено как есть по решению пользователя — не стали усложнять API
отдельным calibrate()-примитивом.
<errno.h>
errno (int), коды = коды DSS (EOK..EUNKERR) + POSIX-имена
(ENOENT/EBADF/EMFILE/…) + алиасы Solid-C (EZERO/EINVFNC/ENOFILE/…).
const char *strerror(int), void perror(const char*).
<dos.h> — DOS-слой Solid-C
| Сигнатура | Описание |
|---|---|
void getdate/gettime(&d) |
struct date/time (Turbo-C; ti_hund=0) |
int setdate/settime(&d) |
RMW полного datetime |
uint8_t getdisk(void) |
текущий диск, 0=A (ESTEX $02) |
int setdisk(uint8_t) |
смена диска; возврат = число дисков ($01) |
int absread/abswrite(disk, sect, cnt, buf) |
секторы ЛОГИЧЕСКОГО диска (BIOS $55/$56); буфер в #4000–#BFFF; abswrite минует ФС! |
<dir.h>
int ffirst(pattern, ffblk_t*, attrib) / int fnext(ffblk_t*) —
поиск по шаблону (ESTEX $19/$1A). Квирк: "."/".." находятся только
итерацией "." (memory/estex_ffirst_dotdot). FA_*-атрибуты.
<sys/stat.h>
int stat(path, struct stat*) / int fstat(fd, ...) — st_mode
(S_ISREG/S_ISDIR), st_size, st_mtime (Unix-эпоха).
<conio.h> — текстовый экран с атрибутами (Turbo-C стиль)
Клавиатура: kbhit getch getche getkey (+KEY_* коды позиций),
char *cgets(buf).
Вывод с атрибутом: putch cputs cprintf (~10× медленнее stdio-пути;
'\n' НЕ транслируется — писать "\r\n").
Атрибуты: textcolor textbackground textattr, set/get_text_attr,
COLOR_*-enum, COLOR(fg,bg), COLOR_BLINK; set/get_putch_raw_mode.
Экран: clrscr clrscr_attr gotoxy home() wherex wherey wherexy scroll wrchar rdchar; режимы gettextmode/settextmode (0x02=40×32,
0x03=80×32).
Порты/IRQ: inp outp enable() disable().
Текстовая палитра: text_pal_load/set_color/get/get_color/reset
(план 0..3 → страница BIOS 4..7).
<bios/text.h> — быстрый BIOS-вывод (rst 8, place-based)
bios_set_place/get_place, bios_write[attr][_until|_stop],
bios_fillchar/fillattr/fillcharattr, bios_clearwin[_ch],
bios_scrollwin. Строка s — в #4000–#BFFF; place продвигается.
<gfx.h> — графика (0x81: 320×256×256; 0x82: 640×256×16)
Setup: gfx_init(mode,page)→prev, gfx_done(prev).
Страницы/банк: gfx_set/get_visible_page, gfx_set/get_draw_page
(double buffering), gfx_set/get_bank (0x50..0x5F, 0x58 = FF-
прозрачность).
gfx_wait_vsync() (2026-07-07, редизайн): ждёт переход бита 5 порта
0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало
кадра, см. MAME sprinter.cpp kbd_fe_r), а не прерывание — поэтому не
путается с клавиатурой/CBL/CTC, деляющими вектор 0xFF. Бит доступен
только пока включён cbl_mode() (bit7 порта 0x004E) — если приложение
уже играет через cbl_open(), бит достаётся бесплатно; иначе
gfx_wait_vsync() лениво занимает bit7 "немым" кодом частоты через
_cbl_port_ref()/_cbl_port_unref() (см. <cbl.h>, _cbl_port.c) —
разделяемое владение портом 0x004E, безопасное при любом порядке
использования с реальным CBL-звуком. Фолбэк на одно кадровое
прерывание (halt), если бит не ведёт себя как ожидается за разумное
число попыток.
Примитивы (суффикс _256 / _16): clear putpixel hline vline line rect fill_rect — через акселератор (hline/vline burst до 256 байт;
в 0x82 vline через RMW). Координаты int, клиппинг по краям.
Текст: gfx_putchar256/16, gfx_text256/16 (8×8, fg/bg; в 0x82 x —
чётный), gfx_load_default_font, gfx_set_font(ptr) (interleaved
font[row*256+char]).
Палитра: gfx_pal_load/set/get/get_color/reset (страницы 0..3).
<irq.h> — user-ISR кадрового прерывания (IM 2)
| Сигнатура | Описание |
|---|---|
int irq_install(isr_t h) |
h зовётся ~50 Гц на кадровых прерываниях; DSS-обработчик чейнится всегда (клавиатура/SYSTIME/мышь живы). 0 / -1+errno (EBUSY повтор, EINVAL — код не в W2: только tiny/big) |
void irq_remove(void) |
вернуть таблицу DSS; идемпотентно; висит на atexit |
int irq_ctc_install(h, div2, div3) |
периодический таймер CTC (вектор 0x06): f = 875000/(div2×div3), div 0=256; пресет кадра IRQ_CTC_VSYNC_DIV2/3 (112×160, ~48.8 Гц); независим от кадрового; трамплин завершает RETI |
void irq_ctc_remove(void) |
глушит CTC (обязательно; atexit подстрахует) |
IRQ_DISABLE()/IRQ_ENABLE() |
di/ei — скобки для чтения shared-переменных из main |
Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор, banked-функции; только volatile-глобалы и быстрая работа (<1 мс).
<cbl.h> — потоковый звук CBL/COVOX (вектор 0xFF, свой ISR)
Без собственного кольца (2026-07-07): у CBL уже есть аппаратный буфер
256 Б (2×128, двойная буферизация на стороне железа — см. официальную
доку "5.3 COVOX-Blaster"); библиотека просто зовёт fill() приложения
ИЗ ISR, а оно само пропихивает данные (откуда угодно) через
cbl_push_otir/accel — без промежуточной копии.
| Сигнатура | Описание |
|---|---|
int cbl_open(freq_code, fmt, pump_mode, underrun_mode, fill) |
включить CBL (0x90|fmt|freq), зарегистрировать callback; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode/underrun_mode плохие или OTIR+16-бит, ENOMEM — буфер тишины) |
void cbl_close(void) |
выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit |
void cbl_push_otir(const void *src, uint16_t n) |
пропихнуть n байт через otir в порт 0x4F; звать ИЗ fill() |
void cbl_push_accel(const void *src, uint16_t n) |
то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит |
uint16_t cbl_requests(void) |
счётчик запросов блока ISR (~fs/(сэмплов в блоке) в секунду) |
uint16_t cbl_underruns(void) |
счётчик недоливов (fill вернул 0 или не задан) |
CBL_FREQ_7K8 .. CBL_FREQ_109K |
коды частоты (биты 3..0 control-порта) |
CBL_FMT_MONO8/MONO16/STEREO8/STEREO16 |
формат (биты 5/6 control-порта); блок 128 Б (8-бит) или 256 Б (16-бит), не зависит от моно/стерео; тишина 0x80 (8-бит) / 0x0000 (16-бит, знаковый) |
CBL_PUMP_OTIR / CBL_PUMP_ACCEL |
способ выдачи — см. ниже |
CBL_UNDERRUN_APP / CBL_UNDERRUN_SILENCE |
поведение при недоливе — см. ниже |
typedef int (*cbl_fill_fn)(uint16_t n); — callback, зовётся ИЗ ISR за
очередным блоком (n = _cbl_block, 128/256). Обязан быть быстрым
— никаких ESTEX/BIOS/gfx-вызовов (тот же констрейнт, что у
irq_install()-хендлера); вернуть ненулевое, если реально пропихнул n
байт. Диск (read()) читать из fill() НЕЛЬЗЯ — см. tests/cblstream,
где под это заведено кольцо уровня приложения.
Два насоса (3-й параметр cbl_open):
- OTIR (
_cbl_pump_otir) —cbl_push_otir()в порт 0x4F; базовый. НЕ умеет 16-бит —cbl_open(..., MONO16/STEREO16, CBL_PUMP_OTIR, ...)вернёт EINVAL: по исходнику MAME (sprinter.cpp) порт данных ВСЕГДА кладёт байт как есть в один слот, не собирая пару байт в 16-бит значение и не сверяясь с 16-бит флагом вообще. - ACCEL (
_cbl_pump_accel) —cbl_push_accel()через акселератор в спец-страницу EMM 0xFD, замапленную в окно W3 на 0xC000 (см. docs/converted/accel_r.txt, Forum.txt); размер блока патчится SMC (LD D,D+ immediateLD A,n+LD L,L— тот же паттерн, что и в libc/gfx/_gfx_hfill256.c); единственный путь для 16-бит. Verified в MAME 2026-07-07 (tests/cbltest, вся accel-половина матрицы прошла без ошибок/underrun).
Поведение при недоливе (4-й параметр cbl_open):
- CBL_UNDERRUN_APP (по умолчанию, 0) — не забота библиотеки, буфер тишины НЕ аллоцируется, в CBL доигрывает то, что уже лежало в его аппаратном буфере.
- CBL_UNDERRUN_SILENCE (1) — насос сам пропихивает тишину; буфер
(128/256 Б по формату) аллоцируется malloc'ом ВНУТРИ
cbl_open()только в этом режиме.cbl_underruns()считает недоливы в обоих режимах — диагностика, поведение не меняет.
Приватное прерывание, к DSS не чейнится; бит 7 порта 0xFE (запрос
блока) читается только пока cbl_open не закрыт (при выключенном CBL
бит всегда 1 — MAME-квирк).
Разделяемое владение портом 0x004E (2026-07-07): бит 5 порта 0xFE
(позиция луча — см. <gfx.h> gfx_wait_vsync()) доступен только пока
включён bit7 порта 0x004E, независимо от того, играет ли реальный
звук. _cbl_port_ref()/_cbl_port_unref() (internal, _cbl_port.c)
дают gfx-модулю занять bit7 "немым" кодом частоты (не заводящим таймер
CBL — без звука/прерываний), не мешая реальной cbl_open()-сессии,
если она уже идёт (и наоборот — cbl_close() возвращает "немой" режим
вместо полного выключения порта, если gfx его ещё держит).
<palette.h> — низкий уровень (BIOS $A4/$A6)
pal_load pal_get pal_set_color pal_get_color (страница 0..7,
записи B,G,R,0), pal_reset(type) / pal_reset_at(type,page,graph);
PAL_GRAPH/PAL_SINCLAIR/PAL_CGA.
<mouse.h> — драйвер RST 30h
mouse_init show hide refresh read(mouse_state_t*) goto bounds_x/y text_cursor load_cursor/get_cursor(mouse_cursor_t*) set_sensitivity get_sensitivity_x/y video_mode_changed. Sensitivity = делитель
(меньше = быстрее). Solid-C алиасы ms_* включены.
<sprinter.h> — платформа
Константы портов (PORT_PAGE_W0..W3, PORT_RGADR, PORT_RGMOD), номера
всех ESTEX-функций (ESTEX_*), BIOS EMM ($C0..$C7); __sfr-доступ и
inline sprinter_page_w0..w3(page); ENV: getenv putenv sysenv.
<sprinter_mem.h> — EMM-страницы и банковый I/O
mem_alloc_pages(n)→blk_id, mem_free_block, mem_get_page(blk,idx), mem_info(&total,&free) (реализации _bios/estex; макро-выбор
MEM_MANAGE_MODE*). HOME-резидентный доступ к чужим страницам:
bank_load_byte/store_byte/read/write (своп W3 внутри; *_w1 —
вариант через окно W1).
<sprinter_exit.h>
atexit (LIFO, 8 слотов), exit (хендлеры+сброс FILE), _exit.
<sprinter_compat.h> / <sprinter_solid.h>
Типы (BYTE/BOOL/WORD/uint/FD/f_point), TRUE/FALSE/OK/ERROR,
setmem movmem (порядок аргументов!), strerr seek tell ltell remove _ffirst _setargv abort(), isascii. <sprinter_solid.h> —
зонтичный: один include для портирования Solid-C программ.