From 50bf5cc771e9f1b4d3c09d22ec1f47c304b30703 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sat, 9 May 2026 23:36:33 +0200 Subject: [PATCH 1/3] feat: dual-mode menu core with switchable native FB reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a runtime-switchable "FB mode" alongside the original menu behavior. When status[9]=0 (default), the menu core renders the original cosine+LFSR pattern through the unchanged PAL/NTSC scandoubler timing — every existing menu surface (HDMI wallpaper compositor, OSD, F1 wallpaper cycle) keeps working. When status[9]=1, native_video_top takes over and feeds VGA from the 320x240 RGBX8888 framebuffer the HPS-side launcher writes into DDR. Carried forward from codex/zaparoo-rgbx8888-native-core (PR #2): - rtl/native_video_reader.sv — DDR burst reader with ping-pong buffers - rtl/native_video_timing.sv — 320x240 NTSC native CRT timing - rtl/native_video_top.sv — wrapper - PLL output1 20 MHz -> 27.027 MHz (required for 15.734 kHz NTSC line rate) Deliberately NOT carried forward — preserves original menu functionality: - CONF_STR title stays "MENU" (so is_menu() in Main_MiSTer still matches, F1 wallpaper cycling still works) - VIDEO_ARX/ARY stay 0/0 (no aspect-ratio change in cosine mode) - PAL/NTSC scandoubler ce_pix logic intact - Original cosine HV counters intact Mux on status[9] & native_active, with fallback to the cosine path until the first DDR frame is loaded so the output is never undriven. The DDR clear loop is removed — it was one-time boot scaffolding using the same DDRAM_* signals the native reader now owns. native_video_reader holds ddr_rd/ddr_we low when status[9]=0, so DDR is unused in the default mode. --- files.qip | 4 +- menu.sv | 88 ++++++---- rtl/native_video_reader.sv | 339 +++++++++++++++++++++++++++++++++++++ rtl/native_video_timing.sv | 96 +++++++++++ rtl/native_video_top.sv | 96 +++++++++++ rtl/pll/pll_0002.v | 2 +- 6 files changed, 592 insertions(+), 33 deletions(-) create mode 100644 rtl/native_video_reader.sv create mode 100644 rtl/native_video_timing.sv create mode 100644 rtl/native_video_top.sv diff --git a/files.qip b/files.qip index 830b598..93247c1 100644 --- a/files.qip +++ b/files.qip @@ -1,5 +1,7 @@ set_global_assignment -name SYSTEMVERILOG_FILE rtl/sdram.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/ddram.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 diff --git a/menu.sv b/menu.sv index 5160e63..1249200 100644 --- a/menu.sv +++ b/menu.sv @@ -336,36 +336,15 @@ always @(posedge clk_sys) begin state <= state+1'd1; end 16: begin - sdram_addr <= addr[24:0]; - sdram_din <= 0; - sdram_we <= we; + sdram_we <= 0; end endcase end end -ddram ddr -( - .*, - .reset(RESET), - .dout(), - .din(0), - .rd(0), - .ready() -); - -reg we; -reg [28:0] addr = 0; - -always @(posedge clk_sys) begin - reg [4:0] cnt = 9; - - if(~RESET & cfg[15]) begin - cnt <= cnt + 1'b1; - we <= &cnt; - if(cnt == 8) addr <= addr + 1'd1; - end -end +// DDR clear loop removed: native_video_reader owns DDRAM_* signals. +// When status[9]=0 the reader is held in idle (rd=0, we=0) and DDR is unused; +// when status[9]=1 the reader takes over to fetch the linux-rendered framebuffer. //////////////////////////// MT32pi ////////////////////////////////// @@ -550,11 +529,58 @@ cos cos(vvc + {vc>>forced_scandoubler, 2'b00}, cos_out); wire [7:0] comp_v = (cos_g >= rnd_c) ? {cos_g - rnd_c, 2'b00} : 8'd0; -assign VGA_DE = ~(HBlank | VBlank); -assign VGA_HS = HSync; -assign VGA_VS = VSync; -assign VGA_G = comp_v; -assign VGA_R = comp_v; -assign VGA_B = comp_v; +// Runtime FB-mode gate driven by the HPS-side launcher via status[9]. +wire mode_zaparoo = status[9]; + +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 native_active; + +native_video_top native_video +( + .clk_sys (clk_sys), + .clk_vid (CLK_VIDEO), + .ce_pix (ce_pix), + .reset (RESET), + + .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 (), + .enable (mode_zaparoo), + .active (native_active) +); + +// Mode A (default): cosine+LFSR pattern drives RGB and the original PAL/NTSC +// scandoubler timing drives sync/DE. HDMI wallpaper compositor runs unchanged. +// Mode B (status[9]=1, frame ready): native_video_top drives RGB+sync from the +// linux-rendered 320x240 RGBX8888 buffer in DDR. Falls back to cosine until the +// first frame is loaded so the screen is never undriven. +wire use_native = mode_zaparoo & native_active; + +assign VGA_DE = use_native ? native_de : ~(HBlank | VBlank); +assign VGA_HS = use_native ? native_hs : HSync; +assign VGA_VS = use_native ? native_vs : VSync; +assign VGA_R = use_native ? native_r : comp_v; +assign VGA_G = use_native ? native_g : comp_v; +assign VGA_B = use_native ? native_b : comp_v; endmodule diff --git a/rtl/native_video_reader.sv b/rtl/native_video_reader.sv new file mode 100644 index 0000000..1931028 --- /dev/null +++ b/rtl/native_video_reader.sv @@ -0,0 +1,339 @@ +// Zaparoo native video DDR reader. +// DDR contract: +// 0x3A000000: control word, (frame_counter << 2) | active_buffer +// 0x3A000100: buffer 0, 320x240 RGBX8888 +// 0x3A04B100: buffer 1, 320x240 RGBX8888 + +module native_video_reader +( + input wire ddr_clk, + input wire ddr_busy, + output reg [7:0] ddr_burstcnt, + output reg [28:0] ddr_addr, + input wire [63:0] ddr_dout, + input wire ddr_dout_ready, + output reg ddr_rd, + output wire [63:0] ddr_din, + output wire [7:0] ddr_be, + output wire ddr_we, + + input wire clk_vid, + input wire ce_pix, + input wire reset, + input wire de, + input wire vblank, + input wire new_frame, + input wire new_line, + input wire [8:0] vcount, + + output reg [7:0] r_out, + output reg [7:0] g_out, + output reg [7:0] b_out, + input wire enable, + output wire frame_ready +); + +assign ddr_din = 64'd0; +assign ddr_be = 8'hFF; +assign ddr_we = 1'b0; + +localparam [28:0] CTRL_ADDR = 29'h07400000; +localparam [28:0] BUF0_ADDR = 29'h07400020; +localparam [28:0] BUF1_ADDR = 29'h07409620; +localparam [7:0] LINE_BURST = 8'd160; +localparam [28:0] LINE_STRIDE = 29'd160; +localparam [8:0] V_ACTIVE = 9'd240; +localparam [19:0] TIMEOUT_MAX = 20'hF_FFFF; + +reg [1:0] enable_sync; +always @(posedge ddr_clk) begin + if(reset) enable_sync <= 2'b0; + else enable_sync <= {enable_sync[0], enable}; +end +wire enable_ddr = enable_sync[1]; + +reg [1:0] new_frame_sync; +always @(posedge ddr_clk) begin + if(reset) new_frame_sync <= 2'b0; + else new_frame_sync <= {new_frame_sync[0], new_frame}; +end +wire new_frame_ddr = ~new_frame_sync[1] & new_frame_sync[0]; + +reg [1:0] new_line_sync; +always @(posedge ddr_clk) begin + if(reset) new_line_sync <= 2'b0; + else new_line_sync <= {new_line_sync[0], new_line}; +end +wire new_line_ddr = ~new_line_sync[1] & new_line_sync[0]; + +reg [1:0] vblank_sync; +always @(posedge ddr_clk) begin + if(reset) vblank_sync <= 2'b0; + else vblank_sync <= {vblank_sync[0], vblank}; +end +wire vblank_ddr = vblank_sync[1]; + +reg [1:0] reset_vid_sync; +always @(posedge clk_vid or posedge reset) begin + if(reset) reset_vid_sync <= 2'b11; + else reset_vid_sync <= {reset_vid_sync[0], 1'b0}; +end +wire reset_vid = reset_vid_sync[1]; + +reg frame_ready_reg; +reg [1:0] frame_ready_sync; +always @(posedge clk_vid) begin + if(reset_vid) frame_ready_sync <= 2'b0; + else frame_ready_sync <= {frame_ready_sync[0], frame_ready_reg}; +end +wire frame_ready_vid = frame_ready_sync[1]; +assign frame_ready = frame_ready_vid; + +localparam [3:0] ST_IDLE = 4'd0; +localparam [3:0] ST_POLL_CTRL = 4'd1; +localparam [3:0] ST_WAIT_CTRL = 4'd2; +localparam [3:0] ST_CHECK_CTRL = 4'd3; +localparam [3:0] ST_READ_LINE = 4'd4; +localparam [3:0] ST_WAIT_LINE = 4'd5; +localparam [3:0] ST_LINE_DONE = 4'd6; +localparam [3:0] ST_WAIT_DISPLAY = 4'd7; + +reg [3:0] state; +reg [31:0] ctrl_word; +reg [29:0] prev_frame_counter; +reg [28:0] buf_base_addr; +reg [8:0] cur_line; +reg [7:0] beat_count; +reg first_frame_loaded; +reg preloading; +reg [19:0] timeout_cnt; +reg fifo_wr; +reg [63:0] fifo_wr_data; +wire fifo_full; + +reg [3:0] fifo_aclr_cnt; +wire fifo_aclr_ddr_active = (fifo_aclr_cnt != 4'd0); +wire fifo_aclr = reset | fifo_aclr_ddr_active; + +always @(posedge ddr_clk) begin + if(reset) begin + state <= ST_IDLE; + ddr_rd <= 1'b0; + ddr_burstcnt <= 8'd1; + ddr_addr <= 29'd0; + ctrl_word <= 32'd0; + prev_frame_counter <= 30'd0; + buf_base_addr <= BUF0_ADDR; + cur_line <= 9'd0; + beat_count <= 8'd0; + first_frame_loaded <= 1'b0; + frame_ready_reg <= 1'b0; + preloading <= 1'b0; + timeout_cnt <= 20'd0; + fifo_wr <= 1'b0; + fifo_wr_data <= 64'd0; + fifo_aclr_cnt <= 4'd0; + end + else begin + fifo_wr <= 1'b0; + if(fifo_aclr_cnt != 4'd0) fifo_aclr_cnt <= fifo_aclr_cnt - 4'd1; + if(!ddr_busy) ddr_rd <= 1'b0; + + if(state == ST_WAIT_LINE && ddr_dout_ready) begin + fifo_wr <= 1'b1; + fifo_wr_data <= ddr_dout; + beat_count <= beat_count + 8'd1; + timeout_cnt <= 20'd0; + end + + case(state) + ST_IDLE: begin + if(enable_ddr && new_frame_ddr) state <= ST_POLL_CTRL; + end + + ST_POLL_CTRL: begin + if(!ddr_busy) begin + ddr_addr <= CTRL_ADDR; + ddr_burstcnt <= 8'd1; + ddr_rd <= 1'b1; + timeout_cnt <= 20'd0; + state <= ST_WAIT_CTRL; + end + end + + ST_WAIT_CTRL: begin + if(ddr_dout_ready) begin + ctrl_word <= ddr_dout[31:0]; + timeout_cnt <= 20'd0; + state <= ST_CHECK_CTRL; + end + else if(timeout_cnt == TIMEOUT_MAX) state <= ST_IDLE; + else timeout_cnt <= timeout_cnt + 20'd1; + end + + ST_CHECK_CTRL: begin + if(ctrl_word[31:2] != prev_frame_counter) begin + prev_frame_counter <= ctrl_word[31:2]; + buf_base_addr <= ctrl_word[0] ? BUF1_ADDR : BUF0_ADDR; + cur_line <= 9'd0; + preloading <= 1'b1; + fifo_aclr_cnt <= 4'd8; + if(first_frame_loaded) frame_ready_reg <= 1'b1; + state <= ST_READ_LINE; + end + else if(first_frame_loaded) begin + cur_line <= 9'd0; + preloading <= 1'b1; + fifo_aclr_cnt <= 4'd8; + state <= ST_READ_LINE; + end + else begin + state <= ST_IDLE; + end + end + + ST_READ_LINE: begin + if(!ddr_busy && !fifo_aclr_ddr_active) begin + ddr_addr <= buf_base_addr + (cur_line * LINE_STRIDE); + ddr_burstcnt <= LINE_BURST; + ddr_rd <= 1'b1; + beat_count <= 8'd0; + timeout_cnt <= 20'd0; + state <= ST_WAIT_LINE; + end + end + + ST_WAIT_LINE: begin + if(beat_count == LINE_BURST) state <= ST_LINE_DONE; + else if(timeout_cnt == TIMEOUT_MAX) state <= ST_IDLE; + else if(!ddr_dout_ready) timeout_cnt <= timeout_cnt + 20'd1; + end + + ST_LINE_DONE: begin + cur_line <= cur_line + 9'd1; + if(cur_line == V_ACTIVE - 9'd1) begin + first_frame_loaded <= 1'b1; + frame_ready_reg <= 1'b1; + preloading <= 1'b0; + state <= ST_IDLE; + end + else if(preloading && cur_line < 9'd1) begin + state <= ST_READ_LINE; + end + else begin + preloading <= 1'b0; + state <= ST_WAIT_DISPLAY; + end + end + + ST_WAIT_DISPLAY: begin + if(cur_line < V_ACTIVE && new_line_ddr && !vblank_ddr) state <= ST_READ_LINE; + end + + default: state <= ST_IDLE; + endcase + end +end + +wire [63:0] fifo_rd_data; +wire fifo_empty; +reg fifo_rd; + +dcfifo #( + .intended_device_family ("Cyclone V"), + .lpm_numwords (512), + .lpm_showahead ("ON"), + .lpm_type ("dcfifo"), + .lpm_width (64), + .lpm_widthu (9), + .overflow_checking ("ON"), + .rdsync_delaypipe (4), + .underflow_checking ("ON"), + .use_eab ("ON"), + .wrsync_delaypipe (4) +) line_fifo ( + .aclr (fifo_aclr), + .data (fifo_wr_data), + .rdclk (clk_vid), + .rdreq (fifo_rd), + .wrclk (ddr_clk), + .wrreq (fifo_wr), + .q (fifo_rd_data), + .rdempty (fifo_empty), + .wrfull (fifo_full), + .eccstatus(), + .rdfull (), + .rdusedw (), + .wrempty (), + .wrusedw () +); + +reg [63:0] pixel_word; +reg pixel_high; +reg pixel_word_valid; + +wire [31:0] pixel_low = pixel_word[31:0]; +wire [31:0] pixel_high_word = pixel_word[63:32]; + +task automatic output_pixel; + input [31:0] pixel; + begin + // linuxfb write path lands as B,G,R,X in DDR on MiSTer; swap here + // so launcher can keep doing row memcpy with no CPU-side repack. + r_out <= pixel[23:16]; + g_out <= pixel[15:8]; + b_out <= pixel[7:0]; + end +endtask + +always @(posedge clk_vid) begin + if(reset_vid) begin + fifo_rd <= 1'b0; + r_out <= 8'd0; + g_out <= 8'd0; + b_out <= 8'd0; + pixel_word <= 64'd0; + pixel_high <= 1'b0; + pixel_word_valid <= 1'b0; + end + else begin + fifo_rd <= 1'b0; + + if(ce_pix) begin + if(de && frame_ready_vid) begin + if(pixel_word_valid) begin + if(pixel_high) begin + output_pixel(pixel_high_word); + pixel_word_valid <= 1'b0; + pixel_high <= 1'b0; + end + else begin + output_pixel(pixel_low); + pixel_high <= 1'b1; + end + end + else if(!fifo_empty) begin + pixel_word <= fifo_rd_data; + pixel_word_valid <= 1'b1; + pixel_high <= 1'b1; + fifo_rd <= 1'b1; + output_pixel(fifo_rd_data[31:0]); + end + else begin + r_out <= 8'd0; + g_out <= 8'd0; + b_out <= 8'd0; + end + end + else begin + r_out <= 8'd0; + g_out <= 8'd0; + b_out <= 8'd0; + pixel_high <= 1'b0; + pixel_word_valid <= 1'b0; + end + end + end +end + +endmodule diff --git a/rtl/native_video_timing.sv b/rtl/native_video_timing.sv new file mode 100644 index 0000000..54a2aee --- /dev/null +++ b/rtl/native_video_timing.sv @@ -0,0 +1,96 @@ +// Zaparoo native video timing: 320x240 at 15.734 kHz from 27 MHz / 4. + +module native_video_timing +( + input wire clk, + input wire ce_pix, + input wire reset, + + output reg hsync, + output reg vsync, + output reg hblank, + output reg vblank, + output reg de, + output reg [9:0] hcount, + output reg [8:0] vcount, + output reg new_frame, + output reg new_line +); + +localparam [9:0] H_ACTIVE = 10'd320; +localparam [9:0] H_FP = 10'd14; +localparam [5:0] H_SYNC = 6'd32; +localparam [9:0] H_BP = 10'd63; +localparam [9:0] H_TOTAL = 10'd429; + +localparam [8:0] V_ACTIVE = 9'd240; +localparam [8:0] V_FP = 9'd6; +localparam [4:0] V_SYNC = 5'd3; +localparam [8:0] V_BP = 9'd13; +localparam [8:0] V_TOTAL = 9'd262; + +localparam [9:0] H_SYNC_START = H_ACTIVE + H_FP; +localparam [9:0] H_SYNC_END = H_SYNC_START + H_SYNC; +localparam [8:0] V_SYNC_START = V_ACTIVE + V_FP; +localparam [8:0] V_SYNC_END = V_SYNC_START + V_SYNC; + +always @(posedge clk) begin + if(reset) begin + hcount <= 10'd0; + vcount <= 9'd0; + hsync <= 1'b0; + vsync <= 1'b0; + hblank <= 1'b0; + vblank <= 1'b0; + de <= 1'b1; + new_frame <= 1'b0; + new_line <= 1'b0; + end + else if(ce_pix) begin + reg next_hblank; + reg next_vblank; + + new_frame <= 1'b0; + new_line <= 1'b0; + + if(hcount == H_TOTAL - 10'd1) begin + hcount <= 10'd0; + if(vcount == V_TOTAL - 9'd1) vcount <= 9'd0; + else vcount <= vcount + 9'd1; + end + else begin + hcount <= hcount + 10'd1; + end + + if(hcount == H_ACTIVE - 10'd1) hblank <= 1'b1; + else if(hcount == H_TOTAL - 10'd1) hblank <= 1'b0; + + if(hcount == H_SYNC_START - 10'd1) hsync <= 1'b1; + else if(hcount == H_SYNC_END - 10'd1) hsync <= 1'b0; + + if(hcount == H_TOTAL - 10'd1) begin + if(vcount == V_ACTIVE - 9'd1) vblank <= 1'b1; + else if(vcount == V_TOTAL - 9'd1) vblank <= 1'b0; + + if(vcount == V_SYNC_START - 9'd1) vsync <= 1'b1; + else if(vcount == V_SYNC_END - 9'd1) vsync <= 1'b0; + end + + if(hcount == H_ACTIVE - 10'd1) new_line <= 1'b1; + if(hcount == H_TOTAL - 10'd1 && vcount == V_ACTIVE - 9'd1) new_frame <= 1'b1; + + next_hblank = hblank; + if(hcount == H_ACTIVE - 10'd1) next_hblank = 1'b1; + else if(hcount == H_TOTAL - 10'd1) next_hblank = 1'b0; + + next_vblank = vblank; + if(hcount == H_TOTAL - 10'd1) begin + if(vcount == V_ACTIVE - 9'd1) next_vblank = 1'b1; + else if(vcount == V_TOTAL - 9'd1) next_vblank = 1'b0; + end + + de <= ~next_hblank & ~next_vblank; + end +end + +endmodule diff --git a/rtl/native_video_top.sv b/rtl/native_video_top.sv new file mode 100644 index 0000000..de4c0f9 --- /dev/null +++ b/rtl/native_video_top.sv @@ -0,0 +1,96 @@ +// Zaparoo native video wrapper: timing + RGBX8888 DDR reader. + +module native_video_top +( + input wire clk_sys, + input wire clk_vid, + input wire ce_pix, + input wire reset, + + input wire ddr_busy, + output wire [7:0] ddr_burstcnt, + output wire [28:0] ddr_addr, + input wire [63:0] ddr_dout, + input wire ddr_dout_ready, + output wire ddr_rd, + output wire [63:0] ddr_din, + output wire [7:0] ddr_be, + output wire ddr_we, + + output wire [7:0] vga_r, + output wire [7:0] vga_g, + output wire [7:0] vga_b, + output wire vga_hs, + output wire vga_vs, + output wire vga_de, + output wire vga_hblank, + output wire vga_vblank, + + input wire enable, + output wire active +); + +wire tim_hs; +wire tim_vs; +wire tim_hblank; +wire tim_vblank; +wire tim_de; +wire [8:0] tim_vcount; +wire tim_new_frame; +wire tim_new_line; + +native_video_timing timing +( + .clk (clk_vid), + .ce_pix (ce_pix), + .reset (reset), + .hsync (tim_hs), + .vsync (tim_vs), + .hblank (tim_hblank), + .vblank (tim_vblank), + .de (tim_de), + .hcount (), + .vcount (tim_vcount), + .new_frame (tim_new_frame), + .new_line (tim_new_line) +); + +wire frame_ready; + +native_video_reader reader +( + .ddr_clk (clk_sys), + .ddr_busy (ddr_busy), + .ddr_burstcnt (ddr_burstcnt), + .ddr_addr (ddr_addr), + .ddr_dout (ddr_dout), + .ddr_dout_ready (ddr_dout_ready), + .ddr_rd (ddr_rd), + .ddr_din (ddr_din), + .ddr_be (ddr_be), + .ddr_we (ddr_we), + + .clk_vid (clk_vid), + .ce_pix (ce_pix), + .reset (reset), + .de (tim_de), + .vblank (tim_vblank), + .new_frame (tim_new_frame), + .new_line (tim_new_line), + .vcount (tim_vcount), + + .r_out (vga_r), + .g_out (vga_g), + .b_out (vga_b), + .enable (enable), + .frame_ready (frame_ready) +); + +assign vga_hs = tim_hs; +assign vga_vs = tim_vs; +assign vga_de = tim_de; +assign vga_hblank = tim_hblank; +assign vga_vblank = tim_vblank; +assign active = enable & frame_ready; + +endmodule diff --git a/rtl/pll/pll_0002.v b/rtl/pll/pll_0002.v index 7320a57..4c7ed14 100644 --- a/rtl/pll/pll_0002.v +++ b/rtl/pll/pll_0002.v @@ -25,7 +25,7 @@ module pll_0002( .output_clock_frequency0("100.000000 MHz"), .phase_shift0("0 ps"), .duty_cycle0(50), - .output_clock_frequency1("20.000000 MHz"), + .output_clock_frequency1("27027027 Hz"), .phase_shift1("0 ps"), .duty_cycle1(50), .output_clock_frequency2("0 MHz"), From ef38ec12e6b0a8804cb2af5878f1f1acb9dfcbe9 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 10 May 2026 00:26:10 +0200 Subject: [PATCH 2/3] fix: drive both modes from the native NTSC timing for clean CRT sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit kept the original cosine timing block (H_TOTAL=638, forced_scandoubler-conditional ce_pix) but switched the PLL to 27.027 MHz. That produced a line rate of ~42 kHz scandoubled or ~21 kHz interlaced — nothing standard, so a CRT could not lock on the analog VGA output. This commit makes native_video_timing the single source of truth for sync and DE in both modes: - ce_pix is now a fixed /4 divider of CLK_VIDEO -> 6.756 MHz pixel rate -> 15.749 kHz line rate (within 0.1% of NTSC 15.734 kHz). - VGA_HS/VS/DE always come from native_video_top regardless of status[9]. - The cosine + LFSR fallback paints into the active area only; outside DE we drive black so sync stays clean. - vvc steps on native_new_frame instead of the old vc wrap; cos LUT is indexed by vcount from the shared timing. - native_video_top exposes vcount and new_frame so the cosine path can reuse the same vertical position the FB reader sees. The cosine pattern still renders (it was always intended as fallback noise), but now at NTSC-spec 320x240 timing instead of broken 27 MHz / 638-cycle timing. Sync locks on real CRTs. PAL parametrisation is deferred — native_video_timing is currently NTSC-only. forced_scandoubler is still wired from hps_io but unused; preserve it as a known placeholder for the eventual PAL/scandoubler follow-up. --- menu.sv | 128 +++++++++++++++------------------------- rtl/native_video_top.sv | 16 +++-- 2 files changed, 59 insertions(+), 85 deletions(-) diff --git a/menu.sv b/menu.sv index 1249200..781f24d 100644 --- a/menu.sv +++ b/menu.sv @@ -459,77 +459,21 @@ wire PAL = status[4]; wire FB = status[5]; wire [2:0] led = status[8:6]; -reg [9:0] hc; -reg [9:0] vc; -reg [9:0] vvc; - -reg [lfsr_n:0] rnd_reg; -wire [lfsr_n:0] rnd; - -wire [5:0] rnd_c = {rnd_reg[0],rnd_reg[1],rnd_reg[2],rnd_reg[2],rnd_reg[2],rnd_reg[2]}; - -lfsr #(lfsr_n) random(rnd); - -always @(posedge CLK_VIDEO) begin - if(forced_scandoubler) ce_pix <= 1; - else ce_pix <= ~ce_pix; - - if(ce_pix) begin - if(hc == 637) begin - hc <= 0; - if(vc == (PAL ? (forced_scandoubler ? 623 : 311) : (forced_scandoubler ? 523 : 261))) begin - vc <= 0; - vvc <= vvc + 9'd6; - end else begin - vc <= vc + 1'd1; - end - end else begin - hc <= hc + 1'd1; - end - - rnd_reg <= rnd; - end -end - -reg HBlank; -reg HSync; -reg VBlank; -reg VSync; - -reg ce_pix; +// Pixel clock: CLK_VIDEO = 27.027 MHz; ce_pix /4 = ~6.756 MHz, which gives +// an NTSC-spec 15.734 kHz line rate when fed into native_video_timing +// (H_TOTAL=429). Both the cosine fallback and the FB reader use this ce_pix. +reg [1:0] ce_div; +reg ce_pix; always @(posedge CLK_VIDEO) begin - if (hc == 529) HBlank <= 1; - else if (hc == 0) HBlank <= 0; - - if (hc == 544) begin - HSync <= 1; - - if(PAL) begin - if(vc == (forced_scandoubler ? 609 : 304)) VSync <= 1; - else if (vc == (forced_scandoubler ? 617 : 308)) VSync <= 0; - - if(vc == (forced_scandoubler ? 601 : 300)) VBlank <= 1; - else if (vc == 0) VBlank <= 0; - end - else begin - if(vc == (forced_scandoubler ? 490 : 245)) VSync <= 1; - else if (vc == (forced_scandoubler ? 496 : 248)) VSync <= 0; - - if(vc == (forced_scandoubler ? 480 : 240)) VBlank <= 1; - else if (vc == 0) VBlank <= 0; - end - end - - if (hc == 590) HSync <= 0; + if (RESET) ce_div <= 2'd0; + else ce_div <= ce_div + 2'd1; + ce_pix <= (ce_div == 2'd0); end -reg [7:0] cos_out; -wire [5:0] cos_g = cos_out[7:3]+6'd32; -cos cos(vvc + {vc>>forced_scandoubler, 2'b00}, cos_out); - -wire [7:0] comp_v = (cos_g >= rnd_c) ? {cos_g - rnd_c, 2'b00} : 8'd0; - -// Runtime FB-mode gate driven by the HPS-side launcher via status[9]. +// 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.734 kHz line rate whether we're +// painting cosine noise or reading a Linux-rendered framebuffer. wire mode_zaparoo = status[9]; wire [7:0] native_r; @@ -538,6 +482,8 @@ 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_active; native_video_top native_video @@ -565,22 +511,46 @@ native_video_top native_video .vga_de (native_de), .vga_hblank (), .vga_vblank (), + .vga_vcount (native_vcount), + .vga_new_frame (native_new_frame), .enable (mode_zaparoo), .active (native_active) ); -// Mode A (default): cosine+LFSR pattern drives RGB and the original PAL/NTSC -// scandoubler timing drives sync/DE. HDMI wallpaper compositor runs unchanged. -// Mode B (status[9]=1, frame ready): native_video_top drives RGB+sync from the -// linux-rendered 320x240 RGBX8888 buffer in DDR. Falls back to cosine until the -// first frame is loaded so the screen is never undriven. +// Cosine + LFSR fallback noise pattern, painted into the 320x240 active area +// of the shared native timing. vvc steps once per frame; the LFSR walks every +// pixel; cos LUT is indexed by vvc + vcount so the pattern shifts vertically +// over time. Outside the active area we drive black to keep sync clean. +reg [9:0] vvc; +reg [lfsr_n:0] rnd_reg; +wire [lfsr_n:0] rnd; +wire [5:0] rnd_c = {rnd_reg[0],rnd_reg[1],rnd_reg[2],rnd_reg[2],rnd_reg[2],rnd_reg[2]}; + +lfsr #(lfsr_n) random(rnd); + +always @(posedge CLK_VIDEO) begin + if (RESET) vvc <= 10'd0; + else if (native_new_frame) vvc <= vvc + 10'd6; + if (ce_pix) rnd_reg <= rnd; +end + +reg [7:0] cos_out; +wire [5:0] cos_g = cos_out[7:3] + 6'd32; +cos cos(vvc + {native_vcount, 2'b00}, cos_out); + +wire [7:0] comp_v = (cos_g >= rnd_c) ? {cos_g - rnd_c, 2'b00} : 8'd0; + +// Mode A (default): cosine pattern paints into the native active area. +// Mode B (status[9]=1, frame ready): DDR-read RGB replaces the cosine pattern. +// Sync/DE come from the same native timing in both cases — the CRT sees one +// continuous, NTSC-spec signal regardless of which RGB source is selected. wire use_native = mode_zaparoo & native_active; -assign VGA_DE = use_native ? native_de : ~(HBlank | VBlank); -assign VGA_HS = use_native ? native_hs : HSync; -assign VGA_VS = use_native ? native_vs : VSync; -assign VGA_R = use_native ? native_r : comp_v; -assign VGA_G = use_native ? native_g : comp_v; -assign VGA_B = use_native ? native_b : comp_v; +assign VGA_DE = native_de; +assign VGA_HS = native_hs; +assign VGA_VS = native_vs; +assign VGA_R = use_native ? native_r : (native_de ? comp_v : 8'd0); +assign VGA_G = use_native ? native_g : (native_de ? comp_v : 8'd0); +assign VGA_B = use_native ? native_b : (native_de ? comp_v : 8'd0); endmodule diff --git a/rtl/native_video_top.sv b/rtl/native_video_top.sv index de4c0f9..55cf43b 100644 --- a/rtl/native_video_top.sv +++ b/rtl/native_video_top.sv @@ -25,6 +25,8 @@ module native_video_top output wire vga_de, output wire vga_hblank, output wire vga_vblank, + output wire [8:0] vga_vcount, + output wire vga_new_frame, input wire enable, output wire active @@ -86,11 +88,13 @@ native_video_reader reader .frame_ready (frame_ready) ); -assign vga_hs = tim_hs; -assign vga_vs = tim_vs; -assign vga_de = tim_de; -assign vga_hblank = tim_hblank; -assign vga_vblank = tim_vblank; -assign active = enable & frame_ready; +assign vga_hs = tim_hs; +assign vga_vs = tim_vs; +assign vga_de = tim_de; +assign vga_hblank = tim_hblank; +assign vga_vblank = tim_vblank; +assign vga_vcount = tim_vcount; +assign vga_new_frame = tim_new_frame; +assign active = enable & frame_ready; endmodule From 3f84a8e928f9525b98bfb66cd286a2f4667b34b2 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 10 May 2026 15:49:26 +0200 Subject: [PATCH 3/3] feat: add OSD H/V image centering offsets (+/-8 px/lines) Shifts the active image by repartitioning the native timing's front and back porches; H_TOTAL/V_TOTAL stay fixed so the CRT keeps the same line and frame rate. V blanking rebalanced from 6/3/13 to 8/3/11 to give a symmetric +/-8 budget without changing refresh rate. Co-Authored-By: Claude Opus 4.7 --- menu.sv | 17 +++++++++++++++-- rtl/native_video_timing.sv | 23 +++++++++++++++++------ rtl/native_video_top.sv | 8 +++++++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/menu.sv b/menu.sv index 781f24d..3c98392 100644 --- a/menu.sv +++ b/menu.sv @@ -208,10 +208,16 @@ assign LED_POWER[0]= FB ? led[2] : act_cnt2[26] ? act_cnt2[25:18] > act_cnt2[7:0 `include "build_id.v" +// Image centering: 4-bit signed in OSD ordering 0,+1..+7,-8..-1 so that the +// power-on default (status bits = 0) maps to "no shift". Bit pattern matches +// 4-bit two's complement when reinterpreted as signed. localparam CONF_STR = { "MENU;UART31250,MIDI;", "-;", - "V,v",`BUILD_DATE + "O[13:10],H Offset,0,+1,+2,+3,+4,+5,+6,+7,-8,-7,-6,-5,-4,-3,-2,-1;", + "O[17:14],V Offset,0,+1,+2,+3,+4,+5,+6,+7,-8,-7,-6,-5,-4,-3,-2,-1;", + "-;", + "V,v",`BUILD_DATE }; wire forced_scandoubler; @@ -514,7 +520,14 @@ native_video_top native_video .vga_vcount (native_vcount), .vga_new_frame (native_new_frame), .enable (mode_zaparoo), - .active (native_active) + .active (native_active), + + // status[13:10] / status[17:14] are 4-bit fields whose bit pattern + // matches signed two's complement when the OSD enum is ordered + // 0,+1..+7,-8..-1 (see CONF_STR). $signed() makes the reinterpretation + // explicit at the port boundary. + .h_offset ($signed(status[13:10])), + .v_offset ($signed(status[17:14])) ); // Cosine + LFSR fallback noise pattern, painted into the 320x240 active area diff --git a/rtl/native_video_timing.sv b/rtl/native_video_timing.sv index 54a2aee..6d46c8d 100644 --- a/rtl/native_video_timing.sv +++ b/rtl/native_video_timing.sv @@ -1,4 +1,7 @@ // Zaparoo native video timing: 320x240 at 15.734 kHz from 27 MHz / 4. +// h_offset/v_offset (signed -8..+7) shift the image by repartitioning +// front porch and back porch. H_TOTAL/V_TOTAL are invariant, so line +// rate and frame rate are unchanged regardless of offset values. module native_video_timing ( @@ -6,6 +9,10 @@ module native_video_timing input wire ce_pix, input wire reset, + // Image centering: positive = shift right/down (FP shrinks, BP grows). + input wire signed [3:0] h_offset, // -8..+7 pixels (budget H_FP=14 / H_BP=63) + input wire signed [3:0] v_offset, // -8..+7 lines (budget V_FP=8 / V_BP=11) + output reg hsync, output reg vsync, output reg hblank, @@ -23,16 +30,20 @@ localparam [5:0] H_SYNC = 6'd32; localparam [9:0] H_BP = 10'd63; localparam [9:0] H_TOTAL = 10'd429; +// V blanking rebalanced from 6/3/13 to 8/3/11 to give symmetric ±8 budget +// while preserving V_TOTAL=262 (and thus 59.94 Hz refresh). localparam [8:0] V_ACTIVE = 9'd240; -localparam [8:0] V_FP = 9'd6; +localparam [8:0] V_FP = 9'd8; localparam [4:0] V_SYNC = 5'd3; -localparam [8:0] V_BP = 9'd13; +localparam [8:0] V_BP = 9'd11; localparam [8:0] V_TOTAL = 9'd262; -localparam [9:0] H_SYNC_START = H_ACTIVE + H_FP; -localparam [9:0] H_SYNC_END = H_SYNC_START + H_SYNC; -localparam [8:0] V_SYNC_START = V_ACTIVE + V_FP; -localparam [8:0] V_SYNC_END = V_SYNC_START + V_SYNC; +// 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 - {{6{h_offset[3]}}, h_offset}); +wire [9:0] H_SYNC_END = H_SYNC_START + H_SYNC; +wire [8:0] V_SYNC_START = V_ACTIVE + (V_FP - {{5{v_offset[3]}}, v_offset}); +wire [8:0] V_SYNC_END = V_SYNC_START + V_SYNC; always @(posedge clk) begin if(reset) begin diff --git a/rtl/native_video_top.sv b/rtl/native_video_top.sv index 55cf43b..3e3127f 100644 --- a/rtl/native_video_top.sv +++ b/rtl/native_video_top.sv @@ -29,7 +29,11 @@ module native_video_top output wire vga_new_frame, input wire enable, - output wire active + output wire active, + + // OSD image centering: signed -8..+7 pixels/lines, 0 = no shift. + input wire signed [3:0] h_offset, + input wire signed [3:0] v_offset ); wire tim_hs; @@ -46,6 +50,8 @@ native_video_timing timing .clk (clk_vid), .ce_pix (ce_pix), .reset (reset), + .h_offset (h_offset), + .v_offset (v_offset), .hsync (tim_hs), .vsync (tim_vs), .hblank (tim_hblank),