Files

660 lines
25 KiB
ArmAsm

;==============================================================================
; @file isr.s
; Формат записи: [VSINTH, VSINTL, HSINT, next_lo, next_hi]
; Читается тремя OUTI: BC=#24AF -> VSINTH, #23AF -> VSINTL, #22AF -> HSINT,
; затем читается next_ptr.
;
; Командные буферы: cmd_buf_a, cmd_buf_b (по 512 байт, 128 команд).
;
; Переменные ISR (volatile, доступны из C через isr.h):
; _isr_active_buf : 0=A, 1=B (пишет ISR, читает main)
; _isr_enabled : 0/1 (пишет main, читает ISR)
; _isr_play_seconds : uint16 (счётчик секунд, пишет ISR)
; _isr_read_ptr : uint16 (указатель чтения в активном буфере)
;
; Расход памяти _DATA:
; _im2_table_buf : 512 байт (гарантирует 256-aligned)
; _cmd_buf_a/b : 2 x 512 = 1024 байт
; Переменные : 14 байт
; Итого : ~1550 байт
;==============================================================================
.module isr_mod
.globl _isr_ms_ctrl
.globl _isr_active_buf
.globl _isr_enabled
.globl _isr_tick_ctr
.globl _isr_border_color
.globl _isr_play_seconds
.globl _isr_read_ptr
.globl _isr_done
.globl _isr_final_pending
.globl _cmd_buf_a
.globl _cmd_buf_b
.globl _isr_init
.globl _isr_deinit
.globl _call_wc_handler
.globl _spectrum_levels
.globl _vgm_chip_type
; Импорт таблицы позиций из pos_table.s
.globl pos_table
; Порты TSConfig
PORT_HSINT = 0x22AF
PORT_VSINTL = 0x23AF
PORT_VSINTH = 0x24AF
PORT_INTMASK = 0x2AAF
PORT_SYSCONF = 0x20AF ; System Config (turbo bits [1:0])
TURBO_7MHZ = 0x01 ; SysConf value: 7 MHz
TURBO_14MHZ = 0x02 ; SysConf value: 14 MHz
; Адрес вектора FRAME INT в IM2-таблице WC (I=#5B, vector=0xFF → #5BFF)
WC_IM2_VEC = 0x5BFF
; Коды команд (8 типов, шаг 0x20 + ISR_DONE)
CMD_WRITE_AY = 0x00
CMD_INC_SEC = 0x10
CMD_WRITE_AY2 = 0x20
CMD_FINALIZE = 0x30
CMD_WRITE_B0 = 0x40
CMD_SKIP_TICKS= 0x50
CMD_WRITE_SAA = 0x60
CMD_WRITE_B1 = 0x80
CMD_WRITE_WAVE= 0x70
CMD_WAIT = 0xC0
CMD_END_BUF = 0xE0
CMD_ISR_DONE = 0xF0
; Отладка: раскрасить бордюр во время ISR (0=выкл, 1=вкл)
DEBUG_BORDER = 0
; Порты OPL3
OPL3_ADDR0 = 0xC4
OPL3_DATA0 = 0xC5
OPL3_ADDR1 = 0xC6
OPL3_DATA1 = 0xC7
; Порты OPL4 Wave-часть (ZXM-MoonSound: MOON_WREG/MOON_WDAT в moon_driver.asm)
OPL4_WREG = 0x7E
OPL4_WDAT = 0x7F
; Порты AY-3-8910 / YM2149
AY_ADDR = 0xFD ; C-byte, B=0xFF → #FFFD
AY_DATA_B = 0xBF ; B-byte, C=#FD → #BFFD
; Порты SAA1099 (MultiSound card, dual-chip via bit7 of address)
; addr=#01FF data=#00FF
; bit7 of reg byte written to #01FF selects chip (0=chip0, 1=chip1)
; chip1 clk on: automatic on first write to chip1 (address with bit7=1)
; both clk off: OUT #FFFD, d[3]=1
ISR_FREQ = 683 ; informational only (actual timing from pos_table)
;==============================================================================
; isr_init() — Перехват вектора IM2 WC и настройка 683 Hz
;==============================================================================
.area _CODE
;==============================================================================
; Вместо собственной IM2-таблицы (512 байт) используем существующую
; таблицу WC. WC работает в IM2 с I=#5B; вектор FRAME (data bus=0xFF)
; читается из адреса #5BFF (2 байта: lo, hi).
;
; isr_init:
; 1. Сохранить текущий вектор (#5BFF) → _isr_saved_vec
; 2. Записать адрес _isr_handler в #5BFF
; 3. Настроить INTMASK, позицию INT, указатели
; 4. IM 2 (для надёжности)
; Вызывающий код делает EI.
;
; isr_deinit:
; 1. Восстановить сохранённый вектор WC
; 2. Сбросить позицию INT на line=0 (1 INT/кадр)
; Вызывающий код делает EI.
;==============================================================================
_isr_init::
di
;--- Сохранить оригинальный вектор WC ---
ld hl, (WC_IM2_VEC)
ld (_isr_saved_vec), hl
;--- Установить наш обработчик ---
ld hl, #_isr_handler
ld (WC_IM2_VEC), hl
;--- Разрешить только FRAME INT ---
ld bc, #PORT_INTMASK
ld a, #0x01
out (c), a
;--- Инициализировать указатель позиции ---
ld hl, #pos_table
ld (_pos_ptr), hl
;--- Инициализировать счётчик ожидания = 0 ---
ld hl, #0
ld (_isr_wait_ctr), hl
;--- Инициализировать read_ptr → cmd_buf_a ---
ld hl, #_cmd_buf_a
ld (_isr_read_ptr), hl
;--- Счётчик тиков ---
xor a
ld (_isr_tick_ctr), a
;--- Счётчик секунд ---
ld hl, #0
ld (_isr_play_seconds), hl
;--- Reset analyzer previous mask + levels (adjacent) ---
xor a
ld hl, #_spectrum_prev_mask
ld (hl), a
ld de, #(_spectrum_prev_mask + 1)
ld bc, #17
ldir
;--- Начальная позиция: строка 0, hpos 0 ---
ld bc, #PORT_HSINT
out (c), a
ld b, #>PORT_VSINTL
out (c), a
ld b, #>PORT_VSINTH
out (c), a
;--- Подтвердить IM2 (WC и так в IM2, но для надёжности) ---
im 2
ret
;==============================================================================
; isr_deinit() — Восстановить вектор WC и сбросить INT-позиции
;==============================================================================
_isr_deinit::
di
;--- Проверить: вызывался ли isr_init? ---
ld hl, (_isr_saved_vec)
ld a, h
or l
ret z ; saved_vec == 0 → init не вызывался
;--- Восстановить оригинальный вектор WC ---
ld (WC_IM2_VEC), hl
;--- Сбросить saved_vec (защита от повторного вызова) ---
ld hl, #0
ld (_isr_saved_vec), hl
;--- Сбросить позицию INT: line=0, hpos=0 (один INT/кадр) ---
ld bc, #PORT_HSINT
xor a
out (c), a
ld b, #>PORT_VSINTL
out (c), a
ld b, #>PORT_VSINTH
out (c), a
;--- INTMASK = FRAME only (WC по умолчанию) ---
ld bc, #PORT_INTMASK
ld a, #0x01
out (c), a
; Не делаем EI — вызывающий код решает сам
ret
;==============================================================================
; ISR_HANDLER — обработчик прерывания FRAME (2734 Гц)
; ─── OUTI-блок и таблицу НЕ ТРОГАТЬ! ────────────────────────────────────
;==============================================================================
.area _CODE
_isr_handler::
push af
.if DEBUG_BORDER
ld a, #0x06 ; DEBUG: yellow border
out (0xFE), a
.endif
push hl
push bc
push de
;--- OUTI×3: записать следующую позицию INT из таблицы ---
; Формат записи: [vsinth_byte, vsintl_byte, hsint_byte, next_lo, next_hi]
; BC стартует 0x24AF: OUTI→0x24AF(VSINTH), B→0x23; OUTI→0x23AF(VSINTL), B→0x22; OUTI→0x22AF(HSINT)
ld hl, (_pos_ptr)
ld bc, #0x25AF ; B=#25, C=#AF
outi ; → VSINTH (#24AF) outi predecrement B: B=0x24, C=0xAF
outi ; → VSINTL (#23AF)
outi ; → HSINT (#22AF)
; Следующий ptr (2 байта)
ld a, (hl)
inc hl
ld h, (hl)
ld l, a ; HL = next_ptr
ld (_pos_ptr), hl
;--- Счётчик тиков: простой INC byte (21T, всегда) ---
ld hl, #_isr_tick_ctr
inc (hl) ; 11T: wrap at 256 — main loop считает секунды
;--- WAIT_CTR: если 0 — выполнять команды ---
ld a, (_isr_enabled)
or a
jp z, _isr_exit ; не активен — быстрый выход
ld hl, (_isr_wait_ctr)
ld a, h
or l
jp z, _isr_exec_cmd
dec hl
ld (_isr_wait_ctr), hl
_isr_exit:
.if DEBUG_BORDER
ld a, (_isr_border_color) ; DEBUG: restore border from variable
out (0xFE), a
.endif
pop de
pop bc
pop hl
pop af
ei
ret
;--- Выполнение команд --------------------------------------------------
_isr_exec_cmd:
ld hl, (_isr_read_ptr)
ex de, hl ; DE = указатель команд
_isr_cmd_loop:
ld a, (de)
inc de
; Dispatch: command types — hot path first
; OPL Bank 0 writes (most common register writes)
cp #CMD_WRITE_B0
jp z, _isr_write_b0
; ★ CMD_SKIP_TICKS — every ISR tick ends with this (promoted from #12)
cp #CMD_SKIP_TICKS
jp z, _isr_skip_ticks
; OPL Bank 1 writes (OPL3 second bank)
cp #CMD_WRITE_B1
jp z, _isr_write_b1
; Wait (long pauses, less common than SKIP_TICKS)
cp #CMD_WAIT
jp z, _isr_do_wait
; AY/SAA/end commands (less frequent per-interrupt)
cp #CMD_WRITE_AY
jp z, _isr_write_ay
cp #CMD_WRITE_AY2
jp z, _isr_write_ay2
cp #CMD_WRITE_SAA
jp z, _isr_write_saa
; OPL4 Wave-часть (ZXM-MoonSound), редко используется
cp #CMD_WRITE_WAVE
jp z, _isr_write_wave
; Rare commands
cp #CMD_END_BUF
jp z, _isr_do_end_buf
cp #CMD_ISR_DONE
jp z, _isr_cmd_done
cp #CMD_INC_SEC
jp z, _isr_inc_sec
cp #CMD_FINALIZE
jp z, _isr_cmd_finalize
; Unknown command: skip 3 bytes, continue
inc de
inc de
inc de
jp _isr_cmd_loop
_isr_do_end_buf:
ld a, (_isr_active_buf)
xor #1
ld (_isr_active_buf), a
jr nz, _isr_end_use_b
ld hl, #_cmd_buf_a
jr _isr_end_save_ptr
_isr_end_use_b:
ld hl, #_cmd_buf_b
_isr_end_save_ptr:
ld (_isr_read_ptr), hl
ex de, hl
jp _isr_cmd_loop
_isr_write_b0:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld a, (de) ; reg
ld c, #OPL3_ADDR0 ; B=#20, don't-care for C4-C7
out (c), a
inc de
ld a, (de) ; val
inc c ; C5 = data port
out (c), a
ld c, #0xAF ; BC=#20AF (B still #20)
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
_isr_write_b1:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld a, (de)
ld c, #OPL3_ADDR1 ; B=#20, don't-care for C4-C7
out (c), a
inc de
ld a, (de)
inc c ; C7 = data port
out (c), a
ld c, #0xAF ; BC=#20AF (B still #20)
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de
jp _isr_cmd_loop
_isr_do_wait:
ld a, (de) ; lo
inc de
ld l, a
ld a, (de) ; hi
inc de
ld h, a
inc de ; skip pad byte
ld (_isr_wait_ctr), hl
ex de, hl
ld (_isr_read_ptr), hl
jp _isr_exit
;--- AY8910 chip 1 write: [reg, val, pad] --------------------------------
_isr_write_ay:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld bc, #0xFFFD
ld a, (_isr_ms_ctrl) ; MultiSound ctrl (bit0=0 → chip1)
out (c), a
ld a, (de) ; reg
out (c), a ; select AY register
inc de
ld a, (de) ; val
ld b, #0xBF ; BC = #BFFD
out (c), a ; write data
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- AY8910 chip 2 write (TurboSound): [reg, val, pad] -------------------
_isr_write_ay2:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld bc, #0xFFFD
ld a, (_isr_ms_ctrl) ; MultiSound ctrl base
or a, #0x01 ; bit0=1 → chip2
out (c), a
ld a, (de) ; reg
out (c), a ; select register
inc de
ld a, (de) ; val
ld b, #0xBF ; BC = #BFFD
out (c), a
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- SAA1099 write: [reg, val, pad] ----------------------------------------
; reg bit7 = chip select (0=chip0, 1=chip1), passed directly to #01FF.
; HDL uses bit7 for CS routing; SAA1099 ignores upper address bits.
_isr_write_saa:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld a, (de) ; reg (bit7 = chip select)
ld bc, #0x01FF ; SAA address port
out (c), a ; write register address (bit7 → chip CS)
inc de
ld a, (de) ; val
ld b, #0x00 ; BC = #00FF SAA data port
out (c), a
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- OPL4 Wave-часть write (ZXM-MoonSound): [reg, val, pad] --------------
; Порты #7E (addr) / #7F (data) — как MOON_WREG/MOON_WDAT в оригинальном
; Z80-драйвере карты (moon_driver.asm). B don't-care (карта декодирует
; только младший байт адреса), как и для OPL3_ADDR0..OPL3_DATA1.
;
; ВАЖНО: в отличие от FM-банков (isr_write_b0/b1), референсный NedoOS-
; драйвер (common/moonsound.asm: opl4_wait) ждёт busy-флаг (бит0 порта
; #C4) перед КАЖДОЙ записью в порты #7E/#7F, включая обычные регистры
; wave-канала (не только memory-access режим) — без этого запись может
; быть потеряна чипом. См. chip_wait()/wave_out_reg()/wave_out_dat() в
; lib/vgm.c (та же логика для синхронной заливки сэмплов).
_isr_write_wave:
ld bc, #PORT_SYSCONF
ld a, #TURBO_7MHZ
out (c), a ; → 7 MHz
ld c, #OPL4_WREG ; B=#20, don't-care (как MOON_WREG)
wwave_wait1:
in a, (0xC4) ; busy-флаг (бит0)
rrca
jr c, wwave_wait1
ld a, (de) ; reg
out (c), a
inc de
inc c ; C=#7F = data port (MOON_WDAT)
wwave_wait2:
in a, (0xC4)
rrca
jr c, wwave_wait2
ld a, (de) ; val
out (c), a
ld bc, #PORT_SYSCONF
ld a, #TURBO_14MHZ
out (c), a ; → 14 MHz
inc de
inc de ; pad
jp _isr_cmd_loop
;--- CMD_INC_SEC: инкремент счётчика секунд [0,0,0] ---------------------
_isr_inc_sec:
inc de
inc de
inc de ; пропустить 3 байта параметров
ld hl, (_isr_play_seconds)
inc hl
ld (_isr_play_seconds), hl
jp _isr_cmd_loop
;--- CMD_SKIP_TICKS: skip N + spectrum bitmask [N,smask_lo,smask_hi] --------
; CMD_SKIP_TICKS(N):
; N=0 — не перепрограммирует, просто exit (1 тик пауза)
; N>=1 — пропустить N записей, перепрограммировать INT (N+1 тиков пауза)
; Max N=55 (кадр минус 1).
_isr_skip_ticks:
ld a, (de) ; N = skip count (0..55)
inc de
;--- Spectrum: KeyOn attack + sustained activity floor ---
push af ; save N
ld a, (de) ; mask_lo
inc de
ld l, a ; L = mask_lo
ld a, (de) ; mask_hi
inc de
ld h, a ; H = mask_hi
push de ; save read_ptr
; OPL4 gets edge/sustain processing. Other chips keep the old
; event-analyzer behaviour by comparing every mask with zero.
ld a, (_vgm_chip_type)
cp #4 ; VGM_CHIP_OPL4
jr z, _skip_spec_opl4
ld de, #0
jr _skip_spec_masks_ready
_skip_spec_opl4:
ld de, (_spectrum_prev_mask)
ld (_spectrum_prev_mask), hl
_skip_spec_masks_ready:
; E/D are previous low/high, L/H are current low/high.
ld c, e ; previous low byte
ld e, h ; preserve current high byte
ld a, l ; current low byte
ld hl, #_spectrum_levels
call _skip_spec_byte
ld c, d ; previous high byte
ld a, e ; current high byte
call _skip_spec_byte
pop de ; restore read_ptr
pop af ; restore N
; Сохранить read_ptr
ex de, hl
ld (_isr_read_ptr), hl
; N=0: просто exit (как CMD_WAIT(0) без накладных на wait_ctr)
or a
jp z, _isr_exit
; Пропустить N записей pos_table: перейти по N-1 ссылкам,
; затем OUTI×3 из N-ой записи (перепрограммировать INT)
ld hl, (_pos_ptr)
dec a ; N-1 линков пропустить
jr z, _skip_program ; N=1: сразу программируем текущую
ld b, a
_skip_loop:
inc hl ; пропустить VSINTH
inc hl ; VSINTL
inc hl ; HSINT
ld a, (hl) ; next_lo
inc hl
ld h, (hl) ; next_hi
ld l, a
djnz _skip_loop
_skip_program:
; HL = запись для перепрограммирования INT портов
ld bc, #0x25AF
outi ; VSINTH (#24AF)
outi ; VSINTL (#23AF)
outi ; HSINT (#22AF)
; Следующий next_ptr
ld a, (hl)
inc hl
ld h, (hl)
ld l, a
ld (_pos_ptr), hl
jp _isr_exit
; Unpack one activity byte into eight spectrum bars.
; A=current bits, C=previous bits, HL=first level; advances HL by 8.
; Rising edge: peak 8. Sustained KeyOn: decay is allowed down to floor 3.
_skip_spec_byte:
ld b, #8
_skip_spec_bit:
rr c ; carry = previous activity bit
jr c, _skip_spec_was_on
rra ; carry = current activity bit
jr nc, _skip_spec_next
ld (hl), #8 ; new KeyOn attack
jr _skip_spec_next
_skip_spec_was_on:
rra ; carry = current activity bit
jr nc, _skip_spec_next ; KeyOff: ordinary decay
push af ; preserve remaining current bits
ld a, (hl)
cp #3
jr nc, _skip_spec_floor_ok
ld (hl), #3 ; sustained activity floor
_skip_spec_floor_ok:
pop af
_skip_spec_next:
inc hl
djnz _skip_spec_bit
ret
;--- CMD_ISR_DONE: заморозить ISR, выставить флаг [0,0,0] ------------------
; ISR зацикливается на этой команде, не переключает буферы.
; Main loop ждёт isr_done == 1 перед завершением.
_isr_cmd_done:
ld a, #1
ld (_isr_done), a
jr _isr_cmd_freeze
;--- CMD_FINALIZE: музыкальный поток полностью исполнен -----------------
; Замереть на маркере и попросить main собрать shutdown как при ESC.
_isr_cmd_finalize:
ld a, #1
ld (_isr_final_pending), a
_isr_cmd_freeze:
; DE уже указывает на первый параметр: вернуть read_ptr на opcode.
ex de, hl
dec hl
ld (_isr_read_ptr), hl
jp _isr_exit
;==============================================================================
; call_wc_handler() — Вызов WC ISR из main loop (C-callable)
;
; WC ожидает периодический вызов своего ISR для обновления TMN,
; клавиатуры, часов и др. Вызываем из main loop ~50 Гц.
;
; Механизм: DI → JP (saved_vec). WC handler делает EI+RET
; обратно к вызывающему коду (tail-call через JP).
;==============================================================================
_call_wc_handler::
ld hl, (_isr_saved_vec)
ld a, h
or l
ret z ; saved_vec==0 → isr_init не вызывался
di
jp (hl) ; tail-call: WC handler делает EI+RET
;==============================================================================
.area _DATA
_isr_ms_ctrl:: .db 0xFE ; MultiSound ctrl: FM OFF + SAA OFF (idle)
_isr_active_buf:: .db 0
_isr_enabled:: .db 0
_isr_tick_ctr:: .db 0 ; инкрементируется каждые ISR тик (2734/сек, wrap 256)
_isr_border_color:: .db 0 ; цвет бордюра при выходе из ISR (пишет main, читает ISR)
_isr_play_seconds:: .dw 0 ; инкрементируется CMD_INC_SEC
_isr_read_ptr:: .dw 0
_isr_wait_ctr:: .dw 0
_isr_done:: .db 0 ; 1 = ISR замер на CMD_ISR_DONE, готов к завершению
_isr_final_pending:: .db 0 ; 1 = последний VGM 0x66 уже исполнен
_pos_ptr: .dw 0 ; инициализируется в isr_init()
_isr_saved_vec: .dw 0 ; сохранённый вектор WC (#5BFF)
;==============================================================================
; Spectrum analyzer levels (16 bars, updated by 16-bit bitmask in CMD_SKIP_TICKS)
;==============================================================================
_spectrum_prev_mask:
.ds 2 ; previous CMD_SKIP activity bits
_spectrum_levels::
.ds 16 ; 16 bytes, one per bar (0-8)
;==============================================================================
; Командные буферы (512 байт каждый)
;==============================================================================
.even
_cmd_buf_a::
.ds 512
_cmd_buf_b::
.ds 512