From 4a7c2d6bb5e0a07e2a4154e2fa4271073cce4deb Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 28 Sep 2026 20:37:02 +0200 Subject: [PATCH 01/11] video: migrate the video master clock to 54 MHz pll_video54 (1350 MHz VCO / 25, exact) replaces the 27 MHz pll_video so the analog H-size retimer gets 8 master clocks per progressive pixel (4 per 480i pixel). Pixel enables become /8 progressive and /4 480i; pixel rates, line lengths and field timing are unchanged, and the full RTL test suite passes with the doubled clock. menu.sdc's async clock group follows the new PLL instance path. Co-Authored-By: Claude Fable 5.1 --- files.qip | 32 +- menu.sdc | 6 +- menu.sv | 878 +++++++++++++++-------------- rtl/pll_video54.qip | 6 + rtl/pll_video54.v | 23 + rtl/pll_video54/pll_video54_0002.v | 86 +++ rtl/zaparoo_pixel_enable.sv | 9 +- tb/native_video_reader_tb.sv | 12 +- tb/native_video_timing_tb.sv | 12 +- tb/pixel_enable_tb.sv | 8 +- 10 files changed, 603 insertions(+), 469 deletions(-) create mode 100644 rtl/pll_video54.qip create mode 100644 rtl/pll_video54.v create mode 100644 rtl/pll_video54/pll_video54_0002.v diff --git a/files.qip b/files.qip index fd608e5..54bf0a6 100644 --- a/files.qip +++ b/files.qip @@ -1,15 +1,17 @@ -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 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_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 5cbcfdb..10fa8f9 100644 --- a/menu.sv +++ b/menu.sv @@ -1,431 +1,447 @@ -//============================================================================ -// -// 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]; - -// 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) -); - -// OSD status comes from the HDMI domain. Main disables OSD before handing -// video to the frontend, so startup stays black without a new bus command. -(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) -reg [1:0] snow_osd_sync = 0; -always @(posedge CLK_VIDEO) begin - if (RESET | ~vid_locked) begin - snow_sample <= 0; - snow_osd_sync <= 0; - end else begin - if (ce_pix) snow_sample <= snow_random[2:0]; - snow_osd_sync <= {snow_osd_sync[0], OSD_STATUS}; - end -end - -// Black handoff, snow behind stock OSD, 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(snow_osd_sync[1]), - .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]; + +// 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) +); +assign CRT_HSIZE_MODE = native_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) +); + +// OSD status comes from the HDMI domain. Main disables OSD before handing +// video to the frontend, so startup stays black without a new bus command. +(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) +reg [1:0] snow_osd_sync = 0; +always @(posedge CLK_VIDEO) begin + if (RESET | ~vid_locked) begin + snow_sample <= 0; + snow_osd_sync <= 0; + end else begin + if (ce_pix) snow_sample <= snow_random[2:0]; + snow_osd_sync <= {snow_osd_sync[0], OSD_STATUS}; + end +end + +// Black handoff, snow behind stock OSD, 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(snow_osd_sync[1]), + .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/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_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/tb/native_video_reader_tb.sv b/tb/native_video_reader_tb.sv index 8516fb5..2927605 100644 --- a/tb/native_video_reader_tb.sv +++ b/tb/native_video_reader_tb.sv @@ -21,19 +21,19 @@ 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 ------------------------------------------------------------- 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 From e744c037fb040d7f6cfb2e25becffcaac97ef279 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 28 Sep 2026 20:54:49 +0200 Subject: [PATCH 02/11] video: harden the H-size retimer and register its testbenches Bypass path now latches HS/VS with data/DE so unity and 480i stay skew-free. Shrink start margin gains one buffered pixel: extra_px floors the fractional lead, and odd scales let the reader pop a slot the cycle after its push, before the registered RAM read settled, corrupting the line tail. The TB now sweeps every UI step (-8..-1, +1, +2) plus bypass, and both retimer TBs run from tb/run.sh (with the hsize map TB). Co-Authored-By: Claude Fable 5.1 --- rtl/zaparoo_hretime.sv | 225 +++++++++++++++++++++++++++++++++++++++++ tb/hretime_480i_tb.sv | 119 ++++++++++++++++++++++ tb/hretime_tb.sv | 170 +++++++++++++++++++++++++++++++ tb/run.sh | 6 ++ 4 files changed, 520 insertions(+) create mode 100644 rtl/zaparoo_hretime.sv create mode 100644 tb/hretime_480i_tb.sv create mode 100644 tb/hretime_tb.sv diff --git a/rtl/zaparoo_hretime.sv b/rtl/zaparoo_hretime.sv new file mode 100644 index 0000000..983df20 --- /dev/null +++ b/rtl/zaparoo_hretime.sv @@ -0,0 +1,225 @@ +// 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 54 MHz Menu source is fixed at eight CLK_VIDEO +// cycles per 240p/PAL pixel and four per 480i pixel. +// +// 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/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..18e5be1 --- /dev/null +++ b/tb/hretime_tb.sv @@ -0,0 +1,170 @@ +// Self-checking Menu horizontal re-timer test. The Menu input is fixed at +// CLK_VIDEO / 8 (54 MHz / 8 = 6.75 MHz) and has the narrow NTSC front porch, +// the binding worst case (PAL porches are wider at the same pixel rate). +// The positive range stops at effective +2 so all pixels drain before HS. +// Sweeps every UI step (effective -8..-1, +1, +2) plus the bypass path, +// asserting pixel identity, emitted count, active span and quiet HSYNC. +`timescale 1ns/1ps + +module hretime_tb; + +localparam integer H_ACTIVE = 352; +localparam integer H_FP = 12; +localparam integer H_SYNC = 32; +localparam integer H_BP = 33; +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 [2: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 == 3'd7); + div <= div + 1'd1; + if(div == 3'd7) 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'd0), .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 != 7) begin + $display("FAIL: -8 progressive spacing %0d, expected 7 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) * 8 * (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/run.sh b/tb/run.sh index 337a1b3..c5d1cca 100755 --- a/tb/run.sh +++ b/tb/run.sh @@ -31,3 +31,9 @@ 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 + +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 From 00db1c7e1f66ed2e2a7559a52e1da2af18a0f98d Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 28 Sep 2026 20:54:49 +0200 Subject: [PATCH 03/11] video: decode the interim word2 H-size control beat The per-vblank poll reads two beats at 0x3A000000: beat 2's low word is [31:16] magic 0x5A52, [7:0] signed h_size, clamped to -8..+2, 0 without the magic or a live word1. Written only by Main; the frontend owns words 0/1 untouched. Two-flop synced out of native_video_top as vga_h_size. Co-Authored-By: Claude Fable 5.1 --- rtl/native_video_reader.sv | 37 +++++++++++++++++++++++++-- rtl/native_video_top.sv | 7 ++++++ tb/native_video_reader_tb.sv | 49 +++++++++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/rtl/native_video_reader.sv b/rtl/native_video_reader.sv index 8c88401..97695ca 100644 --- a/rtl/native_video_reader.sv +++ b/rtl/native_video_reader.sv @@ -8,6 +8,17 @@ // 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 is +// written only by Main (never the frontend, which owns words 0/1), and 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 +61,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 +137,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 +172,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 +197,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 +215,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 +241,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 +285,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 +293,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/tb/native_video_reader_tb.sv b/tb/native_video_reader_tb.sv index 2927605..c1a68a3 100644 --- a/tb/native_video_reader_tb.sv +++ b/tb/native_video_reader_tb.sv @@ -49,6 +49,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 +75,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 +146,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 +172,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,7 +212,7 @@ 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); // Phase 1: v2 writer, mode 0, offsets +5/-3, buffer 0. @@ -312,6 +321,40 @@ 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(3); + 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(3); + check("h_size -8 decoded", dut.reader.h_size_out, -8); + set_word2(1, 8'sd5); + wait_frames(3); + check("h_size +5 clamps to +2", dut.reader.h_size_out, 2); + set_word2(1, -8'sd20); + wait_frames(3); + check("h_size -20 clamps to -8", dut.reader.h_size_out, -8); + set_word2(0, 8'sd2); + wait_frames(3); + check("wrong word2 magic reads 0", dut.reader.h_size_out, 0); + set_word2(1, -8'sd4); + wait_frames(3); + check("h_size follows live writer", dut.reader.h_size_out, -4); + ctrl_q = 64'd0; + wait_frames(3); + check("stopped writer resets h_size", dut.reader.h_size_out, 0); + publish(0, 4'd0, 8'sd0, 4'sd0, 22, 0); + wait_frames(3); + check("no word1 magic keeps h_size 0", dut.reader.h_size_out, 0); + ctrl_q2 = 64'd0; + if (errors == 0) $display("ALL CHECKS PASSED"); else begin $display("%0d CHECK(S) FAILED", errors); From 702c0a4f1c25c352b91c448db6a278f935b6784f Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 28 Sep 2026 20:54:49 +0200 Subject: [PATCH 04/11] video: map the saved H size onto the retimer controls UI 0 = unity = retimer off; positives shift down one for hretime's zero-skip scale; 480i always bypasses while the stored progressive value survives. Exhaustive TB including the 480i-bypass-with-nonzero-size rule. Co-Authored-By: Claude Fable 5.1 --- rtl/zaparoo_hsize_map.sv | 22 ++++++++++++++++++ tb/hsize_map_tb.sv | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 rtl/zaparoo_hsize_map.sv create mode 100644 tb/hsize_map_tb.sv 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/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 From f9093fc6dcf9c85162e5ab183dc03bdd0700cde6 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 28 Sep 2026 20:54:49 +0200 Subject: [PATCH 05/11] video: wire the H-size retimer into the analog output path zaparoo_hretime sits after the VGA OSD, feeding csync/YC/RGB and direct video under the original vga_*_osd names; the pre-OSD HDMI/ascal tap is untouched. A 4-flop CE delay mirrors the OSD's fixed pipeline so CE and data keep the phase scanlines guarantees. Controls ride a new Menu-fork emu sideband (CLK_VIDEO domain, quasi-static). Co-Authored-By: Claude Fable 5.1 --- sys/emu_ports.vh | 312 ++++++++++++++++++++++++----------------------- sys/sys_top.v | 43 ++++++- 2 files changed, 200 insertions(+), 155 deletions(-) diff --git a/sys/emu_ports.vh b/sys/emu_ports.vh index ad59e00..1a07bd6 100644 --- a/sys/emu_ports.vh +++ b/sys/emu_ports.vh @@ -1,153 +1,159 @@ -//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, +output [1:0] CRT_HSIZE_MODE, + +input OSD_STATUS diff --git a/sys/sys_top.v b/sys/sys_top.v index ca94777..02f78c2 100644 --- a/sys/sys_top.v +++ b/sys/sys_top.v @@ -1480,8 +1480,8 @@ 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; +wire [23:0] vga_data_osd0; +wire vga_vs_osd0, vga_hs_osd0, vga_de_osd0; osd vga_osd ( .clk_sys(clk_sys), @@ -1497,6 +1497,42 @@ 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) +); + +// osd is a fixed 4-register pipeline on data and syncs with no CE port; +// delaying the scanlines CE by the same 4 clocks restores at the retimer +// input the CE/data phase that scanlines guarantees at the OSD input. +reg [3:0] vga_ce_osd_sr = 0; +always @(posedge clk_vid) vga_ce_osd_sr <= {vga_ce_osd_sr[2:0], vga_ce_sl}; +wire vga_ce_osd = vga_ce_osd_sr[3]; + +// 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 [1:0] crt_hsize_mode; +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_osd), + .enable(crt_hsize_en), + .mode(crt_hsize_mode), + .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 +1888,9 @@ 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), + .CRT_HSIZE_MODE(crt_hsize_mode), `ifndef MISTER_DUAL_SDRAM .VGA_DISABLE(VGA_DISABLE), From e9310d2f052bea589c66129d1523aebf8c033f64 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 29 Sep 2026 00:13:45 +0200 Subject: [PATCH 06/11] tb: split the reader testbench to fit CI's per-test wall clock At the 54 MHz video clock a simulated frame costs about 8 s of CI CPU, and phases 0-9 together blew the 600 s timeout three checks from the end. The default compile now runs phases 0-5 and -DREADER_TB_PART2 runs phase 0 plus phases 6-9 from a cold reset (the first publish after a writerless reset is trusted, so part 2 needs no earlier phases). Phase 9 waits drop from three frames to two: word2 latches at the next per-frame poll. Co-Authored-By: Claude Fable 5.1 --- tb/native_video_reader_tb.sv | 26 ++++++++++++++++++-------- tb/run.sh | 9 +++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/tb/native_video_reader_tb.sv b/tb/native_video_reader_tb.sv index c1a68a3..691099b 100644 --- a/tb/native_video_reader_tb.sv +++ b/tb/native_video_reader_tb.sv @@ -15,6 +15,11 @@ // - 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 @@ -215,6 +220,7 @@ initial begin 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); @@ -270,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); @@ -329,31 +338,32 @@ initial begin 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(3); + 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(3); + wait_frames(2); check("h_size -8 decoded", dut.reader.h_size_out, -8); set_word2(1, 8'sd5); - wait_frames(3); + wait_frames(2); check("h_size +5 clamps to +2", dut.reader.h_size_out, 2); set_word2(1, -8'sd20); - wait_frames(3); + wait_frames(2); check("h_size -20 clamps to -8", dut.reader.h_size_out, -8); set_word2(0, 8'sd2); - wait_frames(3); + wait_frames(2); check("wrong word2 magic reads 0", dut.reader.h_size_out, 0); set_word2(1, -8'sd4); - wait_frames(3); + wait_frames(2); check("h_size follows live writer", dut.reader.h_size_out, -4); ctrl_q = 64'd0; - wait_frames(3); + 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(3); + 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 diff --git a/tb/run.sh b/tb/run.sh index c5d1cca..b420a16 100755 --- a/tb/run.sh +++ b/tb/run.sh @@ -32,6 +32,15 @@ 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 From bb4dba3788bf1a18f74bca299b7570ce77d62bff Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 29 Sep 2026 00:34:47 +0200 Subject: [PATCH 07/11] osd: pin the analog OSD dot width at the 54 MHz video clock The OSD derives its dot width as floor(DE_width/512) in raw clocks: 2 of a 4-clock pixel at 27 MHz, but 5 of an 8-clock pixel at 54 MHz. The 5:8 beat resamples the glyphs, making strokes alternate width on the CRT. A new FIXED_PIXSZ parameter (default 0 = keep measuring, HDMI instance unchanged) pins the analog instance to 4 clocks per dot, restoring the 27 MHz look: half a progressive pixel, one 480i pixel. Co-Authored-By: Claude Fable 5.1 --- sys/osd.v | 10 +++++++++- sys/sys_top.v | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) 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 02f78c2..254736e 100644 --- a/sys/sys_top.v +++ b/sys/sys_top.v @@ -1482,7 +1482,9 @@ scanlines #(0) VGA_scanlines wire [23:0] vga_data_osd0; wire vga_vs_osd0, vga_hs_osd0, vga_de_osd0; -osd vga_osd +// 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), From 1058a8df7d2824d3e3539c5317b6c2111dc5e1af Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Tue, 29 Sep 2026 10:52:28 +0200 Subject: [PATCH 08/11] video: sample the H-size retimer at the 13.5 MHz OSD dot rate The OSD draws half-pixel glyph strokes (256 dots across 128 progressive pixels). A pixel-rate CE into the retimer kept one of the two dots per pixel and decimated the font, bypassed or not, which is what showed on the CRT at every H size. The retimer now samples at a constant clk_vid/4: one 480i pixel, or one OSD dot, so the OSD survives intact and stretches with the picture. Base is 4 clocks per sample in every mode (the proven 480i cadence, min 3 clocks per registered RAM pop at -8); a progressive line is 704 samples, worst shrink lead 89 of 128 FIFO entries; stretch percentages are unchanged and the mode sideband port is gone (480i gating already lives in zaparoo_hsize_map). hretime_tb reworked to the 704-dot NTSC geometry. Co-Authored-By: Claude Fable 5.1 --- menu.sv | 3 ++- rtl/zaparoo_hretime.sv | 5 +++-- sys/emu_ports.vh | 1 - sys/sys_top.v | 23 +++++++++++++---------- tb/hretime_tb.sv | 33 +++++++++++++++++---------------- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/menu.sv b/menu.sv index 10fa8f9..443d8d9 100644 --- a/menu.sv +++ b/menu.sv @@ -397,7 +397,8 @@ zaparoo_hsize_map hsize_map .enable (CRT_HSIZE_EN), .scale (CRT_HSIZE_SCALE) ); -assign CRT_HSIZE_MODE = native_mode; +// (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 diff --git a/rtl/zaparoo_hretime.sv b/rtl/zaparoo_hretime.sv index 983df20..4bf45e4 100644 --- a/rtl/zaparoo_hretime.sv +++ b/rtl/zaparoo_hretime.sv @@ -3,8 +3,9 @@ // 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 54 MHz Menu source is fixed at eight CLK_VIDEO -// cycles per 240p/PAL pixel and four per 480i pixel. +// 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 diff --git a/sys/emu_ports.vh b/sys/emu_ports.vh index 1a07bd6..8df6649 100644 --- a/sys/emu_ports.vh +++ b/sys/emu_ports.vh @@ -154,6 +154,5 @@ output [6:0] USER_OUT, // quasi-static). Consumed by sys_top's zaparoo_hretime after the VGA OSD. output CRT_HSIZE_EN, output signed [3:0] CRT_HSIZE_SCALE, -output [1:0] CRT_HSIZE_MODE, input OSD_STATUS diff --git a/sys/sys_top.v b/sys/sys_top.v index 254736e..de730a5 100644 --- a/sys/sys_top.v +++ b/sys/sys_top.v @@ -1505,12 +1505,15 @@ osd #(.FIXED_PIXSZ(4)) vga_osd .de_out(vga_de_osd0) ); -// osd is a fixed 4-register pipeline on data and syncs with no CE port; -// delaying the scanlines CE by the same 4 clocks restores at the retimer -// input the CE/data phase that scanlines guarantees at the OSD input. -reg [3:0] vga_ce_osd_sr = 0; -always @(posedge clk_vid) vga_ce_osd_sr <= {vga_ce_osd_sr[2:0], vga_ce_sl}; -wire vga_ce_osd = vga_ce_osd_sr[3]; +// 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 @@ -1519,16 +1522,17 @@ wire vga_ce_osd = vga_ce_osd_sr[3]; // quasi-static; hretime latches them per line at HSYNC). wire crt_hsize_en; wire signed [3:0] crt_hsize_scale; -wire [1:0] crt_hsize_mode; 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_osd), + .ce_in(vga_ce_dot), .enable(crt_hsize_en), - .mode(crt_hsize_mode), + // 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), @@ -1892,7 +1896,6 @@ emu emu .VGA_SCALER(vga_force_scaler), .CRT_HSIZE_EN(crt_hsize_en), .CRT_HSIZE_SCALE(crt_hsize_scale), - .CRT_HSIZE_MODE(crt_hsize_mode), `ifndef MISTER_DUAL_SDRAM .VGA_DISABLE(VGA_DISABLE), diff --git a/tb/hretime_tb.sv b/tb/hretime_tb.sv index 18e5be1..0fbdc48 100644 --- a/tb/hretime_tb.sv +++ b/tb/hretime_tb.sv @@ -1,17 +1,18 @@ -// Self-checking Menu horizontal re-timer test. The Menu input is fixed at -// CLK_VIDEO / 8 (54 MHz / 8 = 6.75 MHz) and has the narrow NTSC front porch, -// the binding worst case (PAL porches are wider at the same pixel rate). -// The positive range stops at effective +2 so all pixels drain before HS. +// 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 pixel identity, emitted count, active span and quiet HSYNC. +// asserting dot identity, emitted count, active span and quiet HSYNC. `timescale 1ns/1ps module hretime_tb; -localparam integer H_ACTIVE = 352; -localparam integer H_FP = 12; -localparam integer H_SYNC = 32; -localparam integer H_BP = 33; +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; @@ -20,7 +21,7 @@ reg clk = 0; always #9.2593 clk = ~clk; reg reset = 1; -reg [2:0] div = 0; +reg [1:0] div = 0; reg ce_in = 0; reg [9:0] hcount = 0; reg [3:0] vcount = 0; @@ -35,9 +36,9 @@ always @(posedge clk) begin vcount <= 0; end else begin - ce_in <= (div == 3'd7); + ce_in <= (div == 2'd3); div <= div + 1'd1; - if(div == 3'd7) begin + if(div == 2'd3) begin if(hcount == H_TOTAL - 1) begin hcount <= 0; vcount <= (vcount == V_TOTAL - 1) ? 0 : vcount + 1'd1; @@ -56,7 +57,7 @@ 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'd0), .scale(scale), + .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) ); @@ -78,8 +79,8 @@ always @(posedge clk) begin if(ce_out_d && de_out) begin if(got == 0) first_tick <= ticks; - if(checking && expected_scale == -8 && got != 0 && ticks - last_tick != 7) begin - $display("FAIL: -8 progressive spacing %0d, expected 7 clocks", ticks - last_tick); + 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; @@ -98,7 +99,7 @@ always @(posedge clk) begin if(checking && got != 0) begin integer expected_span; integer span; - expected_span = ((H_ACTIVE - 1) * 8 * (64 + expected_scale)) / 64; + 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); From df9dc75ddc333e844e1e514518b222bee0e699b0 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Thu, 1 Oct 2026 23:55:05 +0200 Subject: [PATCH 09/11] video: vertical shrink via extra blank lines (word2 v_size) Word2 bits 15:8 carry a signed v_size, clamped to -32..0: -N adds N blank lines per progressive frame, floor(N/2) ahead of the sync and the rest behind it, so the picture shrinks by lowering the frame rate (262+N NTSC, 312+N PAL, down to about 53.5/45.4 Hz at N=32). Active lines, sync width and all horizontal timing are untouched; 480i forces N=0 at the timing latch and keeps its exact 262.5-line fields. Blank lines trigger no DDR reads, and the setting commits only at the frame wrap like the other trims. Timing TB proves the exact totals, odd-N split, clamp, offset interaction and 480i immunity; reader TB part 2 covers the decode, clamp and positive-value rejection. Co-Authored-By: Claude Fable 5.1 --- rtl/native_video_reader.sv | 13 +++++++++++-- rtl/native_video_timing.sv | 29 ++++++++++++++++++++++++----- rtl/native_video_top.sv | 5 +++++ tb/native_video_reader_tb.sv | 29 ++++++++++++++++++++--------- tb/native_video_timing_tb.sv | 33 ++++++++++++++++++++++++++++++++- 5 files changed, 92 insertions(+), 17 deletions(-) diff --git a/rtl/native_video_reader.sv b/rtl/native_video_reader.sv index 97695ca..4742ff6 100644 --- a/rtl/native_video_reader.sv +++ b/rtl/native_video_reader.sv @@ -9,10 +9,12 @@ // 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, +// word2 [31:0] at 0x08: [31:16] magic 0x5A52, [15:8] signed v_size +// (clamped to -32..0; -N adds N blank lines per progressive frame, +// vertical shrink via frame rate, forced 0 in 480i by the timing latch), // [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 is +// both fields read as 0, so legacy writers imply unity size. Word2 is // written only by Main (never the frontend, which owns words 0/1), and 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 @@ -62,6 +64,7 @@ module native_video_reader 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 [5:0] v_extra_out, output reg [7:0] r_out, output reg [7:0] g_out, @@ -179,6 +182,9 @@ 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; +wire signed [7:0] raw_vsize = $signed(ctrl_word2[15:8]); +wire [5:0] ctrl_vextra = (!word2_ok || !raw_vsize[7]) ? 6'd0 : + (raw_vsize < -8'sd32) ? 6'd32 : -raw_vsize[5:0]; // 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; @@ -216,6 +222,7 @@ always @(posedge ddr_clk) begin h_offset_out <= 8'sd0; v_offset_out <= 6'sd0; h_size_out <= 8'sd0; + v_extra_out <= 6'd0; line_words <= 9'd160; scan_lines <= 9'd240; scan_interlaced <= 1'b0; @@ -286,6 +293,7 @@ always @(posedge ddr_clk) begin h_offset_out <= 8'sd0; v_offset_out <= 6'sd0; h_size_out <= 8'sd0; + v_extra_out <= 6'd0; state <= ST_IDLE; end else begin @@ -294,6 +302,7 @@ always @(posedge ddr_clk) begin 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; + v_extra_out <= ctrl_vextra; 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_timing.sv b/rtl/native_video_timing.sv index 34d9f22..827091d 100644 --- a/rtl/native_video_timing.sv +++ b/rtl/native_video_timing.sv @@ -4,6 +4,9 @@ // mode 1: 720x480i60 (CEA-861) ce_pix = 27/2 = 13.5 MHz, 858x525, 15734.27 Hz // mode 2: 352x288p50 (PAL) ce_pix = 27/4 = 6.75 MHz, 432x312, 15625.00 Hz // +// v_extra_in adds 0..32 blank lines per progressive frame (vertical shrink +// via frame rate: totals become 262+N / 312+N), split floor(N/2) front / +// ceil(N/2) back around the sync; 480i forces N = 0. // mode_in/h_offset_in/v_offset_in are quasi-static (two-flop synchronized by // the caller) and are latched here at the field wrap so a mid-frame update // can't corrupt sync. Offsets shift the image by repartitioning front/back @@ -20,6 +23,9 @@ module native_video_timing input wire [1:0] mode_in, input wire signed [7:0] h_offset_in, // + = right, honored -31..+9 px input wire signed [5:0] v_offset_in, // + = down, honored -14..+2 lines + input wire [5:0] v_extra_in, // extra blank lines, honored 0..32; + // vertical shrink by lowering the + // frame rate, progressive only output reg [1:0] mode, // latched active mode; selects the ce_pix divider output reg field, // 480i field number, 0 in progressive modes @@ -79,14 +85,25 @@ function automatic signed [5:0] clamp_v(input signed [5:0] v); else clamp_v = v; endfunction +function automatic [5:0] clamp_ve(input [5:0] v); + clamp_ve = (v > 6'd32) ? 6'd32 : v; +endfunction + reg signed [7:0] h_offset; reg signed [5:0] v_offset; +// Latched extra blank lines (N): floor(N/2) extends the front blanking +// (sync starts later), the rest lands in the back blanking because the +// field wrap moves to V_TOTAL + N. Active lines, sync width and all +// horizontal timing are untouched, so only the frame rate drops. 480i +// forces N = 0 at the latch. +reg [5:0] v_extra; +wire [8:0] V_TOTAL_EFF = V_TOTAL + {3'd0, v_extra}; // Sync starts shift with the offset; two's-complement subtraction in // unsigned arithmetic yields the correct result at both ends of the range. wire [9:0] H_SYNC_START = H_ACTIVE + (H_FP - {{2{h_offset[7]}}, h_offset}); wire [9:0] H_SYNC_END = H_SYNC_START + H_SYNC; -wire [8:0] V_SYNC_START = V_ACTIVE + (V_FP - {{3{v_offset[5]}}, v_offset}); +wire [8:0] V_SYNC_START = V_ACTIVE + {4'd0, v_extra[5:1]} + (V_FP - {{3{v_offset[5]}}, v_offset}); wire [8:0] V_SYNC_END = V_SYNC_START + V_SYNC; // In 480i the odd field's vsync transitions half a scanline (H_TOTAL/2 @@ -100,7 +117,7 @@ wire vs_step = field ? (hcount == (H_TOTAL >> 1) - 10'd1) wire [8:0] vs_line = field ? vcount : (vcount + 9'd1); wire line_wrap = (hcount == H_TOTAL - 10'd1); -wire field_wrap = line_wrap && (vcount == V_TOTAL - 9'd1); +wire field_wrap = line_wrap && (vcount == V_TOTAL_EFF - 9'd1); always @(posedge clk) begin if(reset) begin @@ -108,6 +125,7 @@ always @(posedge clk) begin field <= 1'b0; h_offset <= 8'sd0; v_offset <= 6'sd0; + v_extra <= 6'd0; hcount <= 10'd0; vcount <= 9'd0; hsync <= 1'b0; @@ -127,7 +145,7 @@ always @(posedge clk) begin if(line_wrap) begin hcount <= 10'd0; - if(vcount == V_TOTAL - 9'd1) vcount <= 9'd0; + if(vcount == V_TOTAL_EFF - 9'd1) vcount <= 9'd0; else vcount <= vcount + 9'd1; end else begin @@ -141,6 +159,7 @@ always @(posedge clk) begin if(next_mode != MODE_480I) field <= 1'b0; h_offset <= clamp_h(h_offset_in); v_offset <= clamp_v(v_offset_in); + v_extra <= (next_mode == MODE_480I) ? 6'd0 : clamp_ve(v_extra_in); end if(hcount == H_ACTIVE - 10'd1) hblank <= 1'b1; @@ -156,7 +175,7 @@ always @(posedge clk) begin if(line_wrap) begin if(vcount == V_ACTIVE - 9'd1) vblank <= 1'b1; - else if(vcount == V_TOTAL - 9'd1) vblank <= 1'b0; + else if(vcount == V_TOTAL_EFF - 9'd1) vblank <= 1'b0; end if(hcount == H_ACTIVE - 10'd1) new_line <= 1'b1; @@ -178,7 +197,7 @@ always @(posedge clk) begin next_vblank = vblank; if(line_wrap) begin if(vcount == V_ACTIVE - 9'd1) next_vblank = 1'b1; - else if(vcount == V_TOTAL - 9'd1) next_vblank = 1'b0; + else if(vcount == V_TOTAL_EFF - 9'd1) next_vblank = 1'b0; end de <= ~next_hblank & ~next_vblank; diff --git a/rtl/native_video_top.sv b/rtl/native_video_top.sv index fa1e713..b98d9d9 100644 --- a/rtl/native_video_top.sv +++ b/rtl/native_video_top.sv @@ -54,15 +54,18 @@ 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; +wire [5:0] rd_v_extra; 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]; +reg [5:0] v_extra_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_extra_sync[0] <= rd_v_extra; v_extra_sync[1] <= v_extra_sync[0]; v_offset_sync[0] <= rd_v_offset; v_offset_sync[1] <= v_offset_sync[0]; end @@ -74,6 +77,7 @@ native_video_timing timing .mode_in (mode_sync[1]), .h_offset_in ($signed(h_offset_sync[1])), .v_offset_in ($signed(v_offset_sync[1])), + .v_extra_in (v_extra_sync[1]), .mode (vga_mode), .field (tim_field), .hsync (tim_hs), @@ -115,6 +119,7 @@ native_video_reader reader .h_offset_out (rd_h_offset), .v_offset_out (rd_v_offset), .h_size_out (rd_h_size), + .v_extra_out (rd_v_extra), .r_out (vga_r), .g_out (vga_g), diff --git a/tb/native_video_reader_tb.sv b/tb/native_video_reader_tb.sv index 691099b..72200fe 100644 --- a/tb/native_video_reader_tb.sv +++ b/tb/native_video_reader_tb.sv @@ -151,10 +151,11 @@ 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); +// Publish the interim analog size word2 in the second control beat: +// [15:8] signed v_size (-32..0), [7:0] signed h_size. +task set_word2(input bit magic, input signed [7:0] size, input signed [7:0] vsize); begin - ctrl_q2 = {32'd0, magic ? 16'h5A52 : 16'hBEEF, 8'h00, size}; + ctrl_q2 = {32'd0, magic ? 16'h5A52 : 16'hBEEF, vsize, size}; end endtask @@ -337,23 +338,33 @@ initial begin 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); + set_word2(1, 8'sd2, 8'sd0); 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); + set_word2(1, -8'sd8, 8'sd0); wait_frames(2); check("h_size -8 decoded", dut.reader.h_size_out, -8); - set_word2(1, 8'sd5); + set_word2(1, 8'sd5, 8'sd0); wait_frames(2); check("h_size +5 clamps to +2", dut.reader.h_size_out, 2); - set_word2(1, -8'sd20); + set_word2(1, -8'sd20, 8'sd0); wait_frames(2); check("h_size -20 clamps to -8", dut.reader.h_size_out, -8); - set_word2(0, 8'sd2); + set_word2(0, 8'sd2, 8'sd0); wait_frames(2); check("wrong word2 magic reads 0", dut.reader.h_size_out, 0); - set_word2(1, -8'sd4); + set_word2(1, 8'sd0, -8'sd8); + wait_frames(2); + check("v_size -8 decodes to N=8", dut.reader.v_extra_out, 8); + check("v_extra reaches timing", dut.timing.v_extra, 8); + set_word2(1, 8'sd0, -8'sd40); + wait_frames(2); + check("v_size -40 clamps to N=32", dut.reader.v_extra_out, 32); + set_word2(1, 8'sd0, 8'sd5); + wait_frames(2); + check("positive v_size reads N=0", dut.reader.v_extra_out, 0); + set_word2(1, -8'sd4, 8'sd0); wait_frames(2); check("h_size follows live writer", dut.reader.h_size_out, -4); ctrl_q = 64'd0; diff --git a/tb/native_video_timing_tb.sv b/tb/native_video_timing_tb.sv index efbbe0b..0eaae77 100644 --- a/tb/native_video_timing_tb.sv +++ b/tb/native_video_timing_tb.sv @@ -21,6 +21,7 @@ reg reset = 1; reg [1:0] mode_in = 2'd0; reg signed [7:0] h_offset_in = 8'sd0; reg signed [5:0] v_offset_in = 6'sd0; +reg [5:0] v_extra_in = 6'd0; wire [1:0] mode; wire field, hsync, vsync, hblank, vblank, de, new_frame, new_line; @@ -44,6 +45,7 @@ native_video_timing dut .mode_in (mode_in), .h_offset_in (h_offset_in), .v_offset_in (v_offset_in), + .v_extra_in (v_extra_in), .mode (mode), .field (field), .hsync (hsync), @@ -223,6 +225,35 @@ initial begin h_offset_in = 8'sd0; v_offset_in = 6'sd0; + // ---- vertical shrink: extra blank lines (frame rate only) ----------- + $display("--- v_extra blank lines ---"); + v_extra_in = 6'd32; settle(4); + check("N=32 field period", vs_period, 429*294); + check("N=32 vsync width", vs_width, 429*3); + check("N=32 active lines", de_lines_last, 240); + check("N=32 line period", hs_period, 429); + check("N=32 vsync->active", vs_to_de, 429*35); + v_extra_in = 6'd5; settle(4); + check("N=5 (odd) field period", vs_period, 429*267); + check("N=5 front split floor(N/2)", vs_to_de, 429*22); + v_extra_in = 6'd63; settle(4); + check("N=63 clamps to 32", vs_period, 429*294); + v_extra_in = 6'd32; + v_offset_in = -6'sd14; settle(4); + check("N=32 + v=-14 period", vs_period, 429*294); + check("N=32 + v=-14 vsync->active", vs_to_de, 429*21); + v_offset_in = 6'sd0; + mode_in = 2'd2; settle(4); + check("pal N=32 field period", vs_period, 432*344); + check("pal N=32 active lines", de_lines_last, 288); + check("pal N=32 vsync->active", vs_to_de, 432*37); + mode_in = 2'd1; settle(5); + check("480i ignores N: field A", vs_period, 225225); + check("480i ignores N: field B", vs_period_d, 225225); + check("480i ignores N: lines", de_lines_last, 240); + mode_in = 2'd0; + v_extra_in = 6'd0; + // ---- mode change sanity: back to NTSC after everything -------------- settle(4); check("ntsc restore line period", hs_period, 429); @@ -238,7 +269,7 @@ initial begin end initial begin - #2_000_000_000; // 2 s simulated-time guard + #4_000_000_000; // 2 s simulated-time guard $display("TIMEOUT"); $fatal(1); end From 14015753fbbf2d4c3c25d5301f03b6fb9f88ef88 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Fri, 2 Oct 2026 08:52:44 +0200 Subject: [PATCH 10/11] Revert "video: vertical shrink via extra blank lines (word2 v_size)" This reverts commit df9dc75. The frame-rate shrink is invisible on TVs with frequency-compensated vertical deflection until the standard detector falls out of its ~10% lock window (at -27 lines), where the height snaps instead of scaling. Not useful as a control; analog H size stays. Word2 byte [15:8] returns to reserved-0. Co-Authored-By: Claude Fable 5.1 --- rtl/native_video_reader.sv | 13 ++----------- rtl/native_video_timing.sv | 29 +++++------------------------ rtl/native_video_top.sv | 5 ----- tb/native_video_reader_tb.sv | 29 +++++++++-------------------- tb/native_video_timing_tb.sv | 33 +-------------------------------- 5 files changed, 17 insertions(+), 92 deletions(-) diff --git a/rtl/native_video_reader.sv b/rtl/native_video_reader.sv index 4742ff6..97695ca 100644 --- a/rtl/native_video_reader.sv +++ b/rtl/native_video_reader.sv @@ -9,12 +9,10 @@ // 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] signed v_size -// (clamped to -32..0; -N adds N blank lines per progressive frame, -// vertical shrink via frame rate, forced 0 in 480i by the timing latch), +// 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 -// both fields read as 0, so legacy writers imply unity size. Word2 is +// the field reads as 0, so legacy writers imply unity width. Word2 is // written only by Main (never the frontend, which owns words 0/1), and 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 @@ -64,7 +62,6 @@ module native_video_reader 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 [5:0] v_extra_out, output reg [7:0] r_out, output reg [7:0] g_out, @@ -182,9 +179,6 @@ 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; -wire signed [7:0] raw_vsize = $signed(ctrl_word2[15:8]); -wire [5:0] ctrl_vextra = (!word2_ok || !raw_vsize[7]) ? 6'd0 : - (raw_vsize < -8'sd32) ? 6'd32 : -raw_vsize[5:0]; // 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; @@ -222,7 +216,6 @@ always @(posedge ddr_clk) begin h_offset_out <= 8'sd0; v_offset_out <= 6'sd0; h_size_out <= 8'sd0; - v_extra_out <= 6'd0; line_words <= 9'd160; scan_lines <= 9'd240; scan_interlaced <= 1'b0; @@ -293,7 +286,6 @@ always @(posedge ddr_clk) begin h_offset_out <= 8'sd0; v_offset_out <= 6'sd0; h_size_out <= 8'sd0; - v_extra_out <= 6'd0; state <= ST_IDLE; end else begin @@ -302,7 +294,6 @@ always @(posedge ddr_clk) begin 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; - v_extra_out <= ctrl_vextra; 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_timing.sv b/rtl/native_video_timing.sv index 827091d..34d9f22 100644 --- a/rtl/native_video_timing.sv +++ b/rtl/native_video_timing.sv @@ -4,9 +4,6 @@ // mode 1: 720x480i60 (CEA-861) ce_pix = 27/2 = 13.5 MHz, 858x525, 15734.27 Hz // mode 2: 352x288p50 (PAL) ce_pix = 27/4 = 6.75 MHz, 432x312, 15625.00 Hz // -// v_extra_in adds 0..32 blank lines per progressive frame (vertical shrink -// via frame rate: totals become 262+N / 312+N), split floor(N/2) front / -// ceil(N/2) back around the sync; 480i forces N = 0. // mode_in/h_offset_in/v_offset_in are quasi-static (two-flop synchronized by // the caller) and are latched here at the field wrap so a mid-frame update // can't corrupt sync. Offsets shift the image by repartitioning front/back @@ -23,9 +20,6 @@ module native_video_timing input wire [1:0] mode_in, input wire signed [7:0] h_offset_in, // + = right, honored -31..+9 px input wire signed [5:0] v_offset_in, // + = down, honored -14..+2 lines - input wire [5:0] v_extra_in, // extra blank lines, honored 0..32; - // vertical shrink by lowering the - // frame rate, progressive only output reg [1:0] mode, // latched active mode; selects the ce_pix divider output reg field, // 480i field number, 0 in progressive modes @@ -85,25 +79,14 @@ function automatic signed [5:0] clamp_v(input signed [5:0] v); else clamp_v = v; endfunction -function automatic [5:0] clamp_ve(input [5:0] v); - clamp_ve = (v > 6'd32) ? 6'd32 : v; -endfunction - reg signed [7:0] h_offset; reg signed [5:0] v_offset; -// Latched extra blank lines (N): floor(N/2) extends the front blanking -// (sync starts later), the rest lands in the back blanking because the -// field wrap moves to V_TOTAL + N. Active lines, sync width and all -// horizontal timing are untouched, so only the frame rate drops. 480i -// forces N = 0 at the latch. -reg [5:0] v_extra; -wire [8:0] V_TOTAL_EFF = V_TOTAL + {3'd0, v_extra}; // Sync starts shift with the offset; two's-complement subtraction in // unsigned arithmetic yields the correct result at both ends of the range. wire [9:0] H_SYNC_START = H_ACTIVE + (H_FP - {{2{h_offset[7]}}, h_offset}); wire [9:0] H_SYNC_END = H_SYNC_START + H_SYNC; -wire [8:0] V_SYNC_START = V_ACTIVE + {4'd0, v_extra[5:1]} + (V_FP - {{3{v_offset[5]}}, v_offset}); +wire [8:0] V_SYNC_START = V_ACTIVE + (V_FP - {{3{v_offset[5]}}, v_offset}); wire [8:0] V_SYNC_END = V_SYNC_START + V_SYNC; // In 480i the odd field's vsync transitions half a scanline (H_TOTAL/2 @@ -117,7 +100,7 @@ wire vs_step = field ? (hcount == (H_TOTAL >> 1) - 10'd1) wire [8:0] vs_line = field ? vcount : (vcount + 9'd1); wire line_wrap = (hcount == H_TOTAL - 10'd1); -wire field_wrap = line_wrap && (vcount == V_TOTAL_EFF - 9'd1); +wire field_wrap = line_wrap && (vcount == V_TOTAL - 9'd1); always @(posedge clk) begin if(reset) begin @@ -125,7 +108,6 @@ always @(posedge clk) begin field <= 1'b0; h_offset <= 8'sd0; v_offset <= 6'sd0; - v_extra <= 6'd0; hcount <= 10'd0; vcount <= 9'd0; hsync <= 1'b0; @@ -145,7 +127,7 @@ always @(posedge clk) begin if(line_wrap) begin hcount <= 10'd0; - if(vcount == V_TOTAL_EFF - 9'd1) vcount <= 9'd0; + if(vcount == V_TOTAL - 9'd1) vcount <= 9'd0; else vcount <= vcount + 9'd1; end else begin @@ -159,7 +141,6 @@ always @(posedge clk) begin if(next_mode != MODE_480I) field <= 1'b0; h_offset <= clamp_h(h_offset_in); v_offset <= clamp_v(v_offset_in); - v_extra <= (next_mode == MODE_480I) ? 6'd0 : clamp_ve(v_extra_in); end if(hcount == H_ACTIVE - 10'd1) hblank <= 1'b1; @@ -175,7 +156,7 @@ always @(posedge clk) begin if(line_wrap) begin if(vcount == V_ACTIVE - 9'd1) vblank <= 1'b1; - else if(vcount == V_TOTAL_EFF - 9'd1) vblank <= 1'b0; + else if(vcount == V_TOTAL - 9'd1) vblank <= 1'b0; end if(hcount == H_ACTIVE - 10'd1) new_line <= 1'b1; @@ -197,7 +178,7 @@ always @(posedge clk) begin next_vblank = vblank; if(line_wrap) begin if(vcount == V_ACTIVE - 9'd1) next_vblank = 1'b1; - else if(vcount == V_TOTAL_EFF - 9'd1) next_vblank = 1'b0; + else if(vcount == V_TOTAL - 9'd1) next_vblank = 1'b0; end de <= ~next_hblank & ~next_vblank; diff --git a/rtl/native_video_top.sv b/rtl/native_video_top.sv index b98d9d9..fa1e713 100644 --- a/rtl/native_video_top.sv +++ b/rtl/native_video_top.sv @@ -54,18 +54,15 @@ 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; -wire [5:0] rd_v_extra; 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]; -reg [5:0] v_extra_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_extra_sync[0] <= rd_v_extra; v_extra_sync[1] <= v_extra_sync[0]; v_offset_sync[0] <= rd_v_offset; v_offset_sync[1] <= v_offset_sync[0]; end @@ -77,7 +74,6 @@ native_video_timing timing .mode_in (mode_sync[1]), .h_offset_in ($signed(h_offset_sync[1])), .v_offset_in ($signed(v_offset_sync[1])), - .v_extra_in (v_extra_sync[1]), .mode (vga_mode), .field (tim_field), .hsync (tim_hs), @@ -119,7 +115,6 @@ native_video_reader reader .h_offset_out (rd_h_offset), .v_offset_out (rd_v_offset), .h_size_out (rd_h_size), - .v_extra_out (rd_v_extra), .r_out (vga_r), .g_out (vga_g), diff --git a/tb/native_video_reader_tb.sv b/tb/native_video_reader_tb.sv index 72200fe..691099b 100644 --- a/tb/native_video_reader_tb.sv +++ b/tb/native_video_reader_tb.sv @@ -151,11 +151,10 @@ task publish(input bit magic, input [3:0] pmode, input signed [7:0] hoff, end endtask -// Publish the interim analog size word2 in the second control beat: -// [15:8] signed v_size (-32..0), [7:0] signed h_size. -task set_word2(input bit magic, input signed [7:0] size, input signed [7:0] vsize); +// 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, vsize, size}; + ctrl_q2 = {32'd0, magic ? 16'h5A52 : 16'hBEEF, 8'h00, size}; end endtask @@ -338,33 +337,23 @@ initial begin 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, 8'sd0); + 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, 8'sd0); + set_word2(1, -8'sd8); wait_frames(2); check("h_size -8 decoded", dut.reader.h_size_out, -8); - set_word2(1, 8'sd5, 8'sd0); + 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, 8'sd0); + 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, 8'sd0); + set_word2(0, 8'sd2); wait_frames(2); check("wrong word2 magic reads 0", dut.reader.h_size_out, 0); - set_word2(1, 8'sd0, -8'sd8); - wait_frames(2); - check("v_size -8 decodes to N=8", dut.reader.v_extra_out, 8); - check("v_extra reaches timing", dut.timing.v_extra, 8); - set_word2(1, 8'sd0, -8'sd40); - wait_frames(2); - check("v_size -40 clamps to N=32", dut.reader.v_extra_out, 32); - set_word2(1, 8'sd0, 8'sd5); - wait_frames(2); - check("positive v_size reads N=0", dut.reader.v_extra_out, 0); - set_word2(1, -8'sd4, 8'sd0); + set_word2(1, -8'sd4); wait_frames(2); check("h_size follows live writer", dut.reader.h_size_out, -4); ctrl_q = 64'd0; diff --git a/tb/native_video_timing_tb.sv b/tb/native_video_timing_tb.sv index 0eaae77..efbbe0b 100644 --- a/tb/native_video_timing_tb.sv +++ b/tb/native_video_timing_tb.sv @@ -21,7 +21,6 @@ reg reset = 1; reg [1:0] mode_in = 2'd0; reg signed [7:0] h_offset_in = 8'sd0; reg signed [5:0] v_offset_in = 6'sd0; -reg [5:0] v_extra_in = 6'd0; wire [1:0] mode; wire field, hsync, vsync, hblank, vblank, de, new_frame, new_line; @@ -45,7 +44,6 @@ native_video_timing dut .mode_in (mode_in), .h_offset_in (h_offset_in), .v_offset_in (v_offset_in), - .v_extra_in (v_extra_in), .mode (mode), .field (field), .hsync (hsync), @@ -225,35 +223,6 @@ initial begin h_offset_in = 8'sd0; v_offset_in = 6'sd0; - // ---- vertical shrink: extra blank lines (frame rate only) ----------- - $display("--- v_extra blank lines ---"); - v_extra_in = 6'd32; settle(4); - check("N=32 field period", vs_period, 429*294); - check("N=32 vsync width", vs_width, 429*3); - check("N=32 active lines", de_lines_last, 240); - check("N=32 line period", hs_period, 429); - check("N=32 vsync->active", vs_to_de, 429*35); - v_extra_in = 6'd5; settle(4); - check("N=5 (odd) field period", vs_period, 429*267); - check("N=5 front split floor(N/2)", vs_to_de, 429*22); - v_extra_in = 6'd63; settle(4); - check("N=63 clamps to 32", vs_period, 429*294); - v_extra_in = 6'd32; - v_offset_in = -6'sd14; settle(4); - check("N=32 + v=-14 period", vs_period, 429*294); - check("N=32 + v=-14 vsync->active", vs_to_de, 429*21); - v_offset_in = 6'sd0; - mode_in = 2'd2; settle(4); - check("pal N=32 field period", vs_period, 432*344); - check("pal N=32 active lines", de_lines_last, 288); - check("pal N=32 vsync->active", vs_to_de, 432*37); - mode_in = 2'd1; settle(5); - check("480i ignores N: field A", vs_period, 225225); - check("480i ignores N: field B", vs_period_d, 225225); - check("480i ignores N: lines", de_lines_last, 240); - mode_in = 2'd0; - v_extra_in = 6'd0; - // ---- mode change sanity: back to NTSC after everything -------------- settle(4); check("ntsc restore line period", hs_period, 429); @@ -269,7 +238,7 @@ initial begin end initial begin - #4_000_000_000; // 2 s simulated-time guard + #2_000_000_000; // 2 s simulated-time guard $display("TIMEOUT"); $fatal(1); end From 3f08fe7ad5ce1555ccf0c1428508d855efb8b323 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sat, 3 Oct 2026 00:59:29 +0200 Subject: [PATCH 11/11] docs: word2 ownership now shared with the frontend Comment-only: the frontend writes the saved h_size into word2 like it owns word1's offsets, and Main's OSD save respawns it so the writers never disagree. No logic change. Co-Authored-By: Claude Fable 5.1 --- rtl/native_video_reader.sv | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rtl/native_video_reader.sv b/rtl/native_video_reader.sv index 97695ca..b5b2d4c 100644 --- a/rtl/native_video_reader.sv +++ b/rtl/native_video_reader.sv @@ -12,9 +12,11 @@ // 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 is -// written only by Main (never the frontend, which owns words 0/1), and it -// applies only while word1's magic is valid. The deferred v3 protocol +// 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.