Skip to content

Add pixels-per-clock (1/2/4/8) support - #1

Merged
lcapossio merged 13 commits into
mainfrom
feature/pixels-per-clock
Aug 2, 2026
Merged

Add pixels-per-clock (1/2/4/8) support#1
lcapossio merged 13 commits into
mainfrom
feature/pixels-per-clock

Conversation

@lcapossio

Copy link
Copy Markdown
Owner

Summary

Adds pixels-per-clock (PPC) support to the vtpgZero core: N=1/2/4/8 horizontally-adjacent pixels packed into a single AXI4-Stream beat (lane 0 = leftmost = tdata LSBs). All patterns (solid, grid, checker, colorbar, h/v-gradient, ramp, noise, image, box overlay) work at every PPC, byte-exact against the reference model.

What's included

  • RTL: PIXELS_PER_CLOCK build param; per-lane combinational pattern datapath gated behind PPC>1; single-driver stage-1 pipeline.
  • Verification: cocotb + Icarus data-path suite (12 PPC×format configs) and an iverilog beat-exact model gate (36 configs). Existing Verilator regression / all_modes / seq gates unchanged and green.
  • Hardware: Arty A7-100T demo + self-test extended for PPC>1 — frame_capture serializes wide beats into 32-bit words; run_hw_test reads back PPC and checks byte-exact. PPC=4 verified byte-exact across all 9 patterns on silicon.
  • Docs: README PPC resource table, refreshed resource matrix, cocotb runner docs, PPC>1 hardware-validation notes.

Notable fixes

  • Multi-driven s1 registers at PPC>1: each packed pipeline reg was written by two always blocks (main + per-lane generate). Vivado dropped the per-lane driver (Synth 8-6858/8-6859), zeroing lanes 1..N-1 in silicon while sim tolerated it. Collapsed into one always block with a constant-bound loop.
  • Verilator 5.020 CI: split_var on chain arrays, shift-based pack_pixel (no zero-width replication), coverage_off on PPC>1-only dead code.

Test status

  • CI green on the branch (lint / regression 100% coverage / all_modes / cocotb / seq).
  • PPC=1 and PPC=4 both byte-exact on Arty A7-100T hardware.

Note: PPC>1 demo builds run at a lower clock (per-lane counter chains don't close 130 MHz); correctness, not throughput, is the goal there.

lcapossio added 13 commits July 29, 2026 00:11
Build-time PIXELS_PER_CLOCK (1/2/4/8) packs N adjacent pixels per
AXI-Stream beat, lane 0 in the tdata LSBs. M1 scope: SOLID, GRID,
CHECKER and the box overlay are beat-exact at PPC>1; stateful patterns
still require PPC=1 and fail elaboration if enabled. PPC=1 is unchanged.

Core: x advances by NPPC, width clamped to a multiple; per-lane
grid/checker chains, box comparators, widened pipeline, reusable
pack_pixel(); PPC mirrored RO at reg 0x30.

Verify: model render_frame_beats() plus iverilog harness beat-exact
across 36 configs. Verilator coverage gate still to be re-baselined.
Drop the BOX_IMG_X_STEP=0 "runtime toggle" paragraph.
Add per-lane support at PPC>1 for COLORBAR, HGRAD, VGRAD and RAMP,
joining SOLID/GRID/CHECKER and the box overlay. Colorbar uses a bar-index
chain plus a mode-aware palette function; hgrad/ramp use accumulator
chains; vgrad is constant per beat so it replicates. The elaboration guard
now only forbids NOISE and IMAGE/BOX_IMAGE at PPC>1 (M3). PPC=1 unchanged.

Verify: iverilog<->model beat-exact across 36 configs x 7 patterns (252
checks), guard rejection, and PPC=1 regressions. Bump core to 0.3.0.
The per-lane pattern mux (pat_c*_bus) is only consumed by the PPC>1
pipeline lanes, so at PIXELS_PER_CLOCK=1 it was dead logic relying on
synthesis DCE. Wrap it in `if (NPPC>1)` and start the lane loop at 1
(lane 0 always comes from the scalar mux), so the 1ppc netlist is
provably free of the extra mux and no lane is duplicated at any PPC.
Add per-lane support at PPC>1 for the last patterns: NOISE, IMAGE and the
BOX_IMAGE overlay. NOISE uses a leap-ahead LFSR (feedback unrolled NPPC
times so lanes get consecutive states, base jumps one beat). IMAGE and
BOX_IMAGE replicate their source memory per lane with a combinational,
shift-free read (matches the reference model); the PPC=1 path keeps its
registered BRAM read and legacy 1-px shift, untouched. The elaboration
guard now only rejects an illegal PPC value -- no per-pattern restriction.

Verify: iverilog<->model beat-exact, 321 pattern-checks across ppc
{1,2,4,8} x mode {rgb,raw,yuv} x bpc {8,12,16}, IMAGE with 2x scaling and
a dedicated box-image overlay check. Smoke passes; bad-PPC guard fires.
Bump core to 0.4.0.
@
Add cocotb-driven PPC data-path verification

Icarus-backed cocotb suite (run_ppc.py, test_ppc.py) drives the core
over AXI-Lite and checks each AXIS beat against the reference model for
PIXELS_PER_CLOCK 1/2/4/8 across RGB/RAW/YUV, all 8 patterns. Icarus is
used since Verilator returns a sampled-once packed tdata. 12 suites pass.
@
@
Document cocotb test runners in README

Add a cocotb subsection under "How to test it" covering run_cocotb.py
(control plane) and run_ppc.py (PPC data path vs model), and list the
cocotb/ dir plus the iverilog PPC gate in the file-layout section.
@
@
Add PPC resource sweep and refresh resource matrix

run_matrix.py gains a `ppc` mode that synthesizes the all-patterns
RGB-8b build over PIXELS_PER_CLOCK 1/2/4/8. README resource tables are
regenerated against current RTL (they predated the line-gap feature):
mode/BPC sweep, per-pattern deltas, and tiniest build all refreshed, and
a new pixels-per-clock table added. Scaling is sub-linear: 8x throughput
for +56% LUT / +36% FF. PPC=1 is unchanged from pre-feature RTL.
@
@
Fix Verilator 5.020 CI lint/build failures

CI uses apt Verilator 5.020, which the PPC RTL tripped three ways:
- UNOPTFLAT on the per-lane chain arrays (bpc_l/cxc/hga_l/lf_l/etc.):
  add /* verilator split_var */ so each element is an independent net.
- V3Number internal crash + out-of-range part selects on pack_pixel:
  rewrite packing as width-safe assigns + clamped left shifts (no
  zero-width replications, no dead-branch range errors). Byte-exact.
- coverage_off on the mode/PPC dead-branch helpers (pack_pixel,
  bar_palette, pat_c*_bus, PPC readback) to restore the 100% gate.

Verified on 5.020: lint, regression (100%), all_modes, check_seq.
@
The stage-1 pipeline registers (pat_c*_s1, box_in_s1, box_img_*_s1,
box_on_border_s1) were driven by two always blocks: the main FSM block
wrote lane 0, a per-lane generate block wrote lanes 1..NPPC-1. Vivado
treats each packed register as multi-driven (Synth 8-6858/8-6859),
preserves the reset/GND driver for the upper lanes and drops the
generate driver, zeroing lanes 1..N-1 in silicon. Simulators tolerate
the non-overlapping slice writes, so this only surfaced on hardware.

Collapse both into one always block with a constant-bound loop for the
upper lanes. It unrolls to nothing at NPPC==1, so the 1ppc build stays
byte-identical. Verified byte-exact on Arty A7-100T at PPC=4.
Enable the Arty A7-100T demo and self-test to validate pixels-per-clock
> 1 on silicon:

- frame_capture: serialize each TDATA_WIDTH-bit beat into ceil(W/32)
  little-endian 32-bit BRAM words; WPB==1 path unchanged.
- demo_top: PIXELS_PER_CLOCK localparam (now 4); TDATA_WIDTH derived
  from it; forward the parameter to the IP.
- clk_gen: parameterize CLKOUT0_DIVIDE. PPC>1 builds run the demo
  slower (per-lane counter chains do not close 130 MHz); correctness,
  not throughput, is the goal there.
- run_hw_test: read back PPC, expand expected words via render_frame_beats
  at the beat width, validate serialized capture.

Verified byte-exact across 9 patterns at PPC=4.
Note how to build/run the demo at pixels-per-clock > 1: set
VTPGZ_PIXELS_PER_CLOCK in demo_top.v, rebuild, re-run. frame_capture
serializes wide beats and run_hw_test checks byte-exact. PPC>1 uses a
lower demo clock (per-lane chains do not close 130 MHz). PPC=4 verified
byte-exact on the board.
The single-block s1 latch loop added for the multi-driver fix has zero
iterations at NPPC==1, but unlike the old generate block it is not
elaborated away, so its body counted as uncovered lines in the PPC=1
coverage sim (98%, 622/631). Wrap it in verilator coverage_off, matching
the existing PPC>1-only pat_c*_bus logic. Coverage back to 100%; PPC>1 is
covered by the cocotb data-path suite and the iverilog beat-exact gate.
@lcapossio
lcapossio merged commit 4d141ac into main Aug 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant