Files
Sprinter-SDCC/libc/include/cbl.h
T
snark13 8490288d79 libc/crt0: возврат из main завершает программу по-настоящему
Два бага одного пути завершения, оба видны только на железе.

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
2026-09-02 14:15:48 +03:00

150 lines
10 KiB
C
Raw Permalink 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 — 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