cpld_cpu1) and GPU (cpld_ctrl) — both ATF1508AS CPLD — v16#The two CPUs decode differently now:
cpld_cpu1 (§2). Since v11 it holds
the entire board's decode and glue: address decode, the R/W-gated control terms, the
SHADOW_MODE register, the wait-state generator, the cartridge bank register and the ÷4 audio
clock. It replaced two ATF22V10 GALs (gal_cpu1_decoder + gal_cpu1_ctrl, both completely
full) plus a 74HC273 and two 74HC74s.cpld_ctrl
ATF1508AS CPLD (§3), folded in from the retired gal_gpu_decoder ATF22V10. The CPU2 address
bus (a15..a5) enters cpld_ctrl, which emits the memory/PCB3 chip selects directly and
consumes the rest internally. The equations in §3 are unchanged; only their housing moved.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.
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.
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.
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.
; $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.
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–$BFFFalias is gone. Through v10, A7 was not a decoder input at all, so$BF80–$BFFFaliased straight onto$BF00–$BF7F. A write anywhere in$BFE0–$BFFFwould have clocked the cartridge bank register ($BFE0–$BFEF) or latched SHADOW_MODE ($BFF0–$BFFF), and the LED write at$BFC0asserted/CS_JOY— harmless only because the joystick/OEalso requiresR/W=1. No firmware ever hit it, so this was a latent hazard rather than a live bug; qualifying the sub-decode on!A7removes it and frees$BF80–$BFBF+$BFE0–$BFFFfor future use. ($BFE0–$BFFFis 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 andcpld_cpu1; an exhaustive equivalence sweep over all 65536 addresses × R/W × CART_EN × SHADOW_MODE confirms every other output matches bit-for-bit.
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.
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.)
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.
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.
/CS_ROM is SHADOW_MODE-gated, so the EPROM is only read
during the boot copy. Cost is boot-time only.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 intocpld_ctrl— the chip that already consumed four of its selects and produced theSHADOW_MODEit needed — retiring the last GAL.cpld_ctrlwent 37 → ~48/64 I/O. The equations below are unchanged; references to "the GAL" / "GAL #4" / pin numbers are historical.
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
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)
/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.
; 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
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_REGdecode collision — fixed in v5.1#Was:
VIDEO_REG_SEL = BF_PAGE AND A7constrained 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_REGfired on every write toLED_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 ingpu_os.sand any runtime use of theGPU_LED($08) command (seeMAD65_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–$BFDFnow 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) andrtl/pcb2/gal_gpu_decoder.sv, perCLAUDE.md"Key Design Constraints". CPU1 was never affected (at the timeLED_CPU_REGwas decoded entirely off-GAL by an external 74HC133, with no shared product term; since v11 it comes fromcpld_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=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.
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.
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).
/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
/WEin the system was raw R/W, with no PHI2 term. That is a real hardware defect, not a stylistic point:
- An async SRAM commits the cell at the end of the write pulse, storing whatever is on the bus when
/WErises. With raw R/W that edge landstADS(~30 ns) after PHI2 falls, while the CPU released the data attDHW(~10 ns) — so the chip stores a floating bus.- The address is changing at that same instant (address and R/W both move
tADSafter PHI2 falls), so the write could land in the next cycle's cell.- On back-to-back writes —
JSRpushing PCH/PCL, and every IRQ/BRK/NMI pushing PCH/PCL/P — raw/WEnever rises between the two cycles at all, so a real chip stays in one long transparent write straight across the address change and smears data into intermediate cells.The simulator never showed this because
rtl/mem/sram_io.svmodels 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/WEwith 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.cppasserts the invariant directly —/WEis 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.
✅ GPU side: until GAL v5.1,
LED_GPU_REGwrites aliased intoVIDEO_REGand 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$BFC0write aliased onto/CS_JOY, which is harmless because the joystick/OEalso requiresR/W=1. That alias is gone anyway since v11 decodes A7 (§2a).
The window is 32 bytes: A3–A0 are not decoded (the latch ignores them) and neither is A4.
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)
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_STRBcarries a clock term — and why a plain/CS_LEDdoes 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 validtADS(~30 ns) after that, write datatMDS(~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, tADSinto the next cycle~20 ns after the CPU released the data — clocks a floating bus active high (bare LED_SEL)the assertion, tADSafter PHI2 falls~10 ns before the write data is valid ( tMDS) — clocks the previous cycle'sMeasured 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:32moves address, R/W and data together onposedge phi2, so data is already valid when the strobe asserts. Real silicon has no such luxury. The model-independent check is the onevtestmakes: 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
tADSafter 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:32clocks the address register, and the core clocks R/W and write data, all onposedge phi2). The active-low PHI2-gated form —!(LED_SEL & PHI2), i.e. exactly the shape of/WE, and the one that would justify an_nsuffix — was measured re-clocking the '574 a second time at that posedge, leaving$A9/$58in the latch instead of the POST pattern. That double-capture is a model artifact, not a hardware hazard — but it is whyLED_STRBis active high rather thanLED_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 → $0Fand nothing else.v16 — both boards now use the identical term.
cpld_cpu1emitted a bare/CS_LEDthrough 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 nowLED_STRB = LED_SEL & !PHI2, the same as the GPU side.make vtestasserts the invariant on both boards: the strobe is never high while PHI2 is high, and there is exactly one rising edge per$BFC0–$BFDFwrite.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_decoderintocpld_ctrl—a15..a5and theBF_PAGEterm 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!A4discrepancy: the discrete version covered only$BFC0–$BFCF, the CPLD term covers the documented 32-byte$BFC0–$BFDF.
$BFC0–$BFDFstill selects/CS_VRAM_IMGas 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.
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.
; 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
| 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).
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).
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.)
/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.
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.
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.
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.
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).
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
PCB2 — cpld_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.)
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).
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.