Skip to content
lcapossioPublic

About

A synthesizable Video Test Pattern Generator IP core. Outputs pixels over an AXI4-Stream master interface and is configured at runtime via an AXI4-Lite slave register interface.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Latest commit

 

History

103 Commits

Folders and files

Repository files navigation

vtpgZero — Video Test Pattern Generator

A synthesizable Verilog-2001 video test pattern generator IP core. Outputs pixels over an AXI4-Stream master interface and is configured at runtime via an AXI4-Lite slave register interface. An optional adapter converts that stream to a classic parallel video interface (FVAL/LVAL/DVAL) for frame grabbers and camera-link style sinks.

vtpgZero output streamed live over Ethernet as MJPEG

Index

What this project does

vtpgZero produces video frames of various test patterns at programmable resolution, pixel format, and bit depth. It can free-run from an internal divider or be locked to an external frame-sync signal. The output stream is standard AXI4-Stream with backpressure (tready), tlast = end of line, and tuser = start of frame.

It ships in two flavors:

  • vtpgz_core — direct port-driven core. No AXI slave; drive the cfg_* fields from your own RTL (or tie them to constants for a fully static build). The smallest, most portable form.
  • vtpgz_axilite_top — thin wrapper that adds an AXI4-Lite slave so a CPU/host can configure the core at runtime. This is what the Arty A7-100T demo uses.

Both have identical pattern/output behavior — they only differ in how the cfg_* fields get set.

A third, optional module sits after either of them:

  • vtpgz_axis_to_flvdval — converts the AXI4-Stream output to parallel FVAL/LVAL/DVAL timing. It is a wire-level adapter with no FIFO, so it ties tready high and requires the source to free-run gap-free; that is exactly what vtpgZero does when nothing stalls it. See Parallel video out.

↑ back to top

Features

  • 8 patterns: 8-bar color bars (100% or 75%), horizontal gradient, vertical gradient, checkerboard, solid color, crosshatch/grid, color ramp, LFSR pseudo-random noise.
  • SMPTE RP 219 HD color bars (build-time EN_SMPTE, off by default): the full chart, with 75% bars, -I/+Q, a Y ramp and PLUGE, at any resolution and pixels-per-clock. See SMPTE RP 219 color bars.
  • Bouncing box overlay: optional animated box drawn on top of any pattern. Color, size, speed, and border are runtime-configurable; it bounces off the frame edges. Stripped at elaboration when EN_MOVING_BOX=0.
  • 3 build-time output modes (pick one at synthesis):
    • RGB — RGB888 / RGB101010 / RGB121212
    • RAW — single-component: plain monochrome, or any of the 4 standard Bayer mosaics (RGGB / BGGR / GRBG / GBRG). Smallest configuration, useful as an image-sensor emulator.
    • YUV — native 4:4:4 or 4:2:2 (patterns produce {Y,Cb,Cr} directly, no runtime color conversion). Selectable colorimetry: BT.601 or BT.709, full or limited (studio) range.
  • 5 bit depths (build-time): 8 / 10 / 12 / 14 / 16 bits per component. Patterns render at 12-bit precision; the pack stage truncates LSBs for BPC<12 and zero-extends for BPC>12.
  • Pixels-per-clock (1/2/4/8): pack 1, 2, 4, or 8 horizontally-adjacent pixels into each AXI4-Stream beat for higher line bandwidth. All patterns and output modes are supported; PIXELS_PER_CLOCK=1 (default) is byte-identical to prior releases. At 8 pixels per clock it runs 7680x4320 at 60 fps on a KV260, verified frame by frame on silicon.
  • Auto-derived tdata width — the smallest multiple-of-8 that fits the active components for the chosen mode/bpc. No manual sizing needed.
  • AXI4-Lite slave for runtime configuration (19 writable registers, 22 with EN_SMPTE=1).
  • AXI4-Stream master output with full backpressure support.
  • Parallel video out (FVAL/LVAL/DVAL): optional vtpgz_axis_to_flvdval adapter presenting the classic frame-grabber timing interface, with FIELD_ID for interlaced sources and configurable FVAL front/back porches. No FIFO — it requires a gap-free source and latches a sticky TIMING_ERR if that ever breaks, rather than emitting a corrupted raster. 9 LUT / 5 FF at zero porch.
  • Interlaced video (build-time EN_INTERLACE): per-field SOF plus a fid field-ID sideband and a fid_in lock input, following the AMD/Xilinx v_tpg (PG103) / UG934 convention. Stripped at elaboration when EN_INTERLACE=0.
  • Frame sync: internal (clock divider) or external (rising-edge input).
  • Build-time pattern selection: each pattern is gated by an EN_* parameter and stripped from the netlist when set to 0.
  • Pure Verilog-2001, no SystemVerilog, no vendor primitives.
  • 100% Verilator code coverage (line + toggle) under the bundled testbench, plus a byte-exact RTL↔model gate and on-silicon hardware verification.

↑ back to top

Register map (AXI4-Lite, 32-bit)

Offset Name Description
0x00 CORE_ID RO fixed magic 0x47505456 = ASCII "VTPG" little-endian. Read this first to confirm you're talking to a vtpgZero core.
0x04 VERSION RO {major[8], minor[8], patch[16]}
0x08 CONTROL [0] enable, [1] sw_fsync, [2] ext_sync, [3] interlace
0x0C STATUS RO [0] busy, [1] field_id, [15:8] frame_count
0x10 IMG_WIDTH active pixels per line
0x14 IMG_HEIGHT active lines per frame (per field in interlaced mode — see Interlaced video)
0x18 PATTERN_SEL 0=colorbar 1=hgrad 2=vgrad 3=checker 4=solid 5=smpte 6=grid 7=ramp 8=noise 9=image. 10..15 are unused and render black.
0x1C COLOR_FORMAT RO build-time configuration mirror: [1:0]=output_mode (0=RGB 1=RAW 2=YUV), [2]=yuv_subsample (0=444 1=422), [5:3]=raw_bayer (0=PLAIN 1=RGGB 2=BGGR 3=GRBG 4=GBRG), [6]=rgb_order (0=Xilinx 1=legacy), [15:8]=BPC (8/10/12/14/16), [31:16]=TDATA_WIDTH
0x20 SOLID_COLOR {8'h0, R[8], G[8], B[8]}
0x24 BOX_COLOR moving box color
0x28 BOX_SIZE {width[16], height[16]}. Each half is clamped by the core to 1 <= size <= IMG_WIDTH/IMG_HEIGHT, so a zero field renders a 1x1 box rather than disabling the overlay. Zero is not a supported configuration (it is also a divide-by-zero in the BOX_IMG_*_STEP host precompute); use BOX_COLOR equal to the background to make the overlay invisible.
0x2C BOX_SPEED {dx[16], dy[16]} pixels per frame
0x30 PIXELS_PER_CLOCK RO build-time pixels-per-AXI-beat (1/2/4/8)
0x34 GRID_SPACING grid line spacing in pixels
0x38 GRID_COLOR grid line color
0x3C CHECKER_SIZE checkerboard square size in pixels
0x40 FRAME_RATE_DIV clocks per frame for internal sync mode
0x44 BAR_WIDTH colorbar width in pixels (host writes img_width/8)
0x48 HG_STEP [27:0] horizontal-gradient (and RAMP) step per pixel, Q12.16 12-bit codes. Host writes ceil((0xFFF << 16) / (IMG_WIDTH - 1)).
0x4C VG_STEP [27:0] vertical-gradient step per line, Q12.16. Host writes ceil((0xFFF << 16) / (IMG_HEIGHT - 1)).
0x50 BOX_BORDER {border_width[8], border_color[24]}. Border is drawn inside the box; border_width=0 means no border.
0x54 BOX_IMG_X_STEP Q16 nearest-neighbour step for the box-image overlay. Host writes (BOX_IMAGE_W << 16) / BOX_SIZE.width whenever BOX_SIZE changes. Only meaningful when EN_BOX_IMAGE=1.
0x58 BOX_IMG_Y_STEP Q16 step for the box-image overlay y-axis. Host writes (BOX_IMAGE_H << 16) / BOX_SIZE.height.
0x5C STREAM_ROUTE {tdest[15:0], tid[15:0]} driven onto m_axis_tdest / m_axis_tid, constant across every beat. Only meaningful when TID_WIDTH/TDEST_WIDTH > 0; otherwise the sidebands are absent and this register is inert.
0x60 SMPTE_GEOM0 {bar_c[16], side_d[16]} SMPTE RP 219 bar width and side-panel width. Host writes c = (3*IMG_WIDTH + 14) / 28 (3W/28 rounded), d = (IMG_WIDTH - 7c) / 2. Only exists with EN_SMPTE=1 (reads 0 otherwise). See SMPTE RP 219 color bars.
0x64 SMPTE_GEOM1 {pluge_p[16], row_h[16]} PLUGE step width and row unit. Host writes p = (c + 1) / 3, h = IMG_HEIGHT / 12. EN_SMPTE=1 only.
0x68 SMPTE_RAMP_STEP [15:0] Y-ramp step, 1/256 of a 12-bit code per pixel. Host writes ceil((4095 << 8) / (5c - 1)). EN_SMPTE=1 only.

AXI4-Lite writes honor WSTRB byte lanes. A write with WSTRB=0 acknowledges but leaves the addressed register unchanged, including CONTROL. The COLOR_FORMAT register (0x1C) is read-only and reflects the build-time configuration so software can probe it.

↑ back to top

How to use it

Everything is portable Verilog-2001 with no vendor primitives:

rtl/vtpgz_defs.vh        `include this header (or add rtl/ to your include dirs)
rtl/vtpgz_core.v         the substance: timing engine, patterns, inline pack
                         stage, AXIS output. Configured via cfg_* input ports.
rtl/vtpgz_axil_regs.v    AXI4-Lite slave + register file (only needed with the
                         AXI-Lite wrapper)
rtl/vtpgz_axilite_top.v  thin wrapper that adds the AXI4-Lite slave on top of
                         vtpgz_core for runtime control

A vtpgz_core-only build pulls in just vtpgz_defs.vh and vtpgz_core.v. The AXI-Lite flavor adds the other two files.

Build-time parameters

Parameter Default Effect
C_S_AXI_ADDR_WIDTH 8 AXI4-Lite address width (8 = 256 B, enough for all regs)
C_S_AXI_DATA_WIDTH 32 AXI4-Lite data width (only 32 supported)
EN_COLORBAR … EN_NOISE 1 Per-pattern enables. Set to 0 to strip that generator from the netlist. At least one must remain enabled.
EN_MOVING_BOX 1 Bouncing-box overlay. When 1, the box is drawn on top of any active pattern.
EN_SMPTE 0 SMPTE RP 219 HD color bars as PATTERN_SEL=5. 0 (default) strips the pattern and its three geometry registers. See SMPTE RP 219 color bars.
EN_IMAGE 0 Embed a synth-time image as PATTERN_SEL=9 (inferred BRAM). Stripped when 0. See Image patterns.
EN_BOX_IMAGE 0 Paint an embedded image inside the moving box. Requires EN_MOVING_BOX=1. See Image patterns.
IMAGE_W / IMAGE_H 128 Source image size in BRAM (powers of two). Plus IMAGE_OUT_W/H, IMAGE_HEX_FILE — see Image patterns.
BOX_IMAGE_W / BOX_IMAGE_H 32 Box-image source size. Plus BOX_IMAGE_HEX_FILE.
OUTPUT_MODE 0 (RGB) 0 = RGB; 1 = RAW; 2 = YUV. See Output modes.
YUV_SUBSAMPLE 0 (444) Only for OUTPUT_MODE=2. 0 = 4:4:4; 1 = 4:2:2.
YUV_RANGE 0 (full) Only for OUTPUT_MODE=2. 0 = full range (0..max); 1 = limited/studio range (Y 64..940, C 64..960 at 10 bits). See Output modes.
YUV_MATRIX 0 (BT.601) Only for OUTPUT_MODE=2. 0 = BT.601 (Kr=0.299, Kb=0.114); 1 = BT.709 (Kr=0.2126, Kb=0.0722). Selects the colorbar palette.
BAR_LEVEL 100 Colorbar level in percent, any output mode: 100 or 75 (in YUV, the SMPTE RP 219 bars). Selects the colorbar palette.
RAW_BAYER 1 (RGGB) Only for OUTPUT_MODE=1. 0 = plain monochrome; 1 = RGGB; 2 = BGGR; 3 = GRBG; 4 = GBRG.
RGB_ORDER 0 (Xilinx) Component order in tdata. 0 = {pad,B,G,R} (Xilinx PG044); 1 = {R,G,B,pad} legacy MSB-first.
BPC 8 Bits per component: 8, 10, 12, 14, or 16.
PIXELS_PER_CLOCK 1 Pixels per AXI-Stream beat (1/2/4/8). See Multi-pixel-per-clock.
EN_INTERLACE 0 Add interlaced-video support (fid / fid_in, CONTROL[3]). 0 (default) strips every field register and ties fid / STATUS[1] to 0, so no interlace logic is generated (the ports remain, as constants). See Interlaced video.
TID_WIDTH / TDEST_WIDTH 0 Add optional AXI4-Stream routing sidebands m_axis_tid / m_axis_tdest. 0 (default) strips them to a 1-bit tie-off, leaving the netlist unchanged. Set >0 and drive the value from the STREAM_ROUTE register (0x5C); it stays constant across every beat.
PIX_TDATA_WIDTH / C_AXIS_TDATA_WIDTH (auto) Derived — don't override. Per-pixel and full-beat tdata widths.

Using vtpgz_core (port-driven)

vtpgz_core exposes the configuration fields as plain input ports — the cfg_* ports mirror the writable AXI-Lite register fields (cfg_enable, cfg_img_width, cfg_img_height, cfg_pattern, cfg_solid_color, cfg_box_*, cfg_grid_*, cfg_checker_size, cfg_frame_rate_div, cfg_bar_width, cfg_hg_step, cfg_vg_step, cfg_smpte_*, …). Status outputs sts_busy and sts_frame_count[7:0] are also exposed.

A typical fully-static instantiation — a 1920×1080 colorbar generator with no CPU in sight, free-running at 60 fps from a 130 MHz clock:

vtpgz_core #(
    // Strip everything but colorbar to keep area minimal
    .EN_COLORBAR   (1),
    .EN_HGRAD      (0), .EN_VGRAD     (0), .EN_CHECKER (0),
    .EN_SOLID      (0), .EN_MOVING_BOX(0), .EN_GRID    (0),
    .EN_RAMP       (0), .EN_NOISE     (0),
    .OUTPUT_MODE   (0),  // 0=RGB 1=RAW 2=YUV
    .RGB_ORDER     (0),  // 0=Xilinx 1=legacy
    .BPC           (8)   // 8/10/12/14/16
) u_vtpgz (
    .aclk          (aclk),
    .aresetn       (aresetn),

    // Static configuration -- tie everything to its compile-time value.
    // The synth tool constant-folds disabled patterns and unused slots out.
    .cfg_enable        (1'b1),                  // run forever
    .cfg_sw_fsync      (1'b0),
    .cfg_ext_sync      (1'b0),                  // use internal frame sync
    .cfg_interlace     (1'b0),                  // progressive (EN_INTERLACE=1 only)
    .cfg_img_width     (16'd1920),
    .cfg_img_height    (16'd1080),
    .cfg_pattern       (4'd0),                  // colorbar
    .cfg_solid_color   (24'h000000),
    .cfg_box_color     (24'h000000),
    .cfg_box_width     (16'd0),
    .cfg_box_height    (16'd0),
    .cfg_box_dx        (16'd0),
    .cfg_box_dy        (16'd0),
    .cfg_grid_spacing  (16'd0),
    .cfg_grid_color    (24'h000000),
    .cfg_checker_size  (16'd0),
    .cfg_frame_rate_div(32'd2_166_666),         // 130 MHz / 60 fps
    .cfg_bar_width     (16'd240),               // 1920 / 8
    .cfg_hg_step       (28'd0),
    .cfg_vg_step       (28'd0),

    // Status -- leave dangling if you don't need them
    .sts_busy          (),
    .sts_frame_count   (),
    .sts_field_id      (),

    // AXI4-Stream master (video out)
    .m_axis_tdata      (vid_tdata),
    .m_axis_tvalid     (vid_tvalid),
    .m_axis_tready     (vid_tready),
    .m_axis_tlast      (vid_tlast),
    .m_axis_tuser      (vid_tsof),
    // Interlaced field ID (EN_INTERLACE=1 only; constant 0 otherwise)
    .fid        (vid_fid),

    // External frame sync (only used when cfg_ext_sync=1)
    .frame_sync_in     (1'b0),
    // Source field ID, sampled on the frame_sync_in edge (interlaced
    // external-sync operation only)
    .fid_in            (1'b0)
);

To drive cfg_* from your own RTL instead (an FSM that swaps patterns, a sideband bus, etc.), just connect those signals to your logic instead of constants — behavior is identical to a register write on the AXI-Lite flavor. For the host-precomputed step values, use the same numbers as the Programming sequence below.

Driving the AXI-Stream output

The core is synchronous to a single clock aclk with synchronous active-low reset aresetn, and emits video on a standard AXI4-Stream master (tvalid / tready / tlast = end-of-line / tuser = start-of-frame). To bring it up: hold aresetn low ≥1 cycle then release, drive cfg_* to stable values, assert cfg_enable, pick a frame-sync source (see External frame sync), and pull m_axis_tready high to consume the stream.

vtpgz_core AXI-Stream output: reset -> first frame, with backpressure stall

Source: docs/wavedrom/tdata_stream.json5 (regenerate with python scripts/regen_wavedrom.py). Pxy = pixel at column x, row y; PNL = last beat of the line (tlast=1). In the window above P00..P30 are consumed cleanly, then the sink drops tready for two clocks while the core holds tvalid=1 and the same P30 on tdata; the next beat advances as soon as tready rises again.

Key behaviors:

  • Latency from sync to first beat is 2 clocks. tuser, tlast, and tdata are delayed together, so stream-visible frame contents are unchanged.
  • No blanking intervals — the core streams the active picture only. Handle HBLANK/VBLANK downstream by holding tready low, or with a video-timing block.
  • Backpressure is honoured cycle-by-cycle. On tvalid && !tready all pipeline stages stall in place; nothing is dropped or repeated.
  • Mid-frame syncs are dropped, not queued — a frame_start while a frame is in flight is ignored (correct for a free-running vsync).
  • Reset clears all state, so the first frame after cfg_enable rises uses fresh pattern-counter state.

Using vtpgz_axilite_top (AXI4-Lite controlled)

vtpgz_axilite_top instantiates vtpgz_axil_regs and a vtpgz_core back-to-back. Its public interface is an AXI4-Lite slave for control plus the same AXI4-Stream master for video. Wire your AXI4-Lite master and use the Programming sequence to configure it.

vtpgz_axilite_top #(
    .C_S_AXI_ADDR_WIDTH (8),
    .C_S_AXI_DATA_WIDTH (32),
    // Per-pattern EN_* default to 1; strip unused ones to save area
    .OUTPUT_MODE   (0),  // 0=RGB 1=RAW 2=YUV
    .YUV_SUBSAMPLE (0),  // 0=444 1=422 (only for OUTPUT_MODE=2)
    .RAW_BAYER     (1),  // 0=PLAIN 1=RGGB 2=BGGR 3=GRBG 4=GBRG (only for OUTPUT_MODE=1)
    .RGB_ORDER     (0),  // 0=Xilinx 1=legacy
    .BPC           (8)   // 8/10/12/14/16
    // C_AXIS_TDATA_WIDTH is auto-derived; don't override
) u_vtpgz (
    .aclk (aclk), .aresetn (aresetn),
    // AXI4-Lite slave (control) — standard channels: aw/w/b/ar/r
    .s_axi_awaddr (axi_awaddr), .s_axi_awprot (axi_awprot),
    .s_axi_awvalid(axi_awvalid),.s_axi_awready(axi_awready),
    .s_axi_wdata  (axi_wdata),  .s_axi_wstrb (axi_wstrb),
    .s_axi_wvalid (axi_wvalid), .s_axi_wready(axi_wready),
    .s_axi_bresp  (axi_bresp),  .s_axi_bvalid(axi_bvalid),
    .s_axi_bready (axi_bready),
    .s_axi_araddr (axi_araddr), .s_axi_arprot(axi_arprot),
    .s_axi_arvalid(axi_arvalid),.s_axi_arready(axi_arready),
    .s_axi_rdata  (axi_rdata),  .s_axi_rresp (axi_rresp),
    .s_axi_rvalid (axi_rvalid), .s_axi_rready(axi_rready),
    // AXI4-Stream master (video). Width auto-derived from OUTPUT_MODE/BPC;
    // read it at runtime from COLOR_FORMAT[31:16] if needed.
    .m_axis_tdata (vid_tdata),  .m_axis_tvalid(vid_tvalid),
    .m_axis_tready(vid_tready), .m_axis_tlast (vid_tlast),  // end-of-line
    .m_axis_tuser (vid_tsof),                               // start-of-frame (1 bit)
    // External frame sync (used when CONTROL[2]=1)
    .frame_sync_in (ext_fsync)
);

The core has one clock domain (aclk) — AXI, pattern logic, and video output are all clocked by it. If your video sink runs on a different clock, put an asynchronous AXI4-Stream FIFO on the output. m_axis_tuser is 1 bit (asserts on the first beat of each frame), matching the Xilinx convention.

Programming sequence

  1. Hold aresetn low for at least one aclk cycle and release.

  2. Write IMG_WIDTH, IMG_HEIGHT to the desired resolution.

  3. Write the host-precomputed step values (these replace per-pixel dividers with cheap accumulators in hardware):

    • BAR_WIDTH = IMG_WIDTH / 8 (colorbar bar width)
    • HG_STEP = ceil((0xFFF << 16) / (IMG_WIDTH - 1)) (horizontal gradient / ramp step)
    • VG_STEP = ceil((0xFFF << 16) / (IMG_HEIGHT - 1)) (vertical gradient step)

    The gradient steps are Q12.16 (12-bit codes per pixel or line, 16 fraction bits), so a gradient spans the full 0..0xFFF range at any width up to 65535, 8K included, with every pixel within one code of the ideal ramp. Rounding up makes the last pixel land on 0xFFF; the core saturates the overshoot. Any larger step, up to the full 28 bits, also saturates: once a line (or frame) reaches 0xFFF it stays there and never wraps. In C: ((0xFFFu << 16) + n - 2) / (n - 1) for n > 1. This encoding changed in 0.8.0; host code written for 0.7 or earlier must be updated, see Upgrading from 0.7.

    • with EN_SMPTE=1, the three SMPTE_* geometry registers (formulas in the register map). Their reset values are the 1920x1080 geometry.
  4. Write PATTERN_SEL (0=colorbar, 1..4=gradients/checker/solid, 5=smpte, 6=grid, 7=ramp, 8=noise, 9=image). The bouncing box overlays whatever pattern is selected.

  5. Optional pattern parameters: SOLID_COLOR, BOX_COLOR/BOX_SIZE/ BOX_SPEED/BOX_BORDER, GRID_SPACING/GRID_COLOR, CHECKER_SIZE.

  6. For internal sync mode (CONTROL[2]=0): write FRAME_RATE_DIV (one frame every N aclks).

  7. Write CONTROL = 0x1 (enable). The core starts streaming.

To change configuration, write CONTROL = 0 first, wait for the in-flight frame to drain, then reprogram and re-enable. (COLOR_FORMAT is read-only — it mirrors the build-time configuration; you never write it.)

External frame sync

Drive frame_sync_in with any pulse-style signal that marks the start of a frame and set CONTROL[2] = 1 (ext_sync mode). The core does rising-edge detection internally, so it accepts 1-clock pulses, multi-clock pulses (including signals held high — only the edge counts), and free-running periodic vsync at any rate slower than the frame emission time. This is directly compatible with Xilinx VTC's vsync_out.

A rising edge that arrives while a frame is still in flight is dropped (not queued) — correct for a free-running vsync. For latched-pending behavior, add your own pending-bit FF before frame_sync_in.

Interlaced video

Build with EN_INTERLACE = 1 and set CONTROL[3] to emit interlaced fields. The implementation follows the AMD/Xilinx AXI4-Stream video convention used by the Video Test Pattern Generator (PG103) and UG934, so the stream drops straight into a Xilinx video pipeline:

  • IMG_HEIGHT holds the FIELD height — program frame_height / 2 (540 for 1080i), exactly as PG103 requires for active_height. A field is what the timing engine already calls a frame, so the pixel datapath, the patterns and the reference model are untouched by this feature.
  • m_axis_tuser (SOF) asserts on the first beat of every field; m_axis_tlast stays end-of-line.
  • fid (named as on v_tpg, so Vivado does not fold it into the m_axis interface) carries the field ID: 0 = even (top) field, 1 = odd (bottom) field. It is sampled coincident with SOF and held stable for every beat of the field. It is constant 0 for a progressive stream (CONTROL[3] = 0, or an EN_INTERLACE = 0 build), as UG934 requires.
  • One frame-sync pulse starts one field. FRAME_RATE_DIV therefore sets the field rate, and an external frame_sync_in is expected at twice the frame rate (59.94 Hz for 1080i29.97). STATUS[15:8] counts fields.
  • Internal sync alternates fid itself (0, 1, 0, 1, …). External sync samples the fid_in port on the sync edge instead, so the core stays locked to the field parity of the upstream source; repeat a field by repeating the fid_in value. The port carries the same field information v_tpg's fid_in does, but the sync-edge sampling is vtpgZero's own design — PG103 describes its fid_in as a pass-through input, not a generator parity input. Switching external → internal between fields continues the alternation from the last emitted field rather than repeating it.
  • A fresh enable restarts at the even field: dropping CONTROL[0] (or CONTROL[3]) across an idle cycle clears the parity, so the next field is field 0. A field already in flight always keeps its own fid to the last beat — clearing CONTROL mid-field does not retag the tail.
  • STATUS[1] reads back the field the timing engine is currently producing (source side, so it runs slightly ahead of fid).

Because each field is rendered in field coordinates, a feature N lines tall spans roughly 2N scanlines once the two fields are woven — v_tpg documents the same effect for its box ("NxN for progressive video and Nx2N for interlaced"). So program BOX_SIZE height for half what you want on the woven frame: an N-tall box on screen means programming N/2.

VG_STEP is a step, not a height, so it follows the usual formula against the programmed (field) height — VG_STEP = ceil((0xFFF << 16) / (field_height - 1)), i.e. roughly twice the progressive step for the same frame. Each field then sweeps the full gradient on its own, as v_tpg's field-coordinate rendering does.

Two integration notes for real AMD pipelines:

  • fid is deliberately not part of the inferred m_axis interface, so a block design that connects m_axis does not carry it along — wire fid to the consumer's field input yourself (and configure that core for interlace). AXI4-Stream to Video Out samples it with SOF, as UG934 states.
  • Polarity matches v_tpg (0 = even, 1 = odd), but the Video Processing Subsystem deinterlacer's deint_field_id uses the opposite polarity (PG231) — invert fid when driving that core.

frame_sync_in and fid_in are sampled directly by aclk with no synchronizer: both must be synchronous to aclk, and fid_in must be stable around the sync edge that starts the field. Cross a clock domain yourself before these pins.

Parallel video out (FVAL/LVAL/DVAL)

rtl/vtpgz_axis_to_flvdval.v converts the AXI4-Stream output into the classic parallel video timing interface used by frame grabbers and camera links:

Signal Meaning
FVAL frame valid — high for the whole frame (the whole field when interlaced)
LVAL line valid — high for the whole active line
DVAL data valid — qualifies one beat of PIX_DATA
FIELD_ID fid of the frame FVAL encloses, latched at its SOF
vtpgz_axis_to_flvdval #(
    .TDATA_WIDTH (24),   // one beat; = one pixel at PIXELS_PER_CLOCK=1
    .FVAL_LEAD   (0),    // FVAL front porch, in cycles
    .FVAL_TRAIL  (0)     // FVAL back porch, in cycles
) u_flv (
    .aclk(aclk), .aresetn(aresetn),
    .s_axis_tdata(m_axis_tdata), .s_axis_tvalid(m_axis_tvalid),
    .s_axis_tready(m_axis_tready), .s_axis_tlast(m_axis_tlast),
    .s_axis_tuser(m_axis_tuser), .s_axis_eof(eof), .s_axis_fid(fid),
    .pix_data(pix_data), .fval(fval), .lval(lval), .dval(dval),
    .field_id(field_id),
    .timing_err(timing_err), .timing_err_clr(1'b0)
);

There is no FIFO, and there cannot be a stall. FVAL/LVAL/DVAL has no backpressure, so the adapter ties TREADY high permanently and relies on the source running gap-free. vtpgZero does: with TREADY high it emits exactly IMG_WIDTH/PPC contiguous beats per line, then LINE_GAP_CYCLES of idle, with no bubbles inside a line. Put this behind anything that can stall (a crossbar, a DMA, a slower-draining CDC FIFO) and that breaks — not silently: LVAL stays high across a mid-line bubble while DVAL drops, and TIMING_ERR latches. Such a source needs a line or frame FIFO in front.

The adapter also needs eof, the core's end-of-frame marker (asserted on the same beat as the final m_axis_tlast of the frame). AXIS video leaves EOF implicit; the core already computes it, so it is brought out as a port.

Blanking: it comes from the source, not the adapter

The adapter has no buffer, so it can only reproduce the gaps the source already leaves. Both blanking intervals are therefore set upstream:

Interval Set by Cycles
Horizontal blanking (line break) LINE_GAP_CYCLES (build parameter, min 1) LINE_GAP_CYCLES, after every line but the last
Vertical blanking (frame break) FRAME_RATE_DIV (runtime register) FRAME_RATE_DIV - ACTIVE

where the active time of one frame (field) is

ACTIVE = (IMG_WIDTH / PPC) * IMG_HEIGHT + LINE_GAP_CYCLES * (IMG_HEIGHT - 1)

So to hit a required blanking spec, pick LINE_GAP_CYCLES for the line break and then FRAME_RATE_DIV = ACTIVE + wanted vertical blanking.

FRAME_RATE_DIV must be strictly greater than ACTIVE. Frames do not overrun each other if it is not — the timing engine only accepts a sync tick while it is idle, so ticks landing inside an active frame are simply dropped (including one coincident with the final active cycle). The frame period becomes

PERIOD = FRAME_RATE_DIV * (floor(ACTIVE / FRAME_RATE_DIV) + 1)
VBLANK = PERIOD - ACTIVE

so the frame rate silently drops to a submultiple rather than the blanking going negative. Measured on the RTL with ACTIVE = 131: FRAME_RATE_DIV of 132 gives 1 cycle of blanking, 133 gives 2, 400 gives 269 — but 131 gives a 262-cycle period with 131 cycles of blanking (half the requested rate), and 100 gives a 200-cycle period with 69. Under external frame sync, FRAME_RATE_DIV does not determine vertical blanking at all; the sync source does.

FVAL_LEAD and FVAL_TRAIL carve the FVAL porches out of that vertical blanking, so they are only realisable while FVAL_LEAD + FVAL_TRAIL < FRAME_RATE_DIV - ACTIVE. The front porch is implemented by delaying the pixel path, so it costs FVAL_LEAD cycles of latency and FVAL_LEAD * (TDATA_WIDTH+3) registers — which Vivado largely maps to SRLs, so the real cost is lower than that count suggests. The back porch is just a counter. At FVAL_LEAD = 0 no delay line is generated and FVAL rises together with the first LVAL.

Out-of-context synthesis on xc7a100tcsg324-1 at TDATA_WIDTH=24: FVAL_LEAD=0, FVAL_TRAIL=0 costs 9 LUT / 5 FF; FVAL_LEAD=4, FVAL_TRAIL=6 costs 41 LUT / 65 FF.

If the porches do overrun the vertical blanking, the adapter does not fail silently: a new frame's SOF arriving while the previous frame is still draining the delay line latches TIMING_ERR, which also covers mid-line bubbles. Consecutive frames genuinely overlap inside the delay line there, so no correct FVAL exists to emit — the condition is reported rather than papered over.

A classic parallel interface is one pixel per clock, so use PIXELS_PER_CLOCK=1. At PPC=N the adapter passes TDATA_WIDTH straight through and presents N pixels per DVAL on a wide bus — fine for an on-chip sink, but a true parallel link needs an external N:1 serializer running N times faster.

Multi-pixel-per-clock

At PIXELS_PER_CLOCK of 2/4/8, that many horizontally-adjacent pixels are packed into one wider beat (lane 0 = leftmost pixel in the tdata LSBs), multiplying line bandwidth. Every pattern and the moving-box overlay (fill, border, box-image) produce beat-exact output, across all output modes (RGB / RAW-Bayer / YUV 4:4:4 / YUV 4:2:2) and all bit depths. IMG_WIDTH is clamped down to a multiple of PIXELS_PER_CLOCK. Only an illegal value (not 1/2/4/8) fails elaboration; the active value is mirrored read-only at register 0x30 so software can discover it.

The PIXELS_PER_CLOCK=1 build is byte-identical to prior releases (the per-lane logic is generate-gated behind PPC>1). NOISE uses a leap-ahead LFSR so each lane gets consecutive states. IMAGE / BOX_IMAGE replicate their source memory per lane — see Image patterns.

Output modes

  • RGB (OUTPUT_MODE=0) — 3-component RGB packed as {pad,B,G,R} (or {R,G,B,pad} with RGB_ORDER=1).

  • RAW (OUTPUT_MODE=1) — 1 component per pixel. RAW_BAYER=0 is plain monochrome (G channel); 1..4 are the RGGB / BGGR / GRBG / GBRG Bayer mosaics (standard row-major naming). The smallest configuration (~50 LUT smaller than RGB), useful as an image-sensor emulator.

  • YUV (OUTPUT_MODE=2) — YCbCr emitted directly from the pattern generators, no runtime color-space conversion. The colorbar uses a precomputed palette; grayscale-style patterns put their value in Y and hold Cb=Cr=0x800 (neutral chroma). Color registers are interpreted as {Y,Cb,Cr} triples in YUV builds. YUV_SUBSAMPLE=0 → 4:4:4 ({V,U,Y}); =1 → 4:2:2 ({C,Y}, C alternates Cb on even-x and Cr on odd-x).

    YUV_MATRIX picks the colorbar colorimetry — BT.601 (SD) or BT.709 (HD) — and YUV_RANGE picks full or limited (studio) range. BAR_LEVEL=75 gives 75% bars in any mode (in YUV BT.709 limited at 10 bpc, exactly SMPTE RP 219). Every combination is a build-time constant palette, and none of them costs logic, since only one is ever elaborated.

    Each palette holds the exact code for the build's bit depth. An 8-bit limited code is not the 10-bit one truncated (BT.601 cyan Y is 170 at 8 bits, but 678 >> 2 = 169), so 8-, 10- and 12-bit builds each get their own constants, and all of them equal the published tables. BPC 14/16 use the 12-bit codes. Full range follows H.273 / BT.2100 at every depth. The palettes are generated from the colorimetry by scripts/gen_yuv_palettes.py, which writes them into vtpgz_core.v; CI fails if the two drift apart. The one exception is the shipped default, BT.601 full range 100%, which is kept bit-for-bit as it always was (two chroma values sit one LSB off the formula).

    In a limited-range build the patterns whose luma is a runtime value (gradients, checker, ramp, noise) are compressed onto the legal range by

    Y_lim = 256 + ((Y * 3505) >> 12)        0 -> 256, 4095 -> 3760
    

    which lands exactly on 64..940 at 10 bits and 16..235 at 8. The scale is implemented as shifts and adds, not as a multiply: Vivado infers a DSP48 for the multiply form even with a constant operand, and no mode in this core is meant to cost a DSP. Verified by synthesis, not assumed.

    Color registers are not rescaled, in either range. SOLID_COLOR, GRID_COLOR and the box colors are raw code values that pass through untouched, so a host that writes an exact code reads that exact code back. In a limited-range build it is the host's job to write legal values — the core will happily emit 0x3FF if you ask it to. The grid background does follow the range, because its luma is a build-time constant.

    IMAGE in YUV builds is converted at build time, not in the core. Generate the image memory with scripts/image_to_hex.py --yuv --matrix {601,709} --range {full,limited}, matching the build's YUV_MATRIX and YUV_RANGE, and the core passes those codes through untouched (widened by a shift, so 8-bit 128 is exactly 10-bit 512). A YUV build that enables an image but still points at the shipped RGB mandrill files fails at elaboration; any other RGB memory comes out as the wrong colours, and the core cannot tell, so keep the two in step. The padding around a centred image is the build's black. See docs/images.md.

A pattern stripped at build time still has its PATTERN_SEL slot, but selecting it at runtime produces a black frame. At least one pattern must remain enabled.

SMPTE RP 219 color bars

EN_SMPTE=1 adds the SMPTE RP 219 HD color-bar chart as PATTERN_SEL=5. It is off by default. A build without it has no chart logic and no SMPTE_* registers (they read 0), and slot 5 renders black.

The chart has four row bands, 7/12, 1/12, 1/12 and 3/12 of the height:

Row Left to right
1 40% grey, 75% white, yellow, cyan, green, magenta, red, blue, 40% grey
2 100% cyan, -I, 75% white, 100% blue
3 100% yellow, +Q, Y ramp black to white, 100% white, 100% red
4 15% grey, black, 100% white, black, PLUGE -2% / black / +2% / black / +4%, black, 15% grey

RP 219 allows a choice of color in the second patch of rows 2 and 3; this chart uses -I and +Q. The bars are always 75%: BAR_LEVEL applies to the 8-bar pattern only.

  • Colors. The palette is generated for the build's output mode, matrix, range and bit depth, like the 8-bar palettes. In YUV BT.709 limited range it is RP 219's published table exactly. The 10-bit codes of every color and the 12-bit -I/+Q anchors are checked against literal values by hw/arty_a7_100t/python/check_smpte_spec.py. The -2% PLUGE step is below black, so it only shows in limited-range YUV (46 at 10 bits). In RGB and full-range YUV there is nothing below black and it clips to black.
  • Geometry. The host writes the bar width c, side panel d, PLUGE step p and row unit h (formulas in the register map). The core derives the row-4 widths 3c/2 and 2c, and makes 5c/6 the remainder, so both column layouts end at d + 7c (unless a width is too small and gets clamped, see below). The reset values are the 1920x1080 geometry: c=206, d=239, p=69, h=90. Every bar is c wide, so at 1920 the chart is not pixel-identical to RP 219's table, which alternates 205- and 206-pixel bars around 240-pixel side panels. The structure and colors match. Every width is clamped to at least PIXELS_PER_CLOCK. Like the other geometry, these registers are meant to be written while the core is disabled: a write during a frame takes effect at the next line or frame, not at a clean frame boundary.
  • Ramp. The Y ramp spans the five middle bars. Its step is rounded up and the end clipped, so it reaches white exactly on its last pixel: 64 to 940 at 10 bits in limited range, 0 to full scale otherwise. This needs c >= 4 (a frame at least 33 pixels wide); below that the step does not fit its 16 bits.
  • Interlaced. Like every pattern, the chart is drawn per field. With IMG_HEIGHT set to the field height, h = IMG_HEIGHT / 12 gives a full chart in each field, and the woven frame has the same proportions.
  • Cost. About +660 LUT / +650 FF at 1 pixel per clock and +920 LUT / +830 FF at 4, with no DSP. The Arty demo with the chart still closes 100 MHz at 4 pixels per clock. The ramp's start offset (d + c) * step comes from a serial shift-add multiplier that settles within ~40 clocks of a register write. Full numbers are in docs/resources.md.

Image patterns

EN_IMAGE bakes a 24-bit RGB888 image into inferred BRAM and draws it as PATTERN_SEL=9, centred with black padding and hardware nearest-neighbour scaling. EN_BOX_IMAGE paints a second embedded image inside the moving box. Both are stripped (no BRAM cost) when disabled. Full details — parameters, scaler internals, BRAM sizing, and the PNG/JPG → .mem conversion script — are in docs/images.md.

↑ back to top

Upgrading from 0.7

0.8.0 breaks register compatibility in one place: the gradient step registers. Everything else (addresses, other fields, the ports other than the two step inputs, stream format) is unchanged.

HG_STEP (0x48) and VG_STEP (0x4C) are now Q12.16. Up to 0.7 they held a whole number of 12-bit codes per pixel (or line) in bits [15:0]. Any width above 4096 gave a step of 0, so gradients and RAMP could not sweep at 8K. They now hold codes per step with 16 fraction bits in [27:0]:

0.7 and earlier 0.8.0
Format integer codes, [15:0] Q12.16 codes, [27:0]
HG_STEP 0xFFF / (IMG_WIDTH - 1) ceil((0xFFF << 16) / (IMG_WIDTH - 1))
VG_STEP 0xFFF / (IMG_HEIGHT - 1) ceil((0xFFF << 16) / (IMG_HEIGHT - 1))
1920x1080 values 2, 3 139849, 248721
Reset value 546, 970 (saturated after a few pixels) 139849, 248721 (1920x1080)
Widest full sweep 4096 65535

RAMP (PATTERN_SEL=7) reuses HG_STEP, so it changes too. SMPTE_RAMP_STEP keeps its old format.

Symptom of an un-updated host. No error is raised. The old value is read as 1/65536 of a code per step, so HGRAD, VGRAD and RAMP come out black (or almost black) across the whole frame. The other patterns are unaffected.

Detecting the version. VERSION (0x04) reads {major[8], minor[8], patch[16]}: 0.8.0 is 0x00080000. Host code that has to drive both generations can choose the formula from it:

uint32_t ver   = rd32(VTPGZ_BASE + 0x04);
int      q1216 = ver >= 0x00080000u;          /* 0.8.0 and later */
uint32_t hg    = q1216 ? ((0xFFFu << 16) + w - 2u) / (w - 1u)
                       : 0xFFFu / (w - 1u);
uint32_t vg    = q1216 ? ((0xFFFu << 16) + h - 2u) / (h - 1u)
                       : 0xFFFu / (h - 1u);
wr32(VTPGZ_BASE + 0x48, hg);
wr32(VTPGZ_BASE + 0x4C, vg);

Direct vtpgz_core users. The cfg_hg_step and cfg_vg_step ports widened from [15:0] to [27:0]. A parent that still drives them from a 16-bit signal compiles without error, because Verilog zero-extends the value. It then shows the black-gradient symptom above. Widen the driving signal and switch to the new formula.

Behaviour fix: no stale first beat. The per-line pattern state (VGRAD's accumulator and the checker and grid row counters) now resets on the last beat of a frame instead of at pix_sof. Before 0.8.0, the first beat of every frame after the first one carried the previous frame's last-row value, and a frame one beat wide (IMG_WIDTH == PIXELS_PER_CLOCK) was wrong on every line. Output that matched 0.7 on frame 1 still matches. Frames 2 onward now match frame 1, where before they differed in that first beat.

↑ back to top

How to test it

All simulation is driven by one Python script (no Makefile):

python sim/run_sim.py regression   # lint + build + run + 100% coverage + model gate
python sim/run_sim.py all_modes    # byte-exact sim↔model gate across every mode/BPC
python sim/cocotb/run_ppc.py       # beat-exact pixels-per-clock data path (1/2/4/8)
python sim/run_iverilog_interlace.py  # interlaced field ID (fid) semantics
python sim/run_iverilog_flvdval.py    # FVAL/LVAL/DVAL adapter + gap-free source
python sim/run_iverilog_black.py      # empty/stripped pattern slots are real black in YUV
python sim/check_image_guard.py       # YUV builds refuse the RGB default images
python sim/check_ppc_vs_model.py      # beat-exact RTL vs model at PPC 1/2/4/8 (Icarus), SMPTE included
python hw/arty_a7_100t/python/check_yuv_range.py            # YUV colorimetry vs the standards
python hw/arty_a7_100t/python/check_smpte_spec.py           # SMPTE RP 219 codes, layout and ramp
python hw/arty_a7_100t/python/check_yuv_range_mutations.py  # ...and proof those checks bite

There is also an Icarus smoke test, a cocotb control-plane suite, and a hardware regression on the Arty A7-100T (all 9 patterns byte-exact vs the Python model, verified on silicon in the default RGB build and in a YUV limited-range BT.709 build). Setup, expected output, the 7-phase coverage harness, and the hardware flow are documented in docs/testing.md.

↑ back to top

FPGA resource usage and frequency

Measured on the full Arty A7-100T demo (Vivado 2025.2, default strategies):

Metric Value
Target Digilent Arty A7-100T (XC7A100TCSG324-1, speed grade -1)
Clock 100 MHz at 4 pixels/clock = 400 Mpixel/s (on-board 100 MHz osc via MMCM)
WNS +0.890 ns at 100 MHz (timing met, 0 failing endpoints)
LUTs 3589 / 63400 = 5.66%
FFs 3541 / 126800 = 2.79%
BRAM36 8 / 135 = 5.93%
Hardware test 9/9 patterns byte-exact vs Python model

That row includes the whole demo (core + frame_capture + fpgacapZero JTAG-AXI bridge + BRAM frame buffer + MMCM), and now also the vtpgz_axis_to_flvdval adapter and the flv_monitor raster instrument that the on-silicon parallel-video test reads — neither of which you pay for unless you instantiate them. Standalone, the all-patterns vtpgz_axilite_top is ~1400 LUT / 1268 FF (RGB-8b, OOC synth, no BRAM, no DSP), and ~2450 LUT at 4 pixels per clock. Pixels-per-clock scales sub-linearly: 8× throughput costs about 2.8× the LUTs. The smallest build (1 pattern, RAW-8b) is ~546 LUT.

8Kp60 on silicon. On a KV260 (Zynq UltraScale+, -2LV), the core runs at 8 pixels per clock and 250 MHz and streams 7680x4320 at 60 fps (active video; with CTA-861 blanking the 8Kp60 raster needs 297 MHz). Every frame has the full 4,147,200 beats with no mid-line bubble and an exact 1/60 s period, and its CRC-32 matches the bit-exact model's for all nine patterns. Free-running, it reaches 60.2 fps. Out of context, a ZU7EV -2 closes the core at ~284 MHz. The bench and its results are in hw/kv260/README.md, and the fmax numbers in docs/resources.md.

The full per-configuration matrix — every mode/BPC, per-feature deltas, the PPC sweep, interlace, the YUV colorimetry options, the SMPTE RP 219 chart, and the tiniest build — is in docs/resources.md. Reproduce with python synth/run_matrix.py.

↑ back to top

File layout

rtl/
  vtpgz_defs.vh         constants, register addresses, pattern/format codes
  vtpgz_axil_regs.v     AXI4-Lite slave + register file
  vtpgz_core.v          port-driven core: timing engine, pattern generators,
                        inline pack stage, AXIS output
  vtpgz_axilite_top.v   thin wrapper: vtpgz_axil_regs + vtpgz_core
  vtpgz_axis_to_flvdval.v  optional adapter: AXIS video -> parallel
                        FVAL/LVAL/DVAL (no FIFO, source must be gap-free)
tb/                     Icarus Verilog smoke testbench
sim/                    Verilator/iverilog/cocotb harnesses + run_sim.py
                        (see docs/testing.md)
synth/                  Vivado OOC resource-matrix driver (run_matrix.py)
docs/
  testing.md            simulation + hardware regression detail
  images.md             EN_IMAGE / EN_BOX_IMAGE detail
  resources.md          full FPGA resource matrix
  img/, wavedrom/       figures and their sources
fcapz/                  git submodule: upstream fpgacapZero RTL + host tools
hw/
  arty_a7_100t/         Arty A7-100T reference design + Python host
  kv260/                KV260 DisplayPort reference design
LICENSE                 Apache-2.0

↑ back to top

Author and license

↑ back to top

About

A synthesizable Video Test Pattern Generator IP core. Outputs pixels over an AXI4-Stream master interface and is configured at runtime via an AXI4-Lite slave register interface.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages