libc: IM2 Phase 2b — звук CBL/COVOX через callback fill(), без кольца libc

CBL уже имеет аппаратный буфер 256 Б (2×128, двойная буферизация на
стороне железа) — держать поверх него ещё одно кольцо в libc было бы
лишней копией. cbl_open(freq, fmt, pump_mode, underrun_mode, fill)
регистрирует callback, вызываемый из ISR за очередным блоком; он сам
пропихивает данные приложения (откуда угодно) через cbl_push_otir()/
cbl_push_accel() — без промежуточного буфера.

- два насоса: OTIR (порт 0x4F) и ACCEL (акселератор, спец-страница
  EMM 0xFD@0xC000); OTIR+16-бит запрещён (EINVAL) — по исходнику MAME
  порт данных физически не может собрать 16-бит сэмпл из пары байт;
- форматы CBL_FMT_MONO8/16/STEREO8/16, частоты 7.8..109к;
- CBL_UNDERRUN_APP (по умолчанию, недолив не наша забота) /
  CBL_UNDERRUN_SILENCE (буфер тишины malloc'ится только в этом режиме);
- tests/cbltest: матрица 64 комбинации (2 насоса × 8 форматов × 4
  частоты); tests/cblwav: banked-стрим речи с дискеты (физстраницы
  кэшированы заранее — mem_get_page нельзя звать из fill()/ISR);
  tests/cblstream: единственный случай с собственным кольцом уровня
  приложения (диск нельзя читать из fill()).

Verified в MAME 2026-07-07 — все три теста работают.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 21:21:54 +03:00
parent 8a952b99eb
commit 5086c47f0f
28 changed files with 1208 additions and 22 deletions
+6
View File
@@ -0,0 +1,6 @@
# Build cblstream.exe — потоковое чтение с диска + CBL одновременно.
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := cblstream
EXTRA_DATA := speech30.pcm
include $(PROJ_ROOT)/app.mk
+140
View File
@@ -0,0 +1,140 @@
/*
* cblstream — потоковое чтение с диска ОДНОВРЕМЕННО с воспроизведением
* через CBL. Расчитан на быстрый носитель (HDD) — на floppy
* захлёбывается (см. tests/cblwav, где поэтому весь клип грузится в
* RAM заранее). speech30.pcm — ~39.6 c речи, моно 8-бит unsigned,
* 21.875 кГц.
*
* ЕДИНСТВЕННЫЙ случай из трёх cbl-тестов, где приложению НУЖНО своё
* кольцо (2026-07-07): read() — ESTEX-вызов, а fill()-callback CBL
* зовётся ИЗ ISR, где ESTEX/BIOS под запретом (см. cbl.h). Поэтому
* диск читается ТОЛЬКО в main (в собственное кольцо ring[]), а
* fill_stream() лишь копирует уже готовые байты из ring в стейджинг и
* пропихивает cbl_push_otir() — сам с диском не работает.
*/
#include <stdio.h>
#include <string.h>
#include <conio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <cbl.h>
#include <irq.h>
#define RING_SIZE 2048u
#define READ_CHUNK 1024u
static uint8_t ring[RING_SIZE];
static uint16_t r_rd, r_wr, r_count; /* r_count: main под IRQ_DISABLE, fill() уже в ISR */
static uint8_t staging[128]; /* блок 8-бит моно */
static uint16_t ring_in = 0;
static uint16_t ring_border = 0;
static uint16_t ring_free(void)
{
IRQ_DISABLE();
uint16_t c = r_count;
IRQ_ENABLE();
return (uint16_t)(RING_SIZE - c);
}
/* Вызывать ТОЛЬКО из main. */
static void ring_push(const void *src, uint16_t n)
{
const uint8_t *s = src;
while (n) {
uint16_t tail = (uint16_t)(RING_SIZE - r_wr);
uint16_t chunk = n < tail ? n : tail;
memcpy(ring + r_wr, s, chunk);
r_wr = (uint16_t)((r_wr + chunk) % RING_SIZE);
IRQ_DISABLE();
r_count += chunk;
IRQ_ENABLE();
s += chunk;
n = (uint16_t)(n - chunk);
}
}
/* Зовётся ИЗ ISR — только копия из уже готового ring, никакого ESTEX. */
static int fill_stream(uint16_t n)
{
if (r_count < n) return 0;
uint16_t tail = (uint16_t)(RING_SIZE - r_rd);
if (tail >= n) {
// memcpy(staging, ring + r_rd, n);
cbl_push_otir(ring + r_rd, n);
ring_in++;
} else {
memcpy(staging, ring + r_rd, tail);
memcpy(staging + tail, ring, (uint16_t)(n - tail));
cbl_push_otir(staging, n);
ring_border++;
}
r_rd = (uint16_t)((r_rd + n) % RING_SIZE);
r_count = (uint16_t)(r_count - n); /* уже внутри ISR — IRQ и так замаскированы */
return 1;
}
int main(void)
{
int fd = open("speech30.pcm", O_RDONLY);
if (fd < 0) {
printf("open(speech30.pcm) failed: errno=%d\n", errno);
return 1;
}
static uint8_t readbuf[READ_CHUNK];
uint32_t played = 0;
uint8_t eof = 0;
/* прогреть кольцо реальными данными ДО cbl_open()/puts() — иначе
* пауза тишины на старте (тот же приём, что в cbltest.c/cblwav.c) */
while (!eof && ring_free() >= sizeof(readbuf)) {
int got = read(fd, readbuf, sizeof(readbuf));
if (got <= 0) { eof = 1; break; }
ring_push(readbuf, (uint16_t)got);
played += (uint32_t)got;
}
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, fill_stream) != 0) {
printf("cbl_open failed: errno=%d\n", errno);
close(fd);
return 1;
}
puts("streaming speech30.pcm from disk (21.875 kHz, mono 8-bit)...");
while (!eof) {
if (ring_free() >= sizeof(readbuf)) {
int got = read(fd, readbuf, sizeof(readbuf));
if (got <= 0) { eof = 1; break; }
ring_push(readbuf, (uint16_t)got);
played += (uint32_t)got;
} else {
__asm__("halt"); /* ждём, пока ISR подъест кольцо */
}
}
close(fd);
printf("streamed %lu bytes from disk\n", played);
/* Ждём "почти полного" опустошения, а не точного RING_SIZE: если
* общая длина потока не кратна 128, необслуженный хвост (<128 Б,
* тот же квирк, что и в старом кольце libc — см. cbl.h) навечно
* оставит r_count>0, и точное ожидание никогда бы не завершилось. */
while (ring_free() < RING_SIZE - 127u) __asm__("halt");
printf("requests: %u, underruns: %u (expect 0 during play)\n",
cbl_requests(), cbl_underruns());
printf("ring: in: %u, border: %u\n",
ring_in, ring_border);
cbl_close();
puts("press any key (keyboard must work)...");
getch();
puts("cblstream done.");
return 0;
}
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
# Build cbltest.exe — smoke CBL-звука (пила через кольцевой буфер).
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := cbltest
include $(PROJ_ROOT)/app.mk
+159
View File
@@ -0,0 +1,159 @@
/*
* cbltest — полный перебор CBL-звука (Phase 2b, docs/im2_isr_design.md).
*
* Матрица: 2 насоса (OTIR/ACCEL) × 8 форматов (mono/stereo-L/stereo-R/
* stereo-LR, каждый в 8- и 16-бит) × 4 частоты (7.8/10.9/15.6/21.9 кГц)
* = 64 комбинации, каждая ~1 с пилообразного тона (период 64 сэмпла/
* кадра). Для каждой печатается requests/underruns — 0 недоливов
* ожидается всегда. В MAME на слух должно быть отчётливо слышно:
* смену тембра насоса (не должно быть разницы OTIR/ACCEL), канал
* (левый/правый/оба) и высоту тона (растёт с частотой).
*
* OTIR+16-бит (16 из 64 комбинаций) ПРОПУСКАЮТСЯ — cbl_open() отдаёт
* EINVAL: OTIR-порт данных не умеет собирать 16-бит сэмпл из пары
* байт (только акселератор это делает; см. cbl_open.c, сверено с
* исходником MAME sprinter.cpp).
*
* Новая модель без кольца libc (2026-07-07): fill_tone() зовётся ИЗ
* ISR, сама пропихивает n байт из уже готового периодического чанка
* через cbl_push_otir/accel (по current_pump) — данные никогда не
* кончаются, так что CBL_UNDERRUN_APP (без буфера тишины) достаточно.
* Main-цикл ничего не поллит — просто ждёт ~1 с через halt().
*/
#include <stdio.h>
#include <time.h>
#include <conio.h>
#include <errno.h>
#include <cbl.h>
static uint8_t chunk[256];
static uint8_t current_pump; /* CBL_PUMP_OTIR/ACCEL для активной комбинации */
/* Заполнить chunk одним периодом (64 сэмпла/кадра) пилы — раскладка
* зависит от bits (8/16) и stereo (моно / чередование L,R). */
static void gen_chunk(uint8_t bits, uint8_t stereo, uint8_t want_l, uint8_t want_r)
{
if (!stereo) {
if (bits == 8) {
for (uint16_t i = 0; i < sizeof(chunk); i++) {
chunk[i] = (uint8_t)(i * 4); /* период 64 (обёртка uint8_t) */
}
} else {
int16_t *p = (int16_t *)chunk;
for (uint16_t i = 0; i < sizeof(chunk) / 2; i++) {
uint16_t idx = i & 63;
p[i] = (int16_t)(uint16_t)(idx * 1024u);
}
}
} else if (bits == 8) {
for (uint16_t k = 0; k < sizeof(chunk) / 2; k++) {
uint8_t frame = (uint8_t)(k & 63);
uint8_t tone = (uint8_t)(frame * 4);
chunk[2 * k] = want_l ? tone : 0x80;
chunk[2 * k + 1] = want_r ? tone : 0x80;
}
} else {
int16_t *p = (int16_t *)chunk;
for (uint16_t k = 0; k < sizeof(chunk) / 4; k++) {
uint16_t frame = k & 63;
int16_t tone = (int16_t)(uint16_t)(frame * 1024u);
p[2 * k] = want_l ? tone : 0;
p[2 * k + 1] = want_r ? tone : 0;
}
}
}
/* Зовётся ИЗ ISR (см. cbl.h: обязано быть быстрым). chunk — готовый
* period-64 тон, никогда не "кончается", поэтому всегда успех. */
static int fill_tone(uint16_t n)
{
if (current_pump == CBL_PUMP_ACCEL) cbl_push_accel(chunk, n);
else cbl_push_otir(chunk, n);
return 1;
}
static void play_combo(uint16_t idx, uint16_t total,
const char *pump_name, uint8_t pump_mode,
const char *fmt_name, uint8_t fmt, uint8_t bits,
uint8_t stereo, uint8_t want_l, uint8_t want_r,
const char *freq_name, uint8_t freq_code)
{
gen_chunk(bits, stereo, want_l, want_r);
current_pump = pump_mode;
if (cbl_open(freq_code, fmt, pump_mode, CBL_UNDERRUN_APP, fill_tone) != 0) {
printf("[%2u/%u] %s %s %s: cbl_open failed errno=%d\n",
idx, total, pump_name, fmt_name, freq_name, errno);
return;
}
printf("[%2u/%u] %s %s %s ...\n", idx, total, pump_name, fmt_name, freq_name);
datetime_t t, t2;
getdatetime(&t);
getdatetime(&t2);
while (t.second == t2.second) {
__asm__("halt"); /* ISR сам кормит CBL — просто ждём */
getdatetime(&t2);
}
uint16_t reqs = cbl_requests();
uint16_t undr = cbl_underruns();
cbl_close();
if (undr) printf(" !! underruns=%u (reqs=%u)\n", undr, reqs);
}
int main(void)
{
static const uint8_t pumps[2] = { CBL_PUMP_OTIR, CBL_PUMP_ACCEL };
static const char *const pump_names[2] = { "otir ", "accel" };
static const uint8_t freq_codes[4] = {
CBL_FREQ_7K8, CBL_FREQ_10K9, CBL_FREQ_15K6, CBL_FREQ_21K9
};
static const char *const freq_names[4] = { "7.8k ", "10.9k", "15.6k", "21.9k" };
/* формат: биты, fmt-код, stereo?, L?, R?, имя */
static const uint8_t fmt_bits[8] = { 8, 8, 8, 8, 16, 16, 16, 16 };
static const uint8_t fmt_code[8] = {
CBL_FMT_MONO8, CBL_FMT_STEREO8, CBL_FMT_STEREO8, CBL_FMT_STEREO8,
CBL_FMT_MONO16, CBL_FMT_STEREO16, CBL_FMT_STEREO16, CBL_FMT_STEREO16
};
static const uint8_t fmt_stereo[8] = { 0, 1, 1, 1, 0, 1, 1, 1 };
static const uint8_t fmt_l[8] = { 1, 1, 0, 1, 1, 1, 0, 1 };
static const uint8_t fmt_r[8] = { 1, 0, 1, 1, 1, 0, 1, 1 };
static const char *const fmt_names[8] = {
"mono8 ", "st8-L ", "st8-R ", "st8-LR ",
"mono16 ", "st16-L ", "st16-R ", "st16-LR "
};
uint16_t total = 2u * 8u * 4u;
uint16_t idx = 0;
for (uint8_t p = 0; p < 2; p++) {
for (uint8_t f = 0; f < 8; f++) {
for (uint8_t q = 0; q < 4; q++) {
idx++;
/* OTIR принципиально не может собрать 16-бит сэмпл
* (см. cbl_open.c) — cbl_open вернёт EINVAL; не тратим
* время на заведомо запрещённую комбинацию. */
if (pumps[p] == CBL_PUMP_OTIR && fmt_bits[f] == 16) {
printf("[%2u/%u] %s %s %s : skip (OTIR can't do 16-bit)\n",
idx, total, pump_names[p], fmt_names[f], freq_names[q]);
continue;
}
play_combo(idx, total,
pump_names[p], pumps[p],
fmt_names[f], fmt_code[f], fmt_bits[f],
fmt_stereo[f], fmt_l[f], fmt_r[f],
freq_names[q], freq_codes[q]);
}
}
}
puts("press any key (keyboard must work)...");
getch();
puts("cbltest done.");
return 0;
}
+6
View File
@@ -0,0 +1,6 @@
# Build cblwav.exe — проигрывание speech.pcm через CBL (Phase 2b смоук).
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := cblwav
EXTRA_DATA := speech.pcm
include $(PROJ_ROOT)/app.mk
+134
View File
@@ -0,0 +1,134 @@
/*
* cblwav — проигрывание РЕАЛЬНОГО звука через CBL (Phase 2b смоук).
*
* speech.pcm: моно 8-бит unsigned (центр 0x80), 21.875 кГц — сгенерирован
* macOS `say` + `afconvert -d UI8@21875` (см. docs/im2_isr_design.md).
*
* speech.pcm лежит на ДИСКЕТЕ (mc.img на -flop1) — этот привод слишком
* медленный для потокового чтения в реальном времени. Поэтому клип
* целиком грузится в банковую EMM-память ЗАРАНЕЕ (без таймингов),
* крупными блоками через сырой read(), а во время проигрывания диск
* больше не участвует.
*
* Новая модель без кольца libc (2026-07-07): fill_speech() зовётся
* ИЗ ISR за очередным блоком; она делает bank_read() из УЖЕ ЗАГРУЖЕННОЙ
* страницы прямо в маленький стейджинг-буфер и пропихивает его через
* cbl_push_otir() — без промежуточного кольца.
*
* ВАЖНО: fill() не должен звать mem_get_page() (BIOS rst 8) — BIOS сам
* управляет EI/DI вокруг rst 8, а его `ei` при возврате может
* преждевременно снять маску прерываний, пока мы ещё внутри ISR (см.
* cbl.h: "fill() ОБЯЗАН быть быстрым... никаких ESTEX/BIOS-вызовов").
* Поэтому номера физических страниц считаются ОДИН РАЗ на этапе
* загрузки (в main-контексте) и кэшируются в phys_pages[] — fill()
* только индексирует массив. bank_read() — чистый порт-ввод/вывод,
* без BIOS/ESTEX, вызывать из ISR безопасно.
*
* Хвост не кратный размеру блока (78 КБ % 128 = 43) — fill()
* докладывает остаток тишиной (0x80) прямо в стейджинге на последнем
* неполном блоке, никакого отдельного паддинг-прохода не нужно.
*/
#include <stdio.h>
#include <string.h>
#include <conio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <cbl.h>
#include <sprinter_mem.h>
#define PAGE_SIZE 16384u /* размер страницы EMM */
#define LOAD_CHUNK 4096u /* блок разовой загрузки с диска; делит PAGE_SIZE нацело */
#define SPEECH_SIZE 77995ul /* точный размер tests/cblwav/speech.pcm */
#define MAX_PAGES 8
static uint8_t loadbuf[LOAD_CHUNK];
static uint8_t staging[256]; /* под самый большой блок (16-бит = 256) */
static uint8_t phys_pages[MAX_PAGES]; /* кэш физ. страниц — БЕЗ BIOS-вызовов из fill() */
static uint8_t page_idx;
static uint16_t page_off;
static uint32_t played;
static uint32_t loaded;
static volatile uint8_t done;
static int fill_speech(uint16_t n)
{
if (played >= loaded) { done = 1; return 0; }
uint16_t want = n;
if (want > loaded - played) want = (uint16_t)(loaded - played);
bank_read(phys_pages[page_idx], page_off, staging, want);
if (want < n) memset(staging + want, 0x80, n - want); /* хвост короче блока */
cbl_push_otir(staging, n);
page_off += want;
played += want;
if (page_off >= PAGE_SIZE) {
page_off -= PAGE_SIZE;
page_idx++;
}
return 1;
}
int main(void)
{
int fd = open("speech.pcm", O_RDONLY);
if (fd < 0) {
printf("open(speech.pcm) failed: errno=%d\n", errno);
return 1;
}
uint8_t npages = (uint8_t)((SPEECH_SIZE + PAGE_SIZE - 1) / PAGE_SIZE);
uint8_t blk = mem_alloc_pages(npages);
if (!blk) {
printf("mem_alloc_pages(%u) failed: errno=%d\n", npages, errno);
close(fd);
return 1;
}
puts("loading speech.pcm into RAM (slow: floppy)...");
page_idx = 0;
page_off = 0;
phys_pages[0] = mem_get_page(blk, 0);
while (loaded < SPEECH_SIZE) {
int got = read(fd, loadbuf, sizeof(loadbuf));
if (got <= 0) break;
bank_write(phys_pages[page_idx], page_off, loadbuf, (uint16_t)got);
page_off += (uint16_t)got;
loaded += (uint32_t)got;
if (page_off >= PAGE_SIZE) {
page_off -= PAGE_SIZE;
page_idx++;
phys_pages[page_idx] = mem_get_page(blk, page_idx); /* редко: смена страницы */
}
}
close(fd);
printf("loaded %lu bytes into %u page(s)\n", loaded, npages);
page_idx = 0;
page_off = 0;
played = 0;
done = 0;
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, fill_speech) != 0) {
printf("cbl_open failed: errno=%d\n", errno);
mem_free_block(blk);
return 1;
}
puts("playing speech.pcm (21.875 kHz, mono 8-bit)...");
while (!done) __asm__("halt"); /* ISR сам кормит CBL — просто ждём */
printf("requests: %u, underruns: %u (expect ~1: tail block)\n",
cbl_requests(), cbl_underruns());
cbl_close();
mem_free_block(blk);
puts("press any key (keyboard must work)...");
getch();
puts("cblwav done.");
return 0;
}
File diff suppressed because one or more lines are too long