Skip to content

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_clk

Both 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:

  1. Popcount the 8-bit input via an adder tree.
  2. Select XOR or XNOR mode based on whether the popcount exceeds 4.
  3. Build a 9-bit transition-minimized word by chaining XOR/XNOR of adjacent bits.
  4. 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_sel register increments on vsync rising edge when pattern_pending is 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'b1111100000 pattern.
  • 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];
    }
@endproj
jz
// 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;
        }
    }
@endmod
jz
// 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;
        }
    }
@endmod
jz
// 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;
        }
    }
@endmod
jz
// 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;
        }
    }
@endmod
jz
// 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;
        } } } }
    }
@endmod
jz
// 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;
        } } } }
    }
@endmod
jz
// 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;
        }
    }
@endmod
jz
// 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;
        }
    }
@endmod
jz
@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;
        }
    }
@endmod

JZ-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.