8490288d79
Два бага одного пути завершения, оба видны только на железе.
1. ЗВУК ПРОДОЛЖАЛСЯ ПОСЛЕ ВЫХОДА. cbl_close() закрывает СЕССИЮ, но не
гасит железо: bit7 порта 0x004E держит gfx_wait_vsync ради бита луча,
поэтому порт оставался включённым ("немой" режим), и CBL крутил свои 256
слотов уже под шеллом — тихо ровно до первой чужой записи в порт данных,
а дальше она зацикливалась. Новый cbl_shutdown() гасит bit7 независимо
от держателей и центрует ЦАП обычного COVOX; pop_shutdown зовёт его
последним действием, а _cbl_open_raw регистрирует в atexit его, а не
cbl_close.
2. ЦЕПОЧКА atexit НЕ ВЫПОЛНЯЛАСЬ ПРИ ВОЗВРАТЕ ИЗ main. crt0 уходил прямо
в ESTEX EXIT, то есть нарушал контракт C (возврат из main = exit(status)).
Молча терялись не только гашение звука и снятие vsync-ссылки, но и
_fclosall: буферизованная запись в файлы пропадала, если программа не
звала exit() явно. Теперь crt0 после main дёргает _atexit_hook.
Косвенность обязательна: прямая ссылка crt0 на разматыватель притащила бы
его и стек хендлеров в КАЖДУЮ программу. Указатель живёт в отдельном
data-модуле (два байта _DATA, ни байта кода), ставит его сам atexit() при
первой регистрации — нет регистраций, нет и кода. Тот же приём, что у
_irq_cbl_hook.
Цена замерена: +14 Б всем программам (блок в crt0) и +64 Б тем
одиннадцати, что реально регистрируют хендлеры (CBL, файловые через
_fclosall, irqtest, gfx_dbuf, solidt) — у них раньше эти хендлеры были
мёртвым кодом. Эталоны обновлены (кроме atlas: его +434 Б не отсюда,
замерен тот же и без этих правок).
Проверено в MAME: старт и звук как были, выход по F10 возвращает в шелл
чисто (текстовый режим восстановлен, зависания нет), повторный запуск
работает. Пункт 1 проверяется только на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
150 lines
10 KiB
C
150 lines
10 KiB
C
/*
|
||
* cbl.h — COVOX-Blaster: потоковый вывод звука через прерывания.
|
||
*
|
||
* CBL — COVOX с буферным ОЗУ (256 Б, аппаратно разбито на ДВЕ половины
|
||
* по 128 Б — двойная буферизация целиком на стороне железа, см.
|
||
* официальную документацию "5.3 COVOX-Blaster"): бит 7 порта 0xFE
|
||
* отражает, какая половина СЕЙЧАС играет; когда она флипает, вектор
|
||
* 0xFF поднимает прерывание — пора долить НЕАКТИВНУЮ половину.
|
||
*
|
||
* Библиотека НЕ держит собственного кольца — оно было бы лишним
|
||
* вторым буфером поверх аппаратного. Вместо этого при cbl_open()
|
||
* регистрируется callback fill(): ISR вызывает его напрямую, а
|
||
* callback САМ пропихивает n байт из данных приложения (откуда бы они
|
||
* ни жили — статический массив, банковая EMM-страница, файл) прямо в
|
||
* CBL через cbl_push_otir()/cbl_push_accel() — без промежуточной
|
||
* копии в libc.
|
||
*
|
||
* fill(n) ОБЯЗАН быть быстрым (то же ограничение, что у irq_install()
|
||
* — никаких ESTEX/BIOS/gfx-вызовов, только память и один push) и
|
||
* вернуть ненулевое значение, если реально пропихнул n байт; 0 — если
|
||
* данных сейчас нет (недолив/underrun).
|
||
*
|
||
* Формат данных (bits 6/5 порта 0x4E; см. Forum.txt "Звук через
|
||
* COVOX-Blaster"):
|
||
* 8-бит: unsigned, центр 0x80 (моно — один байт/сэмпл;
|
||
* стерео — чередование L/R байт)
|
||
* 16-бит: signed, центр 0x0000, little-endian (моно — int16/сэмпл;
|
||
* стерео — чередование L/R int16)
|
||
* Блок запроса — 128 байт для 8-бит, 256 байт для 16-бит (не зависит
|
||
* от моно/стерео — это ПОЛНЫЙ байтовый блок, раскладка каналов внутри
|
||
* него на размер не влияет); n, приходящий в fill(), равен этому блоку.
|
||
*
|
||
* Способ выдачи блока — CBL_PUMP_* (см. cbl_open):
|
||
* OTIR — cbl_push_otir(), блочный вывод в порт 0x4F; НЕ умеет
|
||
* 16-бит (см. ниже) — cbl_open(..., 16-бит формат,
|
||
* CBL_PUMP_OTIR, ...) вернёт EINVAL.
|
||
* ACCEL — cbl_push_accel(), запись в спец-страницу EMM 0xFD
|
||
* (маппится в W3 на 0xC000) через акселератор (см.
|
||
* docs/converted/accel_r.txt); единственный способ получить
|
||
* настоящий 16-бит сэмпл.
|
||
*
|
||
* OTIR + 16-бит запрещён не просто "хуже", а физически не работает:
|
||
* по исходнику эмулятора (MAME sprinter.cpp) порт данных 0x4F ВСЕГДА
|
||
* кладёт байт как есть в один слот, не собирая пару байт в 16-бит
|
||
* значение — это умеет только акселераторный путь (страница 0xFD).
|
||
*
|
||
* Поведение при недоливе задаётся ВЫБОРОМ ТОЧКИ ВХОДА (cbl_open /
|
||
* cbl_open_silence), а не параметром:
|
||
* CBL_UNDERRUN_APP (по умолчанию, 0) — недолив не наша забота;
|
||
* в CBL доигрывает то, что уже лежит в его буфере (никакого
|
||
* буфера тишины не аллоцируется).
|
||
* CBL_UNDERRUN_SILENCE (1) — насос сам пропихивает тишину при
|
||
* недоливе; буфер тишины (128/256 Б по формату) аллоцируется
|
||
* malloc'ом внутри cbl_open_silence(), только в этом режиме.
|
||
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
|
||
* поведение не меняет.
|
||
*
|
||
* cbl_close() обязателен — иначе CBL продолжит прерывать шелл после
|
||
* выхода. НО ОН НЕ ГАСИТ ЖЕЛЕЗО ПОЛНОСТЬЮ: пока bit7 держит
|
||
* gfx_wait_vsync() ради бита луча, порт остаётся включённым в "немом"
|
||
* режиме, и буфер крутится под шеллом. Перед возвратом из main звать
|
||
* cbl_shutdown() — он выключает всё и центрует ЦАП.
|
||
*
|
||
* На atexit тут полагаться НЕЛЬЗЯ: цепочку разматывает только exit(), а
|
||
* crt0 при возврате из main уходит прямо в ESTEX EXIT.
|
||
*
|
||
* Требования как у <irq.h>: код/данные в W2 (tiny/big).
|
||
*/
|
||
|
||
#ifndef CBL_H
|
||
#define CBL_H
|
||
|
||
#include <stdint.h>
|
||
|
||
/* Выключить звук ПОЛНОСТЬЮ перед завершением программы: закрыть сессию,
|
||
* снять bit7 независимо от держателей и оставить ЦАП в центре. Звать
|
||
* последним действием завершения; идемпотентно. */
|
||
void cbl_shutdown(void);
|
||
|
||
/* Коды частоты дискретизации (bits 3..0 порта 0x4E; из форума
|
||
* Sprinter Team; коды 0/1 — legacy, не использовать). */
|
||
#define CBL_FREQ_7K8 0x8 /* 7.8125 кГц */
|
||
#define CBL_FREQ_10K9 0x9 /* 10.9375 кГц */
|
||
#define CBL_FREQ_15K6 0xA /* 15.625 кГц */
|
||
#define CBL_FREQ_21K9 0xB /* 21.875 кГц */
|
||
#define CBL_FREQ_31K3 0xC /* 31.25 кГц */
|
||
#define CBL_FREQ_43K8 0xD /* 43.75 кГц */
|
||
#define CBL_FREQ_54K7 0xE /* 54.6875 кГц */
|
||
#define CBL_FREQ_109K 0xF /* 109.375 кГц */
|
||
|
||
/* Формат: биты 5(16-бит)/6(stereo) порта 0x4E — значения готовы для
|
||
* прямого OR с freq_code внутри cbl_open(). */
|
||
#define CBL_FMT_MONO8 0x00 /* 8-бит, моно — блок 128 Б */
|
||
#define CBL_FMT_MONO16 0x20 /* 16-бит, моно — блок 256 Б */
|
||
#define CBL_FMT_STEREO8 0x40 /* 8-бит, стерео — блок 128 Б */
|
||
#define CBL_FMT_STEREO16 0x60 /* 16-бит, стерео — блок 256 Б */
|
||
|
||
/* Способ выдачи блока. */
|
||
#define CBL_PUMP_OTIR 0 /* cbl_push_otir() — базовый, не умеет 16-бит */
|
||
#define CBL_PUMP_ACCEL 1 /* cbl_push_accel() — единственный путь для 16-бит */
|
||
|
||
/* Поведение при недоливе (fill() вернул 0 или не задан). */
|
||
/* Внутренние коды режима недолива: приложение их не передаёт (режим задаёт
|
||
* выбор cbl_open / cbl_open_silence), но насос по ним и различает поведение. */
|
||
#define CBL_UNDERRUN_APP 0 /* не наша забота, буфер тишины не аллоцируется */
|
||
#define CBL_UNDERRUN_SILENCE 1 /* насос сам шлёт тишину из буфера libc */
|
||
|
||
/* fill(n): пропихнуть n байт (через cbl_push_otir/accel) прямо из
|
||
* данных приложения. Вызывается ИЗ ISR — обязан быть быстрым (без
|
||
* ESTEX/BIOS/gfx/malloc). Вернуть ненулевое при успехе, 0 — нет
|
||
* данных сейчас (недолив). */
|
||
typedef int (*cbl_fill_fn)(uint16_t n);
|
||
|
||
/* Включить CBL с частотой CBL_FREQ_*, форматом CBL_FMT_* и способом выдачи
|
||
* CBL_PUMP_*; fill — callback, зовущийся из ISR за новым блоком (может быть
|
||
* NULL, тогда недолив — на каждом запросе). Недолив НЕ наша забота
|
||
* (CBL_UNDERRUN_APP): железо доиграет по кругу неактивную половину, и если
|
||
* это слышно — приложение обязано отдавать тишину само.
|
||
*
|
||
* 0 или -1 + errno (EBUSY — уже открыт; EINVAL — плохой код частоты, формат,
|
||
* pump, или не-W2 режим).
|
||
*
|
||
* ТОЧКИ ВХОДА ДВЕ, и это не косметика: cbl_open() не аллоцирует ничего и
|
||
* потому НЕ ТЯНЕТ malloc в резидент. За тишиной от libc — cbl_open_silence()
|
||
* ниже, и malloc приезжает только тому, кто её позвал. */
|
||
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||
cbl_fill_fn fill);
|
||
|
||
/* То же, но при недоливе тишину шлёт САМА libc (CBL_UNDERRUN_SILENCE):
|
||
* буфер под неё аллоцируется здесь. Добавляет ENOMEM к кодам ошибок.
|
||
* Буфер живёт до конца программы и переиспользуется при повторном открытии
|
||
* — cbl_close() его не освобождает намеренно (иначе free вернул бы malloc
|
||
* в резидент всем подряд). */
|
||
int cbl_open_silence(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||
cbl_fill_fn fill);
|
||
|
||
/* Выключить CBL и отпустить IM2-таблицу. Идемпотентно. */
|
||
void cbl_close(void);
|
||
|
||
/* Пропихнуть n байт из src прямо в CBL (звать ИЗ fill()). */
|
||
void cbl_push_otir (const void *src, uint16_t n);
|
||
void cbl_push_accel(const void *src, uint16_t n);
|
||
|
||
/* Счётчики диагностики: блоки-запросы и недоливы (fill() вернул 0
|
||
* или не задан). */
|
||
uint16_t cbl_requests(void);
|
||
uint16_t cbl_underruns(void);
|
||
|
||
#endif
|