Radius FPD/SE clone with Rotation Transform and Output from HDMI of FPGA Project Board

Trash80toG4

Active Tinkerer
Apr 1, 2022
1,453
463
83
Bermuda Triangle, NC USA
# Scanline Rotation Transform — Engineering Spec

## 0. Project Context: Cloning the 1986 Radius FPD (64/68Hz Board)

This project clones the original 1986 Radius Full Page Display card for the Macintosh SE — specifically the first-generation **64/68Hz board**, not the incompatible second-generation 1987 75Hz board (different signal timing, not interchangeable).

**Confirmed specs:**
- Resolution: **640 wide × 864 high**
- Color depth: **1 bit per pixel** (monochrome, black/white)

Because this is a clone of real vintage hardware (not a new design driven purely by a written spec), the project has an added prerequisite phase: **capturing and characterizing the actual analog signal timing from a real 64/68Hz card**, since the sync/blanking/porch timings are a product of 1986-era hardware design choices, not a standard later-era VESA mode. Guessing at "similar" timings risks producing a signal the original FPD monitor (or a monitor emulating it) won't lock onto.

### Phase 0 — Signal capture & characterization (new, precedes Phase 1)

**Decision: single fixed timing mode.** The clone targets **Crystal 1 (55.000 MHz)** only:
- Horizontal scan rate: 68.75 kHz
- Vertical refresh: 67.139 Hz
- Total pixels/line: 800 (640 active + 160 blanking)
- Total lines/frame: ~1024 (864 active + ~160 blanking)

This crystal is selected because its nominal rates resolve cleanly to round binary-friendly totals, consistent with straightforward TTL counter-based sync generation — the most likely candidate for the board's actual native/primary mode. Crystal 2 (dual-mode support) is out of scope for this clone.

**Action items (single-mode capture, simplified from prior dual-mode plan):**
- Capture real horizontal sync, vertical sync, and pixel clock timing from an actual 64/68Hz card operating in the 55.000 MHz / 68.75kHz / 67.139Hz mode, using a logic analyzer/oscilloscope
- Confirm exact horizontal blanking split (front porch / sync pulse / back porch, out of the 160-pixel blanking total) and vertical blanking split (out of the ~160-line total) — the 800×1024 totals are inferred from nominal rates, but the internal timing breakdown still requires direct measurement
- Document sync pulse widths and polarity (positive vs. negative sync)
- Deliverable: a single timing table (equivalent to one VESA mode entry) for this specific fixed mode, to drive the FPGA's sync generator

### Phase 0.5 — Physical tap point & interface identification (new)

Separate from signal *timing* characterization (Phase 0), this phase identifies *where* and *how* to physically divert the page buffer output from the original card to the FPGA, without disturbing the card's own display path.

**Hardware setup:**
- "Tested good" card: reference/baseline, kept intact and unmodified — used for continuity-verified schematic development and as a known-good comparison signal source
- Project-box spare card: used for invasive work (tapping, soldering, probing) since it's expendable

**Signals to identify at the display connector (TTL interface):**
- Serial pixel data (1-bit monochrome — likely single data line)
- Pixel clock (PCLK)
- Horizontal sync (HSYNC)
- Vertical sync (VSYNC)
- Possible blanking/display-enable line (BLANK/DE) — confirm whether this exists as a discrete signal or must be reconstructed from sync timing in the FPGA

**Key considerations:**
- **Logic levels:** confirm TTL voltage (likely 5V). If FPGA I/O is not 5V-tolerant, tapped signals need level-shifting (buffer IC recommended over a resistor divider, to avoid signal degradation)
- **Non-invasive tap:** use a high-impedance buffer (e.g., 74LVC245 or similar) to tap signals without loading the original circuit — needed since the original card should continue driving the FPD monitor unaffected, if that's still a project goal, while simultaneously feeding the FPGA
- **Tap point location:** identify the point after the page buffer's shift-register/serializer (where pixel data is already serial, sync'd to PCLK) rather than tapping earlier in the pipeline where data may still be in a different form

**Deliverable:** an annotated schematic (extending the continuity-tracing work already underway) identifying exact pin/trace locations for each signal, plus a level-shifting/buffering plan for the FPGA interface.

## 1. Problem Statement (updated)

The (cloned) video card outputs a portrait-orientation raster scan (row-major, top-to-bottom, left-to-right within each row) at 640×864, 1-bit monochrome, using the original 1986 Radius 64/68Hz board's native timing. The physical display panel is a landscape-native modern device (e.g., HDMI monitor), driven via the FPGA project board. A compensating 90° rotation must be applied between source and panel so the image displays right-side-up.

## 2. Confirmed Coordinate Transform

Verified against a worked 4×7 (W×H) example, pixel-by-pixel, against both terminal points (top-right and bottom-right source corners).

**Source (portrait):** width `W`, height `H`, pixel addressed `(x, y)`
- `x ∈ [0, W-1]` (column)
- `y ∈ [0, H-1]` (row)
- Scanned row-major, top to bottom

**Output (landscape buffer):** width `H`, height `W` (dimensions swap)
- Pixel addressed `(x', y')`

**Transform (rotation direction: clockwise):**

```
x' = (H - 1) - y
y' = x
```

### Worked example (W=4, H=7)

| Source (x,y) | Pixel # | Output (x',y') | Output # |
|---|---|---|---|
| (0,0) | p01 | (6,0) | x07 |
| (0,6) | p25 | (0,0) | x01 |
| (3,0) | p04 | (6,3) | x28 |
| (3,6) | p28 | (0,3) | x22 |

All four corners confirmed consistent with the formula above.

## 3. Buffering Requirement (Critical)

This is **not** a line-buffer-compatible transform. Output row `y'=0` requires the first pixel of *every* source row — i.e., the entire first source column — which isn't complete until the last source row has been received.

**Implication:** a full source frame must be buffered before any output row can be emitted.

- Minimum: 1 frame buffer (adds ~1 frame time of latency)
- Recommended: 2 frames (ping-pong / double buffer) to avoid tearing while the next frame is being written while the current one is being read out rotated
- Latency estimate: at 60 Hz, ~16.7 ms minimum (single buffer), ~33 ms typical (double buffer)

## 4. Generalization to Production Resolution

The formula is resolution-independent — substitute actual `H` (source height) for production use:

```
x' = (H_actual - 1) - y
y' = x
```

**Action item:** confirm actual production resolution and aspect ratio before implementation. The 4×7 example was a worked proof-of-concept only; do not hardcode `H=7` into production code.

## 5. Production Configuration: 640×864 Portrait → 1024×768 Landscape (Centered)

**Confirmed:** source is W=640 (wide) × H=864 (tall). Target panel is 1024×768, physically mounted rotated so the viewer sees a portrait image. Since the rotated source (864×640) is smaller than the panel's native landscape resolution (1024×768), it must be centered with a border.

### Step 1 — Rotate (CW)

Using the general formula from Section 4 with `H=864`:
```
x' = 863 - y
y' = x
```
Produces a rotated buffer of 864 (wide) × 640 (tall).

### Step 2 — Center in 1024×768

- Horizontal border: `(1024 - 864) / 2 = 80` px, left and right
- Vertical border: `(768 - 640) / 2 = 64` px, top and bottom

### Combined transform — source `(x, y)` → final panel address `(X, Y)`

```
X = 943 - y (= 863 - y + 80)
Y = x + 64
```
Valid for `x ∈ [0,639]`, `y ∈ [0,863]` → `X ∈ [80,943]`, `Y ∈ [64,703]`. Pixels outside this box are border and need an explicit fill value (e.g., black).

### FPGA Implementation Notes

- **Frame buffer sizing (confirmed 1-bit depth):** source frame = 640 × 864 = 552,960 pixels = 69,120 bytes (~68 KB) at 1 bpp. Double-buffered: ~135 KB total — very likely fits on-chip BRAM on most mid-size FPGA boards, avoiding external DRAM/memory controller complexity.
- **Clock domain crossing:** source pixel clock (native 1986 64/68Hz board timing — to be measured in Phase 0) and 1024×768 HDMI output clock (~65 MHz for standard 60 Hz XGA timing) are independent domains. The frame buffer must be implemented as a proper dual-clock RAM/FIFO, not a single-clock design, to avoid metastability.
- **Border fill:** decide and hardcode a fill value (black recommended) for the 80px/64px borders; gate output explicitly during border regions rather than leaving them undefined.
- **HDMI timing:** 1024×768 is a standard VESA mode with well-documented timing tables — confirm target refresh rate (60 Hz assumed) since it sets the pixel clock and output-side buffer read rate.

## 7. Target Display Selection

**Primary target (this spec): 1024×768** — 15" DVI panel already in hand, HDMI→DVI converted. This is the configuration detailed in Section 5 (centered, no scaling required).

**Considered alternative: 1280×1024** — inexpensive, widely available LCD panels. Deferred to a future phase for the following reason: centering the 864×640 rotated image alone leaves large unused borders (208px sides, 192px top/bottom), so a scaled-up image would be preferable — but scaling a 1-bit monochrome image cleanly requires nearest-neighbor pixel replication (no interpolation possible), and 864×640 doesn't scale by a clean integer factor into 1280×1024. This needs a Bresenham-style pixel-replication accumulator, added frame-buffer read bandwidth, and an extra pipeline stage — nontrivial additional scope.

**Recommendation:** build and validate the 1024×768 centered (no-scaling) pipeline first as the primary deliverable. Treat 1280×1024 (with scaling) as a follow-on enhancement once the core rotation/timing pipeline is proven, not a parallel initial target.

## 8. Open Items Before Implementation

- [x] Confirm production source resolution — **confirmed: 640×864 (W×H), 1986 Radius 64/68Hz board**
- [x] Confirm color depth — **confirmed: 1-bit monochrome**
- [x] Single vs. dual timing mode — **confirmed: single fixed timing, targeting the 55.000MHz/68.75kHz/67.139Hz crystal 1 mode**
- [x] Target display for initial build — **confirmed: 1024×768 15" panel, centered, no scaling (primary); 1280×1024 scaled deferred to future phase**
- [ ] Capture and document exact horizontal/vertical blanking breakdown (front porch/sync/back porch) for the fixed mode (Phase 0)
- [ ] Identify exact tap point for pixel data, PCLK, HSYNC, VSYNC (and BLANK/DE if present) on the display connector (Phase 0.5)
- [ ] Confirm TTL logic voltage and FPGA I/O compatibility; select level-shifting/buffering approach
- [ ] Decide whether original card must continue driving its own FPD monitor simultaneously (affects whether tap must be non-loading/high-impedance) or whether the original display path can be fully diverted
- [ ] Confirm target refresh rate for 1024×768 output (60 Hz assumed)
- [ ] Confirm frame buffer implementation: on-chip BRAM (now very likely sufficient at 1bpp) vs. external memory
- [ ] Confirm latency budget tolerates ~1–2 frames of added delay — flag if this is a touch/interactive/real-time application
- [ ] Verify rotation direction (CW, as derived here) against actual physical mount orientation on real hardware
- [ ] Decide and implement border fill value/logic for the 80px/64px letterbox regions
- [ ] Clarify what "64" and "86" refer to in the board's name (vertical refresh? horizontal scan rate? two distinct modes?) — relevant to Phase 0 timing capture
 

Trash80toG4

Active Tinkerer
Apr 1, 2022
1,453
463
83
Bermuda Triangle, NC USA
Couldn't have said it better myse . . . that way at all! :ROFLMAO: Hope this makes sense to the gang here.

Claude figured out the need for dual-buffering with a single frame delay before I could tell him. Maybe triple-buffering with a two frame delay will be required?

Here's my "Worked example (W=4, H=7)" image from the earlier threads:

FPD-Rotation-004.jpg


I input the IF/Then parameters for the top row and left column of the Portrait setup which translated to the top row and right column of the "Standard Landscape Display" setup and it was a done deal!


edit: never did fix that 75Hz error!
 
Last edited: