Files
Sprinter-SDCC/libc/cbl/_cbl.h
T
snark13 b34997073e libc/cbl: выключение CBL больше не оставляет железо петь одну ноту
cbl_close() не пишет в порт управления ноль: пока gfx_wait_vsync держит
ссылку ради бита луча (в PoP её берёт pop_pace_arm на всю программу),
bit7 остаётся включённым.  А буфер CBL (256 слотов) при включённом bit7
крутится по кругу и не чистится ничем — железо бесконечно поёт хвост
последнего сэмпла, тоном и громкостью по последней мелодии.  Пойман
пользователем на железе: Ctrl+S во время музыки и пропуск заставки
давали ноту до следующего cbl_open.

В MAME не воспроизводится: "немым" кодом частоты был reserved-код 2, у
которого divs[2]==0 и таймер не заводится вовсе.  На железе reserved-коды
не определены, ЦАП тактируется — тишина держалась на свойстве эмулятора,
а не железа.

Теперь тишину даёт СОДЕРЖИМОЕ БУФЕРА:
- _cbl_port_sync() после каждой записи в порт управления зовёт
  _cbl_prime(0x80).  Это закрывает и паузу звука, и включение bit7 ради
  луча на холодном старте (в буфере лежал мусор от прошлой программы), и
  полное выключение — при bit7=0 те же 256 записей уходят прямо в ЦАП
  обычного COVOX и центруют его, снимая щелчок;
- _CBL_VSYNC_FREQ переведён с reserved-кода 2 на документированный 8
  (7,8125 кГц): поведение определено и на железе, и в MAME, прерывания
  по-прежнему выключены (bit4=0), а бит 7 порта 0xFE трамплин смотрит
  только при живом хуке насоса;
- cbl_close() зовёт sync внутри той же DI-скобки, где снимает хук, иначе
  насос долил бы буфер уже после заливки.

Цена: +2 Б программам со звуком, +11 Б графическим (тянется _cbl_prime
следом за gfx_wait_vsync), 256 OUT'ов (~0,3 мс) на редкое событие —
эталоны cblstream/cbltest/cblwav/gfx_dbuf обновлены.  Рост atlas в
size-check к этой правке отношения не имеет (замерен тот же и без неё).

Проверено: кодоген _cbl_port.asm; SprPoP пересобран и прогнан в MAME
(титры → пропуск заставки → уровень 1, пейсинг по лучу жив).  Сам баг
проверяется только на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 10:55:15 +03:00

105 lines
7.7 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* _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 <stdint.h>
#include <cbl.h>
#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