MAD-65
Contents

MAD-65 — Video Circuit#

Detailed Design — v12 (ATF1508AS CPLDs)#


How to read this document. The video engine is three ATF1508AS-10JU84 CPLDs (v7 split; v12 crossbar split-out):

The crossbar still replaces the old 8× 74HC157 (none were re-added); it just lives on its own CPLD now rather than inside cpld_video. See §16.4a for the three-chip pin budget and the current inter-chip net list.

§2–§12 are organised by function, using the v5 block names ("GAL #1", "6× 74HC163", "74HC374"…). The logic is unchanged — only its housing moved — so the timing, equations and waveforms read identically. Read every such name as the function a CPLD now performs; none of those packages exist on the board. The only discrete video-path chips left are 2× 74HC245 (GPU data) and 2× 74HC166 (pixel serialisers).

One thing left PCB2 entirely and is not in this document's scope: the ping-pong shared RAM (SWAP_SEL, the crossbar, the 4× 245) moved to PCB3's cpld_ppr in v8 — see the architecture doc §6. The VSYNC→IRQ synchroniser went with it in v8 but came back in v14: it lives in cpld_ctrl now, so this board generates VSYNC and the frame /IRQ (§6.1). See §16 for the design history and §13 / §16.5 for the chip count.

1. Specifications#

Parameter Value
Logical resolution 400 × 300 px (1 bit/pixel)
Physical resolution 800 × 600 px (vertical 2× doubling, SVGA fill)
Video mode SVGA 800×600 @ 60.317 Hz (standard)
Pixel clock 20.000 MHz (dedicated crystal)
Colour depth 1-bit: white (1) / black (0)
Background copy hardware, at V[0]=1 (CPLD-sequenced, copy-over-bus)
BLANK signal hardware, generated in the CPLD
Hardware scrolling none
BLINDER register CPLD VIDEO_REG bit 1, written by GPU
Background register CPLD VIDEO_REG bit 0, written by GPU
COPY_DIS register CPLD VIDEO_REG bit 2, written by GPU
SHADOW_MODE register (v9) cpld_ctrl VIDEO_REG bit 3 → decoder GAL; picks $C000–$FFFF overlay (boot: ROM/shadow, run: shadow/VRAM-bg)
Boot-ROM wait states 3 per read (cpld_ctrl RDY generator; v9 added 1, v10 widened to 3)
Output standard VGA/SVGA analogue, 75 Ω
Video logic 3× ATF1508AS-10JU84 CPLD (cpld_video + cpld_ctrl, v7; + cpld_xbar, v12); GPU address decoder folded into cpld_ctrl (v13 — no GAL remains)

CRT compatibility note:#

Physical output fills the full SVGA 800×600 frame — 400 logical pixels × 300 logical lines. HSYNC and VSYNC timing is identical to standard SVGA 800×600 @ 60 Hz. All CRT and LCD monitors that accept 800×600 will synchronise correctly with a full-screen image. No black border at top or bottom.

How 400×300 logical fills 800×600 physical — two different doubling mechanisms:#

The horizontal and vertical 2× are achieved in completely different ways. This is the key to understanding why the 74HC166 loads every 8 pixel clocks (not every 16) and never needs a shift-enable divider.

Vertical 2× — done by a counter (explicit line repeat). The V counter runs 0–627 with 600 active lines = 300 logical lines × 2. The VRAM row address is V[9:1], so V[0] (the low bit) selects the first vs. second physical copy of the same logical row — each logical line is fetched and displayed twice. There genuinely are 600 distinct scanlines on the wire.

Horizontal 2× — done by the clock rate (free, no counter). A "true" SVGA 800×600 @ 60 Hz dot clock is 40 MHz with 1056 dots/line. This design runs the pixel clock at exactly half that — 20.000 MHz — with 528 H counts/line. Therefore:

1 H count  =  1 pixel clock (50 ns)  =  1 logical pixel  =  2 physical monitor dots

The active window emits 400 distinct pixel values across ~400 clocks; the monitor's analogue raster spreads them over its 800-dot active area, so each value naturally covers two adjacent physical dots. Nothing in the hardware ever holds a bit for two clocks or shifts twice.

Consequence for the shift register. The 74HC166 is clocked at the full 20 MHz and shifts once per logical pixel. It loads a fresh VRAM byte every 8 pixel clocks = 8 logical pixels = 16 physical screen pixels. You would only need "load every 16, shift every 2nd clock" if the pixel clock were the full 40 MHz — because it's 20 MHz, one clock already is one logical pixel and the horizontal ×2 comes for free from the clock rate.

Simulator note. The Verilator/SDL2 sim mirrors this exactly: its framebuffer is 400 columns wide (one entry per H count / per logical pixel), and the SDL2 window stretches it 2× horizontally to 800. So one 74HC166 output bit = one 400-wide framebuffer column = two 800-wide screen columns.

Shared RAM — not on this board (v8)#

The video circuit datapath is unchanged from v4. SWAP_SEL is no longer here: v5 put it in GAL #4, v6 folded it into the PCB2 CPLD, and v8 moved it off the board entirely to PCB3's cpld_ppr. The VSYNC→CPU IRQ synchroniser made the same trip in v8 but returned in v14 (§6.1): cpld_ctrl synchronises VSYNC and drives vsync_cpu_n, and PCB3 recovers the frame edge from that already-synchronous signal to toggle SWAP_SEL — so the frame IRQ and the ownership swap still come from one edge detector and stay coherent by construction. See the architecture doc §6.

The VRAM bank-switch SEL flip-flop is a separate thing and does live here — a register inside cpld_video, toggled on the VSYNC edge (§8). It was a discrete 74HC74 through v5; there is no 74HC74 on PCB2 now.


2. Timing#

Horizontal (20.000 MHz pixel clock):#

H active:         0–399   (400 pixel clocks = 400 logical pixels, each 50 ns)
H front porch:  400–419   (20 clocks,  1.000 µs)
H sync:         420–483   (64 clocks,  3.200 µs, active low)  ← CRT compatible
H back porch:   484–527   (44 clocks,  2.200 µs)
H total:           528 clocks
H frequency:   20 000 000 / 528 = 37 878.8 Hz

Vertical:#

V active:         0–599   (600 lines = 300 logical lines × 2)
V front porch:  600–600   (1 line)
V sync:         601–604   (4 lines, active low)  ← same as SVGA 800×600 standard
V back porch:   605–627   (23 lines)
V total:           628 lines
Frame rate:    37 878.8 / 628 = 60.317 Hz

Key timing values for GAL logic:#

Signal Value Hex Binary (10-bit H / 10-bit V)
H total 528 $210 10 0001 0000
H blank start 400 $190 01 1001 0000
H sync start 420 $1A4 01 1010 0100
H sync end 484 $1E4 01 1110 0100
H counter end 527 $20F 10 0000 1111
V total 628 lines $274 10 0111 0100
V blank start 600 $258 10 0101 1000
V sync start 601 $259 10 0101 1001
V sync end 605 $25D 10 0101 1101
V counter end 627 $273 10 0111 0011

3. Block Diagram#

Graphical (SVG): MAD65_video_blockdiagram.svg — PCB2 video/VRAM detail; and MAD65_system_blockdiagram.svg for the whole system. The current (v12) topology — no 74HC157 anywhere; the VRAM address crossbar is its own CPLD (cpld_xbar), fed by cpld_video's exported video_addr:

20 MHz ─►┌──────────────────────────────────────────────────────────┐
         │  cpld_video   (ATF1508AS-10JU84, pixel-clock domain)      │  ─► HSYNC / VSYNC ─► PCB3
         │   H counter (0–527) · V counter (0–627)                   │  ─► PIXEL_OUT/BG_OUT/BLANK/VIDEO
         │   VRAM addr-gen  addr = V[9:1]·50 + H[9:3]                │  ◄─ qh_a/qh_b (serial pixel)
         │   SEL (double-buffer) · LOAD_SR · we_copy_n               │
         └───┬──────────────────────────────────────────┬───────────┘
   video_addr[13:0] ─► cpld_xbar ◄─ gpu_addr[13:0]      │ load_sr
   cpld_xbar: A/B-addr = SEL ? gpu : video   7 nets ║ (sel, img_oe_n,
             │                                ║   bg_oe_n, we_copy_n,▼
             │                                ▼  cs_*/rdy)     74HC166 ×2 (serialisers)
             │        ┌──────────────────────────────────────┐
             │        │ cpld_ctrl (ATF1508AS, 14.318 domain)  │ ◄─ CPU2 addr/R-W, cs_vram_*
             │        │  VIDEO_REG ($BFE0, 4 bits)            │ ─► rdy (3 wait states)
             │        │  VRAM /CE·/OE·/WE steering            │
             │        │  GPU-data 245 control                 │
             │        └──────────────┬───────────────────────┘
             ▼                       ▼
        VRAM chips A & B        74HC245 ×2 (GPU data ↔ CPU2 bus)
        4× CY7C199

VIDEO_REG ($BFE0) — four bits, latched inside cpld_ctrl (§11): bit 0: BG_REG — background colour (0=black 0V, 1=dark grey 0.18V) bit 1: BLINDER — pixel blinder (0=white visible 0.7V, 1=white hidden 0V) bit 2: COPY_DIS — background copy disable (0=normal copy, 1=VRAM-image frozen) bit 3: SHADOW_MODE — $C000–$FFFF overlay (v9): 0=boot (read ROM/write shadow), 1=run

VSYNC is generated in cpld_video and drives the SEL double-buffer toggle locally. Since v14 it also crosses one on-board track to cpld_ctrl, whose 2-stage CPU-clock synchroniser produces vsync_cpu_n (§6.1); that signal drives both CPUs' /IRQ and reaches PCB3, where cpld_ppr edge-detects it to toggle the registered ping-pong SWAP_SEL. Through v13 the raw VSYNC level made that trip instead and PCB3 hosted the synchroniser.

The background copy is sequenced by the CPLD over each buffer's own bus (background chip /OE drives, image chip /WE=WE_COPY latches), so it needs no external data MUX — the v5 74HC157 copy MUX is gone.


4. H and V Counters (CPLD block; was 6× 74HC163)#

H counter (10-bit, 0–527):#

CLK ──► [74HC163 #1, bits 0–3] ──RCO──► [74HC163 #2, bits 4–7] ──RCO──► [74HC163 #3, bits 8–11]
        └────────────── LOAD=0 when H=527 (from GAL #1) ─────────────────────────────────────────┘

3× 74HC163 = 12 bits available; 10 bits (H[9:0]) are used (H11, H10 tied to GND or unused). Synchronous reset at H=527: GAL #1 detects H=527 and asserts LOAD with zero on all inputs.

V counter (10-bit, 0–627):#

Identical 3× 74HC163 structure. Incremented by V_INC from GAL #1 when H transitions 527→0. Resets to 0 at V=627 and V_INC (i.e. at that line's end), so V holds 627 for a full line and wraps to 0 in step with H_RESET. V_RESET is gated with V_INC inside GAL #2 (v5.0): without the gate the counter would reload 0 one pixel clock after V became 627, holding V=627 for a single clock and starting the next frame at H=1 (blacking the top line's first byte and shortening the frame by ~1 line).

Pixel serialisation (74HC166):#

Logical Y address = V[9:1]   (V[0] distinguishes first/second physical line)

74HC166 shift register:
  CLK = CLK_PIX  (full 20 MHz pixel clock — no half-rate trick)
  PE  = LOAD_SR  (active low, from GAL #3)

LOAD_SR fires when H[2:0] == 000 (every 8 pixel clocks):
  → 74HC166 loads new byte from VRAM on next CLK edge
  → each bit is output for exactly 1 pixel clock (50 ns)

NOTE (v5.0): the synchronous load means QH lags the H counter by one CLK_PIX —
byte b's bits appear at H = 8b+1 .. 8b+8, not 8b .. 8b+7.  BLANK_H_RIGHT is
registered by one CLK_PIX in GAL #1 so the active window (H = 1..400) follows the
pixel data: no left-edge stale pixel and no lost right column (399).

Vertical doubling: each logical line displayed twice (V[0]=0 first, V[0]=1 second).
Same byte read at V[0]=0 and V[0]=1 — identical scan line displayed twice.

Bytes per line: H active = 0..399 → H[2:0] cycles 0..7 → 400/8 = 50 loads
50 bytes × 8 bits = 400 logical pixels per line  ✓

5. H Sync, BLANK_H, H_RESET, BG_OE (CPLD block; was GAL #1)#

Source: rtl/pcb2/cpld_video.sv (pixel-clock domain). These terms were GAL #1 (U_GAL_VIDEO1) through v5; v6 folded them into the CPLD and no such package or reference designator exists on the board — the block name below is a functional grouping only.

Inputs (11):#

H[9:0] — 10 bits of H counter
V0     — bit 0 of V counter (for BG_OE)

Outputs:#

HSYNC          — SVGA horizontal sync (active low)
BLANK_H_RIGHT  — H >= 400 (active high)
H_RESET        — H = 527, synchronous load H counter to 0
V_INC          — increment V counter (= H_RESET)
BG_OE          — OE for VRAM-background at V[0]=1 (active low)

Logic:#

BLANK_H_RIGHT = H >= 400:
  H9                    (H = 512..527)
  # H8 & H7 & H6        (H = 448..511)
  # H8 & H7 & H5        (H = 416..447)
  # H8 & H7 & H4        (H = 400..415)

HSYNC active when H = 420..483 (64 clocks, 3.200 µs):
  H8 & H7 & !H6 & H5 & (H4 | H3 | H2)    (H = 420..447)
  # H8 & H7 & H6 & !H5                     (H = 448..479)
  # H8 & H7 & H6 & H5 & !H4 & !H3 & !H2   (H = 480..483)

H_RESET = H = 527:
  H9 & !H8 & !H7 & !H6 & !H5 & !H4 & H3 & H2 & H1 & H0

V_INC = H_RESET

BG_OE = V0

6. V Sync, BLANK_V, V_RESET (CPLD block; was GAL #2)#

Source: rtl/pcb2/cpld_video.sv (pixel-clock domain). These terms were GAL #2 (U_GAL_VIDEO2) through v5; v6 folded them into the CPLD and no such package or reference designator exists on the board — the block name below is a functional grouping only.

Inputs (10):#

V[9:0]  — 10 bits of V counter

Outputs:#

VSYNC          — SVGA vertical sync (active low)
BLANK_V_BOTTOM — V >= 600 (active high)
V_RESET        — V = 627, load V counter to 0
V_ACTIVE       — active vertical area (NOT BLANK_V_BOTTOM, to GAL #4)

v14: there is no separate IRQ_RAW output any more. GAL #4 needed one because its synchroniser wanted the active-high level; cpld_ctrl takes the ordinary vsync_n pin and inverts it internally (vsync_s1 <= ~vsync_n), so one signal serves both the VGA connector and the interrupt path. See §6.1.

Logic:#

VSYNC active when V = 601..604 (4 lines — same position as SVGA 800×600 standard):
  V9&!V8&!V7&V6&!V5&V4&V3&!V2&!V1&V0  (V=601)
  # V9&!V8&!V7&V6&!V5&V4&V3&!V2&V1    (V=602, 603)
  # V9&!V8&!V7&V6&!V5&V4&V3&V2&!V1&!V0 (V=604)

BLANK_V_BOTTOM = V >= 600:
  V9 & !V8 & !V7 & V6 & !V5 & V4 & V3    (V = 600..607)
  # V9 & !V8 & !V7 & V6 & V5 & !V4        (V = 608..623)
  # V9 & !V8 & !V7 & V6 & V5 & V4 & !V3 & !V2  (V = 624..627)

V_RESET = V = 627:
  V9 & !V8 & !V7 & V6 & V5 & V4 & !V3 & !V2 & V1 & V0

IRQ_RAW = VSYNC active level (same expression as VSYNC, without negation)

V_ACTIVE = NOT(BLANK_V_BOTTOM) AND NOT(V_RESET)

V timing explanation:#

V = 0–599:   active image (600 physical lines = 300 logical lines × 2)
V = 600:     front porch (1 line)
V = 601–604: VSYNC (4 lines, identical to standard SVGA 800×600 position)
V = 605–627: back porch (23 lines, identical to standard SVGA 800×600)

Result: standard SVGA 800×600 @ 60.317 Hz timing.
Image fills the full active area — no black border.

6.1 VSYNC → VSYNC_CPU_N (cpld_ctrl, v14)#

Source: rtl/pcb2/cpld_ctrl.sv. This is the system's only clock-domain crossing. VSYNC is born in cpld_video's 20 MHz clk_pix domain; cpld_ctrl runs on 14.318 MHz clk_14318 (= PHI2), so it samples vsync_n through two flip-flops and inverts the second:

vsync_s1 <= ~vsync_n        stage 1 — may be metastable
vsync_s2 <= vsync_s1        stage 2 — clean
VSYNC_CPU_N = ~vsync_s2     VSYNC, re-timed to the CPU clock (active low)

VSYNC_CPU_N is a level, low for the whole 4-line VSYNC pulse (~106 µs) — long enough that a CPU running with interrupts masked still sees it on re-enable. It goes three places: CPU2's /IRQ on this board, CPU1's /IRQ on PCB1, and PCB3's cpld_ppr, which edge-detects it with a single flip-flop to toggle SWAP_SEL. One synchroniser, one edge detector, so the frame interrupt and the ping-pong ownership swap cannot disagree about where a frame begins.

Why the name. Two of the three consumers take it as an interrupt; PCB3 does not — cpld_ppr only needs the frame boundary. Naming the net irq_n made the shared-RAM board read as if it cared about interrupts. VSYNC_CPU_N says what the signal is, and the _CPU suffix matches CLK_CPU/CLK_PIX.

Why stage 2. v8's cpld_ppr tapped stage 1 (irq_out_n = ~irq_s1), which is a one-flip-flop crossing — a metastable sample could land on both CPUs' /IRQ pins. The second stage costs one PHI2 (~70 ns) against a 16.6 ms frame.

Why on this board. Through v13 raw VSYNC was cabled to PCB3, synchronised there and the resulting /IRQ cabled back. PCB2 therefore could not raise its own frame interrupt without PCB3 populated and cabled — an awkward dependency, since VSYNC originates here. Counting flip-flops from VSYNC, SWAP_SEL still toggles on the same clock edge it did in v8: v8 spent 2 PHI2 reaching its edge-detect condition and 1 acting on it, v14 spends the first 2 here and the third in cpld_ppr.


7. LOAD_SR, BLANK, VIDEO Output (CPLD block; was GAL #3)#

Source: rtl/pcb2/cpld_video.sv (pixel-clock domain). These terms were GAL #3 (U_GAL_VIDEO3) through v5; v6 folded them into the CPLD and no such package or reference designator exists on the board — the block name below is a functional grouping only.

Inputs (8):#

H[2:0]          — lower 3 bits of H counter (LOAD_SR detection)
QH              — serial pixel bit from 74HC166
BG_REG          — background colour bit (VIDEO_REG bit 0, from cpld_ctrl)
BLINDER         — pixel blinder bit     (VIDEO_REG bit 1, from cpld_ctrl)
BLANK_H_RIGHT   — from GAL #1 (H >= 400)
BLANK_V_BOT     — from GAL #2 (V >= 600)

Outputs:#

LOAD_SR        — load 74HC166 shift register (active low, every 8 clocks)
BLANK          — full BLANK (H and V combined)
PIXEL_OUT      — white pixel in image area (suppressed when BLINDER=1)
BG_OUT         — dark grey background in image area (when BG_REG=1)
VIDEO          — video output (PIXEL_OUT OR BG_OUT)

Logic:#

LOAD_SR = !H2 & !H1 & !H0
  (fires every 8 pixel clocks — byte boundary, H[2:0]=000)

BLANK = BLANK_H_RIGHT # BLANK_V_BOT

PIXEL_OUT = QH & !BLANK & !BLINDER
BG_OUT    = BG_REG & !QH & !BLANK
VIDEO     = PIXEL_OUT # BG_OUT

BLINDER / BG_REG / COPY_DIS operation: see §11.#


8. VRAM /CE and WE_COPY (cpld_ctrl + cpld_video; was GAL #4)#

v8 — this section shrank. GAL #4 also held the IRQ synchroniser and SWAP_SEL. Both moved to PCB3's cpld_ppr (architecture doc §6). SWAP_SEL is still there; the synchroniser came back to PCB2 in v14 and now lives in cpld_ctrl — see §6.1, not here. What remains in this section is the VRAM /CE steering and the background-copy strobe, split across the two CPLDs.

Source: rtl/pcb2/cpld_ctrl.sv (the /CE steering) and rtl/pcb2/cpld_video.sv (we_copy_n).

VRAM /CE steering — cpld_ctrl#

The front (displayed) buffer is enabled unconditionally; the back (GPU-owned) buffer follows the GPU's own select:

gpu_ce_n   = !( !CS_VRAM_IMG # !CS_VRAM_BG )       ; GPU is touching VRAM at all
a_is_video = !SEL                                  ; A is the front buffer when SEL=0
b_is_video =  SEL

ce_a_n = a_is_video ? 0 : gpu_ce_n
ce_b_n = b_is_video ? 0 : gpu_ce_n

Do not use the old CS_VRAM_A = (!SEL & VIDEO_EN) # (SEL & GPU_EN) form. That was the v5.1 equation, and it is wrong: it drops the front buffer's /CE on any cycle the GPU takes the bus, blanking the display intermittently. See §16.7. The video side needs the front buffer selected throughout display, which is why the term above is a constant 0.

The per-chip /OE and /WE are steered the same way — the front buffer's image chip takes img_oe_n/we_copy_n from cpld_video, the back buffer's takes the GPU's.

v16 — the GPU side of that mux is PHI2-qualified, the video side is not. The GPU write terms carry the gate at their source:

GPU_WE_IMG = !CS_VRAM_IMG & !R/W & PHI2 GPU_WE_BG = !CS_VRAM_BG & !R/W & PHI2

so a VRAM write commits on PHI2's falling edge with address and data still valid — the same rule every other write sink in the system now follows (address-decoder doc §3a). WE_COPY is deliberately exempt: it is generated by cpld_video in the 20 MHz clk_pix domain for the background→image copy and has nothing to do with the CPU bus, so gating it with PHI2 would be meaningless and would break the copy.

Background copy strobe — cpld_video#

WE_COPY = !( !BG_OE & V_ACTIVE & CS_VRAM_IMG & !COPY_DIS & !BLANK_HR )

VRAM bank switching — cpld_video#

SEL is a register inside cpld_video, toggled on the VSYNC edge:
  SEL=0: Video reads VRAM_A,  GPU writes VRAM_B
  SEL=1: Video reads VRAM_B,  GPU writes VRAM_A

SEL also steers the VRAM address crossbar, which lives in its own CPLD cpld_xbar since v12 (it was folded into cpld_video in v7, and 8× 74HC157 through v6). SEL is exported from cpld_video to both cpld_ctrl and cpld_xbar (§16.4a).

There is no external 74HC74 — the SEL flip-flop moved into the CPLD in v6, and the ping-pong SWAP_SEL that once shared that package is now a register in cpld_ppr on PCB3, clocked by PHI2.


9. VRAM Address Generator (CPLD block; was 5× 74HC283)#

Formula:#

vram_addr = V[9:1] × 50 + H[9:3]

Where: - V[9:1] = logical line 0–299 - H[9:3] = byte in line 0–49

Verification: - H active = 0..399 → H[9:3] = 0..49 (50 bytes/line) ✓ - 74HC166 loaded every 8 pixel clocks: H[9:3] stable during each load cycle ✓ - max addr = 299 × 50 + 49 = 14,999 = $3A97 (14 bits, well within 32 kB chip) ✓ - GPU window ($8000–$BFDF = 16,352 bytes): max GPU addr = $8000+$3A97 = $BA97 < $BFDF ✓

Multiply ×50 by wiring:#

50 = 32 + 16 + 2 = 2⁵ + 2⁴ + 2¹

Y = V[9:1]  (9 bits, value 0–299)
Y × 32 = {Y, 0, 0, 0, 0, 0}   ← bit shift, zero gates
Y × 16 = {Y, 0, 0, 0, 0}      ← bit shift, zero gates
Y × 2  = {Y, 0}                ← bit shift, zero gates

Stage 1: Y×32 + Y×16 = Y×48   ← 3× 74HC283 (14-bit addition)
Stage 2: Y×48 + Y×2  = Y×50   ← 1× 74HC283 (Y×2 adds into bits [8:1])
Stage 3: Y×50 + H[9:3]        ← 1× 74HC283 (final byte-address addition)

Total: 5× 74HC283 adders.


10. Background Copy Mechanism (CPLD-sequenced; v5 used a 74HC157 MUX)#

Operation (v5.1 — sequential single-port bus):#

The image and background chips of a buffer share one bidirectional data bus. A single- port SRAM cannot output and be written at the same instant, so image read (for display) and background copy (write) are sequenced within the 8-clock byte window. Define the copy window = H[2:0] ∈ {4,5} (H2 & !H1). IMG_OE and BG_OE (both GAL #1) are exact complements, so exactly one chip drives the bus at any time — the 74HC157 copy MUX is gone.

H[2:0]:   0    1    2    3    4    5    6    7
IMG_OE   ██████████████       ·    ·   ██████   image chip /OE (drives bus)
 LOAD_SR ▲ 74HC166 loads image byte off the bus (GAL #3)
BG_OE     ·    ·    ·    ·   ██████    ·    ·    background chip /OE  (V[0]=1 only)
WE_COPY   ·    ·    ·    ·   ██████    ·    ·    image chip /WE       (V[0]=1 only)

V[0]=0 (first physical line):  copy window idle (BG_OE/WE_COPY need V[0]=1) — image
       drives the whole window; byte displayed, VRAM untouched.
V[0]=1 (second physical line): image drives + loads the 166 (display, identical to V[0]=0);
       then in {4,5} the background chip drives the bus and the image chip's /WE latches it
       — the "free background" copy, over the shared bus. Inhibited when COPY_DIS=1.

The Verilator model is structural: rtl/mem/sram_io.sv is the CY7C199 with a real tri-state inout dq, and the VRAM interconnect (4× SRAM, 2× 245, 2× 166) is inlined directly into rtl/pcb2/pcb2.sv. cpld_ctrl puts the image+background chips of each buffer on one shared bus and sequences /OE,/WE by ownership and phase.

The video-owned buffer stays /CE-enabled throughout scanout — ce_a_n = a_is_video ? 0 : gpu_ce_n (§8). This is not an optimisation: the older (!SEL & VIDEO_EN) form dropped the front buffer's /CE on any cycle the GPU took the bus, which blanked the display intermittently. The GPU-owned buffer uses gpu_ce_n.

An earlier revision of this section said the 74HC245 bus-buffer layer was "not yet modelled" and that the module lived in rtl/pcb2/video/vram_sys.sv. Both are obsolete: the 245s are modelled (pcb2.svu_245_a/u_245_b), and vram_sys.sv and the whole rtl/pcb2/video/ directory were removed when the interconnect was inlined. See §16.7–§16.8.

Cycle timing (one byte = 8 pixel clocks @ 20 MHz = 400 ns):#

H=0:  H[2:0]=0, LOAD_SR asserted (load happens on the H=0→1 edge)
H=1:  QH=b7   (byte b's first pixel; active because BLANK_H_RIGHT is delayed 1 clk)
H=2:  QH=b6
H=3:  QH=b5
H=4:  QH=b4
H=5:  QH=b3
H=6:  QH=b2
H=7:  QH=b1
H=8:  QH=b0 ; H[2:0]=0 again → next LOAD_SR (next byte's b7 appears at H=9)

So byte b occupies H = 8b+1 .. 8b+8.  The active window is H = 1..400 (BLANK_H_RIGHT
registered one CLK_PIX), so all 400 columns (byte 0 col 0 .. byte 49 col 7) display
and column 399 is not blanked.

(V[0]=1 only, copy window H[2:0] ∈ {4,5}):
  BG_OE → background chip drives the shared data bus (A14=1, same address generator)
  WE_COPY (GAL #4, combinatorial) → image chip /WE latches the background byte
  Outside the copy window IMG_OE re-enables the image chip (single bus driver at all times)

11. VIDEO_REG (address $BFE0) (CPLD block; was a 74HC374 latch)#

Four active bits, written by GPU at any time (use $BFE0 as canonical address):#

bit 0:   BG_REG   — background colour register
           0 → black background pixels    (0.00 V)
           1 → dark grey background pixels (0.18 V)
           → to GAL #3

bit 1:   BLINDER  — pixel blinder register
           0 → white (bitmap=1) pixels visible   (0.70 V)  — normal
           1 → white (bitmap=1) pixels suppressed (0.00 V) — bitmap hidden
           → to GAL #3

bit 2:   COPY_DIS — background copy disable register
           0 → normal: VRAM-background copied to VRAM-image at each V[0]=1 line
           1 → frozen: WE_COPY inhibited, VRAM-image NOT modified by video circuit
           → to cpld_video's we_copy_n term

bit 3:   SHADOW_MODE — GPU code-RAM overlay select (v9)   → to decoder GAL
           0 → boot: read $C000–$FFFF from ROM (+3 wait states), write to shadow code-RAM
           1 → run:  read $C000–$FFFF from shadow code-RAM (full speed), write to VRAM-background
           reset value 0 (reset vector fetches from ROM); set once at boot, kept set

bits 7–4: reserved (not used)

SHADOW_MODE / shadow code-RAM (v9): cpld_ctrl holds VIDEO_REG bit 3 as shadow_mode (an internal wire since v13 folded the decoder in — was fed off-chip to gal_gpu_decoder), and uses it to steer $C000–$FFFF three ways — /CS_ROM, /CS_SHADOW, /CS_VRAM_BG. cpld_ctrl also holds rdy (RDY) low for three PHI2 cycles on each ROM read, so a boot-ROM access spans 4 cycles (~239 ns of access time — ample for any EPROM grade). A 2-bit counter re-arms at 3, giving every back-to-back fetch its own fresh 3 wait states:

rdy = !( ROM_ACCESS & (WCNT != 3) )        ; cpld_ctrl.sv

This costs nothing at run time: /CS_ROM is SHADOW_MODE-gated, so the ROM is only read during the boot copy. v9 introduced one wait state; v10 widened it to three. See MAD65_architecture.md §5.

GPU control summary:#

BLINDER / BG_REG — output levels in active image area:

BLINDER BG_REG bitmap=1 pixel bitmap=0 pixel
0 0 0.70 V (white) 0.00 V (black)
0 1 0.70 V (white) 0.18 V (grey)
1 0 0.00 V (black) 0.00 V (black)
1 1 0.00 V (black) 0.18 V (grey)

COPY_DIS — background copy mechanism:

COPY_DIS Effect on VRAM-image each V[0]=1 line
0 Normal: VRAM-background byte written to VRAM-image (WE_COPY)
1 Frozen: WE_COPY inhibited — VRAM-image content preserved

BLANK area (outside active image): always 0.00 V — VGA/SVGA compliant.


12. SVGA Connector — Pinout and Analogue Output#

Pin 1  ── RED   ── PIXEL_OUT (75Ω) + BG_OUT (270Ω) ──┐
Pin 2  ── GREEN ── PIXEL_OUT (75Ω) + BG_OUT (270Ω) ──┤ (tied → monochrome)
Pin 3  ── BLUE  ── PIXEL_OUT (75Ω) + BG_OUT (270Ω) ──┘
Pin 5  ── GND
Pin 6  ── RED_RTN  ── GND
Pin 7  ── GRN_RTN  ── GND
Pin 8  ── BLU_RTN  ── GND
Pin 13 ── HSYNC ── from the CPLD (hsync_n output)
Pin 14 ── VSYNC ── from the CPLD (vsync_n output)

Analogue output levels:#

PIXEL_OUT=1, BLINDER=0, BLANK=0  → 75Ω  → ~0.70 V  (white)
BG_OUT=1,    BG_REG=1,  BLANK=0  → 270Ω → ~0.18 V  (dark grey)
Both=0                            →        0.00 V   (black)
BLANK=1                           →        0.00 V   (blanking — SVGA compliant)

13. Component List — Video Module#

The v7 video module (PCB2), as modelled in rtl/pcb2/pcb2.sv / cpld_video.sv / cpld_ctrl.sv:

Component Function Qty
ATF1508AS-10JU84 CPLD cpld_video — H/V counters, sync/blank, VRAM addr-gen (video_addr), SEL, LOAD_SR, pixel path (≈33/64 I/O since v12) 1
ATF1508AS-10JU84 CPLD cpld_ctrl — the GPU address decoder (v13, was the ATF22V10 gal_gpu_decoder), VIDEO_REG (incl. SHADOW_MODE, v9), GPU decode, VRAM /OE//WE//CE steering, 245 control, 3-wait-state RDY generator (v9/v10), the VSYNC→PHI2 synchroniser → vsync_cpu_n (v14, §6.1) LED_STRB (v15) and the PHI2-qualified /WE (v16) (≈52/64 I/O). SWAP_SEL left for PCB3's cpld_ppr in v8 and stayed there 1
ATF1508AS-10JU84 CPLD cpld_xbar (v12) — the VRAM address crossbar: A/B-addr = SEL ? gpu_addr : video_addr (≈57/64 I/O). Split out of cpld_video for I/O margin 1
74HC245 GPU data transceivers (CPU2 bus ↔ back buffer) 2
74HC166 Pixel serialisers (one per buffer; cpld_video picks by SEL) 2
74HC574 v15: LED_GPU_REG ($BFC0–$BFDF) — 8 diagnostic LEDs, clocked by LED_STRB from cpld_ctrl. Not part of the video path 1
CY7C199-15PC VRAM — image A/B + background A/B 4
Resistor 75 Ω VGA white output (R, G, B) 3
Resistor 270 Ω VGA dark grey background output (R, G, B) 3
Crystal 20.000 MHz SVGA pixel clock (standard 800×600 @ 60 Hz) 1

Video logic + interconnect: 3 CPLD (v12) + 4 discrete (2× 245 + 2× 166). The ~16-chip v5 discrete video pile (4× ATF22V10 video GAL, 6× 74HC163, 5× 74HC283, 74HC374, 74HC74 SEL) collapsed into one CPLD in v6; v7 adds a second CPLD to absorb the 8× 74HC157 VRAM address crossbar as well, so only the data transceivers and serialisers remain discrete. The GPU decoder folded into cpld_ctrl in v13 (the last ATF22V10 — no GAL remains). Note the ping-pong shared-RAM interconnect is a separate subsystem — on its own board PCB3 as of v8 (2× SRAM + 4× 74HC245 + cpld_ppr, which absorbs the former 6× 74HC157 crossbar) — see the architecture doc §6.


14. Version History#

Feature v1 (TTL only) v2 (2 GAL) v3 (4 GAL) v4 v5
Total ICs 18–22 16–17 17 + ext. comparator 17 17 (SWAP_SEL in GAL #4)
BLANK signal none yes yes (all 4 edges) yes (H+V, no scroll) yes (unchanged)
BG_REG register none yes yes yes (74HC374 bit 0) yes (unchanged)
BLINDER register none none none yes (74HC374 bit 1) yes (unchanged)
COPY_DIS register none none none yes (74HC374 bit 2) yes (unchanged)
Shared RAM swap IDT7132 (dual-port) ping-pong, sync swap (GAL #4)
Background copy mechanism none none yes (74HC157 MUX) yes + COPY_DIS inhibit yes (unchanged)
Logical resolution 384×240 (incorrect) 400×300 400×300 (unchanged)
Physical resolution 768×480 (non-standard) 800×600 (full SVGA) 800×600 (unchanged)
Pixel clock 25.175 MHz 25.175 MHz 25.175 MHz 20.000 MHz 20.000 MHz (unchanged)

v6 → v16#

Ver Change to the video module
v6 All v5 video logic — the four GALs, 6× 74HC163, 5× 74HC283, the 74HC374 VIDEO_REG and the 74HC74 SEL FF — folded into one ATF1508AS CPLD (cpld_pcb2). Behaviour unchanged; only the housing moved. The VRAM data path became structural (8× 74HC157 + 2× 74HC245 + 2× 74HC166).
v7 cpld_pcb2 split into cpld_video + cpld_ctrl — the second chip made room to absorb the 8× 74HC157 VRAM address crossbar, which disappeared. §16.4a.
v8 The ping-pong shared RAM left PCB2 for its own board, PCB3. cpld_ctrl shed SWAP_SEL, the VSYNC→IRQ synchroniser and the ping-pong 245 enables to cpld_ppr, which also absorbed the 6× 74HC157 ping-pong crossbar. gal_gpu_decoder now emits a single cs_sram_gpu_n. PCB2: 26 → 14 ICs.
v9 Shadow code-RAM (u_shadow, a 6th CY7C199) at $C000–$FFFF so GPU code runs from fast SRAM. VIDEO_REG gains bit 3 = SHADOW_MODE; cpld_ctrl gains a wait-state RDY generator; the decoder GAL gains shadow_mode in / cs_shadow_n out. PCB2: 15 ICs.
v10 Boot-ROM wait states widened 1 → 3 (~239 ns of access time). Free — the ROM is only read during the boot copy.
v11 PCB1-only (its two GALs → cpld_cpu1). No change to the video module; system total 36 → 32 ICs.
v12 The VRAM address crossbar split out of cpld_video into its own ATF1508AS (cpld_xbar), freeing cpld_video from ~59/60 to ~33/64 usable I/O. cpld_video now exports video_addr[13:0]; cpld_xbar muxes it against the CPU2 bus under SEL. No discrete 74HC157 returned. PCB2: 15 → 16 ICs; system total 32 → 33 ICs. §16.4a.
v13 The GPU address decoder — the last ATF22V10 (gal_gpu_decoder) — folded into cpld_ctrl (~37 → ~48/64 I/O). The CPU2 address bus a15..a5 now enters that chip and shadow_mode stops round-tripping off-chip. No GAL remains anywhere in MAD-65. PCB2: 16 → 15 ICs.
v14 The VSYNC→PHI2 synchroniser came back to PCB2, into cpld_ctrl (~48 → ~50/64 I/O): cpld_video's vsync_n crosses one on-board track, two flip-flops guard metastability, and vsync_cpu_n (off stage 2, not stage 1 as v8 had it) drives CPU2 locally and leaves the connector for PCB1 and PCB3. PCB3 keeps SWAP_SEL but only edge-detects the now-synchronous vsync_cpu_n — one flip-flop, no CDC — and cpld_ppr drops 60 → 59/64 I/O. Raw VSYNC no longer leaves PCB2 except to the VGA connector, so the video module raises its own frame interrupt standalone. No IC-count change.
v15 LED_STRB added to cpld_ctrl (~50 → ~51/64 I/O) and the GPU diagnostic LED register populated on-board: one 74HC574 at $BFC0–$BFDF, write-only, clocked by the rising edge of LED_STRB (active-high, PHI1-gated). Not a video-path change — it is here because cpld_ctrl owns the GPU decode. The 74HC133 the original design specified is retired unbuilt (it only ever existed because the pre-v13 ATF22V10 had no spare macrocell). gpu_os has written LED_GPU_REG since it was written — four boot POST stages plus the GPU_LED opcode — and until now nothing latched it. PCB2: 15 → 16 ICs; system total 32 → 33 ICs.
v16 All write strobes PHI2-qualified: /WE = ~(PHI2 & ~R/W), emitted by each board CPLD (cpld_ctrl ≈51 → ≈52/64 I/O). Through v15 every /WE in the system was raw R/W — a real defect, since an async SRAM commits on /WE's rising edge and that edge landed after the CPU had released the write data, with back-to-back pushes never releasing /WE at all. On PCB2 the gate also folds into gpu_we_img / gpu_we_bg, so all four VRAM chips get it. we_copy_n is exempt — it is a clk_pix-domain video strobe, not a CPU write. No IC-count change. See the address-decoder doc §3a.

Current: v16. The video module is 3× ATF1508AS + 2× 74HC245 + 2× 74HC166, plus 4× CY7C199 VRAM. No 74HC157, 74HC163, 74HC283, 74HC374 or 74HC74 remains — see §13 and §16. (v15's 74HC574 is the LED latch, outside the video path.)


15. Open Issues#

  1. WE_COPY timing — BG_OE is combinatorial (V0). WE_COPY fires after one cpld_video propagation delay (≤10 ns for a -10 part). At 20 MHz one clock = 50 ns. CY7C199: tAW ≥ 0 ns, tDW ≥ 25 ns — ample margin. Verify on prototype.

  2. 74HC166 CLK = CLK_PIX — 74HC166 clocked at full 20 MHz. Setup time ~5 ns, hold time ~5 ns. At 20 MHz (50 ns clock): 45 ns margin. ✓

  3. Bus conflict during WE_COPY — during WE_COPY the VRAM-image data bus is driven by the VRAM-background chip over the buffer's own bus (there is no MUX; the v5 74HC157 copy MUX was removed in v5.1 — §10). The GPU's 74HC245 must be off for that buffer, which cpld_ctrl's oe245_a/b_n guarantee: they only enable on ~gpu_ce_n, and we_copy_n is itself gated by cs_vram_img_n (GPU not touching the image chip). §8.

  4. VSYNC to IRQ delay — ~~(GAL #4 FF)~~ Back on this board since v14. cpld_ctrl runs VSYNC through a 2-stage synchroniser to CLK_14318 (2 clocks ≈ 140 ns) and drives vsync_cpu_n; PCB3's cpld_ppr derives SWAP_SEL from that same signal one clock later, so the frame IRQ and the ownership swap stay coherent. Acceptable for a 60 Hz IRQ. See §6.1 and the architecture doc §6. (v8–v13: a 3-stage synchroniser on PCB3, ≈210 ns, with /IRQ off stage 1.)

  5. COPY_DIS usage pattern — set/clear during VBLANK to avoid mid-line artefacts. When COPY_DIS=1, background data in VRAM-background is preserved unchanged.

  6. VRAM-background A14 hardwired — background chip A14 must be tied to VCC (via 100 Ω) from the video address bus side. Background data stored at chip offset $4000 (GPU writes $C000 → chip A14:0=$4000). Video address generator A13:0 connects to both image chip and background chip; A14=1 on background chip only.

  7. GAL #4 SWAP_SEL budget — ~~(SWAP_SEL + IRQ_S2/IRQ_S3 → 8/10 macrocells; verify the fit when re-compiling GAL #4)~~ Obsolete — there is no GAL #4. v6 folded it into the CPLD and v8 moved SWAP_SEL and the sync stages off PCB2 to cpld_ppr on PCB3 (≈60/64 I/O — see the architecture doc §16 open issue 10 for the live CPLD I/O budgets). Nothing to verify.


16. Design history — the CPLD consolidation (v5 → v11)#

⚠️ This section is HISTORY, not the current design. It documents the v6 consolidation that collapsed the discrete logic-glue pile into a single CPLD (cpld_pcb2.sv) — a file and a partition that no longer exist. Two later revisions superseded its conclusions:

It is kept because the reasoning (why consolidate, what the budget looked like, which bugs it fixed) is still useful. For what is actually on the board today, see the masthead, §8 and §13. §§1–15 above specify the video logic itself.

16.1 Motivation#

Two problems drove this:

  1. TTL/GAL count. True concurrent double-buffering (GPU draws the back buffer on any cycle while the video engine scans the front buffer) needs a full address crossbar plus data buffering. Built entirely from discrete 74-series parts that pushed the board toward ~27 chips — unacceptable.
  2. Structural fidelity. The simulator's rtl/pcb2/video/vram_sys.sv faked the crossbar with behavioral ? : muxes (e.g. a_addr = a_is_video ? video_addr : gpu_addr). Those are not models of any real chip, so they can never validate hardware.

The fix for both: put the address muxes and data buffers back as real 74xx parts, and sweep the rest of the video logic — all four video GALs, the H/V counters, the VRAM address generator, the video register, and every steering/arbitration term — into a single ATF1508AS-10JU84 CPLD (128 macrocells, 64 I/O, synthesizable SystemVerilog). The address decoder stays a separate ATF22V10 GAL (see §16.2): it is a stable combinational address→chip-select map that has no need of the video timing, and keeping it out of the CPLD frees ~8 input pins (the CPU A15:A5 bus never enters the CPLD). (This held through v12; v13 folded the decoder into cpld_ctrl after all — that chip had the spare I/O — retiring the last ATF22V10. The A15:A5 bus now enters cpld_ctrl. See §16.4a.)

This aligns perfectly with the project's modelling rule: real chips get behavioral models; everything else is structural wiring. The CPLD is a real chip, so its behavioral model is legitimate. The ? : glue is not re-modeled — it is partitioned: address muxes become 74HC157 instances, data buffers become 74HC245/74HC166 instances, and the arbitration logic moves inside the CPLD behavioral model.

16.2 Partition — what folds in, what stays discrete#

Function (v5.1) v5.1 parts v6 home
Address decoder (gpu_decoder GAL) 1× ATF22V10 separate GAL (unchanged)
Video GAL #1–#4 (sync, blank, CS, WE_COPY, IRQ sync, SWAP_SEL) 4× ATF22V10 CPLD
H counter + V counter 6× 74HC163 CPLD
VRAM address generator (V·50 + H) 5× 74HC283 CPLD
VIDEO_REG (BG_REG / BLINDER / COPY_DIS) 1× 74HC374 CPLD
VRAM-SEL / SWAP_SEL / IRQ sync flip-flops 1× 74HC74 CPLD
All control steering (the vram_sys ? : glue) (faked in sim) CPLD
Address crossbar (video_addr vs gpu_addr → each buffer) (faked) 8× 74HC157
GPU VRAM data buffers (CPU2 bus ↔ back buffer, bidir) (faked) 2× 74HC245
Video-read serialisers (front buffer → pixel) 1× 74HC166 2× 74HC166
VRAM chips 4× CY7C199 unchanged
CPU2 / RAM / ROM / ping-pong SRAM unchanged unchanged

16.3 Block diagram#

MAD-65 PCB2 v6 VRAM/video block diagram

(ASCII version of the same diagram below, for terminals / diff review.)

  CPU2 A15:A5, R/W ─►┌────────────────────┐─► cs_rom_n, cs_ram_lo_n, ce_sram_a/b_n → chips
  cpu1_ce/we ───────►│  ATF22V10 DECODER  │─► cs_vram_img_n, cs_vram_bg_n, cs_video_reg_n ──┐
  swap_sel ─────────►│ (gal_gpu_decoder,  │      (decoded VRAM/reg selects → CPLD)          │
       │             │  unchanged v5.1)   │                                                 │
       │             └────────────────────┘                                                 │
       │   clk_pix, clk_14318, rst_n ─┐   D2:D0, R/W ─┐   qh_a, qh_b ◄─(from 166s below)     │
       │                              ▼               ▼          ▼                          ▼
       │    ┌──────────────────────────────────────────────────────────────────────────────┐
       └────┤ swap_sel           ATF1508AS-10JU84  (CPLD)                                    │
            │  GAL#1–4 · H/V counters · addr-gen (V·50+H) · VIDEO_REG                        │
            │  · SEL/SWAP/IRQ FFs · ALL control steering                                     │
            └──────────────────────────────────────────────────────────────────────────────┘
   video_addr[13:0]│ SEL│ VRAM /CE·/OE·/WE ×10│ 245 ctl│ load_sr │ hsync/vsync/pixel/bg/blank
                   ▼    ▼                     ▼   (to both 166s)
   gpu_addr[13:0] ─►┌───────────────────────┐
   (CPU2 A13:0)     │   8× 74HC157           │  A-addr = SEL ? gpu   : video
                    │   ADDRESS CROSSBAR     │  B-addr = SEL ? video : gpu
                    └───┬───────────────┬────┘
                  A13:0 │         A13:0 │
              ┌─────────┴───┐   ┌───────┴─────┐
              │ IMG_A  BG_A │   │ IMG_B  BG_B │   4× CY7C199
              │  bus data_a │   │  bus data_b │   (A14=0 image, A14=1 background)
              └──┬───────┬──┘   └──┬───────┬──┘
          data_a│       │data_a    │data_b │data_b
                ▼       │          ▼       │
           74HC166_A    │     74HC166_B    │     both always load @ LOAD_SR;
                │qh_a───┘          │qh_b───┘     CPLD selects qh_a/qh_b by SEL
                │                  │             (1-bit serial mux — free)
             ┌──┴───┐           ┌──┴───┐
             │74HC245│ CPU2 D ◄──┤74HC245│  GPU R/W to the BACK buffer; the one
             └──────┘   bus      └──────┘  whose buffer is GPU-owned is enabled,
                                           dir = R/W (write CPU2→bus, read bus→CPU2)

Why dual 74HC166 (vs. one 166 + an 8-bit data mux): with two serialisers the CPLD only needs the two 1-bit outputs qh_a/qh_b and picks one by SEL internally — a free 2:1 in the fabric. The wide data_a/data_b buses therefore never enter the CPLD, which is what keeps the I/O budget inside 64 pins. Both 166s load every byte off their own buffer's bus; only the front-buffer serialiser's output is used.

Copy-over-bus is unchanged and stays internal to each buffer: during the copy window the background chip's /OE drives its buffer bus while the image chip's /WE (WE_COPY) latches it — image and background of a buffer still share one bus (§10), so the copy needs no external buffering. The CPLD sequences /OE///WE exactly as GAL #1/#4 did in v5.

16.4 CPLD pin / interface map#

v7/v12 note. The video engine is now three ATF1508AS (cpld_video + cpld_ctrl + cpld_xbar). The single-chip v6 budget (≈ 55/64 below, historical) could not also absorb the VRAM address crossbar: internalising it costs +28 pins (two 14-bit VRAM addresses out, +14 for the CPU2 address in, −14 for the video_addr it replaces). v7 put the crossbar in cpld_video, which drove that chip to ≈ 59/60 — no fitting margin — so v12 moved the crossbar to its own chip cpld_xbar (no discrete 74HC157 re-added). See §16.4a for the current three-chip budget.

Decoder external, so the CPU address bus does not reach cpld_ctrl; it takes only the decoded selects it consumes.

v6 single-chip budget (historical):

Dir Signals Pins
In clk_pix (20 MHz), clk_14318, rst_n 3
In cs_vram_img_n, cs_vram_bg_n, cs_video_reg_n, R/W (from decoder GAL) 4
In D2:D0 (VIDEO_REG), qh_a, qh_b 5
In cpu1_ce_sram_n, ce_sram_a_n, ce_sram_b_n (ping-pong 245 enable terms) 3
Out video_addr[13:0] (→ address crossbar) 14
Out SEL (crossbar select + qh serial mux), load_sr_n, irq_out, swap_sel (→ decoder) 4
Out VRAM control: {ce, img_oe, img_we, bg_oe, bg_we} × buffer A/B 10
Out GPU 245 control: /OE_a, /OE_b, DIR 3
Out Ping-pong 245 control: oe_cpu1_a, oe_cpu1_b, oe_gpu_a, oe_gpu_b 4
Out hsync_n, vsync_n, pixel_out, bg_out, blank 5
Total ≈ 55 / 64

16.4a Three-chip partition — CURRENT (v7 split, v8/v9 deltas, v12 crossbar split-out)#

This subsection is the authoritative partition, unlike §16.4 above it. Ports match rtl/pcb2/cpld_video.sv, rtl/pcb2/cpld_ctrl.sv and rtl/pcb2/cpld_xbar.sv.

CPLD #1 cpld_video (20 MHz pixel-clock domain) — counters, sync/blank, VRAM address-gen (video_addr), SEL, LOAD_SR, we_copy_n, pixel path. v12 lifted the VRAM address crossbar out to cpld_xbar (CPLD #3), so this chip no longer takes the CPU2 address bus or drives the VRAM addresses; it exports its internal video_addr instead:

Dir Signals Pins
In clk_pix, rst_n, qh_a, qh_b, cs_vram_img_n, bg_reg, blinder, copy_dis 8
Out video_addr[13:0] (→ cpld_xbar) 14
Out hsync_n, vsync_n, blank, pixel_out, bg_out, load_sr_n 6
Out sel, img_oe_n, bg_oe_n, we_copy_n (→ cpld_ctrl; sel also → cpld_xbar) 4
Total ≈ 33 / 64

vsync_n is a board output and, since v14, also an on-board net to cpld_ctrl's synchroniser (§6.1) — one pin, two loads, no extra I/O on this chip. It no longer goes to PCB3. sel is a real pin (it feeds cpld_ctrl and cpld_xbar) but it stays on PCB2 — it is not a port of pcb2 or mad65_top. The SDL harness reads the flip-flop itself, sel_r, which carries /*verilator public_flat_rd*/, exactly as it reads the h[9:0]/v[9:0] counters — those are Verilator/SDL introspection taps only, not physical pins and not module ports either.

CPLD #2 cpld_ctrl (14.318 MHz domain) — the GPU address decoder (v13, folded in from gal_gpu_decoder), VIDEO_REG (incl. SHADOW_MODE), VRAM /OE//WE//CE steering, GPU-data 245 control, the 3-wait-state RDY generator:

Dir Signals Pins
In clk_14318, rst_n, a15..a5 (CPU2 address, v13 — 11), gpu_rw, gpu_din[7:0], sel, img_oe_n, bg_oe_n, we_copy_n 26
In vsync_n (← cpld_video, v14 — the CDC input, §6.1) 1
Out decode selects cs_ram_lo_n, cs_sram_gpu_n (→PCB3), cs_rom_n, cs_shadow_n, cs_vram_img_n (→cpld_video) — v13 5
Out VRAM control {ce, img_oe, img_we, bg_oe, bg_we} × A/B 10
Out GPU 245 oe245_a_n, oe245_b_n, dir245 3
Out bg_reg, blinder, copy_dis (→ cpld_video), rdy (v9/v10 → CPU2) 4
Out vsync_cpu_n (v14 → CPU2 locally, PCB1, PCB3) 1
Total ≈ 50 / 64

v14: vsync_n returns as an input and vsync_cpu_n appears as an output — the VSYNC→PHI2 synchroniser (§6.1). Net +2 pins (48 → 50). PCB3's cpld_ppr loses its vsync_n input and irq_out_n output in exchange for one vsync_cpu_n input (60 → 59).

v13: cs_vram_bg_n, cs_video_reg_n and shadow_mode are now internal to this chip (they only ever fed cpld_ctrl or the decoder); cs_rom_n and cs_vram_img_n flipped from inputs to outputs. Net +11 pins (37 → 48), retiring the last ATF22V10.

v14: vsync_n returns as an input and vsync_cpu_n appears as an output — the VSYNC→PHI2 synchroniser and the frame interrupt (§6.1). Net +2 pins (48 → 50). PCB3's cpld_ppr loses its vsync_n input and irq_out_n output in exchange for one vsync_cpu_n input (60 → 59).

What v8 and v9 changed here. v8 removed the ping-pong control from cpld_ctrl entirely — cpu1_ce_sram_n, ce_sram_a/b_n, vsync_n, swap_sel, irq_out and the four ping-pong 245 enables all went to PCB3's cpld_ppr. (vsync_n came back in v14 — see above — but for the IRQ synchroniser only; SWAP_SEL stayed on PCB3.) v9 added the shadow code-RAM signals: in cs_rom_n, out shadow_mode + rdy; the decoder GAL correspondingly gained a shadow_mode input and a cs_shadow_n output (architecture doc §5). v10 widened the wait states 1 → 3 (no pin change).

CPLD #3 cpld_xbar (v12) — the VRAM address crossbar, on its own chip. Purely combinational (no clock/reset): it muxes cpld_video's video_addr against the CPU2 address bus under SEL and drives both VRAM buffer addresses. Equations are verbatim the old cpld_video crossbar (a_addr = sel ? gpu_addr : video_addr, b_addr the swap):

Dir Signals Pins
In gpu_addr[13:0] (CPU2 A13:0), video_addr[13:0] (← cpld_video), sel (← cpld_video) 29
Out a_addr[13:0], b_addr[13:0] (→ VRAM chips) 28
Total ≈ 57 / 64

Inter-chip nets = 22: cpld_video → cpld_ctrl = sel, img_oe_n, bg_oe_n, we_copy_n; cpld_ctrl → cpld_video = bg_reg, blinder, copy_dis (VIDEO_REG bits — quasi-static, benign CDC as in the single-chip v6); cpld_video → cpld_xbar = video_addr[13:0] + sel (15). v12 trade-off: splitting the crossbar out costs the new 14-bit video_addr bus and re-adds the ~12 ns hop the v7 fold had removed (video_addr now leaves the CPLD before the mux) — but it buys cpld_video back from a fitting-hostile ~59/60 to a comfortable ~33/64, without the discrete 74HC157 mux field that would make PCB2 unroutable.

cs_ram_lo_n and cs_sram_gpu_n come from the decoder GAL and touch neither CPLD; cs_rom_n does reach cpld_ctrl (since v9) for the wait-state generator.

16.5 Chip count#

v5.1 (discrete) v6 v7
Video logic glue (4 video GAL, 6× 163, 5× 283, 374, 74) ~16 1 CPLD 2 CPLD (cpld_video + cpld_ctrl)
Address decoder 1× ATF22V10 1× ATF22V10 1× ATF22V10 (unchanged)
VRAM interconnect (crossbar + data buffers + serialisers) (faked) 8× 157 + 2× 245 + 2× 166 = 12 2× 245 + 2× 166 = 4 (VRAM crossbar folded into cpld_video)
Net logic + interconnect 1 CPLD + 1 GAL + 12 = 14 chips 2 CPLD + 1 GAL + 4 = 7 chips

v6 collapsed the ~16-chip video-logic pile into one CPLD. v7 added a second CPLD to absorb the 8× 74HC157 VRAM address crossbar as well: the CPU2 address enters cpld_video and both VRAM buffer addresses leave it directly, so the video address never crosses a discrete mux.

On the current board the only structural 74xx parts in the video module are the data transceivers (2× 74HC245) and the pixel serialisers (2× 74HC166). v8 then moved the ping-pong subsystem to PCB3 and cpld_ppr absorbed its 6× 74HC157 too, so no 74HC157 exists anywhere in the system — and PCB2 dropped from 26 to 14 ICs (15 once v9 added the shadow RAM).

16.6 Simulator mapping (implemented — v6)#

16.7 Bugs this fixes#

16.8 PCB2 made fully structural (v6/v7-era — the ping-pong half has since moved to PCB3)#

⚠️ v8 supersedes the ping-pong parts of this subsection. It describes the ping-pong SRAMs, their 6× 74HC157 crossbar and their 4× 74HC245 as living on PCB2 — true when it was written, wrong since v8 moved the whole subsystem to PCB3 and cpld_ppr absorbed the crossbar (the 157s no longer exist). The structural principle it records — no behavioural ? : glue, only chip instances and nets — still holds everywhere, and PCB3 was built the same way. For the real ping-pong bus architecture see the architecture doc §6.

The v6 work above left two behavioural ? : piles in pcb2.sv — the CPU2 read-data mux and the ping-pong shared-RAM ownership muxing. Both became real chips + plain wires, so pcb2.sv is chip instances and nets only (all logic lives inside the CPLD or 74xx models). The CPLD was unchanged; nothing here touched the video path (VRAM hashes stayed byte-identical — verified against the §16.6 reference hashes for frame_diagonals, rectangles, gpu_os).

v8: all of it moved to PCB3, and cpld_ppr absorbed the crossbar outright — both CPU address buses enter the chip and both SRAM addresses leave it, so the 6× 74HC157 disappeared. PCB3 = 1 CPLD + 2 SRAM + 4× 245 = 7 ICs. The 245s survive because they are bidirectional byte buses; everything else is inside the CPLD. - Removed: the video_circuit.sv no-op wrapper and sram_2k.sv (ping-pong now uses the tri-state sram_io).

Follow-on cleanup (both boards uniform). The same treatment was then finished across the system:

All VRAM hashes remain byte-identical (verified via gpu_os, which drives the full CPU1 → shared RAM → GPU path).


Document v5.1 — changes from v5.0: — SWAP_SEL moved into GAL #4 as a registered output clocked by the 14.318 MHz * CPU clock (= PHI2): the swap is now synchronous and lands between bus cycles. — 74HC123 monostable removed; the SWAP_SEL 74HC74 half removed (VRAM-SEL 74HC74 * now has a spare half). Component list: total ICs 18→17. — Block diagram / GAL #4 notes updated: VSYNC → GAL #4 3-stage sync → SWAP_SEL.

Document v5.0 — changes from v4.1: — 74HC123 monostable added to PCB2: generates ~2 µs delayed VSYNC for SWAP_SEL FF clock. — SWAP_SEL FF: spare half of the existing VRAM-switch 74HC74 package. — Component list updated: 74HC123 added (total ICs: 17→18). — Block diagram updated to show VSYNC → 74HC74 (VRAM SEL, direct) and * VSYNC → 74HC123 → 74HC74 (SWAP_SEL, delayed) paths. — GAL #4 note updated: 74HC74 package now shared between VRAM SEL and SWAP_SEL.*

Document v4.1 — changes from v4.0: — resolution updated to 400×300 logical / 800×600 physical (full SVGA fill, no black border). — pixel clock changed from 40 MHz to 20 MHz. — 74HC166 CLK changed from H[0] (half-rate) to CLK_PIX (full 20 MHz).