DVI Color Bars
A 1280x720 @ 30Hz DVI display with selectable test patterns: horizontal color bars, vertical color bars, and an optional animated starfield. A debounced button cycles through the patterns. The design runs on both Tang Nano 20K and 9K boards.
Clock Tree
text
27 MHz crystal (SCLK)
└─ PLL (IDIV=7, FBDIV=54, ODIV=4)
└─ 185.625 MHz serial_clk
└─ CLKDIV (DIV_MODE=5)
└─ 37.125 MHz pixel_clkBoth generators appear in a single CLOCK_GEN block. The CLKDIV's input references the PLL's output — the compiler resolves the chain and validates that the VCO (742.5 MHz) is within range and the serial-to-pixel ratio is 5:1 for 10-bit DDR serialization.
Modules
video_timing
CEA-861 compliant 1280x720 timing generator. Constants define the full timing: H_ACTIVE=1280, H_FRONT=110, H_SYNC=40, H_BACK=220 (H_TOTAL=1650); V_ACTIVE=720, V_FRONT=5, V_SYNC=5, V_BACK=20 (V_TOTAL=750). Two counters (h_cnt, v_cnt) free-run and wrap. Hsync and vsync are positive polarity. Outputs include pixel coordinates (x_pos, y_pos) and display_enable.
tmds_encoder_10 / tmds_encoder_2
Two TMDS 8b/10b encoder variants, selected by @feature CONFIG.serializer. Both implement the same DVI-specification encoding pipeline:
- Popcount the 8-bit input via an adder tree.
- Select XOR or XNOR mode based on whether the popcount exceeds 4.
- Build a 9-bit transition-minimized word by chaining XOR/XNOR of adjacent bits.
- Popcount the result, track running disparity with a 5-bit signed counter, and conditionally invert the output for DC balance.
During blanking (display_enable == 0), the encoder outputs fixed control tokens based on c0/c1 and resets disparity to zero.
tmds_encoder_10 outputs the full 10-bit encoded word each pixel clock, intended for connection to a 10:1 serializer (e.g., Gowin OSER10).
tmds_encoder_2 adds an internal 2-bit shift register that serializes the 10-bit word at 5× pixel clock, intended for 2:1 DDR primitives (e.g., Lattice ODDRX1F). The TMDS encoding and disparity update are gated to run once every 5 serial clocks (shift_cnt == 0).
The project file sets CONFIG.serializer = 10 or CONFIG.serializer = 2 to select the appropriate variant via @feature/@else conditional compilation.
hbars / vbars
Purely combinational pattern generators. hbars maps x_pos to 5 horizontal bars of 256 pixels each (Red, Green, Blue, White, Black). vbars maps y_pos to 5 vertical bars of 144 pixels each in the same color order.
warp
Animated starfield with 30 stars. Each star is a 2x2 white pixel that accelerates radially outward from screen center (640, 360). Stars that leave the screen respawn near center with positions randomized by a 32-bit Galois LFSR (taps at bits 31, 21, 1, 0). Updates occur every 4th vsync via a frame counter.
Three @template blocks handle per-star computation:
- STAR_DIST — Absolute distance from center:
dx = |sx - 640|,dy = |sy - 360|. - STAR_HIT — Pixel hit-test against the star's 2x2 bounding box.
- STAR_NEXT — Radial movement with velocity =
distance/32 + 1, or respawn if offscreen.
Each template is expanded 30 times by @apply [NUM_STARS] with IDX substitution.
The warp module is conditionally compiled via @feature CONFIG.warp == 1. The 9K project sets warp = 0 to fit the smaller device; the 20K project sets warp = 1.
debounce
2-stage metastability synchronizer followed by a counter-based debouncer. When the synchronized input disagrees with the stable state, a 20-bit counter increments until it reaches 742,499 (~20ms at 37.125 MHz), then latches the new state. A falling-edge detector produces a single-cycle btn_press pulse.
por
Power-on reset timer. After the FPGA's DONE signal goes high, a 20-bit counter counts to 1,048,575 (~28ms) before releasing the active-low por_n output. This delay ensures the PLL has locked before the design starts running.
dvi_top
Top-level integration. Instantiates all submodules and handles:
- Pattern selection: A 2-bit
pattern_selregister increments on vsync rising edge whenpattern_pendingis set by the debounced button. Color outputs are muxed from hbars, vbars, or warp based on this register. - TMDS output pipeline: A 2-stage register pipeline (pre-mux → output register) ensures a clean FF-to-OSER10 timing path. The TMDS clock channel drives a fixed
10'b1111100000pattern. - Heartbeat: A 25-bit counter toggles an LED.
Differential Output
TMDS pins are declared with mode=DIFFERENTIAL, standard=LVDS25, serialization clock bindings (fclk=serial_clk, pclk=pixel_clk), and reset=pll_lock. The PLL's lock indicator is declared as a WIRE LOCK pll_lock output in the CLOCK_GEN block — this is a non-clock output that the compiler automatically declares as a wire in the generated Verilog. The reset attribute references this lock signal to hold the serializer in reset until the PLL is stable. The compiler automatically inverts the lock signal for the active-low serializer reset. The compiler generates the OSER10 serializer and TLVDS_OBUF differential buffer from these attributes. Each channel shifts out a 10-bit word per pixel clock using DDR at 185.625 MHz.
jz
@project(CHIP="GW2AR-18-QN88-C8-I7") DVI_TEST
@import "por.jz"
@import "video_timing.jz"
@import "tmds_encoder_10.jz"
@import "tmds_encoder_2.jz"
@import "hbars.jz"
@import "vbars.jz"
@import "warp.jz"
@import "debounce.jz"
@import "dvi.jz"
CONFIG {
warp = 1;
serializer = 10;
// 1280x720 @ 30Hz (CEA-861, pixel clock = 37.125 MHz)
h_active = 1280; h_front = 110; h_sync = 40; h_back = 220;
v_active = 720; v_front = 5; v_sync = 5; v_back = 20;
}
@check(CONFIG.serializer == 2 || CONFIG.serializer == 10, "Serialzer must be 2 or 10.");
CLOCKS {
SCLK = { period=37.037 }; // 27MHz crystal
serial_clk; // 185.625MHz (5x pixel, from PLL)
pixel_clk; // 37.125MHz (from CLKDIV)
}
IN_PINS {
SCLK = { standard=LVCMOS33 };
DONE = { standard=LVCMOS33 };
KEY[2] = { standard=LVCMOS33 };
}
OUT_PINS {
LED[6] = { standard=LVCMOS33, drive=8 };
TMDS_CLK = { mode=DIFFERENTIAL, standard=LVDS25, drive=3.5, width=10, fclk=serial_clk, pclk=pixel_clk, reset=pll_lock };
TMDS_DATA[3] = { mode=DIFFERENTIAL, standard=LVDS25, drive=3.5, width=10, fclk=serial_clk, pclk=pixel_clk, reset=pll_lock };
}
MAP {
// System Clock 27MHz
SCLK = 4;
// Buttons (active high)
KEY[0] = 87;
KEY[1] = 88;
// LEDs (active low)
LED[0] = 15;
LED[1] = 16;
LED[2] = 17;
LED[3] = 18;
LED[4] = 19;
LED[5] = 20;
// DVI TMDS differential pairs
TMDS_CLK = { P=33, N=34 };
TMDS_DATA[0] = { P=35, N=36 };
TMDS_DATA[1] = { P=37, N=38 };
TMDS_DATA[2] = { P=39, N=40 };
// DONE (POR)
DONE = IOR32B;
}
CLOCK_GEN {
PLL {
IN REF_CLK SCLK;
OUT BASE serial_clk; // 185.625 MHz (5x pixel clock)
WIRE LOCK pll_lock;
CONFIG {
IDIV = 7; // divider = 8
FBDIV = 54; // multiplier = 55
ODIV = 4; // VCO = 185.625 * 4 = 742.5 MHz
};
};
CLKDIV {
IN REF_CLK serial_clk;
OUT BASE pixel_clk; // 185.625 / 5 = 37.125 MHz
CONFIG {
DIV_MODE = 5;
};
};
}
@top dvi_top {
IN [1] clk = pixel_clk;
IN [1] por = DONE;
IN [1] rst_n = ~KEY[1];
IN [1] next = KEY[0];
OUT [6] leds = ~LED;
OUT [10] tmds_clk = TMDS_CLK;
OUT [10] tmds_d0 = TMDS_DATA[0];
OUT [10] tmds_d1 = TMDS_DATA[1];
OUT [10] tmds_d2 = TMDS_DATA[2];
}
@endprojjz
// DVI 1280x720 @ 30Hz Color Bar Test Pattern Top Module
// Outputs R/G/B/White/Black vertical bars (256px each).
// Uses TMDS encoding for DVI output.
//
// All TMDS outputs go through a registered mux in the SYNCHRONOUS block
// to give the OSER10 a clean register-to-primitive path.
@module dvi_top
PORT {
IN [1] clk; // pixel_clk (37.125 MHz)
IN [1] por; // POR input from DONE
IN [1] rst_n; // Active-low reset from button
IN [1] next; // Pattern select button
OUT [6] leds; // Status LEDs
OUT [10] tmds_clk; // TMDS clock channel (serialized)
OUT [10] tmds_d0; // TMDS data channel 0 (blue)
OUT [10] tmds_d1; // TMDS data channel 1 (green)
OUT [10] tmds_d2; // TMDS data channel 2 (red)
}
WIRE {
reset [1];
por_n [1];
hsync [1];
vsync [1];
de [1];
x_pos [11];
y_pos [10];
red [8];
green [8];
blue [8];
// DVI TMDS encoder outputs
dvi_tmds_d0 [10];
dvi_tmds_d1 [10];
dvi_tmds_d2 [10];
// Debounced button press pulse
next_press [1];
// Color bar pattern outputs
hbar_r [8];
hbar_g [8];
hbar_b [8];
vbar_r [8];
vbar_g [8];
vbar_b [8];
@feature CONFIG.warp == 1
warp_r [8];
warp_g [8];
warp_b [8];
@endfeat
}
REGISTER {
heartbeat_cnt [25] = 25'b0;
heartbeat_led [1] = 1'b0;
// Pattern select (0=hbars, 1=vbars, 2=warp)
pattern_sel [2] = 2'b00;
pattern_pending [1] = 1'b0;
vsync_prev [1] = 1'b0;
// TMDS output register
tmds_d0_r [10] = 10'd0;
tmds_d1_r [10] = 10'd0;
tmds_d2_r [10] = 10'd0;
}
@new por0 por {
IN [1] clk = clk;
IN [1] done = por;
OUT [1] por_n = por_n;
}
@new vt0 video_timing {
IN [1] clk = clk;
IN [1] rst_n = reset;
OUT [1] hsync = hsync;
OUT [1] vsync = vsync;
OUT [1] display_enable = de;
OUT [11] x_pos = x_pos;
OUT [10] y_pos = y_pos;
}
// Blue channel (data channel 0) - carries sync signals
@new enc0 tmds_encoder_10 {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [8] data_in = blue;
IN [1] c0 = hsync;
IN [1] c1 = vsync;
IN [1] display_enable = de;
OUT [10] tmds_out = dvi_tmds_d0;
}
// Green channel (data channel 1)
@new enc1 tmds_encoder_10 {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [8] data_in = green;
IN [1] c0 = 1'b0;
IN [1] c1 = 1'b0;
IN [1] display_enable = de;
OUT [10] tmds_out = dvi_tmds_d1;
}
// Red channel (data channel 2)
@new enc2 tmds_encoder_10 {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [8] data_in = red;
IN [1] c0 = 1'b0;
IN [1] c1 = 1'b0;
IN [1] display_enable = de;
OUT [10] tmds_out = dvi_tmds_d2;
}
@new hb0 hbars {
IN [11] x_pos = x_pos;
OUT [8] red = hbar_r;
OUT [8] green = hbar_g;
OUT [8] blue = hbar_b;
}
@new vb0 vbars {
IN [10] y_pos = y_pos;
OUT [8] red = vbar_r;
OUT [8] green = vbar_g;
OUT [8] blue = vbar_b;
}
@new db0 debounce {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [1] btn_in = next;
OUT [1] btn_press = next_press;
}
@feature CONFIG.warp == 1
@new wp0 warp {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [11] x_pos = x_pos;
IN [10] y_pos = y_pos;
IN [1] vsync = vsync;
OUT [8] red = warp_r;
OUT [8] green = warp_g;
OUT [8] blue = warp_b;
}
@endfeat
ASYNCHRONOUS {
reset <= rst_n & por_n;
// TMDS clock channel and registered output to port
tmds_clk <= 10'b1111100000;
tmds_d0 <= tmds_d0_r;
tmds_d1 <= tmds_d1_r;
tmds_d2 <= tmds_d2_r;
// Pattern mux: select based on pattern_sel
IF (pattern_sel == 2'd0) {
red <= hbar_r;
green <= hbar_g;
blue <= hbar_b;
} ELSE {
@feature CONFIG.warp == 1
IF (pattern_sel == 2'd1) {
red <= vbar_r;
green <= vbar_g;
blue <= vbar_b;
} ELSE {
red <= warp_r;
green <= warp_g;
blue <= warp_b;
}
@else
red <= vbar_r;
green <= vbar_g;
blue <= vbar_b;
@endfeat
}
// LED status
leds <= { heartbeat_led, 5'b00000 };
}
SYNCHRONOUS(CLK=clk RESET=reset RESET_ACTIVE=Low) {
// DVI TMDS straight to output register
tmds_d0_r <= dvi_tmds_d0;
tmds_d1_r <= dvi_tmds_d1;
tmds_d2_r <= dvi_tmds_d2;
vsync_prev <= vsync;
// Latch toggle on button press, apply on vsync rising edge
IF (vsync == 1'b1 && vsync_prev == 1'b0 && pattern_pending == 1'b1) {
@feature CONFIG.warp == 1
IF (pattern_sel == 2'd2) {
pattern_sel <= 2'd0;
} ELSE {
pattern_sel <= pattern_sel + 2'd1;
}
@else
IF (pattern_sel == 2'd1) {
pattern_sel <= 2'd0;
} ELSE {
pattern_sel <= pattern_sel + 2'd1;
}
@endfeat
pattern_pending <= 1'b0;
} ELIF (next_press == 1'b1) {
pattern_pending <= 1'b1;
}
// Heartbeat blinker
IF (heartbeat_cnt == 25'd33_554_431) {
heartbeat_cnt <= 25'b0;
heartbeat_led <= ~heartbeat_led;
} ELSE {
heartbeat_cnt <= heartbeat_cnt + 25'b1;
}
}
@endmodjz
// Video Timing Generator
// Resolution and blanking set via CONFIG parameters.
// Sync polarity: positive (sync HIGH during sync pulse)
@module video_timing
PORT {
IN [1] clk;
IN [1] rst_n;
OUT [1] hsync;
OUT [1] vsync;
OUT [1] display_enable;
OUT [11] x_pos;
OUT [10] y_pos;
}
CONST {
H_ACTIVE = CONFIG.h_active;
H_FRONT = CONFIG.h_front;
H_SYNC = CONFIG.h_sync;
H_BACK = CONFIG.h_back;
H_TOTAL = CONFIG.h_active + CONFIG.h_front + CONFIG.h_sync + CONFIG.h_back;
V_ACTIVE = CONFIG.v_active;
V_FRONT = CONFIG.v_front;
V_SYNC = CONFIG.v_sync;
V_BACK = CONFIG.v_back;
V_TOTAL = CONFIG.v_active + CONFIG.v_front + CONFIG.v_sync + CONFIG.v_back;
}
REGISTER {
h_cnt [11] = 11'b0;
v_cnt [10] = 10'b0;
}
ASYNCHRONOUS {
// Positive sync polarity: HIGH during sync pulse, LOW otherwise
hsync <= (h_cnt >= lit(11, H_ACTIVE + H_FRONT) &&
h_cnt < lit(11, H_ACTIVE + H_FRONT + H_SYNC))
? 1'b1 : 1'b0;
vsync <= (v_cnt >= lit(10, V_ACTIVE + V_FRONT) &&
v_cnt < lit(10, V_ACTIVE + V_FRONT + V_SYNC))
? 1'b1 : 1'b0;
// Display enable: active region
display_enable <= (h_cnt < lit(11, H_ACTIVE) &&
v_cnt < lit(10, V_ACTIVE))
? 1'b1 : 1'b0;
x_pos <= h_cnt;
y_pos <= v_cnt;
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
IF (h_cnt == lit(11, H_TOTAL - 1)) {
h_cnt <= 11'b0;
IF (v_cnt == lit(10, V_TOTAL - 1)) {
v_cnt <= 10'b0;
} ELSE {
v_cnt <= v_cnt + 10'b1;
}
} ELSE {
h_cnt <= h_cnt + 11'b1;
}
}
@endmodjz
// DVI TMDS 8b/10b Encoder
// Full DVI-compliant TMDS encoding with XOR/XNOR selection and
// running disparity tracking for DC balance on AC-coupled links.
@module tmds_encoder_10
PORT {
IN [1] clk;
IN [1] rst_n;
IN [8] data_in;
IN [1] c0;
IN [1] c1;
IN [1] display_enable;
OUT [10] tmds_out;
}
WIRE {
// Popcount of data_in (adder tree)
d_p0 [2]; d_p1 [2]; d_p2 [2]; d_p3 [2];
d_s0 [3]; d_s1 [3];
n1_d [4];
// XOR/XNOR mode selection
use_xnor [1];
// Transition-minimized intermediate word q_m[8:0]
qm0 [1]; qm1 [1]; qm2 [1]; qm3 [1];
qm4 [1]; qm5 [1]; qm6 [1]; qm7 [1];
qm8 [1];
// Popcount of q_m[7:0] (adder tree)
q_p0 [2]; q_p1 [2]; q_p2 [2]; q_p3 [2];
q_s0 [3]; q_s1 [3];
n1_q [4];
// Disparity conditions
cnt_is_zero [1];
qm_balanced [1];
cond1 [1];
cnt_sign [1];
cond_inv [1];
// Arithmetic for disparity update (5-bit two's complement)
diff_n1n0 [5];
diff_n0n1 [5];
qm8_x2 [5];
nqm8_x2 [5];
// Combinational outputs
tmds_data [10];
next_cnt [5];
}
REGISTER {
cnt [5] = 5'b00000;
tmds_reg [10] = 10'b0000000000;
}
ASYNCHRONOUS {
tmds_out <= tmds_reg;
// --- Popcount of data_in ---
d_p0 <= {1'b0, data_in[0]} + {1'b0, data_in[1]};
d_p1 <= {1'b0, data_in[2]} + {1'b0, data_in[3]};
d_p2 <= {1'b0, data_in[4]} + {1'b0, data_in[5]};
d_p3 <= {1'b0, data_in[6]} + {1'b0, data_in[7]};
d_s0 <= {1'b0, d_p0} + {1'b0, d_p1};
d_s1 <= {1'b0, d_p2} + {1'b0, d_p3};
n1_d <= {1'b0, d_s0} + {1'b0, d_s1};
// --- XOR/XNOR selection (DVI spec section 3.3.1) ---
use_xnor <= (n1_d > 4'd4 || (n1_d == 4'd4 && data_in[0] == 1'b0))
? 1'b1 : 1'b0;
// --- Build transition-minimized word q_m ---
qm0 <= data_in[0];
qm1 <= (use_xnor == 1'b1) ? ~(data_in[1] ^ qm0) : (data_in[1] ^ qm0);
qm2 <= (use_xnor == 1'b1) ? ~(data_in[2] ^ qm1) : (data_in[2] ^ qm1);
qm3 <= (use_xnor == 1'b1) ? ~(data_in[3] ^ qm2) : (data_in[3] ^ qm2);
qm4 <= (use_xnor == 1'b1) ? ~(data_in[4] ^ qm3) : (data_in[4] ^ qm3);
qm5 <= (use_xnor == 1'b1) ? ~(data_in[5] ^ qm4) : (data_in[5] ^ qm4);
qm6 <= (use_xnor == 1'b1) ? ~(data_in[6] ^ qm5) : (data_in[6] ^ qm5);
qm7 <= (use_xnor == 1'b1) ? ~(data_in[7] ^ qm6) : (data_in[7] ^ qm6);
qm8 <= (use_xnor == 1'b1) ? 1'b0 : 1'b1;
// --- Popcount of q_m[7:0] ---
q_p0 <= {1'b0, qm0} + {1'b0, qm1};
q_p1 <= {1'b0, qm2} + {1'b0, qm3};
q_p2 <= {1'b0, qm4} + {1'b0, qm5};
q_p3 <= {1'b0, qm6} + {1'b0, qm7};
q_s0 <= {1'b0, q_p0} + {1'b0, q_p1};
q_s1 <= {1'b0, q_p2} + {1'b0, q_p3};
n1_q <= {1'b0, q_s0} + {1'b0, q_s1};
// --- Disparity conditions ---
cnt_is_zero <= (cnt == 5'b00000) ? 1'b1 : 1'b0;
qm_balanced <= (n1_q == 4'd4) ? 1'b1 : 1'b0;
cond1 <= (cnt_is_zero == 1'b1 || qm_balanced == 1'b1)
? 1'b1 : 1'b0;
cnt_sign <= cnt[4];
cond_inv <= ((cnt_sign == 1'b0 && cnt_is_zero == 1'b0 && n1_q > 4'd4) ||
(cnt_sign == 1'b1 && n1_q < 4'd4))
? 1'b1 : 1'b0;
// --- Arithmetic helpers (5-bit two's complement) ---
diff_n1n0 <= {n1_q, 1'b0} - 5'd8;
diff_n0n1 <= 5'd8 - {n1_q, 1'b0};
qm8_x2 <= {3'b000, qm8, 1'b0};
nqm8_x2 <= {3'b000, ~qm8, 1'b0};
// --- Output word and next disparity (DVI spec section 3.3.2) ---
IF (cond1 == 1'b1) {
IF (qm8 == 1'b0) {
// XNOR mode, cnt==0 or balanced: invert data, bit[9]=1
tmds_data <= {1'b1, 1'b0, ~qm7, ~qm6, ~qm5, ~qm4,
~qm3, ~qm2, ~qm1, ~qm0};
next_cnt <= cnt + diff_n0n1;
} ELSE {
// XOR mode, cnt==0 or balanced: keep data, bit[9]=0
tmds_data <= {1'b0, 1'b1, qm7, qm6, qm5, qm4,
qm3, qm2, qm1, qm0};
next_cnt <= cnt + diff_n1n0;
}
} ELIF (cond_inv == 1'b1) {
// Invert to reduce disparity
tmds_data <= {1'b1, qm8, ~qm7, ~qm6, ~qm5, ~qm4,
~qm3, ~qm2, ~qm1, ~qm0};
next_cnt <= cnt + qm8_x2 + diff_n0n1;
} ELSE {
// Don't invert
tmds_data <= {1'b0, qm8, qm7, qm6, qm5, qm4,
qm3, qm2, qm1, qm0};
next_cnt <= cnt - nqm8_x2 + diff_n1n0;
}
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
IF (display_enable == 1'b0) {
// Control period: reset disparity and emit control tokens
cnt <= 5'b00000;
IF (c0 == 1'b0 && c1 == 1'b0) {
tmds_reg <= 10'b1101010100;
} ELIF (c0 == 1'b1 && c1 == 1'b0) {
tmds_reg <= 10'b0010101011;
} ELIF (c0 == 1'b0 && c1 == 1'b1) {
tmds_reg <= 10'b0101010100;
} ELSE {
tmds_reg <= 10'b1010101011;
}
} ELSE {
// Data period: latch encoded word and update disparity
tmds_reg <= tmds_data;
cnt <= next_cnt;
}
}
@endmodjz
// DVI TMDS 8b/10b Encoder with internal 2-bit shift register
// Same TMDS encoding and PORT as tmds_encoder_10. The 10-bit encoded
// word is stored in a register and output on tmds_out[10] exactly like
// the _10 variant. Internally, a parallel shift register also serializes
// the word 2 bits at a time — this output is exposed as serial_d[2] for
// connection to a 2:1 DDR primitive (e.g., ODDRX1F on ECP5).
//
// clk = 5x pixel clock (drives the shift register)
// The TMDS encoding and disparity tracking run at pixel rate by gating
// on shift_cnt == 0 (once every 5 serial clocks).
@module tmds_encoder_2
PORT {
IN [1] clk;
IN [1] rst_n;
IN [8] data_in;
IN [1] c0;
IN [1] c1;
IN [1] display_enable;
OUT [2] tmds_out;
}
WIRE {
// Popcount of data_in (adder tree)
d_p0 [2]; d_p1 [2]; d_p2 [2]; d_p3 [2];
d_s0 [3]; d_s1 [3];
n1_d [4];
// XOR/XNOR mode selection
use_xnor [1];
// Transition-minimized intermediate word q_m[8:0]
qm0 [1]; qm1 [1]; qm2 [1]; qm3 [1];
qm4 [1]; qm5 [1]; qm6 [1]; qm7 [1];
qm8 [1];
// Popcount of q_m[7:0] (adder tree)
q_p0 [2]; q_p1 [2]; q_p2 [2]; q_p3 [2];
q_s0 [3]; q_s1 [3];
n1_q [4];
// Disparity conditions
cnt_is_zero [1];
qm_balanced [1];
cond1 [1];
cnt_sign [1];
cond_inv [1];
// Arithmetic for disparity update (5-bit two's complement)
diff_n1n0 [5];
diff_n0n1 [5];
qm8_x2 [5];
nqm8_x2 [5];
// Combinational TMDS encoded word
tmds_data [10];
next_cnt [5];
}
REGISTER {
cnt [5] = 5'b00000;
// Internal shift register for 2-bit serialization
shift_reg [10] = 10'b0000000000;
shift_cnt [3] = 3'b000;
}
ASYNCHRONOUS {
// 2-bit serial output from shift register (for DDR IO)
tmds_out <= shift_reg[1:0];
// --- Popcount of data_in ---
d_p0 <= {1'b0, data_in[0]} + {1'b0, data_in[1]};
d_p1 <= {1'b0, data_in[2]} + {1'b0, data_in[3]};
d_p2 <= {1'b0, data_in[4]} + {1'b0, data_in[5]};
d_p3 <= {1'b0, data_in[6]} + {1'b0, data_in[7]};
d_s0 <= {1'b0, d_p0} + {1'b0, d_p1};
d_s1 <= {1'b0, d_p2} + {1'b0, d_p3};
n1_d <= {1'b0, d_s0} + {1'b0, d_s1};
// --- XOR/XNOR selection (DVI spec section 3.3.1) ---
use_xnor <= (n1_d > 4'd4 || (n1_d == 4'd4 && data_in[0] == 1'b0))
? 1'b1 : 1'b0;
// --- Build transition-minimized word q_m ---
qm0 <= data_in[0];
qm1 <= (use_xnor == 1'b1) ? ~(data_in[1] ^ qm0) : (data_in[1] ^ qm0);
qm2 <= (use_xnor == 1'b1) ? ~(data_in[2] ^ qm1) : (data_in[2] ^ qm1);
qm3 <= (use_xnor == 1'b1) ? ~(data_in[3] ^ qm2) : (data_in[3] ^ qm2);
qm4 <= (use_xnor == 1'b1) ? ~(data_in[4] ^ qm3) : (data_in[4] ^ qm3);
qm5 <= (use_xnor == 1'b1) ? ~(data_in[5] ^ qm4) : (data_in[5] ^ qm4);
qm6 <= (use_xnor == 1'b1) ? ~(data_in[6] ^ qm5) : (data_in[6] ^ qm5);
qm7 <= (use_xnor == 1'b1) ? ~(data_in[7] ^ qm6) : (data_in[7] ^ qm6);
qm8 <= (use_xnor == 1'b1) ? 1'b0 : 1'b1;
// --- Popcount of q_m[7:0] ---
q_p0 <= {1'b0, qm0} + {1'b0, qm1};
q_p1 <= {1'b0, qm2} + {1'b0, qm3};
q_p2 <= {1'b0, qm4} + {1'b0, qm5};
q_p3 <= {1'b0, qm6} + {1'b0, qm7};
q_s0 <= {1'b0, q_p0} + {1'b0, q_p1};
q_s1 <= {1'b0, q_p2} + {1'b0, q_p3};
n1_q <= {1'b0, q_s0} + {1'b0, q_s1};
// --- Disparity conditions ---
cnt_is_zero <= (cnt == 5'b00000) ? 1'b1 : 1'b0;
qm_balanced <= (n1_q == 4'd4) ? 1'b1 : 1'b0;
cond1 <= (cnt_is_zero == 1'b1 || qm_balanced == 1'b1)
? 1'b1 : 1'b0;
cnt_sign <= cnt[4];
cond_inv <= ((cnt_sign == 1'b0 && cnt_is_zero == 1'b0 && n1_q > 4'd4) ||
(cnt_sign == 1'b1 && n1_q < 4'd4))
? 1'b1 : 1'b0;
// --- Arithmetic helpers (5-bit two's complement) ---
diff_n1n0 <= {n1_q, 1'b0} - 5'd8;
diff_n0n1 <= 5'd8 - {n1_q, 1'b0};
qm8_x2 <= {3'b000, qm8, 1'b0};
nqm8_x2 <= {3'b000, ~qm8, 1'b0};
// --- Output word and next disparity (DVI spec section 3.3.2) ---
IF (cond1 == 1'b1) {
IF (qm8 == 1'b0) {
tmds_data <= {1'b1, 1'b0, ~qm7, ~qm6, ~qm5, ~qm4,
~qm3, ~qm2, ~qm1, ~qm0};
next_cnt <= cnt + diff_n0n1;
} ELSE {
tmds_data <= {1'b0, 1'b1, qm7, qm6, qm5, qm4,
qm3, qm2, qm1, qm0};
next_cnt <= cnt + diff_n1n0;
}
} ELIF (cond_inv == 1'b1) {
tmds_data <= {1'b1, qm8, ~qm7, ~qm6, ~qm5, ~qm4,
~qm3, ~qm2, ~qm1, ~qm0};
next_cnt <= cnt + qm8_x2 + diff_n0n1;
} ELSE {
tmds_data <= {1'b0, qm8, qm7, qm6, qm5, qm4,
qm3, qm2, qm1, qm0};
next_cnt <= cnt - nqm8_x2 + diff_n1n0;
}
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
IF (shift_cnt == 3'd4) {
// Every 5th serial clock: load new word, update encoding
shift_cnt <= 3'd0;
IF (display_enable == 1'b0) {
cnt <= 5'b00000;
IF (c0 == 1'b0 && c1 == 1'b0) {
shift_reg <= 10'b1101010100;
} ELIF (c0 == 1'b1 && c1 == 1'b0) {
shift_reg <= 10'b0010101011;
} ELIF (c0 == 1'b0 && c1 == 1'b1) {
shift_reg <= 10'b0101010100;
} ELSE {
shift_reg <= 10'b1010101011;
}
} ELSE {
shift_reg <= tmds_data;
cnt <= next_cnt;
}
} ELSE {
// Shift out 2 bits: shift right by 2
shift_reg <= {2'b00, shift_reg[9:2]};
shift_cnt <= shift_cnt + 3'd1;
}
}
@endmodjz
// Horizontal color bars pattern generator
// 5 bars: Red, Green, Blue, White, Black (equal width from CONFIG.h_active)
@module hbars
PORT {
IN [11] x_pos;
OUT [8] red;
OUT [8] green;
OUT [8] blue;
}
CONST {
BAR = CONFIG.h_active / 5;
}
ASYNCHRONOUS {
IF (x_pos < lit(11, BAR)) {
red <= 8'hFF; green <= 8'h00; blue <= 8'h00;
} ELSE { IF (x_pos < lit(11, BAR * 2)) {
red <= 8'h00; green <= 8'hFF; blue <= 8'h00;
} ELSE { IF (x_pos < lit(11, BAR * 3)) {
red <= 8'h00; green <= 8'h00; blue <= 8'hFF;
} ELSE { IF (x_pos < lit(11, BAR * 4)) {
red <= 8'hFF; green <= 8'hFF; blue <= 8'hFF;
} ELSE {
red <= 8'h00; green <= 8'h00; blue <= 8'h00;
} } } }
}
@endmodjz
// Vertical color bars pattern generator
// 5 bars: Red, Green, Blue, White, Black (equal height from CONFIG.v_active)
@module vbars
PORT {
IN [10] y_pos;
OUT [8] red;
OUT [8] green;
OUT [8] blue;
}
CONST {
BAR = CONFIG.v_active / 5;
}
ASYNCHRONOUS {
IF (y_pos < lit(10, BAR)) {
red <= 8'hFF; green <= 8'h00; blue <= 8'h00;
} ELSE { IF (y_pos < lit(10, BAR * 2)) {
red <= 8'h00; green <= 8'hFF; blue <= 8'h00;
} ELSE { IF (y_pos < lit(10, BAR * 3)) {
red <= 8'h00; green <= 8'h00; blue <= 8'hFF;
} ELSE { IF (y_pos < lit(10, BAR * 4)) {
red <= 8'hFF; green <= 8'hFF; blue <= 8'hFF;
} ELSE {
red <= 8'h00; green <= 8'h00; blue <= 8'h00;
} } } }
}
@endmodjz
// Starfield warp effect
// Stars accelerate radially from center, respawn via LFSR when offscreen.
// Each star rendered as a 2x2 white pixel on black background.
//
// Tuning: SPEED controls frame divider (higher = slower overall).
// Acceleration curve is velocity = distance/32 + 1 per update.
@module warp
CONST {
NUM_STARS = 30;
SPEED = 4; // Update every Nth vsync (1=fastest)
CX = CONFIG.h_active / 2;
CY = CONFIG.v_active / 2;
MAX_X = CONFIG.h_active - 10;
MAX_Y = CONFIG.v_active - 10;
SPAWN_X = CONFIG.h_active / 2 - 32;
SPAWN_Y = CONFIG.v_active / 2 - 32;
}
PORT {
IN [1] clk;
IN [1] rst_n;
IN [11] x_pos;
IN [10] y_pos;
IN [1] vsync;
OUT [8] red;
OUT [8] green;
OUT [8] blue;
}
WIRE {
vsync_edge [1];
update [1];
star_dx [NUM_STARS * 11];
star_dy [NUM_STARS * 10];
hits [NUM_STARS];
next_x [NUM_STARS * 11];
next_y [NUM_STARS * 10];
}
REGISTER {
vsync_r [1] = 1'b0;
lfsr [32] = 32'hDEADBEEF;
frame_cnt [8] = 8'd0;
star_x [NUM_STARS * 11] = lit(NUM_STARS * 11, 0);
star_y [NUM_STARS * 10] = lit(NUM_STARS * 10, 0);
}
// Absolute distance from screen center
@template STAR_DIST(dx, dy, sx, sy)
dx[IDX*11+10:IDX*11] <=
(sx[IDX*11+10:IDX*11] >= lit(11, CX))
? (sx[IDX*11+10:IDX*11] - lit(11, CX))
: (lit(11, CX) - sx[IDX*11+10:IDX*11]);
dy[IDX*10+9:IDX*10] <=
(sy[IDX*10+9:IDX*10] >= lit(10, CY))
? (sy[IDX*10+9:IDX*10] - lit(10, CY))
: (lit(10, CY) - sy[IDX*10+9:IDX*10]);
@endtemplate
// Pixel hit test — 2x2 dot
@template STAR_HIT(hits, sx, sy, xp, yp)
hits[IDX] <= (
xp >= sx[IDX*11+10:IDX*11] &&
xp <= sx[IDX*11+10:IDX*11] + 11'd1 &&
yp >= sy[IDX*10+9:IDX*10] &&
yp <= sy[IDX*10+9:IDX*10] + 10'd1
) ? 1'b1 : 1'b0;
@endtemplate
// Compute next position: move radially or respawn if offscreen
@template STAR_NEXT(nx, ny, sx, sy, dx, dy, rng)
IF (sx[IDX*11+10:IDX*11] > lit(11, MAX_X) || sy[IDX*10+9:IDX*10] > lit(10, MAX_Y)) {
nx[IDX*11+10:IDX*11] <= lit(11, SPAWN_X) + {5'b00000, rng[IDX*2%26+5:IDX*2%26]};
ny[IDX*10+9:IDX*10] <= lit(10, SPAWN_Y) + {4'b0000, rng[IDX*3%26+5:IDX*3%26]};
} ELSE {
IF (sx[IDX*11+10:IDX*11] >= lit(11, CX)) {
nx[IDX*11+10:IDX*11] <= sx[IDX*11+10:IDX*11]
+ (dx[IDX*11+10:IDX*11] >> 11'd5) + 11'd1;
} ELSE {
nx[IDX*11+10:IDX*11] <= sx[IDX*11+10:IDX*11]
- (dx[IDX*11+10:IDX*11] >> 11'd5) - 11'd1;
}
IF (sy[IDX*10+9:IDX*10] >= lit(10, CY)) {
ny[IDX*10+9:IDX*10] <= sy[IDX*10+9:IDX*10]
+ (dy[IDX*10+9:IDX*10] >> 11'd5) + 10'd1;
} ELSE {
ny[IDX*10+9:IDX*10] <= sy[IDX*10+9:IDX*10]
- (dy[IDX*10+9:IDX*10] >> 11'd5) - 10'd1;
}
}
@endtemplate
ASYNCHRONOUS {
vsync_edge <= vsync & ~vsync_r;
update <= (vsync_edge == 1'b1 && frame_cnt == lit(8, SPEED - 1))
? 1'b1 : 1'b0;
@apply [NUM_STARS] STAR_DIST(star_dx, star_dy, star_x, star_y);
@apply [NUM_STARS] STAR_HIT(hits, star_x, star_y, x_pos, y_pos);
@apply [NUM_STARS] STAR_NEXT(next_x, next_y, star_x, star_y, star_dx, star_dy, lfsr);
IF (hits != lit(NUM_STARS, 0)) {
red <= 8'hFF; green <= 8'hFF; blue <= 8'hFF;
} ELSE {
red <= 8'h00; green <= 8'h00; blue <= 8'h00;
}
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
vsync_r <= vsync;
lfsr <= {lfsr[30:0], lfsr[31] ^ lfsr[21] ^ lfsr[1] ^ lfsr[0]};
// Frame divider
IF (vsync_edge == 1'b1) {
IF (frame_cnt == lit(8, SPEED - 1)) {
frame_cnt <= 8'd0;
} ELSE {
frame_cnt <= frame_cnt + 8'd1;
}
}
// Latch next positions on update
IF (update == 1'b1) {
star_x <= next_x;
star_y <= next_y;
}
}
@endmodjz
// Button Debounce Module
// 2FF synchronizer + timing-based debounce + edge detect.
// Outputs a single-cycle pulse on each physical button press.
// Default debounce window ~20ms at 37.125 MHz (~742,500 cycles).
@module debounce
CONST {
DEBOUNCE_COUNT = 742500;
COUNTER_BITS = clog2(DEBOUNCE_COUNT);
COUNTER_MAX = DEBOUNCE_COUNT - 1;
}
PORT {
IN [1] clk;
IN [1] rst_n;
IN [1] btn_in; // Raw button input (active-low)
OUT [1] btn_press; // Single-cycle pulse on press
}
REGISTER {
// 2FF synchronizer
sync1 [1] = 1'b1;
sync2 [1] = 1'b1;
// Debounce state
stable [1] = 1'b1;
counter [COUNTER_BITS] = COUNTER_BITS'b0;
// Edge detect
stable_prev [1] = 1'b1;
}
ASYNCHRONOUS {
// Falling edge of debounced signal = button press (active-low)
btn_press <= stable_prev & ~stable;
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
// 2FF synchronizer for metastability
sync1 <= btn_in;
sync2 <= sync1;
// Edge detect register
stable_prev <= stable;
// Debounce logic
IF (sync2 != stable) {
IF (counter == lit(COUNTER_BITS, COUNTER_MAX)) {
stable <= sync2;
counter <= COUNTER_BITS'b0;
} ELSE {
counter <= counter + COUNTER_BITS'b1;
}
} ELSE {
counter <= COUNTER_BITS'b0;
}
}
@endmodjz
@module por
PORT {
IN [1] clk;
IN [1] done;
OUT [1] por_n;
}
CONST {
POR_CYCLES = 1_048_576; // ~28ms at 37.125MHz — wait for PLL lock
POR_CNT_BITS = clog2(POR_CYCLES);
POR_MAX = POR_CYCLES - 1;
}
REGISTER {
por_reg [1] = 1'b0;
cnt [POR_CNT_BITS] = POR_CNT_BITS'b0;
}
ASYNCHRONOUS {
por_n <= por_reg;
}
SYNCHRONOUS(CLK=clk) {
IF (done == 1'b0) {
por_reg <= 1'b0;
cnt <= POR_CNT_BITS'b0;
} ELIF (cnt == lit(POR_CNT_BITS, POR_MAX)) {
por_reg <= 1'b1;
cnt <= cnt;
} ELSE {
por_reg <= 1'b0;
cnt <= cnt + POR_CNT_BITS'b1;
}
}
@endmodJZ-HDL Language Features
Clock chain validation. The CLOCK_GEN block declares PLL and CLKDIV together with their input/output relationships. The compiler verifies VCO range, divider ratios, and serializer clock compatibility end-to-end. Traditional flows configure these separately in vendor GUI tools with no cross-validation.
Templates. @template and @apply eliminate repetitive per-instance logic. The 30-star warp module would require 30 copies of identical code in Verilog — or a generate block with synthesis-tool-dependent behavior.
Conditional compilation. @feature / @endfeat directives gate entire module instantiations and their associated wiring on project-level CONFIG values, enabling single-source multi-target builds.
Integrated differential I/O. Pin declarations with mode=DIFFERENTIAL, clock bindings (fclk, pclk), and reset replace manual instantiation of vendor serializer primitives (OSER10, TLVDS_OBUF) and their associated constraint files. The WIRE LOCK output from CLOCK_GEN feeds the serializer reset automatically — the compiler inverts the lock signal so the serializer is held in reset until the PLL stabilizes.