Add OPL4 playback and analyzer support

This commit is contained in:
2026-09-15 14:59:29 +03:00
commit 1686838e03
553 changed files with 50524 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
/* Auto-generated by gen_freq_tables.js — DO NOT EDIT */
#ifndef FREQ_LUT_MAP_H
#define FREQ_LUT_MAP_H
#include "types.h"
/* AY/YM2149: bypass |psg_khz - 1750| <= 35 */
#define FREQ_LUT_HW_KHZ 1750u
#define FREQ_LUT_BYPASS_TOL 35u /* 2% */
/* YM2203: bypass |ym_khz - 3500| <= 70 */
#define FREQ_LUT_HW_YM_KHZ 3500u
#define FREQ_LUT_BYPASS_TOL_YM 70u /* 2% */
#define FREQ_LUT_AY_COUNT 6u
#define FREQ_LUT_YM_COUNT 5u
#define FREQ_LUT_PSG_ENTRIES 4096u /* 12-bit PSG */
#define FREQ_LUT_PSG_BYTES 8192u /* per PSG table */
typedef struct {
uint16_t clk_khz; /* canonical clock in kHz (PSG or YM master) */
uint16_t tol_khz; /* ±tolerance in kHz (5%) */
uint8_t page; /* plugin page (PAGE_BASE-based) */
uint16_t offset; /* PSG table byte offset within page */
} freq_lut_entry_t;
/* AY/YM2149 PSG table map (tolerance = 5% of canonical PSG clock)
* Layout: 2 ratios per page, PSG_A@0x0000, PSG_B@0x2000 */
static const freq_lut_entry_t freq_lut_map_ay[FREQ_LUT_AY_COUNT] = {
{ 1790u, 90u, 1, 0x0000 }, /* NTSC_1790 */
{ 1500u, 75u, 1, 0x2000 }, /* 3MHz_1500 */
{ 2000u, 100u, 2, 0x0000 }, /* 4MHz_2000 */
{ 1000u, 50u, 2, 0x2000 }, /* CPC_1000 */
{ 1250u, 63u, 3, 0x0000 }, /* 1250 */
{ 750u, 38u, 3, 0x2000 } /* 1500_750 */
};
/* YM2203 table map (full master clock, PSG table = same as AY entry) */
static const freq_lut_entry_t freq_lut_map_ym[FREQ_LUT_YM_COUNT] = {
{ 3580u, 179u, 1, 0x0000 }, /* NTSC_1790 (YM2203@3580kHz) */
{ 3000u, 150u, 1, 0x2000 }, /* 3MHz_1500 (YM2203@3000kHz) */
{ 4000u, 200u, 2, 0x0000 }, /* 4MHz_2000 (YM2203@4000kHz) */
{ 2500u, 125u, 3, 0x0000 }, /* 1250 (YM2203@2500kHz) */
{ 1500u, 75u, 3, 0x2000 } /* 1500_750 (YM2203@1500kHz) */
};
/* FM F-Number множитель (YM2203): scaled = fnum × fm_mul / 7.
* 0 = bypass (масштабирование не нужно).
* ASM на Z80: умножение через shifts+adds, деление на 7 binary division */
static const uint8_t freq_lut_fm_mul[FREQ_LUT_YM_COUNT] = {
0, /* NTSC_1790: 1.022727 = bypass (2.3%) */
6, /* 3MHz_1500: 0.857143 = ×6/7 */
8, /* 4MHz_2000: 1.142857 = ×8/7 */
5, /* 1250: 0.714286 = ×5/7 */
3 /* 1500_750: 0.428571 = ×3/7 */
};
/*
* Usage in vgm.c (parse_header):
*
* 1. Bypass check: |clock_khz - HW| <= BYPASS_TOL → native
* 2. Find matching table → set page, PSG base = offset
* 3. For YM2203: fm_mul = freq_lut_fm_mul[j]
*
* PSG: scaled_period = freq_lut_base[period_12bit]
* FM: asm_scale_fm() : fb_scaled = fnum × fm_mul / 7
* if (fb_scaled > 2047) block++, fb_scaled >>= 1
*/
#endif /* FREQ_LUT_MAP_H */
+203
View File
@@ -0,0 +1,203 @@
/**
* @file isr.h
* @brief ISR (Interrupt Service Routine) для FRAME-прерывания TSConfig
* @version 2.0
*
* Зависимости: types.h.
* Реализация: asm/isr.s (чистый ASM, hot-path без C).
*
* ═══════════════════════════════════════════════════════════════════
* АРХИТЕКТУРА
* ═══════════════════════════════════════════════════════════════════
*
* Частота ISR задаётся вариантом сборки (ISR_FREQ в variant_cfg.h).
* N_ENTRIES прерываний × STEP T-states = 71680 T = 1 TV-кадр (замыкание).
*
* Позиции INT — предрассчитанная таблица POS_TABLE (N_ENTRIES x 5 байт).
* ISR читает 3 байта через OUTI и записывает в VSINTH/VSINTL/HSINT,
* затем переходит по next_ptr (замкнутый цикл).
*
* ═══════════════════════════════════════════════════════════════════
* ДВОЙНАЯ БУФЕРИЗАЦИЯ КОМАНД
* ═══════════════════════════════════════════════════════════════════
*
* CMD_BUF_A, CMD_BUF_B — по 512 байт (до 128 команд).
* ISR читает из активного буфера, main loop заполняет свободный.
* Переключение буфера: CMD_END_BUF в конце каждого заполненного.
*
* ═══════════════════════════════════════════════════════════════════
* ФОРМАТ КОМАНДЫ (4 байта, выровнено)
* ═══════════════════════════════════════════════════════════════════
*
* Код │ Байт 1 │ Байт 2 │ Байт 3 │ Действие
* ──────────────┼────────┼────────┼────────┼─────────────────────
* CMD_WRITE_AY │ reg │ val │ 0 │ OUT AY8910 chip 1
* CMD_INC_SEC │ 0 │ 0 │ 0 │ Инкр. isr_play_seconds
* CMD_WRITE_AY2 │ reg │ val │ 0 │ OUT AY8910 chip 2 (TS)
* CMD_CALL_WC │ 0 │ 0 │ 0 │ Вызов WC ISR handler
* CMD_WRITE_B0 │ reg │ val │ 0 │ OUT OPL3 Bank 0
* CMD_SKIP_TICKS│ N │ smsk_lo│ smsk_hi│ Пропустить N позиций pos_table
* CMD_WRITE_SAA │ reg │ val │ 0 │ OUT SAA1099 chip 0
* CMD_WRITE_B1 │ reg │ val │ 0 │ OUT OPL3 Bank 1
* CMD_WRITE_SA2 │ reg │ val │ 0 │ OUT SAA1099 chip 1
* CMD_WRITE_WAVE│ reg │ val │ 0 │ OUT OPL4 Wave-часть (#7E/#7F)
* CMD_WAIT │ lo │ hi │ 0 │ Ждать (hi<<8)|lo тиков
* CMD_END_BUF │ 0 │ 0 │ 0 │ Переключить буфер
*
* CMD_SKIP_TICKS carries 16-bit spectrum bitmask: bit N → bar N triggered
*
* ═══════════════════════════════════════════════════════════════════
* ПРИМЕР ИСПОЛЬЗОВАНИЯ
* ═══════════════════════════════════════════════════════════════════
*
* #include "isr.h"
*
* // 1. Заполнить оба буфера
* fill_buffer(cmd_buf_a);
* fill_buffer(cmd_buf_b);
*
* // 2. Настроить ISR
* isr_enabled = 0; // пока не запускать
* isr_active_buf = 0; // начать с буфера A
* isr_init(); // IM2 + таблица векторов
*
* // 3. Запустить
* isr_enabled = 1;
* __asm__("ei");
*
* // 4. В main loop:
* if (isr_active_buf != last_buf) {
* fill_buffer(isr_active_buf ? cmd_buf_a : cmd_buf_b);
* last_buf = isr_active_buf;
* }
*/
#ifndef ISR_H
#define ISR_H
#include "types.h"
/* ── Коды команд (8 типов, шаг 0x20 + ISR_DONE) ─────────────────── */
#define CMD_WRITE_AY 0x00 /* AY8910 chip 1: [reg, val, 0] */
#define CMD_INC_SEC 0x10 /* Инкрементировать счётчик секунд: [0,0,0] */
#define CMD_WRITE_AY2 0x20 /* AY8910 chip 2 (TurboSound): [reg,val,0]*/
#define CMD_CALL_WC 0x30 /* Вызов WC ISR handler: [0,0,0] */
#define CMD_WRITE_B0 0x40 /* OPL3 Bank 0 write: [reg, val, 0] */
#define CMD_SKIP_TICKS 0x50 /* Пропустить N pos_table: [N,sml,smh] */
#define CMD_WRITE_SAA 0x60 /* SAA1099 write: [reg, val, 0] bit7=chip */
#define CMD_WRITE_B1 0x80 /* OPL3 Bank 1 write: [reg, val, 0] */
#define CMD_WRITE_WAVE 0x70 /* OPL4 Wave-часть write (ZXM-MoonSound, порты #7E/#7F): [reg, val, 0] */
#define CMD_WAIT 0xC0 /* Ждать N ISR-тиков: [lo, hi, 0] */
#define CMD_END_BUF 0xE0 /* Переключить буфер: [0, 0, 0] */
#define CMD_ISR_DONE 0xF0 /* Заморозить ISR, выставить isr_done: [0,0,0] */
/* ── Размер командного буфера ────────────────────────────────────── */
#define CMD_BUF_SIZE 512 /* байт; 128 команд по 4 байта */
/* ── Параметры варианта (ISR_FREQ, VGM_SAMPLE_*, budget) ───── */
#include "variant_cfg.h"
/* ── Переменные ISR (volatile = разделяемые между ISR и main) ────── */
/** Управляющий байт MultiSound (пишется в #FFFD перед каждой записью AY).
* bit0: chip select (0=chip1, 1=chip2) — ISR управляет автоматически.
* bit1: 1=normal read (всегда 1).
* bit2: 0=FM ON (high-Z), 1=FM OFF (forced low).
* bit3: 0=SAA CLK ON, 1=SAA CLK OFF.
* bit[7:4]: 0xF (маркер MultiSound ctrl).
* Idle = 0xFE (FM OFF, SAA OFF). */
extern volatile uint8_t isr_ms_ctrl;
/** Индекс активного буфера: 0 = A, 1 = B.
* Записывается ISR при CMD_END_BUF, читается main loop. */
extern volatile uint8_t isr_active_buf;
/** Флаг включения: 0 = ISR пропускает выполнение команд
* (pos_table и tick_ctr продолжают работать).
* Записывается main loop, читается ISR.
* instant_abort() использует это для заморозки без доп. флагов. */
extern volatile uint8_t isr_enabled;
/** Счётчик тиков ISR. Инкрементируется каждый ISR-вызов (частота = ISR_FREQ),
* оборачивается при 256 (полный оборот ≈ 256/ISR_FREQ секунд).
* main loop читает для определения прошедшего времени. */
extern volatile uint8_t isr_tick_ctr;
/** Цвет бордюра, восстанавливаемый ISR при выходе.
* Записывается main loop (0=black, 2=red и т.д.), читается ISR. */
extern volatile uint8_t isr_border_color;
/** Счётчик секунд воспроизведения.
* Инкрементируется ISR по команде CMD_INC_SEC, читается main loop. */
extern volatile uint16_t isr_play_seconds;
/** Флаг завершения: 1 = ISR заморожен на CMD_ISR_DONE.
* Записывается ISR, читается main loop.
* Main loop очищает при старте воспроизведения. */
extern volatile uint8_t isr_done;
/** Командные буферы A и B. Заполняются main loop, читаются ISR. */
extern uint8_t cmd_buf_a[CMD_BUF_SIZE];
extern uint8_t cmd_buf_b[CMD_BUF_SIZE];
/* ── Spectrum analyzer (16 bars, 0-8 levels) ─────────────────────── */
#define SPECTRUM_BARS 16
#define SPECTRUM_MAX_LEVEL 8
/** уровни спектра для каждой из 16 полос, 0..SPECTRUM_MAX_LEVEL.
* Заполняются ISR (через CMD_SKIP_TICKS), читаются main loop для рендеринга.
* Каждый бит в CMD_SKIP_TICKS соответствует одной полосе, устанавливается при активации.
* Например, если бит 0x0001 установлен, то полоса 0 (самая левая) будет на максимальном уровне.
* Если бит 0x0002 установлен, то полоса 1 будет на максимальном уровне, и так далее.
*/
extern volatile uint8_t spectrum_levels[SPECTRUM_BARS];
/** Контрольный счётчик текущей задержки CMD_WAIT.
* ISR декрементирует при каждом тике, переходит к следующей команде после достижения 0.
* Не используется напрямую main loop — только ISR. */
extern volatile uint16_t isr_wait_ctr;
/** Указатель чтения ISR внутри активного буфера. */
extern volatile uint16_t isr_read_ptr;
/* ── Функции ─────────────────────────────────────────────────────── */
/**
* Перехват вектора IM2 (#5BFF) и настройка 2734 Hz.
*
* Действия:
* 1. Сохранить вектор WC (#5BFF) → isr_saved_vec
* 2. Записать адрес isr_handler в #5BFF
* 3. Настроить INTMASK = FRAME only, позиции INT
* 4. Инициализировать pos_ptr, wait_ctr, tick_ctr, read_ptr
* 5. IM 2 (EI делается вызывающим кодом)
*
* ВАЖНО: вызывать ПОСЛЕ заполнения обоих буферов!
* ВАЖНО: вызывать при DI (прерывания выключены).
*/
void isr_init(void);
/**
* Восстановить вектор WC и сбросить INT-позиции.
*
* Действия:
* 1. Восстановить оригинальный вектор из isr_saved_vec → #5BFF
* 2. Сбросить позицию INT на line=0 (один INT/кадр)
* 3. INTMASK = FRAME only (WC default)
* НЕ делает EI — вызывающий код решает сам.
*
* Безопасно вызывать без предшествующего isr_init (no-op).
*/
void isr_deinit(void);
/**
* Вызов оригинального WC ISR handler из main loop.
*
* WC ожидает периодический вызов своего ISR (обновление таймера,
* клавиатуры, часов). Вызывать ~50 Гц из main loop.
* Делает DI → JP в WC handler → WC делает EI+RET.
* Безопасно вызывать без предшествующего isr_init (no-op).
*/
void call_wc_handler(void);
#endif /* ISR_H */
+56
View File
@@ -0,0 +1,56 @@
/**
* @file keys.h
* @brief PS/2 клавиатура через TSConfig CMOS FIFO (#EFF7/#DFF7/#BFF7)
*
* ps2_init() — одноразовая инициализация: открытие PS/2 портов,
* активация FIFO, старт FSM в режиме DRAIN.
* ps2_poll() — вызывать из главного цикла; rate-limit (~1 раз в TV-кадр).
* За один вызов полностью сливает FIFO (tight loop).
* Возвращает последний найденный KEY_* или KEY_NONE.
*
* Единственный бит состояния — skip flag (после F0 пропустить 1 байт).
* E0 не требует skip — сами коды не совпадают с нашими, а E0 F0 SC
* корректно обрабатывается через F0→skip.
*
* PS/2 Set 2 скан-коды:
* ESC (#76) → KEY_ESC — выход
* SPACE (#29) → KEY_NEXT — следующий трек
* P (#4D) → KEY_PREV — предыдущий трек
* 1 (#16) → KEY_SAA1 — SAA режим 1 (chip0)
* 2 (#1E) → KEY_SAA2 — SAA режим 2 (chip1)
*/
#ifndef KEYS_H
#define KEYS_H
#include "types.h"
/* ── Коды клавиш (возврат ps2_poll) ──────────────────────────────────── */
#define KEY_NONE 0 /* нет нажатия */
#define KEY_ESC 1 /* ESC → выход */
#define KEY_NEXT 2 /* SPACE → следующий */
#define KEY_PREV 3 /* P → предыдущий */
#define KEY_SAA1 4 /* 1 → SAA mode 1 */
#define KEY_SAA2 5 /* 2 → SAA mode 2 */
#define KEY_SAA3 6 /* 3 → SAA mode 3 */
/**
* Инициализация PS/2: открыть порты, активировать FIFO, слив буфера.
* Вызвать один раз при старте плагина (до первого ps2_poll).
*/
void ps2_init(void);
/**
* Деинициализация PS/2: слив FIFO, деактивация FIFO, отключение портов.
* Вызвать при выходе из плагина (после главного цикла).
*/
void ps2_deinit(void);
/**
* Опросить PS/2 FIFO (rate-limited, ~1 раз в кадр).
* Клавиша подтверждается только после полного цикла make + release.
* Возвращает KEY_* при подтверждении, KEY_NONE иначе.
*/
uint8_t ps2_poll(void);
#endif /* KEYS_H */
+135
View File
@@ -0,0 +1,135 @@
/**
* @file opl3.h
* @brief OPL3 (YMF262) — константы регистров и функции записи
* @version 2.0
*
* Самодостаточная библиотека: зависит только от types.h.
* Реализация — asm/opl3.s (чистый inline OUT).
*
* ═══════════════════════════════════════════════════════════════════
* ПОРТЫ TSConfig ДЛЯ OPL3
* ═══════════════════════════════════════════════════════════════════
*
* Порт │ Описание
* ──────┼────────────────────────────
* #C4 │ Bank 0 Address (reg number)
* #C5 │ Bank 0 Data (reg value)
* #C6 │ Bank 1 Address
* #C7 │ Bank 1 Data
*
* Задержка между записью адреса и данных НЕ нужна при 14 МГц
* с wait-states: одна инструкция OUT (C),A занимает достаточно
* тактов чтобы YMF262 обработал адрес до прихода данных.
*
* ═══════════════════════════════════════════════════════════════════
* КАРТА РЕГИСТРОВ OPL3 (Bank 0, то же для Bank 1)
* ═══════════════════════════════════════════════════════════════════
*
* Рег. │ Описание
* ───────────┼────────────────────────────────────────
* 0x01 │ Wavesel enable (бит 5 = 1)
* 0x04 │ Connection select (4-op mode, Bank 0)
* 0x05 (B1) │ OPL3 mode enable (бит 0 = 1)
* 0x08 │ Note select / CSW (только Bank 0)
* 0x20–0x35 │ Operator: AM/VIB/EG type/KSR/MULT
* 0x40–0x55 │ Operator: KSL/Total Level (громкость)
* 0x60–0x75 │ Operator: Attack Rate/Decay Rate
* 0x80–0x95 │ Operator: Sustain Level/Release Rate
* 0xA0–0xA8 │ Channel: F-Number low 8 bits
* 0xB0–0xB8 │ Channel: Key-On + Octave + F-Number hi
* 0xBD │ Percussion mode / AM depth / VIB depth
* 0xC0–0xC8 │ Channel: Feedback + Algorithm + Output
* 0xE0–0xF5 │ Operator: Waveform select
*
* ═══════════════════════════════════════════════════════════════════
* ПРИМЕР
* ═══════════════════════════════════════════════════════════════════
*
* #include "opl3.h"
*
* void play_note(void) {
* opl3_init(); // включить OPL3
* opl3_write_b0(0x20, 0x01); // op1: MULT=1
* opl3_write_b0(0x40, 0x10); // op1: TL=16
* opl3_write_b0(0x60, 0xF0); // op1: AR=F, DR=0
* opl3_write_b0(0x80, 0x77); // op1: SL=7, RR=7
* opl3_write_b0(0xA0, 0x98); // F-num lo
* opl3_write_b0(0xB0, 0x31); // Key-On, Oct=4, F-hi
* }
*/
#ifndef OPL3_H
#define OPL3_H
#include "types.h"
/* ── Порты OPL3 на шине TSConfig ─────────────────────────────────── */
#define OPL3_ADDR0 0xC4 /* Bank 0 register address */
#define OPL3_DATA0 0xC5 /* Bank 0 register data */
#define OPL3_ADDR1 0xC6 /* Bank 1 register address */
#define OPL3_DATA1 0xC7 /* Bank 1 register data */
/* ── Нумерация регистров (часто используемые) ────────────────────── */
#define OPL3_REG_WAVESEL 0x01 /* Waveform select enable (бит 5) */
#define OPL3_REG_CONNSEL 0x04 /* Connection select (4-op, Bank0) */
#define OPL3_REG_OPL3EN 0x05 /* OPL3 mode reg (Bank 1 only!) */
#define OPL3_OPL3EN_ON 0x01 /* NEW=1: OPL3 mode (L/R via C0-C8)*/
#define OPL3_OPL3EN_OFF 0x00 /* NEW=0: OPL2 compat (L+R always) */
#define OPL3_REG_CSW 0x08 /* CSW / Note select (Bank 0) */
#define OPL3_REG_BD 0xBD /* Percussion / AM / VIB depth */
/* Key-On бит в регистрах 0xB0-0xB8 */
#define OPL3_KEY_ON 0x20
/* ── Функции (реализация: asm/opl3.s) ────────────────────────────── */
/** Записать регистр Bank 0: addr -> #C4, data -> #C5 */
void opl3_write_b0(uint8_t addr, uint8_t data);
/** Записать регистр Bank 1: addr -> #C6, data -> #C7 */
void opl3_write_b1(uint8_t addr, uint8_t data);
/**
* Инициализация OPL3:
* 1. Bank1 reg[0x05] = 0x01 — NEW=1, OPL3 mode (регистры C0-C8 управляют L/R)
* 2. Bank0 reg[0x01] = 0x20 — Waveform Select Enable
* 3. Полный сброс всех 18 каналов: KeyOff, TL, F-Num (opl3_reset)
*
* ⚠ После `opl3_init()` чип находится в режиме OPL3 (NEW=1):
* - Регистры C0-C8 задают маршрутизацию L/R (биты 4-5)
* - OPL2/OPL1 VGM файлы НЕ устанавливают биты L/R → тишина!
*
* Для OPL1/OPL2 файлов после init вызовите:
* opl3_write_b1(OPL3_REG_OPL3EN, 0x00); // NEW=0: OPL2-compat mode
* В этом режиме L+R включены автоматически, биты C0-C8[5:4] игнорируются.
*/
void opl3_init(void);
/** Заглушить все 18 каналов (KeyOff: B0-B8 = 0, обе банки) */
void opl3_reset(void);
/** AY-3-8910 / YM2149 chip 1: mixer off + volume 0 */
void ay_silence(void);
/** AY-3-8910 chip 2 (TurboSound): select chip2, silence, switch back */
void ay2_silence(void);
/** AY chip 1: write single register */
void ay_write_reg(uint8_t reg, uint8_t val);
/** AY chip 2 (TurboSound): write single register */
void ay2_write_reg(uint8_t reg, uint8_t val);
/** SAA1099 chip 1: reset + disable + zero amplitudes */
void saa_silence(void);
/** SAA1099 chip 2: reset + disable + zero amplitudes */
void saa2_silence(void);
/** SAA1099 chip 1: write single register */
void saa_write_reg(uint8_t reg, uint8_t val);
/** SAA1099 chip 2: write single register */
void saa2_write_reg(uint8_t reg, uint8_t val);
#endif /* OPL3_H */
+531
View File
@@ -0,0 +1,531 @@
/**
* @file tsconf.h
* @brief Полный справочник регистров TSConfig (TSL FPGA-ускоритель ZX Spectrum)
* @version 2.0
*
* Самодостаточная библиотека: зависит только от types.h.
* Подходит для любого TSConfig-проекта на SDCC Z80.
*
* ═══════════════════════════════════════════════════════════════════
* АРХИТЕКТУРА ПОРТОВ
* ═══════════════════════════════════════════════════════════════════
*
* Все порты TSConfig адресуются по схеме: (REG_NUM << 8) | 0xAF
* Запись: OUT (C),A где BC = порт.
*
* На ASM:
* LD BC, TSCONF_PORT_SYSCONFIG ; BC = 0x20AF
* LD A, TSCONF_CPU_14MHZ
* OUT (C), A
*
* На C:
* tsconf_out(TSCONF_PORT_SYSCONFIG, TSCONF_CPU_14MHZ);
*
* Макросы-обёртки (конец файла) упрощают типичные операции:
* tsconf_set_cpu(TSCONF_CPU_14MHZ);
* tsconf_set_border(1);
* tsconf_set_frame_int(100, 0);
*
* ═══════════════════════════════════════════════════════════════════
* ТАЙМИНГИ TV-РЕЖИМА (Pentagon-128)
* ═══════════════════════════════════════════════════════════════════
*
* Строк в кадре: 320 (0–319)
* Строки 0–31: вертикальное гашение
* Строки 32–319: видимая область (бордер + пиксели)
*
* Пикселей в строке: 448 (7 МГц), = 224 T-states при 3.5 МГц
* Пиксели 0–87: горизонтальное гашение
* Пиксели 88–447: видимая область
*
* T-states в кадре: 320 × 224 = 71680 T
*
* ══════════════════════════════════════════════════════════════════════
* ПРЕРЫВАНИЯ (IM2)
* ══════════════════════════════════════════════════════════════════════
*
* Три источника, каждый выставляет свой вектор на шину данных:
* FRAME (#FF, приоритет 0) — совпадение растровых счётчиков
* с регистрами HSINT/VSINT
* LINE (#FD, приоритет 1) — каждая строка при HCNT = 0
* DMA (#FB, приоритет 2) — конец DMA-транзакции
*
* HSINT = HCNT[8:1]: горизонтальная позиция (0–223). >223 = выкл.
* VSINTL/VSINTH = VCNT[8:0]: вертикальная строка (0–319). >319 = выкл.
*
* VINT_INC[3:0] в VSINTH: автоинкремент VCNT после каждого FRAME-INT
* (позволяет менять строку прерывания без записи в порты).
*
* ═══════════════════════════════════════════════════════════════════
* ВИДЕО
* ═══════════════════════════════════════════════════════════════════
*
* VM[1:0]: 00=ZX (256x192, атрибуты), 01=16c, 10=256c, 11=txt
* RRES[1:0]: 00=256x192, 01=320x200, 10=320x240, 11=360x288
*
* ═══════════════════════════════════════════════════════════════════
* DMA
* ═══════════════════════════════════════════════════════════════════
*
* Адрес = 22-бит: [X(8)][H(6)][L(7)][0] — младший бит всегда 0.
*
* ═══════════════════════════════════════════════════════════════════
* ПРИМЕР (минимальный)
* ═══════════════════════════════════════════════════════════════════
*
* #include "tsconf.h"
*
* void demo(void) {
* tsconf_set_cpu(TSCONF_CPU_14MHZ); // 14 МГц
* tsconf_set_border(2); // красный бордюр
* tsconf_set_vconfig(TSCONF_VM_ZX // ZX-режим
* | TSCONF_RRES_256x192); // 256x192
* tsconf_set_frame_int(100, 0); // INT на строке 100
* tsconf_set_intmask(TSCONF_INT_FRAME); // только FRAME
* tsconf_set_cpu(TSCONF_CPU_3_5MHZ); // вернуть 3.5
* }
*/
#ifndef TSCONF_H
#define TSCONF_H
#include "types.h"
/* ══════════════════════════════════════════════════════════════════════
* Макрос формирования адреса порта
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT(reg) (((uint16_t)(reg) << 8) | 0xAF)
/* ══════════════════════════════════════════════════════════════════════
* ВИДЕО — VConfig (#00AF)
* W: RRES[1:0] | - | NOGFX | NOTSU | GFXOVR* | FT_EN** | VM[1:0]
* R: - | PWR_UP | - | FDRVER | VDVER[2:0]
* * только VDAC, ** только VDAC2. Сброс: 0x00
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_VCONFIG TSCONF_PORT(0x00)
/* VM[1:0] — видеорежим */
#define TSCONF_VM_ZX 0x00 /* ZX-Spectrum, 256×192, атрибуты */
#define TSCONF_VM_16C 0x01 /* 16 цветов, 4 бита/пиксель */
#define TSCONF_VM_256C 0x02 /* 256 цветов, 8 бит/пиксель */
#define TSCONF_VM_TXT 0x03 /* Текстовый режим */
#define TSCONF_VM_MASK 0x03
/* FT_EN, GFXOVR, NOTSU, NOGFX */
#define TSCONF_VC_FT_EN 0x04 /* FT812 enable (VDAC2) */
#define TSCONF_VC_GFXOVR 0x08 /* GFX поверх TSU (VDAC) */
#define TSCONF_VC_NOTSU 0x10 /* Отключить TSU-графику */
#define TSCONF_VC_NOGFX 0x20 /* Отключить основную графику */
/* RRES[1:0] — разрешение экрана */
#define TSCONF_RRES_256x192 (0x00 << 6)
#define TSCONF_RRES_320x200 (0x01 << 6)
#define TSCONF_RRES_320x240 (0x02 << 6)
#define TSCONF_RRES_360x288 (0x03 << 6)
#define TSCONF_RRES_MASK (0x03 << 6)
/* R: Status (то же адрес #00AF, чтение) */
#define TSCONF_STATUS_VDVER_MASK 0x07
#define TSCONF_STATUS_FDRVER 0x10
#define TSCONF_STATUS_PWR_UP 0x40
#define TSCONF_VDVER_2BIT_PWM 0x00
#define TSCONF_VDVER_3BIT 0x01
#define TSCONF_VDVER_4BIT 0x02
#define TSCONF_VDVER_5BIT 0x03
#define TSCONF_VDVER_VDAC2 0x07
/* ── VPage (#01AF): адрес видеостраницы RA[21:14] ─────────────────── */
#define TSCONF_PORT_VPAGE TSCONF_PORT(0x01)
/* ── GXOffsL (#02AF), GXOffsH (#03AF): X-смещение графики ─────────── */
#define TSCONF_PORT_GXOFFSL TSCONF_PORT(0x02) /* XO[7:0] */
#define TSCONF_PORT_GXOFFSH TSCONF_PORT(0x03) /* бит 0 = XO[8] */
/* ── GYOffsL (#04AF), GYOffsH (#05AF): Y-смещение графики ─────────── */
#define TSCONF_PORT_GYOFFSL TSCONF_PORT(0x04) /* YO[7:0] */
#define TSCONF_PORT_GYOFFSH TSCONF_PORT(0x05) /* бит 0 = YO[8] */
/* ── TSConfig (#06AF): включение tile/sprite движка ────────────────── */
#define TSCONF_PORT_TSCONFIG TSCONF_PORT(0x06)
#define TSCONF_TS_EXT 0x01 /* TSU 360×288 (не ZX-Evo) */
#define TSCONF_T0Z_EN 0x04 /* Тайл [0] виден (плоск. 0) */
#define TSCONF_T1Z_EN 0x08 /* Тайл [0] виден (плоск. 1) */
#define TSCONF_T0_EN 0x20 /* Tile-layer 0 enable */
#define TSCONF_T1_EN 0x40 /* Tile-layer 1 enable */
#define TSCONF_S_EN 0x80 /* Sprite layers enable */
/* ── PalSel (#07AF): выбор палитр ──────────────────────────────────── */
/* GPAL[7:4] | T0PAL[7:6] | T1PAL[7:6]. Сброс = 0x0F */
#define TSCONF_PORT_PALSEL TSCONF_PORT(0x07)
/* ── Border (#0FAF): цвет бордюра ──────────────────────────────────── */
#define TSCONF_PORT_BORDER TSCONF_PORT(0x0F)
/* ══════════════════════════════════════════════════════════════════════
* УПРАВЛЕНИЕ ПАМЯТЬЮ
* ══════════════════════════════════════════════════════════════════════
* RA[21:14] — номер 16KB-страницы DRAM
* Page0 = W (write-only), Page1 = W, Page2 = RW, Page3 = RW
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_PAGE0 TSCONF_PORT(0x10) /* окно #0000, W */
#define TSCONF_PORT_PAGE1 TSCONF_PORT(0x11) /* окно #4000, W */
#define TSCONF_PORT_PAGE2 TSCONF_PORT(0x12) /* окно #8000, RW */
#define TSCONF_PORT_PAGE3 TSCONF_PORT(0x13) /* окно #C000, RW */
/* ── FMAddr (#14AF): адрес Font Maps ───────────────────────────────── */
#define TSCONF_PORT_FMADDR TSCONF_PORT(0x14)
#define TSCONF_FMADDR_EN 0x10 /* бит 4 = FM_EN. Биты 3:0 = A[15:12] */
/* ── TMPage (#15AF): страница TileMap ──────────────────────────────── */
#define TSCONF_PORT_TMPAGE TSCONF_PORT(0x15) /* RA[21:14] */
/* ── T0GPage (#16AF): страница графики тайлов плоскости 0 ──────────── */
#define TSCONF_PORT_T0GPAGE TSCONF_PORT(0x16) /* RA[21:17] */
/* ── T1GPage (#17AF): страница графики тайлов плоскости 1 ──────────── */
#define TSCONF_PORT_T1GPAGE TSCONF_PORT(0x17) /* RA[21:17] */
/* ── SGPage (#18AF): страница графики спрайтов ─────────────────────── */
#define TSCONF_PORT_SGPAGE TSCONF_PORT(0x18) /* RA[21:17] */
/* ══════════════════════════════════════════════════════════════════════
* DMA — адреса источника и назначения
* Адрес = 22-бит: [X(8)][H(6)][L(7)][0]
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_DMASADDRL TSCONF_PORT(0x19) /* Source RA[7:1] */
#define TSCONF_PORT_DMASADDRH TSCONF_PORT(0x1A) /* Source RA[13:8] */
#define TSCONF_PORT_DMASADDRX TSCONF_PORT(0x1B) /* Source RA[21:14] */
#define TSCONF_PORT_DMADADDRL TSCONF_PORT(0x1C) /* Dest RA[7:1] */
#define TSCONF_PORT_DMADADDRH TSCONF_PORT(0x1D) /* Dest RA[13:8] */
#define TSCONF_PORT_DMADADDRX TSCONF_PORT(0x1E) /* Dest RA[21:14] */
#define TSCONF_PORT_DMAWPDEV TSCONF_PORT(0x25) /* DMA Wait Port Dev */
#define TSCONF_PORT_DMALEN TSCONF_PORT(0x26) /* Burst Length [7:0] */
#define TSCONF_PORT_DMACTRL TSCONF_PORT(0x27) /* Ctrl W / Status R */
#define TSCONF_PORT_DMANUM TSCONF_PORT(0x28) /* Bursts Num [7:0] */
#define TSCONF_PORT_DMANUMH TSCONF_PORT(0x2C) /* Bursts Num [9:8] */
#define TSCONF_PORT_DMAWPADDR TSCONF_PORT(0x2D) /* Wait Port Addr */
/* DMACtrl W — DDEV[3:0]: режим DMA */
#define TSCONF_DMA_RAM_RAM 0x09 /* RAM→RAM copy */
#define TSCONF_DMA_BLT1_RAM 0x89 /* RAM→RAM, пропуск нулей (OPT=1) */
#define TSCONF_DMA_FILL 0x21 /* Fill: src → dest*N */
#define TSCONF_DMA_RAM_CRAM 0xA1 /* RAM→CRAM (палитра) */
#define TSCONF_DMA_SPI_RAM 0x11 /* SPI→RAM */
#define TSCONF_DMA_RAM_SPI 0x91 /* RAM→SPI */
#define TSCONF_DMA_IDE_RAM 0x19 /* IDE→RAM */
#define TSCONF_DMA_RAM_IDE 0x99 /* RAM→IDE */
#define TSCONF_DMA_FDD_RAM 0x29 /* FDD→RAM */
#define TSCONF_DMA_RAM_WTP 0xB9 /* RAM→Wait Port */
#define TSCONF_DMA_WTP_RAM 0x39 /* Wait Port→RAM */
#define TSCONF_DMA_BLT2_RAM 0x31 /* Блиттер с аддером */
/* DMACtrl — флаги */
#define TSCONF_DMA_ASZ_256 0x00 /* Выравнивание 256 байт */
#define TSCONF_DMA_ASZ_512 0x04 /* Выравнивание 512 байт */
#define TSCONF_DMA_D_ALGN 0x08 /* Выравнивание назначения */
#define TSCONF_DMA_S_ALGN 0x10 /* Выравнивание источника */
/* R: DMAStatus */
#define TSCONF_DMA_ACT 0x80 /* DMA активна */
/* DMAWPDev */
#define TSCONF_DMAWP_GLUCLOCK 0x00
#define TSCONF_DMAWP_COMPORT 0x01
/* ══════════════════════════════════════════════════════════════════════
* СИСТЕМНАЯ КОНФИГУРАЦИЯ
* ══════════════════════════════════════════════════════════════════════ */
/* ── SysConfig (#20AF): частота CPU и кэш ──────────────────────────── */
#define TSCONF_PORT_SYSCONFIG TSCONF_PORT(0x20)
#define TSCONF_CPU_3_5MHZ 0x00
#define TSCONF_CPU_7MHZ 0x01
#define TSCONF_CPU_14MHZ 0x02
#define TSCONF_CPU_MASK 0x03
#define TSCONF_CACHE_EN 0x04 /* Включить кэш (все 4 окна) */
/* ── MemConfig (#21AF) ────────────────────────────────────────────────
* ROM128: бит 0 = то же что бит 4 в #7FFD (BootROM/Basic)
* W0_WE: разрешить запись в окно #0000
* !W0_MAP: 0=маппинг через Page0, 1=стандартная страница 0
* W0_RAM: 0=ROM, 1=RAM в #0000
* LCK128: режим блокировки 128K-совместимости
* ──────────────────────────────────────────────────────────────────── */
#define TSCONF_PORT_MEMCONFIG TSCONF_PORT(0x21)
#define TSCONF_MEM_ROM128 0x01
#define TSCONF_MEM_W0_WE 0x02
#define TSCONF_MEM_W0_MAP_DIS 0x04
#define TSCONF_MEM_W0_RAM 0x08
#define TSCONF_MEM_LCK128_512K 0x00 /* 512K, биты 7:6 #7FFD=Page3[4:3] */
#define TSCONF_MEM_LCK128_128K 0x40 /* 128K, биты 7:6 #7FFD=00 */
#define TSCONF_MEM_LCK128_AUTO 0x80 /* Auto (!a[13]) */
#define TSCONF_MEM_LCK128_1M 0xC0 /* 1024K */
/* ══════════════════════════════════════════════════════════════════════
* ПРЕРЫВАНИЯ — позиция FRAME-INT
* ══════════════════════════════════════════════════════════════════════ */
/* ── HSINT (#22AF): горизонтальная позиция ─────────────────────────── */
/* HCNT[8:1] в диапазоне 0–223; >223 = выключить INT в этой строке */
#define TSCONF_PORT_HSINT TSCONF_PORT(0x22)
#define TSCONF_HSINT_OFF 0xFF
/* ── VSINTL (#23AF): вертикальная позиция [7:0] ────────────────────── */
/* VCNT[7:0]; строки 0–319; >319 = FRAME-INT выключен */
#define TSCONF_PORT_VSINTL TSCONF_PORT(0x23)
/* ── VSINTH (#24AF): вертикальная позиция [8] + автоинкремент ──────── */
/* Бит 0 = VCNT[8]; биты [7:4] = VINT_INC[3:0] */
/* VINT_INC: после каждого FRAME-INT → VCNT += VINT_INC (0 = нет) */
#define TSCONF_PORT_VSINTH TSCONF_PORT(0x24)
#define TSCONF_VSINTH_VCNT8 0x01
#define TSCONF_VSINTH_INC_MASK 0xF0
#define TSCONF_VSINTH_INC(n) (((n) & 0x0F) << 4)
/* ── INTMask (#2AAF): маска прерываний ─────────────────────────────── */
/* Сброс: 0x01 (только FRAME включён) */
#define TSCONF_PORT_INTMASK TSCONF_PORT(0x2A)
#define TSCONF_INT_FRAME 0x01
#define TSCONF_INT_LINE 0x02
#define TSCONF_INT_DMA 0x04
#define TSCONF_INT_WTP 0x08
/* Векторы IM2 (значение на шине данных при прерывании) */
#define TSCONF_IVEC_FRAME 0xFF
#define TSCONF_IVEC_LINE 0xFD
#define TSCONF_IVEC_DMA 0xFB
/* ══════════════════════════════════════════════════════════════════════
* КЭШ — CacheConfig (#2BAF)
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_CACHECONFIG TSCONF_PORT(0x2B)
#define TSCONF_CACHE_EN_0000 0x01
#define TSCONF_CACHE_EN_4000 0x02
#define TSCONF_CACHE_EN_8000 0x04
#define TSCONF_CACHE_EN_C000 0x08
#define TSCONF_CACHE_ALL 0x0F
#define TSCONF_CACHE_NONE 0x00
/* ══════════════════════════════════════════════════════════════════════
* FDD VIRTUAL DRIVE — FDDVirt (#29AF)
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_FDDVIRT TSCONF_PORT(0x29)
#define TSCONF_FDD_A 0x01
#define TSCONF_FDD_B 0x02
#define TSCONF_FDD_C 0x04
#define TSCONF_FDD_D 0x08
#define TSCONF_FDD_VG_OPEN 0x80
/* ══════════════════════════════════════════════════════════════════════
* ТАЙЛОВЫЕ ПЛОСКОСТИ — смещения
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_T0XOFFSL TSCONF_PORT(0x40)
#define TSCONF_PORT_T0XOFFSH TSCONF_PORT(0x41)
#define TSCONF_PORT_T0YOFFSL TSCONF_PORT(0x42)
#define TSCONF_PORT_T0YOFFSH TSCONF_PORT(0x43)
#define TSCONF_PORT_T1XOFFSL TSCONF_PORT(0x44)
#define TSCONF_PORT_T1XOFFSH TSCONF_PORT(0x45)
#define TSCONF_PORT_T1YOFFSL TSCONF_PORT(0x46)
#define TSCONF_PORT_T1YOFFSH TSCONF_PORT(0x47)
/* ══════════════════════════════════════════════════════════════════════
* HUS — Hurricane Sound Engine (#80AF–#87AF)
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_HUSCTRL TSCONF_PORT(0x80) /* W: управление */
#define TSCONF_PORT_HUSSAMPRATE TSCONF_PORT(0x81) /* W: SR[7:0] (1750k/(SR+1)) */
#define TSCONF_PORT_HUSTICKRATEL TSCONF_PORT(0x82) /* W: TR[7:0] */
#define TSCONF_PORT_HUSTICKRATEH TSCONF_PORT(0x83) /* W: TR[9:8] */
#define TSCONF_PORT_HUSRLD0 TSCONF_PORT(0x84) /* Reload ch 0–7 */
#define TSCONF_PORT_HUSRLD1 TSCONF_PORT(0x85) /* Reload ch 8–15 */
#define TSCONF_PORT_HUSRLD2 TSCONF_PORT(0x86)
#define TSCONF_PORT_HUSRLD3 TSCONF_PORT(0x87)
/* R: HUSStatus (#80AF) */
#define TSCONF_HUS_ACT 0x01
#define TSCONF_HUS_TICK 0x02 /* сбрасывается при чтении */
#define TSCONF_HUS_HALF 0x40 /* буфер наполовину пуст */
#define TSCONF_HUS_EMPTY 0x80
/* W: HUSControl */
#define TSCONF_HUS_PAUSE 0x02
#define TSCONF_HUS_RESET 0x80
/* ══════════════════════════════════════════════════════════════════════
* OPL3 (YMF262) — порты на шине TSConfig
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_OPL3_ADDR0 0x00C4
#define TSCONF_PORT_OPL3_DATA0 0x00C5
#define TSCONF_PORT_OPL3_ADDR1 0x00C6
#define TSCONF_PORT_OPL3_DATA1 0x00C7
/* ══════════════════════════════════════════════════════════════════════
* AY-3-8910 / YM2149 — стандартные порты ZX Spectrum 128
* ══════════════════════════════════════════════════════════════════════
* #FFFD — выбор регистра (W) / чтение данных (R)
* #BFFD — запись данных (W)
*
* TurboSound (два AY): запись в #FFFD значения #FF/#FE выбирает чип:
* #FF → чип 0 (первый AY); #FE → чип 1 (второй AY).
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_AY_ADDR 0xFFFD /* register select / read */
#define TSCONF_PORT_AY_DATA 0xBFFD /* data write */
#define TSCONF_AY_TS_CHIP0 0xFF /* TurboSound: select chip 0 */
#define TSCONF_AY_TS_CHIP1 0xFE /* TurboSound: select chip 1 */
/* ══════════════════════════════════════════════════════════════════════
* SAA1099 — порты SAM Coupé / ZX-совместимые
* ══════════════════════════════════════════════════════════════════════
* #01FF — выбор регистра (address write)
* #00FF — запись данных (data write)
* Второй SAA1099 (если есть):
* #01FE — выбор регистра
* #00FE — запись данных
* ══════════════════════════════════════════════════════════════════════ */
#define TSCONF_PORT_SAA_ADDR 0x01FF /* SAA1099 #1 address */
#define TSCONF_PORT_SAA_DATA 0x00FF /* SAA1099 #1 data */
#define TSCONF_PORT_SAA2_ADDR 0x01FE /* SAA1099 #2 address */
#define TSCONF_PORT_SAA2_DATA 0x00FE /* SAA1099 #2 data */
/* ══════════════════════════════════════════════════════════════════════
* WC СИСТЕМНЫЕ ПЕРЕМЕННЫЕ — адреса в ROM-области ядра WC
* ══════════════════════════════════════════════════════════════════════ */
#define WC_ADDR_PAGES 0x6000 /* [3 байта] страницы #0000,#4000,#8000 */
#define WC_ADDR_ABT 0x6004 /* флаг ESC */
#define WC_ADDR_ENT 0x6005 /* флаг ENTER */
#define WC_ADDR_TMN 0x6009 /* таймер (инкр. по INT), uint16 */
#define WC_ADDR_CNFV 0x600D /* версия конфигурации */
#define WC_ADDR_HEI 0x600E /* высота экрана (25/30/36 строк) */
#define WC_CNFV_PWM 0x00
#define WC_CNFV_3BIT 0x01
#define WC_CNFV_4BIT 0x02
#define WC_CNFV_5BIT 0x03
#define WC_CNFV_VDAC2 0x07
/* ══════════════════════════════════════════════════════════════════════
* SFR — прямой доступ к портам из SDCC-C
* ══════════════════════════════════════════════════════════════════════
*
* __sfr __at 0xNN — 8-bit port: out (N), a / in a, (N)
* __sfr __banked __at 0xNNNN — 16-bit port: ld bc,#NNNN; out (c),a
*
* Не создают переменных/символов — чисто compile-time mapping.
* Присваивание sfr_xxx = val; генерирует inline OUT.
* Чтение val = sfr_xxx; генерирует inline IN.
* ══════════════════════════════════════════════════════════════════════ */
/* --- Стандартный ZX Spectrum: 8-bit порт ----------------------------- */
__sfr __at 0xFE sfr_zx_fe; /* border / keyboard */
/* --- TSConfig регистры (addr = reg<<8 | 0xAF) ----------------------- */
__sfr __banked __at 0x00AF sfr_vconfig; /* VConfig */
__sfr __banked __at 0x01AF sfr_vpage; /* VPage */
__sfr __banked __at 0x02AF sfr_gxoffsl; /* GXOffsL */
__sfr __banked __at 0x03AF sfr_gxoffsh; /* GXOffsH */
__sfr __banked __at 0x04AF sfr_gyoffsl; /* GYOffsL */
__sfr __banked __at 0x05AF sfr_gyoffsh; /* GYOffsH */
__sfr __banked __at 0x06AF sfr_tsconfig; /* TSConfig */
__sfr __banked __at 0x07AF sfr_palsel; /* PalSel */
__sfr __banked __at 0x0FAF sfr_border; /* Border Color */
__sfr __banked __at 0x10AF sfr_page0; /* Page0 (#0000) */
__sfr __banked __at 0x11AF sfr_page1; /* Page1 (#4000) */
__sfr __banked __at 0x12AF sfr_page2; /* Page2 (#8000) */
__sfr __banked __at 0x13AF sfr_page3; /* Page3 (#C000) */
__sfr __banked __at 0x14AF sfr_fmaddr; /* FMAddr */
__sfr __banked __at 0x15AF sfr_tmpage; /* TMPage */
__sfr __banked __at 0x16AF sfr_t0gpage; /* T0GPage */
__sfr __banked __at 0x17AF sfr_t1gpage; /* T1GPage */
__sfr __banked __at 0x18AF sfr_sgpage; /* SGPage */
__sfr __banked __at 0x20AF sfr_sysconfig; /* SysConfig (CPU) */
__sfr __banked __at 0x21AF sfr_memconfig; /* MemConfig */
__sfr __banked __at 0x22AF sfr_hsint; /* HSINT */
__sfr __banked __at 0x23AF sfr_vsintl; /* VSINTL */
__sfr __banked __at 0x24AF sfr_vsinth; /* VSINTH */
__sfr __banked __at 0x2AAF sfr_intmask; /* INTMask */
__sfr __banked __at 0x2BAF sfr_cacheconfig; /* CacheConfig */
__sfr __banked __at 0x29AF sfr_fddvirt; /* FDDVirt */
/* --- DMA --- */
__sfr __banked __at 0x1AAF sfr_dmasaddrh; /* DMA Src Addr H */
__sfr __banked __at 0x1BAF sfr_dmasaddrx; /* DMA Src Addr X */
__sfr __banked __at 0x19AF sfr_dmasaddrl; /* DMA Src Addr L */
__sfr __banked __at 0x1CAF sfr_dmadaddrl; /* DMA Dst Addr L */
__sfr __banked __at 0x1DAF sfr_dmadaddrh; /* DMA Dst Addr H */
__sfr __banked __at 0x1EAF sfr_dmadaddrx; /* DMA Dst Addr X */
__sfr __banked __at 0x26AF sfr_dmalen; /* DMA Burst Length */
__sfr __banked __at 0x27AF sfr_dmactrl; /* DMA Ctrl / Status */
__sfr __banked __at 0x28AF sfr_dmanum; /* DMA Bursts Num L */
__sfr __banked __at 0x2CAF sfr_dmanumh; /* DMA Bursts Num H */
/* --- OPL3 (YMF262) --- */
__sfr __banked __at 0x00C4 sfr_opl3_addr0; /* Bank0 address */
__sfr __banked __at 0x00C5 sfr_opl3_data0; /* Bank0 data */
__sfr __banked __at 0x00C6 sfr_opl3_addr1; /* Bank1 address */
__sfr __banked __at 0x00C7 sfr_opl3_data1; /* Bank1 data */
/* --- AY-3-8910 / YM2149 --- */
__sfr __banked __at 0xFFFD sfr_ay_addr; /* reg select / read */
__sfr __banked __at 0xBFFD sfr_ay_data; /* data write */
/* --- SAA1099 --- */
__sfr __banked __at 0x01FF sfr_saa_addr; /* SAA1099 #1 address */
__sfr __banked __at 0x00FF sfr_saa_data; /* SAA1099 #1 data */
__sfr __banked __at 0x01FE sfr_saa2_addr; /* SAA1099 #2 address */
__sfr __banked __at 0x00FE sfr_saa2_data; /* SAA1099 #2 data */
/* ══════════════════════════════════════════════════════════════════════
* ФУНКЦИИ — обёртки
* ══════════════════════════════════════════════════════════════════════ */
/** Запись значения в произвольный TSConfig-порт (asm, для динамических адресов)
* @param port TSCONF_PORT(reg)
* @param val значение */
extern void tsconf_out(uint16_t port, uint8_t val);
/* ── Удобные макросы (через __sfr — inline, без call) ────────────────── */
/** Установить скорость CPU (TSCONF_CPU_3_5MHZ / 7MHZ / 14MHZ) */
#define tsconf_set_cpu(speed) \
(sfr_sysconfig = (uint8_t)((speed) & TSCONF_CPU_MASK))
/** Установить скорость CPU с управлением кэшем */
#define tsconf_set_cpu_cache(speed, cache_en) \
(sfr_sysconfig = (uint8_t)(((speed) & TSCONF_CPU_MASK) | ((cache_en) ? TSCONF_CACHE_EN : 0)))
/** Установить маску прерываний (OR нужных TSCONF_INT_*) */
#define tsconf_set_intmask(mask) \
(sfr_intmask = (uint8_t)(mask))
/** Настроить позицию FRAME-INT: line=0–319, hpos=0–223 */
#define tsconf_set_frame_int(line, hpos) do { \
sfr_vsinth = (uint8_t)(((line) >> 8) & 0x01); \
sfr_vsintl = (uint8_t)((line) & 0xFF); \
sfr_hsint = (uint8_t)(hpos); \
} while(0)
/** Выключить FRAME-INT полностью */
#define tsconf_disable_frame_int() \
(sfr_hsint = TSCONF_HSINT_OFF)
/** Настроить кэш по окнам: маска из TSCONF_CACHE_EN_* */
#define tsconf_set_cache(mask) \
(sfr_cacheconfig = (uint8_t)(mask))
/** Установить цвет бордюра (TSConfig-регистр) */
#define tsconf_set_border(color) \
(sfr_border = (uint8_t)(color))
/** Подключить видеостраницу (номер 0–63) на адрес #C000 */
#define tsconf_set_vpage(page) \
(sfr_vpage = (uint8_t)(page))
/** Установить видеорежим (OR из TSCONF_VM_*, TSCONF_RRES_*, TSCONF_VC_*) */
#define tsconf_set_vconfig(cfg) \
(sfr_vconfig = (uint8_t)(cfg))
/** Подключить RAM-страницу page на окно #C000 (Page3) */
#define tsconf_set_page3(page) \
(sfr_page3 = (uint8_t)(page))
/** Подключить RAM-страницу page на окно #8000 (Page2) */
#define tsconf_set_page2(page) \
(sfr_page2 = (uint8_t)(page))
#endif /* TSCONF_H */
+153
View File
@@ -0,0 +1,153 @@
/**
* @file txtlib.h
* @brief 䇃䇐䈜䈛 для вывода текста в окна WC — динамические буферы и печать строк
* @version 1.0
*
* Зависимости: wc_api.h (wc_prsrw, wc_window_t).
* Реализация: lib/txtlib.c.
*
* Формат буфера:
* buf[0] = capacity — максимальная емкость в символах (задаётся ОДИН РАЗ при buf_init)
* buf[1] = len — текущая длина (количество символов)
* buf[2..] = символы — не завершается \0!
*
* Объявление буфера:
* char work_buf[66]; // capacity = 64 символа
*
* Типичное использование:
* buf_init(work_buf, 64);
* buf_clear(work_buf);
* buf_append_str(work_buf, "Time: ");
* buf_append_u16_dec(work_buf, seconds);
* print_line(&win, 5, work_buf, WC_COLOR(WC_BLACK, WC_BRIGHT_WHITE));
*/
#ifndef TXTLIB_H
#define TXTLIB_H
#include "wc_api.h"
#define FS_SCREEN_COLS 96U /* макс. ширина экрана (90x36, запас) */
/**
* Вернуть (ширина окна - 4) = полезная ширина содержимого.
* Если win->width == 0, используется FS_SCREEN_COLS.
*/
static inline uint8_t get_content_width(const wc_window_t *win) {
uint8_t w = win->width;
return (w ? w : FS_SCREEN_COLS) - 4U;
}
/* ================================================================== */
/* ИНИЦИАЛИЗАЦИЯ И БАЗОВЫЕ ОПЕРАЦИИ */
/* ================================================================== */
/** Инициализировать буфер: записать capacity, len=0. Вызывать ОДИН РАЗ! */
static inline void buf_init(char *buf, uint8_t capacity) {
buf[0] = capacity;
buf[1] = 0;
}
/** Очистить буфер: len=0 (емкость не меняется). */
static inline void buf_clear(char *buf) {
buf[1] = 0;
}
/** Вернуть текущую длину буфера (количество символов). */
static inline uint8_t buf_len(const char *buf) {
return buf[1];
}
/* ================================================================== */
/* APPEND */
/* ================================================================== */
/**
* Добавить строку (\0-terminated) в конец буфера.
* Тихо усекается при переполнении (len не превышает capacity).
* @return len после добавления
*/
uint8_t buf_append_str(char *buf, const char *src);
/**
* Добавить один символ c в буфер.
* @return len после добавления
*/
uint8_t buf_append_char(char *buf, char c);
/**
* Добавить двухсимвольное hex-представление байта ("00".."FF").
* @param val значение 0..255
* @return len после добавления (+2)
*/
uint8_t buf_append_u8_hex(char *buf, uint8_t val);
/**
* Добавить uint16 в десятичном формате (без ведущих нулей).
* @return len после добавления
*/
uint8_t buf_append_u16_dec(char *buf, uint16_t val);
/**
* Добавить uint32 в десятичном формате.
* @return len после добавления
*/
uint8_t buf_append_u32_dec(char *buf, uint32_t val);
/**
* Добавить "MM:SS" (5 символов) в буфер. Без деления — вычитание.
* @param sec секунды (0..5999)
*/
void buf_append_mmss(char *buf, uint16_t sec);
/* ================================================================== */
/* ВЫВОД */
/* ================================================================== */
/**
* Вывести одну строку из буфера в окно win заполняя до ширины окна.
*  Передаётся атрибут цвета attr (EGA: bg[7:4] | fg[3:0]).
*
* @param win окно WC
* @param y строка внутри окна (1-базированное)
* @param buf буфер с длиной в buf[1] (формат buf[])
* @param attr EGA-атрибут: WC_COLOR(bg, fg)
*/
void print_line(wc_window_t *win, uint8_t y, const char *buf, uint8_t attr);
/* ================================================================== */
/* ИНИЦИАЛИЗАЦИЯ И ПРИМЕР ИСПОЛЬЗОВАНИЯ */
/* ================================================================== */
/*
// В начале плагина (например в init)
char meta_buf[122];
char work_buf[66]; // 64 символа
buf_init(meta_buf, 120);
buf_init(work_buf, 64); // или больше, если используешь для marquee
// Заполнение метаданных
buf_clear(meta_buf);
buf_append_str(meta_buf, "Artist - Super Long Track Name 2025 (Remix)");
// В цикле отрисовки:
uint8_t cw = get_content_width(my_win);
// Бегущая строка (используем work_buf как временный)
build_marquee(work_buf, meta_buf + 2, meta_buf[1], scroll_pos, cw);
print_marquee(my_win, 3, work_buf, 0x0F); // белый
scroll_pos++;
// Обычная строка (время)
buf_clear(work_buf);
buf_append_str(work_buf, "Time: ");
buf_append_u16_dec(work_buf, minutes);
buf_append_char(work_buf, ':');
buf_append_u16_dec(work_buf, seconds);
print_line(my_win, 5, work_buf, 0x0A);
*/
#endif // TXTLIB_H
+75
View File
@@ -0,0 +1,75 @@
/**
* @file types.h
* @brief Базовые типы данных для Z80/SDCC (sdcccall(1))
* @version 2.0
*
* Самодостаточный заголовок: не зависит от других файлов проекта.
* Подходит для любого Z80-проекта на SDCC.
*
* ═══════════════════════════════════════════════════════════════════
* SDCC sdcccall(1) CALLING CONVENTION (Z80)
* ═══════════════════════════════════════════════════════════════════
*
* Аргумент | Тип | Регистр
* ------------|-------------------|----------
* 1-й | uint8_t / int8_t | A
* 1-й | uint16_t / ptr | DE
* 2-й и далее | любой | стек (правый первым)
* Возврат | uint8_t | A (расширяется нулём в L)
* Возврат | uint16_t / ptr | HL
*
* ═══════════════════════════════════════════════════════════════════
* РЕКОМЕНДАЦИИ ДЛЯ Z80
* ═══════════════════════════════════════════════════════════════════
*
* - uint32_t в hot-path -> десятки байт кода; избегать.
* - volatile — только для переменных, разделяемых с ISR.
* - __sfr / __xdata — НЕ использовать (это 8051, не Z80).
* - Маленькие inline-функции быстрее, чем call + стек.
* - Массивы до 256 элементов — индексировать uint8_t.
*
* ═══════════════════════════════════════════════════════════════════
* ПРИМЕР
* ═══════════════════════════════════════════════════════════════════
*
* #include "types.h"
*
* static volatile uint8_t flag; // ISR -> main
* static uint16_t counter;
*
* void timer_tick(void) {
* if (++counter >= 2734) { counter = 0; flag = 1; }
* }
*/
#ifndef TYPES_H
#define TYPES_H
/* ── Целочисленные типы фиксированной ширины ─────────────────────── */
typedef unsigned char uint8_t;
typedef signed char int8_t;
typedef unsigned int uint16_t; /* SDCC Z80: int = 16 бит */
typedef signed int int16_t;
typedef unsigned long uint32_t; /* SDCC Z80: long = 32 бит */
typedef signed long int32_t;
/* ── Булев тип ───────────────────────────────────────────────────── */
/* uint8_t = один байт; сравнения JR Z / JR NZ без масок. */
typedef uint8_t bool_t;
#define TRUE ((bool_t)1)
#define FALSE ((bool_t)0)
/* ── NULL ────────────────────────────────────────────────────────── */
#ifndef NULL
#define NULL ((void *)0)
#endif
/* ── Короткие псевдонимы ─────────────────────────────────────────── */
typedef uint8_t u8;
typedef uint16_t u16;
typedef int8_t i8;
typedef int16_t i16;
typedef uint32_t u32;
typedef int32_t i32;
#endif /* TYPES_H */
+51
View File
@@ -0,0 +1,51 @@
/**
* @file variant_cfg.h
* @brief Единственный файл конфигурации варианта сборки.
*
* build_variants.ps1 перегенерирует этот файл целиком для каждого
* варианта. Никакие другие исходники НЕ патчатся.
*
* build.bat читает VARIANT_POS_ENTRIES / VARIANT_POS_STEP /
* VARIANT_DATA_LOC из этого файла (парсинг через for /f).
*/
#ifndef VARIANT_CFG_H
#define VARIANT_CFG_H
/* ── Частота ISR (тиков/сек) ─────────────────────────────────────── */
/* ISR_FREQ = 3 500 000 / VARIANT_POS_STEP */
#define ISR_FREQ 683
/* Тиков ISR на TV-кадр (= 71680 / STEP) */
/* 14@683, 28@1367, 56@2734 */
#define ISR_TICKS_PER_FRAME 14
/* ── Маппинг VGM 44100→ISR тиков ─────────────────────────────────── */
/* SHIFT = log2(44100/ISR_FREQ): 6@683, 5@1367, 4@2734 */
#define VGM_SAMPLE_SHIFT 6
#define VGM_SAMPLE_MASK 0x3F
/* ── Лимит команд между принудительными yield ──────────────────────── */
#define VGM_FILL_CMD_BUDGET 16
/* ── Параметры pos_table (для build.bat → gen_pos_table.js) ──────── */
#define VARIANT_POS_ENTRIES 14
#define VARIANT_POS_STEP 5120
/* ── Адрес начала сегмента данных (для линковщика) ────────────────── */
/* Плотная раскладка: полная версия spectrum.s + загрузчик OPL4.
* Проверяется build.bat: _CODE должен закончиться раньше _DATA,
* а последняя секция — раньше 0xC000. */
#define VARIANT_DATA_LOC 0xB8D0
/* ── Спектроанализатор: скорости затухания (в TV-кадрах) ────────── */
/* Меньше = быстрее затухание. 1 = каждый кадр (~20 мс). */
/* Синхронизировать с EQU в spectrum.s! */
#define DECAY_FRAMES_FAST 1 /* уровни >= 6 */
#define DECAY_FRAMES_MED 2 /* уровни 3-5 */
#define DECAY_FRAMES_SLOW 4 /* уровни 1-2 */
/* ── Отключение обработки коротких пауз 0x70-0x7F ────────────────── */
/* Раскомментировать для «no short waits» варианта: */
/* #define VGM_NO_SHORT_WAITS */
#endif /* VARIANT_CFG_H */
+15
View File
@@ -0,0 +1,15 @@
/**
* @file version.h
* @brief Версия приложения — редактируйте только этот файл
*
* Примеры:
* #define APP_VERSION "v0.5"
* #define APP_VERSION "v0.5-beta"
* #define APP_VERSION "v1.0-rc1"
*/
#ifndef VERSION_H
#define VERSION_H
#define APP_VERSION "v0.9.03-beta"
#endif /* VERSION_H */
+335
View File
@@ -0,0 +1,335 @@
/**
* @file vgm.h
* @brief VGM-парсер для OPL1/OPL2/OPL3 — заголовок + потоковое чтение
* @version 3.0
*
* Зависимости: types.h, isr.h (cmd_buf_a/b), wc_api.h (wc_mngcvpl).
* Реализация: lib/vgm.c.
*
* ═══════════════════════════════════════════════════════════════════
* ОПИСАНИЕ
* ═══════════════════════════════════════════════════════════════════
*
* Преобразует VGM-поток в ISR-команды (CMD_WRITE_B0/B1, CMD_WAIT,
* CMD_END_BUF). VGM данные хранятся в VPL-страницах WC, доступных
* через окно #C000.
*
* Поддерживаемые VGM-команды (OPL1 / OPL2 / OPL3 / OPL4 / AY / SAA):
* 0x5A rr dd — YM3812 (OPL2) write → CMD_WRITE_B0
* 0x5B rr dd — YM3526 (OPL1) write → CMD_WRITE_B0
* 0x5C rr dd — Y8950 (OPL) write → CMD_WRITE_B0
* 0x5E rr dd — YMF262 (OPL3) Bank 0 write → CMD_WRITE_B0
* 0x5F rr dd — YMF262 (OPL3) Bank 1 write → CMD_WRITE_B1
* 0xD0 pp aa dd — YMF278B (OPL4) write, port pp (бит7 = chip select, игн.):
* pp&0x7F==0 → Bank 0 (как 0x5E) → CMD_WRITE_B0
* pp&0x7F==1 → Bank 1 (как 0x5F) → CMD_WRITE_B1
* pp&0x7F==2 → Wave-часть (#7E/#7F) → CMD_WRITE_WAVE
* 0x55 rr dd — YM2203 PSG-часть (AY), reg 0-15 → CMD_WRITE_AY
* 0xA0 rr dd — AY8910 (чип 1/2 по [7]) → CMD_WRITE_AY / CMD_WRITE_AY2
* 0xBD rr dd — SAA1099 (чип 1/2 по [7]) → CMD_WRITE_SAA (bit7 = chip select)
* 0x61 nn nn — Wait N samples (16-bit LE)
* 0x62 — Wait 735 samples (1/60 сек)
* 0x63 — Wait 882 samples (1/50 сек)
* 0x66 — End of data
* 0x67 ... — Data block (пропускается)
* 0x70–0x7F — Wait 1–16 samples
* Прочие — пропускаются по таблице VGM spec (1-байт / 2-байт / 3-байт аргументы)
*
* Масштабирование задержек:
* VGM sample rate = 44100 Hz, ISR freq = 2734 Hz -> делим на 16.
* Задержки < 16 сэмплов игнорируются (округление вниз).
* Неизвестные команды пропускаются по таблице размеров VGM spec.
*
* ═══════════════════════════════════════════════════════════════════
* VGM ЗАГОЛОВОК — ключевые смещения (little-endian)
* ═══════════════════════════════════════════════════════════════════
*
* Offset Size Описание Мин.версия
* ------ ---- -------------------------------- ----------
* 0x00 4 Сигнатура "Vgm " (0x206D6756) 1.00
* 0x08 4 Версия (BCD): 0x0151 = v1.51 1.00
* 0x14 4 GD3 offset (отн. 0x14) 1.00
* 0x18 4 Total samples 1.00
* 0x1C 4 Loop offset (отн. 0x1C) 1.00
* 0x20 4 Loop samples 1.00
* 0x34 4 Data offset (отн. 0x34) 1.50
* 0x50 4 YM3812 (OPL2) clock 1.51
* 0x54 4 YM3526 (OPL1) clock 1.51
* 0x58 4 Y8950 (OPL) clock 1.51
* 0x5C 4 YMF262 (OPL3) clock 1.51
*
* Для версий < 1.50: data offset фиксирован = 0x40.
* Для версий < 1.51: поля чипов за пределами заголовка.
*
* ~93% OPL VGM файлов — YM3812 (OPL2), команда 0x5A.
* ~5% — YMF262 (OPL3), команды 0x5E/0x5F.
*
* ═══════════════════════════════════════════════════════════════════
* ПРИМЕР ИСПОЛЬЗОВАНИЯ
* ═══════════════════════════════════════════════════════════════════
*
* #include "vgm.h"
* #include "isr.h"
* #include "wc_api.h"
*
* // 1. Загрузить VGM в VPL-страницы
* wc_mngcvpl(0);
* wc_load512(0xC000, 32); // 16 КБ
*
* // 2. Разобрать заголовок
* if (vgm_parse_header() != VGM_OK) return;
*
* // 3. Заполнить оба буфера ISR
* vgm_fill_buffer(0); // буфер A
* vgm_fill_buffer(1); // буфер B
*
* // 4. В main loop:
* if (cur_buf != last_buf) {
* vgm_fill_buffer(cur_buf ^ 1); // свободный
* last_buf = cur_buf;
* }
* if (vgm_song_ended && !vgm_rewind_to_loop()) break;
*/
#ifndef VGM_H
#define VGM_H
#include "types.h"
/* ── Масштабирование частот PSG под реальное железо ─────────────────── */
#define FREQ_MODE_NATIVE 0 /* HW clock ≈ VGM clock (±2%), без пересчёта */
#define FREQ_MODE_TABLE 1 /* LUT table match (±5%) */
/** Текущий режим масштабирования (FREQ_MODE_xxx) */
extern uint8_t vgm_freq_mode;
/** Базовый адрес LUT таблицы в Window 0 (#0000-#3FFF).
* Используется в TABLE mode: freq_lut_base[period] → scaled period.
* NULL (0) если режим не TABLE.
* PSG: индекс 0..4095 (12-bit).
* Таблица в plugin page N, подключённой через wc_mng0_pl(N). */
extern uint16_t *freq_lut_base;
/** Clock (кГц) подобранной LUT таблицы (для отображения в UI).
* 0 если режим NATIVE. */
extern uint16_t vgm_freq_lut_khz;
/* ══════════════════════════════════════════════════════════════════════
* ID чипов = смещение clock-поля в VGM-заголовке.
* Имена определены только для поддерживаемых чипов.
* ══════════════════════════════════════════════════════════════════════ */
#define VGM_OFF_SN76489 0x0C /* PSG SN76489 / SN76496 */
#define VGM_OFF_YM2413 0x10 /* OPLL */
#define VGM_OFF_YM2203 0x44 /* OPN (PC-88) */
#define VGM_OFF_YM3812 0x50 /* OPL2 (AdLib / SB) */
#define VGM_OFF_YM3526 0x54 /* OPL (ISA AdLib) */
#define VGM_OFF_Y8950 0x58 /* MSX-AUDIO / OPL */
#define VGM_OFF_YMF262 0x5C /* OPL3 (SB Pro 2) */
#define VGM_OFF_YMF278B 0x60 /* OPL4 */
#define VGM_OFF_AY8910 0x74 /* AY-3-8910 / YM2149 */
#define VGM_OFF_SAA1099 0xC8 /* SAA1099 (SAM Coupé) */
/* ── Тип OPL-чипа (обратная совместимость) ───────────────────────── */
#define VGM_CHIP_NONE 0
#define VGM_CHIP_OPL 1 /* YM3526 (OPL1) + Y8950: cmd 0x5B/0x5C */
#define VGM_CHIP_OPL2 2 /* YM3812 (OPL2): cmd 0x5A */
#define VGM_CHIP_OPL3 3 /* YMF262 (OPL3): cmd 0x5E/5F */
#define VGM_CHIP_OPL4 4 /* YMF278B (OPL4): cmd 0xD0 (FM порты 0/1 —
* те же регистры, что и OPL3; порт 2 —
* wave-часть через #7E/#7F, ZXM-MoonSound) */
/* Приоритет определения типа: YMF278B > YMF262 > YM3812 > YM3526/Y8950
* Используется в build_playback_queue() (main.c) для выбора cmdblock init:
* VGM_CHIP_OPL4 → CMDBLK_INIT_OPL4 (NEW=1 + NEW2=1, разблокирует wave-часть)
* VGM_CHIP_OPL3 → CMDBLK_INIT_OPL3 (NEW=1, L/R через C0-C8)
* иначе → CMDBLK_INIT_OPL2 (NEW=0, L+R авто, OPL2-compat) */
/* ── Коды результата ─────────────────────────────────────────────── */
#define VGM_OK 0
#define VGM_ERR_HEADER 1 /* Нет сигнатуры "Vgm " или повреждён */
#define VGM_ERR_NOOPL 2 /* Нет OPL-чипа (clock = 0 для всех) */
/* ── Флаги чипа (vgm_chip_entry_t.flags) ─────────────────────────── */
#define VGM_CF_DUAL 0x01 /* Bit 30 clock: два экземпляра чипа */
/* ══════════════════════════════════════════════════════════════════════
* Структура обнаруженного чипа
* ══════════════════════════════════════════════════════════════════════ */
typedef struct {
uint8_t id; /* VGM_OFF_xxx (header offset of clock) */
uint8_t flags; /* VGM_CF_DUAL и пр. */
uint16_t clock_khz; /* clock / 1000 (0–65535 кГц) */
} vgm_chip_entry_t; /* 4 байта */
#define VGM_MAX_CHIPS 8 /* макс. чипов в vgm_chip_list[] */
/* ── Состояние парсера (глобальные переменные) ───────────────────── */
/** 1 = достигнут конец VGM-данных (команда 0x66 или конец файла) */
extern volatile uint8_t vgm_song_ended;
/** 1 = пауза (буферы заполняются тишиной) */
extern uint8_t vgm_paused;
/** Текущая VPL-страница, отображённая на #C000 */
extern uint8_t vgm_cur_page;
/** Указатель чтения VGM (#C000–#FFFF) */
extern uint16_t vgm_read_ptr;
/** Номер последней VPL-страницы (0-based) */
extern uint8_t vgm_end_page;
/** Адрес конца данных в последней странице */
extern uint16_t vgm_end_addr;
/** Версия VGM (BCD): 0x0151 = v1.51, 0x0171 = v1.71 */
extern uint16_t vgm_version;
/** Тип обнаруженного OPL-чипа (VGM_CHIP_xxx) */
extern uint8_t vgm_chip_type;
/** Массив обнаруженных чипов (для вывода информации о файле) */
extern uint8_t vgm_chip_count;
extern vgm_chip_entry_t vgm_chip_list[VGM_MAX_CHIPS];
/** Адрес точки петли (#C000–#FFFF); 0 = нет петли */
extern uint16_t vgm_loop_addr;
/** VPL-страница точки петли */
extern uint8_t vgm_loop_page;
/** Общая длительность воспроизведения (секунды), с учётом loop rewinds */
extern uint16_t vgm_total_seconds;
/** Флаг: использовать ли loop при воспроизведении.
* Устанавливается в vgm_parse_header() с учётом loop policy. */
extern uint8_t vgm_loop_enabled;
/* ── Loop/duration policy (читаются из INI, устанавливаются main.c) ── */
extern uint8_t cfg_loop_rewinds; /* кол-во повторов петли (default 1) */
extern uint8_t cfg_min_duration; /* ниже этой длительности (сек) петля запрещена (default 10) */
extern uint16_t cfg_max_duration; /* выше этой длительности (сек) петля запрещена (default 240) */
/* ── GD3 метаданные (только English, ASCII) ─────────────────────── */
#define VGM_GD3_LEN 48 /* макс. длина одного поля (+1 для '\0') */
extern char vgm_gd3_track[VGM_GD3_LEN]; /* Название трека (EN) */
extern char vgm_gd3_game[VGM_GD3_LEN]; /* Название игры (EN) */
extern char vgm_gd3_system[VGM_GD3_LEN]; /* Название системы (EN) */
extern char vgm_gd3_author[VGM_GD3_LEN]; /* Автор (EN) */
/* ── Функции ─────────────────────────────────────────────────────── */
/**
* Разобрать заголовок VGM из #C000 (страница 0 должна быть загружена).
* Заполняет vgm_read_ptr, vgm_cur_page, vgm_loop_addr/page,
* vgm_version, vgm_chip_type, vgm_chip_list[]/vgm_chip_count.
* @return VGM_OK (0) или VGM_ERR_HEADER (1)
*/
uint8_t vgm_parse_header(void);
/**
* Заполнить командный буфер buf_idx (0 = A, 1 = B).
*
* Поддерживаемые VGM-команды:
* 0x5A/0x5B/0x5C/0x5E → CMD_WRITE_B0 (OPL1/OPL2/Y8950/OPL3 bank 0)
* 0x5F → CMD_WRITE_B1 (OPL3 bank 1)
* 0xD0 (port 0/1/2) → CMD_WRITE_B0 / CMD_WRITE_B1 / CMD_WRITE_WAVE (YMF278B/OPL4)
* 0x55 → CMD_WRITE_AY (YM2203 PSG-часть, reg 0-15)
* 0xA0 → CMD_WRITE_AY / CMD_WRITE_AY2 (AY8910, dual)
* 0xBD → CMD_WRITE_SAA (SAA1099, bit7 = chip select)
* 0x61/0x62/0x63/0x70-0x7F → CMD_WAIT
* 0x66 → end; 0x67 → data block skip
*
* Гарантирует CMD_END_BUF в конце буфера.
* @param buf_idx 0 = cmd_buf_a, 1 = cmd_buf_b
*/
void vgm_fill_buffer(uint8_t buf_idx);
/**
* Перемотать VGM на точку петли (если установлена).
* Сбрасывает vgm_song_ended = 0 при успехе.
* @return 1 = петля есть (перемотано), 0 = нет петли
*/
uint8_t vgm_rewind_to_loop(void);
/**
* Разобрать GD3-тег (метаданные) из VGM файла.
* Вызывать после vgm_parse_header().
* Заполняет vgm_gd3_track/game/system/author (ASCII, EN only).
* Переключает VPL-страницу — после вызова нужно восстановить
* vgm_cur_page через wc_mngcvpl().
*/
void vgm_parse_gd3(void);
/**
* Получить имя чипа по header offset (VGM_OFF_xxx).
* @return указатель на строку (ROM), например "YM3812", "AY8910".
* Для неизвестных чипов возвращает NULL.
*/
const char *vgm_chip_name(uint8_t id);
/* ─── VGM-format command blocks (cmdblocks page) ────────────────── */
/** Plugin page содержащая VGM-format command blocks */
#define CMDBLK_PAGE 5
/** Индексы блоков в таблице указателей по адресу 0xC000 */
#define CMDBLK_INIT_OPL3 0 /* OPL3 init: NEW=1, 4-op=0, test=0 */
#define CMDBLK_INIT_OPL2 1 /* OPL2 compat: NEW=0, 4-op=0, test=0 */
#define CMDBLK_SILENCE_OPL 2 /* OPL silence: KeyOff+TL+waveform+BD */
#define CMDBLK_SILENCE_AY 3 /* AY chip 1: mixer off, vol=0 */
#define CMDBLK_SILENCE_AY2 4 /* AY chip 2 (TS): mixer off, vol=0 */
#define CMDBLK_SILENCE_SAA 5 /* SAA1099 chip 1: reset + amp=0 */
#define CMDBLK_SILENCE_SAA2 6 /* SAA1099 chip 2: reset + amp=0 */
#define CMDBLK_SAA_CLK_ON 7 /* MultiSound: SAA clock enable */
#define CMDBLK_SAA_CLK_OFF 8 /* MultiSound: SAA clock disable */
#define CMDBLK_SILENCE_YM2203 9 /* YM2203 chip 1: SSG+FM silence */
#define CMDBLK_SILENCE_YM2203_2 10 /* YM2203 chip 2 (TS): SSG+FM silence */
#define CMDBLK_SAA_INIT 11 /* SAA1099 chip 1: Sound Enable ON */
#define CMDBLK_SAA2_INIT 12 /* SAA1099 chip 2: Sound Enable ON+clk */
#define CMDBLK_INIT_OPL4 13 /* OPL4 init: NEW=1 + NEW2=1 (wave unlock) */
#define CMDBLK_SILENCE_WAVE 14 /* OPL4 wave-часть silence: 24 канала KeyOff+Damp+TL max */
/* ─── Очередь высокоуровневых команд (HL queue) ───────────────── */
/** Типы HL-команд */
#define HLCMD_CMDBLK 1 /* Выполнить VGM-format cmdblock (param = CMDBLK_xxx) */
#define HLCMD_PLAY 2 /* Воспроизведение VGM с текущей позиции до 0x66 */
#define HLCMD_LOOP 3 /* Перемотка к loop-точке, затем play (морфируется→PLAY) */
#define HLCMD_ISR_DONE 4 /* Записать CMD_ISR_DONE в буфер, ISR замирает */
#define HL_QUEUE_MAX 16
typedef struct { uint8_t cmd, param; } hl_entry_t;
/** Глобальные переменные HL queue — заполняются в main.c (build_playback_queue),
* используются внутри vgm_fill_buffer(). */
extern hl_entry_t vgm_hl_queue[HL_QUEUE_MAX];
extern uint8_t vgm_hl_len; /* всего записей */
extern uint8_t vgm_hl_pos; /* текущая запись */
extern uint8_t vgm_hl_abort_pos; /* переход при abort пользователя */
extern uint8_t vgm_loop_count; /* инкрементируется при каждом LOOP */
/* ─── VGZ (gzip-сжатый VGM) ─────────────────────────────────────── */
/**
* Копировать n_pages страниц из мегабуфера (#20+) в TAP (#A1+).
* DI на время работы, восстанавливает Win 0 / Win 3 и EI при выходе.
*
* @param n_pages количество страниц (макс. 31 — размер TAP области)
*/
void copy_pages_to_tap(uint8_t n_pages);
/**
* Распаковать VGZ из TAP-страниц в мегабуфер.
* Вызывается из C — ассемблерная обёртка (inflate_call.s).
*
* @param src_page первая физ. страница источника (TAP, напр. 0xA1)
* @param dst_page первая физ. страница назначения (мегабуфер, напр. 0x20)
* @return 0 — успех, 1 — ошибка (неверные данные / не gzip)
*/
uint8_t inflate_vgz(uint8_t src_page, uint8_t dst_page);
#endif /* VGM_H */
File diff suppressed because it is too large Load Diff