/* * _cbl.h — внутренности CBL-модуля (НЕ публичный заголовок). * * Механика (форум Sprinter Team + официальная документация "5.3 * COVOX-Blaster" + docs/converted/accel_r.txt): * - порт управления 0x004E — 16-битный (только ld bc,#0x004E / * out (c),a): bit7 = CBL on, bit6 = stereo, bit5 = 16-бит, * bit4 = int enable, bits3..0 = частота; выключение — запись 0; * - буфер 256 Б аппаратно разбит на две половины по 128 Б; бит 7 * порта 0xFE отражает, какая половина ИГРАЕТ сейчас — флип этого * бита поднимает прерывание (вектор 0xFF), пора долить неактивную * половину; * - размер блока — 128 Б для 8-бит формата, 256 Б для 16-бит (не * зависит от моно/стерео) — см. _cbl_block; * - выдача блока — ДВА способа (cbl_push_otir.c/cbl_push_accel.c): * OTIR в порт 0x4F, либо акселератором в спец-страницу EMM 0xFD * (маппится в W3 на 0xC000); библиотека сама НИЧЕГО не пропихивает * — это делает fill()-callback приложения; * - насос вызывается из кадрового трамплина через хук _irq_cbl_hook * (см. _irq.h) — прямой ссылки irq→cbl нет, DCE сохраняется. * * Никакого собственного кольца НЕТ — второй буфер поверх аппаратного * был бы избыточен. _cbl_fill — callback приложения; _cbl_silence — * буфер тишины, malloc'ится в cbl_open() ТОЛЬКО при * CBL_UNDERRUN_SILENCE (иначе NULL). * * _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих * держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL- * сессии. Нужно gfx_wait_vsync() (см. libbgi/common/gfx_wait_vsync.c): * бит 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp * kbd_fe_r), но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный * CBL уже открыт — портом владеет он, ref/unref его не трогают; иначе * включают bit7 "немым" кодом частоты _CBL_VSYNC_FREQ и заливают * аппаратный буфер тишиной — БЕЗ ЗАЛИВКИ ЖЕЛЕЗО ПОЁТ (см. ниже). * * ЖЕЛЕЗО ПОЁТ ТО, ЧТО ЛЕЖИТ В БУФЕРЕ. Буфер CBL — 256 слотов, которые * при включённом bit7 крутятся ПО КРУГУ и которые никто не чистит; * фирменный пример Sprinter Team (docs/converted/Forum.txt, "Самый * простой пример") на этом и построен: залил 256 отсчётов — "должен * пищать одну ноту". Значит любое состояние "bit7 включён, а насос не * работает" — это бесконечная нота из хвоста последнего сэмпла. * * В MAME этого не видно: там код частоты 2 не заводил таймер вовсе * (divs[2]=0, sprinter.cpp:822), поэтому "немой" режим был тих даром. * На железе код 2 — reserved (Forum.txt:704), поведение не определено, и * ЦАП тактируется: пользователь поймал бесконечную ноту после Ctrl+S и * после пропуска заставки (2026-09-02). Поэтому теперь: (1) частота — * документированная, самая медленная; (2) буфер ВСЕГДА оставляем залитым * тишиной — и на входе в "немой" режим, и при полном выключении (там те * же записи центруют ЦАП обычного COVOX, bit7=0 → порт данных идёт прямо * в ЦАП). Цена — 256 OUT'ов (~0,3 мс) на редкое событие. */ #ifndef _CBL_INTERNAL_H #define _CBL_INTERNAL_H #include #include #define _CBL_CTRL_PORT 0x004E #define _CBL_DATA_PORT 0x4F #define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */ #define _CBL_VSYNC_FREQ 0x08 /* "немой" режим: самый медленный ДОКУМЕНТИРОВАННЫЙ * код (7,8125 кГц). Тишина обеспечена не * остановкой таймера (это свойство MAME, не * железа), а залитым тишиной буфером. */ extern uint16_t _cbl_block; /* размер блока: 128 или 256 (по формату) */ extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */ extern uint16_t _cbl_undr; /* из них недолив (fill вернул 0/не задан) */ extern cbl_fill_fn _cbl_fill; /* callback приложения, может быть NULL */ extern uint8_t _cbl_underrun_mode; /* CBL_UNDERRUN_* */ extern uint8_t *_cbl_silence; /* буфер тишины или NULL */ extern uint8_t _cbl_port_refs; /* держатели bit7 помимо реальной CBL-сессии */ /* Запись в 16-битный порт управления 0x004E. */ void _cbl_ctrl(uint8_t v); /* Общее тело обеих публичных точек входа (cbl_open / cbl_open_silence). * Буфер тишины не трогает — им владеет cbl_open_silence.c; так malloc не * приезжает в резидент приложениям, которые льют тишину сами. */ int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode, uint8_t underrun_mode, cbl_fill_fn fill); /* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после * ЛЮБОЙ записи в порт управления: при включении — иначе первые 23 мс * играет то, что лежало в буфере раньше; при выключении — иначе железо * крутит хвост последнего сэмпла бесконечной нотой. Подробный разбор — * в шапке _cbl_prime.c. */ void _cbl_prime(uint8_t silence); /* ISR-насосы: вызвать fill(), при недоливе — тишина по режиму (один * регистрируется в _irq_cbl_hook при cbl_open — по выбору CBL_PUMP_*). */ void _cbl_pump_otir(void); void _cbl_pump_accel(void); /* Разделяемое владение портом 0x004E (см. описание выше). * _cbl_port_sync пересчитывает нужное значение порта из текущего * состояния (реальная сессия? держатели?) и пишет его — вызывается * из ref/unref, а также из cbl_close() после снятия _irq_cbl_hook. */ void _cbl_port_ref(void); void _cbl_port_unref(void); void _cbl_port_sync(void); #endif