// ============================================================================ // gs_z_flush_writer.sv (Ch323 Brick 2 — tile color/Z-flush LPDDR writer; PACKED) // // Writes a gs_stub tile-flush stream (one 32-bit word per tile pixel on design_clk — // either the TP_ZFLUSH Z stream or the TP_FLUSH color stream) to an FPGA-private // LPDDR4B scratch region (emif_clk). Used twice in the de25 top: once for Z, once for // the 32-bit color spill (the module is generic — it writes the 32-bit `data` at // BASE + `addr`). // // PACKED (Ch323 board fix): the FIRST cut did ONE single-32-bit-lane AXI write PER // pixel through a strictly-sequential AW->W->B FSM. The tile sweep emits one pixel per // design_clk (256 back-to-back), but each isolated write pays the full LPDDR round-trip // latency, so the drain fell far behind the emit rate and the 16-deep async FIFO // OVERFLOWED within ~16 px — dropping most of the spilled tile (grey-with-specks on // HDMI, spill_ovf=1). The proven framebuffer writer (gs_lpddr_axi_master) avoids this by // PACKING pixels into 256-bit beats; this writer now does the same with 32-bit lanes: // // design_clk : PACKER — accumulate 8 consecutive 32-bit pixels of a tile-row into one // 256-bit (32-byte) beat {block_off, data, strb}, keyed by the 32-byte // block address (addr[29:5]). A 16-px tile-row is exactly two 32-byte- // aligned blocks, so each beat completes naturally on its 8th px (no // dangling partial); a block-address change flushes the in-flight beat. // One FIFO push per 8 px => 8x fewer AXI writes => the sequential drain // keeps up with the same small FIFO. // async FIFO : gray-code CDC, carries {block_off[29:0], data[255:0], strb[31:0]}. // emif_clk : AXI FSM — pop a beat, issue a single-beat INCR write (AWSIZE=5 = 32 B, // AWLEN=0, full WSTRB on the populated lanes) at BASE + block_off. // // The packed beats land at exactly the offsets gs_tile_reload reads back (row r at // BASE + r*STRIDE, two 32-byte beats), so the reload side is unchanged. // // SEPARATE base (Codex): BASE is distinct from the color FB and the other scratch // region. A synthesis-off CANARY asserts no beat lands inside the canary-guard regions. // // Counters (Codex, distinct per instance): z_write_beats (256-bit beats written), // z_wr_errs (non-OKAY responses), fifo_overflow (sticky). // // NOTE (parity with gs_lpddr_axi_master): assumes the flush stream produces FULL 8-lane // beats (true for a tile width that is a multiple of 8 — the 16-wide spill tile). A // trailing partial beat at end-of-stream is NOT flushed. // ============================================================================ `timescale 1ns/1ps module gs_z_flush_writer #( parameter [29:0] Z_BASE = 30'h0010_0000, // LPDDR byte base of this scratch region (DISTINCT) parameter [29:0] FB_BASE = 30'd0, // color framebuffer base (canary guard) parameter int FB_BYTES = 32'h0001_0000, // color framebuffer size (canary guard) parameter [29:0] TEX_BASE = 30'h0020_0000, // other scratch base (canary guard) parameter int TEX_BYTES = 32'h0000_8000, // other scratch size (canary guard) parameter int FIFO_DEPTH = 16 )( // ---- GS / design clock domain: the flush emit stream ---- input logic gs_clk, input logic gs_rst_n, input logic enable, // 1 = accept emits (default off => inert) input logic z_flush_emit, // one pulse per tile pixel input logic [31:0] z_flush_addr, // scratch-RELATIVE byte offset (pixel_index*4) input logic [31:0] z_flush_data, // 32-bit word for this pixel (Z or color) // ---- status (emif_clk domain unless noted) ---- output logic [31:0] z_write_beats, // 256-bit beats written (cumulative) output logic [31:0] z_wr_errs, // non-OKAY write responses (cumulative) output logic fifo_overflow, // sticky (gs domain): an emit dropped (FIFO full) // Pipeline-split counters (Codex): emit/push (GS, reset by gs_rst_n=per-render core reset) and // pop/beats (EMIF, reset by trace_clear) localize any spill divergence: healthy = 512/64/64/64; // push>64 = packer partial beats; pop/beats>push = FIFO/reset broken; beats!=pop = AXI-FSM bug. input logic trace_clear, // resets the EMIF-domain counters (beats/pop) per render output logic [31:0] dbg_beat_count, // beats committed (B handshakes) since the last trace_clear output logic [31:0] dbg_emit_count, // GS: enable&&z_flush_emit accepted (per render) output logic [31:0] dbg_push_count, // GS: beats pushed into the FIFO (per render) output logic [31:0] dbg_pop_count, // EMIF: beats popped from the FIFO (since trace_clear) output logic [31:0] dbg_aw_count, // EMIF: AW handshakes (since trace_clear) output logic [31:0] dbg_w_count, // EMIF: W handshakes (since trace_clear) // ---- AXI4 write channel to the EMIF user port (emif_clk, 256-bit) ---- input logic axi_clk, input logic axi_rst_n, output logic [29:0] awaddr, output logic [1:0] awburst, output logic [6:0] awid, output logic [7:0] awlen, output logic [2:0] awsize, output logic awvalid, input logic awready, output logic [255:0] wdata, output logic [31:0] wstrb, output logic wlast, output logic wvalid, input logic wready, input logic [1:0] bresp, input logic bvalid, output logic bready ); assign awburst = 2'b01; // INCR assign awid = 7'd6; // distinct from FB writer(0)/probes/reload(6 too; arb priority disambiguates) assign awlen = 8'd0; // single beat assign awsize = 3'b101; // 32 bytes (256-bit) assign bready = 1'b1; localparam int PW = 318; // {block_off[29:0], data[255:0], strb[31:0]} // ============================ design_clk PACKER ============================ // Accumulate 8 consecutive 32-bit pixels into one 256-bit beat keyed by the 32-byte // block address; push a COMPLETE beat to the FIFO (one push per 8 px, not per px). logic [29:0] cur_off; logic [255:0] cur_data; logic [31:0] cur_strb; logic has_data; logic fifo_wr; logic [PW-1:0] fifo_wdata; wire fifo_full, fifo_empty; wire [PW-1:0] fifo_rdata; logic fifo_rd; always_ff @(posedge gs_clk or negedge gs_rst_n) begin if (!gs_rst_n) begin cur_off <= '0; cur_data <= '0; cur_strb <= '0; has_data <= 1'b0; fifo_wr <= 1'b0; fifo_wdata <= '0; fifo_overflow <= 1'b0; dbg_emit_count <= 32'd0; dbg_push_count <= 32'd0; end else begin fifo_wr <= 1'b0; if (enable && z_flush_emit) dbg_emit_count <= dbg_emit_count + 32'd1; if (fifo_wr && !fifo_full) dbg_push_count <= dbg_push_count + 32'd1; if (enable && z_flush_emit) begin logic [29:0] block_off; logic [2:0] lane; // 0..7 (which 32-bit lane) logic [255:0] nd; logic [31:0] ns; block_off = {z_flush_addr[29:5], 5'd0}; lane = z_flush_addr[4:2]; if (has_data && (block_off != cur_off)) begin // block changed before the previous beat filled — flush it, restart. fifo_wdata <= {cur_off, cur_data, cur_strb}; fifo_wr <= 1'b1; cur_off <= block_off; cur_data <= (256'd0 | (256'(z_flush_data) << ({29'd0, lane} * 32))); cur_strb <= (32'hF << ({29'd0, lane} * 4)); has_data <= 1'b1; end else begin nd = has_data ? cur_data : 256'd0; ns = has_data ? cur_strb : 32'd0; nd[ ({29'd0, lane} * 32) +: 32 ] = z_flush_data; ns[ ({29'd0, lane} * 4) +: 4 ] = 4'hF; if (&ns) begin // beat complete (all 8 lanes) — flush, beat consumed. fifo_wdata <= {block_off, nd, ns}; fifo_wr <= 1'b1; has_data <= 1'b0; end else begin cur_off <= block_off; cur_data <= nd; cur_strb <= ns; has_data <= 1'b1; end end end // overflow witness: a push attempt while the FIFO is full (must stay 0). if (fifo_wr && fifo_full) fifo_overflow <= 1'b1; end end // CRITICAL (Ch323 board bug): the async FIFO's two pointers MUST reset together. The // packer side uses gs_rst_n (= core reset, which a CORE_CTRL pulse toggles EVERY render); // the read side uses axi_rst_n (= EMIF cal, power-on only). If wrst_n followed gs_rst_n, // each render's core-reset pulse would reset ONLY the write pointer → gray-code pointer // desync → FIFO corruption (garbage data, spurious overflow, writes that never commit). // Sim missed it (single reset, both sides together). So reset BOTH FIFO sides from the // STABLE axi_rst_n: assert async on axi_rst_n, deassert synchronized into gs_clk. reg [1:0] wrst_sync; always_ff @(posedge gs_clk or negedge axi_rst_n) begin if (!axi_rst_n) wrst_sync <= 2'b00; else wrst_sync <= {wrst_sync[0], 1'b1}; end wire fifo_wrst_n = wrst_sync[1]; gs_async_fifo #(.WIDTH(PW), .DEPTH(FIFO_DEPTH)) u_fifo ( .wclk(gs_clk), .wrst_n(fifo_wrst_n), .wr(fifo_wr && !fifo_full), .wdata(fifo_wdata), .wfull(fifo_full), .rclk(axi_clk), .rrst_n(axi_rst_n), .rd(fifo_rd), .rdata(fifo_rdata), .rempty(fifo_empty) ); // ============================ emif_clk AXI FSM ============================ wire [29:0] beat_block = fifo_rdata[PW-1 -: 30]; // block_off[29:0] wire [255:0] beat_data = fifo_rdata[287:32]; wire [31:0] beat_strb = fifo_rdata[31:0]; wire [29:0] full_addr = Z_BASE + beat_block; typedef enum logic [1:0] { W_IDLE, W_AW, W_W, W_B } wstate_t; wstate_t wst; logic [29:0] lat_addr; logic [255:0] lat_data; logic [31:0] lat_strb; always_ff @(posedge axi_clk or negedge axi_rst_n) begin if (!axi_rst_n) begin wst <= W_IDLE; awaddr <= '0; awvalid <= 1'b0; wdata <= '0; wstrb <= '0; wlast <= 1'b0; wvalid <= 1'b0; fifo_rd <= 1'b0; z_write_beats <= 32'd0; z_wr_errs <= 32'd0; dbg_beat_count <= 32'd0; dbg_pop_count <= 32'd0; dbg_aw_count <= 32'd0; dbg_w_count <= 32'd0; lat_addr <= '0; lat_data <= '0; lat_strb <= '0; end else begin fifo_rd <= 1'b0; if (trace_clear) begin dbg_beat_count <= 32'd0; dbg_pop_count <= 32'd0; dbg_aw_count <= 32'd0; dbg_w_count <= 32'd0; end if (fifo_rd) dbg_pop_count <= dbg_pop_count + 32'd1; if (awvalid && awready) dbg_aw_count <= dbg_aw_count + 32'd1; if (wvalid && wready) dbg_w_count <= dbg_w_count + 32'd1; case (wst) W_IDLE: if (!fifo_empty) begin lat_addr <= full_addr; lat_data <= beat_data; lat_strb <= beat_strb; fifo_rd <= 1'b1; // pop this beat awaddr <= {full_addr[29:5], 5'd0}; // 32-byte aligned awvalid <= 1'b1; wst <= W_AW; // synthesis translate_off if (((full_addr >= FB_BASE) && (full_addr < FB_BASE + FB_BYTES[29:0])) || ((full_addr >= TEX_BASE) && (full_addr < TEX_BASE + TEX_BYTES[29:0]))) $error("gs_z_flush_writer CANARY: beat addr 0x%07x overlaps a canary-guard region", full_addr); // synthesis translate_on end W_AW: if (awready) begin awvalid <= 1'b0; wdata <= lat_data; wstrb <= lat_strb; wlast <= 1'b1; wvalid <= 1'b1; wst <= W_W; end W_W: if (wready) begin wvalid <= 1'b0; wlast <= 1'b0; wst <= W_B; end W_B: if (bvalid) begin if (bresp != 2'b00) z_wr_errs <= z_wr_errs + 32'd1; z_write_beats <= z_write_beats + 32'd1; dbg_beat_count <= dbg_beat_count + 32'd1; wst <= W_IDLE; end default: wst <= W_IDLE; endcase end end endmodule