MAD-65
Contents

MAD-65 — Address Decoders#

CPU1 (cpld_cpu1) and GPU (cpld_ctrl) — both ATF1508AS CPLD — v16#


1. General#

The two CPUs decode differently now:

All chip-select signals are active low (/CS).

The LED diagnostic module is decoded by the CPLD on both sides (§4) — CPU1 since v11, the GPU since v15. Neither side needs the 74HC133 the original design called for. On PCB1 the module remains an optional external card; on PCB2 it is populated on the board. Both emit the identical signal, LED_STRB (v16): active high and PHI1-gated, so the module's latch captures valid write data. Through v15 the CPU1 side was a bare /CS_LED decode with no clock term, which cannot drive an edge-triggered latch correctly — see §4.

Shared RAM (ping-pong, 2× HM6116) is mapped at $7800–$7FFF on both CPU1 and GPU sides. Because this address falls within the lower half of the address space (A15=0), the lower RAM chip (CY7C199 #1) must not respond at $7800–$7FFF. Both decoders emit /CS_RAM_LO, which drives the lower RAM /CE pin directly — replacing the previous 74HC04 NOT(A15) inverter. /CS_RAM_LO is asserted for $0000–$77FF only, and deasserted at $7800–$7FFF.

Both ping-pong SRAM chips (SRAM_A and SRAM_B) live on PCB3 (v8). Each board emits a single shared-RAM request — /CS_SRAM_CPU1 from PCB1, /CS_SRAM_GPU from PCB2 — and PCB3's cpld_ppr combines them with SWAP_SEL to generate the physical chip enables /CE_SRAM_A and /CE_SRAM_B.

Upper RAM on the CPU1 side (CY7C199 #2) has its /CE driven by !A15 (chip selected across $8000–$FFFF so that writes always reach RAM), and its /OE driven by !(R/W & RAM_RD_REGION) — so the chip only drives the data bus on a read that is not overlaid by the EPROM, the active cartridge, or the I/O page. Gating /OE rather than /CE is what lets writes still land in RAM under the cartridge window (cart ROM is read-only) — and, in boot mode, under the EPROM, which is what makes the shadow code-RAM work at all. Both terms are internal to cpld_cpu1 (§2); through v10 RAM_RD_REGION was a physical pin between two GALs and R/W had to be AND-ed in by the second one, because the decoder 22V10 had no spare input pin for R/W. The GPU has no upper RAM chip — the entire $8000–$FFFF is occupied by VRAM-image, video register, ROM, shadow code-RAM and VRAM-background.


2. CPLD CPU1 — the whole board's decode and glue (cpld_cpu1)#

Device: ATF1508AS-10JU84 (PLCC84). Single synthesis source: rtl/pcb1/cpld_cpu1.sv. There is no .pld — like the PCB2 and PCB3 CPLDs, this design is written in SystemVerilog and programmed over JTAG (ATMISP + USB-Blaster), not with the TL866/Xgpro DIP flow. PCB1 therefore needs a JTAG header, which it did not when it carried only DIP GALs.

Why this replaced two GALs (v11)#

Through v10, CPU1 used two ATF22V10s: gal_cpu1_decoder and gal_cpu1_ctrl. Both ended up completely full — 10/10 macrocells each, and the decoder also used all 12 of its input pins {1, 2–11, 13} (pin 12 is GND) on 11 address lines + CART_EN. PCB1 had no room left for any further decode change, and several v10 decisions existed only to work around that:

v10 workaround why v11
/CS_ROM lived in the ctrl GAL, RAM_HI_CE_N in the decoder SHADOW_MODE could not reach the decoder (no free pin), and the macrocell count had to be rebalanced both terms simply live next to what uses them
RAM_RD_REGION was deliberately shadow-unaware; the ctrl GAL OR-ed the shadow region back in same — the decoder could not see SHADOW_MODE one clean shadow-aware equation
/CS_BANK was widened to $BF60–$BF7F, then re-split on A4 in the ctrl GAL the decoder had no macrocell for a second output both halves decoded directly
SHADOW_MODE was a one-way strobe latch the ctrl GAL had no data-bus pins a real register bit (§2c)
RDY crossed two GALs in series on the cart path (~10 ns margin; needed a -10PU part) /CS_CART came from the other chip one chip: 30 ns tADS + 10 = 40 ns, ~30 ns margin

The consolidation also absorbed the cartridge bank register (74HC273 + ½ 74HC74) and the ÷4 audio divider (74HC74). PCB1: 14 → 10 ICs. See bom.md.

Pin budget (~46 of ~64 usable)#

PLCC84 = 64 I/O + 4 dedicated inputs, less the 4 taken by JTAG ⇒ ~60 I/O + 4 dedicated. Macrocells: ~35 of 128. Plenty of headroom, which was the point.

Inputs (23):

A15–A7                  — primary decode.  A7 is NEW in v11 (see the $BF map below)
A6, A5, A4              — I/O sub-decode
D7–D0                   — NEW in v11: the bank + shadow registers.  INPUT ONLY —
                          the CPLD never drives the CPU1 data bus
R/W                     — at last a real input (the 22V10 decoder never had one)
PHI2                    — dedicated GCLK1 pin
/RESET                  — dedicated GCLR pin (global async reset)

Outputs (23):

/CS_RAM_LO      — Lower RAM ($0000–$77FF; excludes $7800–$7FFF shared RAM)
/CS_SRAM_CPU1   — Shared RAM request (crosses PCB1→PCB3)
/CS_CART        — Cartridge ($8000–$9FFF, when CART_EN=1)
/CS_ROM         — EPROM ($C000–$FFFF), SHADOW_MODE-gated
/CS_SND1        — SN76489 #1 ($BF00–$BF0F)
/CS_SND2        — SN76489 #2 ($BF10–$BF1F)
/CS_YM          — YM2413 ($BF20–$BF3F)
LED_STRB        — LED module write ($BFC0–$BFDF), v11; v16 made it a PHI1-gated
                  ACTIVE-HIGH strobe (was the bare decode /CS_LED) — see §4
/WE             — NEW in v16: PHI2-qualified write strobe (§3a) — both RAMs,
                  both SN76489, the YM2413 and the PCB3 shared-RAM port
RAM_HI_CE_N     — upper-RAM /CE (= !A15)
OE_RAM_HI_N     — upper-RAM /OE
N_RW            — !R/W: memory /OE
JOY1_OE_N       — port 1 74HC244 /OE ($BF40, A4=0)
JOY2_OE_N       — port 2 74HC244 /OE ($BF50, A4=1)
CART_BANK[6:0]  — NEW in v11: absorbed bank register → cartridge A19–A13
RDY             — CPU RDY: 3 wait states per EPROM/cartridge READ
SHADOW_MODE     — $BF70 bit 0 (pinned out only as a bring-up probe)
CLK_AUDIO       — NEW in v11: absorbed ÷4 divider → SN76489 CLK ×2 + YM2413 XIN

Four signals that used to need pins are now internal wires, because they only ever crossed between the two old GALs and nothing else on the board consumes them: RAM_RD_REGION, /CS_BANK_LO, /CS_JOY, CART_EN.

Decode logic#

; $BF page: $BF00–$BFFF.  A7 is NOT part of the page test — the whole 256-byte
; page must stay excluded from RAM_RD_REGION below.
BF_PAGE = A15 & !A14 & A13 & A12 & A11 & A10 & A9 & A8
IO_LO   = BF_PAGE & !A7                   ; $BF00–$BF7F — the real I/O block (v11)

; Shared RAM: $7800–$7FFF
SRAM_SEL = !A15 & A14 & A13 & A12 & A11

; Overlay regions inside the upper RAM chip's $8000–$FFFF span (active-high terms).
; v11: ROM_SEL carries the SHADOW_MODE gate at its SOURCE.  In v10 the gate could
; only be applied in the second GAL, which is what forced RAM_RD_REGION to stay
; shadow-unaware and needed the OR-back-in hack.
ROM_SEL  = A15 &  A14 & !SHADOW_MODE      ; $C000–$FFFF EPROM
CART_SEL = A15 & !A14 & !A13 & CART_EN    ; $8000–$9FFF active cartridge

/CS_RAM_LO    = !( !A15 & !SRAM_SEL )
/CS_SRAM_CPU1 = !SRAM_SEL
/CS_ROM       = !ROM_SEL
/CS_CART      = !CART_SEL
RAM_HI_CE_N   = !A15

; $BF page sub-decode — all !A7-qualified in v11
/CS_SND1     = !( IO_LO & !A6 & !A5 & !A4 )   ; $BF00–$BF0F
/CS_SND2     = !( IO_LO & !A6 & !A5 &  A4 )   ; $BF10–$BF1F
/CS_YM       = !( IO_LO & !A6 &  A5       )   ; $BF20–$BF3F (A4 → YM2413 A0 on the PCB)
CS_JOY       =    IO_LO &  A6 & !A5           ; $BF40–$BF5F  (internal)
CS_BANK_LO   =    IO_LO &  A6 &  A5 & !A4     ; $BF60–$BF6F  (internal)
SHADOW_SEL   =    IO_LO &  A6 &  A5 &  A4     ; $BF70–$BF7F  (internal)
; LED module: $BFC0–$BFDF, write-only.  ACTIVE HIGH and PHI1-gated (v16, §4)
LED_SEL      = BF_PAGE & A7 & A6 & !A5 & !R/W
LED_STRB     = LED_SEL & !PHI2

; v16: the board's write strobe — every RAM/sound/PCB3 write sink uses this (§3a)
/WE          = !( PHI2 & !R/W )

; R/W-gated glue
N_RW      = !R/W
JOY1_OE_N = !( CS_JOY & R/W & !A4 )
JOY2_OE_N = !( CS_JOY & R/W &  A4 )

; Upper-RAM read region.  /CE stays !A15 so writes always reach RAM — under the
; cartridge window (cart ROM is read-only) and, in boot mode, under the EPROM,
; which is what makes the ROM→shadow copy work at all.
RAM_RD_REGION = A15 & !ROM_SEL & !CART_SEL & !BF_PAGE
OE_RAM_HI_N   = !( R/W & RAM_RD_REGION )

; Wait states: 3 per EPROM/cartridge READ (~239 ns of access time)
SLOW = R/W & (ROM_SEL # CART_SEL)
RDY  = !( SLOW & (WCNT != 3) )

Why SLOW needs the R/W term: neither ROM_SEL nor CART_SEL carries a R/W term, so both assert on writes to their ranges. Those writes land in the upper RAM underneath (the EPROM's /OE is held high by N_RW, so it never drives) and need no wait states. Without the gate every EPROM→shadow copy write would stall 3 cycles for nothing and roughly double the boot copy.

2a. CPU1 Address Decode Table#

Address         A15 A14 A13 A12 A11 A10 A9  A8  A7  Active CS          Register
$0000–$77FF      0   X   X   X   X   X   X   X   X  /CS_RAM_LO         (lower RAM /CE)
$7800–$7FFF      0   1   1   1   1   X   X   X   X  /CS_SRAM_CPU1      (→ PCB3, SRAM_A or _B)
$8000–$9FFF      1   0   0   X   X   X   X   X   X  /CS_CART (CART_EN=1) else RAM (RAM_RD_REGION)
$A000–$BEFF      1   0   1   X   X   X   X   X   X  RAM upper          (/CE=!A15, /OE via RAM_RD_REGION)
$BF00–$BF0F      1   0   1   1   1   1   1   1   0  /CS_SND1           AUDIO_SN1_REG
$BF10–$BF1F      1   0   1   1   1   1   1   1   0  /CS_SND2           AUDIO_SN2_REG
$BF20–$BF2F      1   0   1   1   1   1   1   1   0  /CS_YM  (A4→A0=0)  AUDIO_AY_REG
$BF30–$BF3F      1   0   1   1   1   1   1   1   0  /CS_YM  (A4→A0=1)  AUDIO_AY_DATA
$BF40–$BF5F      1   0   1   1   1   1   1   1   0  /CS_JOY            JOY_REG
$BF60–$BF6F      1   0   1   1   1   1   1   1   0  CS_BANK_LO         CART_BANK
$BF70–$BF7F      1   0   1   1   1   1   1   1   0  SHADOW_SEL         SHADOW_REG (bit 0)
$BF80–$BFBF      1   0   1   1   1   1   1   1   1  (reserved — no CS generated)
$BFC0–$BFDF      1   0   1   1   1   1   1   1   1  LED_STRB (write)   LED_CPU_REG
$BFE0–$BFFF      1   0   1   1   1   1   1   1   1  (reserved — CPU1 side)
$C000–$FFFF      1   1   X   X   X   X   X   X   X  /CS_ROM  (SHADOW_MODE=0) 16 kB OS EPROM
                                                    RAM upper (SHADOW_MODE=1) shadow code-RAM

v11 — the $BF80–$BFFF alias is gone. Through v10, A7 was not a decoder input at all, so $BF80–$BFFF aliased straight onto $BF00–$BF7F. A write anywhere in $BFE0–$BFFF would have clocked the cartridge bank register ($BFE0–$BFEF) or latched SHADOW_MODE ($BFF0–$BFFF), and the LED write at $BFC0 asserted /CS_JOY — harmless only because the joystick /OE also requires R/W=1. No firmware ever hit it, so this was a latent hazard rather than a live bug; qualifying the sub-decode on !A7 removes it and frees $BF80–$BFBF + $BFE0–$BFFF for future use. ($BFE0–$BFFF is deliberately left reserved on this side too, mirroring the GPU's VIDEO_REG.) This is the only intended behavioural difference between the v10 GAL pair and cpld_cpu1; an exhaustive equivalence sweep over all 65536 addresses × R/W × CART_EN × SHADOW_MODE confirms every other output matches bit-for-bit.

2b. CART_BANK ($BF60) — details#

bit 7:    CART_EN   (1=cartridge active, 0=RAM underneath)
bits 6–0: BANK[6:0] — bank number 0–127, window size 8 kB → 1 MB max

Implementation (v11): a register inside cpld_cpu1, clocked by PHI2 on a $BF60–$BF6F
write, async-cleared by /RESET via the CPLD's GCLR.  CART_EN never leaves the chip —
it feeds /CS_CART internally.  BANK[6:0] → cartridge address lines A13–A19.

Was, through v10: bit 7 in a dedicated 74HC74 half + bits 6–0 in a 74HC273, with
CART_EN routed back out to a decoder-GAL input pin.  Both packages are gone.

Decode is $BF60–$BF6F only — a $BF70 write cannot disturb it.

2c. SHADOW_REG ($BF70) — details#

bit 0:    SHADOW_MODE   0 = boot ($C000+ reads the EPROM, writes the shadow)
                        1 = run  ($C000+ reads the shadow at full speed)
bits 7–1: reserved, write 0

Write-only.  Async-cleared to 0 by /RESET — mandatory, the reset vector must
fetch from the EPROM.

v11 made this a real register bit. Through v10 it was a one-way strobe: any write set it, and only /RESET cleared it — because gal_cpu1_ctrl had no data-bus pins, so a strobe-set latch was the only option that added none. The CPLD has D7–D0, so $BF70 behaves like a normal register now, and the EPROM can be re-selected after boot (e.g. to verify the shadow copy).

Only one bit lives here, so firmware needs no zero-page mirror — just write the literal. (The GPU's VIDEO_REG packs four bits and does need a mirror; the v9 work found that a read-modify-write of that mirror can clobber SHADOW_MODE. Do not repeat that pattern here.)

2d. Boot sequence#

0.  RESET → SHADOW_MODE=0, CART_EN=0 (both cleared by the CPLD's GCLR)
0a. CPU1 fetches the reset vector from the EPROM at 3 wait states
0b. OS copies $C000–$FFFF EPROM → upper RAM (the shadow), then writes $01 to $BF70
    → SHADOW_MODE=1.  The copy includes the vectors, so the shadow is byte-identical
    and PC continuity across the switch is seamless.  All later boot work runs at
    full speed.
1.  RAM at $8000–$9FFF (CART_EN=0)
2.  OS clears all RAM including $8000–$9FFF
3.  OS enables bank 0: write $80 to $BF60 → CART_EN=1, BANK=0
4.  OS reads $8000 → checks for a cartridge signature (at 3 wait states — see below)
5a. Signature OK   → cartridge present → jump to $8000
5b. Signature =$00 → no cartridge → write $00 to $BF60 → CART_EN=0
                   → $8000–$9FFF returns as RAM → start demo

Important: RAM must be cleared BEFORE enabling the cartridge (step 2→3).
           If the cartridge is enabled first, $8000–$9FFF cannot be zeroed
           and will contain garbage if the cartridge is later disabled.

2e. Wait states#

3 per EPROM read and per cartridge read (~239 ns of access time). At 14.318 MHz a single-cycle read leaves tACC = tCYC − tADS − tDSR ≈ 30 ns, which no EPROM grade meets.


3. GPU — Address Decoder (in cpld_ctrl)#

Device: ATF1508AS-10JU84 CPLD (cpld_ctrl, rtl/pcb2/cpld_ctrl.sv) — since v13.

v13: this decode was a standalone ATF22V10-15PU GAL (gal_gpu_decoder) through v12. It folded into cpld_ctrl — the chip that already consumed four of its selects and produced the SHADOW_MODE it needed — retiring the last GAL. cpld_ctrl went 37 → ~48/64 I/O. The equations below are unchanged; references to "the GAL" / "GAL #4" / pin numbers are historical.

$C000–$FFFF overlay — ROM / VRAM-background:#

READ  ($C000–$FFFF, R/W=1):  ROM 16 kB (27C256/AT28C256) — GPU firmware
WRITE ($C000–$FFFF, R/W=0):  VRAM-background — background bitmap storage

Distinction by R/W:
  R/W=1 (read)  → /CS_ROM active,     /CS_VRAM_BG inactive
  R/W=0 (write) → /CS_VRAM_BG active, /CS_ROM inactive

Inputs (12 dedicated + 2 repurposed, v5.1):#

A6 and A5 (pins 14, 15) were spare I/O macrocells with no output equation assigned — used here as plain extra inputs. (The now-retired CPU1 decoder GAL used the mirror-image trick, claiming its last spare macrocell as the RAM_RD_REGION output in v5.2a.)

A15, A14, A13           — primary address space division
A12, A11                — Shared RAM at $7800 decoding (with A14, A13)
A10, A9, A8             — $BF page decoding (with A13, A12, A11)
A7, A6, A5              — video register distinction ($BFE0–$BFFF; v5.1,
                           was A7 alone — see decode logic below)
RW                      — bus direction (1=read, 0=write)
SWAP_SEL                — current ping-pong state (registered GAL #4 output, CPU-clock/PHI2 domain)
/CS_SRAM_CPU1           — CPU1 Shared RAM request (from PCB1 interconnect)

Outputs (8 used, 2 spare):#

/CS_RAM_LO      — Lower RAM ($0000–$77FF; excludes $7800–$7FFF Shared RAM)
/CS_VRAM_IMG    — VRAM-image ($8000–$BFDF, GPU write buffer)
/CS_VIDEO_REG   — Video register ($BFE0–$BFFF): BLINDER + BG_REG + COPY_DIS
/CS_ROM         — ROM ($C000–$FFFF, read only, R/W=1)
/CS_VRAM_BG     — VRAM-background ($C000–$FFFF, write only, R/W=0)
/GPU_EN_OUT     — GPU active on VRAM (→ GAL #4 video via inverter)
/CE_SRAM_A      — Chip enable for SRAM_A (routes CPU1 or GPU per SWAP_SEL)
/CE_SRAM_B      — Chip enable for SRAM_B (routes CPU1 or GPU per SWAP_SEL)

GPU has only one RAM chip — /CE driven by GAL /CS_RAM_LO:

CY7C199 #1  /CE = GAL /CS_RAM_LO  → $0000–$77FF  (only RAM chip on GPU side)

The entire upper half $8000–$FFFF is occupied by VRAM-image, video register, ROM and VRAM-background. There is no upper RAM chip for GPU.

Decode logic:#

; GPU's own Shared RAM access: $7800–$7FFF
; A15=0, A14=1, A13=1, A12=1, A11=1
SRAM_SEL_GPU = NOT(A15) AND A14 AND A13 AND A12 AND A11

; CPU1 SRAM active (active high, from /CS_SRAM_CPU1 input)
CPU1_SRAM_ACTIVE = NOT(/CS_SRAM_CPU1)

; $BF page: $BF00–$BFFF
; A15=1, A14=0, A13=1, A12=1, A11=1, A10=1, A9=1, A8=1
BF_PAGE = A15 AND NOT(A14) AND A13 AND A12 AND A11 AND A10 AND A9 AND A8

; Video register: $BFE0–$BFFF (last 32 bytes of $BF page, A7=A6=A5=1)
; v5.1: A6 & A5 added — A7 alone covered the whole $BF80–$BFFF and
; collided with LED_GPU_REG at $BFC0–$BFDF (§4). See "GPU decode
; collision (fixed v5.1)" note below the GPU Address Decode Table.
VIDEO_REG_SEL = BF_PAGE AND A7 AND A6 AND A5

; Upper area $C000–$FFFF: A15=1, A14=1
UPPER_SEL = A15 AND A14

; Lower RAM: $0000–$77FF (A15=0, excluding $7800–$7FFF Shared RAM)
/CS_RAM_LO = NOT( NOT(A15) AND NOT(SRAM_SEL_GPU) )

; SRAM_A: CPU1 owns when SWAP_SEL=0; GPU owns when SWAP_SEL=1
/CE_SRAM_A = NOT( (CPU1_SRAM_ACTIVE AND NOT(SWAP_SEL))
                   OR (SRAM_SEL_GPU AND SWAP_SEL) )

; SRAM_B: CPU1 owns when SWAP_SEL=1; GPU owns when SWAP_SEL=0
/CE_SRAM_B = NOT( (CPU1_SRAM_ACTIVE AND SWAP_SEL)
                   OR (SRAM_SEL_GPU AND NOT(SWAP_SEL)) )

/CS_VIDEO_REG = NOT( VIDEO_REG_SEL )

; LED module: $BFC0–$BFDF, write-only (A7=1, A6=1, A5=0) — v15, see §4
; Disjoint from VIDEO_REG_SEL by A5, so an LED write cannot disturb VIDEO_REG.
; ACTIVE HIGH and PHI1-gated: this is the 74HC574's CLK, not a chip select.  The
; !PHI2 term puts the single rising edge at PHI2's falling edge, with write data
; still valid, and prevents a second edge from the stale-address window (§4).
LED_SEL       = BF_PAGE AND A7 AND A6 AND NOT(A5) AND NOT(RW)
LED_STRB       = LED_SEL AND NOT(PHI2)

; VRAM-image: $8000–$BFDF
/CS_VRAM_IMG  = NOT(
                  A15 AND NOT(A14)       ; $8000–$BFFF
                  AND NOT(VIDEO_REG_SEL) ; exclude video register ($BFE0–$BFFF)
                )

; ROM: $C000–$FFFF on read (R/W=1)
/CS_ROM       = NOT( UPPER_SEL AND RW )

; VRAM-background: $C000–$FFFF on write (R/W=0)
/CS_VRAM_BG   = NOT( UPPER_SEL AND NOT(RW) )

; GPU_EN: active when GPU writes to VRAM-image
/GPU_EN_OUT   = CS_VRAM_IMG   ; active low output → invert → active high to GAL #4

GPU Address Decode Table:#

Address         A15 A14  RW   Active CS          Register / Area
$0000–$77FF      0   X   X    /CS_RAM_LO         (lower RAM /CE)
$7800–$7FFF      0   1   X    /CE_SRAM_A or _B   (selected by SWAP_SEL)
$8000–$BFDF      1   0   X    /CS_VRAM_IMG       VRAM-image (15,000 of 16,352 bytes @ 400×300)
$BFC0–$BFDF      1   0   0    LED_STRB (write)    LED_GPU_REG — v15; also still /CS_VRAM_IMG
                                                 (lands in the image's unused tail, past $BA97)
$BFE0–$BFFF      1   0   X    /CS_VIDEO_REG      VIDEO_REG
$C000–$FFFF      1   1   1    /CS_ROM             GPU ROM (read)
$C000–$FFFF      1   1   0    /CS_VRAM_BG        VRAM-background (write; A14 hardwired VCC via 100Ω)

✅ GPU VIDEO_REG / LED_GPU_REG decode collision — fixed in v5.1#

Was: VIDEO_REG_SEL = BF_PAGE AND A7 constrained only A15–A8 (=$BF) and A7=1. That product term was satisfied by the entire $BF80–$BFFF (128 bytes), not just the intended $BFE0–$BFFF (32 bytes). The LED diagnostic window $BFC0–$BFDF (§4: A7=1, A6=1, A5=0) sat inside that wider span, so /CS_VIDEO_REG fired on every write to LED_GPU_REG, corrupting BG_REG (bit 0) / BLINDER (bit 1) / COPY_DIS (bit 2) with whatever bit pattern was sent to the LEDs — affecting both the boot POST sequence in gpu_os.s and any runtime use of the GPU_LED ($08) command (see MAD65_GPU_OS.md).

Fix (2026-06-16): added A6 and A5 as two new GAL inputs (pins 14, 15 — previously-spare I/O macrocells used as plain inputs) and narrowed the term to VIDEO_REG_SEL = BF_PAGE AND A7 AND A6 AND A5, matching $BFE0–$BFFF exactly. $BFC0–$BFDF now correctly falls back to /CS_VRAM_IMG (harmless VRAM overlay, same pattern as CPU1's upper RAM) as originally documented. Updated in lockstep: gal/MAD65_GPU_DECODER.pld (now v5.1) and rtl/pcb2/gal_gpu_decoder.sv, per CLAUDE.md "Key Design Constraints". CPU1 was never affected (at the time LED_CPU_REG was decoded entirely off-GAL by an external 74HC133, with no shared product term; since v11 it comes from cpld_cpu1, still on its own product term).

GPU LED emulation in the simulator (window-title display) can now be safely added on top of this without the decode collision corrupting VIDEO_REG.

SWAP_SEL routing:#

SWAP_SEL=0:                         SWAP_SEL=1:
  /CE_SRAM_A active when CPU1_REQ     /CE_SRAM_A active when GPU_REQ
  /CE_SRAM_B active when GPU_REQ      /CE_SRAM_B active when CPU1_REQ

At any given time, exactly one chip is selected per access. Neither chip is ever selected by both CPUs simultaneously — exclusive access is guaranteed.

VIDEO_REG ($BFE0–$BFFF) — details:#

74HC374 latch — written by GPU, outputs connected directly to GAL #3 of video circuit

bit 0: BG_REG   — background colour
         0 → bitmap=0 pixels at 0.00 V (black)
         1 → bitmap=0 pixels at 0.18 V (dark grey)
         → output to GAL #3 (video circuit)

bit 1: BLINDER  — pixel blinder
         0 → bitmap=1 pixels at 0.70 V (white, visible)
         1 → bitmap=1 pixels at 0.00 V (hidden, forced black)
         → output to GAL #3 (video circuit)

bit 2: COPY_DIS — hardware background copy disable
         0 → WE_COPY active: VRAM-background copied to VRAM-image at each V[0]=1
         1 → WE_COPY inhibited: VRAM-image frozen (video circuit does not write it)
         → output to GAL #4 (video circuit)

bits 7–3: not used

GPU writes to any address in $BFE0–$BFFF (74HC374 ignores lower address bits A4:0). Convention: use $BFE0 as canonical address.

ROM / VRAM-background overlay note:#

Two physically separate chips share the same address range $C000–$FFFF. R/W signal determines which one responds: - On read: ROM drives the data bus, VRAM-background /CS is inactive - On write: ROM ignores the write (/WE tied to VCC), VRAM-background latches data

No bus conflict — ROM /OE must be connected directly to R/W (R/W=1 → /OE active → ROM drives bus). Never connect ROM /OE to GND — this would cause bus contention when GPU writes to VRAM-background (R/W=0).


3a. Write strobes — /WE = !(PHI2 & !R/W) (v16, both boards)#

Every write strobe in MAD-65 is qualified with PHI2. Each board CPLD emits one /WE, used by all of that board's write sinks:

/WE = !( PHI2 & !R/W )        ; cpld_cpu1 (PCB1) and cpld_ctrl (PCB2)
board consumers of /WE
PCB1 lower RAM, upper RAM, SN76489 ×2, YM2413, and the /WE sent to PCB3
PCB2 lower RAM, shadow code-RAM, the /WE sent to PCB3, and — folded into GPU_WE_IMG / GPU_WE_BG — all four VRAM chips
PCB3 none of its own: cpld_ppr receives an already-qualified /WE from each board and only muxes it, staying a pure crossbar

WE_COPY is deliberately exempt. It is the background→image copy strobe generated by cpld_video in the 20 MHz pixel-clock domain and has nothing to do with the CPU bus.

⚠ Why this is required — through v15 it was missing everywhere#

Through v15 every /WE in the system was raw R/W, with no PHI2 term. That is a real hardware defect, not a stylistic point:

The simulator never showed this because rtl/mem/sram_io.sv models the cell as transparent (level-sensitive) rather than edge-committing — a workaround that masked the defect instead of reproducing it. Its header comment even described the back-to-back-push scenario as the reason for the workaround. Qualifying /WE with PHI2 removes the need for that reasoning entirely: each write cycle now gets its own clean pulse, committing on PHI2's falling edge with address, R/W and data all still valid.

sim/vtest_v11.cpp asserts the invariant directly — /WE is never asserted while PHI2 is low, at the RAM pins of all three boards — plus a companion check that writes still occur, so the test cannot pass vacuously.


4. LED Diagnostic Module (optional; decoded differently per side)#

GPU side: until GAL v5.1, LED_GPU_REG writes aliased into VIDEO_REG and corrupted BG_REG/BLINDER/COPY_DIS — see the "decode collision — fixed in v5.1" callout under §3's GPU Address Decode Table. CPU1 side was never affected: its $BFC0 write aliased onto /CS_JOY, which is harmless because the joystick /OE also requires R/W=1. That alias is gone anyway since v11 decodes A7 (§2a).

Registers: LED_CPU_REG (CPU1 side) / LED_GPU_REG (GPU side) — Address: $BFC0–$BFDF#

The window is 32 bytes: A3–A0 are not decoded (the latch ignores them) and neither is A4.

Address decoding — CPU1 side (v11: from the CPLD)#

cpld_cpu1 emits LED_STRB directly (§2), so the CPU1 module no longer needs its 74HC133 — it is down to a single 74HC374 + LEDs:

LED_SEL  = BF_PAGE & A7 & A6 & !A5 & !R/W          ; $BFC0–$BFDF, write-only
LED_STRB = LED_SEL & !PHI2                          ; -> 74HC374 CLK (rising edge latches)

Address decoding — GPU side (v15: from the CPLD)#

cpld_ctrl emits the strobe directly (§3). Note it is active high and PHI1-gated — it is a clock for the '574, not a chip select:

LED_SEL = BF_PAGE & A7 & A6 & !A5 & !R/W          ; $BFC0–$BFDF, write-only
LED_STRB = LED_SEL & !PHI2                          ; -> 74HC574 CLK (rising edge latches)

⚠ Why LED_STRB carries a clock term — and why a plain /CS_LED does not work#

This is the same rule as /WE (§3a), not a special case. On a 65xx bus a write cycle begins when PHI2 falls: address and R/W go valid tADS (~30 ns) after that, write data tMDS (~40 ns) after it, and all are released only ~10 ns (tAH/tDHW) after the next falling edge. A bare decode is mis-timed in either polarity, because both of its edges sit outside that window:

form its rising edge why that is wrong
active low (/CS_LED, the v11 form) the DEassertion, tADS into the next cycle ~20 ns after the CPU released the data — clocks a floating bus
active high (bare LED_SEL) the assertion, tADS after PHI2 falls ~10 ns before the write data is valid (tMDS) — clocks the previous cycle's

Measured with the active-low form: the latch captured $A9 (LDA #), the following opcode fetch. A clock term pulls the commit edge onto PHI2's falling edge — the one instant where address, R/W and write data are all simultaneously valid.

Careful when re-testing: an active-high bare decode still looks correct in this repo's simulator, because rtl/cpu/w65c02.sv:32 moves address, R/W and data together on posedge phi2, so data is already valid when the strobe asserts. Real silicon has no such luxury. The model-independent check is the one vtest makes: the strobe must never be high while PHI2 is high.

PHI1 vs PHI2 gating is immaterial in hardware — both put the edge on PHI2's falling edge, and neither glitches, because address/R\W settle tADS after PHI2 falls, long before the next PHI2 rise. PHI1 is used because it is also correct under this repo's posedge-based CPU model (rtl/cpu/w65c02.sv:32 clocks the address register, and the core clocks R/W and write data, all on posedge phi2). The active-low PHI2-gated form — !(LED_SEL & PHI2), i.e. exactly the shape of /WE, and the one that would justify an _n suffix — was measured re-clocking the '574 a second time at that posedge, leaving $A9/$58 in the latch instead of the POST pattern. That double-capture is a model artifact, not a hardware hazard — but it is why LED_STRB is active high rather than LED_STRB_N.

'574 vs '573 is also immaterial. An edge-triggered flip-flop and a transparent latch both work with a correct strobe and both fail with a bare decode. The '574 was chosen for its pinout.

With the gate, the GPU boot POST latches $01 → $03 → $07 → $0F and nothing else.

v16 — both boards now use the identical term. cpld_cpu1 emitted a bare /CS_LED through v15 and had exactly this defect; it never bit because PCB1's LED module is an optional external card, not modelled in RTL and evidently never built. It is now LED_STRB = LED_SEL & !PHI2, the same as the GPU side. make vtest asserts the invariant on both boards: the strobe is never high while PHI2 is high, and there is exactly one rising edge per $BFC0–$BFDF write.

v15 — the GPU 74HC133 is retired before it was ever built. This side used to keep a discrete decoder because "the GPU's 22V10 has no macrocell to spare": the full 16-bit compare needed 13 inputs (A15, !A14, A13–A8, A7, A6, !A5, !A4, !R/W), which meant a 74HC133 13-input NAND or two cascaded 74HC21. That constraint died with v13, which folded gal_gpu_decoder into cpld_ctrla15..a5 and the BF_PAGE term were already on that chip, so the decode costs one product term and one output pin (~50 → ~51/64). Both sides now match bit for bit, which also settles the old !A4 discrepancy: the discrete version covered only $BFC0–$BFCF, the CPLD term covers the documented 32-byte $BFC0–$BFDF.

$BFC0–$BFDF still selects /CS_VRAM_IMG as well, so an LED write also lands in VRAM-image. That is harmless — the framebuffer is 15,000 bytes and ends at $BA97, so the write goes into the unused tail — and it is the same overlay CPU1 has, where the LED write also lands in upper RAM.

Module schematic:#

CPU ──[data bus D7–D0]──► '374 / '574  (CLK = the write strobe, /OE tied low)
                               │
                         Q7–Q0 ──► 8× 330Ω resistor ──► 8× LED ──► GND

CPU1 side: LED_STRB from cpld_cpu1  ──► CLK of a 74HC374  (optional external card)
GPU  side: LED_STRB from cpld_ctrl  ──► CLK of a 74HC574  (on-board, PCB2)

The latch closes on the rising edge of its CLK. Placing that edge correctly is the whole difficulty — see the PHI1 warning under Address decoding — GPU side.

The GPU side uses a 74HC574 rather than a '374. They are functionally identical octal D flip-flops; the '574 simply puts all eight D inputs on one side of the package and all eight Q outputs on the other, which routes far more cleanly from the data bus straight out to the LED resistor array. PCB1's module is an optional external card and keeps its '374.

Example POST usage in ROM (pseudocode):#

; POST stages indicated by LED
LDA #%00000001 : STA $BFC0  ; LED0 — power on / start
LDA #%00000011 : STA $BFC0  ; LED1 — RAM initialisation
LDA #%00000111 : STA $BFC0  ; LED2 — Shared RAM test
LDA #%00001111 : STA $BFC0  ; LED3 — VRAM test
LDA #%00011111 : STA $BFC0  ; LED4 — cartridge check
LDA #%11111111 : STA $BFC0  ; all LEDs — system ready

5. Summary — Decoder Components#

Component CPU1 GPU Total
ATF1508AS CPLD (decode + glue) 1 (cpld_cpu1, ~46/64 pins, ~35/128 macrocells) 1
GAL ATF22V10 (decoder) 1 (8/10 outputs used) 1
ATF1508AS CPLD (video) 2 (all video logic — video-circuit doc) 2
CY7C199-15PC (RAM) 2 (lower+upper) 2 (lower + shadow) 4
CY7C199-15PC (VRAM) 4 (image A/B + bg A/B) 4
→ same chip for all ← unified component → 8× CY7C199 total 8
2 kB SRAM 2 (SRAM_A + SRAM_B, on PCB3) 2
27C256 / AT28C256 1 (CPU1 ROM 16 kB) 1 (GPU ROM 16 kB) 2
74HC74 (CART_EN FF) ~~1 (½ package)~~ 0 0
74HC273 (bank reg) ~~1~~ 0 0
LED module decode 0 (LED_STRB from the CPLD) 0 (LED_STRB from the CPLD) 0
1× 74HC374 (optional external) 1× 74HC574 (on-board, PCB2) 2
8× LED + 8× 330Ω 8× LED + 8× 330Ω

v11: CPU1's two ATF22V10s, the 74HC273 bank register, the CART_EN 74HC74 and the LED module's 74HC133 all collapsed into cpld_cpu1. See bom.md for the per-board totals (PCB1: 14 → 10 ICs).

Note: IDT7132 (v4) is removed. The GPU VRAM-SEL 74HC74 is gone — the SEL double-buffer flip-flop is inside the video CPLD (v6). Since v8 the two ping-pong SRAMs and their transceivers live on PCB3; each CPU board just emits a shared-RAM request, and PCB3's cpld_ppr does the routing (architecture doc §6).


6. Open Issues#

  1. CART_EN on reset — ~~(74HC74 FF must power up in state 0; connect its /CLR to /RESET)~~ RESOLVED in v11. There is no 74HC74 any more: the bank register lives inside cpld_cpu1 and is async-cleared by the CPLD's dedicated GCLR pin, so CART_EN=0 (cartridge disabled) at power-on by construction. Verified in RTL (cd sim && make vtest).

  2. SWAP_SEL on reset — SWAP_SEL is a registered GAL #4 output; its async reset is tied to system /RESET → SWAP_SEL=0 at power-on: CPU1 owns SRAM_A, GPU owns SRAM_B. (v5.0-hw: SWAP_SEL moved from a 74HC74 clocked by a 74HC123 monostable into GAL #4, clocked by the 14.318 MHz CPU clock so the swap is synchronous — see the video doc.)

  3. /CS_VRAM_IMG → GPU_EN — the /CS_VRAM_IMG output from GPU GAL must be inverted (via 74HC04) and fed as GPU_EN to GAL #4 of the video circuit. GAL #4 uses active-high GPU_EN to arbitrate VRAM access.

  4. ROM /OE wiring — connect ROM /OE directly to R/W line: R/W=1 (read) → /OE=1 → ROM drives bus ✓ R/W=0 (write) → /OE=0 → ROM silent ✓ Do NOT connect /OE to GND — bus conflict on VRAM-background writes.

  5. GAL propagation delay — ATF22V10-15PU: tpd = 15 ns max. At 14.318 MHz (cycle = 69.8 ns) there is ~54 ns margin after GAL settles. No issues expected at this clock speed.

  6. Lower RAM /CE routing — both decoders (CPU1's CPLD and the GPU's GAL) provide /CS_RAM_LO to drive CY7C199 #1 /CE directly. Verify PCB routing connects CY7C199 #1 /CE to that pin.

  7. CPU1 data bus buffer (PCB2) — 74HC245 on PCB2 isolates CPU1's data bus (from interconnect) from PCB2's internal bus during GPU SRAM access. OE: driven by the OR of /CE_SRAM_A and /CE_SRAM_B for CPU1 accesses (i.e. when CPU1_SRAM_ACTIVE=1). DIR: CPU1 R/W (1=read → SRAM drives towards CPU1, 0=write → CPU1 drives SRAM).

  8. Inter-board connectors — ~~(PCB1↔PCB2 interconnect additions, v5)~~ Restructured in v8. There is no PCB1↔PCB2 interconnect any more: the ping-pong SRAMs moved to PCB3, and PCB1 and PCB2 each cable into their own PCB3 port. Each port carries that CPU's A10:0, D7:0 (bidirectional), R/W and its single shared-RAM request — /CS_SRAM_CPU1 from cpld_cpu1, or /CS_SRAM_GPU from cpld_ctrl (v13; was gal_gpu_decoder). clk_cpu, rst_n and the frame /IRQ are shared across all three boards. v14: that /IRQ is generated on PCB2cpld_ctrl synchronises its own VSYNC into the CPU clock domain — and is consumed by CPU1, CPU2 and cpld_ppr's SWAP_SEL edge detector alike. Raw VSYNC is no longer a backplane signal at all, and PCB3 no longer returns anything on this pin. SWAP_SEL stays inside cpld_ppr and needs no connector pin. (The v5 note this replaces listed A12:A0 and described the lines as "already present for IDT7132 port A" — the IDT7132 dual-port RAM was removed in v5, and only A10:0 is needed for a 2 kB window.)

  9. Upper RAM /OE gating (CPU1) — ~~(the GAL emits an address-only RAM_RD_REGION; a second chip AND-s in R/W, because the decoder 22V10 has no spare input pin for it)~~ RESOLVED in v11. The underlying requirement stands and always will: CY7C199 #2 has /CE = !A15, so it is selected across the whole $8000–$FFFF half, and on a read inside an overlay region (EPROM $C000–$FFFF, active cartridge $8000–$9FFF, or the I/O page $BF00–$BFFF) it would contend with the overlay device unless /OE is gated. /CE must stay !A15 so writes still reach RAM under the cartridge window (cart ROM is read-only), across $A000–$BEFF, and — since v10 — under the EPROM, which is what makes the shadow copy work.

What was an issue was the contortion the 22V10's pin famine forced: R/W could not be a decoder input (all 12 pins were used by 11 address lines + CART_EN), so the decoder emitted an address-only RAM_RD_REGION on a physical pin and gal_cpu1_ctrl AND-ed R/W in — and once v10 added shadow mode, that second GAL also had to OR the shadow region back in, because RAM_RD_REGION could not be made shadow-aware at its source: /OE_RAM_HI = !( R/W & ( RAM_RD_REGION # (A15 & A14 & SHADOW_MODE) ) ).

cpld_cpu1 has R/W, A7 and D7–D0 to spare, so RAM_RD_REGION is an internal wire with the clean equation again, shadow-aware for free because ROM_SEL carries the gate at its source: RAM_RD_REGION = A15 & !ROM_SEL & !CART_SEL & !BF_PAGE ; ROM_SEL = A15 & A14 & !SHADOW_MODE OE_RAM_HI_N = !( R/W & RAM_RD_REGION ) The two forms are equivalent for every input combination — confirmed by an exhaustive sweep over all 65536 addresses × R/W × CART_EN × SHADOW_MODE, not by inspection (§2a).

  1. CPU1 GAL input pin map vs. real 22V10 — ~~(pre-existing, flagged)~~ FIXED in decoder v6.0 (system v10); moot since v11. The .pld assigned the 12 decode inputs to PINs 2–13, but on a real ATF22V10 DIP-24 pin 12 is GND (and pin 24 is VCC). v6.0 moved the 12 inputs onto the real input pins {1, 2–11, 13}: PIN 1 = A15, PINs 2–11 = A14…A4, PIN 13 = CART_EN. No equation changed; the footprint did.

    That correction is what proved there was no free input pin for R/W (Open Issue 9) or for SHADOW_MODE, which forced the awkward v10 split — /CS_ROM out of the decoder into gal_cpu1_ctrl (the only chip that could see SHADOW_MODE) and RAM_HI_CE_N the other way to rebalance. Worth recording as a lesson: v10 was very nearly built against the "PIN 1 is spare" claim, i.e. against a pinout that cannot physically exist. Both GALs are gone in v11, so the constraint no longer binds CPU1 — but it still applies to the GPU's 22V10 (§3), which has 8/10 outputs and some margin.


Note: from here down, changelog entries use the old per-document revision numbers (v4.x–v6.0). Since v11 this doc tracks the system revision instead — the two schemes had drifted apart and the doc-revision numbers were colliding confusingly with the system's (doc "v7.0" vs system v7, which were unrelated changes). The entry below was written as doc-v7.0.

System v11 — changes from doc-v6.0: — CPU1's two ATF22V10 GALs replaced by ONE ATF1508AS-10JU84 CPLD, cpld_cpu1. §2 and §2b * merged into a single §2; gal/MAD65_CPU1_DECODER.pld and gal/MAD65_CPU1_CTRL.pld deleted — * rtl/pcb1/cpld_cpu1.sv is the single synthesis source, programmed over JTAG (PCB1 now needs * a JTAG header). The CPLD also absorbed the cartridge bank register (74HC273 + ½ 74HC74), the * ÷4 audio divider (74HC74) and the LED module's 74HC133. PCB1: 14 → 10 ICs. — Every v10 pin-famine workaround undone: /CS_ROM and RAM_HI_CE_N sit where they belong; * RAM_RD_REGION is shadow-aware and internal; /CS_BANK is no longer widened-then-re-split. — A7 is decoded at last: $BF80–$BFFF no longer aliases onto $BF00–$BF7F. This is the ONLY * intended behavioural change vs the v10 GAL pair — verified by an exhaustive equivalence sweep * (all 65536 addresses × R/W × CART_EN × SHADOW_MODE). $BF70 SHADOW_REG is a real register bit (bit 0), not a one-way strobe; the EPROM can be * re-selected after boot. Firmware must now write the value, not just any byte. — The u_ctrl speed-grade warning is gone: RDY no longer crosses two chips (~10 ns → ~30 ns * of margin). Open Issues 1, 9 and 10 marked resolved.*

Document v5.2b (EN) — changes from v5.2a: — SWAP_SEL is now a registered output of video GAL #4, clocked by the 14.318 MHz * CPU clock (= PHI2). The swap is synchronous and lands between bus cycles. — Summary table: 74HC123 monostable removed; the SWAP_SEL 74HC74 half removed * (VRAM-SEL 74HC74 now has a spare half). GPU GAL logic unchanged.

Document v5.2a (EN) — changes from v5.2: — CPU1 upper RAM (CY7C199 #2): /OE now gated to prevent bus contention on * overlay reads. New active-high GAL output RAM_RD_REGION (PIN 16); one * external 74HC00 gate forms /OE_RAM_HI = !(RAM_RD_REGION & R/W). /CE = A15 * unchanged so writes still reach RAM in the cart window. — Noted R/W cannot be a GAL input (all 12 input pins used) — Open Issues 9–10. — CPU1 GAL: 10/10 outputs used.*

Document v5.2 (EN) — changes from v5.1: — CPU1 ROM relocated/enlarged to $C000–$FFFF (16 kB): /CS_ROM = A15·A14. — CPU1 cartridge window relocated to $8000–$9FFF (8 kB): /CS_CART = A15·!A14·!A13·CART_EN. — CPU1 upper RAM now $A000–$BEFF; I/O page unchanged at $BF00–$BFFF. — CPU1 ROM part 27C64/AT28C64 → 27C256/AT28C256 (same ROM part as GPU board). — GPU GAL unchanged.

Document v5.1 (EN) — changes from v5.0: — CPU1: SN76489 #3 and #4 removed. YM2413 (OPLL) added at $BF20–$BF3F. — /CS_SND3 and /CS_SND4 outputs merged into single /CS_YM (PIN 17); PIN 16 spare. — YM2413 A0 pin wired directly to CPU A4 on PCB — no additional GAL output required. — CPU1 GAL: 9/10 outputs used (was 10/10). GPU GAL unchanged. — Audio output changed to stereo: SN76489 #1+#2 mixed to centre; YM2413 on L/R rails.

Document v5.0 (EN) — changes from v4.2: — IDT7132 replaced by ping-pong scheme; /CS_SRAM_CPU1 signal added to PCB interconnect. — GPU GAL: /CS_SRAM replaced by /CE_SRAM_A and /CE_SRAM_B; SWAP_SEL and * /CS_SRAM_CPU1 added as inputs (all 12 GAL inputs now used); 8/10 outputs used. — CPU1 GAL: /CS_SRAM renamed /CS_SRAM_CPU1 (logic unchanged, 10/10 outputs). — Summary table updated: IDT7132 removed; HM6116 ×2 and 74HC123 ×1 added. — Open issues updated: SWAP_SEL reset, CPU1 data bus buffer, interconnect pinout.*

Document v4.2 (EN) — changes from v4.1: — Shared RAM moved from $8000–$87FF to $7800–$7FFF on both CPU1 and GPU sides.