DVI with Audio
A 1280x720 @ 30Hz DVI display with embedded audio. Four BRAM-backed tone generators play classical melodies as square wave PCM, mixed into a stereo stream and injected into DVI data island packets during horizontal blanking. An 80-bar spectrum analyzer with peak hold and color-gradient rendering provides real-time visualization on screen.
The design uses the same clock architecture as the DVI Color Bars example: 27 MHz crystal → 185.625 MHz serial clock (PLL) → 37.125 MHz pixel clock (CLKDIV).
Audio Pipeline
text
Binary melody files (track0-3.bin)
└─ melodies (BRAM, 3-cycle read latency)
└─ tone_gen ×4 (Bresenham 48kHz, square wave PCM)
└─ mixer (4ch signed sum + DVI subpacket + BCH ECC)
└─ dvi_data_island (packet injection during hblank)melodies
BRAM wrapper storing note data as pairs of 32-bit words (64 bits per note). Up to 1500 notes across 3 songs of 500 notes each. A phase-alternating read toggles between the two words of each note entry, latching word_hi and word_lo on alternate cycles. Fields extracted combinationally:
| Field | Bits | Description |
|---|---|---|
| half_period | 16 | Square wave half-period in 48 kHz samples |
| duration | 24 | Note length in samples |
| gap | 16 | Articulation silence at end of note |
| volume | 8 | Amplitude (0-255) |
A sentinel value (half_period = 0xFFFF) marks the end of each song for automatic loop-back.
tone_gen
Each instance reads from its own BRAM melody via OVERRIDE of a DATA_FILE constant. A Bresenham accumulator derives a 48 kHz sample clock from the 37.125 MHz pixel clock (step=16, threshold=12375). The tone generator toggles polarity at the note's half-period boundary to produce a square wave, applies volume scaling (left-shifted 3 bits for 8x gain), and runs the output through a 2-tap moving average filter for anti-aliasing. During the note's gap period, the output is zeroed for articulation.
A button press cycles between three songs by advancing song_base through offsets 0, 500, and 1000.
mixer
Purely combinational. Sums four signed 16-bit PCM channels using sadd for correct sign-extended widening, then formats the result as a DVI audio subpacket:
- Left-justifies the 16-bit mix to 24 bits.
- Computes even parity via XOR folding.
- Builds status byte 6 with parity for both L and R channels (mono duplication).
- Generates BCH(64,56) ECC using polynomial 0x83 in a combinational XOR network.
dvi_data_island
Injects four packet types into the 370-clock horizontal blanking interval:
text
Blanking period:
├─ Preamble: 8 clocks
├─ Leading guard: 2 clocks
├─ PKT0 (AVI): 32 clocks
├─ PKT1 (ACR): 32 clocks
├─ PKT2 (Audio): 32 clocks
├─ PKT3 (AIF): 32 clocks
├─ Trailing guard: 2 clocks
└─ Control periodShadow registers pre-load each packet's header and four 64-bit subpackets one cycle before transmission starts, reducing the bit-extraction mux to a single SELECT table. Audio samples are buffered between lines (2-3 samples per line at 48 kHz / ~22.5 kHz line rate).
The ACR packet carries N=6144 and CTS=37125, satisfying 48000 = 6144/37125 × 37,125,000.
terc4_encoder
Purely combinational 4-to-10-bit lookup table implementing the 16 TERC4 codewords from the DVI specification. Three instances encode the three data island channels.
Spectrum Analyzer
spectrum_analyzer
Maps each tone generator's current half-period to one of 80 log-spaced frequency bins (65 Hz to 5 kHz). For visual fullness, simulated 3rd and 5th harmonics are added with spectral spread:
- Fundamental: full volume at center bar, half at ±1, quarter at ±2.
- 3rd harmonic: 1/4 volume at center, 1/8 at ±1, 1/16 at ±2.
- 5th harmonic: 1/8 volume at center, 1/16 at ±1.
Amplitudes are smoothed per frame with an exponential moving average and stored in DISTRIBUTED RAM alongside peak-hold values with a 4-bit decay timer.
spectrum_display
Renders 80 color-gradient bars with reflections. Each bar occupies a 16-pixel pitch (10px body + 6px gap). Bars grow upward from y=600 in segments of 16 pixels (12px body + 4px gap), up to 25 segments (400px maximum height). A reflection at 1/4 brightness extends below the baseline for up to 6 segments.
The color gradient runs from red (bar 0) through yellow (bar 39) to blue (bar 79), computed via a linear ramp approximation (half_pos × 13 >> 1).
Per-Instance Data with OVERRIDE
A single tone_gen module declares a DATA_FILE constant. Each of the four instances overrides it to point at a different binary file:
jz
@new tg0 tone_gen {
OVERRIDE {
DATA_FILE = "out/track0.bin";
}
IN [1] clk = clk;
IN [1] rst_n = reset;
...
}The OVERRIDE propagates through the module hierarchy — tone_gen passes DATA_FILE down to its melodies instance, which uses it in @file for BLOCK RAM initialization. The compiler generates separate BRAM contents for each instance.
Output Pipeline
The top-level module uses a two-stage output register pipeline for clean timing to the IO serializer. A per-cycle mux selects between four output modes:
- Data island + guard: channel 0 gets TERC4, channels 1-2 get guard pattern
0100110011. - Data island active: all three channels get TERC4-encoded packet data.
- Video guard band: channels 0 and 2 get
1011001100, channel 1 gets0100110011. - Default: all channels get standard TMDS-encoded video/control data.
Build Process
bash
cd examples/dvi_audio
python3 tools/generate_melodies.py out/
makeThe script produces track0.bin through track3.bin from musical notation, which are embedded into BLOCK RAM via @file during compilation.
jz
@project(CHIP="GW2AR-18-QN88-C8-I7") DVI_AUDIO_TEST
@import "por.jz"
@import "video_timing.jz"
@import "tmds_encoder_2.jz"
@import "tmds_encoder_10.jz"
@import "terc4_encoder.jz"
@import "dvi_data_island.jz"
@import "melodies.jz"
@import "tone_gen.jz"
@import "mixer.jz"
@import "spectrum_analyzer.jz"
@import "spectrum_display.jz"
@import "debounce.jz"
@import "dvi.jz"
CONFIG {
serializer = 10;
}
@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 Vertical Color Bar with Audio
// Outputs R/G/B/White/Black horizontal bars (by y_pos).
// Uses TMDS encoding for DVI output with Data Island packets
// (AVI InfoFrame, ACR, Audio Sample, Audio InfoFrame) during blanking.
// Audio: 4-track Ode to Joy, square wave, 2ch 16-bit L-PCM at 48kHz.
//
// 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; // Next song button (active-low)
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];
// TERC4 encoder outputs
terc4_ch0_data [4];
terc4_ch1_data [4];
terc4_ch2_data [4];
terc4_d0 [10];
terc4_d1 [10];
terc4_d2 [10];
// Data island control
di_active [1];
di_preamble [1];
di_guard [1];
// DVI encoder control signals
enc1_c0 [1];
enc1_c1 [1];
enc2_c0 [1];
enc2_c1 [1];
// Video preamble and guard band (combinational)
video_preamble_pre [1];
video_guard_pre [1];
// Tone generator PCM outputs
tg0_sample [16];
tg0_valid [1];
tg1_sample [16];
tg1_valid [1];
tg2_sample [16];
tg2_valid [1];
tg3_sample [16];
tg3_valid [1];
// Tone generator audio state outputs
tg0_half_period [16];
tg0_volume [8];
tg0_in_gap [1];
tg1_half_period [16];
tg1_volume [8];
tg1_in_gap [1];
tg2_half_period [16];
tg2_volume [8];
tg2_in_gap [1];
tg3_half_period [16];
tg3_volume [8];
tg3_in_gap [1];
// Frame pulse for spectrum analyzer
frame_pulse [1];
// Button debounce
next_press [1];
// Spectrum analyzer <-> display interconnect
sp_rd_bar [7];
sp_rd_amp [16];
// Mixer outputs
mix_samp_lo [32];
mix_samp_hi [32];
mix_valid [1];
}
REGISTER {
heartbeat_cnt [25] = 25'b0;
heartbeat_led [1] = 1'b0;
// Data island pipeline (1 cycle to align TERC4 with encoder output)
di_active_r [1] = 1'b0;
di_guard_r [1] = 1'b0;
terc4_d0_r [10] = 10'd0;
terc4_d1_r [10] = 10'd0;
terc4_d2_r [10] = 10'd0;
// Video preamble and guard band
video_preamble_r [1] = 1'b0;
video_guard_r [1] = 1'b0;
// Two-stage TMDS output pipeline:
// Stage 1 (pre): mux logic settles here
tmds_d0_pre [10] = 10'd0;
tmds_d1_pre [10] = 10'd0;
tmds_d2_pre [10] = 10'd0;
// Stage 2 (r): simple copy, clean FF-to-OSER10 path
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 db0 debounce {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [1] btn_in = next;
OUT [1] btn_press = next_press;
}
@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;
}
@feature CONFIG.serializer == 10
// 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) - preamble CTL on c0/c1
@new enc1 tmds_encoder_10 {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [8] data_in = green;
IN [1] c0 = enc1_c0;
IN [1] c1 = enc1_c1;
IN [1] display_enable = de;
OUT [10] tmds_out = dvi_tmds_d1;
}
// Red channel (data channel 2) - preamble CTL on c0/c1
@new enc2 tmds_encoder_10 {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [8] data_in = red;
IN [1] c0 = enc2_c0;
IN [1] c1 = enc2_c1;
IN [1] display_enable = de;
OUT [10] tmds_out = dvi_tmds_d2;
}
@else
@endfeat
// TERC4 encoders for data island period
@new t4_0 terc4_encoder {
IN [4] data_in = terc4_ch0_data;
OUT [10] terc4_out = terc4_d0;
}
@new t4_1 terc4_encoder {
IN [4] data_in = terc4_ch1_data;
OUT [10] terc4_out = terc4_d1;
}
@new t4_2 terc4_encoder {
IN [4] data_in = terc4_ch2_data;
OUT [10] terc4_out = terc4_d2;
}
// Tone generators (4 independent tracks)
@new tg0 tone_gen {
OVERRIDE {
DATA_FILE = "../out/track0.bin";
}
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [1] next_song = next_press;
OUT [16] sample = tg0_sample;
OUT [1] samp_valid = tg0_valid;
OUT [16] half_period_out = tg0_half_period;
OUT [8] volume_out = tg0_volume;
OUT [1] in_gap_out = tg0_in_gap;
}
@new tg1 tone_gen {
OVERRIDE {
DATA_FILE = "../out/track1.bin";
}
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [1] next_song = next_press;
OUT [16] sample = tg1_sample;
OUT [1] samp_valid = tg1_valid;
OUT [16] half_period_out = tg1_half_period;
OUT [8] volume_out = tg1_volume;
OUT [1] in_gap_out = tg1_in_gap;
}
@new tg2 tone_gen {
OVERRIDE {
DATA_FILE = "../out/track2.bin";
}
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [1] next_song = next_press;
OUT [16] sample = tg2_sample;
OUT [1] samp_valid = tg2_valid;
OUT [16] half_period_out = tg2_half_period;
OUT [8] volume_out = tg2_volume;
OUT [1] in_gap_out = tg2_in_gap;
}
@new tg3 tone_gen {
OVERRIDE {
DATA_FILE = "../out/track3.bin";
}
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [1] next_song = next_press;
OUT [16] sample = tg3_sample;
OUT [1] samp_valid = tg3_valid;
OUT [16] half_period_out = tg3_half_period;
OUT [8] volume_out = tg3_volume;
OUT [1] in_gap_out = tg3_in_gap;
}
// 4-channel mixer + DVI subpacket formatter
@new mx0 mixer {
IN [16] s0 = tg0_sample;
IN [16] s1 = tg1_sample;
IN [16] s2 = tg2_sample;
IN [16] s3 = tg3_sample;
IN [1] samp_valid = tg0_valid;
OUT [32] samp_lo = mix_samp_lo;
OUT [32] samp_hi = mix_samp_hi;
OUT [1] out_valid = mix_valid;
}
// DVI data island controller (AVI + ACR + Audio Sample + Audio InfoFrame)
@new di0 dvi_data_island {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [1] hsync = hsync;
IN [1] vsync = vsync;
IN [1] display_enable = de;
IN [11] x_pos = x_pos;
IN [32] samp_lo = mix_samp_lo;
IN [32] samp_hi = mix_samp_hi;
IN [1] samp_valid = mix_valid;
OUT [4] terc4_ch0 = terc4_ch0_data;
OUT [4] terc4_ch1 = terc4_ch1_data;
OUT [4] terc4_ch2 = terc4_ch2_data;
OUT [1] data_island_active = di_active;
OUT [1] preamble_active = di_preamble;
OUT [1] guard_active = di_guard;
}
@new sa0 spectrum_analyzer {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [16] ch0_half_period = tg0_half_period;
IN [8] ch0_volume = tg0_volume;
IN [1] ch0_in_gap = tg0_in_gap;
IN [16] ch1_half_period = tg1_half_period;
IN [8] ch1_volume = tg1_volume;
IN [1] ch1_in_gap = tg1_in_gap;
IN [16] ch2_half_period = tg2_half_period;
IN [8] ch2_volume = tg2_volume;
IN [1] ch2_in_gap = tg2_in_gap;
IN [16] ch3_half_period = tg3_half_period;
IN [8] ch3_volume = tg3_volume;
IN [1] ch3_in_gap = tg3_in_gap;
IN [1] frame_pulse = frame_pulse;
IN [7] rd_bar = sp_rd_bar;
OUT [16] rd_amp = sp_rd_amp;
}
@new sd0 spectrum_display {
IN [1] clk = clk;
IN [1] rst_n = reset;
IN [11] x_pos = x_pos;
IN [10] y_pos = y_pos;
OUT [7] rd_bar = sp_rd_bar;
IN [16] rd_amp = sp_rd_amp;
OUT [8] red = red;
OUT [8] green = green;
OUT [8] blue = blue;
}
ASYNCHRONOUS {
reset <= rst_n & por_n;
// Frame pulse: high for 1 cycle at top-left pixel (start of each frame)
frame_pulse <= (x_pos == 11'd0 && y_pos == 10'd0) ? 1'b1 : 1'b0;
// TMDS clock channel: fixed 1111100000 pattern
tmds_clk <= 10'b1111100000;
// Registered TMDS output to port
tmds_d0 <= tmds_d0_r;
tmds_d1 <= tmds_d1_r;
tmds_d2 <= tmds_d2_r;
// Preamble CTL signals on ch1/ch2
// Data island preamble: ch1 c0=1,c1=0; ch2 c0=1,c1=0
// Video preamble: ch1 c0=1,c1=0; ch2 c0=0,c1=0
IF (di_preamble == 1'b1) {
enc1_c0 <= 1'b1;
enc1_c1 <= 1'b0;
enc2_c0 <= 1'b1;
enc2_c1 <= 1'b0;
} ELIF (video_preamble_r == 1'b1) {
enc1_c0 <= 1'b1;
enc1_c1 <= 1'b0;
enc2_c0 <= 1'b0;
enc2_c1 <= 1'b0;
} ELSE {
enc1_c0 <= 1'b0;
enc1_c1 <= 1'b0;
enc2_c0 <= 1'b0;
enc2_c1 <= 1'b0;
}
// Video preamble: 4-cycle pipeline (preamble_r + CTL -> encoder + output reg + OSER)
video_preamble_pre <= (
x_pos >= 11'd1640 && x_pos < 11'd1648 &&
(y_pos < 10'd719 || y_pos == 10'd749)
) ? 1'b1 : 1'b0;
// Video guard band: 3-cycle pipeline (guard_r + mux reg + OSER)
video_guard_pre <= (
(x_pos == 11'd1648 || x_pos == 11'd1649) &&
(y_pos < 10'd719 || y_pos == 10'd749)
) ? 1'b1 : 1'b0;
// LED status
leds <= { heartbeat_led, 5'b00000 };
}
SYNCHRONOUS(CLK=clk RESET=reset RESET_ACTIVE=Low) {
// Registration pipeline
di_active_r <= di_active;
di_guard_r <= di_guard;
terc4_d0_r <= terc4_d0;
terc4_d1_r <= terc4_d1;
terc4_d2_r <= terc4_d2;
video_preamble_r <= video_preamble_pre;
video_guard_r <= video_guard_pre;
// Output mux: select between data island TERC4, video guard, and DVI TMDS
IF (di_active_r == 1'b1) {
IF (di_guard_r == 1'b1) {
// DI guard band: ch0=TERC4, ch1/ch2=fixed pattern
tmds_d0_pre <= terc4_d0_r;
tmds_d1_pre <= 10'b0100110011;
tmds_d2_pre <= 10'b0100110011;
} ELSE {
// DI packet data: all channels TERC4
tmds_d0_pre <= terc4_d0_r;
tmds_d1_pre <= terc4_d1_r;
tmds_d2_pre <= terc4_d2_r;
}
} ELIF (video_guard_r == 1'b1) {
// Video guard band (DVI 1.0 / HDMI 1.4a S5.2.2)
tmds_d0_pre <= 10'b1011001100;
tmds_d1_pre <= 10'b0100110011;
tmds_d2_pre <= 10'b1011001100;
} ELSE {
// Control/preamble/video: DVI TMDS
tmds_d0_pre <= dvi_tmds_d0;
tmds_d1_pre <= dvi_tmds_d1;
tmds_d2_pre <= dvi_tmds_d2;
}
// Stage 2: Simple copy → clean FF-to-OSER10 path
tmds_d0_r <= tmds_d0_pre;
tmds_d1_r <= tmds_d1_pre;
tmds_d2_r <= tmds_d2_pre;
// 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
// 1280x720 @ 30Hz Video Timing Generator
// CEA-861 timings, pixel clock = 37.125 MHz (half of 74.25 MHz)
// H total: 1650 (1280 active + 110 front + 40 sync + 220 back)
// V total: 750 (720 active + 5 front + 5 sync + 20 back)
// 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 {
// Horizontal timing
H_ACTIVE = 1280;
H_FRONT = 110;
H_SYNC = 40;
H_BACK = 220;
H_TOTAL = 1650;
// Vertical timing
V_ACTIVE = 720;
V_FRONT = 5;
V_SYNC = 5;
V_BACK = 20;
V_TOTAL = 750;
}
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 [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 TMDS encoded word
tmds_data [10];
next_cnt [5];
}
REGISTER {
cnt [5] = 5'b00000;
tmds_reg [10] = 10'b0000000000;
// Internal shift register for 2-bit serialization
shift_reg [10] = 10'b0000000000;
shift_cnt [3] = 3'b000;
}
ASYNCHRONOUS {
// Output the full 10-bit encoded word (same as tmds_encoder_10)
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) {
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) {
tmds_reg <= 10'b1101010100;
shift_reg <= 10'b1101010100;
} ELIF (c0 == 1'b1 && c1 == 1'b0) {
tmds_reg <= 10'b0010101011;
shift_reg <= 10'b0010101011;
} ELIF (c0 == 1'b0 && c1 == 1'b1) {
tmds_reg <= 10'b0101010100;
shift_reg <= 10'b0101010100;
} ELSE {
tmds_reg <= 10'b1010101011;
shift_reg <= 10'b1010101011;
}
} ELSE {
tmds_reg <= tmds_data;
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
// DVI Data Island Controller with Audio
// Injects 4 DVI data island packets during horizontal blanking periods:
// PKT0: AVI InfoFrame (video format descriptor)
// PKT1: ACR (Audio Clock Regeneration, N=6144, CTS=37125 for 48kHz @ 37.125MHz)
// PKT2: Audio Sample (L-PCM 2ch 16-bit, 2-3 samples per line)
// PKT3: Audio InfoFrame (audio format descriptor)
//
// Uses shadow registers to reduce bit-extraction SELECT tables from 4 sets to 1.
// At each packet boundary, the next packet's data is copied into the shadow.
//
// Audio samples are provided externally via samp_lo/samp_hi/samp_valid ports.
//
// Data island timing within hblank (370 pixel clocks):
// Preamble: 8 clocks (x=1449..1456)
// Leading guard: 2 clocks (x=1457..1458)
// Packet 0 (AVI): 32 clocks (x=1459..1490)
// Packet 1 (ACR): 32 clocks (x=1491..1522)
// Packet 2 (Audio):32 clocks (x=1523..1554)
// Packet 3 (AIF): 32 clocks (x=1555..1586)
// Trailing guard: 2 clocks (x=1587..1588)
// Control period: 51 clocks until video preamble at x=1640
@module dvi_data_island
PORT {
IN [1] clk;
IN [1] rst_n;
// Video timing inputs
IN [1] hsync;
IN [1] vsync;
IN [1] display_enable;
IN [11] x_pos;
// Audio sample inputs (from tone generator)
IN [32] samp_lo; // L+R subpacket low word
IN [32] samp_hi; // L+R subpacket high word
IN [1] samp_valid; // pulses high for 1 cycle when a new sample is ready
// TERC4 data outputs (active during data island)
OUT [4] terc4_ch0; // {parity, hdr_bit, vsync, hsync}
OUT [4] terc4_ch1; // subpacket even bits
OUT [4] terc4_ch2; // subpacket odd bits
// Control signals
OUT [1] data_island_active; // HIGH during guard bands + packet data
OUT [1] preamble_active; // HIGH during data island preamble
OUT [1] guard_active; // HIGH during guard bands only
}
CONST {
H_ACTIVE = 1280;
H_TOTAL = 1650;
V_ACTIVE = 720;
// Data island timing
DI_PREAMBLE_START = 1449;
DI_GUARD_START = 1457;
DI_PKT0_START = 1459;
DI_PKT1_START = 1491;
DI_PKT2_START = 1523;
DI_PKT3_START = 1555;
DI_TRAIL_START = 1587;
DI_TRAIL_END = 1589;
// Shadow swap points (1 cycle before each packet start)
DI_SHD_SWAP1 = 1490;
DI_SHD_SWAP2 = 1522;
DI_SHD_SWAP3 = 1554;
}
WIRE {
in_hblank [1];
in_preamble [1];
in_guard_lead [1];
in_packet [1];
in_guard_trail [1];
in_data_island [1];
pkt_clock [5];
// Shadow bit extraction outputs
shd_hdr_bit [1];
shd_sub_even [4];
shd_sub_odd [4];
}
REGISTER {
// Shadow registers (loaded with current packet data before each packet)
shd_header [32] = 32'd0;
shd_sp0_lo [32] = 32'd0;
shd_sp0_hi [32] = 32'd0;
shd_sp1_lo [32] = 32'd0;
shd_sp1_hi [32] = 32'd0;
shd_sp2_lo [32] = 32'd0;
shd_sp2_hi [32] = 32'd0;
shd_sp3_lo [32] = 32'd0;
shd_sp3_hi [32] = 32'd0;
// Audio sample packet (PKT2) - built at H_ACTIVE from sample buffer
p2_header [32] = 32'd0;
p2_sp0_lo [32] = 32'd0;
p2_sp0_hi [32] = 32'd0;
p2_sp1_lo [32] = 32'd0;
p2_sp1_hi [32] = 32'd0;
p2_sp2_lo [32] = 32'd0;
p2_sp2_hi [32] = 32'd0;
// Sample buffer (filled by samp_valid between H_ACTIVE events)
// Up to 3 samples per line (48000/22500 = 2.133 samples/line)
samp_buf0_lo [32] = 32'd0;
samp_buf0_hi [32] = 32'd0;
samp_buf1_lo [32] = 32'd0;
samp_buf1_hi [32] = 32'd0;
samp_buf2_lo [32] = 32'd0;
samp_buf2_hi [32] = 32'd0;
samp_count [2] = 2'd0;
}
ASYNCHRONOUS {
// Blanking region detection
in_hblank <= (display_enable == 1'b0) ? 1'b1 : 1'b0;
// Data island sub-regions
in_preamble <= (in_hblank == 1'b1 &&
x_pos >= lit(11, DI_PREAMBLE_START) && x_pos < lit(11, DI_GUARD_START)) ? 1'b1 : 1'b0;
in_guard_lead <= (in_hblank == 1'b1 &&
x_pos >= lit(11, DI_GUARD_START) && x_pos < lit(11, DI_PKT0_START)) ? 1'b1 : 1'b0;
in_packet <= (in_hblank == 1'b1 &&
x_pos >= lit(11, DI_PKT0_START) && x_pos < lit(11, DI_TRAIL_START)) ? 1'b1 : 1'b0;
in_guard_trail <= (in_hblank == 1'b1 &&
x_pos >= lit(11, DI_TRAIL_START) && x_pos < lit(11, DI_TRAIL_END)) ? 1'b1 : 1'b0;
in_data_island <= (in_guard_lead == 1'b1 || in_packet == 1'b1 || in_guard_trail == 1'b1) ? 1'b1 : 1'b0;
data_island_active <= in_data_island;
preamble_active <= in_preamble;
guard_active <= (in_guard_lead == 1'b1 || in_guard_trail == 1'b1) ? 1'b1 : 1'b0;
// Packet clock (0-31 within each packet)
// All 4 packets start at x[4:0]=19, so this formula works for all
pkt_clock <= x_pos[4:0] - 5'd19;
// ---------------------------------------------------------------
// Shadow header bit extraction (1 bit per clock, 32 bits total)
// ---------------------------------------------------------------
SELECT (pkt_clock) {
CASE (5'd0) { shd_hdr_bit <= shd_header[0]; }
CASE (5'd1) { shd_hdr_bit <= shd_header[1]; }
CASE (5'd2) { shd_hdr_bit <= shd_header[2]; }
CASE (5'd3) { shd_hdr_bit <= shd_header[3]; }
CASE (5'd4) { shd_hdr_bit <= shd_header[4]; }
CASE (5'd5) { shd_hdr_bit <= shd_header[5]; }
CASE (5'd6) { shd_hdr_bit <= shd_header[6]; }
CASE (5'd7) { shd_hdr_bit <= shd_header[7]; }
CASE (5'd8) { shd_hdr_bit <= shd_header[8]; }
CASE (5'd9) { shd_hdr_bit <= shd_header[9]; }
CASE (5'd10) { shd_hdr_bit <= shd_header[10]; }
CASE (5'd11) { shd_hdr_bit <= shd_header[11]; }
CASE (5'd12) { shd_hdr_bit <= shd_header[12]; }
CASE (5'd13) { shd_hdr_bit <= shd_header[13]; }
CASE (5'd14) { shd_hdr_bit <= shd_header[14]; }
CASE (5'd15) { shd_hdr_bit <= shd_header[15]; }
CASE (5'd16) { shd_hdr_bit <= shd_header[16]; }
CASE (5'd17) { shd_hdr_bit <= shd_header[17]; }
CASE (5'd18) { shd_hdr_bit <= shd_header[18]; }
CASE (5'd19) { shd_hdr_bit <= shd_header[19]; }
CASE (5'd20) { shd_hdr_bit <= shd_header[20]; }
CASE (5'd21) { shd_hdr_bit <= shd_header[21]; }
CASE (5'd22) { shd_hdr_bit <= shd_header[22]; }
CASE (5'd23) { shd_hdr_bit <= shd_header[23]; }
CASE (5'd24) { shd_hdr_bit <= shd_header[24]; }
CASE (5'd25) { shd_hdr_bit <= shd_header[25]; }
CASE (5'd26) { shd_hdr_bit <= shd_header[26]; }
CASE (5'd27) { shd_hdr_bit <= shd_header[27]; }
CASE (5'd28) { shd_hdr_bit <= shd_header[28]; }
CASE (5'd29) { shd_hdr_bit <= shd_header[29]; }
CASE (5'd30) { shd_hdr_bit <= shd_header[30]; }
CASE (5'd31) { shd_hdr_bit <= shd_header[31]; }
DEFAULT { shd_hdr_bit <= 1'b0; }
}
// ---------------------------------------------------------------
// Shadow subpacket bit extraction (interleaved across 4 subpackets)
// At clock T: ch1 = {sp3[2T], sp2[2T], sp1[2T], sp0[2T]}
// ch2 = {sp3[2T+1], sp2[2T+1], sp1[2T+1], sp0[2T+1]}
// T=0..15 uses sp_lo registers, T=16..31 uses sp_hi registers
// ---------------------------------------------------------------
SELECT (pkt_clock) {
CASE (5'd0) { shd_sub_even <= { shd_sp3_lo[0], shd_sp2_lo[0], shd_sp1_lo[0], shd_sp0_lo[0] };
shd_sub_odd <= { shd_sp3_lo[1], shd_sp2_lo[1], shd_sp1_lo[1], shd_sp0_lo[1] }; }
CASE (5'd1) { shd_sub_even <= { shd_sp3_lo[2], shd_sp2_lo[2], shd_sp1_lo[2], shd_sp0_lo[2] };
shd_sub_odd <= { shd_sp3_lo[3], shd_sp2_lo[3], shd_sp1_lo[3], shd_sp0_lo[3] }; }
CASE (5'd2) { shd_sub_even <= { shd_sp3_lo[4], shd_sp2_lo[4], shd_sp1_lo[4], shd_sp0_lo[4] };
shd_sub_odd <= { shd_sp3_lo[5], shd_sp2_lo[5], shd_sp1_lo[5], shd_sp0_lo[5] }; }
CASE (5'd3) { shd_sub_even <= { shd_sp3_lo[6], shd_sp2_lo[6], shd_sp1_lo[6], shd_sp0_lo[6] };
shd_sub_odd <= { shd_sp3_lo[7], shd_sp2_lo[7], shd_sp1_lo[7], shd_sp0_lo[7] }; }
CASE (5'd4) { shd_sub_even <= { shd_sp3_lo[8], shd_sp2_lo[8], shd_sp1_lo[8], shd_sp0_lo[8] };
shd_sub_odd <= { shd_sp3_lo[9], shd_sp2_lo[9], shd_sp1_lo[9], shd_sp0_lo[9] }; }
CASE (5'd5) { shd_sub_even <= { shd_sp3_lo[10], shd_sp2_lo[10], shd_sp1_lo[10], shd_sp0_lo[10] };
shd_sub_odd <= { shd_sp3_lo[11], shd_sp2_lo[11], shd_sp1_lo[11], shd_sp0_lo[11] }; }
CASE (5'd6) { shd_sub_even <= { shd_sp3_lo[12], shd_sp2_lo[12], shd_sp1_lo[12], shd_sp0_lo[12] };
shd_sub_odd <= { shd_sp3_lo[13], shd_sp2_lo[13], shd_sp1_lo[13], shd_sp0_lo[13] }; }
CASE (5'd7) { shd_sub_even <= { shd_sp3_lo[14], shd_sp2_lo[14], shd_sp1_lo[14], shd_sp0_lo[14] };
shd_sub_odd <= { shd_sp3_lo[15], shd_sp2_lo[15], shd_sp1_lo[15], shd_sp0_lo[15] }; }
CASE (5'd8) { shd_sub_even <= { shd_sp3_lo[16], shd_sp2_lo[16], shd_sp1_lo[16], shd_sp0_lo[16] };
shd_sub_odd <= { shd_sp3_lo[17], shd_sp2_lo[17], shd_sp1_lo[17], shd_sp0_lo[17] }; }
CASE (5'd9) { shd_sub_even <= { shd_sp3_lo[18], shd_sp2_lo[18], shd_sp1_lo[18], shd_sp0_lo[18] };
shd_sub_odd <= { shd_sp3_lo[19], shd_sp2_lo[19], shd_sp1_lo[19], shd_sp0_lo[19] }; }
CASE (5'd10) { shd_sub_even <= { shd_sp3_lo[20], shd_sp2_lo[20], shd_sp1_lo[20], shd_sp0_lo[20] };
shd_sub_odd <= { shd_sp3_lo[21], shd_sp2_lo[21], shd_sp1_lo[21], shd_sp0_lo[21] }; }
CASE (5'd11) { shd_sub_even <= { shd_sp3_lo[22], shd_sp2_lo[22], shd_sp1_lo[22], shd_sp0_lo[22] };
shd_sub_odd <= { shd_sp3_lo[23], shd_sp2_lo[23], shd_sp1_lo[23], shd_sp0_lo[23] }; }
CASE (5'd12) { shd_sub_even <= { shd_sp3_lo[24], shd_sp2_lo[24], shd_sp1_lo[24], shd_sp0_lo[24] };
shd_sub_odd <= { shd_sp3_lo[25], shd_sp2_lo[25], shd_sp1_lo[25], shd_sp0_lo[25] }; }
CASE (5'd13) { shd_sub_even <= { shd_sp3_lo[26], shd_sp2_lo[26], shd_sp1_lo[26], shd_sp0_lo[26] };
shd_sub_odd <= { shd_sp3_lo[27], shd_sp2_lo[27], shd_sp1_lo[27], shd_sp0_lo[27] }; }
CASE (5'd14) { shd_sub_even <= { shd_sp3_lo[28], shd_sp2_lo[28], shd_sp1_lo[28], shd_sp0_lo[28] };
shd_sub_odd <= { shd_sp3_lo[29], shd_sp2_lo[29], shd_sp1_lo[29], shd_sp0_lo[29] }; }
CASE (5'd15) { shd_sub_even <= { shd_sp3_lo[30], shd_sp2_lo[30], shd_sp1_lo[30], shd_sp0_lo[30] };
shd_sub_odd <= { shd_sp3_lo[31], shd_sp2_lo[31], shd_sp1_lo[31], shd_sp0_lo[31] }; }
CASE (5'd16) { shd_sub_even <= { shd_sp3_hi[0], shd_sp2_hi[0], shd_sp1_hi[0], shd_sp0_hi[0] };
shd_sub_odd <= { shd_sp3_hi[1], shd_sp2_hi[1], shd_sp1_hi[1], shd_sp0_hi[1] }; }
CASE (5'd17) { shd_sub_even <= { shd_sp3_hi[2], shd_sp2_hi[2], shd_sp1_hi[2], shd_sp0_hi[2] };
shd_sub_odd <= { shd_sp3_hi[3], shd_sp2_hi[3], shd_sp1_hi[3], shd_sp0_hi[3] }; }
CASE (5'd18) { shd_sub_even <= { shd_sp3_hi[4], shd_sp2_hi[4], shd_sp1_hi[4], shd_sp0_hi[4] };
shd_sub_odd <= { shd_sp3_hi[5], shd_sp2_hi[5], shd_sp1_hi[5], shd_sp0_hi[5] }; }
CASE (5'd19) { shd_sub_even <= { shd_sp3_hi[6], shd_sp2_hi[6], shd_sp1_hi[6], shd_sp0_hi[6] };
shd_sub_odd <= { shd_sp3_hi[7], shd_sp2_hi[7], shd_sp1_hi[7], shd_sp0_hi[7] }; }
CASE (5'd20) { shd_sub_even <= { shd_sp3_hi[8], shd_sp2_hi[8], shd_sp1_hi[8], shd_sp0_hi[8] };
shd_sub_odd <= { shd_sp3_hi[9], shd_sp2_hi[9], shd_sp1_hi[9], shd_sp0_hi[9] }; }
CASE (5'd21) { shd_sub_even <= { shd_sp3_hi[10], shd_sp2_hi[10], shd_sp1_hi[10], shd_sp0_hi[10] };
shd_sub_odd <= { shd_sp3_hi[11], shd_sp2_hi[11], shd_sp1_hi[11], shd_sp0_hi[11] }; }
CASE (5'd22) { shd_sub_even <= { shd_sp3_hi[12], shd_sp2_hi[12], shd_sp1_hi[12], shd_sp0_hi[12] };
shd_sub_odd <= { shd_sp3_hi[13], shd_sp2_hi[13], shd_sp1_hi[13], shd_sp0_hi[13] }; }
CASE (5'd23) { shd_sub_even <= { shd_sp3_hi[14], shd_sp2_hi[14], shd_sp1_hi[14], shd_sp0_hi[14] };
shd_sub_odd <= { shd_sp3_hi[15], shd_sp2_hi[15], shd_sp1_hi[15], shd_sp0_hi[15] }; }
CASE (5'd24) { shd_sub_even <= { shd_sp3_hi[16], shd_sp2_hi[16], shd_sp1_hi[16], shd_sp0_hi[16] };
shd_sub_odd <= { shd_sp3_hi[17], shd_sp2_hi[17], shd_sp1_hi[17], shd_sp0_hi[17] }; }
CASE (5'd25) { shd_sub_even <= { shd_sp3_hi[18], shd_sp2_hi[18], shd_sp1_hi[18], shd_sp0_hi[18] };
shd_sub_odd <= { shd_sp3_hi[19], shd_sp2_hi[19], shd_sp1_hi[19], shd_sp0_hi[19] }; }
CASE (5'd26) { shd_sub_even <= { shd_sp3_hi[20], shd_sp2_hi[20], shd_sp1_hi[20], shd_sp0_hi[20] };
shd_sub_odd <= { shd_sp3_hi[21], shd_sp2_hi[21], shd_sp1_hi[21], shd_sp0_hi[21] }; }
CASE (5'd27) { shd_sub_even <= { shd_sp3_hi[22], shd_sp2_hi[22], shd_sp1_hi[22], shd_sp0_hi[22] };
shd_sub_odd <= { shd_sp3_hi[23], shd_sp2_hi[23], shd_sp1_hi[23], shd_sp0_hi[23] }; }
CASE (5'd28) { shd_sub_even <= { shd_sp3_hi[24], shd_sp2_hi[24], shd_sp1_hi[24], shd_sp0_hi[24] };
shd_sub_odd <= { shd_sp3_hi[25], shd_sp2_hi[25], shd_sp1_hi[25], shd_sp0_hi[25] }; }
CASE (5'd29) { shd_sub_even <= { shd_sp3_hi[26], shd_sp2_hi[26], shd_sp1_hi[26], shd_sp0_hi[26] };
shd_sub_odd <= { shd_sp3_hi[27], shd_sp2_hi[27], shd_sp1_hi[27], shd_sp0_hi[27] }; }
CASE (5'd30) { shd_sub_even <= { shd_sp3_hi[28], shd_sp2_hi[28], shd_sp1_hi[28], shd_sp0_hi[28] };
shd_sub_odd <= { shd_sp3_hi[29], shd_sp2_hi[29], shd_sp1_hi[29], shd_sp0_hi[29] }; }
CASE (5'd31) { shd_sub_even <= { shd_sp3_hi[30], shd_sp2_hi[30], shd_sp1_hi[30], shd_sp0_hi[30] };
shd_sub_odd <= { shd_sp3_hi[31], shd_sp2_hi[31], shd_sp1_hi[31], shd_sp0_hi[31] }; }
DEFAULT { shd_sub_even <= 4'b0000; shd_sub_odd <= 4'b0000; }
}
// TERC4 channel outputs
IF (in_guard_lead == 1'b1 || in_guard_trail == 1'b1) {
terc4_ch0 <= { 2'b11, vsync, hsync };
terc4_ch1 <= 4'b0000;
terc4_ch2 <= 4'b0000;
} ELIF (in_packet == 1'b1) {
terc4_ch0 <= { 1'b1, shd_hdr_bit, vsync, hsync };
terc4_ch1 <= shd_sub_even;
terc4_ch2 <= shd_sub_odd;
} ELSE {
terc4_ch0 <= { 2'b11, vsync, hsync };
terc4_ch1 <= 4'b0000;
terc4_ch2 <= 4'b0000;
}
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
// ---- Packet loading and shadow management ----
// At H_ACTIVE: build audio sample packet from buffer, load shadow with PKT0
// At swap points: load shadow with next packet's data
// Sample buffering runs in all non-H_ACTIVE branches
IF (x_pos == lit(11, H_ACTIVE)) {
// Build audio sample packet (PKT2) from accumulated samples
IF (samp_count == 2'd3) {
// 3 samples collected
p2_header <= 32'h4D000702;
p2_sp0_lo <= samp_buf0_lo;
p2_sp0_hi <= samp_buf0_hi;
p2_sp1_lo <= samp_buf1_lo;
p2_sp1_hi <= samp_buf1_hi;
p2_sp2_lo <= samp_buf2_lo;
p2_sp2_hi <= samp_buf2_hi;
} ELIF (samp_count == 2'd2) {
// 2 samples collected
p2_header <= 32'h80000302;
p2_sp0_lo <= samp_buf0_lo;
p2_sp0_hi <= samp_buf0_hi;
p2_sp1_lo <= samp_buf1_lo;
p2_sp1_hi <= samp_buf1_hi;
p2_sp2_lo <= 32'd0;
p2_sp2_hi <= 32'd0;
} ELIF (samp_count == 2'd1) {
// 1 sample collected (first line after reset)
p2_header <= 32'h65000102;
p2_sp0_lo <= samp_buf0_lo;
p2_sp0_hi <= samp_buf0_hi;
p2_sp1_lo <= 32'd0;
p2_sp1_hi <= 32'd0;
p2_sp2_lo <= 32'd0;
p2_sp2_hi <= 32'd0;
} ELSE {
// No samples - send silence
p2_header <= 32'h65000102;
p2_sp0_lo <= 32'd0;
p2_sp0_hi <= 32'd0;
p2_sp1_lo <= 32'd0;
p2_sp1_hi <= 32'd0;
p2_sp2_lo <= 32'd0;
p2_sp2_hi <= 32'd0;
}
// Reset sample buffer for next line, capturing if samp_valid fires now
IF (samp_valid == 1'b1) {
samp_buf0_lo <= samp_lo;
samp_buf0_hi <= samp_hi;
samp_count <= 2'd1;
} ELSE {
samp_count <= 2'd0;
}
// Load shadow with PKT0: AVI InfoFrame
// Header: {ECC=0xE4, Len=0x0D, Ver=0x02, Type=0x82}
shd_header <= 32'hE40D0282;
// SP0: {PB3=0, PB2=0, PB1=0, PB0=checksum=0x6F}
shd_sp0_lo <= 32'h0000006F;
shd_sp0_hi <= 32'h5F000000;
shd_sp1_lo <= 32'd0;
shd_sp1_hi <= 32'd0;
shd_sp2_lo <= 32'd0;
shd_sp2_hi <= 32'd0;
shd_sp3_lo <= 32'd0;
shd_sp3_hi <= 32'd0;
} ELIF (x_pos == lit(11, DI_SHD_SWAP1)) {
// Shadow <- PKT1: ACR (N=6144, CTS=37125=0x009105)
// Header: {ECC=0x4A, 0x00, 0x00, Type=0x01}
shd_header <= 32'h4A000001;
// SP0: {CTS[7:0]=0x05, CTS[15:8]=0x91, CTS[19:16]=0x00, 0x00}
shd_sp0_lo <= 32'h05910000;
// SP0 hi: {ECC=0x16, N[7:0]=0x00, N[15:8]=0x18, N[19:16]=0x00}
shd_sp0_hi <= 32'h16001800;
shd_sp1_lo <= 32'd0;
shd_sp1_hi <= 32'd0;
shd_sp2_lo <= 32'd0;
shd_sp2_hi <= 32'd0;
shd_sp3_lo <= 32'd0;
shd_sp3_hi <= 32'd0;
// Sample buffering (samp_valid may fire this cycle)
IF (samp_valid == 1'b1) {
IF (samp_count == 2'd0) {
samp_buf0_lo <= samp_lo;
samp_buf0_hi <= samp_hi;
samp_count <= 2'd1;
} ELIF (samp_count == 2'd1) {
samp_buf1_lo <= samp_lo;
samp_buf1_hi <= samp_hi;
samp_count <= 2'd2;
} ELIF (samp_count == 2'd2) {
samp_buf2_lo <= samp_lo;
samp_buf2_hi <= samp_hi;
samp_count <= 2'd3;
}
}
} ELIF (x_pos == lit(11, DI_SHD_SWAP2)) {
// Shadow <- PKT2: Audio Sample (from pre-built registers)
shd_header <= p2_header;
shd_sp0_lo <= p2_sp0_lo;
shd_sp0_hi <= p2_sp0_hi;
shd_sp1_lo <= p2_sp1_lo;
shd_sp1_hi <= p2_sp1_hi;
shd_sp2_lo <= p2_sp2_lo;
shd_sp2_hi <= p2_sp2_hi;
shd_sp3_lo <= 32'd0;
shd_sp3_hi <= 32'd0;
// Sample buffering
IF (samp_valid == 1'b1) {
IF (samp_count == 2'd0) {
samp_buf0_lo <= samp_lo;
samp_buf0_hi <= samp_hi;
samp_count <= 2'd1;
} ELIF (samp_count == 2'd1) {
samp_buf1_lo <= samp_lo;
samp_buf1_hi <= samp_hi;
samp_count <= 2'd2;
}
}
} ELIF (x_pos == lit(11, DI_SHD_SWAP3)) {
// Shadow <- PKT3: Audio InfoFrame (2ch L-PCM 48kHz 16-bit)
// Header: {ECC=0x4A, Len=0x0A, Ver=0x01, Type=0x84}
shd_header <= 32'h4A0A0184;
// SP0: {PB3=0, PB2=0x0D(48kHz/16bit), PB1=0x11(PCM/2ch), PB0=chksum=0x53}
shd_sp0_lo <= 32'h000D1153;
shd_sp0_hi <= 32'hB9000000;
shd_sp1_lo <= 32'd0;
shd_sp1_hi <= 32'd0;
shd_sp2_lo <= 32'd0;
shd_sp2_hi <= 32'd0;
shd_sp3_lo <= 32'd0;
shd_sp3_hi <= 32'd0;
// Sample buffering
IF (samp_valid == 1'b1) {
IF (samp_count == 2'd0) {
samp_buf0_lo <= samp_lo;
samp_buf0_hi <= samp_hi;
samp_count <= 2'd1;
} ELIF (samp_count == 2'd1) {
samp_buf1_lo <= samp_lo;
samp_buf1_hi <= samp_hi;
samp_count <= 2'd2;
}
}
} ELSE {
// Default cycle: sample buffering only
IF (samp_valid == 1'b1) {
IF (samp_count == 2'd0) {
samp_buf0_lo <= samp_lo;
samp_buf0_hi <= samp_hi;
samp_count <= 2'd1;
} ELIF (samp_count == 2'd1) {
samp_buf1_lo <= samp_lo;
samp_buf1_hi <= samp_hi;
samp_count <= 2'd2;
} ELIF (samp_count == 2'd2) {
samp_buf2_lo <= samp_lo;
samp_buf2_hi <= samp_hi;
samp_count <= 2'd3;
}
}
}
}
@endmodjz
// TERC4 (Transition-minimized Error Reduction Coding, 4-bit)
// DVI/HDMI 1.4 spec encoding for data island periods.
// 4-bit input -> 10-bit TERC4 output, purely combinational.
@module terc4_encoder
PORT {
IN [4] data_in;
OUT [10] terc4_out;
}
WIRE {
result [10];
}
ASYNCHRONOUS {
SELECT (data_in) {
CASE (4'b0000) { result <= 10'b1010011100; }
CASE (4'b0001) { result <= 10'b1001100011; }
CASE (4'b0010) { result <= 10'b1011100100; }
CASE (4'b0011) { result <= 10'b1011100010; }
CASE (4'b0100) { result <= 10'b0101110001; }
CASE (4'b0101) { result <= 10'b0100011110; }
CASE (4'b0110) { result <= 10'b0110001110; }
CASE (4'b0111) { result <= 10'b0100111100; }
CASE (4'b1000) { result <= 10'b1011001100; }
CASE (4'b1001) { result <= 10'b0100111001; }
CASE (4'b1010) { result <= 10'b0110011100; }
CASE (4'b1011) { result <= 10'b1011000110; }
CASE (4'b1100) { result <= 10'b1010001110; }
CASE (4'b1101) { result <= 10'b1001110001; }
CASE (4'b1110) { result <= 10'b0101100011; }
CASE (4'b1111) { result <= 10'b1011000011; }
DEFAULT { result <= 10'b1010011100; }
}
terc4_out <= result;
}
@endmodjz
// Tone Generator
// Reads melody data from BRAM, sequences notes, runs a Bresenham 48kHz
// sample clock, and outputs raw signed 16-bit PCM samples.
// Square wave tone with per-note articulation gap and volume.
// Each instance reads its own track binary file (up to 500 notes).
@module tone_gen
CONST {
DATA_FILE = "../out/track0.bin";
}
PORT {
IN [1] clk;
IN [1] rst_n;
IN [1] next_song; // single-cycle pulse to switch to next song
OUT [16] sample; // raw signed 16-bit PCM
OUT [1] samp_valid; // pulses high for 1 cycle when a new sample is ready
OUT [16] half_period_out; // current note half-period (frequency indicator)
OUT [8] volume_out; // current note volume (amplitude envelope)
OUT [1] in_gap_out; // articulation gap flag
}
CONST {
// Bresenham audio sample rate: 48000/37125000 = 16/12375
BRES_STEP = 16;
BRES_THRESH = 12375;
BRES_FIRE_MIN = 12359;
}
WIRE {
// Bresenham pre-computation
bres_fire [1];
bres_next [14];
// Melody/tone wires
mel_addr [11];
mel_half_period [16];
mel_duration [24];
mel_gap [16];
mel_volume [8];
at_song_end [1];
cur_half_period [16];
cur_note_dur [24];
cur_gap [16];
cur_volume [8];
in_gap [1];
// Current audio sample value
cur_sample [16];
// 2-tap moving average filter
filt_sum [17]; // sadd sign-extended sum
filt_out [16]; // averaged output
}
REGISTER {
// Bresenham audio sample clock accumulator
bres_acc [14] = 14'd0;
// Melody sequencer and tone generator
tone_phase [16] = 16'd0;
tone_polarity [1] = 1'b0;
note_idx [10] = 10'd0;
song_base [11] = 11'd0; // 0=song 1, 500=song 2, 1000=song 3
note_dur_cnt [24] = 24'd0;
// Sample valid pulse
samp_valid_r [1] = 1'b0;
// Previous sample for 2-tap moving average filter
prev_sample [16] = 16'd0;
}
@new mel0 melodies {
OVERRIDE {
DATA_FILE = DATA_FILE;
}
IN [1] clk = clk;
IN [1] rst_n = rst_n;
IN [11] addr = mel_addr;
OUT [16] half_period = mel_half_period;
OUT [24] duration = mel_duration;
OUT [16] gap = mel_gap;
OUT [8] volume = mel_volume;
}
ASYNCHRONOUS {
// ---- Bresenham pre-computation ----
bres_fire <= (bres_acc >= lit(14, BRES_FIRE_MIN)) ? 1'b1 : 1'b0;
IF (bres_acc >= lit(14, BRES_FIRE_MIN)) {
bres_next <= bres_acc - lit(14, BRES_FIRE_MIN);
} ELSE {
bres_next <= bres_acc + lit(14, BRES_STEP);
}
// ---- Melody BRAM address ----
mel_addr <= song_base + { 1'b0, note_idx };
// Drive current note parameters from BRAM output
cur_half_period <= mel_half_period;
cur_note_dur <= mel_duration;
cur_gap <= mel_gap;
cur_volume <= mel_volume;
// Sentinel detection: half_period = 0xFFFF marks end of song
at_song_end <= (mel_half_period == 16'hFFFF) ? 1'b1 : 1'b0;
// Gap: silence during last N samples of each note for articulation
in_gap <= (note_dur_cnt >= cur_note_dur - { 8'h00, cur_gap }) ? 1'b1 : 1'b0;
// ---- Current audio sample value (raw signed 16-bit PCM) ----
// Volume is unsigned 8-bit, scaled up 8x (shift left 3) so the
// 4-channel mix uses ~15% of the 16-bit range (max sum ~4800).
IF (in_gap == 1'b1) {
cur_sample <= 16'h0000;
} ELIF (tone_polarity == 1'b0) {
cur_sample <= { 5'h00, cur_volume, 3'h0 };
} ELSE {
cur_sample <= 16'h0000 - { 5'h00, cur_volume, 3'h0 };
}
// 2-tap moving average anti-alias filter: (cur + prev) / 2
filt_sum <= sadd(cur_sample, prev_sample);
filt_out <= filt_sum[16:1];
// Drive output ports
sample <= filt_out;
samp_valid <= samp_valid_r;
half_period_out <= cur_half_period;
volume_out <= cur_volume;
in_gap_out <= in_gap;
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
// ---- Bresenham accumulator (runs every cycle) ----
bres_acc <= bres_next;
// ---- Sample valid pulse: high for 1 cycle when Bresenham fires ----
samp_valid_r <= bres_fire;
// ---- Capture previous sample for moving average filter ----
IF (bres_fire == 1'b1) {
prev_sample <= cur_sample;
}
// ---- Song switch: cycle song_base on next_song pulse ----
IF (next_song == 1'b1) {
IF (song_base == 11'd0) {
song_base <= 11'd500;
} ELIF (song_base == 11'd500) {
song_base <= 11'd1000;
} ELSE {
song_base <= 11'd0;
}
note_idx <= 10'd0;
note_dur_cnt <= 24'd0;
tone_phase <= 16'd0;
tone_polarity <= 1'b0;
} ELSE {
// ---- Audio state advance ----
// Song-end sentinel: wrap back to start immediately
IF (at_song_end == 1'b1) {
note_idx <= 10'd0;
note_dur_cnt <= 24'd0;
tone_phase <= 16'd0;
tone_polarity <= 1'b0;
} ELIF (bres_fire == 1'b1) {
// Note boundary: advance to next note and reset tone
IF (note_dur_cnt + 24'd1 >= cur_note_dur) {
note_dur_cnt <= 24'd0;
tone_phase <= 16'd0;
tone_polarity <= 1'b0;
note_idx <= note_idx + 10'd1;
} ELSE {
note_dur_cnt <= note_dur_cnt + 24'd1;
// Advance tone: toggle polarity at half-period boundary
IF (tone_phase + 16'd1 >= cur_half_period) {
tone_phase <= 16'd0;
tone_polarity <= ~tone_polarity;
} ELSE {
tone_phase <= tone_phase + 16'd1;
}
}
}
}
}
@endmodjz
// Melody BRAM wrapper
// Stores 64-bit note entries as pairs of 32-bit words in BRAM.
// Entry format: {half_period[63:48], duration[47:24], gap[23:8], volume[7:0]}
// Even address (note*2): {half_period[15:0], duration[23:8]}
// Odd address (note*2+1): {duration[7:0], gap[15:0], volume[7:0]}
// Sentinel: half_period = 0xFFFF marks end of song
// 500 notes per track, one binary file per track.
//
// Latency: 3 cycles (phase toggle reads 2 BRAM words). Negligible for audio.
@module melodies
CONST {
DATA_FILE = "../out/track0.bin";
}
PORT {
IN [1] clk;
IN [1] rst_n;
IN [11] addr;
OUT [16] half_period;
OUT [24] duration;
OUT [16] gap;
OUT [8] volume;
}
MEM(TYPE=BLOCK) {
mel [32] [3072] = @file(DATA_FILE) {
OUT read SYNC;
};
}
WIRE {
read_addr [12];
}
REGISTER {
phase [1] = 1'b0;
word_hi [32] = 32'd0;
word_lo [32] = 32'd0;
}
ASYNCHRONOUS {
// Note address doubled, phase selects hi/lo word
read_addr <= { addr, phase };
// Extract fields from captured word pair
// Hi word: {half_period[15:0], duration[23:8]}
// Lo word: {duration[7:0], gap[15:0], volume[7:0]}
half_period <= word_hi[31:16];
duration <= { word_hi[15:0], word_lo[31:24] };
gap <= word_lo[23:8];
volume <= word_lo[7:0];
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
mel.read.addr <= read_addr;
phase <= ~phase;
// Capture BRAM data: hi word arrives when phase=1, lo word when phase=0
IF (phase == 1'b1) {
word_hi <= mel.read.data;
} ELSE {
word_lo <= mel.read.data;
}
}
@endmodjz
// 4-Channel Audio Mixer + DVI Subpacket Formatter
// Sums 4 signed 16-bit PCM channels and formats as DVI audio subpacket words.
// Includes BCH(64,56) ECC computation (polynomial 0x83, LSB-first).
// Purely combinational (ASYNC only).
@module mixer
PORT {
IN [16] s0; // channel 0 PCM sample
IN [16] s1; // channel 1 PCM sample
IN [16] s2; // channel 2 PCM sample
IN [16] s3; // channel 3 PCM sample
IN [1] samp_valid; // sample valid pulse
OUT [32] samp_lo; // DVI subpacket low word
OUT [32] samp_hi; // DVI subpacket high word
OUT [1] out_valid; // pass-through valid
}
WIRE {
sum01 [17]; // sadd(s0, s1) — 17-bit signed
sum23 [17]; // sadd(s2, s3) — 17-bit signed
sum_all [18]; // sadd(sum01, sum23) — 18-bit signed
mix [16]; // final 16-bit signed PCM
samp_24 [24]; // left-justified to 24 bits
// Parity computation (XOR of all 24 bits, folded in stages)
p8 [8]; // byte0 ^ byte1 ^ byte2
p4 [4]; // fold 8 to 4
p2 [2]; // fold 4 to 2
parity [1]; // final even parity bit
sb6 [8]; // status byte 6
// Subpacket data (internal wires, before port assignment)
lo [32]; // samp_lo value
hi_data [24]; // upper 24 bits: {sb6, R[23:16], R[15:8]}
// BCH ECC
ecc [8]; // BCH(64,56) ECC result
}
ASYNCHRONOUS {
// Sum 4 channels using sadd for proper sign-extended widening
sum01 <= sadd(s0, s1);
sum23 <= sadd(s2, s3);
sum_all <= sadd(sum01, sum23);
mix <= sum_all[15:0];
// Left-justify 16-bit sample to 24 bits
samp_24 <= { mix, 8'h00 };
// Even parity: XOR all 24 bits by folding bytes then nibbles
p8 <= samp_24[7:0] ^ samp_24[15:8] ^ samp_24[23:16];
p4 <= p8[3:0] ^ p8[7:4];
p2 <= p4[1:0] ^ p4[3:2];
parity <= p2[0] ^ p2[1];
// Status byte 6: {P_L, 000, P_R, 000} -- parity for both L and R
sb6 <= { parity, 3'b000, parity, 3'b000 };
// DVI subpacket format (L=R mono):
// lo = { R[7:0], L[23:16], L[15:8], L[7:0] }
lo <= { samp_24[7:0], samp_24[23:16], samp_24[15:8], samp_24[7:0] };
// Upper 24 data bits for ECC: {sb6, R[23:16], R[15:8]}
hi_data <= { sb6, samp_24[23:16], samp_24[15:8] };
// BCH(64,56) ECC: polynomial 0x83, LSB-first, right-shift LFSR
// data[31:0] = lo, data[55:32] = hi_data
ecc[0] <= lo[0] ^ lo[1] ^ lo[3] ^ lo[4] ^ lo[5] ^ lo[6] ^ lo[11] ^ lo[12] ^ lo[14] ^ lo[17] ^ lo[21] ^ lo[22] ^ lo[23] ^ lo[24] ^ lo[25] ^ lo[27] ^ lo[29] ^ lo[30] ^ lo[31] ^ hi_data[2] ^ hi_data[5] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[10] ^ hi_data[11] ^ hi_data[12] ^ hi_data[15] ^ hi_data[16] ^ hi_data[17] ^ hi_data[18] ^ hi_data[20] ^ hi_data[21] ^ hi_data[23];
ecc[1] <= lo[0] ^ lo[2] ^ lo[3] ^ lo[7] ^ lo[11] ^ lo[13] ^ lo[14] ^ lo[15] ^ lo[17] ^ lo[18] ^ lo[21] ^ lo[26] ^ lo[27] ^ lo[28] ^ lo[29] ^ hi_data[0] ^ hi_data[2] ^ hi_data[3] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[13] ^ hi_data[15] ^ hi_data[19] ^ hi_data[20] ^ hi_data[22] ^ hi_data[23];
ecc[2] <= lo[0] ^ lo[5] ^ lo[6] ^ lo[8] ^ lo[11] ^ lo[15] ^ lo[16] ^ lo[17] ^ lo[18] ^ lo[19] ^ lo[21] ^ lo[23] ^ lo[24] ^ lo[25] ^ lo[28] ^ lo[31] ^ hi_data[1] ^ hi_data[2] ^ hi_data[3] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[9] ^ hi_data[10] ^ hi_data[11] ^ hi_data[12] ^ hi_data[14] ^ hi_data[15] ^ hi_data[17] ^ hi_data[18];
ecc[3] <= lo[0] ^ lo[1] ^ lo[6] ^ lo[7] ^ lo[9] ^ lo[12] ^ lo[16] ^ lo[17] ^ lo[18] ^ lo[19] ^ lo[20] ^ lo[22] ^ lo[24] ^ lo[25] ^ lo[26] ^ lo[29] ^ hi_data[0] ^ hi_data[2] ^ hi_data[3] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[10] ^ hi_data[11] ^ hi_data[12] ^ hi_data[13] ^ hi_data[15] ^ hi_data[16] ^ hi_data[18] ^ hi_data[19];
ecc[4] <= lo[0] ^ lo[1] ^ lo[2] ^ lo[7] ^ lo[8] ^ lo[10] ^ lo[13] ^ lo[17] ^ lo[18] ^ lo[19] ^ lo[20] ^ lo[21] ^ lo[23] ^ lo[25] ^ lo[26] ^ lo[27] ^ lo[30] ^ hi_data[1] ^ hi_data[3] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[11] ^ hi_data[12] ^ hi_data[13] ^ hi_data[14] ^ hi_data[16] ^ hi_data[17] ^ hi_data[19] ^ hi_data[20];
ecc[5] <= lo[0] ^ lo[1] ^ lo[2] ^ lo[3] ^ lo[8] ^ lo[9] ^ lo[11] ^ lo[14] ^ lo[18] ^ lo[19] ^ lo[20] ^ lo[21] ^ lo[22] ^ lo[24] ^ lo[26] ^ lo[27] ^ lo[28] ^ lo[31] ^ hi_data[2] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[12] ^ hi_data[13] ^ hi_data[14] ^ hi_data[15] ^ hi_data[17] ^ hi_data[18] ^ hi_data[20] ^ hi_data[21];
ecc[6] <= lo[1] ^ lo[2] ^ lo[3] ^ lo[4] ^ lo[9] ^ lo[10] ^ lo[12] ^ lo[15] ^ lo[19] ^ lo[20] ^ lo[21] ^ lo[22] ^ lo[23] ^ lo[25] ^ lo[27] ^ lo[28] ^ lo[29] ^ hi_data[0] ^ hi_data[3] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[10] ^ hi_data[13] ^ hi_data[14] ^ hi_data[15] ^ hi_data[16] ^ hi_data[18] ^ hi_data[19] ^ hi_data[21] ^ hi_data[22];
ecc[7] <= lo[0] ^ lo[2] ^ lo[3] ^ lo[4] ^ lo[5] ^ lo[10] ^ lo[11] ^ lo[13] ^ lo[16] ^ lo[20] ^ lo[21] ^ lo[22] ^ lo[23] ^ lo[24] ^ lo[26] ^ lo[28] ^ lo[29] ^ lo[30] ^ hi_data[1] ^ hi_data[4] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[10] ^ hi_data[11] ^ hi_data[14] ^ hi_data[15] ^ hi_data[16] ^ hi_data[17] ^ hi_data[19] ^ hi_data[20] ^ hi_data[22] ^ hi_data[23];
// Drive output ports
samp_lo <= lo;
samp_hi <= { ecc, hi_data };
out_valid <= samp_valid;
}
@endmodjz
// 80-Bar Spectrum Analyzer
// Maps 4 audio channels' frequencies to 80 log-spaced bins (65 Hz - 5 kHz),
// adds simulated odd harmonics (3rd, 5th) with spectral spread for visual fullness.
// Smooths amplitudes per frame, stores in DISTRIBUTED RAM.
// Exposes combinational read port for the display module.
@module spectrum_analyzer
PORT {
IN [1] clk;
IN [1] rst_n;
IN [16] ch0_half_period; IN [8] ch0_volume; IN [1] ch0_in_gap;
IN [16] ch1_half_period; IN [8] ch1_volume; IN [1] ch1_in_gap;
IN [16] ch2_half_period; IN [8] ch2_volume; IN [1] ch2_in_gap;
IN [16] ch3_half_period; IN [8] ch3_volume; IN [1] ch3_in_gap;
IN [1] frame_pulse;
IN [7] rd_bar;
OUT [16] rd_amp;
}
WIRE {
// Per-channel bar indices (combinational from half_period)
ch0_fund [7]; ch0_h3 [7]; ch0_h5 [7];
ch1_fund [7]; ch1_h3 [7]; ch1_h5 [7];
ch2_fund [7]; ch2_h3 [7]; ch2_h5 [7];
ch3_fund [7]; ch3_h3 [7]; ch3_h5 [7];
// Per-channel effective volume (0 during gap)
ch0_vol_eff [8]; ch1_vol_eff [8];
ch2_vol_eff [8]; ch3_vol_eff [8];
// Signed distance from sweep_idx to each source bar
d0f [8]; d0h3 [8]; d0h5 [8];
d1f [8]; d1h3 [8]; d1h5 [8];
d2f [8]; d2h3 [8]; d2h5 [8];
d3f [8]; d3h3 [8]; d3h5 [8];
// Absolute distances
a0f [7]; a0h3 [7]; a0h5 [7];
a1f [7]; a1h3 [7]; a1h5 [7];
a2f [7]; a2h3 [7]; a2h5 [7];
a3f [7]; a3h3 [7]; a3h5 [7];
// 12 separate contribution values (no accumulation)
c0f [8]; c0h3 [8]; c0h5 [8];
c1f [8]; c1h3 [8]; c1h5 [8];
c2f [8]; c2h3 [8]; c2h5 [8];
c3f [8]; c3h3 [8]; c3h5 [8];
// Sum tree
sum_01f [9]; sum_23f [9];
sum_01h3 [9]; sum_23h3 [9];
sum_01h5 [9]; sum_23h5 [9];
sum_fund [10]; sum_h3 [10]; sum_h5 [10];
target_acc [11];
target_amp [8];
// Smoothing
smooth_cur [8];
smooth_delta [9];
smooth_step [9];
smooth_next [9];
smooth_result [8];
// Peak (from sweep_ram: [15:12]=peak_top4, [11:8]=timer)
peak_approx [8]; // reconstructed: {sweep[15:12], 4'b0}
timer_cur [4];
// Pre-computed write values for MEM (single write point)
new_peak_amp [8];
new_peak_timer [4];
display_wr_val [16];
sweep_wr_val [16];
}
REGISTER {
// Latched channel state (updated each frame)
lat_vol0 [8] = 8'd0; lat_vol1 [8] = 8'd0;
lat_vol2 [8] = 8'd0; lat_vol3 [8] = 8'd0;
lat_fund0 [7] = 7'd127; lat_fund1 [7] = 7'd127;
lat_fund2 [7] = 7'd127; lat_fund3 [7] = 7'd127;
lat_h3_0 [7] = 7'd127; lat_h3_1 [7] = 7'd127;
lat_h3_2 [7] = 7'd127; lat_h3_3 [7] = 7'd127;
lat_h5_0 [7] = 7'd127; lat_h5_1 [7] = 7'd127;
lat_h5_2 [7] = 7'd127; lat_h5_3 [7] = 7'd127;
// Sweep state machine
sweep_idx [7] = 7'd0;
sweeping [1] = 1'b0;
}
MEM(TYPE=DISTRIBUTED) {
// Display: [7:0]=smooth_amp, [15:8]=peak_amp — read by display module
display_ram [16] [128] = 16'd0 { OUT rd ASYNC; IN wr; };
// Sweep: [7:0]=smooth, [11:8]=timer, [15:12]=peak_top4 — read by sweep
sweep_ram [16] [128] = 16'd0 { OUT rd ASYNC; IN wr; };
}
ASYNCHRONOUS {
// ---- Read port for display ----
rd_amp <= display_ram.rd[rd_bar];
// ---- Effective volume ----
ch0_vol_eff <= (ch0_in_gap == 1'b0) ? ch0_volume : 8'd0;
ch1_vol_eff <= (ch1_in_gap == 1'b0) ? ch1_volume : 8'd0;
ch2_vol_eff <= (ch2_in_gap == 1'b0) ? ch2_volume : 8'd0;
ch3_vol_eff <= (ch3_in_gap == 1'b0) ? ch3_volume : 8'd0;
// ---- Half-period to bar index mapping (ch0) ----
IF (ch0_half_period == 16'd0 || ch0_half_period == 16'hFFFF) {
ch0_fund <= 7'd127; ch0_h3 <= 7'd127; ch0_h5 <= 7'd127;
} ELIF (ch0_half_period > 16'd231) {
ch0_fund <= 7'd7; ch0_h3 <= 7'd27; ch0_h5 <= 7'd42;
} ELIF (ch0_half_period > 16'd206) {
ch0_fund <= 7'd9; ch0_h3 <= 7'd29; ch0_h5 <= 7'd44;
} ELIF (ch0_half_period > 16'd189) {
ch0_fund <= 7'd11; ch0_h3 <= 7'd31; ch0_h5 <= 7'd47;
} ELIF (ch0_half_period > 16'd174) {
ch0_fund <= 7'd12; ch0_h3 <= 7'd32; ch0_h5 <= 7'd48;
} ELIF (ch0_half_period > 16'd155) {
ch0_fund <= 7'd14; ch0_h3 <= 7'd34; ch0_h5 <= 7'd50;
} ELIF (ch0_half_period > 16'd141) {
ch0_fund <= 7'd17; ch0_h3 <= 7'd37; ch0_h5 <= 7'd52;
} ELIF (ch0_half_period > 16'd129) {
ch0_fund <= 7'd18; ch0_h3 <= 7'd38; ch0_h5 <= 7'd54;
} ELIF (ch0_half_period > 16'd115) {
ch0_fund <= 7'd20; ch0_h3 <= 7'd40; ch0_h5 <= 7'd56;
} ELIF (ch0_half_period > 16'd103) {
ch0_fund <= 7'd22; ch0_h3 <= 7'd42; ch0_h5 <= 7'd58;
} ELIF (ch0_half_period > 16'd94) {
ch0_fund <= 7'd24; ch0_h3 <= 7'd44; ch0_h5 <= 7'd60;
} ELIF (ch0_half_period > 16'd87) {
ch0_fund <= 7'd25; ch0_h3 <= 7'd45; ch0_h5 <= 7'd61;
} ELIF (ch0_half_period > 16'd77) {
ch0_fund <= 7'd27; ch0_h3 <= 7'd47; ch0_h5 <= 7'd63;
} ELIF (ch0_half_period > 16'd71) {
ch0_fund <= 7'd29; ch0_h3 <= 7'd49; ch0_h5 <= 7'd65;
} ELIF (ch0_half_period > 16'd65) {
ch0_fund <= 7'd30; ch0_h3 <= 7'd50; ch0_h5 <= 7'd66;
} ELIF (ch0_half_period > 16'd58) {
ch0_fund <= 7'd33; ch0_h3 <= 7'd53; ch0_h5 <= 7'd68;
} ELIF (ch0_half_period > 16'd52) {
ch0_fund <= 7'd35; ch0_h3 <= 7'd55; ch0_h5 <= 7'd70;
} ELIF (ch0_half_period > 16'd47) {
ch0_fund <= 7'd37; ch0_h3 <= 7'd57; ch0_h5 <= 7'd72;
} ELIF (ch0_half_period > 16'd43) {
ch0_fund <= 7'd38; ch0_h3 <= 7'd58; ch0_h5 <= 7'd74;
} ELIF (ch0_half_period > 16'd39) {
ch0_fund <= 7'd40; ch0_h3 <= 7'd60; ch0_h5 <= 7'd76;
} ELIF (ch0_half_period > 16'd35) {
ch0_fund <= 7'd42; ch0_h3 <= 7'd62; ch0_h5 <= 7'd78;
} ELSE {
ch0_fund <= 7'd127; ch0_h3 <= 7'd127; ch0_h5 <= 7'd127;
}
// ---- Half-period to bar index mapping (ch1) ----
IF (ch1_half_period == 16'd0 || ch1_half_period == 16'hFFFF) {
ch1_fund <= 7'd127; ch1_h3 <= 7'd127; ch1_h5 <= 7'd127;
} ELIF (ch1_half_period > 16'd231) {
ch1_fund <= 7'd7; ch1_h3 <= 7'd27; ch1_h5 <= 7'd42;
} ELIF (ch1_half_period > 16'd206) {
ch1_fund <= 7'd9; ch1_h3 <= 7'd29; ch1_h5 <= 7'd44;
} ELIF (ch1_half_period > 16'd189) {
ch1_fund <= 7'd11; ch1_h3 <= 7'd31; ch1_h5 <= 7'd47;
} ELIF (ch1_half_period > 16'd174) {
ch1_fund <= 7'd12; ch1_h3 <= 7'd32; ch1_h5 <= 7'd48;
} ELIF (ch1_half_period > 16'd155) {
ch1_fund <= 7'd14; ch1_h3 <= 7'd34; ch1_h5 <= 7'd50;
} ELIF (ch1_half_period > 16'd141) {
ch1_fund <= 7'd17; ch1_h3 <= 7'd37; ch1_h5 <= 7'd52;
} ELIF (ch1_half_period > 16'd129) {
ch1_fund <= 7'd18; ch1_h3 <= 7'd38; ch1_h5 <= 7'd54;
} ELIF (ch1_half_period > 16'd115) {
ch1_fund <= 7'd20; ch1_h3 <= 7'd40; ch1_h5 <= 7'd56;
} ELIF (ch1_half_period > 16'd103) {
ch1_fund <= 7'd22; ch1_h3 <= 7'd42; ch1_h5 <= 7'd58;
} ELIF (ch1_half_period > 16'd94) {
ch1_fund <= 7'd24; ch1_h3 <= 7'd44; ch1_h5 <= 7'd60;
} ELIF (ch1_half_period > 16'd87) {
ch1_fund <= 7'd25; ch1_h3 <= 7'd45; ch1_h5 <= 7'd61;
} ELIF (ch1_half_period > 16'd77) {
ch1_fund <= 7'd27; ch1_h3 <= 7'd47; ch1_h5 <= 7'd63;
} ELIF (ch1_half_period > 16'd71) {
ch1_fund <= 7'd29; ch1_h3 <= 7'd49; ch1_h5 <= 7'd65;
} ELIF (ch1_half_period > 16'd65) {
ch1_fund <= 7'd30; ch1_h3 <= 7'd50; ch1_h5 <= 7'd66;
} ELIF (ch1_half_period > 16'd58) {
ch1_fund <= 7'd33; ch1_h3 <= 7'd53; ch1_h5 <= 7'd68;
} ELIF (ch1_half_period > 16'd52) {
ch1_fund <= 7'd35; ch1_h3 <= 7'd55; ch1_h5 <= 7'd70;
} ELIF (ch1_half_period > 16'd47) {
ch1_fund <= 7'd37; ch1_h3 <= 7'd57; ch1_h5 <= 7'd72;
} ELIF (ch1_half_period > 16'd43) {
ch1_fund <= 7'd38; ch1_h3 <= 7'd58; ch1_h5 <= 7'd74;
} ELIF (ch1_half_period > 16'd39) {
ch1_fund <= 7'd40; ch1_h3 <= 7'd60; ch1_h5 <= 7'd76;
} ELIF (ch1_half_period > 16'd35) {
ch1_fund <= 7'd42; ch1_h3 <= 7'd62; ch1_h5 <= 7'd78;
} ELSE {
ch1_fund <= 7'd127; ch1_h3 <= 7'd127; ch1_h5 <= 7'd127;
}
// ---- Half-period to bar index mapping (ch2) ----
IF (ch2_half_period == 16'd0 || ch2_half_period == 16'hFFFF) {
ch2_fund <= 7'd127; ch2_h3 <= 7'd127; ch2_h5 <= 7'd127;
} ELIF (ch2_half_period > 16'd231) {
ch2_fund <= 7'd7; ch2_h3 <= 7'd27; ch2_h5 <= 7'd42;
} ELIF (ch2_half_period > 16'd206) {
ch2_fund <= 7'd9; ch2_h3 <= 7'd29; ch2_h5 <= 7'd44;
} ELIF (ch2_half_period > 16'd189) {
ch2_fund <= 7'd11; ch2_h3 <= 7'd31; ch2_h5 <= 7'd47;
} ELIF (ch2_half_period > 16'd174) {
ch2_fund <= 7'd12; ch2_h3 <= 7'd32; ch2_h5 <= 7'd48;
} ELIF (ch2_half_period > 16'd155) {
ch2_fund <= 7'd14; ch2_h3 <= 7'd34; ch2_h5 <= 7'd50;
} ELIF (ch2_half_period > 16'd141) {
ch2_fund <= 7'd17; ch2_h3 <= 7'd37; ch2_h5 <= 7'd52;
} ELIF (ch2_half_period > 16'd129) {
ch2_fund <= 7'd18; ch2_h3 <= 7'd38; ch2_h5 <= 7'd54;
} ELIF (ch2_half_period > 16'd115) {
ch2_fund <= 7'd20; ch2_h3 <= 7'd40; ch2_h5 <= 7'd56;
} ELIF (ch2_half_period > 16'd103) {
ch2_fund <= 7'd22; ch2_h3 <= 7'd42; ch2_h5 <= 7'd58;
} ELIF (ch2_half_period > 16'd94) {
ch2_fund <= 7'd24; ch2_h3 <= 7'd44; ch2_h5 <= 7'd60;
} ELIF (ch2_half_period > 16'd87) {
ch2_fund <= 7'd25; ch2_h3 <= 7'd45; ch2_h5 <= 7'd61;
} ELIF (ch2_half_period > 16'd77) {
ch2_fund <= 7'd27; ch2_h3 <= 7'd47; ch2_h5 <= 7'd63;
} ELIF (ch2_half_period > 16'd71) {
ch2_fund <= 7'd29; ch2_h3 <= 7'd49; ch2_h5 <= 7'd65;
} ELIF (ch2_half_period > 16'd65) {
ch2_fund <= 7'd30; ch2_h3 <= 7'd50; ch2_h5 <= 7'd66;
} ELIF (ch2_half_period > 16'd58) {
ch2_fund <= 7'd33; ch2_h3 <= 7'd53; ch2_h5 <= 7'd68;
} ELIF (ch2_half_period > 16'd52) {
ch2_fund <= 7'd35; ch2_h3 <= 7'd55; ch2_h5 <= 7'd70;
} ELIF (ch2_half_period > 16'd47) {
ch2_fund <= 7'd37; ch2_h3 <= 7'd57; ch2_h5 <= 7'd72;
} ELIF (ch2_half_period > 16'd43) {
ch2_fund <= 7'd38; ch2_h3 <= 7'd58; ch2_h5 <= 7'd74;
} ELIF (ch2_half_period > 16'd39) {
ch2_fund <= 7'd40; ch2_h3 <= 7'd60; ch2_h5 <= 7'd76;
} ELIF (ch2_half_period > 16'd35) {
ch2_fund <= 7'd42; ch2_h3 <= 7'd62; ch2_h5 <= 7'd78;
} ELSE {
ch2_fund <= 7'd127; ch2_h3 <= 7'd127; ch2_h5 <= 7'd127;
}
// ---- Half-period to bar index mapping (ch3) ----
IF (ch3_half_period == 16'd0 || ch3_half_period == 16'hFFFF) {
ch3_fund <= 7'd127; ch3_h3 <= 7'd127; ch3_h5 <= 7'd127;
} ELIF (ch3_half_period > 16'd231) {
ch3_fund <= 7'd7; ch3_h3 <= 7'd27; ch3_h5 <= 7'd42;
} ELIF (ch3_half_period > 16'd206) {
ch3_fund <= 7'd9; ch3_h3 <= 7'd29; ch3_h5 <= 7'd44;
} ELIF (ch3_half_period > 16'd189) {
ch3_fund <= 7'd11; ch3_h3 <= 7'd31; ch3_h5 <= 7'd47;
} ELIF (ch3_half_period > 16'd174) {
ch3_fund <= 7'd12; ch3_h3 <= 7'd32; ch3_h5 <= 7'd48;
} ELIF (ch3_half_period > 16'd155) {
ch3_fund <= 7'd14; ch3_h3 <= 7'd34; ch3_h5 <= 7'd50;
} ELIF (ch3_half_period > 16'd141) {
ch3_fund <= 7'd17; ch3_h3 <= 7'd37; ch3_h5 <= 7'd52;
} ELIF (ch3_half_period > 16'd129) {
ch3_fund <= 7'd18; ch3_h3 <= 7'd38; ch3_h5 <= 7'd54;
} ELIF (ch3_half_period > 16'd115) {
ch3_fund <= 7'd20; ch3_h3 <= 7'd40; ch3_h5 <= 7'd56;
} ELIF (ch3_half_period > 16'd103) {
ch3_fund <= 7'd22; ch3_h3 <= 7'd42; ch3_h5 <= 7'd58;
} ELIF (ch3_half_period > 16'd94) {
ch3_fund <= 7'd24; ch3_h3 <= 7'd44; ch3_h5 <= 7'd60;
} ELIF (ch3_half_period > 16'd87) {
ch3_fund <= 7'd25; ch3_h3 <= 7'd45; ch3_h5 <= 7'd61;
} ELIF (ch3_half_period > 16'd77) {
ch3_fund <= 7'd27; ch3_h3 <= 7'd47; ch3_h5 <= 7'd63;
} ELIF (ch3_half_period > 16'd71) {
ch3_fund <= 7'd29; ch3_h3 <= 7'd49; ch3_h5 <= 7'd65;
} ELIF (ch3_half_period > 16'd65) {
ch3_fund <= 7'd30; ch3_h3 <= 7'd50; ch3_h5 <= 7'd66;
} ELIF (ch3_half_period > 16'd58) {
ch3_fund <= 7'd33; ch3_h3 <= 7'd53; ch3_h5 <= 7'd68;
} ELIF (ch3_half_period > 16'd52) {
ch3_fund <= 7'd35; ch3_h3 <= 7'd55; ch3_h5 <= 7'd70;
} ELIF (ch3_half_period > 16'd47) {
ch3_fund <= 7'd37; ch3_h3 <= 7'd57; ch3_h5 <= 7'd72;
} ELIF (ch3_half_period > 16'd43) {
ch3_fund <= 7'd38; ch3_h3 <= 7'd58; ch3_h5 <= 7'd74;
} ELIF (ch3_half_period > 16'd39) {
ch3_fund <= 7'd40; ch3_h3 <= 7'd60; ch3_h5 <= 7'd76;
} ELIF (ch3_half_period > 16'd35) {
ch3_fund <= 7'd42; ch3_h3 <= 7'd62; ch3_h5 <= 7'd78;
} ELSE {
ch3_fund <= 7'd127; ch3_h3 <= 7'd127; ch3_h5 <= 7'd127;
}
// ---- Signed distances from sweep_idx to each source bar ----
d0f <= { 1'b0, sweep_idx } - { 1'b0, lat_fund0 };
d0h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_0 };
d0h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_0 };
d1f <= { 1'b0, sweep_idx } - { 1'b0, lat_fund1 };
d1h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_1 };
d1h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_1 };
d2f <= { 1'b0, sweep_idx } - { 1'b0, lat_fund2 };
d2h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_2 };
d2h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_2 };
d3f <= { 1'b0, sweep_idx } - { 1'b0, lat_fund3 };
d3h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_3 };
d3h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_3 };
// Absolute values
a0f <= (d0f[7] == 1'b1) ? (7'd0 - d0f[6:0]) : d0f[6:0];
a0h3 <= (d0h3[7] == 1'b1) ? (7'd0 - d0h3[6:0]) : d0h3[6:0];
a0h5 <= (d0h5[7] == 1'b1) ? (7'd0 - d0h5[6:0]) : d0h5[6:0];
a1f <= (d1f[7] == 1'b1) ? (7'd0 - d1f[6:0]) : d1f[6:0];
a1h3 <= (d1h3[7] == 1'b1) ? (7'd0 - d1h3[6:0]) : d1h3[6:0];
a1h5 <= (d1h5[7] == 1'b1) ? (7'd0 - d1h5[6:0]) : d1h5[6:0];
a2f <= (d2f[7] == 1'b1) ? (7'd0 - d2f[6:0]) : d2f[6:0];
a2h3 <= (d2h3[7] == 1'b1) ? (7'd0 - d2h3[6:0]) : d2h3[6:0];
a2h5 <= (d2h5[7] == 1'b1) ? (7'd0 - d2h5[6:0]) : d2h5[6:0];
a3f <= (d3f[7] == 1'b1) ? (7'd0 - d3f[6:0]) : d3f[6:0];
a3h3 <= (d3h3[7] == 1'b1) ? (7'd0 - d3h3[6:0]) : d3h3[6:0];
a3h5 <= (d3h5[7] == 1'b1) ? (7'd0 - d3h5[6:0]) : d3h5[6:0];
// ---- Per-source contributions (no self-reference) ----
// Ch0 fundamental: full volume center, half +-1, quarter +-2
IF (a0f == 7'd0) {
c0f <= lat_vol0;
} ELIF (a0f == 7'd1) {
c0f <= { 1'b0, lat_vol0[7:1] };
} ELIF (a0f == 7'd2) {
c0f <= { 2'b00, lat_vol0[7:2] };
} ELSE {
c0f <= 8'd0;
}
// Ch0 3rd harmonic: 1/4 center, 1/8 +-1, 1/16 +-2
IF (a0h3 == 7'd0) {
c0h3 <= { 2'b00, lat_vol0[7:2] };
} ELIF (a0h3 == 7'd1) {
c0h3 <= { 3'b000, lat_vol0[7:3] };
} ELIF (a0h3 == 7'd2) {
c0h3 <= { 4'b0000, lat_vol0[7:4] };
} ELSE {
c0h3 <= 8'd0;
}
// Ch0 5th harmonic: 1/8 center, 1/16 +-1
IF (a0h5 == 7'd0) {
c0h5 <= { 3'b000, lat_vol0[7:3] };
} ELIF (a0h5 == 7'd1) {
c0h5 <= { 4'b0000, lat_vol0[7:4] };
} ELSE {
c0h5 <= 8'd0;
}
// Ch1 fundamental
IF (a1f == 7'd0) {
c1f <= lat_vol1;
} ELIF (a1f == 7'd1) {
c1f <= { 1'b0, lat_vol1[7:1] };
} ELIF (a1f == 7'd2) {
c1f <= { 2'b00, lat_vol1[7:2] };
} ELSE {
c1f <= 8'd0;
}
IF (a1h3 == 7'd0) {
c1h3 <= { 2'b00, lat_vol1[7:2] };
} ELIF (a1h3 == 7'd1) {
c1h3 <= { 3'b000, lat_vol1[7:3] };
} ELIF (a1h3 == 7'd2) {
c1h3 <= { 4'b0000, lat_vol1[7:4] };
} ELSE {
c1h3 <= 8'd0;
}
IF (a1h5 == 7'd0) {
c1h5 <= { 3'b000, lat_vol1[7:3] };
} ELIF (a1h5 == 7'd1) {
c1h5 <= { 4'b0000, lat_vol1[7:4] };
} ELSE {
c1h5 <= 8'd0;
}
// Ch2 fundamental
IF (a2f == 7'd0) {
c2f <= lat_vol2;
} ELIF (a2f == 7'd1) {
c2f <= { 1'b0, lat_vol2[7:1] };
} ELIF (a2f == 7'd2) {
c2f <= { 2'b00, lat_vol2[7:2] };
} ELSE {
c2f <= 8'd0;
}
IF (a2h3 == 7'd0) {
c2h3 <= { 2'b00, lat_vol2[7:2] };
} ELIF (a2h3 == 7'd1) {
c2h3 <= { 3'b000, lat_vol2[7:3] };
} ELIF (a2h3 == 7'd2) {
c2h3 <= { 4'b0000, lat_vol2[7:4] };
} ELSE {
c2h3 <= 8'd0;
}
IF (a2h5 == 7'd0) {
c2h5 <= { 3'b000, lat_vol2[7:3] };
} ELIF (a2h5 == 7'd1) {
c2h5 <= { 4'b0000, lat_vol2[7:4] };
} ELSE {
c2h5 <= 8'd0;
}
// Ch3 fundamental
IF (a3f == 7'd0) {
c3f <= lat_vol3;
} ELIF (a3f == 7'd1) {
c3f <= { 1'b0, lat_vol3[7:1] };
} ELIF (a3f == 7'd2) {
c3f <= { 2'b00, lat_vol3[7:2] };
} ELSE {
c3f <= 8'd0;
}
IF (a3h3 == 7'd0) {
c3h3 <= { 2'b00, lat_vol3[7:2] };
} ELIF (a3h3 == 7'd1) {
c3h3 <= { 3'b000, lat_vol3[7:3] };
} ELIF (a3h3 == 7'd2) {
c3h3 <= { 4'b0000, lat_vol3[7:4] };
} ELSE {
c3h3 <= 8'd0;
}
IF (a3h5 == 7'd0) {
c3h5 <= { 3'b000, lat_vol3[7:3] };
} ELIF (a3h5 == 7'd1) {
c3h5 <= { 4'b0000, lat_vol3[7:4] };
} ELSE {
c3h5 <= 8'd0;
}
// ---- Sum tree (no self-reference) ----
sum_01f <= { 1'b0, c0f } + { 1'b0, c1f };
sum_23f <= { 1'b0, c2f } + { 1'b0, c3f };
sum_01h3 <= { 1'b0, c0h3 } + { 1'b0, c1h3 };
sum_23h3 <= { 1'b0, c2h3 } + { 1'b0, c3h3 };
sum_01h5 <= { 1'b0, c0h5 } + { 1'b0, c1h5 };
sum_23h5 <= { 1'b0, c2h5 } + { 1'b0, c3h5 };
sum_fund <= { 1'b0, sum_01f } + { 1'b0, sum_23f };
sum_h3 <= { 1'b0, sum_01h3 } + { 1'b0, sum_23h3 };
sum_h5 <= { 1'b0, sum_01h5 } + { 1'b0, sum_23h5 };
target_acc <= { 1'b0, sum_fund } + { 1'b0, sum_h3 } + { 1'b0, sum_h5 };
IF (target_acc > 11'd255) {
target_amp <= 8'hFF;
} ELSE {
target_amp <= target_acc[7:0];
}
// ---- Smoothing (read smooth from sweep_ram[7:0]) ----
smooth_cur <= sweep_ram.rd[sweep_idx][7:0];
smooth_delta <= { 1'b0, target_amp } - { 1'b0, smooth_cur };
smooth_step <= { smooth_delta[8], smooth_delta[8], smooth_delta[8:2] };
smooth_next <= { 1'b0, smooth_cur } + smooth_step;
IF (smooth_next[8] == 1'b1) {
smooth_result <= 8'd0;
} ELSE {
smooth_result <= smooth_next[7:0];
}
// ---- Peak read (from sweep_ram: [15:12]=peak_top4, [11:8]=timer) ----
peak_approx <= { sweep_ram.rd[sweep_idx][15:12], 4'b0000 };
timer_cur <= sweep_ram.rd[sweep_idx][11:8];
// ---- Pre-compute write values ----
IF (smooth_result > peak_approx) {
new_peak_amp <= smooth_result;
new_peak_timer <= 4'd15;
display_wr_val <= { smooth_result, smooth_result };
} ELIF (timer_cur == 4'd0) {
new_peak_amp <= smooth_result;
new_peak_timer <= 4'd0;
display_wr_val <= { smooth_result, smooth_result };
} ELSE {
new_peak_amp <= peak_approx;
new_peak_timer <= timer_cur - 4'd1;
display_wr_val <= { peak_approx, smooth_result };
}
sweep_wr_val <= { new_peak_amp[7:4], new_peak_timer, smooth_result };
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
IF (frame_pulse == 1'b1 && sweeping == 1'b0) {
// Latch volumes and bar indices
lat_vol0 <= ch0_vol_eff;
lat_vol1 <= ch1_vol_eff;
lat_vol2 <= ch2_vol_eff;
lat_vol3 <= ch3_vol_eff;
lat_fund0 <= ch0_fund; lat_fund1 <= ch1_fund;
lat_fund2 <= ch2_fund; lat_fund3 <= ch3_fund;
lat_h3_0 <= ch0_h3; lat_h3_1 <= ch1_h3;
lat_h3_2 <= ch2_h3; lat_h3_3 <= ch3_h3;
lat_h5_0 <= ch0_h5; lat_h5_1 <= ch1_h5;
lat_h5_2 <= ch2_h5; lat_h5_3 <= ch3_h5;
sweeping <= 1'b1;
sweep_idx <= 7'd0;
} ELIF (sweeping == 1'b1) {
// Write both RAMs (single write per cycle)
sweep_ram.wr[sweep_idx] <= sweep_wr_val;
display_ram.wr[sweep_idx] <= display_wr_val;
IF (sweep_idx == 7'd79) {
sweeping <= 1'b0;
} ELSE {
sweep_idx <= sweep_idx + 7'd1;
}
}
}
@endmodjz
// Spectrum Display — 80-Bar Renderer
// Reads packed bar state from DISTRIBUTED RAM (smooth+peak amplitudes),
// renders 80 narrow pill-shaped bar graphs with reflections and peak hold.
//
// Bar layout: 80 bars, 16px pitch (10px body + 6px gap) = 1280px total.
// Bars grow upward from baseline at y=600. Max height: 25 segments (400px).
// Segment: 12px body + 4px gap = 16px pitch.
// Reflection: below baseline, up to 6 segments, 1/4 brightness.
// Peak hold: single bright segment above bar (SHOW_PEAK=1 to enable).
// Color: smooth gradient red(bar 0) -> yellow(bar 39) -> blue(bar 79).
@module spectrum_display
CONST {
SHOW_PEAK = 0; // 1=draw peak hold pill above bar, 0=disable
}
PORT {
IN [1] clk;
IN [1] rst_n;
IN [11] x_pos;
IN [10] y_pos;
// RAM read interface
OUT [7] rd_bar; // bar index to read (0..79)
IN [16] rd_amp; // [7:0]=smooth_amp, [15:8]=peak_amp
// Pixel output
OUT [8] red;
OUT [8] green;
OUT [8] blue;
}
WIRE {
// Bar detection from x_pos
bar_idx [7]; // x_pos[10:4] = 0..79
bar_x [4]; // x_pos[3:0] = 0..15
in_body [1]; // bar_x < 10
in_bar [1]; // valid bar and in body
// Vertical geometry
above_base [1];
below_base [1];
pix_above [10]; // pixels above baseline (0..399)
pix_below [10]; // pixels below baseline (0..99)
// Segment computation (above baseline)
seg_idx [5]; // segment index (0..24)
y_in_seg [4]; // pixel within segment (0..15)
in_pill [1]; // y_in_seg < 12
// Reflection segment (below baseline)
ref_seg_idx [5];
ref_y_in_seg [4];
in_ref_pill [1];
// Bar state from RAM
smooth_amp [8];
peak_amp [8];
// Amplitude to segments
bar_segs_raw [5];
bar_segs [5];
peak_segs_raw [5];
peak_segs [5];
ref_segs [5];
// Color gradient computation
// half_pos: position within current color half (0-39)
half_pos [6];
// ramp = half_pos * 13 >> 1 (approximates half_pos * 255/39)
ramp_x13 [10]; // half_pos * 13, max 39*13=507
ramp [8]; // ramp_x13 >> 1, capped at 255
// Color palette (computed from gradient)
base_r [8];
base_g [8];
base_b [8];
// Pixel classification
is_bar_seg [1];
is_peak_seg [1];
is_ref_seg [1];
// Combinational RGB
next_r [8];
next_g [8];
next_b [8];
}
REGISTER {
red_r [8] = 8'd0;
green_r [8] = 8'd0;
blue_r [8] = 8'd0;
}
ASYNCHRONOUS {
// ---- Bar column detection ----
bar_idx <= x_pos[10:4];
bar_x <= x_pos[3:0];
in_body <= (bar_x < 4'd10) ? 1'b1 : 1'b0;
in_bar <= (bar_idx < 7'd80 && in_body == 1'b1) ? 1'b1 : 1'b0;
// Drive RAM read address
rd_bar <= bar_idx;
// Extract bar state from packed RAM value
smooth_amp <= rd_amp[7:0];
peak_amp <= rd_amp[15:8];
// ---- Amplitude to segments ----
bar_segs_raw <= smooth_amp[7:3];
IF (bar_segs_raw > 5'd25) {
bar_segs <= 5'd25;
} ELSE {
bar_segs <= bar_segs_raw;
}
peak_segs_raw <= peak_amp[7:3];
IF (peak_segs_raw > 5'd25) {
peak_segs <= 5'd25;
} ELSE {
peak_segs <= peak_segs_raw;
}
ref_segs <= { 2'b00, bar_segs[4:2] };
// ---- Vertical geometry ----
above_base <= (y_pos >= 10'd200 && y_pos < 10'd600) ? 1'b1 : 1'b0;
below_base <= (y_pos >= 10'd600 && y_pos < 10'd700) ? 1'b1 : 1'b0;
pix_above <= 10'd599 - y_pos;
pix_below <= y_pos - 10'd600;
seg_idx <= pix_above[8:4];
y_in_seg <= pix_above[3:0];
ref_seg_idx <= pix_below[8:4];
ref_y_in_seg <= pix_below[3:0];
in_pill <= (y_in_seg < 4'd12) ? 1'b1 : 1'b0;
in_ref_pill <= (ref_y_in_seg < 4'd12) ? 1'b1 : 1'b0;
// ---- Smooth color gradient: red(0) -> yellow(39) -> blue(79) ----
// half_pos = position within current half (0-39)
IF (bar_idx < 7'd40) {
half_pos <= bar_idx[5:0];
} ELSE {
half_pos <= bar_idx[5:0] - 6'd40;
}
// ramp = half_pos * 13 >> 1 ≈ half_pos * 6.5 (maps 0-39 to 0-253)
// half_pos * 13 = half_pos * 8 + half_pos * 4 + half_pos * 1
ramp_x13 <= { 4'b0000, half_pos } + { 3'b000, half_pos, 1'b0 } +
{ 2'b00, half_pos, 2'b00 } + { 1'b0, half_pos, 3'b000 };
IF (ramp_x13[9:1] > 9'd255) {
ramp <= 8'hFF;
} ELSE {
ramp <= ramp_x13[8:1];
}
// Bars 0-39: red -> yellow (R=FF, G ramps up, B=00)
// Bars 40-79: yellow -> blue (R ramps down, G ramps down, B ramps up)
IF (bar_idx < 7'd40) {
base_r <= 8'hFF;
base_g <= ramp;
base_b <= 8'h00;
} ELSE {
base_r <= 8'hFF - ramp;
base_g <= 8'hFF - ramp;
base_b <= ramp;
}
// ---- Pixel classification ----
is_bar_seg <= (in_bar == 1'b1 && above_base == 1'b1 && in_pill == 1'b1 && seg_idx < bar_segs) ? 1'b1 : 1'b0;
is_peak_seg <= (in_bar == 1'b1 && above_base == 1'b1 && in_pill == 1'b1 && seg_idx == peak_segs && peak_segs > bar_segs) ? lit(1, SHOW_PEAK) : 1'b0;
is_ref_seg <= (in_bar == 1'b1 && below_base == 1'b1 && in_ref_pill == 1'b1 && ref_seg_idx < ref_segs) ? 1'b1 : 1'b0;
// ---- Combinational pixel output ----
IF (is_bar_seg == 1'b1) {
next_r <= base_r;
next_g <= base_g;
next_b <= base_b;
} ELIF (is_peak_seg == 1'b1) {
// Peak: brighter version (+0x40, saturate at 0xFF)
IF (base_r > 8'hBF) {
next_r <= 8'hFF;
} ELSE {
next_r <= base_r + 8'h40;
}
IF (base_g > 8'hBF) {
next_g <= 8'hFF;
} ELSE {
next_g <= base_g + 8'h40;
}
IF (base_b > 8'hBF) {
next_b <= 8'hFF;
} ELSE {
next_b <= base_b + 8'h40;
}
} ELIF (is_ref_seg == 1'b1) {
// Reflection: 1/4 brightness
next_r <= { 2'b00, base_r[7:2] };
next_g <= { 2'b00, base_g[7:2] };
next_b <= { 2'b00, base_b[7:2] };
} ELSE {
next_r <= 8'h00;
next_g <= 8'h00;
next_b <= 8'h00;
}
// Registered output
red <= red_r;
green <= green_r;
blue <= blue_r;
}
SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
red_r <= next_r;
green_r <= next_g;
blue_r <= next_b;
}
@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
OVERRIDE for parameterized instances. A single module definition serves all four tone generators. OVERRIDE replaces Verilog's parameter passing with a mechanism that propagates through the module hierarchy — the override on tone_gen automatically reaches the melodies instance inside it, which controls the @file BRAM initializer. Verilog would require explicit parameter threading through every intermediate module.
Intrinsic signed arithmetic. sadd performs sign-extended addition with automatic width promotion. In Verilog, mixing signed and unsigned arithmetic requires careful $signed() casts that are easy to get wrong.
Memory type control. MEM(TYPE=BLOCK) for tone data and MEM(TYPE=DISTRIBUTED) for the spectrum analyzer's small lookup tables give the designer explicit control over BRAM vs. LUT RAM inference. Verilog leaves this to synthesis heuristics that vary by vendor.