114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""Generates the ColecoVision/Adam cartridge viewer (Z80 machine code).
|
|
|
|
Sets the TMS9918A to Graphics Mode 2, builds the name table, copies the
|
|
6144-byte pattern from cartridge ROM to VRAM >0000, and expands the 768 per-cell
|
|
colour bytes x8 into the 6144-byte colour table at VRAM >2000 -- the same picture
|
|
the TI-99 viewer makes, but in Z80 with the ColecoVision VDP ports.
|
|
|
|
display: forever (hold), key (poll the controller's fire button then reset),
|
|
seconds (count VDP frame flags then reset). A cartridge has no OS to return to,
|
|
so key/seconds reset the machine (JP 0), which re-runs the cart and re-displays.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .z80 import Asm
|
|
|
|
VDP_DATA = 0xBE # data port (VRAM, auto-increment)
|
|
VDP_CTRL = 0xBF # control port (address / register / status)
|
|
VDP_REGS = [0x02, 0xC0, 0x0E, 0xFF, 0x03, 0x36, 0x07, 0x01] # Graphics Mode 2
|
|
PATTERN_BYTES = 6144
|
|
NCELLS = 768
|
|
RATE = 60 # NTSC frames/second
|
|
JOY_SEG = 0xC0 # OUT (>C0) selects the joystick / left-fire segment
|
|
JOY_PORT = 0xFC # IN (>FC) reads controller 1
|
|
SOUND_PORT = 0xFF # SN76489 sound chip
|
|
# SN76489 "attenuation = 15 (off)" latch byte for each of the 4 channels.
|
|
SOUND_OFF = [0x9F, 0xBF, 0xDF, 0xFF]
|
|
|
|
|
|
def _set_write_addr(a: Asm, addr: int):
|
|
a.ld_a(addr & 0xFF); a.out_n_a(VDP_CTRL)
|
|
a.ld_a(((addr >> 8) & 0x3F) | 0x40); a.out_n_a(VDP_CTRL)
|
|
|
|
|
|
def build(code_base: int, data_addr: int, display: str = "forever",
|
|
seconds: int = 0) -> bytes:
|
|
a = Asm(code_base)
|
|
a.di()
|
|
|
|
# ---- silence the SN76489 (our >55AA header skips the BIOS sound init, so the
|
|
# chip powers up holding a tone); set all 4 channels to attenuation off ----
|
|
for v in SOUND_OFF:
|
|
a.ld_a(v)
|
|
a.out_n_a(SOUND_PORT)
|
|
|
|
# ---- programme the 8 VDP registers ----
|
|
for reg, val in enumerate(VDP_REGS):
|
|
a.ld_a(val); a.out_n_a(VDP_CTRL)
|
|
a.ld_a(0x80 | reg); a.out_n_a(VDP_CTRL)
|
|
|
|
# ---- name table >3800 = 0,1,...,255 repeated 3 times (768 bytes) ----
|
|
_set_write_addr(a, 0x3800)
|
|
a.ld_a(0)
|
|
a.ld_c(3)
|
|
a.label("name_o")
|
|
a.ld_b(0) # B=0 -> DJNZ runs 256 times
|
|
a.label("name_i")
|
|
a.out_n_a(VDP_DATA)
|
|
a.inc_a()
|
|
a.djnz("name_i")
|
|
a.dec_c()
|
|
a.jr_nz("name_o")
|
|
|
|
# ---- pattern table >0000 = 6144 bytes from ROM (24 * 256) ----
|
|
_set_write_addr(a, 0x0000)
|
|
a.ld_hl(data_addr)
|
|
a.ld_c(PATTERN_BYTES // 256)
|
|
a.label("pat_o")
|
|
a.ld_b(0)
|
|
a.label("pat_i")
|
|
a.ld_a_hl()
|
|
a.out_n_a(VDP_DATA)
|
|
a.inc_hl()
|
|
a.djnz("pat_i")
|
|
a.dec_c()
|
|
a.jr_nz("pat_o")
|
|
|
|
# ---- colour table >2000 = each cell colour written 8x (768 -> 6144) ----
|
|
_set_write_addr(a, 0x2000)
|
|
a.ld_hl(data_addr + PATTERN_BYTES)
|
|
a.ld_c(NCELLS // 256)
|
|
a.label("col_o")
|
|
a.ld_b(0)
|
|
a.label("col_i")
|
|
a.ld_a_hl()
|
|
for _ in range(8):
|
|
a.out_n_a(VDP_DATA)
|
|
a.inc_hl()
|
|
a.djnz("col_i")
|
|
a.dec_c()
|
|
a.jr_nz("col_o")
|
|
|
|
# ---- hold the picture ----
|
|
if display == "key":
|
|
a.label("kwait")
|
|
a.ld_a(0); a.out_n_a(JOY_SEG) # select joystick / left-fire segment
|
|
a.in_a_n(JOY_PORT)
|
|
a.and_n(0x40) # bit 6 = fire button (active low)
|
|
a.jr_nz("kwait") # still high -> not pressed
|
|
a.jp(0x0000) # reset -> re-display
|
|
elif display == "seconds":
|
|
a.ld_de(max(1, min(0xFFFF, int(seconds) * RATE)))
|
|
a.label("swait")
|
|
a.in_a_n(VDP_CTRL) # read VDP status (clears frame flag)
|
|
a.and_n(0x80)
|
|
a.jr_z("swait") # no new frame yet
|
|
a.dec_de()
|
|
a.ld_a_d(); a.or_e()
|
|
a.jr_nz("swait")
|
|
a.jp(0x0000) # reset -> re-display
|
|
else: # forever
|
|
a.label("hang")
|
|
a.jr("hang")
|
|
return a.resolve()
|