Skip to content

Latest commit

 

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BlitCRT

A programmable 15kHz video card in an FPGA. It streams blits rather than buffering frames.

Author: Ben Templeman (alphanu1)

BlitCRT turns a Cyclone IV FPGA (Waveshare CoreEP4CE10 on a DVK600) into a 15kHz arcade-monitor video card driven over a USB FIFO. It speaks the CRT1 protocol, the same wire protocol as CRTPi, the Raspberry Pi 4 version of this idea (github.com/alphanu1/rpi-video-card). GroovyMAME and RetroArch (MME4CRT) via switchres drive either device with the same host code:

MME4CRT / switchres  ->  CRT1  ->  BlitCRT (FT2232H FIFO)
                               ->  Pi2SCART DAC  ->  15kHz CRT

BlitCRT is the device (the FPGA video card). CRT1 is the wire protocol, which CRTPi speaks too. CRTPi does the same job on a Raspberry Pi 4 instead of an FPGA; both present as a USB video card for 15kHz CRTs.

The test card

On power-up, and until the first frame arrives, BlitCRT paints a test card from the timing generator alone. No host, no RAM:

BlitCRT test card

It shows SMPTE colour bars, a 1px white border, and a two-line readout of the current resolution and the measured vertical refresh and pixel clock, e.g. 320x240 / 60.01HZ 6.400MHZ. The refresh and clock are measured on-device against the 50MHz crystal (rtl/mode_meter.v), not taken from the host. They reflect the achieved timing, including PLL quantization. Switching resolutions over UART re-renders the card with updated numbers on each SET_MODE. A mode can be confirmed on the CRT this way without ever streaming a frame. The card gives way to live video on the first CMD_FRAME.

Simulated output

These images are rendered from the RTL. A testbench runs the video_timing_prog and splash_pattern modules, captures the RGB value the design drives at every active pixel of a frame, and writes it to a PNG. Three modes are shown. The readout tracks each resolution and its measured fractional refresh (59.94, 50.17) and pixel clock.

320x240 @ 60.01Hz 256x224 @ 59.94Hz 384x288 @ 50.17Hz
320x240 256x224 384x288

Regenerate with sim/tb_dump.v (see docs/VIDEOCARD_V2.md).

Simulation waveforms

Timing diagrams from the same simulation. sim/tb_wave.v dumps a VCD of video_timing_prog and splash_pattern at the 6.4MHz pixel clock.

Scanline: data-enable, the RGB bus stepping through the bars, and the hsync pulse in blanking. One line is 63.6us, a 15.7kHz line rate.

scanline waveform

Frame: the hsync train, one pulse per line, with a single vsync pulse per frame. Vsync is every 16.66ms, i.e. 60Hz.

frame waveform

Blit streamer, not a framebuffer

BlitCRT holds no frame queue. The host does not hand it whole frames to buffer and display later. Instead the host pushes damage rectangles over CRT1 (a CMD_FRAME with x/y/w/h and pixels), and BlitCRT writes those bytes into the live scanout buffer as they arrive. The timing generator reads that same buffer out to the CRT continuously. Writes land in the region being scanned out, in place. There is no separate present or flip step.

Latency is roughly the wire-transfer time, about 6.6ms over the sync FIFO. That is under half a frame, versus the one or two frames a buffered path adds. Partial updates are cheap. A game touching a quarter of the screen sends a quarter of the bytes, because damage rectangles are part of the protocol. There is no reordering or queue management to reason about.

A classic framebuffer card writes a full frame to off-screen memory, then a flip at vsync swaps it in. That adds at least one frame of latency and needs double-buffering.

Source layout

rtl/          Verilog source
  fb_top_v2.v         top level
  crt1_ft245.v        CRT1 packet engine (SET_MODE/SET_PLL/FRAME/PAL)
  ft245_rx.v          FT245 async FIFO read engine
  ft245_tx.v          FT245 async FIFO write engine (reply path)
  uart_rx.v           UART byte source for bring-up
  video_timing_prog.v runtime-programmable timing generator
  framebuffer.v       4bpp dual-clock line/scanout buffer
  palette.v           16 x RGB444 lookup
  splash_pattern.v    test card (bars, border, res/Hz/clock readout)
  mode_meter.v        measures refresh + pixel clock from frame period
  pll_ctl.v           runtime pixel-clock reconfiguration sequencer
sim/          iverilog testbenches
quartus/      pins_pi2scart.tcl, SDC, templates
host/         C client (crt1.c/.h, crt1_ftdi.c), Python, ref_sweep.py
integration/  MME4CRT reference backend
docs/         VIDEOCARD_V2, QUARTUS_BUILD, PI2SCART_OUTPUT

The -wired bundle adds the generated Quartus project and the two ALTPLL megafunctions to the same tree. It compiles without regenerating IP.

The CRT1 protocol (summary)

12-byte header (magic 0x31545243 "CRT1", cmd, flags, seq, len LE) then a command payload. Host to device: GET_INFO, SET_MODE (32-byte wire modeline, switchres position semantics), SET_PLL (m/n/c dividers computed host-side), FRAME (x/y/w/h damage rect and pixels), SET_PAL. Device to host: EVT_INFO, EVT_MODE_RESULT (achieved clock readback), EVT_STATUS. Full spec in docs/ here, and in CRTPi's docs/PROTOCOL.md (github.com/alphanu1/rpi-video-card), which defines the same wire format.

Hardware I/O

  • Video out: RGB666 plus separate H/V sync on DVK600 32I/Os_2 holes 1..20, into a Pi2SCART resistor-ladder DAC. See docs/PI2SCART_OUTPUT.md for the pin/ball/ribbon map and the 5V safety note.
  • Host link, selected by the USE_UART build parameter:
    • USE_UART=1 (default): CRT1 bytes over a plain USB-serial adapter (FTDI/CP2102/CH340) on one pin (8I/Os_1 hole 1, PIN_F11), 2Mbaud = 0.2MB/s. This is for bring-up and resolution-change testing, not video streaming. A mode switch (SET_PLL+SET_MODE, ~0.65ms) and a static test frame go through fine. A full 320x240 4bpp frame takes ~192ms (about 5fps), too slow for live emulator output.
    • USE_UART=0: FT2232H FT245 FIFO on 32I/Os_2 holes 21..32. Async today (~1MB/s), enough for 60Hz partial-frame updates from MME4CRT. The sync FIFO (~40MB/s) adds headroom for full-frame 60Hz and, later, direct-colour host feeds (RGB565/RGB888, see Future development). Both links are assigned to distinct pins, so a build can select either by the USE_UART parameter without changing pins. They feed the same packet engine. Nothing downstream changes.
  • Clock: on-board 50MHz oscillator (PIN_E16). Reset on PIN_B16.

Full pin mapping

All signals land on the DVK600 32I/Os_2 bank, which the CoreEP4CE10 H_Up header feeds straight through. Hole numbers below are the silkscreen labels on the header. Wire the ribbon from those holes.

Video (always used), holes 1..20

signal      hole   EP4CE10   Pi2SCART target
vid_r6[5]    1      PIN_R11   R7  (Red MSB)
vid_r6[4]    2      PIN_N12   R6
vid_r6[3]    3      PIN_P11   R5
vid_r6[2]    4      PIN_N11   R4
vid_r6[1]    5      PIN_P9    R3
vid_r6[0]    6      PIN_N9    R2  (Red LSB)
vid_g6[5]    7      PIN_R10   G7  (Green MSB)
vid_g6[4]    8      PIN_T11   G6
vid_g6[3]    9      PIN_R9    G5
vid_g6[2]   10      PIN_T10   G4
vid_g6[1]   11      PIN_R8    G3
vid_g6[0]   12      PIN_T9    G2  (Green LSB)
vid_b6[5]   13      PIN_R7    B7  (Blue MSB)
vid_b6[4]   14      PIN_T8    B6
vid_b6[3]   15      PIN_R6    B5
vid_b6[2]   16      PIN_T7    B4
vid_b6[1]   17      PIN_R5    B3
vid_b6[0]   18      PIN_T6    B2  (Blue LSB)
vid_hs_n    19      PIN_R4    HSync
vid_vs_n    20      PIN_T5    VSync

The Pi2SCART's RGB pins are not in numeric order on its connector. Each colour bit goes to a specific pin. docs/PI2SCART_OUTPUT.md lists the CN3 pin per bit.

Host links (both assigned; USE_UART selects which is active)

The UART and the FT245 FIFO are on separate pins. One build can pick either link with the USE_UART parameter, with no pin change. Wire only the one the build uses.

UART (USE_UART=1 default), one wire from a USB-serial adapter, on its own pin outside the 32I/Os_2 bank:

signal        header/hole      EP4CE10   wire to
uart_rx_pin   8I/Os_1 hole 1   PIN_F11   USB-serial TX (plus GND to board GND)

FT245 FIFO (USE_UART=0), 32I/Os_2 holes 21..32:

signal      hole   EP4CE10   FT2232H (channel A, BDBUS)
ft_data[0]  21      PIN_R3    D0
ft_data[1]  22      PIN_T4    D1
ft_data[2]  23      PIN_M9    D2
ft_data[3]  24      PIN_T3    D3
ft_data[4]  25      PIN_K9    D4
ft_data[5]  26      PIN_L9    D5
ft_data[6]  27      PIN_L8    D6
ft_data[7]  28      PIN_K8    D7
ft_rxf_n    29      PIN_M7    RXF#
ft_txe_n    30      PIN_M8    TXE#
ft_rd_n     31      PIN_M6    RD#
ft_wr_n     32      PIN_L7    WR#

System: clk50 = PIN_E16 (on-board osc), rst_n = PIN_B16 (RESET key).

The UART now has its own pin (PIN_F11) and no longer shares a hole with ft_data[0]. Still run one link at a time: do not connect a USB-serial adapter and an FT2232H together. On the Pi2SCART side, never wire its 5V pins (header 2/4) to any FPGA pin. The I/O is 3.3V.

Throughput

Frames are streamed in the native 4bpp packed format (a 16-colour palette index per pixel; the palette expands to the RGB666 DAC output). A 320x240 frame is 38.4KB, so 60Hz needs 2.3MB/s. The links:

link                 rate       320x240 4bpp frame   4bpp 60Hz full-frame?
UART 2Mbaud          0.2 MB/s   192 ms  (~5 fps)     no
FT245 async          ~1 MB/s    38 ms   (~26 fps)    no, but partials fine
FT245 sync FIFO      ~40 MB/s   ~1 ms                yes, wide margin

A mode change is small: SET_PLL+SET_MODE is ~196 bytes, 0.65ms over UART. UART is therefore fine for switching resolutions and painting a static frame, but too slow for live video. The FT2232H sync FIFO carries full 4bpp frames at 60Hz with margin to spare (2.3MB/s needed against ~40MB/s). Async FT245 (~1MB/s) handles partial-frame (damage-rect) updates at 60Hz and full frames at a reduced rate, but not full-frame 60Hz. Direct-colour host feeds raise the bar: RGB565 is 9.2MB/s and RGB888 18.4MB/s at 60Hz, both firmly in sync-FIFO territory (and gated on external RAM, see Future development). Output is RGB666 either way.

Clock accuracy

PLL quantization is under 10.3ppm across the arcade clock battery. The reference oscillator's own tolerance dominates. The suggested upgrade is a +/-1ppm 50MHz TCXO. The target frequency choice is under 1ppm either way (see host/ref_sweep.py). Exact synthesis via an Si5351A is a later option. Details in docs/VIDEOCARD_V2.md.

Build and bring-up

  1. Generate the two ALTPLL megafunctions in Quartus (docs/QUARTUS_BUILD.md), or use the -wired bundle which already has them.
  2. source quartus/pins_pi2scart.tcl in the Tcl console.
  3. Add an SDC (create_clock 50MHz, derive_pll_clocks; quartus/fb_top.sdc).
  4. Start Compilation, then flash the .sof over JTAG.
  5. Power on. The test card appears on the CRT with no host attached. That covers PLL lock, timing, the DAC and the ribbon in one step.
  6. To test resolution switching from the PC (USE_UART=1): wire a USB-serial adapter's TX to 8I/Os_1 hole 1 (PIN_F11) and GND to board GND, then run host/crt1_uart_test.py PORT . This confirms mode switching and a painted frame. It is not fast enough for live video. For 60Hz video, build with USE_UART=0 and use the FT2232H sync-FIFO path.

Future development

More simultaneous colours. The DAC output is already RGB666 (18-bit). Each pixel is currently a 4-bit index into a 16-entry palette. Only 16 colours appear at once. A direct-colour mode stores full colour per pixel and drops that limit: each pixel holds RGB666 directly, or a host feed in RGB565/RGB888 reduced to RGB666 at output. The FT245 sync FIFO has the bandwidth. A direct-colour frame is 3 bytes/pixel: 13.8MB/s at 320x240 60Hz, 19.9MB/s at 384x288, both under the ~40MB/s ceiling.

What gates it is on-chip RAM. The scanout buffer is currently EP4CE10 block RAM (~414 Kbit total). 320x240 at 4bpp is 300 Kbit and fits. A direct-colour frame at 3 bytes/pixel is 1800 Kbit (320x240) or 2650 Kbit (384x288). Both are well over budget and need external memory. The CoreEP4CE10 has no onboard SDRAM. The fix is Waveshare's SDRAM Board (B) on the DVK600 (H57V1262GTR, 8M x 16-bit = 16 MB), or an equivalent, plus an SDRAM controller and the clock-domain work to feed scanout from it. The controller is the work. Capacity is not a concern. A single frame is 225 KB (320x240) or 324 KB (384x288), against 16 MB on the board, room for around 50 frames.

Higher resolutions need the same external memory, and width triggers it before colour depth does. 640x480 60i is a valid 15kHz mode: it is two 640x240 fields woven together at 60 fields per second. The line rate stays ~15.7kHz, the same as 320x240 240p, which is what lets a 15kHz CRT display it. The pixel clock roughly doubles to ~12.6MHz for the extra horizontal pixels, which is an easy PLL target, and the bandwidth is fine (4.6MB/s at 4bpp, 27.6MB/s direct-colour, both within the sync FIFO). The blocker is buffer RAM. A single 640-wide field at 4bpp is 600 Kbit, already over the ~414 Kbit on-chip budget. Any 640-wide mode needs SDRAM even at 4bpp and even interlaced. In SDRAM it is small: a 640x480 4bpp frame is 150 KB, direct RGB666 is 900 KB, against 16 MB on the board. The timing generator already takes arbitrary modelines. Interlace adds a field toggle in the vertical counter. The work is the SDRAM path, not the timing.

Adding SDRAM does not turn BlitCRT into a framebuffer. The blit-streamer model is defined by the write path, not by where the pixels live: the host sends damage rectangles, the device writes them into the live scanout region, and scanout reads the same memory continuously, with no present or flip step. A single RGB666 frame in SDRAM keeps exactly that model and the same latency, just with full per-pixel colour. Double-buffering (450 KB, still a fraction of the board) is an option later if tearing on large fast updates becomes visible. It copies dirty regions rather than presenting a whole frame, and stays a blit streamer. A true framebuffer with a vsync flip would mean dropping the damage-rect model and the low-latency behaviour, and is not planned.

The Pi2SCART DAC stays at RGB666 (6 bits per channel). A host sending RGB888 is truncated or dithered to RGB666 at output. True 8-bit-per- channel output would need a wider DAC than the Pi2SCART ladder.

Other items: sync-FIFO FT2232H bring-up (EEPROM to 245 FIFO mode plus a clocked FIFO in fabric), and a +/-1ppm TCXO or Si5351A for exact clocks (see Clock accuracy).

Status

RTL complete and simulated (testbenches for the UART and FT245 paths, the mode meter, and PLL reconfig). Megafunctions generated and wired. The top elaborates clean. Pins assigned. Remaining: full Quartus compile and hardware bring-up, then the sync-FIFO upgrade for full-frame 60Hz headroom and, with external RAM, direct-colour modes (see Future development).

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages