diff --git a/files.qip b/files.qip index 97e9abc..a46c2c7 100644 --- a/files.qip +++ b/files.qip @@ -1,16 +1,18 @@ -set_global_assignment -name QIP_FILE rtl/pll_video.qip -set_global_assignment -name SDC_FILE menu.sdc -set_global_assignment -name SYSTEMVERILOG_FILE rtl/sdram.sv -set_global_assignment -name VERILOG_FILE rtl/lfsr.v -set_global_assignment -name SYSTEMVERILOG_FILE rtl/cos.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_reader.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_timing.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_top.sv -set_global_assignment -name SYSTEMVERILOG_FILE menu.sv -set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_vblank_latch.sv -set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_latch_sys_top_bridge.sv -set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_bootstrap_black.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_bootstrap_video.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_snow_phase.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_pixel_enable.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_snow_enable.sv +set_global_assignment -name QIP_FILE rtl/pll_video54.qip +set_global_assignment -name SDC_FILE menu.sdc +set_global_assignment -name SYSTEMVERILOG_FILE rtl/sdram.sv +set_global_assignment -name VERILOG_FILE rtl/lfsr.v +set_global_assignment -name SYSTEMVERILOG_FILE rtl/cos.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_reader.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_timing.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_top.sv +set_global_assignment -name SYSTEMVERILOG_FILE menu.sv +set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_vblank_latch.sv +set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_latch_sys_top_bridge.sv +set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_bootstrap_black.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_bootstrap_video.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_snow_phase.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_pixel_enable.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_snow_enable.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_hretime.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_hsize_map.sv diff --git a/menu.sdc b/menu.sdc index 0a01b31..d664c5a 100644 --- a/menu.sdc +++ b/menu.sdc @@ -1,12 +1,12 @@ # Core-level timing constraints (processed after sys/sys_top.sdc). -# CLK_VIDEO comes from a dedicated PLL (rtl/pll_video.v) and is asynchronous +# CLK_VIDEO comes from a dedicated PLL (rtl/pll_video54.v) and is asynchronous # to every other clock in the design: all crossings into and out of the video # domain go through two-flop synchronizers or the line FIFO's dual-clock # logic (rtl/native_video_reader.sv, rtl/native_video_top.sv). Without this -# group, derive_pll_clocks leaves the 27 MHz output related to the other +# group, derive_pll_clocks leaves the 54 MHz output related to the other # clocks (shared 50 MHz reference, and absent from sys_top.sdc's exclusive # groups), and the fitter tries to close those CDC paths against a ~1 ns # edge relationship. set_clock_groups -asynchronous \ - -group [get_clocks { *|pll_video|pll_video_inst|altera_pll_i|*[*].*|divclk}] + -group [get_clocks { *|pll_video54|pll_video54_inst|altera_pll_i|*[*].*|divclk}] diff --git a/menu.sv b/menu.sv index 93b032b..fe4c6b2 100644 --- a/menu.sv +++ b/menu.sv @@ -1,434 +1,451 @@ -//============================================================================ -// -// Menu for MiSTer. -// Copyright (C) 2017-2020 Sorgelig -// -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// -//============================================================================ - -module emu -( - `include "sys/emu_ports.vh" -); - -assign ADC_BUS = 'Z; -assign {UART_RTS, UART_DTR} = 0; -assign {SD_SCK, SD_MOSI, SD_CS} = 'Z; - -assign DDRAM_CLK = clk_sys; -assign CE_PIXEL = ce_pix; - -assign VGA_SL = 0; -// Field number for 480i: ascal (HDMI) keys deinterlacing off this, and the -// analog csync path passes it through. 0 in the progressive modes. -assign VGA_F1 = native_field; -assign VIDEO_ARX = 0; -assign VIDEO_ARY = 0; -assign VGA_SCALER= 0; -assign VGA_DISABLE = 0; - -assign AUDIO_MIX = 0; -assign HDMI_FREEZE = 0; -assign HDMI_BLACKOUT = 0; -assign HDMI_BOB_DEINT = 0; - -assign LED_DISK = 0; -assign LED_POWER[1]= 1; -assign BUTTONS = 0; - -reg [26:0] act_cnt; -always @(posedge clk_sys) act_cnt <= act_cnt + 1'd1; -assign LED_USER = FB ? led[0] : act_cnt[26] ? act_cnt[25:18] > act_cnt[7:0] : act_cnt[25:18] <= act_cnt[7:0]; - -wire [26:0] act_cnt2 = {~act_cnt[26],act_cnt[25:0]}; -assign LED_POWER[0]= FB ? led[2] : act_cnt2[26] ? act_cnt2[25:18] > act_cnt2[7:0] : act_cnt2[25:18] <= act_cnt2[7:0]; - - -`include "build_id.v" -// No video options here: native video mode and centering trims arrive via -// the DDR control block written by the launcher (see rtl/native_video_reader.sv). -localparam CONF_STR = { - "MENU;UART31250,MIDI;", - "- ;", - "V,v",`BUILD_DATE -}; - -wire forced_scandoubler; -wire [31:0] status; - -hps_io #(.CONF_STR(CONF_STR)) hps_io -( - .clk_sys(clk_sys), - .HPS_BUS(HPS_BUS), - .forced_scandoubler(forced_scandoubler), - .status(status), - .status_menumask(cfg) -); - -//////////////////// CLOCKS /////////////////// -wire locked, clk_sys; -pll pll -( - .refclk(CLK_50M), - .rst(0), - .outclk_0(clk_sys), - .outclk_1(), // stock 27.027 MHz output, unused (see pll_video) - .locked(locked) -); - -// Exact 27.000000 MHz video clock from its own PLL: 27 MHz can't share a -// VCO with the 100 MHz clk_sys (lcm = 2700 MHz, above the VCO ceiling). -wire vid_locked; -pll_video pll_video -( - .refclk(CLK_50M), - .rst(0), - .outclk_0(CLK_VIDEO), - .locked(vid_locked) -); - - -///////////////////// SDRAM /////////////////// -// -// Helper functionality: -// SDRAM and DDR3 RAM are being cleared while this core is working. -// some cores behave incorrectly if started with non-clean RAM. - -sdram sdr -( - .*, - .init(~locked), - .clk(clk_sys), - .addr(sdram_addr), - .wtbt(3), - .dout(sdram_dout), - .din(sdram_din), - .rd(sdram_rd), - .we(sdram_we), - .ready(sdram_ready) -); - -reg [26:0] sdram_addr; -wire sdram_ready; -wire [15:0] sdram_dout; -reg [15:0] sdram_din; -reg sdram_we; -reg sdram_rd; -reg [15:0] cfg = 0; - -always @(posedge clk_sys) begin - reg [4:0] state = 0; - - sdram_rd <= 0; - sdram_we <= 0; - - if(RESET) begin - state <= 0; - cfg <= 0; - end - else begin - case(state) - 0: if(sdram_ready) begin - cfg <= 0; - state <= state+1'd1; - end - 1: begin - sdram_addr <= 'h4000000; - sdram_din <= 3128; - sdram_we <= 1; - state <= state+1'd1; - end - 2: state <= state+1'd1; - 3: if(sdram_ready) begin - sdram_addr <= 'h2000000; - sdram_din <= 2064; - sdram_we <= 1; - state <= state+1'd1; - end - 4: state <= state+1'd1; - 5: if(sdram_ready) begin - sdram_addr <= 'h0000000; - sdram_din <= 1032; - sdram_we <= 1; - state <= state+1'd1; - end - 6: state <= state+1'd1; - 7: if(sdram_ready) begin - sdram_addr <= 'h1000000; - sdram_din <= 12345; - sdram_we <= 1; - state <= state+1'd1; - end - 8: state <= state+1'd1; - 9: if(sdram_ready) begin - sdram_addr <= 'h4000000; - sdram_rd <= 1; - state <= state+1'd1; - end - 10: state <= state+1'd1; - 11: if(sdram_ready) begin - cfg[2] <= (sdram_dout == 3128); - sdram_addr <= 'h2000000; - sdram_rd <= 1; - state <= state+1'd1; - end - 12: state <= state+1'd1; - 13: if(sdram_ready) begin - cfg[1] <= (sdram_dout == 2064); - sdram_addr <= 'h0000000; - sdram_rd <= 1; - state <= state+1'd1; - end - 14: state <= state+1'd1; - 15: if(sdram_ready) begin - cfg[0] <= (sdram_dout == 1032); - cfg[15] <= 1; - state <= state+1'd1; - end - 16: begin - sdram_we <= 0; - end - endcase - end -end - -// DDR clear loop removed: native_video_reader owns DDRAM_* signals. -// The reader polls the launcher's control block once per vblank; until the -// launcher publishes frames it issues a single 64-bit read per frame and the -// core shows the noise pattern. - -//////////////////////////// MT32pi ////////////////////////////////// - -// -// Pin | USB Name | Signal -// ----+----------+-------------- -// 0 | D+ | I/O I2C_SDA / RX (midi in) -// 1 | D- | O TX (midi out) -// 2 | TX- | I I2S_WS (1 == right) -// 3 | GND_d | I I2C_SCL -// 4 | RX+ | I I2S_BCLK -// 5 | RX- | I I2S_DAT -// 6 | TX+ | - none -// - -reg [15:0] mt32_i2s_r, mt32_i2s_l; -wire midi_rx; - -assign AUDIO_L = mt32_i2s_l; -assign AUDIO_R = mt32_i2s_r; -assign AUDIO_S = 1; - -assign USER_OUT[0] = 1; -assign USER_OUT[1] = UART_RXD; -assign USER_OUT[6:2] = '1; -assign UART_TXD = midi_rx; - - -// -// crossed/straight cable selection -// - -generate -genvar i; -for(i = 0; i<2; i++) begin : clk_rate - wire clk_in = i ? USER_IN[6] : USER_IN[4]; - reg [4:0] cnt; - always @(posedge CLK_AUDIO) begin : clkr - reg clk_sr, clk, old_clk; - reg [4:0] cnt_tmp; - - clk_sr <= clk_in; - if (clk_sr == clk_in) clk <= clk_sr; - - if(~&cnt_tmp) cnt_tmp <= cnt_tmp + 1'd1; - else cnt <= '1; - - old_clk <= clk; - if(~old_clk & clk) begin - cnt <= cnt_tmp; - cnt_tmp <= 0; - end - end -end - -reg crossed; -always @(posedge CLK_AUDIO) crossed <= (clk_rate[0].cnt <= clk_rate[1].cnt); -endgenerate - -wire i2s_ws = crossed ? USER_IN[2] : USER_IN[5]; -wire i2s_data = crossed ? USER_IN[5] : USER_IN[2]; -wire i2s_bclk = crossed ? USER_IN[4] : USER_IN[6]; -assign midi_rx = crossed ? USER_IN[6] : USER_IN[4]; - -always @(posedge CLK_AUDIO) begin : i2s_proc - reg [15:0] i2s_buf = 0; - reg [4:0] i2s_cnt = 0; - reg clk_sr; - reg i2s_clk = 0; - reg old_clk, old_ws; - reg i2s_next = 0; - - // Debounce clock - clk_sr <= i2s_bclk; - if (clk_sr == i2s_bclk) i2s_clk <= clk_sr; - - // Latch data and ws on rising edge - old_clk <= i2s_clk; - if (i2s_clk && ~old_clk) begin - - if (~i2s_cnt[4]) begin - i2s_cnt <= i2s_cnt + 1'd1; - i2s_buf[~i2s_cnt[3:0]] <= i2s_data; - end - - // Word Select will change 1 clock before the new word starts - old_ws <= i2s_ws; - if (old_ws != i2s_ws) i2s_next <= 1; - end - - if (i2s_next) begin - i2s_next <= 0; - i2s_cnt <= 0; - i2s_buf <= 0; - - if (i2s_ws) mt32_i2s_l <= i2s_buf; - else mt32_i2s_r <= i2s_buf; - end - - if (RESET) begin - i2s_buf <= 0; - mt32_i2s_l <= 0; - mt32_i2s_r <= 0; - end -end - -///////////////////// VIDEO /////////////////// - -wire FB = status[5]; -wire [2:0] led = status[8:6]; -// Set by Main while no frontend owns or is about to take the screen. -wire snow_allowed = status[10]; - -// Pixel clock: CLK_VIDEO = 27.000 MHz (the universal SD video clock). -// ce_pix /4 = 6.75 MHz gives exactly 15734.27 Hz (NTSC, 429-px line) and -// 15625.00 Hz (PAL, 432-px line); the 480i mode runs /2 = 13.5 MHz with an -// 858-px line for the same 15734.27 Hz. Both the cosine fallback and the FB -// reader use this ce_pix. -wire [1:0] native_mode; -wire ce_pix; -zaparoo_pixel_enable pixel_enable ( - .clk(CLK_VIDEO), - .reset(RESET | ~vid_locked), - .mode(native_mode), - .ce_pix(ce_pix) -); - -// Native video timing + DDR reader. Timing outputs (sync, DE, vcount, frame -// edge) are the SINGLE source of truth for VGA scanout in both modes — that's -// what guarantees the CRT sees a clean 15 kHz line rate whether we're -// painting cosine noise or reading a Linux-rendered framebuffer. Mode and -// centering trims come from the launcher's DDR control block, not the OSD. -wire [7:0] native_r; -wire [7:0] native_g; -wire [7:0] native_b; -wire native_hs; -wire native_vs; -wire native_de; -wire [8:0] native_vcount; -wire native_new_frame; -wire native_field; -wire native_active; - -native_video_top native_video -( - .clk_sys (clk_sys), - .clk_vid (CLK_VIDEO), - .ce_pix (ce_pix), - .reset (RESET | ~vid_locked), - - .ddr_busy (DDRAM_BUSY), - .ddr_burstcnt (DDRAM_BURSTCNT), - .ddr_addr (DDRAM_ADDR), - .ddr_dout (DDRAM_DOUT), - .ddr_dout_ready (DDRAM_DOUT_READY), - .ddr_rd (DDRAM_RD), - .ddr_din (DDRAM_DIN), - .ddr_be (DDRAM_BE), - .ddr_we (DDRAM_WE), - - .vga_r (native_r), - .vga_g (native_g), - .vga_b (native_b), - .vga_hs (native_hs), - .vga_vs (native_vs), - .vga_de (native_de), - .vga_hblank (), - .vga_vblank (), - .vga_vcount (native_vcount), - .vga_new_frame (native_new_frame), - .vga_mode (native_mode), - .vga_field (native_field), - .active (native_active) -); - -// Keep upstream's asynchronous noise source and grayscale weighting. Only -// the frame-phase enable differs: native timing stretches new_frame over -// several video clocks, so it must be sampled on ce_pix, not every clock. -wire [62:0] snow_random; -reg [2:0] snow_sample = 0; -wire [9:0] snow_phase; -wire [7:0] snow_cos; -wire [5:0] snow_level = {1'b0, snow_cos[7:3]} + 6'd32; -wire [5:0] snow_noise = {snow_sample[0], snow_sample[1], {4{snow_sample[2]}}}; -wire [7:0] snow_pixel = (snow_level >= snow_noise) ? - {snow_level - snow_noise, 2'b00} : 8'd0; - -lfsr #(.N(63)) snow_source(snow_random); -cos snow_wave(snow_phase, snow_cos); -zaparoo_snow_phase snow_motion ( - .clk(CLK_VIDEO), .reset(RESET | ~vid_locked), .ce_pix(ce_pix), - .new_frame(native_new_frame), .vcount(native_vcount), .phase(snow_phase) -); - -always @(posedge CLK_VIDEO) begin - if (RESET | ~vid_locked) snow_sample <= 0; - else if (ce_pix) snow_sample <= snow_random[2:0]; -end - -// Snow shows behind the stock OSD, and whenever Main reports that no frontend -// will draw. Main disables the OSD and leaves status[10] clear before handing -// video to the frontend, so that startup stays black. -wire show_snow; -zaparoo_snow_enable snow_enable ( - .clk(CLK_VIDEO), .reset(RESET | ~vid_locked), - .osd_status(OSD_STATUS), .snow_allowed(snow_allowed), - .show_snow(show_snow) -); - -// Black handoff, snow, or native frontend RGB. All three share the same -// native sync/DE; no video-mode switch is needed. -zaparoo_bootstrap_video bootstrap_video ( - .native_active(native_active), - .native_rgb({native_r, native_g, native_b}), - .show_snow(show_snow), - .snow_rgb({snow_pixel, snow_pixel, snow_pixel}), - .de_in(native_de), .hs_in(native_hs), .vs_in(native_vs), - .rgb_out({VGA_R, VGA_G, VGA_B}), - .de_out(VGA_DE), .hs_out(VGA_HS), .vs_out(VGA_VS) -); - -endmodule +//============================================================================ +// +// Menu for MiSTer. +// Copyright (C) 2017-2020 Sorgelig +// +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +//============================================================================ + +module emu +( + `include "sys/emu_ports.vh" +); + +assign ADC_BUS = 'Z; +assign {UART_RTS, UART_DTR} = 0; +assign {SD_SCK, SD_MOSI, SD_CS} = 'Z; + +assign DDRAM_CLK = clk_sys; +assign CE_PIXEL = ce_pix; + +assign VGA_SL = 0; +// Field number for 480i: ascal (HDMI) keys deinterlacing off this, and the +// analog csync path passes it through. 0 in the progressive modes. +assign VGA_F1 = native_field; +assign VIDEO_ARX = 0; +assign VIDEO_ARY = 0; +assign VGA_SCALER= 0; +assign VGA_DISABLE = 0; + +assign AUDIO_MIX = 0; +assign HDMI_FREEZE = 0; +assign HDMI_BLACKOUT = 0; +assign HDMI_BOB_DEINT = 0; + +assign LED_DISK = 0; +assign LED_POWER[1]= 1; +assign BUTTONS = 0; + +reg [26:0] act_cnt; +always @(posedge clk_sys) act_cnt <= act_cnt + 1'd1; +assign LED_USER = FB ? led[0] : act_cnt[26] ? act_cnt[25:18] > act_cnt[7:0] : act_cnt[25:18] <= act_cnt[7:0]; + +wire [26:0] act_cnt2 = {~act_cnt[26],act_cnt[25:0]}; +assign LED_POWER[0]= FB ? led[2] : act_cnt2[26] ? act_cnt2[25:18] > act_cnt2[7:0] : act_cnt2[25:18] <= act_cnt2[7:0]; + + +`include "build_id.v" +// No video options here: native video mode and centering trims arrive via +// the DDR control block written by the launcher (see rtl/native_video_reader.sv). +localparam CONF_STR = { + "MENU;UART31250,MIDI;", + "- ;", + "V,v",`BUILD_DATE +}; + +wire forced_scandoubler; +wire [31:0] status; + +hps_io #(.CONF_STR(CONF_STR)) hps_io +( + .clk_sys(clk_sys), + .HPS_BUS(HPS_BUS), + .forced_scandoubler(forced_scandoubler), + .status(status), + .status_menumask(cfg) +); + +//////////////////// CLOCKS /////////////////// +wire locked, clk_sys; +pll pll +( + .refclk(CLK_50M), + .rst(0), + .outclk_0(clk_sys), + .outclk_1(), // stock 27.027 MHz output, unused (see pll_video54) + .locked(locked) +); + +// Exact 54.000000 MHz video clock from its own PLL (1350 MHz VCO / 25). +// 54 MHz gives the analog H-size retimer 8 master clocks per progressive +// pixel (4 per 480i pixel); it can't share a VCO with the 100 MHz clk_sys. +wire vid_locked; +pll_video54 pll_video54 +( + .refclk(CLK_50M), + .rst(0), + .outclk_0(CLK_VIDEO), + .locked(vid_locked) +); + + +///////////////////// SDRAM /////////////////// +// +// Helper functionality: +// SDRAM and DDR3 RAM are being cleared while this core is working. +// some cores behave incorrectly if started with non-clean RAM. + +sdram sdr +( + .*, + .init(~locked), + .clk(clk_sys), + .addr(sdram_addr), + .wtbt(3), + .dout(sdram_dout), + .din(sdram_din), + .rd(sdram_rd), + .we(sdram_we), + .ready(sdram_ready) +); + +reg [26:0] sdram_addr; +wire sdram_ready; +wire [15:0] sdram_dout; +reg [15:0] sdram_din; +reg sdram_we; +reg sdram_rd; +reg [15:0] cfg = 0; + +always @(posedge clk_sys) begin + reg [4:0] state = 0; + + sdram_rd <= 0; + sdram_we <= 0; + + if(RESET) begin + state <= 0; + cfg <= 0; + end + else begin + case(state) + 0: if(sdram_ready) begin + cfg <= 0; + state <= state+1'd1; + end + 1: begin + sdram_addr <= 'h4000000; + sdram_din <= 3128; + sdram_we <= 1; + state <= state+1'd1; + end + 2: state <= state+1'd1; + 3: if(sdram_ready) begin + sdram_addr <= 'h2000000; + sdram_din <= 2064; + sdram_we <= 1; + state <= state+1'd1; + end + 4: state <= state+1'd1; + 5: if(sdram_ready) begin + sdram_addr <= 'h0000000; + sdram_din <= 1032; + sdram_we <= 1; + state <= state+1'd1; + end + 6: state <= state+1'd1; + 7: if(sdram_ready) begin + sdram_addr <= 'h1000000; + sdram_din <= 12345; + sdram_we <= 1; + state <= state+1'd1; + end + 8: state <= state+1'd1; + 9: if(sdram_ready) begin + sdram_addr <= 'h4000000; + sdram_rd <= 1; + state <= state+1'd1; + end + 10: state <= state+1'd1; + 11: if(sdram_ready) begin + cfg[2] <= (sdram_dout == 3128); + sdram_addr <= 'h2000000; + sdram_rd <= 1; + state <= state+1'd1; + end + 12: state <= state+1'd1; + 13: if(sdram_ready) begin + cfg[1] <= (sdram_dout == 2064); + sdram_addr <= 'h0000000; + sdram_rd <= 1; + state <= state+1'd1; + end + 14: state <= state+1'd1; + 15: if(sdram_ready) begin + cfg[0] <= (sdram_dout == 1032); + cfg[15] <= 1; + state <= state+1'd1; + end + 16: begin + sdram_we <= 0; + end + endcase + end +end + +// DDR clear loop removed: native_video_reader owns DDRAM_* signals. +// The reader polls the launcher's control block once per vblank; until the +// launcher publishes frames it issues a single 64-bit read per frame and the +// core shows the noise pattern. + +//////////////////////////// MT32pi ////////////////////////////////// + +// +// Pin | USB Name | Signal +// ----+----------+-------------- +// 0 | D+ | I/O I2C_SDA / RX (midi in) +// 1 | D- | O TX (midi out) +// 2 | TX- | I I2S_WS (1 == right) +// 3 | GND_d | I I2C_SCL +// 4 | RX+ | I I2S_BCLK +// 5 | RX- | I I2S_DAT +// 6 | TX+ | - none +// + +reg [15:0] mt32_i2s_r, mt32_i2s_l; +wire midi_rx; + +assign AUDIO_L = mt32_i2s_l; +assign AUDIO_R = mt32_i2s_r; +assign AUDIO_S = 1; + +assign USER_OUT[0] = 1; +assign USER_OUT[1] = UART_RXD; +assign USER_OUT[6:2] = '1; +assign UART_TXD = midi_rx; + + +// +// crossed/straight cable selection +// + +generate +genvar i; +for(i = 0; i<2; i++) begin : clk_rate + wire clk_in = i ? USER_IN[6] : USER_IN[4]; + reg [4:0] cnt; + always @(posedge CLK_AUDIO) begin : clkr + reg clk_sr, clk, old_clk; + reg [4:0] cnt_tmp; + + clk_sr <= clk_in; + if (clk_sr == clk_in) clk <= clk_sr; + + if(~&cnt_tmp) cnt_tmp <= cnt_tmp + 1'd1; + else cnt <= '1; + + old_clk <= clk; + if(~old_clk & clk) begin + cnt <= cnt_tmp; + cnt_tmp <= 0; + end + end +end + +reg crossed; +always @(posedge CLK_AUDIO) crossed <= (clk_rate[0].cnt <= clk_rate[1].cnt); +endgenerate + +wire i2s_ws = crossed ? USER_IN[2] : USER_IN[5]; +wire i2s_data = crossed ? USER_IN[5] : USER_IN[2]; +wire i2s_bclk = crossed ? USER_IN[4] : USER_IN[6]; +assign midi_rx = crossed ? USER_IN[6] : USER_IN[4]; + +always @(posedge CLK_AUDIO) begin : i2s_proc + reg [15:0] i2s_buf = 0; + reg [4:0] i2s_cnt = 0; + reg clk_sr; + reg i2s_clk = 0; + reg old_clk, old_ws; + reg i2s_next = 0; + + // Debounce clock + clk_sr <= i2s_bclk; + if (clk_sr == i2s_bclk) i2s_clk <= clk_sr; + + // Latch data and ws on rising edge + old_clk <= i2s_clk; + if (i2s_clk && ~old_clk) begin + + if (~i2s_cnt[4]) begin + i2s_cnt <= i2s_cnt + 1'd1; + i2s_buf[~i2s_cnt[3:0]] <= i2s_data; + end + + // Word Select will change 1 clock before the new word starts + old_ws <= i2s_ws; + if (old_ws != i2s_ws) i2s_next <= 1; + end + + if (i2s_next) begin + i2s_next <= 0; + i2s_cnt <= 0; + i2s_buf <= 0; + + if (i2s_ws) mt32_i2s_l <= i2s_buf; + else mt32_i2s_r <= i2s_buf; + end + + if (RESET) begin + i2s_buf <= 0; + mt32_i2s_l <= 0; + mt32_i2s_r <= 0; + end +end + +///////////////////// VIDEO /////////////////// + +wire FB = status[5]; +wire [2:0] led = status[8:6]; +// Set by Main while no frontend owns or is about to take the screen. +wire snow_allowed = status[10]; + +// Pixel clock: CLK_VIDEO = 54.000 MHz (2x the universal SD video clock, +// for fine-grained analog H-size retiming). ce_pix /8 = 6.75 MHz gives +// exactly 15734.27 Hz (NTSC, 429-px line) and 15625.00 Hz (PAL, 432-px +// line); the 480i mode runs /4 = 13.5 MHz with an 858-px line for the same +// 15734.27 Hz. Both the cosine fallback and the FB reader use this ce_pix. +wire [1:0] native_mode; +wire ce_pix; +zaparoo_pixel_enable pixel_enable ( + .clk(CLK_VIDEO), + .reset(RESET | ~vid_locked), + .mode(native_mode), + .ce_pix(ce_pix) +); + +// Native video timing + DDR reader. Timing outputs (sync, DE, vcount, frame +// edge) are the SINGLE source of truth for VGA scanout in both modes — that's +// what guarantees the CRT sees a clean 15 kHz line rate whether we're +// painting cosine noise or reading a Linux-rendered framebuffer. Mode and +// centering trims come from the launcher's DDR control block, not the OSD. +wire [7:0] native_r; +wire [7:0] native_g; +wire [7:0] native_b; +wire native_hs; +wire native_vs; +wire native_de; +wire [8:0] native_vcount; +wire native_new_frame; +wire native_field; +wire native_active; +wire signed [7:0] native_h_size; + +native_video_top native_video +( + .clk_sys (clk_sys), + .clk_vid (CLK_VIDEO), + .ce_pix (ce_pix), + .reset (RESET | ~vid_locked), + + .ddr_busy (DDRAM_BUSY), + .ddr_burstcnt (DDRAM_BURSTCNT), + .ddr_addr (DDRAM_ADDR), + .ddr_dout (DDRAM_DOUT), + .ddr_dout_ready (DDRAM_DOUT_READY), + .ddr_rd (DDRAM_RD), + .ddr_din (DDRAM_DIN), + .ddr_be (DDRAM_BE), + .ddr_we (DDRAM_WE), + + .vga_r (native_r), + .vga_g (native_g), + .vga_b (native_b), + .vga_hs (native_hs), + .vga_vs (native_vs), + .vga_de (native_de), + .vga_hblank (), + .vga_vblank (), + .vga_vcount (native_vcount), + .vga_new_frame (native_new_frame), + .vga_mode (native_mode), + .vga_field (native_field), + .vga_h_size (native_h_size), + .active (native_active) +); + +// Analog H-size sideband to sys_top's post-OSD retimer. Everything here is +// CLK_VIDEO-domain and quasi-static (h_size is two-flop synced inside +// native_video_top, native_mode is the timing module's latched mode), so no +// new CDC is introduced; hretime latches the controls per line at HSYNC. +zaparoo_hsize_map hsize_map +( + .mode (native_mode), + .h_size (native_h_size), + .enable (CRT_HSIZE_EN), + .scale (CRT_HSIZE_SCALE) +); +// (480i bypass is decided here in hsize_map; sys_top's retimer runs at a +// constant 13.5 MHz sample rate in every mode.) + +// Keep upstream's asynchronous noise source and grayscale weighting. Only +// the frame-phase enable differs: native timing stretches new_frame over +// several video clocks, so it must be sampled on ce_pix, not every clock. +wire [62:0] snow_random; +reg [2:0] snow_sample = 0; +wire [9:0] snow_phase; +wire [7:0] snow_cos; +wire [5:0] snow_level = {1'b0, snow_cos[7:3]} + 6'd32; +wire [5:0] snow_noise = {snow_sample[0], snow_sample[1], {4{snow_sample[2]}}}; +wire [7:0] snow_pixel = (snow_level >= snow_noise) ? + {snow_level - snow_noise, 2'b00} : 8'd0; + +lfsr #(.N(63)) snow_source(snow_random); +cos snow_wave(snow_phase, snow_cos); +zaparoo_snow_phase snow_motion ( + .clk(CLK_VIDEO), .reset(RESET | ~vid_locked), .ce_pix(ce_pix), + .new_frame(native_new_frame), .vcount(native_vcount), .phase(snow_phase) +); + +always @(posedge CLK_VIDEO) begin + if (RESET | ~vid_locked) snow_sample <= 0; + else if (ce_pix) snow_sample <= snow_random[2:0]; +end + +// Snow shows behind the stock OSD, and whenever Main reports that no frontend +// will draw. Main disables the OSD and leaves status[10] clear before handing +// video to the frontend, so that startup stays black. +wire show_snow; +zaparoo_snow_enable snow_enable ( + .clk(CLK_VIDEO), .reset(RESET | ~vid_locked), + .osd_status(OSD_STATUS), .snow_allowed(snow_allowed), + .show_snow(show_snow) +); + +// Black handoff, snow, or native frontend RGB. All three share the same +// native sync/DE; no video-mode switch is needed. +zaparoo_bootstrap_video bootstrap_video ( + .native_active(native_active), + .native_rgb({native_r, native_g, native_b}), + .show_snow(show_snow), + .snow_rgb({snow_pixel, snow_pixel, snow_pixel}), + .de_in(native_de), .hs_in(native_hs), .vs_in(native_vs), + .rgb_out({VGA_R, VGA_G, VGA_B}), + .de_out(VGA_DE), .hs_out(VGA_HS), .vs_out(VGA_VS) +); + +endmodule diff --git a/rtl/native_video_reader.sv b/rtl/native_video_reader.sv index 8c88401..b5b2d4c 100644 --- a/rtl/native_video_reader.sv +++ b/rtl/native_video_reader.sv @@ -8,6 +8,19 @@ // mode: 0 = 352x240p60, 1 = 720x480i60, 2 = 352x288p50 (+v = down) // 0x3A001000: buffer 0 0x3A180000: buffer 1 (tight stride, width*4 B) // +// Interim H-size extension: the poll reads a SECOND beat (bytes 0x08-0x0F). +// word2 [31:0] at 0x08: [31:16] magic 0x5A52, [15:8] reserved 0, +// [7:0] signed h_size, clamped here to -8..+2 (analog width stretch in +// 1/64-pixel-period steps; 0 = retimer bypass). Without the 0x5A52 magic +// the field reads as 0, so legacy writers imply unity width. Word2 shares +// word1's ownership: a running frontend writes it (saved h_size, reserved +// byte 0) and Main writes it from its OSD page, test pattern and +// post-blank republish, with Main's save respawning the frontend so the +// writers never disagree. It applies only while word1's magic is valid. The deferred v3 protocol +// (plans/menu-crt-video-plan.md sec. 7) repurposes byte 0x08 and moves +// 0x5A52 into word1's magic slot; a v3 writer against this reader parks it +// safely idle, and matched releases replace this layout wholesale. +// // The magic is mandatory, and a block is only painted once the writer has // been shown to be live. Nothing clears DDR on this core's startup, so a // previous core's leftovers sit at CTRL_ADDR looking like a control block; @@ -50,6 +63,7 @@ module native_video_reader output reg [1:0] mode_out, output reg signed [7:0] h_offset_out, output reg signed [5:0] v_offset_out, + output reg signed [7:0] h_size_out, output reg [7:0] r_out, output reg [7:0] g_out, @@ -125,6 +139,8 @@ localparam [3:0] ST_WAIT_DISPLAY = 4'd7; reg [3:0] state; reg [31:0] ctrl_word; reg [31:0] ctrl_word1; +reg [31:0] ctrl_word2; +reg ctrl_beat; reg [29:0] prev_frame_counter; reg [28:0] buf_base_addr; reg [8:0] cur_line; @@ -158,6 +174,14 @@ wire signed [3:0] legacy_v = $signed(ctrl_word1[7:4]); // Preserve legacy clamp semantics while extended writers use wider porches. wire signed [7:0] legacy_h = (ctrl_h > 8'sd8) ? 8'sd8 : (ctrl_h < -8'sd8) ? -8'sd8 : ctrl_h; +// Interim word2 H size: only meaningful under its own magic; clamp to the +// qualified analog range (positive growth is porch-limited, see hretime). +wire word2_ok = (ctrl_word2[31:16] == 16'h5A52); +wire signed [7:0] raw_hsize = $signed(ctrl_word2[7:0]); +wire signed [7:0] ctrl_hsize = !word2_ok ? 8'sd0 : + (raw_hsize > 8'sd2) ? 8'sd2 : + (raw_hsize < -8'sd8) ? -8'sd8 : raw_hsize; + // 480i: source line = displayed line * 2 + field, from one progressive frame. wire [8:0] src_line = scan_interlaced ? ({cur_line[7:0], 1'b0} + {8'd0, field_ddr}) : cur_line; wire [28:0] line_base = buf_base_addr + src_line * line_words; @@ -175,6 +199,8 @@ always @(posedge ddr_clk) begin ddr_addr <= 29'd0; ctrl_word <= 32'd0; ctrl_word1 <= 32'd0; + ctrl_word2 <= 32'd0; + ctrl_beat <= 1'b0; prev_frame_counter <= 30'd0; buf_base_addr <= BUF0_V2; cur_line <= 9'd0; @@ -191,6 +217,7 @@ always @(posedge ddr_clk) begin mode_out <= 2'd0; h_offset_out <= 8'sd0; v_offset_out <= 6'sd0; + h_size_out <= 8'sd0; line_words <= 9'd160; scan_lines <= 9'd240; scan_interlaced <= 1'b0; @@ -216,17 +243,23 @@ always @(posedge ddr_clk) begin ST_POLL_CTRL: begin if(!ddr_busy) begin ddr_addr <= CTRL_ADDR; - ddr_burstcnt <= 8'd1; + ddr_burstcnt <= 8'd2; ddr_rd <= 1'b1; + ctrl_beat <= 1'b0; timeout_cnt <= 20'd0; state <= ST_WAIT_CTRL; end end ST_WAIT_CTRL: begin - if(ddr_dout_ready) begin + if(ddr_dout_ready && !ctrl_beat) begin ctrl_word <= ddr_dout[31:0]; ctrl_word1 <= ddr_dout[63:32]; + ctrl_beat <= 1'b1; + timeout_cnt <= 20'd0; + end + else if(ddr_dout_ready) begin + ctrl_word2 <= ddr_dout[31:0]; timeout_cnt <= 20'd0; state <= ST_CHECK_CTRL; end @@ -254,6 +287,7 @@ always @(posedge ddr_clk) begin mode_out <= 2'd0; h_offset_out <= 8'sd0; v_offset_out <= 6'sd0; + h_size_out <= 8'sd0; state <= ST_IDLE; end else begin @@ -261,6 +295,7 @@ always @(posedge ddr_clk) begin h_offset_out <= extended ? ctrl_h : legacy_h; v_offset_out <= extended ? $signed(ctrl_word1[7:2]) : (legacy_v > 4'sd2 ? 6'sd2 : {{2{legacy_v[3]}}, legacy_v}); + h_size_out <= ctrl_hsize; line_words <= (ctrl_mode == 2'd1) ? 9'd360 : 9'd176; scan_lines <= (ctrl_mode == 2'd2) ? 9'd288 : 9'd240; scan_interlaced <= (ctrl_mode == 2'd1); diff --git a/rtl/native_video_top.sv b/rtl/native_video_top.sv index 4bf8393..fa1e713 100644 --- a/rtl/native_video_top.sv +++ b/rtl/native_video_top.sv @@ -34,6 +34,7 @@ module native_video_top output wire vga_new_frame, output wire [1:0] vga_mode, // active timing mode; selects ce_pix divider + output wire signed [7:0] vga_h_size, // analog H size for the sys_top retimer output wire vga_field, // 480i field number (VGA_F1) output wire active ); @@ -52,13 +53,16 @@ wire tim_field; wire [1:0] rd_mode; wire signed [7:0] rd_h_offset; wire signed [5:0] rd_v_offset; +wire signed [7:0] rd_h_size; reg [1:0] mode_sync [1:0]; reg [7:0] h_offset_sync [1:0]; reg [5:0] v_offset_sync [1:0]; +reg [7:0] h_size_sync [1:0]; always @(posedge clk_vid) begin mode_sync[0] <= rd_mode; mode_sync[1] <= mode_sync[0]; h_offset_sync[0] <= rd_h_offset; h_offset_sync[1] <= h_offset_sync[0]; + h_size_sync[0] <= rd_h_size; h_size_sync[1] <= h_size_sync[0]; v_offset_sync[0] <= rd_v_offset; v_offset_sync[1] <= v_offset_sync[0]; end @@ -110,6 +114,7 @@ native_video_reader reader .mode_out (rd_mode), .h_offset_out (rd_h_offset), .v_offset_out (rd_v_offset), + .h_size_out (rd_h_size), .r_out (vga_r), .g_out (vga_g), @@ -127,4 +132,6 @@ assign vga_new_frame = tim_new_frame; assign vga_field = tim_field; assign active = frame_ready; +assign vga_h_size = $signed(h_size_sync[1]); + endmodule diff --git a/rtl/pll_video54.qip b/rtl/pll_video54.qip new file mode 100644 index 0000000..03c3e05 --- /dev/null +++ b/rtl/pll_video54.qip @@ -0,0 +1,6 @@ +set_global_assignment -name VERILOG_FILE [file join $::quartus(qip_path) "pll_video54.v"] +set_global_assignment -name VERILOG_FILE [file join $::quartus(qip_path) "pll_video54/pll_video54_0002.v"] + +set_instance_assignment -name PLL_COMPENSATION_MODE DIRECT -to "*pll_video54_0002*|altera_pll:altera_pll_i*|*" +set_instance_assignment -name PLL_AUTO_RESET ON -to "*pll_video54_0002*|altera_pll:altera_pll_i*|*" +set_instance_assignment -name PLL_BANDWIDTH_PRESET AUTO -to "*pll_video54_0002*|altera_pll:altera_pll_i*|*" diff --git a/rtl/pll_video54.v b/rtl/pll_video54.v new file mode 100644 index 0000000..3f0468e --- /dev/null +++ b/rtl/pll_video54.v @@ -0,0 +1,23 @@ +// Dedicated 54.000000 MHz video PLL for the future high-resolution Menu +// re-timer clock. From the 50 MHz reference it uses a 1350 MHz VCO and a +// divide-by-25 output, so the result is exact and uses integer PLL ratios. +// +// This replaces pll_video (27 MHz) as Menu's video master. The timing engine +// derives its established source-pixel rates with /8 and /4 enables. + +`timescale 1 ps / 1 ps +module pll_video54 ( + input wire refclk, // refclk.clk + input wire rst, // reset.reset + output wire outclk_0, // outclk0.clk + output wire locked // locked.export + ); + + pll_video54_0002 pll_video54_inst ( + .refclk (refclk), // refclk.clk + .rst (rst), // reset.reset + .outclk_0 (outclk_0), // outclk0.clk + .locked (locked) // locked.export + ); + +endmodule diff --git a/rtl/pll_video54/pll_video54_0002.v b/rtl/pll_video54/pll_video54_0002.v new file mode 100644 index 0000000..22baae9 --- /dev/null +++ b/rtl/pll_video54/pll_video54_0002.v @@ -0,0 +1,86 @@ +`timescale 1ns/10ps +module pll_video54_0002( + + // interface 'refclk' + input wire refclk, + + // interface 'reset' + input wire rst, + + // interface 'outclk0' + output wire outclk_0, + + // interface 'locked' + output wire locked +); + + altera_pll #( + .fractional_vco_multiplier("false"), + .reference_clock_frequency("50.0 MHz"), + .operation_mode("direct"), + .number_of_clocks(1), + .output_clock_frequency0("54.000000 MHz"), + .phase_shift0("0 ps"), + .duty_cycle0(50), + .output_clock_frequency1("0 MHz"), + .phase_shift1("0 ps"), + .duty_cycle1(50), + .output_clock_frequency2("0 MHz"), + .phase_shift2("0 ps"), + .duty_cycle2(50), + .output_clock_frequency3("0 MHz"), + .phase_shift3("0 ps"), + .duty_cycle3(50), + .output_clock_frequency4("0 MHz"), + .phase_shift4("0 ps"), + .duty_cycle4(50), + .output_clock_frequency5("0 MHz"), + .phase_shift5("0 ps"), + .duty_cycle5(50), + .output_clock_frequency6("0 MHz"), + .phase_shift6("0 ps"), + .duty_cycle6(50), + .output_clock_frequency7("0 MHz"), + .phase_shift7("0 ps"), + .duty_cycle7(50), + .output_clock_frequency8("0 MHz"), + .phase_shift8("0 ps"), + .duty_cycle8(50), + .output_clock_frequency9("0 MHz"), + .phase_shift9("0 ps"), + .duty_cycle9(50), + .output_clock_frequency10("0 MHz"), + .phase_shift10("0 ps"), + .duty_cycle10(50), + .output_clock_frequency11("0 MHz"), + .phase_shift11("0 ps"), + .duty_cycle11(50), + .output_clock_frequency12("0 MHz"), + .phase_shift12("0 ps"), + .duty_cycle12(50), + .output_clock_frequency13("0 MHz"), + .phase_shift13("0 ps"), + .duty_cycle13(50), + .output_clock_frequency14("0 MHz"), + .phase_shift14("0 ps"), + .duty_cycle14(50), + .output_clock_frequency15("0 MHz"), + .phase_shift15("0 ps"), + .duty_cycle15(50), + .output_clock_frequency16("0 MHz"), + .phase_shift16("0 ps"), + .duty_cycle16(50), + .output_clock_frequency17("0 MHz"), + .phase_shift17("0 ps"), + .duty_cycle17(50), + .pll_type("General"), + .pll_subtype("General") + ) altera_pll_i ( + .rst (rst), + .outclk (outclk_0), + .locked (locked), + .fboutclk ( ), + .fbclk (1'b0), + .refclk (refclk) + ); +endmodule diff --git a/rtl/zaparoo_hretime.sv b/rtl/zaparoo_hretime.sv new file mode 100644 index 0000000..4bf45e4 --- /dev/null +++ b/rtl/zaparoo_hretime.sv @@ -0,0 +1,226 @@ +// Horizontal analogue re-timer for the Menu CRT output. +// +// Each source pixel is emitted once, but is held for a slightly different +// number of CLK_VIDEO cycles. This adjusts the analogue picture width without +// resampling or changing normal HDMI/ascal. Raw direct-video HDMI follows the +// same analogue branch. The Menu feeds it 13.5 MHz samples (four 54 MHz +// clocks each): one 480i pixel, or half a progressive pixel = one OSD dot, +// so the OSD's half-pixel strokes survive retiming. +// +// The source is intentionally based on jtframe_hretime by Andrea Bogazzi and +// the CRT-Adjust module by Umberto Parisi. It is kept as a Menu-specific +// implementation: no build macros or per-core parameters are required here. + +module zaparoo_hretime +( + input wire clk, + input wire reset, + input wire ce_in, + input wire enable, + input wire [1:0] mode, + // Two's-complement -8..+7. With enable set, 0..+7 map to +1..+8; + // Menu clamps the externally supplied setting to a safe maximum of +2. + input wire signed [3:0] scale, + + input wire [23:0] din, + input wire hs_in, + input wire vs_in, + input wire de_in, + + output wire ce_out, + output wire [23:0] dout, + output wire hs_out, + output wire vs_out, + output wire de_out +); + +localparam integer STEP = 64; +// At 480i/-8 the re-timer buffers 90 pixels before its first output. 128 +// entries preserves that leading delay without wrapping the source line. +localparam integer DEPTH = 128; +localparam integer HW = 10; +localparam integer MAX_BASE = 8; +localparam integer SLOG = $clog2(STEP); +localparam integer AW = $clog2(DEPTH); +localparam integer ACCW = $clog2(MAX_BASE * (STEP + 8)) + 1; +localparam integer PW = SLOG + HW; +localparam integer SDEP = 1 << $clog2((DEPTH * MAX_BASE) / 2 + 1); +localparam integer SAW = $clog2(SDEP); + +(* ramstyle = "no_rw_check" *) reg [23:0] pixel_mem [0:DEPTH-1]; +(* ramstyle = "no_rw_check" *) reg [1:0] sync_mem [0:SDEP-1]; + +reg [23:0] mem_rd = 0; +reg [23:0] fifo_dout = 0; +reg [23:0] din_l = 0; +reg [AW-1:0] wptr = 0; +reg [AW-1:0] rptr = 0; +reg [HW-1:0] wcnt = 0; +reg [HW-1:0] rcnt = 0; +reg [HW-1:0] nactive = 0; +reg [ACCW-1:0] acc = 0; +reg [SAW-1:0] sync_wp = 0; +reg hs_l = 0; +reg ce_slow = 0; +reg started = 0; +reg fifo_de = 0; +reg de_l = 0; +reg hs_dly = 0; +reg vs_dly = 0; +reg hs_b = 0; // bypass HS, latched with din_l/de_l so the +reg vs_b = 0; // bypass path stays skew-free vs data and DE +reg enable_l = 0; +reg signed [3:0] scale_l = 0; +reg [3:0] base_l = MAX_BASE; + +// Skip a zero scale while enabled: the four-bit input expresses the sixteen +// non-zero steps -8..-1 and +1..+8, matching the frontend selector. +wire signed [4:0] effective_scale = + {scale_l[3], scale_l} + {4'd0, ~scale_l[3]}; +wire [ACCW-1:0] next_acc = acc + STEP[ACCW-1:0]; +wire [3:0] base_from_mode = (mode == 2'd1) ? 4'd4 : 4'd8; + +reg [ACCW-1:0] period = MAX_BASE * STEP; +reg [HW-1:0] extra_px = 0; +reg [SAW-1:0] sync_dly = 1; + +always @(posedge clk) begin : delay_pipeline + reg [4:0] abs_scale; + reg [PW-1:0] product; + reg [HW+4:0] delay_full; + reg [HW+3:0] delay_half; + if(reset) begin + period <= MAX_BASE * STEP; + extra_px <= 0; + sync_dly <= 1; + end + else begin + abs_scale = effective_scale[4] ? -effective_scale : effective_scale; + period <= (base_l == 4'd4) ? 4 * (STEP + effective_scale) : + 8 * (STEP + effective_scale); + product = nactive * abs_scale; + extra_px <= product[PW-1:SLOG]; + delay_full = extra_px * base_l; + delay_half = delay_full[HW+4:1]; + sync_dly <= delay_half >= SDEP ? {SAW{1'b1}} : + (delay_half == 0 ? {{SAW-1{1'b0}}, 1'b1} : delay_half[SAW-1:0]); + end +end + +wire bypass = reset | ~enable_l; +wire hs_pos = hs_in & ~hs_l; +wire push = ce_in & de_in & ~bypass; +wire pop = ce_slow & started & (rcnt < wcnt); + +assign ce_out = bypass ? ce_in : ce_slow; +assign hs_out = bypass ? hs_b : hs_dly; +assign vs_out = bypass ? vs_b : vs_dly; +assign de_out = bypass ? de_l : fifo_de; +assign dout = bypass ? din_l : fifo_dout; + +always @(posedge clk) begin + if(reset) begin + hs_l <= 0; + ce_slow <= 0; + wptr <= 0; + wcnt <= 0; + nactive <= 0; + acc <= 0; + enable_l <= 0; + scale_l <= 0; + base_l <= MAX_BASE; + din_l <= 0; + de_l <= 0; + hs_b <= 0; + vs_b <= 0; + end + else begin + hs_l <= hs_in; + ce_slow <= 0; + if(hs_pos) begin + acc <= 0; + end + else if(next_acc >= period) begin + acc <= next_acc - period; + ce_slow <= 1; + end + else begin + acc <= next_acc; + end + + // The current source line feeds the re-timed FIFO. Latching the control + // and source cadence at hsync prevents a live update from tearing a line. + if(hs_pos) begin + wptr <= 0; + wcnt <= 0; + enable_l <= enable; + scale_l <= scale; + base_l <= base_from_mode; + if(wcnt != 0) nactive <= wcnt; + end + else if(push) begin + pixel_mem[wptr] <= din; + wptr <= wptr + 1'd1; + wcnt <= wcnt + 1'd1; + end + + if(ce_in) begin + din_l <= din; + de_l <= de_in; + hs_b <= hs_in; + vs_b <= vs_in; + end + end +end + +always @(posedge clk) begin + if(reset) begin + mem_rd <= 0; + rptr <= 0; + rcnt <= 0; + started <= 0; + fifo_de <= 0; + fifo_dout <= 0; + sync_wp <= 0; + hs_dly <= 0; + vs_dly <= 0; + end + else begin + // M10K read is registered. With the 54 MHz Menu cadence, the fastest + // permitted output (480i, -8) is one pixel per three clocks, so data + // settles before every pop. + mem_rd <= pixel_mem[rptr]; + if(hs_pos) begin + rptr <= 0; + rcnt <= 0; + started <= 0; + fifo_de <= 0; + fifo_dout <= 0; + end + else begin + // Shrink needs the truncated fractional lead (extra_px floors + // nactive*|scale|/64) plus one pixel so the reader never pops a + // slot the cycle after its push, before the registered RAM read + // has settled: odd scales otherwise corrupt the line tail. + if(!started && wcnt > (effective_scale[4] ? extra_px + 1'd1 : {HW{1'b0}})) + started <= 1; + if(ce_slow) begin + fifo_de <= pop; + if(pop) begin + fifo_dout <= mem_rd; + rptr <= rptr + 1'd1; + rcnt <= rcnt + 1'd1; + end + else begin + fifo_dout <= 0; + end + end + end + + sync_mem[sync_wp] <= {hs_in, vs_in}; + sync_wp <= sync_wp + 1'd1; + {hs_dly, vs_dly} <= sync_mem[sync_wp - sync_dly]; + end +end + +endmodule diff --git a/rtl/zaparoo_hsize_map.sv b/rtl/zaparoo_hsize_map.sv new file mode 100644 index 0000000..2495c89 --- /dev/null +++ b/rtl/zaparoo_hsize_map.sv @@ -0,0 +1,22 @@ +// Maps the saved analog H-size setting onto the zaparoo_hretime controls. +// +// h_size is the persisted UI value, -8..+2 (reader-clamped): steps of 1/64 +// of the nominal pixel period. 0 means unity and turns the retimer off +// entirely (bypass). hretime's scale input is zero-skip (-8..-1 direct, +// 0..+7 mean effective +1..+8), so positive UI values shift down by one. +// +// 480i (mode 1) always bypasses: effective size is forced to zero while the +// saved progressive value is preserved upstream (reader/TOML keep it). +module zaparoo_hsize_map ( + input wire [1:0] mode, + input wire signed [7:0] h_size, + output wire enable, + output wire signed [3:0] scale +); + +assign enable = (h_size != 8'sd0) && (mode != 2'd1); + +wire signed [7:0] mapped = h_size[7] ? h_size : h_size - 8'sd1; +assign scale = mapped[3:0]; + +endmodule diff --git a/rtl/zaparoo_pixel_enable.sv b/rtl/zaparoo_pixel_enable.sv index 45418eb..e83ad9e 100644 --- a/rtl/zaparoo_pixel_enable.sv +++ b/rtl/zaparoo_pixel_enable.sv @@ -7,15 +7,16 @@ module zaparoo_pixel_enable ( input wire [1:0] mode, output reg ce_pix ); - reg [1:0] ce_div; + // 54 MHz master: /8 = 6.75 MHz progressive, /4 = 13.5 MHz 480i. + reg [2:0] ce_div; always @(posedge clk) begin if (reset) begin - ce_div <= 2'd0; + ce_div <= 3'd0; ce_pix <= 1'b0; end else begin - ce_div <= ce_div + 2'd1; - ce_pix <= (mode == 2'd1) ? ce_div[0] : (ce_div == 2'd0); + ce_div <= ce_div + 3'd1; + ce_pix <= (mode == 2'd1) ? (ce_div[1:0] == 2'd0) : (ce_div == 3'd0); end end endmodule diff --git a/sys/emu_ports.vh b/sys/emu_ports.vh index ad59e00..8df6649 100644 --- a/sys/emu_ports.vh +++ b/sys/emu_ports.vh @@ -1,153 +1,158 @@ -//Master input clock -input CLK_50M, - -//Async reset from top-level module. -//Can be used as initial reset. -input RESET, - -//Must be passed to hps_io module -inout [45:0] HPS_BUS, - -//Base video clock. Usually equals to CLK_SYS. -output CLK_VIDEO, - -//Multiple resolutions are supported using different CE_PIXEL rates. -//Must be based on CLK_VIDEO -output CE_PIXEL, - -//Video aspect ratio for HDMI. Most retro systems have ratio 4:3. -//if VIDEO_ARX[12] or VIDEO_ARY[12] is set then [11:0] contains scaled size instead of aspect ratio. -output [12:0] VIDEO_ARX, -output [12:0] VIDEO_ARY, - -output [7:0] VGA_R, -output [7:0] VGA_G, -output [7:0] VGA_B, -output VGA_HS, -output VGA_VS, -output VGA_DE, // = ~(VBlank | HBlank) -output VGA_F1, -output [1:0] VGA_SL, -output VGA_SCALER, // Force VGA scaler -output VGA_DISABLE, // analog out is off - -input [11:0] HDMI_WIDTH, -input [11:0] HDMI_HEIGHT, -output HDMI_FREEZE, -output HDMI_BLACKOUT, -output HDMI_BOB_DEINT, - -`ifdef MISTER_FB -// Use framebuffer in DDRAM -// FB_FORMAT: -// [2:0] : 011=8bpp(palette) 100=16bpp 101=24bpp 110=32bpp -// [3] : 0=16bits 565 1=16bits 1555 -// [4] : 0=RGB 1=BGR (for 16/24/32 modes) -// -// FB_STRIDE either 0 (rounded to 256 bytes) or multiple of pixel size (in bytes) -output FB_EN, -output [4:0] FB_FORMAT, -output [11:0] FB_WIDTH, -output [11:0] FB_HEIGHT, -output [31:0] FB_BASE, -output [13:0] FB_STRIDE, -input FB_VBL, -input FB_LL, -output FB_FORCE_BLANK, - -`ifdef MISTER_FB_PALETTE -// Palette control for 8bit modes. -// Ignored for other video modes. -output FB_PAL_CLK, -output [7:0] FB_PAL_ADDR, -output [23:0] FB_PAL_DOUT, -input [23:0] FB_PAL_DIN, -output FB_PAL_WR, -`endif -`endif - -output LED_USER, // 1 - ON, 0 - OFF. - -// b[1]: 0 - LED status is system status OR'd with b[0] -// 1 - LED status is controled solely by b[0] -// hint: supply 2'b00 to let the system control the LED. -output [1:0] LED_POWER, -output [1:0] LED_DISK, - -// I/O board button press simulation (active high) -// b[1]: user button -// b[0]: osd button -output [1:0] BUTTONS, - -input CLK_AUDIO, // 24.576 MHz -output [15:0] AUDIO_L, -output [15:0] AUDIO_R, -output AUDIO_S, // 1 - signed audio samples, 0 - unsigned -output [1:0] AUDIO_MIX, // 0 - no mix, 1 - 25%, 2 - 50%, 3 - 100% (mono) - -//ADC -inout [3:0] ADC_BUS, - -//SD-SPI -output SD_SCK, -output SD_MOSI, -input SD_MISO, -output SD_CS, -input SD_CD, - -//High latency DDR3 RAM interface -//Use for non-critical time purposes -output DDRAM_CLK, -input DDRAM_BUSY, -output [7:0] DDRAM_BURSTCNT, -output [28:0] DDRAM_ADDR, -input [63:0] DDRAM_DOUT, -input DDRAM_DOUT_READY, -output DDRAM_RD, -output [63:0] DDRAM_DIN, -output [7:0] DDRAM_BE, -output DDRAM_WE, - -//SDRAM interface with lower latency -output SDRAM_CLK, -output SDRAM_CKE, -output [12:0] SDRAM_A, -output [1:0] SDRAM_BA, -inout [15:0] SDRAM_DQ, -output SDRAM_DQML, -output SDRAM_DQMH, -output SDRAM_nCS, -output SDRAM_nCAS, -output SDRAM_nRAS, -output SDRAM_nWE, - -`ifdef MISTER_DUAL_SDRAM -//Secondary SDRAM -//Set all output SDRAM_* signals to Z ASAP if SDRAM2_EN is 0 -input SDRAM2_EN, -output SDRAM2_CLK, -output [12:0] SDRAM2_A, -output [1:0] SDRAM2_BA, -inout [15:0] SDRAM2_DQ, -output SDRAM2_nCS, -output SDRAM2_nCAS, -output SDRAM2_nRAS, -output SDRAM2_nWE, -`endif - -input UART_CTS, -output UART_RTS, -input UART_RXD, -output UART_TXD, -output UART_DTR, -input UART_DSR, - -// Open-drain User port. -// 0 - D+/RX -// 1 - D-/TX -// 2..6 - USR2..USR6 -// Set USER_OUT to 1 to read from USER_IN. -input [6:0] USER_IN, -output [6:0] USER_OUT, - -input OSD_STATUS +//Master input clock +input CLK_50M, + +//Async reset from top-level module. +//Can be used as initial reset. +input RESET, + +//Must be passed to hps_io module +inout [45:0] HPS_BUS, + +//Base video clock. Usually equals to CLK_SYS. +output CLK_VIDEO, + +//Multiple resolutions are supported using different CE_PIXEL rates. +//Must be based on CLK_VIDEO +output CE_PIXEL, + +//Video aspect ratio for HDMI. Most retro systems have ratio 4:3. +//if VIDEO_ARX[12] or VIDEO_ARY[12] is set then [11:0] contains scaled size instead of aspect ratio. +output [12:0] VIDEO_ARX, +output [12:0] VIDEO_ARY, + +output [7:0] VGA_R, +output [7:0] VGA_G, +output [7:0] VGA_B, +output VGA_HS, +output VGA_VS, +output VGA_DE, // = ~(VBlank | HBlank) +output VGA_F1, +output [1:0] VGA_SL, +output VGA_SCALER, // Force VGA scaler +output VGA_DISABLE, // analog out is off + +input [11:0] HDMI_WIDTH, +input [11:0] HDMI_HEIGHT, +output HDMI_FREEZE, +output HDMI_BLACKOUT, +output HDMI_BOB_DEINT, + +`ifdef MISTER_FB +// Use framebuffer in DDRAM +// FB_FORMAT: +// [2:0] : 011=8bpp(palette) 100=16bpp 101=24bpp 110=32bpp +// [3] : 0=16bits 565 1=16bits 1555 +// [4] : 0=RGB 1=BGR (for 16/24/32 modes) +// +// FB_STRIDE either 0 (rounded to 256 bytes) or multiple of pixel size (in bytes) +output FB_EN, +output [4:0] FB_FORMAT, +output [11:0] FB_WIDTH, +output [11:0] FB_HEIGHT, +output [31:0] FB_BASE, +output [13:0] FB_STRIDE, +input FB_VBL, +input FB_LL, +output FB_FORCE_BLANK, + +`ifdef MISTER_FB_PALETTE +// Palette control for 8bit modes. +// Ignored for other video modes. +output FB_PAL_CLK, +output [7:0] FB_PAL_ADDR, +output [23:0] FB_PAL_DOUT, +input [23:0] FB_PAL_DIN, +output FB_PAL_WR, +`endif +`endif + +output LED_USER, // 1 - ON, 0 - OFF. + +// b[1]: 0 - LED status is system status OR'd with b[0] +// 1 - LED status is controled solely by b[0] +// hint: supply 2'b00 to let the system control the LED. +output [1:0] LED_POWER, +output [1:0] LED_DISK, + +// I/O board button press simulation (active high) +// b[1]: user button +// b[0]: osd button +output [1:0] BUTTONS, + +input CLK_AUDIO, // 24.576 MHz +output [15:0] AUDIO_L, +output [15:0] AUDIO_R, +output AUDIO_S, // 1 - signed audio samples, 0 - unsigned +output [1:0] AUDIO_MIX, // 0 - no mix, 1 - 25%, 2 - 50%, 3 - 100% (mono) + +//ADC +inout [3:0] ADC_BUS, + +//SD-SPI +output SD_SCK, +output SD_MOSI, +input SD_MISO, +output SD_CS, +input SD_CD, + +//High latency DDR3 RAM interface +//Use for non-critical time purposes +output DDRAM_CLK, +input DDRAM_BUSY, +output [7:0] DDRAM_BURSTCNT, +output [28:0] DDRAM_ADDR, +input [63:0] DDRAM_DOUT, +input DDRAM_DOUT_READY, +output DDRAM_RD, +output [63:0] DDRAM_DIN, +output [7:0] DDRAM_BE, +output DDRAM_WE, + +//SDRAM interface with lower latency +output SDRAM_CLK, +output SDRAM_CKE, +output [12:0] SDRAM_A, +output [1:0] SDRAM_BA, +inout [15:0] SDRAM_DQ, +output SDRAM_DQML, +output SDRAM_DQMH, +output SDRAM_nCS, +output SDRAM_nCAS, +output SDRAM_nRAS, +output SDRAM_nWE, + +`ifdef MISTER_DUAL_SDRAM +//Secondary SDRAM +//Set all output SDRAM_* signals to Z ASAP if SDRAM2_EN is 0 +input SDRAM2_EN, +output SDRAM2_CLK, +output [12:0] SDRAM2_A, +output [1:0] SDRAM2_BA, +inout [15:0] SDRAM2_DQ, +output SDRAM2_nCS, +output SDRAM2_nCAS, +output SDRAM2_nRAS, +output SDRAM2_nWE, +`endif + +input UART_CTS, +output UART_RTS, +input UART_RXD, +output UART_TXD, +output UART_DTR, +input UART_DSR, + +// Open-drain User port. +// 0 - D+/RX +// 1 - D-/TX +// 2..6 - USR2..USR6 +// Set USER_OUT to 1 to read from USER_IN. +input [6:0] USER_IN, +output [6:0] USER_OUT, + +// Menu-fork sideband: analog H-size retimer controls (CLK_VIDEO domain, +// quasi-static). Consumed by sys_top's zaparoo_hretime after the VGA OSD. +output CRT_HSIZE_EN, +output signed [3:0] CRT_HSIZE_SCALE, + +input OSD_STATUS diff --git a/sys/osd.v b/sys/osd.v index 724ab95..7bbfc4d 100644 --- a/sys/osd.v +++ b/sys/osd.v @@ -22,6 +22,13 @@ module osd ); parameter OSD_COLOR = 3'd4; +// Nonzero: fixed video clocks per OSD dot, skipping the DE-width +// measurement. The measurement picks floor(active_width/512), which at the +// Menu core's 54 MHz video clock lands on 5 clocks against 8-clock pixels: +// the 5:8 beat makes glyph strokes alternate width. Pinning the analog +// instance to 4 restores the 27 MHz look (half a progressive pixel, one +// 480i pixel, per OSD dot). +parameter FIXED_PIXSZ = 0; localparam OSD_WIDTH = 12'd256; localparam OSD_HEIGHT = 12'd64; @@ -115,7 +122,8 @@ always @(posedge clk_video) begin if(~deD && de_in) cnt <= 0; if(deD && ~de_in) begin - pixsz <= (((cnt+1'b1) >> (9-rot[0])) > 1) ? (((cnt+1'b1) >> (9-rot[0])) - 1'd1) : 22'd0; + pixsz <= (FIXED_PIXSZ != 0) ? FIXED_PIXSZ - 1 : + (((cnt+1'b1) >> (9-rot[0])) > 1) ? (((cnt+1'b1) >> (9-rot[0])) - 1'd1) : 22'd0; pixcnt <= 0; end end diff --git a/sys/sys_top.v b/sys/sys_top.v index ca94777..de730a5 100644 --- a/sys/sys_top.v +++ b/sys/sys_top.v @@ -1480,9 +1480,11 @@ scanlines #(0) VGA_scanlines .ce_out(vga_ce_sl) ); -wire [23:0] vga_data_osd; -wire vga_vs_osd, vga_hs_osd, vga_de_osd; -osd vga_osd +wire [23:0] vga_data_osd0; +wire vga_vs_osd0, vga_hs_osd0, vga_de_osd0; +// Pin the OSD dot width at the 54 MHz clock (see FIXED_PIXSZ in osd.v); +// the HDMI instance keeps measuring, its scaled stream is 1 clk/pixel. +osd #(.FIXED_PIXSZ(4)) vga_osd ( .clk_sys(clk_sys), @@ -1497,6 +1499,46 @@ osd vga_osd .vs_in(vga_vs_sl), .de_in(vga_de_sl), + .dout(vga_data_osd0), + .hs_out(vga_hs_osd0), + .vs_out(vga_vs_osd0), + .de_out(vga_de_osd0) +); + +// The retimer samples at a constant 13.5 MHz (clk_vid/4): one 480i pixel, +// or half a progressive pixel = one OSD dot (osd.v FIXED_PIXSZ). Sampling +// at the OSD dot rate carries the OSD's half-pixel glyph strokes through +// the retimer intact; a pixel-rate CE keeps only one of the two dots per +// pixel and decimates the OSD font. Dot and pixel grids are phase-stable +// against this free-running divider, so each dot is sampled exactly once. +reg [1:0] vga_ce_dot_div = 0; +always @(posedge clk_vid) vga_ce_dot_div <= vga_ce_dot_div + 1'd1; +wire vga_ce_dot = (vga_ce_dot_div == 2'd0); + +// Menu-fork analog H-size retimer (rtl/zaparoo_hretime.sv). Analog branch +// only: csync/YC/RGB and direct video below all consume its outputs under +// the original vga_*_osd names; the HDMI/ascal tap is upstream of the OSD +// and is untouched. Controls come from the emu sideband (CLK_VIDEO domain, +// quasi-static; hretime latches them per line at HSYNC). +wire crt_hsize_en; +wire signed [3:0] crt_hsize_scale; +wire [23:0] vga_data_osd; +wire vga_vs_osd, vga_hs_osd, vga_de_osd; +zaparoo_hretime vga_hretime +( + .clk(clk_vid), + .reset(reset), + .ce_in(vga_ce_dot), + .enable(crt_hsize_en), + // Base 4 clocks per sample in every mode (the proven 480i cadence); + // 480i gating happens upstream in zaparoo_hsize_map. + .mode(2'd1), + .scale(crt_hsize_scale), + .din(vga_data_osd0), + .hs_in(vga_hs_osd0), + .vs_in(vga_vs_osd0), + .de_in(vga_de_osd0), + .ce_out(), .dout(vga_data_osd), .hs_out(vga_hs_osd), .vs_out(vga_vs_osd), @@ -1852,6 +1894,8 @@ emu emu .VGA_DE(de_emu), .VGA_F1(f1), .VGA_SCALER(vga_force_scaler), + .CRT_HSIZE_EN(crt_hsize_en), + .CRT_HSIZE_SCALE(crt_hsize_scale), `ifndef MISTER_DUAL_SDRAM .VGA_DISABLE(VGA_DISABLE), diff --git a/tb/hretime_480i_tb.sv b/tb/hretime_480i_tb.sv new file mode 100644 index 0000000..c975104 --- /dev/null +++ b/tb/hretime_480i_tb.sv @@ -0,0 +1,119 @@ +// Self-checking 480i /4 retimer test at the Menu's 54 MHz master rate. +// -8 shrink must retain all 720 pixels and never emit faster than one pixel +// every three master clocks, leaving time for a registered M10K read. +`timescale 1ns/1ps + +module hretime_480i_tb; + +localparam integer H_ACTIVE = 720; +localparam integer H_FP = 19; +localparam integer H_SYNC = 62; +localparam integer H_BP = 57; +localparam integer H_TOTAL = H_ACTIVE + H_FP + H_SYNC + H_BP; + +reg clk = 0; +always #9.2593 clk = ~clk; // 54 MHz +reg reset = 1; +reg [1:0] div = 0; +reg ce_in = 0; +reg [9:0] hcount = 0; +reg enable = 0; + +always @(posedge clk) begin + if(reset) begin + div <= 0; + ce_in <= 0; + hcount <= 0; + end + else begin + ce_in <= (div == 2'd3); + div <= div + 1'd1; + if(div == 2'd3) hcount <= (hcount == H_TOTAL - 1) ? 0 : hcount + 1'd1; + end +end + +wire hs_in = hcount >= H_ACTIVE + H_FP && hcount < H_ACTIVE + H_FP + H_SYNC; +wire de_in = hcount < H_ACTIVE; +wire ce_out, hs_out, de_out; +wire [23:0] dout; +zaparoo_hretime dut +( + .clk(clk), .reset(reset), .ce_in(ce_in), .enable(enable), .mode(2'd1), .scale(-4'sd8), + .din({14'd0, hcount}), .hs_in(hs_in), .vs_in(1'b0), .de_in(de_in), + .ce_out(ce_out), .dout(dout), .hs_out(hs_out), .vs_out(), .de_out(de_out) +); + +integer ticks = 0; +integer got = 0; +integer first_tick = 0; +integer last_tick = 0; +integer errors = 0; +reg checking = 0; +reg ce_out_d = 0; +reg hs_out_d = 0; + +always @(posedge clk) begin + ticks <= ticks + 1; + ce_out_d <= ce_out; + hs_out_d <= hs_out; + if(ce_out_d && de_out) begin + if(got == 0) first_tick <= ticks; + if(checking && dout[9:0] !== got[9:0]) begin + $display("FAIL: 480i pixel %0d got %03x", got, dout[9:0]); + errors <= errors + 1; + end + if(checking && got != 0 && ticks - last_tick < 3) begin + $display("FAIL: 480i spacing %0d clocks, need >= 3", ticks - last_tick); + errors <= errors + 1; + end + if(checking && hs_out) begin + $display("FAIL: 480i active pixel during HS"); + errors <= errors + 1; + end + last_tick <= ticks; + got <= got + 1; + end + if(hs_out && !hs_out_d) begin + if(checking && got != 0) begin + if(got != H_ACTIVE) begin + $display("FAIL: 480i emitted %0d pixels, expected %0d", got, H_ACTIVE); + errors <= errors + 1; + end + if(last_tick - first_tick < 2512 || last_tick - first_tick > 2521) begin + $display("FAIL: 480i active span %0d, expected about 2516", last_tick - first_tick); + errors <= errors + 1; + end + end + got <= 0; + first_tick <= 0; + last_tick <= 0; + end +end + +task wait_lines(input integer n); + integer i; + begin + for(i = 0; i < n; i = i + 1) @(posedge hs_out); + end +endtask + +initial begin + repeat(8) @(posedge clk); + reset = 0; + wait_lines(3); + enable = 1; + wait_lines(6); + checking = 1; + wait_lines(3); + checking = 0; + if(errors == 0) $display("PASS: 480i -8 hretime preserves pixels with safe M10K cadence"); + else $fatal(1, "%0d 480i hretime check(s) failed", errors); + $finish; +end + +initial begin + #2_000_000_000; + $fatal(1, "480i hretime timeout"); +end + +endmodule diff --git a/tb/hretime_tb.sv b/tb/hretime_tb.sv new file mode 100644 index 0000000..0fbdc48 --- /dev/null +++ b/tb/hretime_tb.sv @@ -0,0 +1,171 @@ +// Self-checking Menu horizontal re-timer test at the wired cadence: the +// retimer samples at CLK_VIDEO / 4 (13.5 MHz), so a progressive NTSC line +// is 704 half-pixel dots (the OSD dot rate) with the narrow NTSC porches, +// the binding worst case (PAL porches are wider at the same rate). +// The positive range stops at effective +2 so all dots drain before HS. +// Sweeps every UI step (effective -8..-1, +1, +2) plus the bypass path, +// asserting dot identity, emitted count, active span and quiet HSYNC. +`timescale 1ns/1ps + +module hretime_tb; + +localparam integer H_ACTIVE = 704; // dots: 352 px x 2 +localparam integer H_FP = 24; +localparam integer H_SYNC = 64; +localparam integer H_BP = 66; +localparam integer H_TOTAL = H_ACTIVE + H_FP + H_SYNC + H_BP; +localparam integer V_ACTIVE = 3; +localparam integer V_TOTAL = 4; + +reg clk = 0; +always #9.2593 clk = ~clk; + +reg reset = 1; +reg [1:0] div = 0; +reg ce_in = 0; +reg [9:0] hcount = 0; +reg [3:0] vcount = 0; +reg enable = 0; +reg signed [3:0] scale = 0; + +always @(posedge clk) begin + if(reset) begin + div <= 0; + ce_in <= 0; + hcount <= 0; + vcount <= 0; + end + else begin + ce_in <= (div == 2'd3); + div <= div + 1'd1; + if(div == 2'd3) begin + if(hcount == H_TOTAL - 1) begin + hcount <= 0; + vcount <= (vcount == V_TOTAL - 1) ? 0 : vcount + 1'd1; + end + else hcount <= hcount + 1'd1; + end + end +end + +wire hs_in = hcount >= H_ACTIVE + H_FP && hcount < H_ACTIVE + H_FP + H_SYNC; +wire vs_in = vcount == V_ACTIVE && hcount < H_SYNC; +wire de_in = hcount < H_ACTIVE && vcount < V_ACTIVE; +wire [23:0] din = {16'd0, hcount[7:0]}; + +wire ce_out, hs_out, vs_out, de_out; +wire [23:0] dout; +zaparoo_hretime dut +( + .clk(clk), .reset(reset), .ce_in(ce_in), .enable(enable), .mode(2'd1), .scale(scale), + .din(din), .hs_in(hs_in), .vs_in(vs_in), .de_in(de_in), + .ce_out(ce_out), .dout(dout), .hs_out(hs_out), .vs_out(vs_out), .de_out(de_out) +); + +integer ticks = 0; +integer got = 0; +integer errors = 0; +integer first_tick = 0; +integer last_tick = 0; +integer expected_scale = 0; +reg checking = 0; +reg ce_out_d = 0; +reg hs_out_d = 0; + +always @(posedge clk) begin + ticks <= ticks + 1; + ce_out_d <= ce_out; + hs_out_d <= hs_out; + + if(ce_out_d && de_out) begin + if(got == 0) first_tick <= ticks; + if(checking && expected_scale == -8 && got != 0 && ticks - last_tick < 3) begin + $display("FAIL: -8 spacing %0d, registered RAM read needs >= 3 clocks", ticks - last_tick); + errors <= errors + 1; + end + last_tick <= ticks; + if(checking && dout[7:0] !== got[7:0]) begin + $display("FAIL: scale %0d pixel %0d got %02x", expected_scale, got, dout[7:0]); + errors <= errors + 1; + end + if(checking && hs_out) begin + $display("FAIL: scale %0d has active pixels during HS", expected_scale); + errors <= errors + 1; + end + got <= got + 1; + end + + if(hs_out && !hs_out_d) begin + if(checking && got != 0) begin + integer expected_span; + integer span; + expected_span = ((H_ACTIVE - 1) * 4 * (64 + expected_scale)) / 64; + span = last_tick - first_tick; + if(got != H_ACTIVE) begin + $display("FAIL: scale %0d emitted %0d pixels, expected %0d", expected_scale, got, H_ACTIVE); + errors <= errors + 1; + end + if(span < expected_span - 5 || span > expected_span + 5) begin + $display("FAIL: scale %0d active span %0d, expected %0d", expected_scale, span, expected_span); + errors <= errors + 1; + end + end + got <= 0; + first_tick <= 0; + last_tick <= 0; + end +end + +task wait_lines(input integer n); + integer i; + begin + for(i = 0; i < n; i = i + 1) @(posedge hs_out); + end +endtask + +task exercise(input signed [3:0] requested_scale, input integer effective_scale); + begin + scale = requested_scale; + expected_scale = effective_scale; + wait_lines(5); // control latches at HS, then the FIFO/centering pipeline settles + checking = 1; + wait_lines(3); + checking = 0; + end +endtask + +initial begin + repeat(8) @(posedge clk); + reset = 0; + wait_lines(3); + enable = 1; + exercise(-8, -8); + exercise(-7, -7); + exercise(-6, -6); + exercise(-5, -5); + exercise(-4, -4); + exercise(-3, -3); + exercise(-2, -2); + exercise(-1, -1); + exercise(0, 1); + exercise(1, 2); + // Bypass (UI size 0): latched-skew-free passthrough, identity and span + // at exact unity cadence. + enable = 0; + expected_scale = 0; + wait_lines(5); + checking = 1; + wait_lines(3); + checking = 0; + wait_lines(3); + if(errors == 0) $display("PASS: Menu hretime preserves pixels and safe analogue timing"); + else $fatal(1, "%0d hretime check(s) failed", errors); + $finish; +end + +initial begin + #2_000_000_000; + $fatal(1, "hretime timeout"); +end + +endmodule diff --git a/tb/hsize_map_tb.sv b/tb/hsize_map_tb.sv new file mode 100644 index 0000000..9afcf3d --- /dev/null +++ b/tb/hsize_map_tb.sv @@ -0,0 +1,50 @@ +// Exhaustive check of the H-size -> retimer control mapping: every UI step +// (-8..+2) against every mode. This is the only place the "480i bypasses +// even with a nonzero stored size" rule is asserted in simulation. +`timescale 1ns/1ps + +module hsize_map_tb; + +reg [1:0] mode = 0; +reg signed [7:0] h_size = 0; +wire enable; +wire signed [3:0] scale; + +zaparoo_hsize_map dut (.mode(mode), .h_size(h_size), .enable(enable), .scale(scale)); + +integer errors = 0; +integer m, s; +reg exp_en; +reg signed [3:0] exp_scale; + +initial begin + for (m = 0; m < 4; m = m + 1) begin + for (s = -8; s <= 2; s = s + 1) begin + mode = m[1:0]; + h_size = s[7:0]; + #1; + exp_en = (s != 0) && (m != 1); + if (enable !== exp_en) begin + errors = errors + 1; + $display("FAIL: mode %0d size %0d enable %b, expected %b", m, s, enable, exp_en); + end + if (exp_en) begin + // hretime zero-skip: effective = scale + (scale >= 0 ? 1 : 0) + exp_scale = (s < 0) ? s[3:0] : (s - 1); + if (scale !== exp_scale) begin + errors = errors + 1; + $display("FAIL: mode %0d size %0d scale %0d, expected %0d", m, s, scale, exp_scale); + end + end + end + end + if (errors == 0) $display("PASS: hsize map covers every UI step, unity off, 480i bypass"); + else $fatal(1, "%0d hsize map check(s) failed", errors); + $finish; +end + +initial begin + #100000; $fatal(1, "hsize map test timeout"); +end + +endmodule diff --git a/tb/native_video_reader_tb.sv b/tb/native_video_reader_tb.sv index 8516fb5..691099b 100644 --- a/tb/native_video_reader_tb.sv +++ b/tb/native_video_reader_tb.sv @@ -15,25 +15,30 @@ // - DDR timeout: unresponsive bus drops active instead of latching stale // // Run: tb/run.sh +// +// Split across two compiles so each stays inside the repo's 600 s per-test +// wall clock at the 54 MHz video clock (about 8 s of CPU per simulated +// frame on CI): the default build runs phases 0-5, and -DREADER_TB_PART2 +// runs phase 0 plus phases 6-9 from a cold reset. `timescale 1ns/1ps module native_video_reader_tb; reg clk_sys = 0; always #5 clk_sys = ~clk_sys; // 100 MHz DDR-side -reg clk_vid = 0; always #18.5185 clk_vid = ~clk_vid; // 27 MHz +reg clk_vid = 0; always #9.2593 clk_vid = ~clk_vid; // 54 MHz reg reset = 1; wire [1:0] vmode; wire vfield; -// ce_pix divider mirrors menu.sv. -reg [1:0] ce_div = 0; +// ce_pix divider mirrors menu.sv: /8 progressive, /4 480i. +reg [2:0] ce_div = 0; reg ce_pix = 0; always @(posedge clk_vid) begin - if (reset) ce_div <= 2'd0; - else ce_div <= ce_div + 2'd1; - ce_pix <= (vmode == 2'd1) ? ce_div[0] : (ce_div == 2'd0); + if (reset) ce_div <= 3'd0; + else ce_div <= ce_div + 3'd1; + ce_pix <= (vmode == 2'd1) ? (ce_div[1:0] == 2'd0) : (ce_div == 3'd0); end // ---- DDR model ------------------------------------------------------------- @@ -49,6 +54,7 @@ reg [63:0] ddr_dout = 0; reg ddr_dout_ready = 0; reg [63:0] ctrl_q = 64'd0; // {word1, word0} as published by the "writer" +reg [63:0] ctrl_q2 = 64'd0; // beat 2 at 0x08: {reserved word3, word2} reg respond_en = 1; integer req_n = 0; @@ -74,7 +80,8 @@ always @(posedge clk_sys) begin else if (cur_left != 0) begin if (lat != 0) lat <= lat - 1; else begin - ddr_dout <= (cur_addr == CTRL_ADDR) ? ctrl_q : PIX_DATA; + ddr_dout <= (cur_addr == CTRL_ADDR) ? ctrl_q : + (cur_addr == CTRL_ADDR + 29'd1) ? ctrl_q2 : PIX_DATA; ddr_dout_ready <= 1; cur_addr <= cur_addr + 29'd1; cur_left <= cur_left - 1; @@ -144,6 +151,13 @@ task publish(input bit magic, input [3:0] pmode, input signed [7:0] hoff, end endtask +// Publish the interim analog H-size word2 in the second control beat. +task set_word2(input bit magic, input signed [7:0] size); + begin + ctrl_q2 = {32'd0, magic ? 16'h5A52 : 16'hBEEF, 8'h00, size}; + end +endtask + // Verify one whole frame's DDR request sequence: a control poll followed by // nlines line fetches of bpl bursts of blen beats each, line l fetched from // base + src*stride + b*blen, where src = l (progressive) or 2*l + field @@ -163,7 +177,7 @@ task check_frame_fetch(input string tag, input [28:0] base, input integer stride check({tag, " fifo overflow-free"}, dut.reader.line_fifo.overflow_count, 0); $display("info %s fifo peak occupancy: %0d / 1024 words", tag, dut.reader.line_fifo.peak_used); check({tag, " ctrl poll addr"}, req_addr[s], CTRL_ADDR); - check({tag, " ctrl poll burst"}, req_cnt[s], 1); + check({tag, " ctrl poll burst"}, req_cnt[s], 2); check({tag, " requests/frame"}, req_n >= s + 1 + nlines*bpl, 1); for (l = 0; l < nlines; l = l + 1) begin src = intl ? (2*l + fld) : l; @@ -203,9 +217,10 @@ initial begin $display("--- phase 0: no writer ---"); wait_frames(3); check("idle: active low", {31'd0, active}, 0); - check("idle: only ctrl polls", req_cnt[req_n-1], 1); + check("idle: only ctrl polls", req_cnt[req_n-1], 2); check("idle: poll addr", req_addr[req_n-1], CTRL_ADDR); +`ifndef READER_TB_PART2 // Phase 1: v2 writer, mode 0, offsets +5/-3, buffer 0. $display("--- phase 1: v2 mode 0 ---"); publish(1, 4'd0, 8'sd5, -4'sd3, 1, 0); @@ -261,6 +276,9 @@ initial begin wait_frames(2); check_frame_fetch("pal", BUF0_V2, 176, 288, 176, 1, 0, fld_a); +`endif + +`ifdef READER_TB_PART2 // Phase 6: 480i. $display("--- phase 6: v2 mode 1 (480i) ---"); publish(1, 4'd1, 8'sd0, 4'sd0, 13, 0); @@ -312,6 +330,41 @@ initial begin wait_frames(3); check("unknown magic rejected", {31'd0, active}, 0); + // Phase 9: interim word2 H size (analog width stretch). + $display("--- phase 9: word2 h_size ---"); + publish(1, 4'd0, 8'sd0, 4'sd0, 21, 0); + wait (active === 1'b1); + wait_frames(2); + check("h_size default 0", dut.reader.h_size_out, 0); + check("h_size top port 0", dut.vga_h_size, 0); + set_word2(1, 8'sd2); + wait_frames(2); + check("h_size +2 decoded", dut.reader.h_size_out, 2); + check("h_size top port +2", dut.vga_h_size, 2); + set_word2(1, -8'sd8); + wait_frames(2); + check("h_size -8 decoded", dut.reader.h_size_out, -8); + set_word2(1, 8'sd5); + wait_frames(2); + check("h_size +5 clamps to +2", dut.reader.h_size_out, 2); + set_word2(1, -8'sd20); + wait_frames(2); + check("h_size -20 clamps to -8", dut.reader.h_size_out, -8); + set_word2(0, 8'sd2); + wait_frames(2); + check("wrong word2 magic reads 0", dut.reader.h_size_out, 0); + set_word2(1, -8'sd4); + wait_frames(2); + check("h_size follows live writer", dut.reader.h_size_out, -4); + ctrl_q = 64'd0; + wait_frames(2); + check("stopped writer resets h_size", dut.reader.h_size_out, 0); + publish(0, 4'd0, 8'sd0, 4'sd0, 22, 0); + wait_frames(2); + check("no word1 magic keeps h_size 0", dut.reader.h_size_out, 0); + ctrl_q2 = 64'd0; +`endif + if (errors == 0) $display("ALL CHECKS PASSED"); else begin $display("%0d CHECK(S) FAILED", errors); diff --git a/tb/native_video_timing_tb.sv b/tb/native_video_timing_tb.sv index d1ce2d4..efbbe0b 100644 --- a/tb/native_video_timing_tb.sv +++ b/tb/native_video_timing_tb.sv @@ -14,7 +14,7 @@ module native_video_timing_tb; reg clk = 0; -always #18.5185 clk = ~clk; // 27 MHz +always #9.2593 clk = ~clk; // 54 MHz reg reset = 1; @@ -27,13 +27,13 @@ wire field, hsync, vsync, hblank, vblank, de, new_frame, new_line; wire [9:0] hcount; wire [8:0] vcount; -// ce_pix divider mirrors menu.sv: /4 (6.75 MHz) progressive, /2 (13.5 MHz) 480i. -reg [1:0] ce_div = 0; +// ce_pix divider mirrors menu.sv: /8 (6.75 MHz) progressive, /4 (13.5 MHz) 480i. +reg [2:0] ce_div = 0; reg ce_pix = 0; always @(posedge clk) begin - if (reset) ce_div <= 2'd0; - else ce_div <= ce_div + 2'd1; - ce_pix <= (mode == 2'd1) ? ce_div[0] : (ce_div == 2'd0); + if (reset) ce_div <= 3'd0; + else ce_div <= ce_div + 3'd1; + ce_pix <= (mode == 2'd1) ? (ce_div[1:0] == 2'd0) : (ce_div == 3'd0); end native_video_timing dut diff --git a/tb/pixel_enable_tb.sv b/tb/pixel_enable_tb.sv index 892540a..1dab955 100644 --- a/tb/pixel_enable_tb.sv +++ b/tb/pixel_enable_tb.sv @@ -11,19 +11,19 @@ module pixel_enable_tb; // Both progressive modes, interlace, and the reserved-mode fallback. // Assert reset at every divider phase, as PLL lock may disappear mid-frame. for (integer m = 0; m < 4; m = m + 1) begin - for (integer phase = 0; phase < 4; phase = phase + 1) begin + for (integer phase = 0; phase < 8; phase = phase + 1) begin @(negedge clk); reset = 1; mode = m; repeat (5) begin @(posedge clk); #1; assert(ce_pix === 1'b0) else $fatal(1, "pixel enable active during reset"); end @(negedge clk); reset = 0; - for (integer i = 0; i < 8 + phase; i = i + 1) begin + for (integer i = 0; i < 16 + phase; i = i + 1) begin @(posedge clk); if (i == 0) assert(ce_pix === 1'b0) else $fatal(1, "consumer saw stale enable at reset release"); #1; - assert(ce_pix === ((m == 1) ? (i % 2 == 1) : (i % 4 == 0))) + assert(ce_pix === ((m == 1) ? (i % 4 == 0) : (i % 8 == 0))) else $fatal(1, "divider cadence/phase changed: mode=%0d cycle=%0d", m, i); end end @@ -32,6 +32,6 @@ module pixel_enable_tb; $finish; end initial begin - #20000; $fatal(1, "pixel enable test timeout"); + #40000; $fatal(1, "pixel enable test timeout"); end endmodule diff --git a/tb/run.sh b/tb/run.sh index 015ac19..fd910a0 100755 --- a/tb/run.sh +++ b/tb/run.sh @@ -33,3 +33,18 @@ run_test native_video_timing_tb \ run_test native_video_reader_tb \ ../rtl/native_video_timing.sv ../rtl/native_video_reader.sv \ ../rtl/native_video_top.sv dcfifo_sim.sv native_video_reader_tb.sv + +# Part 2 (phases 6-9) of the same testbench; the split keeps each run +# inside the 600 s wall clock at the 54 MHz video clock. +echo "=== native_video_reader_tb (part 2) ===" +iverilog -g2012 -I ../sys -DREADER_TB_PART2=1 -s native_video_reader_tb \ + -o ../test-output/rtl/native_video_reader_tb_part2.vvp \ + ../rtl/native_video_timing.sv ../rtl/native_video_reader.sv \ + ../rtl/native_video_top.sv dcfifo_sim.sv native_video_reader_tb.sv +timeout 600 vvp ../test-output/rtl/native_video_reader_tb_part2.vvp + +run_test hsize_map_tb ../rtl/zaparoo_hsize_map.sv hsize_map_tb.sv + +run_test hretime_tb ../rtl/zaparoo_hretime.sv hretime_tb.sv + +run_test hretime_480i_tb ../rtl/zaparoo_hretime.sv hretime_480i_tb.sv