EXP-0053: sdram_cdc_bridge.v decouples the SDRAM clock (115.2MHz, real value derived from the board's own existing PLL VCO=576MHz, verified via ecppll) from the 64MHz compute domain. Isolated: 137/137 tests, 0 errors, but real measured speedup is only 1.095x (not the naive 1.8x clock-ratio estimate) -- the CDC handshake's own synchronizer round-trip is a fixed per-transaction tax. EXP-0054: sdram_controller_openrow.v implements the page-hit/ keep-row-open optimization sdram_controller.v's own header had always deferred. weight_prefetch_engine_wide.v's real production traffic is strictly sequential per job and mostly stays within one SDRAM row -- closing/reopening it every tile (today's fixed auto-precharge policy) wastes tRP+tRCD for no reason. Isolated: 154/154 tests, 0 errors, 0 protocol violations (including the new refresh-while-row-open hazard, fixed via an explicit precharge-before-refresh path). Real measured speedup on the actual sequential access pattern: 1.141x. EXP-0055: composed both, then integrated into the real D-Stress benchmark (N=4/N=8, 256/256 bit-exact in every config). Result: open-row ALONE gives a real, consistent ~5% cycle-count improvement (47445/47468 vs baseline 49927/49909). CDC alone is a real ~8% REGRESSION. Combined is still a ~4% regression -- the CDC's fixed tax is paid on every transaction regardless of row-hit, and real D-Stress traffic interleaves weight-fetch/activation-result access far more than the isolated same-row test exercised, so open-row's real saving doesn't offset it. Decision: do not adopt the CDC approach; open-row alone is the disclosed, real win worth considering for production next, pending an explicit go-ahead (not applied to the real board top in this commit -- all additive, existing production RTL untouched). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
254 lines
11 KiB
Verilog
254 lines
11 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// EXP-0053 -- SDRAM clock-domain-crossing bridge.
|
|
//
|
|
// PURPOSE: let sdram_controller.v run on its OWN, faster clock
|
|
// (target: 115.2MHz, derived from the SAME PLL VCO as the existing
|
|
// 64MHz clk_sys -- see ecp5_pll_sys_clk_dualclk.v) while every
|
|
// existing caller (sdram_unified_backend.v's ctrl_req/ctrl_addr/...
|
|
// signals) stays on the unchanged 64MHz compute domain. Presents the
|
|
// EXACT same req/wr/addr/wdata/wmask -> rdata/ready/busy contract as
|
|
// sdram_controller.v itself, so it is a drop-in replacement for the
|
|
// direct sdram_controller instantiation at that one call site
|
|
// (verified by the isolated tb_sdram_cdc_bridge.v before any
|
|
// integration).
|
|
//
|
|
// WHY 115.2MHz and not the chip's rated 143MHz max (AS4C32M16SA-7,
|
|
// tCK=7ns min): the board's single PLL VCO is fixed at 576MHz by the
|
|
// existing, already-verified 64MHz CLKOP config (CLKFB_DIV=4,
|
|
// CLKOP_DIV=9 -- unchanged, not touched by this experiment). The only
|
|
// integer VCO/N divisors near the chip's ceiling are 576/4=144MHz
|
|
// (0.8% OVER the 143MHz max -- rejected, not "correctness first") and
|
|
// 576/5=115.2MHz (real ~19% margin under the rated max). 115.2MHz is
|
|
// therefore the fastest SAFE clock obtainable from this board's
|
|
// existing PLL without touching the verified 64MHz compute domain.
|
|
// Real measured gain vs the current 64MHz single-domain design is
|
|
// therefore 115.2/64 = 1.8x raw controller-clock speedup, NOT the 2.2x
|
|
// a naive 143MHz assumption would suggest -- this correction is
|
|
// intentional, verified against real ecppll output, not estimated.
|
|
//
|
|
// PROTOCOL: single-outstanding-request only (matches every existing
|
|
// caller's own req/busy/ready idiom exactly -- this bridge does NOT
|
|
// add multi-request pipelining; that is EXP-0052's explicitly
|
|
// deferred, larger, riskier follow-up, out of scope here). Because at
|
|
// most one transaction is ever in flight, a classic two-domain
|
|
// "toggle + last-seen" handshake is provably safe:
|
|
// - the requesting (slow) domain latches addr/wr/wdata/wmask and
|
|
// flips req_toggle_slow on the SAME clock edge, then holds ALL of
|
|
// those signals perfectly stable (no new request is ever issued
|
|
// while busy=1) until the response toggle comes back;
|
|
// - the fast domain double-flop-synchronizes req_toggle_slow (2 FF,
|
|
// standard metastability margin) and compares it against its own
|
|
// "last serviced" copy -- a mismatch means a new request is
|
|
// pending. Because addr/wr/wdata/wmask changed on the SAME edge
|
|
// that flipped the toggle, and never change again before the
|
|
// response, they are safe to sample directly (no per-bit
|
|
// synchronizer needed) once the synchronized toggle has visibly
|
|
// changed -- this is the standard "quasi-static bus + toggle"
|
|
// CDC idiom, not a shortcut.
|
|
// - the same reasoning applies in reverse for ack_toggle_fast/rdata
|
|
// going back to the slow domain.
|
|
// Reset: rst_slow and rst_fast are separate inputs, each assumed
|
|
// ALREADY synchronized to its own clock domain by the caller (this
|
|
// module does not itself synchronize an async reset -- matches this
|
|
// project's existing convention of a single, pre-synchronized `rst`
|
|
// per clock domain, see ecp5_pll_sys_clk.v's own reset handling).
|
|
// ============================================================
|
|
module sdram_cdc_bridge #(
|
|
parameter CLK_FREQ_MHZ_FAST = 115, // deliberately rounded DOWN from
|
|
// the real 115.2MHz (never over-
|
|
// count available ns/cycle --
|
|
// same "ceiling division" spirit
|
|
// as sdram_controller.v's own
|
|
// ns_to_cycles), so every derived
|
|
// timing constant (T_RCD/T_RP/...)
|
|
// gets AT LEAST as many cycles as
|
|
// the real, slightly-faster clock
|
|
// requires.
|
|
parameter BURST_LEN = 8,
|
|
parameter ROW_BITS = 13,
|
|
parameter COL_BITS = 10,
|
|
parameter BANK_BITS = 2,
|
|
parameter ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS
|
|
)(
|
|
input wire clk_slow,
|
|
input wire rst_slow, // pre-synchronized to clk_slow
|
|
input wire clk_fast,
|
|
input wire rst_fast, // pre-synchronized to clk_fast
|
|
|
|
// ---- slow-domain caller interface (identical shape to
|
|
// sdram_controller.v's own ports) ----
|
|
input wire req,
|
|
input wire wr,
|
|
input wire [ADDR_WIDTH-1:0] addr,
|
|
input wire [16*BURST_LEN-1:0] wdata,
|
|
input wire [2*BURST_LEN-1:0] wmask,
|
|
output reg [16*BURST_LEN-1:0] rdata,
|
|
output reg ready,
|
|
output wire busy,
|
|
|
|
// ---- real SDRAM pins, driven directly by the fast-domain
|
|
// sdram_controller instance ----
|
|
output wire sdram_cke,
|
|
output wire sdram_cs_n,
|
|
output wire sdram_ras_n,
|
|
output wire sdram_cas_n,
|
|
output wire sdram_we_n,
|
|
output wire [BANK_BITS-1:0] sdram_ba,
|
|
output wire [ROW_BITS-1:0] sdram_a,
|
|
inout wire [15:0] sdram_dq,
|
|
output wire [1:0] sdram_dqm
|
|
);
|
|
|
|
// ============================================================
|
|
// Slow domain: capture request, drive toggle, wait for ack
|
|
// ============================================================
|
|
reg busy_slow;
|
|
reg req_toggle_slow;
|
|
reg wr_lat;
|
|
reg [ADDR_WIDTH-1:0] addr_lat;
|
|
reg [16*BURST_LEN-1:0] wdata_lat;
|
|
reg [2*BURST_LEN-1:0] wmask_lat;
|
|
|
|
assign busy = busy_slow;
|
|
|
|
// synchronize ack_toggle_fast (fast domain) into the slow domain
|
|
wire ack_toggle_fast;
|
|
reg ack_toggle_sync1, ack_toggle_sync2;
|
|
always @(posedge clk_slow) begin
|
|
if (rst_slow) begin
|
|
ack_toggle_sync1 <= 1'b0;
|
|
ack_toggle_sync2 <= 1'b0;
|
|
end else begin
|
|
ack_toggle_sync1 <= ack_toggle_fast;
|
|
ack_toggle_sync2 <= ack_toggle_sync1;
|
|
end
|
|
end
|
|
|
|
reg last_ack_toggle_seen_slow;
|
|
wire [16*BURST_LEN-1:0] rdata_fast_captured;
|
|
|
|
always @(posedge clk_slow) begin
|
|
if (rst_slow) begin
|
|
busy_slow <= 1'b0;
|
|
req_toggle_slow <= 1'b0;
|
|
last_ack_toggle_seen_slow <= 1'b0;
|
|
ready <= 1'b0;
|
|
rdata <= {(16*BURST_LEN){1'b0}};
|
|
wr_lat <= 1'b0;
|
|
addr_lat <= {ADDR_WIDTH{1'b0}};
|
|
wdata_lat <= {(16*BURST_LEN){1'b0}};
|
|
wmask_lat <= {(2*BURST_LEN){1'b0}};
|
|
end else begin
|
|
ready <= 1'b0;
|
|
|
|
if (req && !busy_slow) begin
|
|
wr_lat <= wr;
|
|
addr_lat <= addr;
|
|
wdata_lat <= wdata;
|
|
wmask_lat <= wmask;
|
|
req_toggle_slow <= ~req_toggle_slow;
|
|
busy_slow <= 1'b1;
|
|
end
|
|
|
|
if (busy_slow && (ack_toggle_sync2 != last_ack_toggle_seen_slow)) begin
|
|
last_ack_toggle_seen_slow <= ack_toggle_sync2;
|
|
rdata <= rdata_fast_captured;
|
|
ready <= 1'b1;
|
|
busy_slow <= 1'b0;
|
|
end
|
|
end
|
|
end
|
|
|
|
// ============================================================
|
|
// Fast domain: synchronize request toggle, drive the real
|
|
// sdram_controller, capture response, drive ack toggle back
|
|
// ============================================================
|
|
reg ctrl_req_f;
|
|
reg ctrl_wr_f;
|
|
reg [ADDR_WIDTH-1:0] ctrl_addr_f;
|
|
reg [16*BURST_LEN-1:0] ctrl_wdata_f;
|
|
reg [2*BURST_LEN-1:0] ctrl_wmask_f;
|
|
wire [16*BURST_LEN-1:0] ctrl_rdata_f;
|
|
wire ctrl_ready_f, ctrl_busy_f;
|
|
|
|
reg req_toggle_sync1, req_toggle_sync2;
|
|
always @(posedge clk_fast) begin
|
|
if (rst_fast) begin
|
|
req_toggle_sync1 <= 1'b0;
|
|
req_toggle_sync2 <= 1'b0;
|
|
end else begin
|
|
req_toggle_sync1 <= req_toggle_slow;
|
|
req_toggle_sync2 <= req_toggle_sync1;
|
|
end
|
|
end
|
|
|
|
localparam F_IDLE = 1'b0, F_WAIT = 1'b1;
|
|
reg f_state;
|
|
reg last_req_toggle_seen_fast;
|
|
reg ack_toggle_fast_r;
|
|
reg [16*BURST_LEN-1:0] rdata_fast_captured_r;
|
|
|
|
assign ack_toggle_fast = ack_toggle_fast_r;
|
|
assign rdata_fast_captured = rdata_fast_captured_r;
|
|
|
|
always @(posedge clk_fast) begin
|
|
if (rst_fast) begin
|
|
f_state <= F_IDLE;
|
|
last_req_toggle_seen_fast <= 1'b0;
|
|
ack_toggle_fast_r <= 1'b0;
|
|
rdata_fast_captured_r <= {(16*BURST_LEN){1'b0}};
|
|
ctrl_req_f <= 1'b0;
|
|
ctrl_wr_f <= 1'b0;
|
|
ctrl_addr_f <= {ADDR_WIDTH{1'b0}};
|
|
ctrl_wdata_f<= {(16*BURST_LEN){1'b0}};
|
|
ctrl_wmask_f<= {(2*BURST_LEN){1'b0}};
|
|
end else begin
|
|
ctrl_req_f <= 1'b0;
|
|
case (f_state)
|
|
F_IDLE: begin
|
|
if (req_toggle_sync2 != last_req_toggle_seen_fast) begin
|
|
// addr_lat/wr_lat/wdata_lat/wmask_lat (slow-
|
|
// domain regs) are quasi-static: they changed
|
|
// on the exact same slow-domain edge that
|
|
// flipped req_toggle_slow, and will not change
|
|
// again until busy_slow deasserts (long after
|
|
// this transaction completes) -- safe to
|
|
// sample directly, see module header.
|
|
ctrl_req_f <= 1'b1;
|
|
ctrl_wr_f <= wr_lat;
|
|
ctrl_addr_f <= addr_lat;
|
|
ctrl_wdata_f<= wdata_lat;
|
|
ctrl_wmask_f<= wmask_lat;
|
|
last_req_toggle_seen_fast <= req_toggle_sync2;
|
|
f_state <= F_WAIT;
|
|
end
|
|
end
|
|
F_WAIT: begin
|
|
if (ctrl_ready_f) begin
|
|
rdata_fast_captured_r <= ctrl_rdata_f;
|
|
ack_toggle_fast_r <= ~ack_toggle_fast_r;
|
|
f_state <= F_IDLE;
|
|
end
|
|
end
|
|
default: f_state <= F_IDLE;
|
|
endcase
|
|
end
|
|
end
|
|
|
|
sdram_controller #(
|
|
.CLK_FREQ_MHZ(CLK_FREQ_MHZ_FAST), .BURST_LEN(BURST_LEN),
|
|
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
|
|
) u_sdram_ctrl (
|
|
.clk(clk_fast), .rst(rst_fast),
|
|
.req(ctrl_req_f), .wr(ctrl_wr_f), .addr(ctrl_addr_f),
|
|
.wdata(ctrl_wdata_f), .wmask(ctrl_wmask_f),
|
|
.rdata(ctrl_rdata_f), .ready(ctrl_ready_f), .busy(ctrl_busy_f),
|
|
.sdram_cke(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n),
|
|
.sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n),
|
|
.sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm)
|
|
);
|
|
|
|
endmodule
|