"""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()