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
+13
View File
@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build VGM Player",
"type": "shell",
"command": "cmd /c build.bat",
"isBackground": false,
"problemMatcher": [],
"group": "build"
}
]
}
+584
View File
@@ -0,0 +1,584 @@
; ============================================================================
; cmdblocks.asm — VGM-format command blocks for chip init/silence
;
; Compiled with sjasmplus → raw binary 16KB, appended as plugin page 5.
;
; Each block contains standard VGM opcodes identical to those in .vgm files:
; 0x5E rr vv — OPL3 bank 0 write
; 0x5F rr vv — OPL3 bank 1 write
; 0x55 rr vv — YM2203 write (also used for MultiSound control regs 0xF0+)
; 0xA0 rr vv — AY8910 write (bit7 of rr = chip 2)
; 0xBD rr vv — SAA1099 write (bit7 of rr = chip 2)
; 0x66 — end of block
;
; The standard vgm_fill_buffer() parses these blocks in cmdblk_mode,
; converting VGM opcodes to ISR commands exactly as it does for .vgm data.
; This eliminates the need for a separate command copier — budget counting,
; buffer overflow protection and command parsing all come for free.
;
; NOTE on SAA clock: MultiSound intercepts AY reg select 0xF0-0xFF on
; port 0xFFFD for hardware control. VGM opcode 0xA0 strips bit7 of
; the register byte for chip selection, so 0xA0 CANNOT send regs >= 0x80
; to chip 1. We use YM2203 opcode 0x55 instead — it sends the register
; byte raw to 0xFFFD, which MultiSound intercepts correctly.
;
; Layout:
; 0xC000 : pointer table (uint16 × N) — absolute addresses of blocks
; 0xC000+N*2.. : block data (variable-length VGM commands, 3 bytes each)
;
; Usage from C:
; wc_mngc_pl(CMDBLK_PAGE);
; vgm_read_ptr = ((uint16_t*)0xC000)[blk_idx];
; vgm_cmdblk_mode = 1; vgm_song_ended = 0;
; vgm_fill_buffer(buf_idx);
; vgm_cmdblk_mode = 0;
;
; Assembled with: sjasmplus asm\cmdblocks.asm
; → SAVEBIN "build/cmdblocks.bin", #C000, #4000
; ============================================================================
DEVICE NOSLOT64K
ORG #C000
; ── VGM opcodes ─────────────────────────────────────────────────────
VGM_OPL_B0 EQU #5E ; OPL3 bank 0: 5E rr vv
VGM_OPL_B1 EQU #5F ; OPL3 bank 1: 5F rr vv
VGM_YM2203 EQU #55 ; YM2203 SSG+FM chip 1: 55 rr vv
VGM_YM2203_2 EQU #A5 ; YM2203 SSG+FM chip 2: A5 rr vv
VGM_AY EQU #A0 ; AY8910: A0 rr vv (bit7 = chip 2)
VGM_SAA EQU #BD ; SAA1099: BD rr vv (bit7 = chip 2)
VGM_OPL4 EQU #D0 ; YMF278B (OPL4): D0 pp rr vv (pp = port 0/1/2)
VGM_END EQU #66 ; End of data
; ── Macros (each emits standard VGM bytes, 3 bytes per command) ─────
MACRO opl3_bank0 addr, val
db VGM_OPL_B0, addr, val
ENDM
MACRO opl3_bank1 addr, val
db VGM_OPL_B1, addr, val
ENDM
MACRO ym2203_write reg, val
db VGM_YM2203, reg, val
ENDM
MACRO ym2203_2_write reg, val
db VGM_YM2203_2, reg, val
ENDM
MACRO ay_write reg, val
db VGM_AY, reg, val
ENDM
MACRO ay2_write reg, val
db VGM_AY, reg | #80, val
ENDM
MACRO saa_write reg, val
db VGM_SAA, reg, val
ENDM
MACRO saa2_write reg, val
db VGM_SAA, reg | #80, val
ENDM
MACRO opl4_wave_write reg, val
db VGM_OPL4, #02, reg, val
ENDM
MACRO blk_end
db VGM_END
ENDM
; ═══════════════════════════════════════════════════════════════════════
; Pointer table (indices = CMDBLK_xxx from vgm.h)
; ═══════════════════════════════════════════════════════════════════════
ptr_table:
dw blk_init_opl3 ; 0 = CMDBLK_INIT_OPL3
dw blk_init_opl2 ; 1 = CMDBLK_INIT_OPL2
dw blk_silence_opl ; 2 = CMDBLK_SILENCE_OPL
dw blk_silence_ay ; 3 = CMDBLK_SILENCE_AY
dw blk_silence_ay2 ; 4 = CMDBLK_SILENCE_AY2
dw blk_silence_saa ; 5 = CMDBLK_SILENCE_SAA
dw blk_silence_saa2 ; 6 = CMDBLK_SILENCE_SAA2
dw blk_saa_clock_on ; 7 = CMDBLK_SAA_CLK_ON
dw blk_saa_clock_off ; 8 = CMDBLK_SAA_CLK_OFF
dw blk_silence_ym2203 ; 9 = CMDBLK_SILENCE_YM2203
dw blk_silence_ym2203_2 ; 10 = CMDBLK_SILENCE_YM2203_2
dw blk_saa_init ; 11 = CMDBLK_SAA_INIT
dw blk_saa2_init ; 12 = CMDBLK_SAA2_INIT
dw blk_init_opl4 ; 13 = CMDBLK_INIT_OPL4
dw blk_silence_wave ; 14 = CMDBLK_SILENCE_WAVE
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_INIT_OPL3 — OPL3 init (NEW=1, clean state)
;
; OPL3 NEW=1 enables full OPL3 register set. 4-op connections are
; cleared (reg 0x104=0) to avoid leftover pairing from a previous
; track. Test/WSE register 0x01 is zeroed — OPL3 does not use WSE;
; waveform select is implicit via E0-F5 registers.
;
; VGM stream: 5F 05 01 5F 04 00 5E 01 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_init_opl3:
opl3_bank1 #05, #01 ; VGM: 5F 05 01 — OPL3 NEW=1
opl3_bank1 #04, #00 ; VGM: 5F 04 00 — 4-op OFF (clean)
opl3_bank0 #01, #00 ; VGM: 5E 01 00 — Test/WSE=0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_INIT_OPL4 — YMF278B (OPL4, ZXM-MoonSound) init: NEW=1 + NEW2=1
;
; Как CMDBLK_INIT_OPL3 (NEW=1, L/R через C0-C8), но дополнительно взводит
; бит NEW2 (бит 1 регистра Bank1 #05). На реальном чипе YMF278B этот бит
; обязателен — без него чип игнорирует любые записи в wave-часть (порты
; #7E/#7F), т.е. VGM-команды 0xD0 с портом 2 останутся немы.
;
; VGM stream: 5F 05 03 5F 04 00 5E 01 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_init_opl4:
opl3_bank1 #05, #03 ; VGM: 5F 05 03 — NEW=1 + NEW2=1 (разблокировать wave)
opl3_bank1 #04, #00 ; VGM: 5F 04 00 — 4-op OFF (clean)
opl3_bank0 #01, #00 ; VGM: 5E 01 00 — Test/WSE=0
opl4_wave_write #02, #10 ; WT headers 384-511 from 0x200000,
; memory access OFF (sound generation mode)
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_INIT_OPL2 — OPL2 compat init (NEW=0, clean state)
;
; Forces OPL2 compatibility mode. Critically: 0x104=0 clears any
; residual 4-op state that would persist after an OPL3 track (the
; most common cause of OPL2 artifacts). Test/WSE register = 0.
;
; VGM stream: 5F 05 00 5F 04 00 5E 01 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_init_opl2:
opl3_bank1 #05, #00 ; VGM: 5F 05 00 — OPL3 NEW=0 (OPL2)
opl3_bank1 #04, #00 ; VGM: 5F 04 00 — 4-op OFF (clean)
opl3_bank0 #01, #00 ; VGM: 5E 01 00 — Test/WSE=0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_OPL — Full OPL3 silence (both banks)
;
; VGM stream (55 commands):
; 5E B0..B8 00 × 9 Key Off Bank 0 (ch 0-8)
; 5F B0..B8 00 × 9 Key Off Bank 1 (ch 0-8)
; 5E 4x 3F ×18 TL max Bank 0 (18 operators)
; 5F 4x 3F ×18 TL max Bank 1 (18 operators)
; 5E BD 00 Percussion Off
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_opl:
; ── Key Off: Bank 0 — VGM: 5E B0..B8 00 ──
opl3_bank0 #B0, #00
opl3_bank0 #B1, #00
opl3_bank0 #B2, #00
opl3_bank0 #B3, #00
opl3_bank0 #B4, #00
opl3_bank0 #B5, #00
opl3_bank0 #B6, #00
opl3_bank0 #B7, #00
opl3_bank0 #B8, #00
; ── Key Off: Bank 1 — VGM: 5F B0..B8 00 ──
opl3_bank1 #B0, #00
opl3_bank1 #B1, #00
opl3_bank1 #B2, #00
opl3_bank1 #B3, #00
opl3_bank1 #B4, #00
opl3_bank1 #B5, #00
opl3_bank1 #B6, #00
opl3_bank1 #B7, #00
opl3_bank1 #B8, #00
; ── TL max: Bank 0 — VGM: 5E {40-45,48-4D,50-55} 3F ──
opl3_bank0 #40, #3F
opl3_bank0 #41, #3F
opl3_bank0 #42, #3F
opl3_bank0 #43, #3F
opl3_bank0 #44, #3F
opl3_bank0 #45, #3F
opl3_bank0 #48, #3F
opl3_bank0 #49, #3F
opl3_bank0 #4A, #3F
opl3_bank0 #4B, #3F
opl3_bank0 #4C, #3F
opl3_bank0 #4D, #3F
opl3_bank0 #50, #3F
opl3_bank0 #51, #3F
opl3_bank0 #52, #3F
opl3_bank0 #53, #3F
opl3_bank0 #54, #3F
opl3_bank0 #55, #3F
; ── TL max: Bank 1 — VGM: 5F {40-45,48-4D,50-55} 3F ──
opl3_bank1 #40, #3F
opl3_bank1 #41, #3F
opl3_bank1 #42, #3F
opl3_bank1 #43, #3F
opl3_bank1 #44, #3F
opl3_bank1 #45, #3F
opl3_bank1 #48, #3F
opl3_bank1 #49, #3F
opl3_bank1 #4A, #3F
opl3_bank1 #4B, #3F
opl3_bank1 #4C, #3F
opl3_bank1 #4D, #3F
opl3_bank1 #50, #3F
opl3_bank1 #51, #3F
opl3_bank1 #52, #3F
opl3_bank1 #53, #3F
opl3_bank1 #54, #3F
opl3_bank1 #55, #3F
; ── Waveform reset: Bank 0 — VGM: 5E {E0-E5,E8-ED,F0-F5} 00 ──
; Clears non-sine waveforms that linger between tracks.
; Critical for clean OPL2 playback after OPL3 usage.
opl3_bank0 #E0, #00
opl3_bank0 #E1, #00
opl3_bank0 #E2, #00
opl3_bank0 #E3, #00
opl3_bank0 #E4, #00
opl3_bank0 #E5, #00
opl3_bank0 #E8, #00
opl3_bank0 #E9, #00
opl3_bank0 #EA, #00
opl3_bank0 #EB, #00
opl3_bank0 #EC, #00
opl3_bank0 #ED, #00
opl3_bank0 #F0, #00
opl3_bank0 #F1, #00
opl3_bank0 #F2, #00
opl3_bank0 #F3, #00
opl3_bank0 #F4, #00
opl3_bank0 #F5, #00
; ── Waveform reset: Bank 1 — VGM: 5F {E0-E5,E8-ED,F0-F5} 00 ──
opl3_bank1 #E0, #00
opl3_bank1 #E1, #00
opl3_bank1 #E2, #00
opl3_bank1 #E3, #00
opl3_bank1 #E4, #00
opl3_bank1 #E5, #00
opl3_bank1 #E8, #00
opl3_bank1 #E9, #00
opl3_bank1 #EA, #00
opl3_bank1 #EB, #00
opl3_bank1 #EC, #00
opl3_bank1 #ED, #00
opl3_bank1 #F0, #00
opl3_bank1 #F1, #00
opl3_bank1 #F2, #00
opl3_bank1 #F3, #00
opl3_bank1 #F4, #00
opl3_bank1 #F5, #00
; ── Percussion Off — VGM: 5E BD 00 ──
opl3_bank0 #BD, #00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_WAVE — YMF278B (OPL4) wave-часть silence, 24 канала
;
; CMDBLK_SILENCE_OPL глушит только FM-банки (0x5E/0x5F) — wave-каналы
; (табличный синтез, до 24 голосов) им не затрагиваются и продолжают
; звучать после выхода из плеера. Для каждого канала N (0-23):
; рег. (0x68+N), data=0x40 — Key Off + Damp (бит7:6=01, форсированный
; быстрый спад огибающей, EG_DMP)
; рег. (0x50+N), data=0xFF — TL=0x7F (макс. затухание) + LD=1
; (мгновенно, без интерполяции громкости)
; Оба варианта дублируют друг друга (как KeyOff+TL max у FM-банков) —
; гарантия тишины независимо от состояния огибающей канала.
;
; VGM stream: D0 02 68 40 ... D0 02 7F 40 (Key Off+Damp, 24 канала)
; D0 02 50 FF ... D0 02 67 FF (TL max, 24 канала)
; 66
; ═══════════════════════════════════════════════════════════════════════
blk_silence_wave:
opl4_wave_write #68, #40
opl4_wave_write #69, #40
opl4_wave_write #6A, #40
opl4_wave_write #6B, #40
opl4_wave_write #6C, #40
opl4_wave_write #6D, #40
opl4_wave_write #6E, #40
opl4_wave_write #6F, #40
opl4_wave_write #70, #40
opl4_wave_write #71, #40
opl4_wave_write #72, #40
opl4_wave_write #73, #40
opl4_wave_write #74, #40
opl4_wave_write #75, #40
opl4_wave_write #76, #40
opl4_wave_write #77, #40
opl4_wave_write #78, #40
opl4_wave_write #79, #40
opl4_wave_write #7A, #40
opl4_wave_write #7B, #40
opl4_wave_write #7C, #40
opl4_wave_write #7D, #40
opl4_wave_write #7E, #40
opl4_wave_write #7F, #40
opl4_wave_write #50, #FF
opl4_wave_write #51, #FF
opl4_wave_write #52, #FF
opl4_wave_write #53, #FF
opl4_wave_write #54, #FF
opl4_wave_write #55, #FF
opl4_wave_write #56, #FF
opl4_wave_write #57, #FF
opl4_wave_write #58, #FF
opl4_wave_write #59, #FF
opl4_wave_write #5A, #FF
opl4_wave_write #5B, #FF
opl4_wave_write #5C, #FF
opl4_wave_write #5D, #FF
opl4_wave_write #5E, #FF
opl4_wave_write #5F, #FF
opl4_wave_write #60, #FF
opl4_wave_write #61, #FF
opl4_wave_write #62, #FF
opl4_wave_write #63, #FF
opl4_wave_write #64, #FF
opl4_wave_write #65, #FF
opl4_wave_write #66, #FF
opl4_wave_write #67, #FF
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_AY — Silence AY8910 chip 1
;
; VGM stream:
; A0 07 3F Mixer: all tone+noise off
; A0 08 00 Volume A = 0
; A0 09 00 Volume B = 0
; A0 0A 00 Volume C = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ay:
ay_write 7, #3F ; VGM: A0 07 3F
ay_write 8, #00 ; VGM: A0 08 00
ay_write 9, #00 ; VGM: A0 09 00
ay_write 10, #00 ; VGM: A0 0A 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_AY2 — Silence AY8910 chip 2 (TurboSound)
;
; VGM stream (bit7 = chip 2):
; A0 87 3F Mixer off
; A0 88 00 Volume A = 0
; A0 89 00 Volume B = 0
; A0 8A 00 Volume C = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ay2:
ay2_write 7, #3F ; VGM: A0 87 3F
ay2_write 8, #00 ; VGM: A0 88 00
ay2_write 9, #00 ; VGM: A0 89 00
ay2_write 10, #00 ; VGM: A0 8A 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_SAA — Silence SAA1099 chip 1
;
; VGM stream:
; BD 1C 02 Reset all (sound gen + internal state)
; BD 1C 00 Sound enable off
; BD 00..05 00 Amplitude ch 0-5 = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_saa:
saa_write #1C, #02 ; VGM: BD 1C 02 — Reset all
saa_write #1C, #00 ; VGM: BD 1C 00 — Sound enable off
saa_write #00, #00 ; VGM: BD 00 00 — Amp ch0 = 0
saa_write #01, #00 ; VGM: BD 01 00 — Amp ch1 = 0
saa_write #02, #00 ; VGM: BD 02 00 — Amp ch2 = 0
saa_write #03, #00 ; VGM: BD 03 00 — Amp ch3 = 0
saa_write #04, #00 ; VGM: BD 04 00 — Amp ch4 = 0
saa_write #05, #00 ; VGM: BD 05 00 — Amp ch5 = 0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_SAA2 — Silence SAA1099 chip 2
;
; VGM stream (bit7 = chip 2):
; BD 9C 02 Reset all
; BD 9C 00 Sound enable off
; BD 80..85 00 Amplitude ch 0-5 = 0
; 66 End
; ═══════════════════════════════════════════════════════════════════════
blk_silence_saa2:
saa2_write #1C, #02 ; VGM: BD 9C 02 — Reset all
saa2_write #1C, #00 ; VGM: BD 9C 00 — Sound enable off
saa2_write #00, #00 ; VGM: BD 80 00 — Amp ch0 = 0
saa2_write #01, #00 ; VGM: BD 81 00 — Amp ch1 = 0
saa2_write #02, #00 ; VGM: BD 82 00 — Amp ch2 = 0
saa2_write #03, #00 ; VGM: BD 83 00 — Amp ch3 = 0
saa2_write #04, #00 ; VGM: BD 84 00 — Amp ch4 = 0
saa2_write #05, #00 ; VGM: BD 85 00 — Amp ch5 = 0
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA_CLK_ON — Enable SAA1099 + FM clock (MultiSound)
;
; Uses YM2203 opcode (0x55) so that register 0xF3 is sent raw to 0xFFFD.
; AY opcode 0xA0 would strip bit7 for chip selection → can't reach 0xF3.
; MultiSound intercepts reg select 0xF0-0xFF for hardware control.
; bit0=1(YM2) bit1=1(normal) bit2=0(FM ON) bit3=0(SAA ON)
;
; VGM stream: 55 F3 00 66
; ═══════════════════════════════════════════════════════════════════════
blk_saa_clock_on:
ym2203_write #F3, #00 ; VGM: 55 F3 00 — SAA+FM clock ON
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA_CLK_OFF — Disable SAA1099 + FM clock (MultiSound)
;
; VGM stream: 55 FF 00 66
; bit2=1(FM OFF) bit3=1(SAA OFF)
; ═══════════════════════════════════════════════════════════════════════
blk_saa_clock_off:
ym2203_write #FF, #00 ; VGM: 55 FF 00 — SAA+FM clock OFF
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_YM2203 — Silence YM2203 (SSG + FM) + reset prescaler
;
; Prescaler reset (reg 0x2D, write-only, value ignored):
; 55 2D 00 Reset prescaler → defaults: /6 FM, /4 SSG
; Prevents state leak from tracks that set /3+/2 via 0x2E.
; SSG (AY-compatible):
; 55 07 3F Mixer: all tone+noise off
; 55 08 00 SSG Volume A = 0
; 55 09 00 SSG Volume B = 0
; 55 0A 00 SSG Volume C = 0
; FM — Key Off all 3 channels:
; 55 28 00 Key Off ch 0
; 55 28 01 Key Off ch 1
; 55 28 02 Key Off ch 2
; FM — TL max (0x7F) all 12 operators:
; 55 40..42 7F slot 1 (M1): ch 0,1,2
; 55 44..46 7F slot 3 (C1): ch 0,1,2
; 55 48..4A 7F slot 2 (M2): ch 0,1,2
; 55 4C..4E 7F slot 4 (C2): ch 0,1,2
; Total: 20 VGM commands + end = 61 bytes
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ym2203:
; ── SSG silence — VGM: 55 rr vv ──
ym2203_write 7, #3F ; VGM: 55 07 3F — Mixer off
ym2203_write 8, #00 ; VGM: 55 08 00 — SSG Vol A = 0
ym2203_write 9, #00 ; VGM: 55 09 00 — SSG Vol B = 0
ym2203_write 10, #00 ; VGM: 55 0A 00 — SSG Vol C = 0
; ── FM Key Off — VGM: 55 28 xx ──
ym2203_write #28, #00 ; VGM: 55 28 00 — Key Off ch 0
ym2203_write #28, #01 ; VGM: 55 28 01 — Key Off ch 1
ym2203_write #28, #02 ; VGM: 55 28 02 — Key Off ch 2
; ── FM TL max — VGM: 55 4x 7F ──
ym2203_write #40, #7F ; VGM: 55 40 7F — TL slot1 ch0
ym2203_write #41, #7F ; VGM: 55 41 7F — TL slot1 ch1
ym2203_write #42, #7F ; VGM: 55 42 7F — TL slot1 ch2
ym2203_write #44, #7F ; VGM: 55 44 7F — TL slot3 ch0
ym2203_write #45, #7F ; VGM: 55 45 7F — TL slot3 ch1
ym2203_write #46, #7F ; VGM: 55 46 7F — TL slot3 ch2
ym2203_write #48, #7F ; VGM: 55 48 7F — TL slot2 ch0
ym2203_write #49, #7F ; VGM: 55 49 7F — TL slot2 ch1
ym2203_write #4A, #7F ; VGM: 55 4A 7F — TL slot2 ch2
ym2203_write #4C, #7F ; VGM: 55 4C 7F — TL slot4 ch0
ym2203_write #4D, #7F ; VGM: 55 4D 7F — TL slot4 ch1
ym2203_write #4E, #7F ; VGM: 55 4E 7F — TL slot4 ch2
; ── Prescaler reset — VGM: 55 2D 00 ──
ym2203_write #2F, #01 ; VGM: 55 2D 00
ym2203_write #2D, #01 ; VGM: A5 2E 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SILENCE_YM2203_2 — Silence YM2203 chip 2 (TurboSound)
;
; Uses VGM opcode 0xA5 (dual chip: 0x55 → 0xA5) → vgm_fill_buffer
; generates CMD_WRITE_AY2 which selects chip 2 via isr_ms_ctrl.
;
; Prescaler reset:
; A5 2D 00 Reset prescaler → defaults: /6 FM, /4 SSG
; SSG (chip 2):
; A5 07 3F Mixer: all tone+noise off
; A5 08 00 SSG Volume A = 0
; A5 09 00 SSG Volume B = 0
; A5 0A 00 SSG Volume C = 0
; FM — Key Off all 3 channels (chip 2):
; A5 28 00 Key Off ch 0
; A5 28 01 Key Off ch 1
; A5 28 02 Key Off ch 2
; FM — TL max (0x7F) all 12 operators (chip 2):
; A5 40..42 7F slot 1 (M1): ch 0,1,2
; A5 44..46 7F slot 3 (C1): ch 0,1,2
; A5 48..4A 7F slot 2 (M2): ch 0,1,2
; A5 4C..4E 7F slot 4 (C2): ch 0,1,2
; Total: 20 VGM commands + end = 61 bytes
; ═══════════════════════════════════════════════════════════════════════
blk_silence_ym2203_2:
; ── SSG silence (chip 2) — VGM: A5 rr vv ──
ym2203_2_write 7, #3F ; VGM: A5 07 3F — Mixer off
ym2203_2_write 8, #00 ; VGM: A5 08 00 — SSG Vol A = 0
ym2203_2_write 9, #00 ; VGM: A5 09 00 — SSG Vol B = 0
ym2203_2_write 10, #00 ; VGM: A5 0A 00 — SSG Vol C = 0
; ── FM Key Off (chip 2) — VGM: A5 28 xx ──
ym2203_2_write #28, #00 ; VGM: A5 28 00 — Key Off ch 0
ym2203_2_write #28, #01 ; VGM: A5 28 01 — Key Off ch 1
ym2203_2_write #28, #02 ; VGM: A5 28 02 — Key Off ch 2
; ── FM TL max (chip 2) — VGM: A5 4x 7F ──
ym2203_2_write #40, #7F ; VGM: A5 40 7F — TL slot1 ch0
ym2203_2_write #41, #7F ; VGM: A5 41 7F — TL slot1 ch1
ym2203_2_write #42, #7F ; VGM: A5 42 7F — TL slot1 ch2
ym2203_2_write #44, #7F ; VGM: A5 44 7F — TL slot3 ch0
ym2203_2_write #45, #7F ; VGM: A5 45 7F — TL slot3 ch1
ym2203_2_write #46, #7F ; VGM: A5 46 7F — TL slot3 ch2
ym2203_2_write #48, #7F ; VGM: A5 48 7F — TL slot2 ch0
ym2203_2_write #49, #7F ; VGM: A5 49 7F — TL slot2 ch1
ym2203_2_write #4A, #7F ; VGM: A5 4A 7F — TL slot2 ch2
ym2203_2_write #4C, #7F ; VGM: A5 4C 7F — TL slot4 ch0
ym2203_2_write #4D, #7F ; VGM: A5 4D 7F — TL slot4 ch1
ym2203_2_write #4E, #7F ; VGM: A5 4E 7F — TL slot4 ch2
; ── Prescaler reset (chip 2) — VGM: A5 2D 00 ──
; ── Prescaler reset — VGM: 55 2D 00 ──
ym2203_2_write #2F, #01 ; VGM: 55 2D 00
ym2203_2_write #2D, #01 ; VGM: A5 2E 00
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA_INIT — SAA1099 chip 1 Sound Enable ON
;
; Writes reg 0x1C = 0x01 to chip 0 (Sound Enable ON).
; Parsed via cmdblk → fb_is_vgm==0 → bypasses saa_mode filter.
;
; VGM stream: BD 1C 01 66
; ═══════════════════════════════════════════════════════════════════════
blk_saa_init:
saa_write #1C, #01 ; VGM: BD 1C 01 — Sound Enable ON (chip 0)
blk_end
; ═══════════════════════════════════════════════════════════════════════
; CMDBLK_SAA2_INIT — SAA1099 chip 2 Sound Enable ON (+ clock enable)
;
; Writes reg 0x1C|0x80 = 0x9C to chip 1 (bit7 selects chip 1).
; In HDL: address write 0x9C → saa_reg_is_1c=1, then data 0x01 with
; d[0]=1 triggers saa2_clk_en (one-way clock enable for chip 2).
; Parsed via cmdblk → fb_is_vgm==0 → bypasses saa_mode filter.
;
; VGM stream: BD 9C 01 66
; ═══════════════════════════════════════════════════════════════════════
blk_saa2_init:
saa2_write #1C, #01 ; VGM: BD 9C 01 — Sound Enable ON (chip 1 + clk)
blk_end
; ═══════════════════════════════════════════════════════════════════════
; Padding to 16 KB
; ═══════════════════════════════════════════════════════════════════════
IF $ < #10000
ds #10000 - $, #00
ENDIF
SAVEBIN "build/cmdblocks.bin", #C000, #4000
+160
View File
@@ -0,0 +1,160 @@
;==============================================================================
; @file crt0.s
; @brief Стартовый код WC-плагина (SDCC Z80, --no-std-crt0)
;
; Этот файл заменяет стандартный crt0 SDCC. Он должен линковаться
; ПЕРВЫМ в цепочке .rel-файлов, чтобы _plugin_entry оказался
; по адресу --code-loc (обычно #8000).
;
; Входные параметры WC (при CALL plugin_base):
; A' = индекс расширения файла
; BC = указатель на имя файла (z-строка)
; HL = размер файла (младшее слово)
; DE = размер файла (старшее слово)
; IX = указатель на структуру активной панели WC
;
; Выход: A = код возврата (0=ESC, 2=следующий, 4=предыдущий)
;
; Адресное пространство:
; #8000-#BFFF = код + данные плагина
; #C000-#FFFF = VPL окно (данные VGM через WC API)
;
; ВАЖНО: gsinit обнуляет _DATA и копирует _INITIALIZER → _INITIALIZED.
; Без этого при повторном запуске (NEXT/PREV) стат. переменные
; содержат мусор от предыдущего запуска.
;==============================================================================
.module crt0
.globl _main
.globl _wc_exit_code
.globl _wc_file_idx
.globl _wc_file_count
; Линкер-символы для runtime-инициализации C
.globl s__DATA
.globl l__DATA
.globl s__INITIALIZED
.globl s__INITIALIZER
.globl l__INITIALIZER
;------------------------------------------------------------------------------
; Entry point (WC вызывает по "CALL plugin_base")
; Вход:
; A' = индекс расширения файла
; BC = указатель на имя файла (z-строка)
; HL = размер файла (младшее слово)
; DE = размер файла (старшее слово)
; IX = указатель на структуру активной панели (нужен WC API)
;------------------------------------------------------------------------------
.area _CODE
_plugin_entry::
;--------------------------------------------------------------
; Сохранить параметры WC на стеке: gsinit обнулит _DATA,
; поэтому нельзя записывать в глобалы до инициализации.
;--------------------------------------------------------------
push hl ; size_lo
push de ; size_hi
push bc ; file_name ptr
ex af, af'
push af ; file_ext (в A)
ex af, af'
;--- Сохранить HL'/DE' (номер объекта / кол-во файлов) ---
exx
push hl ; file_idx (HL')
push de ; file_count (DE')
exx
;--------------------------------------------------------------
; Инициализировать C runtime (обнулить _DATA,
; скопировать начальные значения в _INITIALIZED)
;--------------------------------------------------------------
call gsinit
;--------------------------------------------------------------
; Восстановить параметры WC из стека в (теперь чистые) глобалы
;--------------------------------------------------------------
pop de ; file_count (DE')
ld (_wc_file_count), de
pop hl ; file_idx (HL')
ld (_wc_file_idx), hl
pop af
ld (_wc_file_ext), a
pop bc
ld (_wc_file_name), bc
pop de ; size_hi
pop hl ; size_lo
ld (_wc_file_size+0), hl
ld (_wc_file_size+2), de
; Вызвать main()
call _main
; main() вернула void, код выхода — в _wc_exit_code
ld a, (_wc_exit_code)
; IX не менялся — просто возврат
ret
;------------------------------------------------------------------------------
; Глобальные переменные параметров WC (читаются из main.c)
;------------------------------------------------------------------------------
.area _DATA
_wc_file_ext:: .db 0
_wc_file_name:: .dw 0
_wc_file_size:: .dw 0, 0 ; 32-bit LE
_wc_exit_code:: .db 0
_wc_file_idx:: .dw 0 ; HL': номер объекта в панели (от 1)
_wc_file_count::.dw 0 ; DE': кол-во объектов в панели (вкл. "..")
;------------------------------------------------------------------------------
; Инициализация C runtime (SDCC: _GSINIT выполняется при старте)
;
; 1) Обнулить _DATA — все глобальные/статические переменные без
; явного инициализатора (аналог BSS).
; 2) Скопировать _INITIALIZER → _INITIALIZED — переменные с явным
; начальным значением (static uint8_t x = 5; и т.п.).
;------------------------------------------------------------------------------
.area _GSINIT
gsinit::
; ── 1. Обнулить _DATA ────────────────────────────────────────
ld bc, #l__DATA
ld a, b
or a, c
jr z, 00001$ ; l__DATA == 0 → нечего обнулять
ld hl, #s__DATA
ld (hl), #0 ; первый байт = 0
dec bc
ld a, b
or a, c
jr z, 00001$ ; l__DATA == 1 → уже готово
ld d, h
ld e, l
inc de ; DE = s__DATA + 1
ldir ; заполнить остаток нулями
00001$:
; ── 2. Скопировать _INITIALIZER → _INITIALIZED ──────────────
ld bc, #l__INITIALIZER
ld a, b
or a, c
jr z, 00002$ ; нет инициализированных данных
ld hl, #s__INITIALIZER ; источник (в ROM / code page 1)
ld de, #s__INITIALIZED ; приёмник (в RAM / _DATA area)
ldir
00002$:
.area _GSFINAL
gsfinal::
ret
File diff suppressed because it is too large Load Diff
+330
View File
@@ -0,0 +1,330 @@
;==============================================================================
; inflate_call.s — SDCC обёртка (sdcccall(1)) для вызова inflate из C
;
; uint8_t inflate_vgz(uint8_t src_page, uint8_t dst_page);
; void copy_pages_to_tap(uint8_t n_pages);
;
; sdcccall(1): A = первый аргумент
; Возврат inflate_vgz: A = 0 (успех) / 1 (ошибка)
;
; Inflate binary живёт в экстра-странице WMF. Обёртка маппит её в Win 3
; (ORG 0xC000) и делает JP 0xC000. Decode buffer — в Win 2 (0x8000),
; настраивается самим inflate. При выходе inflate восстанавливает Win 2.
;
; ╔══════════════════════════════════════════════════════════════════════╗
; ║ Почему код inflate обязан быть в Win 3, а не Win 2: ║
; ║ DEFLATE backref вычисляет HL = write_ptr − distance. ║
; ║ При decode buffer в Win 2 (0x8000) → HL ∈ [0x0000..0xBFFF], ║
; ║ никогда не попадает в Win 3 (0xC000+), где живёт код. ║
; ║ Если поменять местами (код Win 2, decode Win 3) → HL может ║
; ║ попасть в 0x8000–0xBFFF, что потребует ремапа Win 2 во время ║
; ║ исполнения из неё — невозможно. ║
; ╚══════════════════════════════════════════════════════════════════════╝
;
; copy_pages_to_tap:
; Копирует N страниц из мегабуфера (#20+) в TAP-страницы (#A1+).
; DI на время копирования, восстанавливает Win 0/Win 3 из sys vars.
;
; inflate_vgz:
; 1. Сохранить аргументы, IX, SP, Win 1
; 2. DI, маппить Win 3 = inflate page через порт
; 3. SP = 0xFFF0, push return addr → JP 0xC000
; 4. По возврату: SP, Win 1 восстанавливаем, EI, IX → ret
; Win 0 / Win 3 — восстанавливает вызывающий C-код через WC API
;==============================================================================
.module inflate_call
.globl _inflate_vgz
.globl _copy_pages_to_tap
; Progress bar params (filled by C before inflate_vgz)
.globl _ifl_pb_text_pg
.globl _ifl_pb_attr_ofs
.globl _ifl_pb_total
.globl _ifl_pb_bw
.globl _ifl_pb_green
;--- Константы ---------------------------------------------------------------
INFLATE_PAGE_IDX = 4 ; индекс inflate в плагине (1 code + 3 LUT)
; физ. страница = base_page + INFLATE_PAGE_IDX
; base_page читается из WC sys var (0x6002)
PORT_W0 = 0x10AF ; TSConfig Win 0 page port
PORT_W1 = 0x11AF ; TSConfig Win 1 page port
PORT_W3 = 0x13AF ; TSConfig Win 3 page port
;--- DATA (0xB800+) ----------------------------------------------------------
.area _DATA
_ifl_src_pg: .ds 1 ; src_page (физ. стр. TAP)
_ifl_dst_pg: .ds 1 ; dst_page (физ. стр. мегабуфера)
_ifl_sv_pg1: .ds 1 ; saved Win 1 physical page
_ifl_sv_pg2: .ds 1 ; saved Win 2 physical page → inflate D reg
_ifl_sv_sp: .ds 2 ; saved SP
_ifl_inflate_pg: .ds 1 ; computed inflate phys page (debug/scan)
; Progress bar parameters (set by C, copied to inflate page at 0xC1F0)
_ifl_pb_text_pg: .ds 1 ; text screen physical page
_ifl_pb_attr_ofs: .ds 2 ; attr base offset within text page (lo, hi)
_ifl_pb_total: .ds 1 ; estimated total output pages (0=disabled)
_ifl_pb_bw: .ds 1 ; bar width in columns
_ifl_pb_green: .ds 1 ; green attr value
;--- CODE (0x8000+) ----------------------------------------------------------
.area _CODE
_inflate_vgz::
push ix ; callee-saved (SDCC)
;--- 1. сохранить аргументы ---
ld (_ifl_src_pg), a ; A = src_page
ld a, l
ld (_ifl_dst_pg), a ; L = dst_page
;--- 2. DI ---
di
;--- 3. сохранить Win 1, Win 2 из WC sys vars (@ 0x6001/0x6002) ---
ld a, (0x6001)
ld (_ifl_sv_pg1), a
ld a, (0x6002)
ld (_ifl_sv_pg2), a
;--- 4. сохранить SP ---
ld (_ifl_sv_sp), sp
.ifdef DBG_ENABLE
;--- DBG: 0x01 = saved context, about to map inflate page ---
ld bc, #0xFFAF
ld a, #0x01
out (c), a
.endif
;--- 5. маппить Win 3 = inflate page (dynamic: base + IDX) ---
ld a, (0x6002) ; Win 2 phys page = plugin base
.ifdef DBG_ENABLE
ld bc, #0xFEAF
out (c), a ; DBG: FEAF = base page from 0x6002
.endif
add a, #INFLATE_PAGE_IDX ; + 5 = inflate page
ld (_ifl_inflate_pg), a ; save computed inflate page
.ifdef DBG_ENABLE
ld bc, #0xFDAF
out (c), a ; DBG: FDAF = computed inflate page
.endif
ld bc, #PORT_W3
out (c), a
.ifdef DBG_ENABLE
;--- DBG: 0x02 = Win3 mapped to inflate page ---
ld bc, #0xFFAF
ld a, #0x02
out (c), a
ld a, (0xC000)
ld bc, #0xFEAF
out (c), a ; byte[0] — expect 0xF5 (push af)
ld a, (0xC001)
ld bc, #0xFDAF
out (c), a ; byte[1] — expect 0xC5 (push bc)
;=== DBG: scan pages base+3..base+7 looking for inflate ===
ld a, (0x6002)
add a, #3
ld d, a
ld e, #0
_ifl_scan_loop:
ld a, d
ld bc, #PORT_W3
out (c), a
ld a, #0xD0
add a, e
ld bc, #0xFFAF
out (c), a
ld a, d
ld bc, #0xFDAF
out (c), a
ld a, (0xC000)
ld bc, #0xFEAF
out (c), a
inc d
inc e
ld a, e
cp #5
jr nz, _ifl_scan_loop
;--- DBG: 0x04 = scan done, re-map Win3 to inflate page ---
ld bc, #0xFFAF
ld a, #0x04
out (c), a
ld a, (_ifl_inflate_pg)
ld bc, #PORT_W3
out (c), a
.endif
;--- 6. стек в Win 3, push return addr ---
ld sp, #0xFFF0
ld hl, #_ifl_return
push hl
.ifdef DBG_ENABLE
;--- DBG: 0x05 = SP set, return addr pushed, loading regs ---
ld bc, #0xFFAF
ld a, #0x05
out (c), a
ld a, (_ifl_src_pg)
ld bc, #0xFEAF
out (c), a
ld a, (_ifl_dst_pg)
ld bc, #0xFDAF
out (c), a
.endif
;--- 6b. Copy progress bar params to inflate page (0xC1F0+) ---
; Win 3 = inflate page (mapped in step 5)
; Win 2 = _CODE/_DATA (still accessible for reading _ifl_pb_*)
ld a, (_ifl_pb_text_pg)
ld (0xC1F0), a
ld hl, (_ifl_pb_attr_ofs)
ld (0xC1F1), hl
ld a, (_ifl_pb_total)
ld (0xC1F3), a
ld a, (_ifl_pb_bw)
ld (0xC1F4), a
ld a, (_ifl_pb_green)
ld (0xC1F5), a
; Initialize runtime state
xor a
ld (0xC1F6), a ; pb_error = 0
ld (0xC1F7), a ; pb_col = 0
;--- 7. входные регистры inflate: A=src, E=dst, D=saved_pg2 ---
ld a, (_ifl_sv_pg2)
ld d, a ; D = saved Win 2 page
ld a, (_ifl_dst_pg)
ld e, a ; E = dst_page
ld a, (_ifl_src_pg) ; A = src_page
.ifdef DBG_ENABLE
;--- DBG: 0x06 = about to JP 0xC000 ---
push af
push bc
ld bc, #0xFFAF
ld a, #0x06
out (c), a
pop bc
pop af
.endif
;--- 8. запуск inflate ---
; JP 0xC200 = inflate_init (safe zone, survives backref LDIR)
; НЕ JP 0xC000! Entry zone 0xC000–0xC1FF перезаписывается
; LDIR overflow при распаковке backref'ов.
jp 0xC200
;--- сюда вернёмся по RET из inflate ---
; A = 0 (ok) / 1 (error)
; Win 2 уже восстановлен inflate → _DATA и код доступны
_ifl_return:
ld e, a ; результат → E
.ifdef DBG_ENABLE
;--- DBG: 0x03 = returned from inflate, A=result ---
ld bc, #0xFFAF
ld a, #0x03
out (c), a
ld a, e
ld bc, #0xFEAF
out (c), a
.endif
;--- восстановить SP (→ C-стек, сразу после push ix) ---
ld sp, (_ifl_sv_sp)
;--- восстановить Win 1 (WC API точка входа @ 0x6006) ---
ld a, (_ifl_sv_pg1)
ld bc, #PORT_W1
out (c), a
;--- вернуть результат ---
ld a, e
pop ix ; восстановить IX
ei ; EI + задержка 1 instr → ret атомарен
ret
;==============================================================================
; void copy_pages_to_tap(uint8_t n_pages)
;
; Копирует n_pages страниц из мегабуфера (#20..#20+n-1) в TAP (#A1..#A1+n-1).
; Использует прямые port-записи в Win 0 / Win 3.
; DI на время работы; при выходе: Win 0, Win 3 восстановлены, EI.
;
; sdcccall(1): A = n_pages
;==============================================================================
MEGABUF_START = 0x20 ; первая физ. страница мегабуфера
TAP_START = 0xA1 ; первая физ. страница TAP
_copy_pages_to_tap::
or a
ret z ; n=0 → ничего делать
push ix
ld c, a ; C = n_pages (сохраним)
di
;--- сохранить текущие страницы Win 0 / Win 3 ---
ld a, (0x6000)
ld (_cpt_sv_pg0), a
ld a, (0x6003)
ld (_cpt_sv_pg3), a
;--- подготовить счётчики ---
ld d, #MEGABUF_START ; D = src phys page
ld e, #TAP_START ; E = dst phys page
ld b, c ; B = loop counter
_cpt_loop:
push bc
push de
;--- Map Win 0 = megabuf page D ---
ld a, d
ld bc, #PORT_W0
out (c), a
;--- Map Win 3 = TAP page E ---
ld a, e
ld b, #0x13 ; C ещё 0xAF от предыдущего BC
out (c), a
;--- LDIR 16 КБ: Win 0 → Win 3 ---
ld hl, #0x0000
ld de, #0xC000
ld bc, #0x4000
ldir
pop de
inc d ; следующая megabuf page
inc e ; следующая TAP page
pop bc
djnz _cpt_loop
;--- восстановить Win 0 ---
ld a, (_cpt_sv_pg0)
ld bc, #PORT_W0
out (c), a
;--- восстановить Win 3 ---
ld a, (_cpt_sv_pg3)
ld b, #0x13
out (c), a
ei
pop ix
ret
;--- static vars for copy (в CODE — не в DATA, чтобы не зависеть от Win 2) ---
_cpt_sv_pg0: .ds 1
_cpt_sv_pg3: .ds 1
+663
View File
@@ -0,0 +1,663 @@
;==============================================================================
; @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 _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_CALL_WC = 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_CALL_WC
jp z, _isr_call_wc
; 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:
inc de
inc de
inc de ; пропустить 3 байта параметров
; Выставить флаг готовности к завершению
ld a, #1
ld (_isr_done), a
; Перемотать read_ptr на начало CMD_ISR_DONE (DE - 4)
ex de, hl
dec hl
dec hl
dec hl
dec hl
ld (_isr_read_ptr), hl
jp _isr_exit
;--- CMD_CALL_WC: вызов оригинального WC handler [0,0,0] ------------------
; Внутренняя команда ISR (не используется buffer filler, зарезервировано)
_isr_call_wc:
inc de
inc de
inc de ; пропустить 3 байта параметров
jp _isr_cmd_loop ; просто пропустить
;==============================================================================
; 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, готов к завершению
_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
+335
View File
@@ -0,0 +1,335 @@
;==============================================================================
; @file opl3.s
; @brief OPL3/AY/SAA register operations for SDCC Z80 (sdcccall(1))
;
; Порты TSConfig для OPL3:
; #C4 = Bank 0 Address #C5 = Bank 0 Data
; #C6 = Bank 1 Address #C7 = Bank 1 Data
;==============================================================================
.module opl3
.area _CODE
;----------------------------------------------------------------------
; opl3_write_b0(uint8_t addr, uint8_t data) — Bank 0
;----------------------------------------------------------------------
.globl _opl3_write_b0
_opl3_write_b0::
ld c, #0xC4
jr _opl3_write
;----------------------------------------------------------------------
; opl3_write_b1(uint8_t addr, uint8_t data) — Bank 1
;----------------------------------------------------------------------
.globl _opl3_write_b1
_opl3_write_b1::
ld c, #0xC6
_opl3_write:
out (c), a ; addr (B don't-care for C4-C7)
ld hl, #2
add hl, sp
ld a, (hl)
inc c
out (c), a ; data
ret
;----------------------------------------------------------------------
; tsconf_out(uint16_t port, uint8_t val)
;----------------------------------------------------------------------
.globl _tsconf_out
_tsconf_out::
ld b, d
ld c, e
ld hl, #2
add hl, sp
ld a, (hl)
out (c), a
ret
;----------------------------------------------------------------------
; opl3_init() — OPL3 enable + wavesel + reset (falls through)
;----------------------------------------------------------------------
.globl _opl3_init
_opl3_init::
ld a, #0x05
ld c, #0xC6
out (c), a ; B don't-care for C4-C7
ld a, #0x01 ; NEW bit only (OPL3 enable)
inc c
out (c), a
ld a, #0x01
ld c, #0xC4
out (c), a
ld a, #0x20
inc c
out (c), a
; fall through to opl3_reset
;----------------------------------------------------------------------
; opl3_reset() — Full OPL3 silence: zero ALL significant registers
; Both banks: Key-Off (B0-B8=0), Total Level max atten (40-55=3F),
; Percussion off (BD=0), F-Num (A0-A8=0), Feedback (C0-C8=0),
; AM/Vib/Mult (20-35=0), AR/DR (60-75=0), SL/RR (80-95=0),
; Waveform (E0-F5=0).
; Uses _opl3_zero_range and _opl3_fill_range helpers.
;----------------------------------------------------------------------
.globl _opl3_reset
_opl3_reset::
; ── Bank 0 ──
ld c, #0xC4
call _opl3_reset_bank
; Percussion off: BD = 0 (Bank 0 only)
ld a, #0xBD
out (c), a
inc c
xor a, a
out (c), a
dec c
; ── Bank 1 ──
ld c, #0xC6
; fall through to _opl3_reset_bank
;----------------------------------------------------------------------
; _opl3_reset_bank — zero all significant regs of one OPL3 bank
; C = addr port (0xC4 or 0xC6)
; Destroys: A, B, D, E
;----------------------------------------------------------------------
_opl3_reset_bank:
; KeyOff: B0-B8 = 0
ld d, #0xB0
ld e, #9
call _opl3_zero_range
; Total Level max attenuation: 40-55 = 0x3F
ld d, #0x40
ld e, #22
ld a, #0x3F
call _opl3_fill_range
; F-Num lo: A0-A8 = 0
ld d, #0xA0
ld e, #9
call _opl3_zero_range
; Feedback/Output: C0-C8 = 0
ld d, #0xC0
ld e, #9
call _opl3_zero_range
; AM/Vib/Mult: 20-35 = 0
ld d, #0x20
ld e, #22
call _opl3_zero_range
; AR/DR: 60-75 = 0
ld d, #0x60
ld e, #22
call _opl3_zero_range
; SL/RR: 80-95 = 0
ld d, #0x80
ld e, #22
call _opl3_zero_range
; Waveform: E0-F5 = 0
ld d, #0xE0
ld e, #22
jr _opl3_zero_range ; tail-call → ret
;----------------------------------------------------------------------
; _opl3_zero_range — fill E consecutive regs starting at D with 0
; C = port, D = start reg, E = count
;----------------------------------------------------------------------
_opl3_zero_range:
xor a, a
; fall through
;----------------------------------------------------------------------
; _opl3_fill_range — fill E consecutive regs starting at D with value A
; C = port (0xC4 for Bank0, 0xC6 for Bank1)
; D = start register, E = count, A = value to write
; Destroys: A, B, D, E
;----------------------------------------------------------------------
_opl3_fill_range:
ld b, a ; B = value to write
_opl3_fr_lp:
ld a, d
out (c), a ; write register address
inc c
ld a, b
out (c), a ; write value
dec c
inc d
dec e
jr nz, _opl3_fr_lp
ret
;----------------------------------------------------------------------
; Общая подпрограмма: silence текущего AY чипа. C уже = 0xFD.
; Reg 7 = 0x3F (mixer off), Regs 8-10 = 0 (volume off)
;----------------------------------------------------------------------
_ay_silence_core:
ld b, #0xFF
ld a, #7
out (c), a
ld b, #0xBF
ld a, #0x3F
out (c), a
ld d, #8
_ayc_lp:
ld b, #0xFF
ld a, d
out (c), a
ld b, #0xBF
xor a, a
out (c), a
inc d
ld a, d
cp a, #11
jr c, _ayc_lp
ret
;----------------------------------------------------------------------
; ay_write_reg(uint8_t reg, uint8_t val) — write AY chip 1 register
; reg in A, val on stack
;----------------------------------------------------------------------
.globl _ay_write_reg
_ay_write_reg::
ld c, #0xFD
ld b, #0xFF
out (c), a ; select register
ld hl, #2
add hl, sp
ld a, (hl)
ld b, #0xBF
out (c), a ; write value
ret
;----------------------------------------------------------------------
; ay2_write_reg(uint8_t reg, uint8_t val) — write AY chip 2 register
;----------------------------------------------------------------------
.globl _ay2_write_reg
_ay2_write_reg::
ld c, #0xFD
ld b, #0xFF
push af ; save reg
ld a, #0xFE
out (c), a ; select chip 2
pop af
ld b, #0xFF
out (c), a ; select register
ld hl, #2
add hl, sp
ld a, (hl)
ld b, #0xBF
out (c), a ; write value
ld b, #0xFF
ld a, #0xFF
out (c), a ; switch back to chip 1
ret
;----------------------------------------------------------------------
; saa_write_reg(uint8_t reg, uint8_t val) — SAA1099 chip 1
; addr=#00FF, data=#01FF
;----------------------------------------------------------------------
.globl _saa_write_reg
_saa_write_reg::
ld c, #0xFF
ld b, #0x00
out (c), a ; addr = reg
ld hl, #2
add hl, sp
ld a, (hl)
inc b ; B = 0x01
out (c), a ; data = val
ret
;----------------------------------------------------------------------
; saa2_write_reg(uint8_t reg, uint8_t val) — SAA1099 chip 2
; addr=#02FF, data=#03FF
;----------------------------------------------------------------------
.globl _saa2_write_reg
_saa2_write_reg::
ld c, #0xFF
ld b, #0x02
out (c), a ; addr = reg
ld hl, #2
add hl, sp
ld a, (hl)
inc b ; B = 0x03
out (c), a ; data = val
ret
;----------------------------------------------------------------------
; ay_silence() — AY chip 1 (FFFD/BFFD)
;----------------------------------------------------------------------
.globl _ay_silence
_ay_silence::
ld c, #0xFD
jr _ay_silence_core
;----------------------------------------------------------------------
; ay2_silence() — TurboSound chip 2: select → silence → switch back
;----------------------------------------------------------------------
.globl _ay2_silence
_ay2_silence::
ld c, #0xFD
ld b, #0xFF
ld a, #0xFE
out (c), a ; select chip 2
call _ay_silence_core
ld b, #0xFF
ld a, #0xFF
out (c), a ; switch back to chip 1
ret
;----------------------------------------------------------------------
; Общая подпрограмма: silence SAA чипа. C=0xFF, B=addr port hi.
; Data port = B+1 (inc b / dec b).
; Reset (0x1C=02), disable (0x1C=00), amplitude regs 0x00-0x05=0.
;----------------------------------------------------------------------
_saa_silence_core:
ld a, #0x1C
out (c), a ; addr
inc b
ld a, #0x02
out (c), a ; data: reset
dec b
ld a, #0x1C
out (c), a ; addr
inc b
xor a, a
out (c), a ; data: sound off
dec b
ld d, #0
_saac_lp:
ld a, d
out (c), a ; addr: reg N
inc b
xor a, a
out (c), a ; data: 0
dec b
inc d
ld a, d
cp a, #6
jr c, _saac_lp
ret
;----------------------------------------------------------------------
; saa_silence() — SAA1099 chip 1 (addr=#00FF, data=#01FF)
;----------------------------------------------------------------------
.globl _saa_silence
_saa_silence::
ld c, #0xFF
ld b, #0x00
jr _saa_silence_core
;----------------------------------------------------------------------
; saa2_silence() — SAA1099 chip 2 (addr=#02FF, data=#03FF)
;----------------------------------------------------------------------
.globl _saa2_silence
_saa2_silence::
ld c, #0xFF
ld b, #0x02
jr _saa_silence_core
+58
View File
@@ -0,0 +1,58 @@
;============================================================================
; pos_table.s — Таблица позиций INT (СГЕНЕРИРОВАНО, не редактировать)
;
; entries=14, step=5120, line_len=224
; ISR freq: 3,500,000 / 5120 = 684 Hz (~44100/16)
;============================================================================
.module pos_table_mod
.globl pos_table
.area _CODE
pos_table:
_pt_0:
.db 0x00, 0x00, 0x00 ; [ 0] line= 0, hpos= 0
.dw _pt_1
_pt_1:
.db 0x00, 0x16, 0xC0 ; [ 1] line= 22, hpos=192
.dw _pt_2
_pt_2:
.db 0x00, 0x2D, 0xA0 ; [ 2] line= 45, hpos=160
.dw _pt_3
_pt_3:
.db 0x00, 0x44, 0x80 ; [ 3] line= 68, hpos=128
.dw _pt_4
_pt_4:
.db 0x00, 0x5B, 0x60 ; [ 4] line= 91, hpos= 96
.dw _pt_5
_pt_5:
.db 0x00, 0x72, 0x40 ; [ 5] line=114, hpos= 64
.dw _pt_6
_pt_6:
.db 0x00, 0x89, 0x20 ; [ 6] line=137, hpos= 32
.dw _pt_7
_pt_7:
.db 0x00, 0xA0, 0x00 ; [ 7] line=160, hpos= 0
.dw _pt_8
_pt_8:
.db 0x00, 0xB6, 0xC0 ; [ 8] line=182, hpos=192
.dw _pt_9
_pt_9:
.db 0x00, 0xCD, 0xA0 ; [ 9] line=205, hpos=160
.dw _pt_10
_pt_10:
.db 0x00, 0xE4, 0x80 ; [10] line=228, hpos=128
.dw _pt_11
_pt_11:
.db 0x00, 0xFB, 0x60 ; [11] line=251, hpos= 96
.dw _pt_12
_pt_12:
.db 0x01, 0x12, 0x40 ; [12] line=274, hpos= 64
.dw _pt_13
_pt_13:
.db 0x01, 0x29, 0x20 ; [13] line=297, hpos= 32
.dw pos_table
.globl pos_table_end
pos_table_end:
+953
View File
@@ -0,0 +1,953 @@
;==============================================================================
; spectrum.s — Spectrum analyzer: render + hot-loop helpers (Z80 ASM)
;
; Hand-optimised replacements for C functions in main.c / vgm.c:
; spectrum_render() — 4-row bar graph, direct VRAM write
; spectrum_opl_b0(r,v) — OPL B0-B8 key-on + 0xBD percussion
; spectrum_opl_b1(r,v) — OPL Bank1 B0-B6 key-on
; spectrum_ay(r,v) — AY period shadow + vol trigger + YM2203 FM (chip1)
; spectrum_ay2(r,v) — same for chip2 (base offset = 3)
; spectrum_saa(r,v) — SAA octave shadow + amp trigger
;
; sdcccall(1) conventions:
; 1st u8 → A, 2nd u8 → L, 3rd u8 → stack+2
; 1st u16 → HL, return u8 → A, return u16 → HL
;
; Port 0x13AF = Page3 window (TSConfig)
;==============================================================================
.module spectrum
; ── Exports ──────────────────────────────────────────────────────────
.globl _spectrum_render
.globl _spectrum_decay
.globl _spectrum_decay_reset
.globl _spectrum_opl_b0
.globl _spectrum_opl_b1
.globl _spectrum_ay
.globl _spectrum_ay2
.globl _spectrum_saa
.globl _spectrum_saa2
.globl _spectrum_wave
.globl _spec_period_to_bar
.globl _spectrum_font_init
.globl _spectrum_font_restore
; ── Imports ──────────────────────────────────────────────────────────
.globl _spectrum_levels ; uint8_t[16] in isr.s _DATA
.globl _isr_tick_ctr ; volatile uint8_t in isr.s _DATA
.globl _s_pb_text_pg ; uint8_t in main.c _DATA
.globl _s_spec_char_ofs ; uint16_t[4] in main.c _DATA
.globl _spec_mask ; uint16_t in vgm.c _DATA
.globl _spec_ay_period ; uint16_t[6] in vgm.c _DATA
.globl _spec_saa_oct ; uint8_t[6] in vgm.c _DATA
.globl _spec_saa_oct2 ; uint8_t[6] in vgm.c _DATA (chip 2)
.globl _spec_saa_dual ; uint8_t in vgm.c _DATA
.globl _spec_opl_bd ; uint8_t in vgm.c _DATA
.globl _spec_fm_block ; uint8_t[6] in vgm.c _DATA
.globl _spec_wave_active ; uint8_t[3]: 24-bit KeyOn mask
; ── Constants ────────────────────────────────────────────────────────
PORT_PAGE3 = 0x13AF
SPEC_BARS = 16
SPEC_LEFT_PAD = 5
SPEC_BAR_W = 3 ; 2 filled + 1 space
CHAR_FULL = 0x02 ; custom glyph: full-height striped bar
CHAR_HALF = 0x01 ; custom glyph: half-height striped bar
CLR_WIN = 0x70 ; WC_COLOR(WC_WHITE, WC_BLACK)
WC_PAGE_FONT0 = 0x01
FONT_BASE = 0xC000
; ── Timing: must match variant_cfg.h ────────────────────────────────
ISR_TICKS_PER_FRAME = 14 ; = ISR_TICKS_PER_FRAME in variant_cfg.h
; ── Decay: must match DECAY_FRAMES_* in variant_cfg.h ────────────────
DECAY_FAST = 1 ; = DECAY_FRAMES_FAST
DECAY_MED = 2 ; = DECAY_FRAMES_MED
DECAY_SLOW = 4 ; = DECAY_FRAMES_SLOW
;======================================================================
; Scratch variables in _DATA (kept minimal)
;======================================================================
.area _DATA
say_base: .db 0 ; temp: chip base index (0 or 3)
sr_row_ctr: .db 0 ; temp: row iteration counter
sr_rp_ptr: .dw 0 ; temp: current row-params pointer
sd_last_tick: .db 0 ; isr_tick_ctr snapshot for frame gate
sd_fast_ctr: .db DECAY_FAST ; countdown: decay levels >= 6
sd_med_ctr: .db DECAY_MED ; countdown: decay levels 3-5
sd_slow_ctr: .db DECAY_SLOW ; countdown: decay levels 1-2
sr_prev_hash: .db 0 ; XOR-rotate hash of spectrum_levels from previous render
.area _CODE
; Row parameter table: [thresh_full, thresh_half, color] × 4 rows
; Row 0 (top=peak): thresholds 8,7 color=BRIGHT_RED
; Row 1: thresholds 6,5 color=BRIGHT_YELLOW
; Row 2: thresholds 4,3 color=BRIGHT_GREEN
; Row 3 (bottom): thresholds 2,1 color=GREEN
sr_row_params:
.db 8, 7, 0x7A ; row 0: white bg + bright red
.db 6, 5, 0x7E ; row 1: white bg + bright yellow
.db 4, 3, 0x7C ; row 2: white bg + bright green
.db 2, 1, 0x74 ; row 3: white bg + green
;======================================================================
; spectrum_render(void)
;
; Optimised Z80 register layout — zero push/pop in inner bar loop:
; IX = char_ptr (stride +3 per bar via: inc ix x3)
; IY = attr_ptr (stride +3 per bar via: inc iy x3)
; HL = spectrum_levels + bar_index
; B = bar counter (DJNZ)
; C = thresh_full (row-constant, set ONCE before bar loop)
; D = thresh_half (row-constant, set ONCE before bar loop)
; E = color (row-constant, set ONCE before bar loop)
;
; Old inner loop loaded 0(ix)/1(ix)/2(ix) on every bar = 3x19=57 T
; per bar x 16 bars x 4 rows = 3648 wasted T-states per frame.
; New inner loop: C/D/E loaded once per row, never reloaded per bar.
; Total savings: ~3500 T-states per spectrum_render call.
;======================================================================
_spectrum_render::
; XOR-rotate hash over spectrum_levels[16] (~100T).
; Detects ANY change versus previous frame, not just zero/non-zero.
; Hash = seed, then for each byte: RLCA + XOR (hl).
ld hl, #_spectrum_levels
xor a
.rept 16
rlca
xor (hl)
inc hl
.endm
; A = rolling hash of current levels
ld hl, #sr_prev_hash
cp (hl) ; same as last frame?
ret z ; unchanged → skip render (~7000T saved)
ld (hl), a ; store new hash
push ix
push iy
ld bc, #PORT_PAGE3
in a, (c)
push af
push bc
ld a, (_s_pb_text_pg)
out (c), a
ld a, #4
ld (sr_row_ctr), a
ld hl, #sr_row_params
ld (sr_rp_ptr), hl
sr_row_loop:
;--------------------------------------------------------------
; row_index = 4 - row_ctr (0..3)
; Compute IX = char_ptr, IY = attr_ptr for this row.
;--------------------------------------------------------------
ld a, (sr_row_ctr)
ld e, a
ld a, #4
sub e
add a, a ; A = row_index * 2
ld e, a
ld d, #0
; IX = 0xC000 + s_spec_char_ofs[row_index] + SPEC_LEFT_PAD
ld hl, #_s_spec_char_ofs
add hl, de
ld a, (hl)
inc hl
ld h, (hl)
ld l, a ; HL = s_spec_char_ofs[row_index]
ld bc, #(0xC000 + SPEC_LEFT_PAD)
add hl, bc
push hl
pop ix ; IX = char_ptr
; IY = IX + 0x0080 (attr_ofs = char_ofs | 0x0080 in TSConf)
push ix
pop hl
ld de, #0x0080
add hl, de
push hl
pop iy ; IY = attr_ptr
;--------------------------------------------------------------
; Load row params ONCE into C/D/E: all three are row-constants.
; C, D, E survive the bar loop untouched (djnz only changes B).
;--------------------------------------------------------------
ld hl, (sr_rp_ptr)
ld c, (hl) ; C = thresh_full
inc hl
ld d, (hl) ; D = thresh_half
inc hl
ld e, (hl) ; E = color
inc hl
ld (sr_rp_ptr), hl ; advance for next row
;--------------------------------------------------------------
; Bar loop: 16 bars — all comparisons use stable C,D,E regs.
; No push/pop needed: all state lives in IX, IY, HL, B, C, D, E.
;--------------------------------------------------------------
ld hl, #_spectrum_levels
ld b, #SPEC_BARS
sr_bar_loop:
ld a, (hl)
cp c ; NC if level >= thresh_full
jr nc, sr_full
cp d ; NC if level >= thresh_half
jr nc, sr_half
sr_empty:
ld a, #' '
ld 0(ix), a
ld 1(ix), a
ld a, #CLR_WIN
ld 0(iy), a
ld 1(iy), a
jr sr_next
sr_half:
ld a, #CHAR_HALF
ld 0(ix), a
ld 1(ix), a
ld 0(iy), e ; E = row color (stable per row)
ld 1(iy), e
jr sr_next
sr_full:
ld a, #CHAR_FULL
ld 0(ix), a
ld 1(ix), a
ld 0(iy), e ; E = row color (stable per row)
ld 1(iy), e
sr_next:
inc hl ; next spectrum level
inc ix ; char_ptr += SPEC_BAR_W (3)
inc ix
inc ix
inc iy ; attr_ptr += SPEC_BAR_W (3)
inc iy
inc iy
djnz sr_bar_loop
ld a, (sr_row_ctr)
dec a
ld (sr_row_ctr), a
jp nz, sr_row_loop
pop bc
pop af
out (c), a
pop iy
pop ix
ret
;======================================================================
; spectrum_decay_reset() — initialize decay timers at song start
;======================================================================
_spectrum_decay_reset::
ld a, (_isr_tick_ctr)
ld (sd_last_tick), a
ld a, #DECAY_FAST
ld (sd_fast_ctr), a
ld a, #DECAY_MED
ld (sd_med_ctr), a
ld a, #DECAY_SLOW
ld (sd_slow_ctr), a
; Reset render hash so first frame always renders
ld a, #0xFF
ld (sr_prev_hash), a
ret
;======================================================================
; spectrum_decay() — frame-gated non-linear level decay
;
; Called every main-loop iteration; returns immediately if less than
; ISR_TICKS_PER_FRAME ticks have elapsed since the last frame.
;
; Three countdown timers control decay speed per level range:
; Levels >= 6 : -1 every 3 frames (~60 ms)
; Levels 3-5 : -1 every 6 frames (~180 ms)
; Levels 1-2 : -1 every 10 frames (~200 ms)
;
; Register layout:
; C = decay-flags bitmask (bit2=fast, bit1=med, bit0=slow)
; B = bar loop counter
; HL = pointer into spectrum_levels[]
;======================================================================
_spectrum_decay::
; ── Frame gate ──────────────────────────────────────────────
ld a, (_isr_tick_ctr)
ld hl, #sd_last_tick
sub (hl) ; elapsed = now - last
cp #ISR_TICKS_PER_FRAME
ret c ; < 1 frame, skip
; Advance by one frame
ld a, (hl)
add a, #ISR_TICKS_PER_FRAME
ld (hl), a
; ── Build decay-flags in C ──────────────────────────────────
ld c, #0
ld hl, #sd_fast_ctr ; 5-frame timer (levels >= 6)
dec (hl)
jr nz, sd_nf
ld (hl), #DECAY_FAST
set 2, c
sd_nf:
ld hl, #sd_med_ctr ; med-frame timer (levels 3-5)
dec (hl)
jr nz, sd_nm
ld (hl), #DECAY_MED
set 1, c
sd_nm:
ld hl, #sd_slow_ctr ; slow-frame timer (levels 1-2)
dec (hl)
jr nz, sd_ns
ld (hl), #DECAY_SLOW
set 0, c
sd_ns:
ld a, c
or a
ret z ; no tier expired → nothing to decay
; ── Single loop over 16 bars ────────────────────────────────
ld hl, #_spectrum_levels
; Quick all-zero check: if all bars are 0, skip decay loop
ld a, (hl)
inc hl
.rept 15
or (hl)
inc hl
.endm
ret z ; all bars zero → nothing to decay
ld hl, #_spectrum_levels
ld b, #SPEC_BARS
sd_bar_loop:
ld a, (hl)
or a
jr z, sd_next ; level 0 → skip
cp #6
jr nc, sd_high ; level >= 6 → fast tier
cp #3
jr nc, sd_mid ; level >= 3 → medium tier
; level 1-2: slow tier
bit 0, c
jr z, sd_next
dec (hl)
jr sd_next
sd_mid:
bit 1, c
jr z, sd_next
dec (hl)
jr sd_next
sd_high:
bit 2, c
jr z, sd_next
dec (hl)
sd_next:
inc hl
djnz sd_bar_loop
ret
;======================================================================
; spectrum_opl_b0(reg, val) — A=reg, L=val
;
; OPL Bank0: B0-B8 key-on + 0xBD percussion (rising edge).
; Sets bits in spec_mask.
;======================================================================
_spectrum_opl_b0::
; Save reg and val
ld d, a ; D = reg
ld e, l ; E = val
; ── Check B0-B8 key-on ──
cp a, #0xB0
jr c, sob0_check_bd ; reg < 0xB0
cp a, #0xB9
jr nc, sob0_check_bd ; reg > 0xB8
bit 5, e ; KeyOn bit in val
jr z, sob0_check_bd ; not keyed on
; bar = (val >> 1) & 0x0F
ld a, e
srl a
and a, #0x0F
; spec_mask |= (1 << bar)
call _set_spec_bit
; Fall through to check BD
sob0_check_bd:
ld a, d
cp a, #0xBD
ret nz ; not reg 0xBD, done
; ── 0xBD percussion handling ──
bit 5, e ; rhythm mode ON?
jr z, sob0_save_bd ; no → just save shadow
; rising = val & ~old & 0x1F
ld a, (_spec_opl_bd)
cpl ; ~old
and a, e ; val & ~old
and a, #0x1F ; mask lower 5 bits
jr z, sob0_save_bd ; no rising edges
; Check each percussion bit
ld b, a ; B = rising edges mask
bit 4, b ; BD?
jr z, sob0_no_bd
ld a, #2 ; BD → bar 2
call _set_spec_bit
sob0_no_bd:
bit 3, b ; SD?
jr z, sob0_no_sd
ld a, #8 ; SD → bar 8
call _set_spec_bit
sob0_no_sd:
bit 2, b ; TOM?
jr z, sob0_no_tom
ld a, #5 ; TOM → bar 5
call _set_spec_bit
sob0_no_tom:
bit 1, b ; CY?
jr z, sob0_no_cy
ld a, #12 ; CY → bar 12
call _set_spec_bit
sob0_no_cy:
bit 0, b ; HH?
jr z, sob0_save_bd
ld a, #14 ; HH → bar 14
call _set_spec_bit
sob0_save_bd:
ld a, e
ld (_spec_opl_bd), a
ret
;======================================================================
; spectrum_opl_b1(reg, val) — A=reg, L=val
;
; OPL Bank1: B0-B6 key-on only. Same logic, smaller range.
;======================================================================
_spectrum_opl_b1::
cp a, #0xB0
ret c ; reg < 0xB0
cp a, #0xB7
ret nc ; reg > 0xB6
bit 5, l ; KeyOn bit
ret z
; bar = (val >> 1) & 0x0F
ld a, l
srl a
and a, #0x0F
jp _set_spec_bit ; tail-call
;======================================================================
; spectrum_wave(reg, val) — A=reg, L=val
;
; YMF278B wave activity tracker. Key registers 0x68-0x7F maintain a
; 24-bit KeyOn mask. asm_emit_skip_spec folds channels 16-23 onto bars
; 0-7 and transports the current activity together with every wait.
;======================================================================
_spectrum_wave::
ld e, l ; E = key register value
cp #0x68
ret c
cp #0x80
ret nc
sub #0x68 ; A = channel 0..23
ld d, a
and #0x07
ld hl, #sw_bit_mask
add a, l
ld l, a
jr nc, sw_mask_nc
inc h
sw_mask_nc:
ld c, (hl) ; C = bit within mask byte
ld a, d
rrca
rrca
rrca
and #0x03 ; byte index 0..2
ld hl, #_spec_wave_active
add a, l
ld l, a
jr nc, sw_ptr_nc
inc h
sw_ptr_nc:
bit 7, e
jr z, sw_key_off
ld a, (hl)
or c
ld (hl), a
ret
sw_key_off:
ld a, c
cpl
and (hl)
ld (hl), a
ret
sw_bit_mask:
.db 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
;======================================================================
; _set_spec_bit — set bit A (0..15) in spec_mask
; Input: A = bit number (0..15)
; Trashes: A, D, E, HL
; Preserves: B, C (no push/pop needed)
; Reuses the 8-byte wave-channel bit table; bit3 selects low/high byte.
;======================================================================
_set_spec_bit:
ld d, a ; preserve bar number
and #0x07
ld hl, #sw_bit_mask
add a, l
ld l, a
jr nc, ssb_ptr_ok
inc h
ssb_ptr_ok:
ld e, (hl) ; E = 8-bit mask
bit 3, d
jr z, ssb_low_byte
ld d, e ; bars 8..15: mask in high byte
ld e, #0
jr ssb_merge
ssb_low_byte:
ld d, #0 ; bars 0..7: mask in low byte
ssb_merge:
ld hl, (_spec_mask)
ld a, l
or e
ld l, a
ld a, h
or d
ld h, a
ld (_spec_mask), hl
ret
;======================================================================
; spectrum_ay(reg, val) — A=reg, L=val (chip1, base=0)
; spectrum_ay2(reg, val) — A=reg, L=val (chip2, base=3)
;
; AY: shadow tone period (regs 0-5), trigger bar on volume (regs 8-10)
; YM2203 FM: shadow block (0xA4-0xA6), key-on (0x28)
;======================================================================
_spectrum_ay2::
ld d, a ; D = reg
ld e, l ; E = val
ld a, #3
jr say_base_set
_spectrum_ay::
ld d, a ; D = reg
ld e, l ; E = val
xor a, a ; base = 0
say_base_set:
ld (say_base), a ; save base
; ── Check reg <= 5 (tone period shadow) ──
ld a, d ; A = reg
cp a, #6
jr nc, say_check_vol
; ch = base + (reg >> 1)
srl a ; A = reg >> 1
ld hl, #say_base
add a, (hl) ; A = base + (reg>>1) = ch index
; Offset into spec_ay_period[ch] (uint16_t array)
add a, a ; A = ch * 2
ld hl, #_spec_ay_period
add a, l
ld l, a
jr nc, say_nc1
inc h
say_nc1:
; HL → spec_ay_period[ch]
bit 0, d ; reg & 1 ?
jr nz, say_hi_nibble
; Low byte: period = (period & 0x0F00) | val
ld a, e ; A = val
ld (hl), a ; write low byte
ret
say_hi_nibble:
; High nibble: period = (period & 0x00FF) | ((val & 0x0F) << 8)
inc hl ; point to high byte
ld a, e
and a, #0x0F
ld (hl), a
ret
; ── Check reg 8-10 (volume → trigger bar) ──
say_check_vol:
ld a, d
cp a, #8
jr c, say_check_fm_block
cp a, #11
jr nc, say_check_fm_block
; Check val & 0x0F != 0
ld a, e
and a, #0x0F
ret z ; volume=0, skip
; ch_index = base + (reg - 8)
ld a, d
sub a, #8
ld hl, #say_base
add a, (hl) ; A = array index
; Load spec_ay_period[index]
add a, a ; * 2
ld hl, #_spec_ay_period
add a, l
ld l, a
jr nc, say_nc2
inc h
say_nc2:
ld a, (hl)
inc hl
ld h, (hl)
ld l, a ; HL = period
; Fall through to spec_period_to_bar + set_spec_bit
call _spec_period_to_bar ; A = bar index
jp _set_spec_bit ; tail-call
; ── Check FM block shadow (regs 0xA4-0xA6) ──
say_check_fm_block:
ld a, d
cp a, #0xA4
jr c, say_check_keyon
cp a, #0xA7
jr nc, say_check_keyon
; spec_fm_block[base + (reg - 0xA4)] = (val >> 3) & 0x07
sub a, #0xA4
ld hl, #say_base
add a, (hl) ; A = index
ld hl, #_spec_fm_block
add a, l
ld l, a
jr nc, say_nc3
inc h
say_nc3:
ld a, e
srl a
srl a
srl a
and a, #0x07
ld (hl), a
ret
; ── Check FM key-on (reg 0x28) ──
say_check_keyon:
ld a, d
cp a, #0x28
ret nz
; Check operators keyed on (val & 0xF0)
ld a, e
and a, #0xF0
ret z
; ch = val & 0x03
ld a, e
and a, #0x03
cp a, #3
ret nc ; ch > 2 invalid
; bar = spec_fm_block[base + ch] << 1
ld hl, #say_base
add a, (hl) ; A = index
ld hl, #_spec_fm_block
add a, l
ld l, a
jr nc, say_nc4
inc h
say_nc4:
ld a, (hl) ; A = block (0..7)
add a, a ; A = block * 2 = bar
jp _set_spec_bit ; tail-call
;======================================================================
; spec_period_to_bar(period) — HL=period, returns A=bar (0..15)
;
; Cascaded compare on high byte first, then low byte.
; Optimised: single compare chain, no function calls.
;======================================================================
_spec_period_to_bar::
ld a, h ; A = period >> 8
cp a, #12
jr c, sptb_lt12
xor a, a ; return 0
ret
sptb_lt12:
cp a, #8
jr c, sptb_lt8
ld a, #1
ret
sptb_lt8:
cp a, #6
jr c, sptb_lt6
ld a, #2
ret
sptb_lt6:
cp a, #4
jr c, sptb_lt4
ld a, #3
ret
sptb_lt4:
cp a, #3
jr c, sptb_lt3
ld a, #4
ret
sptb_lt3:
cp a, #2
jr c, sptb_lt2
ld a, #5
ret
sptb_lt2:
cp a, #1
jr c, sptb_lt1
; h == 1: check low byte
ld a, l
cp a, #128
ld a, #6
ret nc ; period >= 384 (1*256+128)
ld a, #7
ret ; period >= 256
sptb_lt1:
; h == 0: use low byte
ld a, l
cp a, #192
jr c, sptb_p1
ld a, #8
ret
sptb_p1:
cp a, #128
jr c, sptb_p2
ld a, #9
ret
sptb_p2:
cp a, #96
jr c, sptb_p3
ld a, #10
ret
sptb_p3:
cp a, #64
jr c, sptb_p4
ld a, #11
ret
sptb_p4:
cp a, #48
jr c, sptb_p5
ld a, #12
ret
sptb_p5:
cp a, #32
jr c, sptb_p6
ld a, #13
ret
sptb_p6:
cp a, #16
jr c, sptb_p7
ld a, #14
ret
sptb_p7:
ld a, #15
ret
;======================================================================
; spectrum_saa(reg, val) — A=reg, L=val
;
; SAA1099 chip 0: shadow octave (regs 0x10-0x12), trigger on amplitude (0-5).
; Bar mapping: if dual → bar=octave (0-7); if single → bar=octave*2 (0-14).
;======================================================================
_spectrum_saa::
ld d, a ; D = reg
ld e, l ; E = val
; ── Check octave shadow (regs 0x10-0x12) ──
cp a, #0x10
jr c, ssaa_check_amp
cp a, #0x13
jr nc, ssaa_check_amp
; ch = (reg - 0x10) * 2
sub a, #0x10
add a, a ; A = (reg-0x10)*2 = ch_base
ld hl, #_spec_saa_oct
add a, l
ld l, a
jr nc, ssaa_nc1
inc h
ssaa_nc1:
; spec_saa_oct[ch] = val & 0x07
ld a, e
and a, #0x07
ld (hl), a
; spec_saa_oct[ch+1] = (val >> 4) & 0x07
inc hl
ld a, e
rrca
rrca
rrca
rrca
and a, #0x07
ld (hl), a
ret
; ── Check amplitude (regs 0x00-0x05) ──
ssaa_check_amp:
ld a, d
cp a, #0x06
ret nc ; reg >= 6, not amplitude
; Check val != 0
ld a, e
or a, a
ret z
; bar = spec_saa_oct[reg]
ld a, d ; A = reg (0..5)
ld hl, #_spec_saa_oct
add a, l
ld l, a
jr nc, ssaa_nc2
inc h
ssaa_nc2:
ld a, (hl) ; A = octave (0..7)
; if dual: bar = octave; if single: bar = octave * 2
ld hl, #_spec_saa_dual
bit 0, (hl)
jr nz, ssaa_bar_done
add a, a ; A = octave * 2
ssaa_bar_done:
jp _set_spec_bit ; tail-call
;======================================================================
; spectrum_saa2(reg, val) — A=reg, L=val
;
; SAA1099 chip 1: shadow octave to spec_saa_oct2[],
; trigger on amplitude with bar = octave + 8 (bars 8-15).
;======================================================================
_spectrum_saa2::
ld d, a ; D = reg
ld e, l ; E = val
; ── Check octave shadow (regs 0x10-0x12) ──
cp a, #0x10
jr c, ssaa2_check_amp
cp a, #0x13
jr nc, ssaa2_check_amp
; ch = (reg - 0x10) * 2
sub a, #0x10
add a, a
ld hl, #_spec_saa_oct2
add a, l
ld l, a
jr nc, ssaa2_nc1
inc h
ssaa2_nc1:
ld a, e
and a, #0x07
ld (hl), a
inc hl
ld a, e
rrca
rrca
rrca
rrca
and a, #0x07
ld (hl), a
ret
; ── Check amplitude (regs 0x00-0x05) ──
ssaa2_check_amp:
ld a, d
cp a, #0x06
ret nc
ld a, e
or a, a
ret z
; bar = spec_saa_oct2[reg] + 8
ld a, d
ld hl, #_spec_saa_oct2
add a, l
ld l, a
jr nc, ssaa2_nc2
inc h
ssaa2_nc2:
ld a, (hl) ; A = octave (0..7)
add a, #8 ; bars 8-15 for chip 2
jp _set_spec_bit ; tail-call
;======================================================================
; spectrum_font_init() — install custom glyphs for chars 0x01-0x02
;
; Maps font page into page3 window, saves original 16 bytes (2 glyphs),
; writes striped-bar patterns from glyph_half / glyph_full.
; Must be called once before playback. Pairs with spectrum_font_restore.
;======================================================================
_spectrum_font_init::
ld bc, #PORT_PAGE3
in a, (c)
push af
push bc
ld a, #WC_PAGE_FONT0
out (c), a
ld hl, #(FONT_BASE + 8)
ld de, #sf_font_save
ld bc, #16
ldir
ld hl, #sf_glyph_half
ld de, #(FONT_BASE + 8)
ld bc, #16
ldir
pop bc
pop af
out (c), a
ret
;======================================================================
; spectrum_font_restore() — restore original glyphs for chars 0x01-0x02
;
; Must be called on exit to avoid corrupting the system font.
;======================================================================
_spectrum_font_restore::
ld bc, #PORT_PAGE3
in a, (c)
push af
push bc
ld a, #WC_PAGE_FONT0
out (c), a
ld hl, #sf_font_save
ld de, #(FONT_BASE + 8)
ld bc, #16
ldir
pop bc
pop af
out (c), a
ret
sf_glyph_half:
.db 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00
sf_glyph_full:
.db 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00
sf_font_save:
.ds 16
+561
View File
@@ -0,0 +1,561 @@
;==============================================================================
; txtlib.s — Text buffer library (hand-optimised Z80, sdcccall(1))
;
; Replaces lib/txtlib.c. Every public function matches the ABI
; generated by SDCC for the prototypes in inc/txtlib.h.
;
; Buffer layout: buf[0] = max capacity, buf[1] = current length,
; buf[2..] = character data.
;
; sdcccall(1) register conventions:
; 1st ptr/u16 → HL 2nd ptr/u16 → DE
; 1st u8 → A 2nd u8 (when 1st is u8) → L
; Further args → stack (callee-cleans)
; Return u8 → A Return u16 → HL
;==============================================================================
.module txtlib_mod
.globl _buf_append_str
.globl _buf_append_char
.globl _buf_append_u8_hex
.globl _buf_append_u16_dec
.globl _buf_append_u32_dec
.globl _buf_append_mmss
.globl _print_line
.globl _wc_prsrw_attr ; from wc_api.s
.area _CODE
;----------------------------------------------------------------------
; Helper: nibble (0–15 in A) → ASCII char ('0'–'F') in A
;----------------------------------------------------------------------
_nib2asc:
cp a, #10
jr c, 001$
add a, #7
001$: add a, #0x30
ret
;======================================================================
; uint8_t buf_append_str(char *buf, const char *src)
; HL=buf DE=src → A=added (no stack params)
;======================================================================
_buf_append_str::
ld c, (hl) ; C = max
inc hl
ld b, (hl) ; B = curlen
ld a, c
sub a, b ; A = remaining
jr z, bas_z
jr c, bas_z
push hl ; save &buf[1]
ld c, a ; C = remaining
inc hl ; HL → buf[2]
ld a, b
add a, l
ld l, a
jr nc, bas_nc
inc h
bas_nc: ld b, #0 ; B = added
bas_lp: ld a, (de)
or a, a
jr z, bas_dn
ld (hl), a
inc hl
inc de
inc b
dec c
jr nz, bas_lp
bas_dn: pop hl ; &buf[1]
ld a, (hl)
add a, b
ld (hl), a
ld a, b
ret
bas_z: xor a, a
ret
;======================================================================
; uint8_t buf_append_char(char *buf, char c)
; HL=buf stack: c(1) → A=0|1 callee-cleans 1
;======================================================================
_buf_append_char::
push ix
ld ix, #0
add ix, sp
; 4(ix) = c
ld c, (hl) ; max
inc hl
ld a, (hl) ; curlen
cp a, c
jr nc, bac_f
ld e, a
inc a
ld (hl), a ; buf[1]++
inc hl ; → buf[2]
ld a, e
add a, l
ld l, a
jr nc, bac_nc
inc h
bac_nc: ld a, 4 (ix)
ld (hl), a
ld a, #1
jr bac_r
bac_f: xor a, a
bac_r: pop ix
pop hl
inc sp ; skip c (1 byte)
jp (hl)
;======================================================================
; uint8_t buf_append_u8_hex(char *buf, uint8_t val)
; HL=buf stack: val(1) → A=0|2 callee-cleans 1
;======================================================================
_buf_append_u8_hex::
push ix
ld ix, #0
add ix, sp
; 4(ix) = val
ld c, (hl) ; max
inc hl
ld a, c
sub a, (hl) ; remaining
cp a, #2
jr c, bh8_f
ld b, (hl) ; curlen
ld a, b
add a, #2
ld (hl), a ; buf[1] += 2
inc hl
ld a, b
add a, l
ld l, a
jr nc, bh8_nc
inc h
bh8_nc: ld a, 4 (ix)
rrca
rrca
rrca
rrca
and a, #0x0F
call _nib2asc
ld (hl), a
inc hl
ld a, 4 (ix)
and a, #0x0F
call _nib2asc
ld (hl), a
ld a, #2
jr bh8_r
bh8_f: xor a, a
bh8_r: pop ix
pop hl
inc sp
jp (hl)
;----------------------------------------------------------------------
; Shared helper: emit one decimal digit.
; A = digit (0–9)
; B = "added" counter (0 = not-yet-started → suppress leading 0)
; C = remaining buffer space
; IY = destination pointer (advanced on write)
;----------------------------------------------------------------------
_emit_digit:
push af
or a, b ; z iff digit==0 AND not started
jr z, ed_skip
ld a, c
or a, a
jr z, ed_skip ; no space
dec c
pop af
add a, #0x30
ld 0 (iy), a
inc iy
inc b
ret
ed_skip:
pop af
ret
;----------------------------------------------------------------------
; Shared helper: extract one 32-bit decimal digit via repeated
; subtraction of a 4-byte constant taken from inline data after CALL.
;
; Usage: call _d32_digit
; .db lo, b1, b2, hi ; 32-bit subtrahend (LE)
;
; In: DEHL = 32-bit value, -3(ix) = scratch,
; B = started/added, C = remaining, IY = dest ptr
; Out: DEHL = remainder, digit emitted via _emit_digit
;----------------------------------------------------------------------
_d32_digit:
ex (sp), hl ; HL = inline-data ptr, (SP)=val_lo
ld a, (hl)
ld (d32c0 + 1), a
inc hl
ld a, (hl)
ld (d32c1 + 1), a
inc hl
ld a, (hl)
ld (d32c2 + 1), a
inc hl
ld a, (hl)
ld (d32c3 + 1), a
inc hl ; HL past data = true ret addr
ex (sp), hl ; (SP)=true ret, HL=val_lo restored
ld -3 (ix), #0xFF
d32d_lp:inc -3 (ix)
push de
push hl
ld a, l
d32c0: sub a, #0x00 ; patched: subtrahend byte 0
ld l, a
ld a, h
d32c1: sbc a, #0x00 ; patched: byte 1
ld h, a
ld a, e
d32c2: sbc a, #0x00 ; patched: byte 2
ld e, a
ld a, d
d32c3: sbc a, #0x00 ; patched: byte 3
ld d, a
jr c, d32d_un
pop af
pop af
jr d32d_lp
d32d_un:pop hl
pop de
ld a, -3 (ix)
jp _emit_digit ; tail-call, ret goes past inline data
;----------------------------------------------------------------------
; Shared 16-bit decimal division: 10000/1000/100/10/ones.
; Entry _div_10k_ones: full 16-bit decimal (10000..1).
; Entry _div_1k_ones: skip 10000, do 1000..1.
; In: DE=value, B=added counter, C=remaining, IY=dst.
; Out: B=updated, DE=remainder. A,HL clobbered.
;----------------------------------------------------------------------
_div_10k_ones:
;-- 10000 (neg 0xD8F0) --
ld a, #0xFF
_d10k: inc a
ld hl, #0xD8F0
add hl, de
jr nc, _d10ke
ex de, hl
jr _d10k
_d10ke: call _emit_digit
_div_1k_ones:
;-- 1000 (neg 0xFC18) --
ld a, #0xFF
_d1k: inc a
ld hl, #0xFC18
add hl, de
jr nc, _d1ke
ex de, hl
jr _d1k
_d1ke: call _emit_digit
;-- 100 (neg 0xFF9C) --
ld a, #0xFF
_d100: inc a
ld hl, #0xFF9C
add hl, de
jr nc, _d100e
ex de, hl
jr _d100
_d100e: call _emit_digit
;-- 10 (neg 0xFFF6) --
ld a, #0xFF
_d10: inc a
ld hl, #0xFFF6
add hl, de
jr nc, _d10e
ex de, hl
jr _d10
_d10e: call _emit_digit
;-- ones (always emit) --
ld a, c
or a, a
ret z
ld a, e
add a, #0x30
ld 0 (iy), a
inc iy
inc b
ret
;======================================================================
; uint8_t buf_append_u16_dec(char *buf, uint16_t val)
; HL=buf DE=val → A=added (no stack params)
;======================================================================
_buf_append_u16_dec::
push iy
ld c, (hl)
inc hl
ld a, c
sub a, (hl)
jr z, d16_z
jr c, d16_z
ld c, a ; C = remaining
ld b, (hl) ; B = curlen (temp)
push hl ; save &buf[1]
inc hl
ld a, b
add a, l
ld l, a
jr nc, d16_nc
inc h
d16_nc: push hl
pop iy ; IY = dst
ld b, #0 ; B = added/started
call _div_10k_ones
pop hl ; &buf[1]
ld a, (hl)
add a, b
ld (hl), a
ld a, b
pop iy
ret
d16_z: xor a, a
pop iy
ret
;======================================================================
; uint8_t buf_append_u32_dec(char *buf, uint32_t val)
; HL=buf stack: val(4) → A=added callee-cleans 4
; 4(ix)=byte0(LSB) 5(ix)=byte1 6(ix)=byte2 7(ix)=byte3(MSB)
;======================================================================
_buf_append_u32_dec::
push ix
ld ix, #0
add ix, sp
push iy ; -2,-1(ix)
push af ; -4,-3(ix) scratch
push af ; -6,-5(ix) scratch
; -3(ix) used as digit counter per power
ld c, (hl)
inc hl
ld a, c
sub a, (hl)
jp z, d32_z
jp c, d32_z
ld c, a ; C = remaining
ld b, (hl) ; B = curlen (temp)
push hl ; save &buf[1] → -8,-7(ix)
inc hl
ld a, b
add a, l
ld l, a
jr nc, d32_nc
inc h
d32_nc: push hl
pop iy ; IY = dst
; Load 32-bit val into DEHL
ld l, 4 (ix)
ld h, 5 (ix)
ld e, 6 (ix)
ld d, 7 (ix)
ld b, #0 ; B = added/started
;-- 1,000,000 / 100,000 / 10,000 via shared subroutine --
call _d32_digit
.db 0x40, 0x42, 0x0F, 0x00 ; 1,000,000
call _d32_digit
.db 0xA0, 0x86, 0x01, 0x00 ; 100,000
call _d32_digit
.db 0x10, 0x27, 0x00, 0x00 ; 10,000
;-- remainder < 10000, fits in HL → move to DE --
ex de, hl
call _div_1k_ones
pop hl ; &buf[1]
ld a, (hl)
add a, b
ld (hl), a
ld a, b ; return value
d32_exit:
; clean locals, restore, callee-clean 4
pop de ; eat -6,-5
pop de ; eat -4,-3
pop iy
pop ix
pop hl ; ret addr
pop de ; skip val lo
pop de ; skip val hi
jp (hl)
d32_z: xor a, a
jr d32_exit
;======================================================================
; void print_line(wc_window_t *win, uint8_t y, const char *buf,
; uint8_t attr)
; HL=win stack: y(1) buf(2) attr(1) callee-cleans 4
; Prints buf content + spaces padding to fill content width.
;======================================================================
_print_line::
push ix
ld ix, #0
add ix, sp
; 4(ix)=y 5,6(ix)=buf 7(ix)=attr
push af ; -2,-1(ix): cw at -1, slen at -2
push hl ; -4,-3(ix): win
;-- cw = get_content_width(win) --
ld de, #4
add hl, de
ld a, (hl)
or a, a
jr nz, pl_nz
ld a, #96
pl_nz: sub a, #4
ld -1 (ix), a
;-- slen = buf[1] --
ld l, 5 (ix)
ld h, 6 (ix)
inc hl
ld a, (hl)
ld -2 (ix), a
;-- print_len = min(slen, cw) --
cp a, -1 (ix)
jr c, pl_min
ld a, -1 (ix)
pl_min: ld c, a ; C = print_len
;-- 1st wc_prsrw_attr(win, data, y, 2, print_len, attr) --
ld a, 7 (ix)
push af
inc sp
ld b, #0
push bc
ld h, #2
ld l, 4 (ix)
push hl
; DE = buf + 2
ld a, 5 (ix)
add a, #2
ld e, a
ld a, 6 (ix)
adc a, #0
ld d, a
; HL = win
ld l, -4 (ix)
ld h, -3 (ix)
call _wc_prsrw_attr
;-- padding? --
ld a, -2 (ix)
cp a, -1 (ix)
jr nc, pl_done
; pad = cw − slen
ld a, -1 (ix)
sub a, -2 (ix)
ld c, a
; Fill buf[2..2+pad-1] with 0x20 (runtime space fill)
ld l, 5 (ix)
ld h, 6 (ix)
inc hl
inc hl ; HL = &buf[2]
ld b, c ; B = pad count
ld a, #0x20
pf_fill:ld (hl), a
inc hl
djnz pf_fill ; B=0 after loop
;-- 2nd wc_prsrw_attr(win, &buf[2], y, 2+slen, pad, attr) --
ld a, 7 (ix)
push af
inc sp
push bc ; len=pad (B=0, C=pad)
ld a, -2 (ix)
add a, #2
ld h, a ; x = 2 + slen
ld l, 4 (ix) ; y
push hl
ld e, 5 (ix)
ld d, 6 (ix)
inc de
inc de ; DE = &buf[2]
ld l, -4 (ix)
ld h, -3 (ix)
call _wc_prsrw_attr
pl_done:
ld sp, ix
pop ix
pop hl
pop af
pop af ; callee-clean 4
jp (hl)
;----------------------------------------------------------------------
; buf_append_mmss(buf, sec) — append "MM:SS" (5 chars) to buffer
;
; sdcccall(1): HL = buf, DE = sec (uint16_t, 0..5999)
; No division — subtraction loops only (~30 bytes code).
;----------------------------------------------------------------------
_buf_append_mmss:
push hl
pop ix ; IX = buf
; DE = total seconds. Divide by 60 via subtraction → B=min, E=sec
ld b, #0
mmss_d60:
ld a, e
sub a, #60
ld c, a
ld a, d
sbc a, #0
jr c, mmss_d60_done
ld d, a
ld e, c
inc b
jr mmss_d60
mmss_d60_done:
; B = minutes, E = seconds
push de ; save seconds (mmss_2dig clobbers DE)
ld a, b
call mmss_2dig ; appends 2 digits of A
; Append ':'
ld a, 1 (ix)
ld c, a
ld b, #0
inc a
ld 1 (ix), a
push ix
pop hl
add hl, bc
inc hl ; skip buf[0] (max)
inc hl ; skip buf[1] (len)
ld (hl), #0x3A ; ':'
; Append seconds
pop de ; restore E = seconds
ld a, e
; fall through to mmss_2dig — but IX is still buf
; call mmss_2dig
call mmss_2dig
ret
; Subroutine: append 2 ASCII digits of A (0..99) to buf at IX
; Destroys A, C, B, HL
mmss_2dig:
ld c, #0x30 ; tens digit = '0'
mmss_t10:
sub a, #10
jr c, mmss_t10_done
inc c
jr mmss_t10
mmss_t10_done:
add a, #10 ; undo last sub
add a, #0x30 ; units → ASCII
ld b, a ; save units
; Append tens (C) then units (B)
ld a, 1 (ix) ; len
ld l, a
ld h, #0
add a, #2
ld 1 (ix), a ; len += 2
push ix
pop de ; DE = buf base
add hl, de
inc hl ; skip buf[0] (max)
inc hl ; skip buf[1] (len)
ld (hl), c ; tens
inc hl
ld (hl), b ; units
ret
File diff suppressed because it is too large Load Diff
+172
View File
@@ -0,0 +1,172 @@
@echo off
chcp 65001 >nul
REM ===========================================================================
REM build.bat — Сборка VGM Player Plugin (SDCC/Z80)
REM
REM Инструменты:
REM sdcc — компилятор C (sdcccall(1), mz80, без frame pointer)
REM sdas — ассемблер SDCC (ASM файлы)
REM sdld / sdcc --link-cmd-file — линковщик
REM node — JS-скрипты (gen_pos_table, ihx2wmf)
REM
REM Структура вывода: build/ (временные .rel) → vgmplay.wmf
REM ===========================================================================
setlocal
cd /d "%~dp0"
REM --- Папка для временных файлов ---
if not exist build mkdir build
REM --- Флаги SDCC ---
REM -mz80 : целевая архитектура Z80
REM --sdcccall 1 : соглашение вызова sdcccall(1) (регистровая передача)
REM --no-std-crt0 : не линковать стандартный crt0 (наш в asm/crt0.s)
REM --opt-code-size : оптимизация по размеру (важно для 8-бит)
REM --no-peep-asm : не трогать __naked asm-блоки
set CFLAGS=-mz80 --sdcccall 1 --no-std-crt0 --opt-code-size -I inc --disable-warning 85
REM --- Флаги ассемблера (sdas) ---
set ASFLAGS=-plosff
REM --- Читаем параметры варианта из variant_cfg.h ---
for /f "tokens=3" %%a in ('findstr /C:"#define VARIANT_POS_ENTRIES" inc\variant_cfg.h') do set POS_ENTRIES=%%a
for /f "tokens=3" %%a in ('findstr /C:"#define VARIANT_POS_STEP" inc\variant_cfg.h') do set POS_STEP=%%a
for /f "tokens=3" %%a in ('findstr /C:"#define VARIANT_DATA_LOC" inc\variant_cfg.h') do set DATA_LOC=%%a
echo variant_cfg: entries=%POS_ENTRIES% step=%POS_STEP% data-loc=%DATA_LOC%
echo [1/8] Генерация pos_table.s...
node scripts\gen_pos_table.js --entries %POS_ENTRIES% --step %POS_STEP%
if errorlevel 1 ( echo FAIL gen_pos_table && goto :err )
echo [2/8] Генерация freq_tables.bin...
node scripts\gen_freq_tables.js --page-base 1 --binary build\freq_tables.bin --cheader inc\freq_lut_map.h
if errorlevel 1 ( echo FAIL gen_freq_tables && goto :err )
echo [3/9] Сборка inflate.asm (sjasmplus)...
..\tools\sjasm\sjasmplus.exe asm\inflate.asm >nul 2>&1
if errorlevel 1 ( echo FAIL inflate.asm && goto :err )
REM inflate.asm имеет SAVEBIN "build/inflate.bin" внутри
REM Паддинг inflate.bin до 16KB (чтобы cmdblocks.bin начинался на границе страницы)
powershell -NoProfile -Command "$f=[IO.File]::ReadAllBytes('build\inflate.bin'); if($f.Length -lt 16384){$pad=New-Object byte[] (16384-$f.Length); [IO.File]::WriteAllBytes('build\inflate.bin',$f+$pad); Write-Host (' inflate.bin padded: '+$f.Length+' -> 16384')}"
if errorlevel 1 ( echo FAIL pad inflate && goto :err )
echo [3b/9] Сборка cmdblocks.asm (sjasmplus)...
..\tools\sjasm\sjasmplus.exe asm\cmdblocks.asm >nul 2>&1
if errorlevel 1 ( echo FAIL cmdblocks.asm && goto :err )
REM cmdblocks.asm имеет SAVEBIN "build/cmdblocks.bin" внутри
echo [4/9] Компиляция C файлов...
sdcc %CFLAGS% -c main.c -o build\main.rel >nul
if errorlevel 1 ( echo FAIL main.c && goto :err )
sdcc %CFLAGS% -c lib\vgm.c -o build\vgm.rel >nul
if errorlevel 1 ( echo FAIL vgm.c && goto :err )
sdcc %CFLAGS% -c lib\keys.c -o build\keys.rel >nul
if errorlevel 1 ( echo FAIL keys.c && goto :err )
REM txtlib is now hand-written assembly (asm\txtlib.s), assembled below
echo [5/9] Сборка ASM файлов...
sdasz80 %ASFLAGS% build\crt0.rel asm\crt0.s >nul
if errorlevel 1 ( echo FAIL crt0.s && goto :err )
sdasz80 %ASFLAGS% build\isr.rel asm\isr.s >nul
if errorlevel 1 ( echo FAIL isr.s && goto :err )
sdasz80 %ASFLAGS% build\pos_table.rel asm\pos_table.s >nul
if errorlevel 1 ( echo FAIL pos_table.s && goto :err )
call lib\wc_api\build_lib.bat >nul
if errorlevel 1 ( echo FAIL wc_api.lib && goto :err )
sdasz80 %ASFLAGS% build\txtlib.rel asm\txtlib.s >nul
if errorlevel 1 ( echo FAIL txtlib.s && goto :err )
sdasz80 %ASFLAGS% build\inflate_call.rel asm\inflate_call.s >nul
if errorlevel 1 ( echo FAIL inflate_call.s && goto :err )
sdasz80 %ASFLAGS% build\spectrum.rel asm\spectrum.s >nul
if errorlevel 1 ( echo FAIL spectrum.s && goto :err )
echo [6/9] Линковка...
REM Порядок важен: crt0 первым (entry point), затем модули
REM Layout: CODE #8000–#B8CF, DATA #B8D0–#BFFF
REM Все секции (DATA + GSINIT + HOME + INITIALIZER) должны
REM уместиться до #BFFF (Win2). Если CODE вырастет →
REM понизить --data-loc или оптимизировать код.
sdcc -mz80 --no-std-crt0 --out-fmt-ihx --code-loc 0x8000 --data-loc %DATA_LOC% ^
build\crt0.rel ^
build\main.rel ^
build\vgm.rel ^
build\keys.rel ^
build\isr.rel ^
build\pos_table.rel ^
build\wc_api.lib ^
build\txtlib.rel ^
build\inflate_call.rel ^
build\spectrum.rel ^
-o build\vgmplay.ihx >nul
if errorlevel 1 ( echo FAIL link && goto :err )
REM --- Вывод карты памяти секций из map-файла ---
powershell -NoProfile -Command ^
"$m=Get-Content build\vgmplay.map -Raw;" ^
"function gs($p){if($m -match '([0-9a-f]{8})\s+'+$p){return $Matches[1]}return '00000000'};" ^
"$secs=@('CODE','DATA','GSINIT','GSFINAL','INITIALIZED','HOME','INITIALIZER');" ^
"Write-Host '';" ^
"Write-Host ' Section Start End Size';" ^
"Write-Host ' -------------- -------- -------- ----------';" ^
"$lastEnd=0;$codeEnd=0;$dataStart=0;" ^
"foreach($s in $secs){" ^
" $st=[Convert]::ToInt32((gs ('s__'+$s)),16);" ^
" $ln=[Convert]::ToInt32((gs ('l__'+$s)),16);" ^
" if($ln -eq 0){continue};" ^
" $en=$st+$ln-1;" ^
" if($en -gt $lastEnd){$lastEnd=$en};" ^
" if($s -eq 'CODE'){$codeEnd=$en};" ^
" if($s -eq 'DATA'){$dataStart=$st};" ^
" Write-Host (' _'+$s.PadRight(14)+' 0x'+$st.ToString('X4')+' 0x'+$en.ToString('X4')+' '+$ln.ToString().PadLeft(5)+' bytes')" ^
"};" ^
"$free=0xBFFF-$lastEnd;" ^
"Write-Host ' -------------- -------- -------- ----------';" ^
"Write-Host (' Last byte: 0x'+$lastEnd.ToString('X4')+' Free before C000: '+$free+' bytes');" ^
"if($codeEnd -ge $dataStart -and $dataStart -gt 0){Write-Host (' *** ERROR: _CODE (end 0x'+$codeEnd.ToString('X4')+') overlaps _DATA (start 0x'+$dataStart.ToString('X4')+')! ***') -ForegroundColor Red; exit 1};" ^
"if($lastEnd -ge 0xC000){Write-Host ' *** ERROR: sections overflow past 0xBFFF! ***' -ForegroundColor Red; exit 1}"
if errorlevel 1 goto :err
echo [7/9] Генерация WMF (multi-page)...
REM Склеить freq_tables.bin + inflate page + cmdblocks.bin
copy /b build\freq_tables.bin+build\inflate.bin+build\cmdblocks.bin build\extra_combined.bin >nul
if errorlevel 1 ( echo FAIL combine extra && goto :err )
node scripts\ihx2wmf.js build\vgmplay.ihx ..\DiskRef\WC\VGMPLAY.WMF --extra build\extra_combined.bin
if errorlevel 1 ( echo FAIL ihx2wmf && goto :err )
copy /Y ..\DiskRef\WC\VGMPLAY.WMF ..\VGMPLAY.WMF >nul
echo [8/9] Проверка размеров binary...
powershell -NoProfile -Command "Get-ChildItem build\*.bin | Select-Object Name, Length | Format-Table -AutoSize"
echo [9/9] Сборка образа диска...
if not exist "..\tools\robimg.exe" (
echo SKIP: tools\robimg.exe не найден
goto :ok
)
del /Q ..\tsconf.img 2>nul
..\tools\robimg.exe -p="..\tsconf.img" -s=280000 -C="..\DiskRef" >nul 2>&1
if errorlevel 1 ( echo FAIL robimg && goto :err )
echo tsconf.img готов
:ok
echo.
echo === BUILD OK ===
goto :end
:err
echo.
echo === BUILD FAILED ===
exit /b 1
:end
endlocal
+236
View File
@@ -0,0 +1,236 @@
<#
.SYNOPSIS
Build multiple ISR frequency / command budget variants for A/B testing.
Each variant produces a separate tsconf_*.img disk image.
.DESCRIPTION
Generates inc\variant_cfg.h for each variant, runs build.bat, copies
the output image. Restores the original variant_cfg.h afterward.
NO source files are regex-patched — only variant_cfg.h is overwritten.
.NOTES
C compilation is SLOW (~60 sec per variant). Do NOT interrupt (Ctrl-C).
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path # src_sdcc
$projRoot = Split-Path -Parent $root # project root
$cfgFile = "$root\inc\variant_cfg.h"
# ── Frequency tiers ──────────────────────────────────────────────────
# ISR_FREQ = 3 500 000 / Step; Entries = 71 680 / Step; TPF = Entries
# Shift = log2(44100 / Freq) rounded down
$freqTiers = @{
'2734' = @{ Tag=''; Freq=2734; Step=1280; Entries=56; Shift=4; Mask='0x0F'; TPF=56; Budgets=@(8,16) }
'1367' = @{ Tag='hf'; Freq=1367; Step=2560; Entries=28; Shift=5; Mask='0x1F'; TPF=28; Budgets=@(8,16,32) }
'683' = @{ Tag='qf'; Freq=683; Step=5120; Entries=14; Shift=6; Mask='0x3F'; TPF=14; Budgets=@(16,32,64) }
}
# ── Generate variants: 2+3+3 = 8 base × 2 NS = 16 total ─────────────
$baseVariants = @()
foreach ($ft in $freqTiers.Values | Sort-Object Freq -Descending) {
foreach ($b in $ft.Budgets) {
$suffix = if ($ft.Tag) { "_$($ft.Tag)" } else { '' }
$name = "b${b}${suffix}"
$baseVariants += @{
Name = $name
Budget = $b
Entries = $ft.Entries
Step = $ft.Step
Freq = $ft.Freq
Shift = $ft.Shift
Mask = $ft.Mask
TPF = $ft.TPF
DataLoc = '0xB600'
NS = $false
}
}
}
# Auto-generate _ns (no short waits) clones
$nsVariants = foreach ($bv in $baseVariants) {
$nv = $bv.Clone()
$nv.Name = $bv.Name + '_ns'
$nv.NS = $true
$nv
}
$variants = $baseVariants + $nsVariants
# ── Save original variant_cfg.h ─────────────────────────────────────
$cfgBackup = Get-Content $cfgFile -Raw
function Restore-Cfg {
Set-Content $script:cfgFile -Value $script:cfgBackup -NoNewline
}
# ── Helper: generate variant_cfg.h ──────────────────────────────────
function Write-VariantCfg($v) {
$nsLine = if ($v.NS) { "#define VGM_NO_SHORT_WAITS" } else { "/* #define VGM_NO_SHORT_WAITS */" }
$content = @"
/* variant_cfg.h -- AUTO-GENERATED by build_variants.ps1
* Variant: $($v.Name) Freq=$($v.Freq)Hz Budget=$($v.Budget) NS=$($v.NS)
*/
#ifndef VARIANT_CFG_H
#define VARIANT_CFG_H
#define ISR_FREQ $($v.Freq)
#define ISR_TICKS_PER_FRAME $($v.TPF)
#define VGM_SAMPLE_SHIFT $($v.Shift)
#define VGM_SAMPLE_MASK $($v.Mask)
#define VGM_FILL_CMD_BUDGET $($v.Budget)
#define VARIANT_POS_ENTRIES $($v.Entries)
#define VARIANT_POS_STEP $($v.Step)
#define VARIANT_DATA_LOC $($v.DataLoc)
$nsLine
#endif /* VARIANT_CFG_H */
"@
Set-Content $script:cfgFile -Value $content -NoNewline
}
# ── Output directory ─────────────────────────────────────────────────
$outDir = "$projRoot\variants"
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
# ── Build loop ──────────────────────────────────────────────────────
$total = $variants.Count
$idx = 0
$results = @()
foreach ($v in $variants) {
$idx++
$name = $v.Name
Write-Host "`n========== [$idx/$total] Building $name ==========" -ForegroundColor Cyan
Write-Host " Budget=$($v.Budget) Freq=$($v.Freq)Hz Entries=$($v.Entries) Step=$($v.Step) Shift=$($v.Shift) TPF=$($v.TPF) NS=$($v.NS)"
# --- Generate variant_cfg.h ---
Write-VariantCfg $v
# --- Clean .rel files to force recompile ---
Remove-Item "$root\build\*.rel" -ErrorAction SilentlyContinue
# --- Run build ---
Write-Host " Building... (this takes ~60 sec)" -ForegroundColor Yellow
$buildOut = cmd /c "cd /d $root && build.bat 2>&1"
$buildOk = $buildOut | Where-Object { $_ -match 'BUILD OK' }
$buildFail = $buildOut | Where-Object { $_ -match 'FAIL|ERROR|overflow' }
if ($buildFail -or -not $buildOk) {
Write-Host " *** BUILD FAILED for $name ***" -ForegroundColor Red
$buildOut | Where-Object { $_ -match 'FAIL|error|warning|_CODE|_DATA|Last byte|overflow|variant_cfg' } |
ForEach-Object { Write-Host " $_" }
$results += [pscustomobject]@{ Name=$name; Status='FAIL'; Code=0; Free=0 }
continue
}
# --- Extract sizes from output ---
$codeLine = $buildOut | Where-Object { $_ -match '_CODE' } | Select-Object -First 1
$freeLine = $buildOut | Where-Object { $_ -match 'Free before' } | Select-Object -First 1
$codeSize = if ($codeLine -match '(\d+)\s+bytes') { [int]$Matches[1] } else { 0 }
$freeSize = if ($freeLine -match 'Free before C000:\s*(\d+)') { [int]$Matches[1] } else { 0 }
# --- Show map ---
$buildOut | Where-Object { $_ -match 'variant_cfg:|Section|_CODE|_DATA|_GSINIT|_HOME|_INITIAL|Last byte|Code:|Written.*WMF' } |
ForEach-Object { Write-Host " $_" }
# --- Copy output image ---
$srcImg = "$projRoot\tsconf.img"
$dstImg = "$outDir\tsconf_$name.img"
if (Test-Path $srcImg) {
Copy-Item $srcImg $dstImg -Force
Write-Host " -> $dstImg" -ForegroundColor Green
} else {
Write-Host " WARNING: tsconf.img not found" -ForegroundColor Yellow
}
# Also copy WMF
$srcWmf = "$projRoot\DiskRef\WC\VGMPLAY.WMF"
$dstWmf = "$outDir\VGMPLAY_$name.WMF"
if (Test-Path $srcWmf) {
Copy-Item $srcWmf $dstWmf -Force
}
$results += [pscustomobject]@{ Name=$name; Status='OK'; Code=$codeSize; Free=$freeSize }
}
# ── Restore original variant_cfg.h ──────────────────────────────────
Write-Host "`n========== Restoring original variant_cfg.h ==========" -ForegroundColor Cyan
Restore-Cfg
# Rebuild baseline to leave workspace clean
Remove-Item "$root\build\*.rel" -ErrorAction SilentlyContinue
Write-Host " Rebuilding baseline..." -ForegroundColor Yellow
$null = cmd /c "cd /d $root && build.bat 2>&1"
Write-Host " Done." -ForegroundColor Green
# ── Summary ──────────────────────────────────────────────────────────
Write-Host "`n========== VARIANT SUMMARY ==========" -ForegroundColor Cyan
$fmt = "{0,-18} {1,6} {2,6} {3,6} {4,3} {5,5} {6,5}"
Write-Host ($fmt -f 'Name','Budget','Freq','Code','NS','Free','St')
Write-Host ($fmt -f '----','------','----','----','--','----','--')
foreach ($r in $results) {
$v = $variants | Where-Object { $_.Name -eq $r.Name }
$nsTag = if ($v.NS) { 'yes' } else { ' - ' }
$color = if ($r.Status -eq 'OK') { 'Green' } else { 'Red' }
Write-Host ($fmt -f $r.Name, $v.Budget, $v.Freq, $r.Code, $nsTag, $r.Free, $r.Status) -ForegroundColor $color
}
Write-Host "`nImages in: $outDir"
# ── Generate README.txt ──────────────────────────────────────────────
$readme = @"
Variants of VGM player for A/B testing
=======================================
Parameters stored in inc\variant_cfg.h (single config file).
build_variants.ps1 regenerates variant_cfg.h for each variant.
Frequency tiers:
qf - quarter freq (ISR 683 Hz, step 5120, 14 ticks/frame)
(none) - base freq (ISR 2734 Hz, step 1280, 56 ticks/frame)
hf - half freq (ISR 1367 Hz, step 2560, 28 ticks/frame)
Other tags:
b<N> - command budget (max commands between forced pauses)
ns - no short waits (0x70-0x7F pauses skipped)
"@
$readme += "{0,-30} {1,6} {2,8} {3,3} {4,6} {5,5} {6}`n" -f 'Image','Budget','Freq','NS','Code','Free','Status'
$readme += "{0,-30} {1,6} {2,8} {3,3} {4,6} {5,5} {6}`n" -f '-----','------','----','--','----','----','------'
foreach ($r in $results) {
$v = $variants | Where-Object { $_.Name -eq $r.Name }
$nsTag = if ($v.NS) { 'yes' } else { ' - ' }
$freq = "$($v.Freq)Hz"
$readme += "{0,-30} {1,6} {2,8} {3,3} {4,6} {5,5} {6}`n" -f "tsconf_$($r.Name).img", $v.Budget, $freq, $nsTag, $r.Code, $r.Free, $r.Status
}
$readme += @"
What to test
------------
1. Higher budget helps (b32/b48/b64 vs b8/b16)
-> Increase VGM_FILL_CMD_BUDGET.
2. Lower freq helps (hf or qf better than base)
-> ISR overhead is the bottleneck, reduce frequency.
3. Higher freq helps (base better than hf/qf)
-> More granular timing needed, keep higher ISR.
4. _ns variants better -> short waits 0x70-0x7F are the problem.
5. Nothing helps -> something else (OPL3 write delay, VGM parsing, etc.)
"@
Set-Content "$outDir\README.txt" -Value $readme -NoNewline -Encoding UTF8
Write-Host "`nREADME.txt written to $outDir" -ForegroundColor Green
Write-Host "`nLegend:" -ForegroundColor DarkGray
Write-Host " qf = quarter freq (683 Hz)" -ForegroundColor DarkGray
Write-Host " hf = half freq (1367 Hz)" -ForegroundColor DarkGray
Write-Host " (no suffix) = base freq (2734 Hz)" -ForegroundColor DarkGray
Write-Host " bN = budget N commands per ISR tick" -ForegroundColor DarkGray
Write-Host " ns = no short waits (0x70-0x7F skipped)" -ForegroundColor DarkGray
Binary file not shown.
+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
+203
View File
@@ -0,0 +1,203 @@
/**
* @file keys.c
* @brief PS/2 клавиатура через TSConfig CMOS FIFO (порты #EFF7/#DFF7/#BFF7)
* @see inc/keys.h
*
* Аппаратный PS/2 FIFO буфер в FPGA TSConfig, регистр CMOS #F0.
*
* ps2_poll() за один вызов полностью сливает FIFO (tight loop).
* Если среди прочитанных байтов есть make-код нашей клавиши — возвращает
* последний найденный KEY_*. Стая байтов никогда не копится.
*
* Единственный бит состояния — skip flag (после F0 нужно пропустить 1 байт).
* E0 (extended prefix) не требует skip: сам E0 не совпадает с нашими кодами,
* а release extended (E0 F0 SC) корректно обрабатывается через F0→skip.
*
* Опрос ~1 раз в TV-кадр (rate-limited по isr_tick_ctr).
* ps2_poll полностью на asm.
*/
#include "../inc/types.h"
#include "../inc/keys.h"
#include "../inc/isr.h" /* isr_tick_ctr (volatile uint8_t) */
#include "../inc/variant_cfg.h" /* ISR_TICKS_PER_FRAME */
/* Единственная FSM-переменная: skip flag (0 или 1). */
static uint8_t ps2_state; /* 0 = normal, 1 = skip next byte */
static uint8_t ps2_last_tick;
/* ── ps2_init: активация PS/2 FIFO + одноразовый слив ────────────── */
void ps2_init(void)
{
__asm
; Разрешить PS/2 порты (бит 7 #EFF7)
ld bc, #0xEFF7
ld a, #0x80
out (c), a
; Выбрать CMOS reg #F0, записать 2 (активация), re-select #F0
ld bc, #0xDFF7
ld a, #0xF0
out (c), a
ld bc, #0xBFF7
ld a, #0x02
out (c), a
ld bc, #0xDFF7
ld a, #0xF0
out (c), a
; Слив FIFO (tight loop, макс 256 итераций)
ld bc, #0xBFF7
ld e, #0
_pi_drain:
in a, (c)
or a, a
jr z, _pi_done ; 0x00 = пусто
inc a
jr z, _pi_ovf ; 0xFF = overflow
dec e
jr nz, _pi_drain
jr _pi_done
_pi_ovf:
ld bc, #0xDFF7
ld a, #0x0C
out (c), a
ld bc, #0xBFF7
ld a, #0x01
out (c), a
ld bc, #0xDFF7
ld a, #0xF0
out (c), a
_pi_done:
__endasm;
ps2_state = 0;
ps2_last_tick = isr_tick_ctr;
}
/* ── ps2_deinit: слив FIFO перед выходом ──────────────────────────── */
void ps2_deinit(void)
{
__asm
ld bc, #0xDFF7
ld a, #0xF0
out (c), a
ld bc, #0xBFF7
ld e, #0
_pd_loop:
in a, (c)
or a, a
jr z, _pd_done ; 0x00 = пусто
inc a
jr z, _pd_ovf ; 0xFF = overflow
dec e
jr nz, _pd_loop
jr _pd_done
_pd_ovf:
ld bc, #0xDFF7
ld a, #0x0C
out (c), a
ld bc, #0xBFF7
ld a, #0x01
out (c), a
ld bc, #0xDFF7
ld a, #0xF0
out (c), a
_pd_done:
__endasm;
}
/* ── ps2_poll: tight-drain FIFO, rate-limited, полностью asm ──────── */
uint8_t ps2_poll(void) __naked
{
__asm
;; ── Rate limit: ~1 TV-кадр ────────────────────────
ld a, (_isr_tick_ctr)
ld hl, #_ps2_last_tick
ld e, a
sub a, (hl)
cp a, #ISR_TICKS_PER_FRAME
jp c, _pp_none
ld (hl), e
;; ── Подготовка: выбрать FIFO, init регистров ──────
ld bc, #0xDFF7
ld a, #0xF0
out (c), a
ld bc, #0xBFF7 ; BC = порт FIFO на весь цикл
ld a, (_ps2_state)
ld d, a ; D = skip flag (от пред. вызова)
ld e, #0 ; E = результат (KEY_NONE)
;; ── Цикл: читаем FIFO до пустого ─────────────────
_pp_loop:
in a, (c) ; BC = #BFF7
or a, a
jr z, _pp_done ; 0x00 = пусто → выход
inc a
jr z, _pp_ovf ; 0xFF = overflow
dec a ; восстановить байт
bit 0, d
jr nz, _pp_clr_skip ; skip-флаг → пропустить байт
cp a, #0xF0
jr z, _pp_set_skip ; F0 → пропустить след. байт
;; ── Трансляция scan code → KEY_* ──────────────────
cp a, #0x76 ; ESC
jr z, _pp_k_esc
cp a, #0x29 ; SPACE
jr z, _pp_k_next
cp a, #0x4D ; P
jr z, _pp_k_prev
cp a, #0x16 ; 1
jr z, _pp_k_saa1
cp a, #0x1E ; 2
jr z, _pp_k_saa2
jr _pp_loop ; неизвестная → продолжаем слив
_pp_k_esc: ld e, #KEY_ESC
jr _pp_loop
_pp_k_next: ld e, #KEY_NEXT
jr _pp_loop
_pp_k_prev: ld e, #KEY_PREV
jr _pp_loop
_pp_k_saa1: ld e, #KEY_SAA1
jr _pp_loop
_pp_k_saa2: ld e, #KEY_SAA2
jr _pp_loop
_pp_set_skip:
ld d, #1
jr _pp_loop
_pp_clr_skip:
ld d, #0
jr _pp_loop
;; ── Overflow: сброс FIFO ─────────────────────────
_pp_ovf:
ld bc, #0xDFF7
ld a, #0x0C
out (c), a
ld bc, #0xBFF7
ld a, #0x01
out (c), a
ld bc, #0xDFF7
ld a, #0xF0
out (c), a
ld d, #0 ; сбросить skip
;; fall-through
;; ── Готово: сохранить skip, вернуть KEY ───────────
_pp_done:
ld a, d
ld (_ps2_state), a ; skip flag для след. вызова
ld a, e ; A = KEY_* (sdcccall(1))
ret
_pp_none:
xor a, a ; KEY_NONE = 0
ret
__endasm;
}
+2017
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
@echo off
REM ===========================================================================
REM build_lib.bat — Сборка статической библиотеки wc_api.lib
REM
REM Каждая функция WC API — отдельный .rel модуль.
REM Линковщик SDCC подтягивает из .lib только модули с нужными символами.
REM ===========================================================================
setlocal
cd /d "%~dp0"
set ASFLAGS=-plosff
set BUILDDIR=..\..\build\wc_api
set LIBFILE=..\..\build\wc_api.lib
REM --- Папка для .rel файлов ---
if not exist "%BUILDDIR%" mkdir "%BUILDDIR%"
REM --- Сборка всех .s → .rel ---
for %%f in (*.s) do (
sdasz80 %ASFLAGS% "%BUILDDIR%\%%~nf.rel" "%%f" >nul
if errorlevel 1 ( echo FAIL %%f && exit /b 1 )
)
REM --- Удалить старую библиотеку ---
if exist "%LIBFILE%" del "%LIBFILE%"
REM --- Создать .lib архив (Windows cmd не раскрывает *.rel для sdar) ---
setlocal enabledelayedexpansion
set RELFILES=
for %%f in ("%BUILDDIR%\*.rel") do set RELFILES=!RELFILES! "%%f"
sdar -rc "%LIBFILE%" %RELFILES% >nul
if errorlevel 1 ( echo FAIL sdar && exit /b 1 )
endlocal
echo wc_api.lib OK
exit /b 0
+11
View File
@@ -0,0 +1,11 @@
.module wc_adir
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_adir
_wc_adir::
ex af, af'
push ix
ld a, #0x38
call WC_ENTRY
pop ix
ret
+13
View File
@@ -0,0 +1,13 @@
.module wc_chtosep
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_chtosep
_wc_chtosep::
push ix
ld b, d
ld c, e ; BC = bufend (from DE)
ex de, hl ; DE = buf (from HL)
ld a, #0x34
call WC_ENTRY
pop ix
ret
+12
View File
@@ -0,0 +1,12 @@
.module wc_curser
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_curser
_wc_curser::
push ix
push hl
pop ix
ld a, #0x07
call WC_ENTRY
pop ix
ret
+12
View File
@@ -0,0 +1,12 @@
.module wc_cursor
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_cursor
_wc_cursor::
push ix
push hl
pop ix
ld a, #0x06
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_delfl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_delfl
_wc_delfl::
push ix
ld a, #0x4B
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_dmapl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_dmapl
_wc_dmapl::
ex af, af'
push ix
ld a, #0x0D
call WC_ENTRY
pop ix
ret
+15
View File
@@ -0,0 +1,15 @@
.module wc_fentry
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_fentry
_wc_fentry::
push ix
; HL = name (sdcccall(1); WC нужен HL — уже на месте!)
ld a, #0x3B
call WC_ENTRY
ld a, #0
jr z, _wc_fentry_end
inc a
_wc_fentry_end:
pop ix
ret
+22
View File
@@ -0,0 +1,22 @@
.module wc_findnext
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_findnext
_wc_findnext::
push ix
ld ix, #0
add ix, sp
ld a, 4(ix) ; A = flags (from stack)
ex af, af' ; A' = flags
ex de, hl ; DE = entry_buf (SDCC HL → WC DE)
ld a, #0x3A
call WC_ENTRY
ld a, #0
jr z, _wc_findnext_end
inc a
_wc_findnext_end:
pop ix
; callee cleans 1 byte (flags)
pop hl
inc sp
jp (hl)
+25
View File
@@ -0,0 +1,25 @@
.module wc_gadrw
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gadrw
_wc_gadrw::
push ix
push hl ; save win (HL = 1st arg)
ld ix, #0
add ix, sp
ld d, 6(ix) ; D = y
ld e, 7(ix) ; E = x
ld l, 0(ix)
ld h, 1(ix) ; HL = win
push hl
pop ix ; IX = win
ld a, #0x05
call WC_ENTRY ; HL = result addr
ex de, hl ; DE = result (sdcccall(1) uint16)
pop hl ; remove saved win (use HL, keep DE)
pop ix ; restore caller IX
; callee cleans 2 bytes (y+x); preserve DE = return value
pop hl ; HL = return address
inc sp
inc sp ; discard y(1)+x(1)
jp (hl) ; return, DE = result
+10
View File
@@ -0,0 +1,10 @@
.module wc_gdir
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gdir
_wc_gdir::
push ix
ld a, #0x3F
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_gedpl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gedpl
_wc_gedpl::
push ix
ld a, #0x0F
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_gfile
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gfile
_wc_gfile::
push ix
ld a, #0x3E
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_gipagpl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gipagpl
_wc_gipagpl::
push ix
ld a, #0x32
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_gvmod
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gvmod
_wc_gvmod::
ex af, af'
push ix
ld a, #0x42
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_gvsgp
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gvsgp
_wc_gvsgp::
push ix
ld c, a
ld a, #0x47
call WC_ENTRY
pop ix
ret
+17
View File
@@ -0,0 +1,17 @@
.module wc_gvtl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gvtl
_wc_gvtl::
push ix
ld ix, #0
add ix, sp
ld c, 4(ix) ; C = page
ld b, a ; B = plane
ld a, #0x46
call WC_ENTRY
pop ix
; callee cleans 1 byte (page)
pop hl
inc sp
jp (hl)
+11
View File
@@ -0,0 +1,11 @@
.module wc_gvtm
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gvtm
_wc_gvtm::
push ix
ld c, a
ld a, #0x45
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_gxoff
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gxoff
_wc_gxoff::
push ix
ld a, #0x44
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_gyoff
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_gyoff
_wc_gyoff::
push ix
ld a, #0x43
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_int_pl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_int_pl
_wc_int_pl::
ex af, af'
push ix
ld a, #0x56
call WC_ENTRY
pop ix
ret
+13
View File
@@ -0,0 +1,13 @@
.module wc_int_pl_handler
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_int_pl_handler
_wc_int_pl_handler::
push ix
; HL = handler_addr (sdcccall(1); WC тоже нужен HL)
ld a, #0xFF
ex af, af' ; A' = 0xFF
ld a, #0x56
call WC_ENTRY
pop ix
ret
+19
View File
@@ -0,0 +1,19 @@
.module wc_istr
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_istr
_wc_istr::
push ix
ld ix, #0
add ix, sp
ld a, 4(ix) ; A = mode (from stack)
ex af, af' ; A' = mode
push hl
pop ix ; IX = win (HL = 1st arg)
ld a, #0x09
call WC_ENTRY
pop ix
; callee cleans 1 byte (mode)
pop hl
inc sp
jp (hl)
+11
View File
@@ -0,0 +1,11 @@
.module wc_kbscn
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_kbscn
_wc_kbscn::
ex af, af' ; A' = mode
push ix
ld a, #0x2A
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_alt
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_alt
_wc_key_alt::
push ix
ld a, #0x27
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_any
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_any
_wc_key_any::
push ix
ld a, #0x2D
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_bspc
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_bspc
_wc_key_bspc::
push ix
ld a, #0x18
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_caps
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_caps
_wc_key_caps::
push ix
ld a, #0x2C
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_ctrl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_ctrl
_wc_key_ctrl::
push ix
ld a, #0x29
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_del
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_del
_wc_key_del::
push ix
ld a, #0x2B
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_down
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_down
_wc_key_down::
push ix
ld a, #0x12
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_end
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_end
_wc_key_end::
push ix
ld a, #0x1C
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_enter
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_enter
_wc_key_enter::
push ix
ld a, #0x16
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_esc
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_esc
_wc_key_esc::
push ix
ld a, #0x17
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f1
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f1
_wc_key_f1::
push ix
ld a, #0x1D
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f10
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f10
_wc_key_f10::
push ix
ld a, #0x26
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f2
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f2
_wc_key_f2::
push ix
ld a, #0x1E
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f3
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f3
_wc_key_f3::
push ix
ld a, #0x1F
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f4
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f4
_wc_key_f4::
push ix
ld a, #0x20
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f5
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f5
_wc_key_f5::
push ix
ld a, #0x21
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f6
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f6
_wc_key_f6::
push ix
ld a, #0x22
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f7
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f7
_wc_key_f7::
push ix
ld a, #0x23
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f8
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f8
_wc_key_f8::
push ix
ld a, #0x24
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_f9
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_f9
_wc_key_f9::
push ix
ld a, #0x25
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_home
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_home
_wc_key_home::
push ix
ld a, #0x1B
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_ins
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_ins
_wc_key_ins::
push ix
ld a, #0x55
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_left
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_left
_wc_key_left::
push ix
ld a, #0x13
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_pgdn
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_pgdn
_wc_key_pgdn::
push ix
ld a, #0x1A
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_pgup
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_pgup
_wc_key_pgup::
push ix
ld a, #0x19
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_right
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_right
_wc_key_right::
push ix
ld a, #0x14
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_shift
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_shift
_wc_key_shift::
push ix
ld a, #0x28
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_space
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_space
_wc_key_space::
push ix
ld a, #0x10
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_tab
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_tab
_wc_key_tab::
push ix
ld a, #0x15
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_up
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_up
_wc_key_up::
push ix
ld a, #0x11
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_wait_any
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_wait_any
_wc_key_wait_any::
push ix
ld a, #0x2F
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_key_wait_release
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_key_wait_release
_wc_key_wait_release::
push ix
ld a, #0x2E
call WC_ENTRY
pop ix
ret
+16
View File
@@ -0,0 +1,16 @@
.module wc_load256
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_load256
_wc_load256::
push ix
ld ix, #0
add ix, sp
ld b, 4(ix) ; B = blocks (1 byte on stack)
; HL = dest (already in HL)
ld a, #0x3C
call WC_ENTRY
pop ix
pop hl ; HL = return address
inc sp ; discard 1-byte 'blocks' arg
jp (hl) ; return (callee cleans stack arg)
+16
View File
@@ -0,0 +1,16 @@
.module wc_load512
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_load512
_wc_load512::
push ix
ld ix, #0
add ix, sp
ld b, 4(ix) ; B = blocks (1 byte on stack)
; HL = dest (sdcccall(1) first uint16 → HL)
ld a, #0x30
call WC_ENTRY
pop ix
pop hl ; HL = return address
inc sp ; discard 1-byte 'blocks' arg
jp (hl) ; return (callee cleans stack arg)
+11
View File
@@ -0,0 +1,11 @@
.module wc_loadnone
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_loadnone
_wc_loadnone::
push ix
ld b, a
ld a, #0x3D
call WC_ENTRY
pop ix
ret
+34
View File
@@ -0,0 +1,34 @@
.module wc_mezz
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mezz
_wc_mezz::
push ix
push hl ; save win (HL = 1st arg)
ld ix, #0
add ix, sp
ld a, 6(ix) ; A = msg_num
ld l, 7(ix)
ld h, 8(ix) ; HL = str
ld d, 9(ix) ; D = y
ld e, 10(ix) ; E = x
exx
ex af, af' ; A' = msg_num
ld l, 0(ix)
ld h, 1(ix) ; HL = win
push hl
pop ix ; IX = win
exx ; HL=str, D=y, E=x
ld a, #0x0C
call WC_ENTRY
ld a, d ; return: next row
pop hl ; remove saved win
pop ix
; callee cleans 5 bytes
pop hl ; HL = return address
inc sp
inc sp
inc sp
inc sp
inc sp
jp (hl)
+10
View File
@@ -0,0 +1,10 @@
.module wc_mkdir
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mkdir
_wc_mkdir::
push ix
ld a, #0x49
call WC_ENTRY
pop ix
ret
+10
View File
@@ -0,0 +1,10 @@
.module wc_mkfile
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mkfile
_wc_mkfile::
push ix
ld a, #0x48
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_mng0_pl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mng0_pl
_wc_mng0_pl::
ex af, af'
push ix
ld a, #0x4E
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_mng0vpl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mng0vpl
_wc_mng0vpl::
ex af, af'
push ix
ld a, #0x50
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_mng8_pl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mng8_pl
_wc_mng8_pl::
ex af, af'
push ix
ld a, #0x4F
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_mng8vpl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mng8vpl
_wc_mng8vpl::
ex af, af'
push ix
ld a, #0x51
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_mngc_pl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mngc_pl
_wc_mngc_pl::
ex af, af'
push ix
ld a, #0x00
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_mngcvpl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mngcvpl
_wc_mngcvpl::
ex af, af'
push ix
ld a, #0x41
call WC_ENTRY
pop ix
ret
+11
View File
@@ -0,0 +1,11 @@
.module wc_mngv_pl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_mngv_pl
_wc_mngv_pl::
ex af, af'
push ix
ld a, #0x40
call WC_ENTRY
pop ix
ret
+18
View File
@@ -0,0 +1,18 @@
.module wc_nork
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_nork
_wc_nork::
push ix
ld ix, #0
add ix, sp
ld a, 4(ix) ; A = val (from stack)
ex af, af' ; A' = val
; HL = addr (1st arg, already in HL)
ld a, #0x0A
call WC_ENTRY
pop ix
; callee cleans 1 byte (val)
pop hl
inc sp
jp (hl)
+15
View File
@@ -0,0 +1,15 @@
.module wc_prm_pl
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_prm_pl
_wc_prm_pl::
ex af, af'
push ix
ld a, #0x53
call WC_ENTRY
; Z=нет параметра → вернуть 0xFF, иначе A=номер опции
jr nz, _wc_prm_pl_end
ld a, #0xFF
_wc_prm_pl_end:
pop ix
ret
+34
View File
@@ -0,0 +1,34 @@
.module wc_prsrw
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_prsrw
_wc_prsrw::
push ix
push hl ; save win (HL = 1st arg)
push de ; save str (DE = 2nd arg)
ld ix, #0
add ix, sp
ld b, 11(ix) ; B = len_hi
ld c, 10(ix) ; C = len_lo
ld d, 8(ix) ; D = y
ld e, 9(ix) ; E = x
ld l, 0(ix)
ld h, 1(ix) ; HL = str
exx
ld l, 2(ix)
ld h, 3(ix) ; HL = win
push hl
pop ix ; IX = win
exx ; BC=len, DE=y|x, HL=str
ld a, #0x03
call WC_ENTRY
pop de ; remove saved str
pop hl ; remove saved win
pop ix ; restore caller IX
; callee cleans: y(1)+x(1)+len(2) = 4 bytes
pop hl ; HL = return address
inc sp
inc sp
inc sp
inc sp
jp (hl)
+50
View File
@@ -0,0 +1,50 @@
.module wc_prsrw_attr
.area _CODE
WC_ENTRY = 0x6006
.globl _wc_prsrw_attr
_wc_prsrw_attr::
push ix
push hl ; save win (HL = 1st arg)
push de ; save str (DE = 2nd arg)
ld ix, #0
add ix, sp
; ── вызов #03 (prsrw) ──────────────────────────────────
ld b, 11(ix) ; B = len_hi
ld c, 10(ix) ; C = len_lo
ld d, 8(ix) ; D = y
ld e, 9(ix) ; E = x
ld l, 0(ix)
ld h, 1(ix) ; HL = str
exx
ld l, 2(ix)
ld h, 3(ix) ; HL = win
ld a, 12(ix) ; A = attr (IX всё ещё → фрейм)
push af ; сохраняем attr на стеке
push hl
pop ix ; IX = win
exx ; BC=len, DE=y|x, HL=str; HL'=win
ld a, #0x03
call WC_ENTRY
; ── вызов #04 (priat) ──────────────────────────────────
pop af ; A = attr
ex af, af' ; A' = attr
exx ; HL = win (из HL'), BC=len, DE=y|x
push hl
pop ix ; IX = win
exx ; restore BC=len, DE=y|x
ld a, #0x04
call WC_ENTRY
pop de ; remove saved str
pop hl ; remove saved win
pop ix ; restore caller IX
; callee cleans 5 bytes: y(1)+x(1)+len(2)+attr(1)
pop hl ; HL = return address
inc sp
inc sp
inc sp
inc sp
inc sp
jp (hl)

Some files were not shown because too many files have changed in this diff Show More