exp: SDRAM CDC bridge + open-row policy (EXP-0053/54/55) -- open-row is a real ~5% D-Stress win, CDC bridge measured net-negative once integrated

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
This commit is contained in:
2026-09-16 07:07:57 +02:00
co-authored by Claude Sonnet 5
parent 03b5cbc25b
commit ee5a68f6e6
16 changed files with 6499 additions and 0 deletions
@@ -0,0 +1,173 @@
`timescale 1ns/1ps
// ================================================================
// Neural Memory System (NMS) -- STEP19 real hardware-facing top level.
//
// SINGLE EXTERNAL SDRAM ONLY. Forked from nms_neural_multiprocessor_
// sdram_pack128.v (STEP18) with the ONE change this step's own
// governing spec mandates: the real hardware/v1/rtl/psram_controller.v
// + memory_interface.v pairing (activation-fill + result-writeback,
// 16-bit) is REMOVED from the V2 physical path entirely and replaced
// by sdram_unified_backend.v's own AR port, sharing the SAME single
// physical AS4C4M16SA-6TIN SDRAM chip and the SAME single sdram_
// controller.v instance the weight-fetch path (W port) already uses.
//
// slot_mem_arbiter.v (16-bit, activation+result) and slot_mem_
// arbiter_wide.v (64-bit, weight) are BOTH reused completely
// UNCHANGED -- their own downstream ports now both terminate at
// sdram_unified_backend.v instead of two separate physical chains.
// nms_dataflow_core_sdram.v, nms_activation_fill_ctrl_v3.v, nms_
// memory_manager_stream_wide.v, weight_prefetch_engine_wide.v, and
// neural_processor.v are ALL byte-for-byte unchanged -- this is a
// pure memory-side substitution, per the governing spec's own
// explicit instruction.
//
// V1 (hardware/v1/**) is untouched -- psram_controller.v and memory_
// interface.v simply are no longer INSTANTIATED by this top-level;
// neither file was modified, and V1's own golden-reference status is
// unaffected.
//
// Real pin count (weight+activation+result, ALL through ONE chip):
// 2(BA)+12(A)+1(CKE)+1(CS#)+1(RAS#)+1(CAS#)+1(WE#)+2(DQM)+16(DQ) = 37
// pins total -- the SAME 37 pins the weight-only path already used in
// STEP16-18 (no NEW physical SDRAM pins are needed to add activation/
// result traffic, since it shares the identical physical bus).
// ================================================================
module nms_neural_multiprocessor_sdram_cdc #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ACC_WIDTH = 32,
parameter ADDR_WIDTH = 26,
parameter N_SLOTS = 2,
parameter N_NODES = 16,
parameter MAX_DEPS = 4,
parameter QUEUE_DEPTH = 8,
parameter MAX_TILES = 16,
parameter PREFETCH_DISTANCE = 8,
parameter CLK_FREQ_MHZ = 80
)(
input wire clk,
input wire rst,
input wire clk_fast,
input wire rst_fast,
input wire reg_valid,
output wire reg_ready,
input wire [$clog2(N_NODES)-1:0] reg_node_id,
input wire [$clog2(MAX_DEPS+1)-1:0] reg_required,
input wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids,
input wire [ADDR_WIDTH-1:0] reg_x_base,
input wire [ADDR_WIDTH-1:0] reg_w_base,
input wire [15:0] reg_n_tiles,
input wire [ADDR_WIDTH-1:0] reg_result_addr,
// FPGA_DATA_READY: system-idle sticky flag, see nms_dataflow_core_sdram.v
output wire data_ready,
// ---- STEP19: ONE physical SDRAM interface, ALL traffic
// (weights + activations + results) ----
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 [1:0] sdram_ba,
output wire [12:0] sdram_a,
inout wire [15:0] sdram_dq,
output wire [1:0] sdram_dqm
);
wire [N_SLOTS:0] slot_mem_req, slot_mem_wr;
wire [ADDR_WIDTH*(N_SLOTS+1)-1:0] slot_mem_addr;
wire [16*(N_SLOTS+1)-1:0] slot_mem_wdata, slot_mem_rdata;
wire [N_SLOTS:0] slot_mem_lb_n, slot_mem_ub_n;
wire [N_SLOTS:0] slot_mem_ready;
wire [N_SLOTS-1:0] wide_slot_mem_req;
wire [ADDR_WIDTH*N_SLOTS-1:0] wide_slot_mem_addr;
wire [64*N_SLOTS-1:0] wide_slot_mem_rdata;
wire [N_SLOTS-1:0] wide_slot_mem_ready;
nms_dataflow_core_sdram #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH),
.N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH),
.MAX_TILES(MAX_TILES), .PREFETCH_DISTANCE(PREFETCH_DISTANCE)
) u_dataflow_core (
.clk(clk), .rst(rst),
.reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id),
.reg_required(reg_required), .reg_producer_ids(reg_producer_ids),
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
.reg_result_addr(reg_result_addr),
.data_ready(data_ready),
.slot_mem_req(slot_mem_req), .slot_mem_wr(slot_mem_wr), .slot_mem_addr(slot_mem_addr),
.slot_mem_wdata(slot_mem_wdata), .slot_mem_lb_n(slot_mem_lb_n), .slot_mem_ub_n(slot_mem_ub_n),
.slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready),
.wide_slot_mem_req(wide_slot_mem_req), .wide_slot_mem_addr(wide_slot_mem_addr),
.wide_slot_mem_rdata(wide_slot_mem_rdata), .wide_slot_mem_ready(wide_slot_mem_ready)
);
// ---- AR: activation-fill (shared, 1 port) + per-slot result
// writeback (N_SLOTS ports), arbitrated exactly as before ----
wire arb_m_req, arb_m_wr;
wire [ADDR_WIDTH-1:0] arb_m_addr;
wire [15:0] arb_m_wdata;
wire arb_m_lb_n, arb_m_ub_n;
wire [15:0] arb_m_rdata;
wire arb_m_ready;
slot_mem_arbiter #(
.ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS+1)
) u_arbiter (
.clk(clk), .rst(rst),
.s_req(slot_mem_req), .s_wr(slot_mem_wr), .s_addr(slot_mem_addr),
.s_wdata(slot_mem_wdata), .s_lb_n(slot_mem_lb_n), .s_ub_n(slot_mem_ub_n),
.s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready),
.m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata),
.m_lb_n(arb_m_lb_n), .m_ub_n(arb_m_ub_n),
.m_rdata(arb_m_rdata), .m_ready(arb_m_ready)
);
// ---- W: weight fetch (N_SLOTS ports), arbitrated exactly as
// before -- weight fetch never writes, same tie-off convention
// as STEP16-18 ----
wire [N_SLOTS-1:0] wide_s_wr = {N_SLOTS{1'b0}};
wire [64*N_SLOTS-1:0] wide_s_wdata = {(64*N_SLOTS){1'b0}};
wire [N_SLOTS-1:0] wide_s_lb_n = {N_SLOTS{1'b0}};
wire [N_SLOTS-1:0] wide_s_ub_n = {N_SLOTS{1'b0}};
wire wide_arb_m_req, wide_arb_m_wr;
wire [ADDR_WIDTH-1:0] wide_arb_m_addr;
wire [63:0] wide_arb_m_wdata;
wire wide_arb_m_lb_n, wide_arb_m_ub_n;
wire [63:0] wide_arb_m_rdata;
wire wide_arb_m_ready;
slot_mem_arbiter_wide #(
.ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS), .DATA_WIDTH(64)
) u_arbiter_wide (
.clk(clk), .rst(rst),
.s_req(wide_slot_mem_req), .s_wr(wide_s_wr), .s_addr(wide_slot_mem_addr),
.s_wdata(wide_s_wdata), .s_lb_n(wide_s_lb_n), .s_ub_n(wide_s_ub_n),
.s_rdata(wide_slot_mem_rdata), .s_ready(wide_slot_mem_ready),
.m_req(wide_arb_m_req), .m_wr(wide_arb_m_wr), .m_addr(wide_arb_m_addr), .m_wdata(wide_arb_m_wdata),
.m_lb_n(wide_arb_m_lb_n), .m_ub_n(wide_arb_m_ub_n),
.m_rdata(wide_arb_m_rdata), .m_ready(wide_arb_m_ready)
);
// ---- STEP19: ONE physical SDRAM backend, both W and AR ports ----
sdram_unified_backend_cdc #(
.ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ)
) u_sdram_backend (
.clk(clk), .rst(rst), .clk_fast(clk_fast), .rst_fast(rst_fast),
.w_req(wide_arb_m_req), .w_addr(wide_arb_m_addr),
.w_rdata(wide_arb_m_rdata), .w_ready(wide_arb_m_ready),
.ar_req(arb_m_req), .ar_wr(arb_m_wr), .ar_addr(arb_m_addr), .ar_wdata(arb_m_wdata),
.ar_lb_n(arb_m_lb_n), .ar_ub_n(arb_m_ub_n),
.ar_rdata(arb_m_rdata), .ar_ready(arb_m_ready),
.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
@@ -0,0 +1,186 @@
`timescale 1ns/1ps
// ================================================================
// EXP-0055 -- combined fork of nms_neural_multiprocessor_sdram_
// unified.v (STEP19), same minimal-diff discipline as the pipelined
// fork (EXP-0052): the ONLY changes are two new clk_fast/rst_fast
// ports (forwarded straight through) and instantiating sdram_unified_
// backend_combined.v (EXP-0055's CDC+page-hit backend) instead of
// sdram_unified_backend.v. u_dataflow_core, both arbiters, are all
// byte-for-byte unchanged.
//
// Everything below is STEP19's own original header, preserved as-is:
//
// Neural Memory System (NMS) -- STEP19 real hardware-facing top level.
//
// SINGLE EXTERNAL SDRAM ONLY. Forked from nms_neural_multiprocessor_
// sdram_pack128.v (STEP18) with the ONE change this step's own
// governing spec mandates: the real hardware/v1/rtl/psram_controller.v
// + memory_interface.v pairing (activation-fill + result-writeback,
// 16-bit) is REMOVED from the V2 physical path entirely and replaced
// by sdram_unified_backend.v's own AR port, sharing the SAME single
// physical AS4C4M16SA-6TIN SDRAM chip and the SAME single sdram_
// controller.v instance the weight-fetch path (W port) already uses.
//
// slot_mem_arbiter.v (16-bit, activation+result) and slot_mem_
// arbiter_wide.v (64-bit, weight) are BOTH reused completely
// UNCHANGED -- their own downstream ports now both terminate at
// sdram_unified_backend.v instead of two separate physical chains.
// nms_dataflow_core_sdram.v, nms_activation_fill_ctrl_v3.v, nms_
// memory_manager_stream_wide.v, weight_prefetch_engine_wide.v, and
// neural_processor.v are ALL byte-for-byte unchanged -- this is a
// pure memory-side substitution, per the governing spec's own
// explicit instruction.
//
// V1 (hardware/v1/**) is untouched -- psram_controller.v and memory_
// interface.v simply are no longer INSTANTIATED by this top-level;
// neither file was modified, and V1's own golden-reference status is
// unaffected.
//
// Real pin count (weight+activation+result, ALL through ONE chip):
// 2(BA)+12(A)+1(CKE)+1(CS#)+1(RAS#)+1(CAS#)+1(WE#)+2(DQM)+16(DQ) = 37
// pins total -- the SAME 37 pins the weight-only path already used in
// STEP16-18 (no NEW physical SDRAM pins are needed to add activation/
// result traffic, since it shares the identical physical bus).
// ================================================================
module nms_neural_multiprocessor_sdram_combined #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ACC_WIDTH = 32,
parameter ADDR_WIDTH = 26,
parameter N_SLOTS = 2,
parameter N_NODES = 16,
parameter MAX_DEPS = 4,
parameter QUEUE_DEPTH = 8,
parameter MAX_TILES = 16,
parameter PREFETCH_DISTANCE = 8,
parameter CLK_FREQ_MHZ = 80
)(
input wire clk,
input wire rst,
// ---- EXP-0055: fast SDRAM clock domain, forwarded straight
// through to sdram_unified_backend_combined.v -- see that
// module's own header ----
input wire clk_fast,
input wire rst_fast,
input wire reg_valid,
output wire reg_ready,
input wire [$clog2(N_NODES)-1:0] reg_node_id,
input wire [$clog2(MAX_DEPS+1)-1:0] reg_required,
input wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids,
input wire [ADDR_WIDTH-1:0] reg_x_base,
input wire [ADDR_WIDTH-1:0] reg_w_base,
input wire [15:0] reg_n_tiles,
input wire [ADDR_WIDTH-1:0] reg_result_addr,
// FPGA_DATA_READY: system-idle sticky flag, see nms_dataflow_core_sdram.v
output wire data_ready,
// ---- STEP19: ONE physical SDRAM interface, ALL traffic
// (weights + activations + results) ----
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 [1:0] sdram_ba,
output wire [12:0] sdram_a,
inout wire [15:0] sdram_dq,
output wire [1:0] sdram_dqm
);
wire [N_SLOTS:0] slot_mem_req, slot_mem_wr;
wire [ADDR_WIDTH*(N_SLOTS+1)-1:0] slot_mem_addr;
wire [16*(N_SLOTS+1)-1:0] slot_mem_wdata, slot_mem_rdata;
wire [N_SLOTS:0] slot_mem_lb_n, slot_mem_ub_n;
wire [N_SLOTS:0] slot_mem_ready;
wire [N_SLOTS-1:0] wide_slot_mem_req;
wire [ADDR_WIDTH*N_SLOTS-1:0] wide_slot_mem_addr;
wire [64*N_SLOTS-1:0] wide_slot_mem_rdata;
wire [N_SLOTS-1:0] wide_slot_mem_ready;
nms_dataflow_core_sdram #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH),
.N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH),
.MAX_TILES(MAX_TILES), .PREFETCH_DISTANCE(PREFETCH_DISTANCE)
) u_dataflow_core (
.clk(clk), .rst(rst),
.reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id),
.reg_required(reg_required), .reg_producer_ids(reg_producer_ids),
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
.reg_result_addr(reg_result_addr),
.data_ready(data_ready),
.slot_mem_req(slot_mem_req), .slot_mem_wr(slot_mem_wr), .slot_mem_addr(slot_mem_addr),
.slot_mem_wdata(slot_mem_wdata), .slot_mem_lb_n(slot_mem_lb_n), .slot_mem_ub_n(slot_mem_ub_n),
.slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready),
.wide_slot_mem_req(wide_slot_mem_req), .wide_slot_mem_addr(wide_slot_mem_addr),
.wide_slot_mem_rdata(wide_slot_mem_rdata), .wide_slot_mem_ready(wide_slot_mem_ready)
);
// ---- AR: activation-fill (shared, 1 port) + per-slot result
// writeback (N_SLOTS ports), arbitrated exactly as before ----
wire arb_m_req, arb_m_wr;
wire [ADDR_WIDTH-1:0] arb_m_addr;
wire [15:0] arb_m_wdata;
wire arb_m_lb_n, arb_m_ub_n;
wire [15:0] arb_m_rdata;
wire arb_m_ready;
slot_mem_arbiter #(
.ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS+1)
) u_arbiter (
.clk(clk), .rst(rst),
.s_req(slot_mem_req), .s_wr(slot_mem_wr), .s_addr(slot_mem_addr),
.s_wdata(slot_mem_wdata), .s_lb_n(slot_mem_lb_n), .s_ub_n(slot_mem_ub_n),
.s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready),
.m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata),
.m_lb_n(arb_m_lb_n), .m_ub_n(arb_m_ub_n),
.m_rdata(arb_m_rdata), .m_ready(arb_m_ready)
);
// ---- W: weight fetch (N_SLOTS ports), arbitrated exactly as
// before -- weight fetch never writes, same tie-off convention
// as STEP16-18 ----
wire [N_SLOTS-1:0] wide_s_wr = {N_SLOTS{1'b0}};
wire [64*N_SLOTS-1:0] wide_s_wdata = {(64*N_SLOTS){1'b0}};
wire [N_SLOTS-1:0] wide_s_lb_n = {N_SLOTS{1'b0}};
wire [N_SLOTS-1:0] wide_s_ub_n = {N_SLOTS{1'b0}};
wire wide_arb_m_req, wide_arb_m_wr;
wire [ADDR_WIDTH-1:0] wide_arb_m_addr;
wire [63:0] wide_arb_m_wdata;
wire wide_arb_m_lb_n, wide_arb_m_ub_n;
wire [63:0] wide_arb_m_rdata;
wire wide_arb_m_ready;
slot_mem_arbiter_wide #(
.ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS), .DATA_WIDTH(64)
) u_arbiter_wide (
.clk(clk), .rst(rst),
.s_req(wide_slot_mem_req), .s_wr(wide_s_wr), .s_addr(wide_slot_mem_addr),
.s_wdata(wide_s_wdata), .s_lb_n(wide_s_lb_n), .s_ub_n(wide_s_ub_n),
.s_rdata(wide_slot_mem_rdata), .s_ready(wide_slot_mem_ready),
.m_req(wide_arb_m_req), .m_wr(wide_arb_m_wr), .m_addr(wide_arb_m_addr), .m_wdata(wide_arb_m_wdata),
.m_lb_n(wide_arb_m_lb_n), .m_ub_n(wide_arb_m_ub_n),
.m_rdata(wide_arb_m_rdata), .m_ready(wide_arb_m_ready)
);
// ---- STEP19: ONE physical SDRAM backend, both W and AR ports ----
sdram_unified_backend_combined #(
.ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ)
) u_sdram_backend (
.clk(clk), .rst(rst), .clk_fast(clk_fast), .rst_fast(rst_fast),
.w_req(wide_arb_m_req), .w_addr(wide_arb_m_addr),
.w_rdata(wide_arb_m_rdata), .w_ready(wide_arb_m_ready),
.ar_req(arb_m_req), .ar_wr(arb_m_wr), .ar_addr(arb_m_addr), .ar_wdata(arb_m_wdata),
.ar_lb_n(arb_m_lb_n), .ar_ub_n(arb_m_ub_n),
.ar_rdata(arb_m_rdata), .ar_ready(arb_m_ready),
.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
@@ -0,0 +1,171 @@
`timescale 1ns/1ps
// ================================================================
// Neural Memory System (NMS) -- STEP19 real hardware-facing top level.
//
// SINGLE EXTERNAL SDRAM ONLY. Forked from nms_neural_multiprocessor_
// sdram_pack128.v (STEP18) with the ONE change this step's own
// governing spec mandates: the real hardware/v1/rtl/psram_controller.v
// + memory_interface.v pairing (activation-fill + result-writeback,
// 16-bit) is REMOVED from the V2 physical path entirely and replaced
// by sdram_unified_backend.v's own AR port, sharing the SAME single
// physical AS4C4M16SA-6TIN SDRAM chip and the SAME single sdram_
// controller.v instance the weight-fetch path (W port) already uses.
//
// slot_mem_arbiter.v (16-bit, activation+result) and slot_mem_
// arbiter_wide.v (64-bit, weight) are BOTH reused completely
// UNCHANGED -- their own downstream ports now both terminate at
// sdram_unified_backend.v instead of two separate physical chains.
// nms_dataflow_core_sdram.v, nms_activation_fill_ctrl_v3.v, nms_
// memory_manager_stream_wide.v, weight_prefetch_engine_wide.v, and
// neural_processor.v are ALL byte-for-byte unchanged -- this is a
// pure memory-side substitution, per the governing spec's own
// explicit instruction.
//
// V1 (hardware/v1/**) is untouched -- psram_controller.v and memory_
// interface.v simply are no longer INSTANTIATED by this top-level;
// neither file was modified, and V1's own golden-reference status is
// unaffected.
//
// Real pin count (weight+activation+result, ALL through ONE chip):
// 2(BA)+12(A)+1(CKE)+1(CS#)+1(RAS#)+1(CAS#)+1(WE#)+2(DQM)+16(DQ) = 37
// pins total -- the SAME 37 pins the weight-only path already used in
// STEP16-18 (no NEW physical SDRAM pins are needed to add activation/
// result traffic, since it shares the identical physical bus).
// ================================================================
module nms_neural_multiprocessor_sdram_openrow #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ACC_WIDTH = 32,
parameter ADDR_WIDTH = 26,
parameter N_SLOTS = 2,
parameter N_NODES = 16,
parameter MAX_DEPS = 4,
parameter QUEUE_DEPTH = 8,
parameter MAX_TILES = 16,
parameter PREFETCH_DISTANCE = 8,
parameter CLK_FREQ_MHZ = 80
)(
input wire clk,
input wire rst,
input wire reg_valid,
output wire reg_ready,
input wire [$clog2(N_NODES)-1:0] reg_node_id,
input wire [$clog2(MAX_DEPS+1)-1:0] reg_required,
input wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids,
input wire [ADDR_WIDTH-1:0] reg_x_base,
input wire [ADDR_WIDTH-1:0] reg_w_base,
input wire [15:0] reg_n_tiles,
input wire [ADDR_WIDTH-1:0] reg_result_addr,
// FPGA_DATA_READY: system-idle sticky flag, see nms_dataflow_core_sdram.v
output wire data_ready,
// ---- STEP19: ONE physical SDRAM interface, ALL traffic
// (weights + activations + results) ----
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 [1:0] sdram_ba,
output wire [12:0] sdram_a,
inout wire [15:0] sdram_dq,
output wire [1:0] sdram_dqm
);
wire [N_SLOTS:0] slot_mem_req, slot_mem_wr;
wire [ADDR_WIDTH*(N_SLOTS+1)-1:0] slot_mem_addr;
wire [16*(N_SLOTS+1)-1:0] slot_mem_wdata, slot_mem_rdata;
wire [N_SLOTS:0] slot_mem_lb_n, slot_mem_ub_n;
wire [N_SLOTS:0] slot_mem_ready;
wire [N_SLOTS-1:0] wide_slot_mem_req;
wire [ADDR_WIDTH*N_SLOTS-1:0] wide_slot_mem_addr;
wire [64*N_SLOTS-1:0] wide_slot_mem_rdata;
wire [N_SLOTS-1:0] wide_slot_mem_ready;
nms_dataflow_core_sdram #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH),
.N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH),
.MAX_TILES(MAX_TILES), .PREFETCH_DISTANCE(PREFETCH_DISTANCE)
) u_dataflow_core (
.clk(clk), .rst(rst),
.reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id),
.reg_required(reg_required), .reg_producer_ids(reg_producer_ids),
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
.reg_result_addr(reg_result_addr),
.data_ready(data_ready),
.slot_mem_req(slot_mem_req), .slot_mem_wr(slot_mem_wr), .slot_mem_addr(slot_mem_addr),
.slot_mem_wdata(slot_mem_wdata), .slot_mem_lb_n(slot_mem_lb_n), .slot_mem_ub_n(slot_mem_ub_n),
.slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready),
.wide_slot_mem_req(wide_slot_mem_req), .wide_slot_mem_addr(wide_slot_mem_addr),
.wide_slot_mem_rdata(wide_slot_mem_rdata), .wide_slot_mem_ready(wide_slot_mem_ready)
);
// ---- AR: activation-fill (shared, 1 port) + per-slot result
// writeback (N_SLOTS ports), arbitrated exactly as before ----
wire arb_m_req, arb_m_wr;
wire [ADDR_WIDTH-1:0] arb_m_addr;
wire [15:0] arb_m_wdata;
wire arb_m_lb_n, arb_m_ub_n;
wire [15:0] arb_m_rdata;
wire arb_m_ready;
slot_mem_arbiter #(
.ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS+1)
) u_arbiter (
.clk(clk), .rst(rst),
.s_req(slot_mem_req), .s_wr(slot_mem_wr), .s_addr(slot_mem_addr),
.s_wdata(slot_mem_wdata), .s_lb_n(slot_mem_lb_n), .s_ub_n(slot_mem_ub_n),
.s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready),
.m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata),
.m_lb_n(arb_m_lb_n), .m_ub_n(arb_m_ub_n),
.m_rdata(arb_m_rdata), .m_ready(arb_m_ready)
);
// ---- W: weight fetch (N_SLOTS ports), arbitrated exactly as
// before -- weight fetch never writes, same tie-off convention
// as STEP16-18 ----
wire [N_SLOTS-1:0] wide_s_wr = {N_SLOTS{1'b0}};
wire [64*N_SLOTS-1:0] wide_s_wdata = {(64*N_SLOTS){1'b0}};
wire [N_SLOTS-1:0] wide_s_lb_n = {N_SLOTS{1'b0}};
wire [N_SLOTS-1:0] wide_s_ub_n = {N_SLOTS{1'b0}};
wire wide_arb_m_req, wide_arb_m_wr;
wire [ADDR_WIDTH-1:0] wide_arb_m_addr;
wire [63:0] wide_arb_m_wdata;
wire wide_arb_m_lb_n, wide_arb_m_ub_n;
wire [63:0] wide_arb_m_rdata;
wire wide_arb_m_ready;
slot_mem_arbiter_wide #(
.ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS), .DATA_WIDTH(64)
) u_arbiter_wide (
.clk(clk), .rst(rst),
.s_req(wide_slot_mem_req), .s_wr(wide_s_wr), .s_addr(wide_slot_mem_addr),
.s_wdata(wide_s_wdata), .s_lb_n(wide_s_lb_n), .s_ub_n(wide_s_ub_n),
.s_rdata(wide_slot_mem_rdata), .s_ready(wide_slot_mem_ready),
.m_req(wide_arb_m_req), .m_wr(wide_arb_m_wr), .m_addr(wide_arb_m_addr), .m_wdata(wide_arb_m_wdata),
.m_lb_n(wide_arb_m_lb_n), .m_ub_n(wide_arb_m_ub_n),
.m_rdata(wide_arb_m_rdata), .m_ready(wide_arb_m_ready)
);
// ---- STEP19: ONE physical SDRAM backend, both W and AR ports ----
sdram_unified_backend_openrow #(
.ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ)
) u_sdram_backend (
.clk(clk), .rst(rst),
.w_req(wide_arb_m_req), .w_addr(wide_arb_m_addr),
.w_rdata(wide_arb_m_rdata), .w_ready(wide_arb_m_ready),
.ar_req(arb_m_req), .ar_wr(arb_m_wr), .ar_addr(arb_m_addr), .ar_wdata(arb_m_wdata),
.ar_lb_n(arb_m_lb_n), .ar_ub_n(arb_m_ub_n),
.ar_rdata(arb_m_rdata), .ar_ready(arb_m_ready),
.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
+253
View File
@@ -0,0 +1,253 @@
`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
@@ -0,0 +1,262 @@
`timescale 1ns/1ps
// ============================================================
// EXP-0055 -- combined fork of sdram_cdc_bridge.v (EXP-0053): the ONLY
// change is instantiating sdram_controller_openrow.v (EXP-0054,
// page-hit/keep-row-open policy) instead of plain sdram_controller.v
// as the fast-domain controller. The CDC handshake itself (toggle +
// last-seen, quasi-static bus sampling) is byte-for-byte unchanged --
// it treats whatever fast-domain controller it wraps as a black box
// behind the same req/wr/addr/wdata/wmask -> rdata/ready/busy
// contract, so this combination was expected to compose cleanly, and
// is verified as such by tb_sdram_cdc_bridge_openrow.v before any
// further integration.
//
// 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_openrow #(
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_openrow #(
.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
@@ -0,0 +1,460 @@
`timescale 1ns/1ps
// ============================================================
// EXP-0054 -- open-row (page-hit) SDR SDRAM controller, forked from
// sdram_controller.v (STEP16). Implements the "page-hit/keep-row-open
// optimization" that sdram_controller.v's own header explicitly
// deferred:
// "ALWAYS uses auto-precharge... NOT the fastest possible design
// (no page-hit/keep-row-open optimization, unlike psram_
// controller.v's own real page-mode), but it is trivially correct"
//
// MOTIVATION: weight_prefetch_engine_wide.v (real production traffic,
// instantiated by nms_dataflow_core_sdram.v, PREFETCH_DISTANCE=8)
// already issues a stream of STRICTLY SEQUENTIAL tile addresses per
// job. With ROW_BITS=13/COL_BITS=10 (AS4C32M16SA: 1024 columns/row,
// 4 words/tile at 16-bit words -- see sdram_controller.v's own TILE
// comment), a single row holds 256 consecutive tiles before crossing
// a row boundary -- most real jobs' weight streams never leave the
// row they started in. Closing and reopening that row on EVERY single
// tile (today's fixed auto-precharge policy) pays tRP+tRCD twice per
// transaction for no reason when the next transaction is going to hit
// the SAME row anyway.
//
// POLICY: never auto-precharge (A10=0 on every READ/WRITE). Track the
// single currently-open bank+row (this controller has always modeled
// "one transaction in flight" -- this experiment keeps that same
// single-open-row scope, not per-bank tracking across multiple
// simultaneously-open banks, matching the project's own established
// risk posture). On the NEXT request (evaluated in S_IDLE, exactly
// where every prior request was already evaluated):
// - SAME bank+row as currently open ("row hit"): skip ACTIVATE
// entirely -- issue READ/WRITE directly, saving tRCD.
// - DIFFERENT bank+row while a row IS open ("row miss"): issue an
// explicit PRECHARGE first (this controller no longer gets that
// for free via auto-precharge), wait tRP, THEN activate the new
// row exactly as before -- same total cost as today's design,
// just paid on-demand instead of unconditionally after every
// transaction.
// - no row open (e.g. right after reset/refresh): activate directly,
// unchanged from today.
//
// REFRESH INTERACTION (the one real correctness hazard this policy
// introduces, absent from the original always-precharged design):
// JEDEC AUTO REFRESH requires ALL banks precharged first. The
// original S_IDLE refresh branch's own comment ("no row is ever left
// open between transactions... so we can refresh immediately") is no
// longer true under this policy -- fixed here by precharging first
// (S_PRE_THEN_REF_WAIT) whenever row_open is set at the moment
// refresh comes due, before issuing AUTO REFRESH exactly as before.
//
// WRITE RECOVERY (tWR): the original design folded tWR into its
// always-paid post-burst precharge wait ("T_RP + 1'b1 // tWR folded
// in conservatively"). This design no longer precharges after every
// write, so tWR is now paid explicitly and alone (T_WR=2 CLK, real
// AS4C32M16SA datasheet value, same explicit-CLK-units treatment as
// T_MRD) via a new S_WRITE_RECOVERY_WAIT state, before the row-open
// path returns to S_IDLE and can accept a same-row follow-on command.
//
// DISCLOSED, NOT INDEPENDENTLY VERIFIED: read-burst-end -> next
// command (read-to-read or read-to-write, same open row) has NO extra
// wait beyond the existing 1-cycle-minimum S_IDLE turnaround, on the
// reasoning that JEDEC SDR SDRAM page-mode reads support back-to-back
// column access with no additional bubble. sdram_model.v (this
// project's own real-command-sequence checker) does NOT itself assert
// tCCD/tRTW/tWTR -- it only checks ACTIVATE-while-active, tRP, tRAS
// (min), refresh spacing, and access-with-no-active-row (see its own
// VIOLATION messages). tb_sdram_controller_openrow.v exercises
// read-after-read and write-after-read same-row sequences explicitly
// and checks DATA correctness, but a genuine read-to-write DQ bus
// turnaround hazard would not be caught by sdram_model.v itself if
// present -- flagged here exactly as this project's own convention
// requires, not silently assumed safe.
//
// Every timing constant, the mrs_value encoding, the req_pending
// unconditional-latch fix, and the address decomposition are carried
// over UNCHANGED from sdram_controller.v -- only the state machine's
// precharge policy and the two new wait states are new.
// ============================================================
module sdram_controller_openrow #(
parameter CLK_FREQ_MHZ = 64,
parameter BURST_LEN = 4, // 1, 4, or 8 -- same real scope as sdram_controller.v (see its own mrs_value)
parameter ROW_BITS = 13,
parameter COL_BITS = 10,
parameter BANK_BITS = 2,
parameter ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS
)(
input wire clk,
input wire rst,
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 reg busy,
output reg sdram_cke,
output reg sdram_cs_n,
output reg sdram_ras_n,
output reg sdram_cas_n,
output reg sdram_we_n,
output reg [1:0] sdram_ba,
output reg [ROW_BITS-1:0] sdram_a,
inout wire [15:0] sdram_dq,
output reg [1:0] sdram_dqm
);
localparam BURST_IDXW = (BURST_LEN <= 1) ? 1 : $clog2(BURST_LEN);
initial if (ADDR_WIDTH != BANK_BITS + ROW_BITS + COL_BITS) begin
$display("FATAL sdram_controller_openrow: ADDR_WIDTH=%0d != BANK_BITS(%0d)+ROW_BITS(%0d)+COL_BITS(%0d)=%0d",
ADDR_WIDTH, BANK_BITS, ROW_BITS, COL_BITS, BANK_BITS+ROW_BITS+COL_BITS);
$finish;
end
function integer ns_to_cycles;
input integer ns;
begin
ns_to_cycles = (ns * CLK_FREQ_MHZ + 999) / 1000;
end
endfunction
localparam T_RCD = ns_to_cycles(15);
localparam T_RP = ns_to_cycles(15);
localparam T_MRD = 2;
localparam T_WR = 2; // real AS4C32M16SA datasheet value, explicit CLK units (same treatment as T_MRD)
localparam T_INIT_US= 200;
localparam T_INIT = T_INIT_US * CLK_FREQ_MHZ;
localparam CAS_LATENCY = 3;
localparam T_REFI = ns_to_cycles(64000000 / (1 << ROW_BITS) + 1);
localparam CNTW = $clog2((T_INIT>T_REFI ? T_INIT : T_REFI) + 1);
function [CNTW-1:0] T_RC_MINUS1;
localparam integer T_RC = ns_to_cycles(65);
begin
T_RC_MINUS1 = T_RC[CNTW-1:0] - 1'b1;
end
endfunction
localparam
S_INIT_WAIT = 5'd0,
S_INIT_PRE_WAIT = 5'd2,
S_INIT_REF = 5'd3,
S_INIT_REF_WAIT = 5'd4,
S_INIT_MRS_WAIT = 5'd6,
S_IDLE = 5'd7,
S_REFRESH_WAIT = 5'd9,
S_ACTIVATE_WAIT = 5'd11,
S_CAS_WAIT = 5'd13,
S_BURST_READ = 5'd14,
S_BURST_WRITE = 5'd15,
S_PRE_THEN_ACT_WAIT = 5'd17,
S_PRE_THEN_REF_WAIT = 5'd18,
S_WRITE_RECOVERY_WAIT= 5'd19;
reg [4:0] state;
reg [CNTW-1:0] wait_cnt;
reg [3:0] init_ref_cnt;
reg [CNTW-1:0] refresh_timer;
reg [BURST_IDXW-1:0] burst_idx;
reg req_wr_reg;
reg [BANK_BITS-1:0] req_bank_reg;
reg [ROW_BITS-1:0] req_row_reg;
reg [COL_BITS-1:0] req_col_reg;
reg [16*BURST_LEN-1:0] wdata_reg;
reg [2*BURST_LEN-1:0] wmask_reg;
// ---- open-row tracking (new vs sdram_controller.v) ----
reg row_open;
reg [BANK_BITS-1:0] open_bank;
reg [ROW_BITS-1:0] open_row;
wire [BANK_BITS-1:0] addr_bank = addr[ADDR_WIDTH-1 -: BANK_BITS];
wire [ROW_BITS-1:0] addr_row = addr[ADDR_WIDTH-BANK_BITS-1 -: ROW_BITS];
wire [COL_BITS-1:0] addr_col = addr[COL_BITS-1:0];
reg req_pending;
wire eff_wr = req ? wr : req_wr_reg;
wire [BANK_BITS-1:0] eff_bank = req ? addr_bank : req_bank_reg;
wire [ROW_BITS-1:0] eff_row = req ? addr_row : req_row_reg;
wire [COL_BITS-1:0] eff_col = req ? addr_col : req_col_reg;
wire [16*BURST_LEN-1:0] eff_wdata = req ? wdata : wdata_reg;
wire [2*BURST_LEN-1:0] eff_wmask = req ? wmask : wmask_reg;
reg dq_out_en;
reg [15:0] dq_out;
assign sdram_dq = dq_out_en ? dq_out : 16'hzzzz;
function [ROW_BITS-1:0] mrs_value;
input integer burst_len;
reg [2:0] bl_code;
reg [ROW_BITS-1:0] v;
begin
bl_code = (burst_len==1) ? 3'b000 :
(burst_len==2) ? 3'b001 :
(burst_len==4) ? 3'b010 :
(burst_len==8) ? 3'b011 : 3'b111;
v = {ROW_BITS{1'b0}};
v[6:4] = 3'b011;
v[3] = 1'b0;
v[2:0] = bl_code;
mrs_value = v;
end
endfunction
always @(posedge clk) begin
if (rst) begin
state <= S_INIT_WAIT;
wait_cnt <= T_INIT[CNTW-1:0];
init_ref_cnt <= 4'd0;
refresh_timer <= T_REFI[CNTW-1:0];
sdram_cke <= 1'b1;
sdram_cs_n <= 1'b1;
sdram_ras_n <= 1'b1;
sdram_cas_n <= 1'b1;
sdram_we_n <= 1'b1;
sdram_ba <= 2'b00;
sdram_a <= {ROW_BITS{1'b0}};
sdram_dqm <= 2'b00;
dq_out_en <= 1'b0;
ready <= 1'b0;
busy <= 1'b1;
req_pending <= 1'b0;
row_open <= 1'b0;
open_bank <= {BANK_BITS{1'b0}};
open_row <= {ROW_BITS{1'b0}};
end else begin
sdram_cs_n <= 1'b0;
sdram_ras_n <= 1'b1;
sdram_cas_n <= 1'b1;
sdram_we_n <= 1'b1;
ready <= 1'b0;
dq_out_en <= 1'b0;
sdram_dqm <= 2'b00;
if (refresh_timer != 0) refresh_timer <= refresh_timer - 1'b1;
if (req) begin
req_wr_reg <= wr;
req_bank_reg <= addr_bank;
req_row_reg <= addr_row;
req_col_reg <= addr_col;
wdata_reg <= wdata;
wmask_reg <= wmask;
req_pending <= 1'b1;
end
case (state)
S_INIT_WAIT: begin
busy <= 1'b1;
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else begin
sdram_ras_n <= 1'b0; sdram_we_n <= 1'b0;
sdram_a[10] <= 1'b1;
wait_cnt <= T_RP[CNTW-1:0] - 1'b1;
state <= S_INIT_PRE_WAIT;
end
end
S_INIT_PRE_WAIT: begin
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else state <= S_INIT_REF;
end
S_INIT_REF: begin
sdram_ras_n <= 1'b0; sdram_cas_n <= 1'b0;
wait_cnt <= T_RC_MINUS1();
state <= S_INIT_REF_WAIT;
end
S_INIT_REF_WAIT: begin
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else if (init_ref_cnt < 4'd7) begin
init_ref_cnt <= init_ref_cnt + 1'b1;
state <= S_INIT_REF;
end else begin
sdram_ras_n <= 1'b0; sdram_cas_n <= 1'b0; sdram_we_n <= 1'b0;
sdram_ba <= 2'b00;
sdram_a <= mrs_value(BURST_LEN);
wait_cnt <= T_MRD[CNTW-1:0] - 1'b1;
state <= S_INIT_MRS_WAIT;
end
end
S_INIT_MRS_WAIT: begin
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else begin
busy <= 1'b0;
state <= S_IDLE;
end
end
S_IDLE: begin
busy <= 1'b0;
if (refresh_timer == 0) begin
busy <= 1'b1;
if (row_open) begin
// JEDEC: all banks must be precharged before
// AUTO REFRESH -- no longer free/automatic
// under the open-row policy (see header).
sdram_ras_n <= 1'b0; sdram_we_n <= 1'b0;
sdram_ba <= open_bank;
sdram_a[10] <= 1'b1;
row_open <= 1'b0;
wait_cnt <= T_RP[CNTW-1:0] - 1'b1;
state <= S_PRE_THEN_REF_WAIT;
end else begin
sdram_ras_n <= 1'b0; sdram_cas_n <= 1'b0;
wait_cnt <= T_RC_MINUS1();
refresh_timer <= T_REFI[CNTW-1:0];
state <= S_REFRESH_WAIT;
end
end else if (req || req_pending) begin
busy <= 1'b1;
req_wr_reg <= eff_wr;
req_bank_reg <= eff_bank;
req_row_reg <= eff_row;
req_col_reg <= eff_col;
wdata_reg <= eff_wdata;
wmask_reg <= eff_wmask;
req_pending <= 1'b0;
if (row_open && eff_bank == open_bank && eff_row == open_row) begin
// ROW HIT: skip ACTIVATE entirely, saves tRCD.
burst_idx <= {BURST_IDXW{1'b0}};
sdram_cas_n <= 1'b0;
sdram_we_n <= eff_wr ? 1'b0 : 1'b1;
sdram_ba <= eff_bank;
sdram_a <= {{(ROW_BITS-11){1'b0}}, 1'b0, {(10-COL_BITS){1'b0}}, eff_col}; // A10=0: no auto-precharge
if (eff_wr) begin
dq_out_en <= 1'b1;
dq_out <= eff_wdata[15:0];
sdram_dqm <= eff_wmask[1:0];
state <= S_BURST_WRITE;
end else begin
wait_cnt <= CAS_LATENCY[CNTW-1:0];
state <= S_CAS_WAIT;
end
end else if (row_open) begin
// ROW MISS, a different row is open: precharge
// it first (paid on-demand, same total cost as
// today's unconditional auto-precharge, just
// deferred until actually needed).
sdram_ras_n <= 1'b0; sdram_we_n <= 1'b0;
sdram_ba <= open_bank;
sdram_a[10] <= 1'b1;
row_open <= 1'b0;
wait_cnt <= T_RP[CNTW-1:0] - 1'b1;
state <= S_PRE_THEN_ACT_WAIT;
end else begin
// no row open at all: activate directly.
sdram_ras_n <= 1'b0;
sdram_ba <= eff_bank;
sdram_a <= eff_row;
wait_cnt <= T_RCD[CNTW-1:0] - 1'b1;
state <= S_ACTIVATE_WAIT;
end
end
end
S_PRE_THEN_REF_WAIT: begin
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else begin
sdram_ras_n <= 1'b0; sdram_cas_n <= 1'b0;
wait_cnt <= T_RC_MINUS1();
refresh_timer <= T_REFI[CNTW-1:0];
state <= S_REFRESH_WAIT;
end
end
S_PRE_THEN_ACT_WAIT: begin
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else begin
sdram_ras_n <= 1'b0;
sdram_ba <= req_bank_reg;
sdram_a <= req_row_reg;
wait_cnt <= T_RCD[CNTW-1:0] - 1'b1;
state <= S_ACTIVATE_WAIT;
end
end
S_REFRESH_WAIT: begin
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else state <= S_IDLE;
end
S_ACTIVATE_WAIT: begin
if (wait_cnt != 0) begin
wait_cnt <= wait_cnt - 1'b1;
end else begin
sdram_cas_n <= 1'b0;
sdram_we_n <= req_wr_reg ? 1'b0 : 1'b1;
sdram_ba <= req_bank_reg;
sdram_a <= {{(ROW_BITS-11){1'b0}}, 1'b0, {(10-COL_BITS){1'b0}}, req_col_reg}; // A10=0
burst_idx <= {BURST_IDXW{1'b0}};
row_open <= 1'b1;
open_bank <= req_bank_reg;
open_row <= req_row_reg;
if (req_wr_reg) begin
dq_out_en <= 1'b1;
dq_out <= wdata_reg[15:0];
sdram_dqm <= wmask_reg[1:0];
state <= S_BURST_WRITE;
end else begin
wait_cnt <= CAS_LATENCY[CNTW-1:0];
state <= S_CAS_WAIT;
end
end
end
S_CAS_WAIT: begin
if (wait_cnt != 0) begin
wait_cnt <= wait_cnt - 1'b1;
end else begin
rdata[0 +: 16] <= sdram_dq;
if (BURST_LEN == 1) begin
ready <= 1'b1;
state <= S_IDLE; // row stays open, no precharge
end else begin
burst_idx <= burst_idx + 1'b1;
state <= S_BURST_READ;
end
end
end
S_BURST_READ: begin
rdata[burst_idx*16 +: 16] <= sdram_dq;
if (burst_idx == BURST_LEN[BURST_IDXW-1:0] - 1'b1) begin
ready <= 1'b1;
state <= S_IDLE; // row stays open, no precharge
end else begin
burst_idx <= burst_idx + 1'b1;
end
end
S_BURST_WRITE: begin
if (burst_idx < BURST_LEN[BURST_IDXW-1:0] - 1'b1) begin
burst_idx <= burst_idx + 1'b1;
dq_out_en <= 1'b1;
dq_out <= wdata_reg[(burst_idx+1'b1)*16 +: 16];
sdram_dqm <= wmask_reg[(burst_idx+1'b1)*2 +: 2];
end else begin
ready <= 1'b1;
// tWR now paid alone (no longer folded with tRP,
// since we no longer precharge unconditionally --
// see header).
wait_cnt <= T_WR[CNTW-1:0] - 1'b1;
state <= S_WRITE_RECOVERY_WAIT;
end
end
S_WRITE_RECOVERY_WAIT: begin
if (wait_cnt != 0) wait_cnt <= wait_cnt - 1'b1;
else state <= S_IDLE; // row stays open, no precharge
end
default: state <= S_IDLE;
endcase
end
end
endmodule
@@ -0,0 +1,385 @@
`timescale 1ns/1ps
// ============================================================
// NMS STEP19 -- UNIFIED single-SDRAM memory backend.
//
// Replaces BOTH physical memory paths that existed through STEP18
// (sdram_weight_backend_pack128.v for weights, and hardware/v1/rtl/
// memory_interface.v + psram_controller.v for activation-fill/result-
// writeback) with ONE physical AS4C4M16SA-6TIN SDRAM chip, ONE
// sdram_controller.v instance (BURST_LEN=8), serving THREE logical
// traffic classes through TWO external ports that exactly match what
// the existing, UNCHANGED consumers already drive:
//
// W port (64-bit): weight_prefetch_engine_wide.v's own real
// traffic, via slot_mem_arbiter_wide.v -- IDENTICAL external
// contract to STEP18's sdram_weight_backend_pack128.v (byte
// address in, 64-bit mem_rdata out), and internally reuses that
// module's own validated N_ENTRIES=4 "other half" cache
// unchanged (EXP-0046/ERR-0022's own fix, not re-derived here).
//
// AR port (16-bit, byte-maskable): nms_activation_fill_ctrl_v3.v's
// own activation reads AND every per-slot nms_memory_manager_
// stream_wide.v's own result writes, via slot_mem_arbiter.v --
// IDENTICAL external contract to the real V1 psram_controller.v
// port it replaces (word address in, 16-bit mem_wdata/mem_rdata,
// mem_lb_n/mem_ub_n byte-lane write masking). Neither
// nms_activation_fill_ctrl_v3.v nor nms_memory_manager_stream_
// wide.v needed ANY change -- they already produce a WORD
// address and already drive lb_n/ub_n exactly as the real V1
// PSRAM controller expected.
//
// Neither weight_prefetch_engine_wide.v, nms_activation_fill_ctrl_v3.
// v, nms_memory_manager_stream_wide.v, nor neural_processor.v changed
// AT ALL for this step -- this is a pure memory-side substitution,
// per the governing spec's own explicit instruction.
//
// KEY ENABLING FACT: real SDR SDRAM's own DQM pins are a per-BYTE
// write mask (STEP19's own real, tested extension to sdram_
// controller.v's `wmask` port) -- this lets a single-BYTE result
// write happen INSIDE a shared BURST_LEN=8 (128-bit) transaction by
// masking out every byte except the one/two the caller actually wants
// written, with NO read-modify-write needed at all (the real SDRAM
// chip itself leaves masked bytes untouched, by JEDEC definition).
// Activation reads need no such trick -- a full 128-bit block is
// fetched and the caller's own requested 16-bit word is extracted
// combinationally from it.
//
// Arbitration: simple, correctness-first 2-way priority (weight
// traffic strongly dominates real measured traffic -- STEP17 showed
// the activation/result path at <=7.2% of all external-memory
// activity -- so W is granted priority when both are pending, AR is
// never starved since W's own real traffic pattern always eventually
// idles between tiles/jobs). Exactly one physical SDRAM transaction
// in flight at a time (matches sdram_controller.v's own inherent
// single-transaction design, STEP18 Part E's own documented, accepted
// scope boundary -- not revisited here).
// ============================================================
module sdram_unified_backend_cdc #(
parameter ADDR_WIDTH = 26, // byte address width (W port convention)
parameter CLK_FREQ_MHZ = 64,
parameter W_ENTRIES = 4, // weight-cache depth, >= real N_SLOTS
// physical SDRAM geometry, forwarded directly to sdram_controller.v
// (AS4C32M16SA defaults: 13 row bits/A0-A12, 10 col bits/A0-A9,
// 2 bank bits/BA0-BA1) -- must satisfy ADDR_WIDTH-1 ==
// BANK_BITS+ROW_BITS+COL_BITS (byte address = word address + 1 bit),
// asserted at elaboration below.
parameter ROW_BITS = 13,
parameter COL_BITS = 10,
parameter BANK_BITS = 2
)(
input wire clk,
input wire rst,
input wire clk_fast,
input wire rst_fast,
// ---- W: weight fetch (64-bit, byte address, read-only) ----
input wire w_req,
input wire [ADDR_WIDTH-1:0] w_addr,
output reg [63:0] w_rdata,
output reg w_ready,
// ---- AR: activation-fill (read) + result-writeback (write),
// 16-bit, WORD address (matches the real V1 psram_controller.v
// convention this port replaces exactly) ----
input wire ar_req,
input wire ar_wr,
input wire [ADDR_WIDTH-1:0] ar_addr, // word address, low 22 bits meaningful
// (matches slot_mem_arbiter.v's own
// m_addr width convention exactly --
// that arbiter's real callers only ever
// drive a 22-bit-significant word
// address into an ADDR_WIDTH-wide bus)
input wire [15:0] ar_wdata,
input wire ar_lb_n,
input wire ar_ub_n,
output reg [15:0] ar_rdata,
output reg ar_ready,
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
);
initial if (ADDR_WIDTH != BANK_BITS + ROW_BITS + COL_BITS + 1) begin
$display("FATAL sdram_unified_backend: ADDR_WIDTH(%0d) != BANK_BITS(%0d)+ROW_BITS(%0d)+COL_BITS(%0d)+1",
ADDR_WIDTH, BANK_BITS, ROW_BITS, COL_BITS);
$finish;
end
// ============================================================
// W-port cache (identical logic to sdram_weight_backend_pack128.v
// -- an N_ENTRIES-deep, fully-associative "other half" cache,
// round-robin allocated; safe under any sizing, see that module's
// own header/ERR-0022 for the full rationale, not repeated here)
// ============================================================
localparam WEIDXW = (W_ENTRIES <= 1) ? 1 : $clog2(W_ENTRIES);
reg w_cache_valid [0:W_ENTRIES-1];
reg [ADDR_WIDTH-1:0] w_cache_addr [0:W_ENTRIES-1];
reg [63:0] w_cache_data [0:W_ENTRIES-1];
reg [WEIDXW-1:0] w_alloc_ptr;
// ERR-0029 fix (N=8 @64MHz critical-path, measured via real P&R:
// worst seed1 total delay 17.909ns, 84% routing, dominant hop
// 2.5-2.8ns): the original RTL used a sequential for-loop that
// overwrites w_hit_idx_c on every match ("last valid+matching entry
// wins"), which Yosys/nextpnr synthesized as a serially-dependent
// cascade of PFUMX/OFX fast-mux primitives -- each entry's result
// depends on the previous one, forcing nextpnr to place the whole
// chain along one physical path with no freedom to shorten it. This
// is the same architectural fix class as ERR-0028 (activation_fill_
// ctrl's max-tree): replace the serial dependency chain with a flat
// one-hot compare (fully parallel, W_ENTRIES=4 comparators, no
// inter-entry dependency) followed by a single-level priority-encode
// casez, preserving the EXACT original "highest index wins" semantics
// bit-for-bit (verified: original loop always ends on the highest ei
// that matched, since ei counts up without break).
wire [W_ENTRIES-1:0] w_match_oh;
genvar wgi;
generate
for (wgi = 0; wgi < W_ENTRIES; wgi = wgi + 1) begin : GEN_WMATCH
assign w_match_oh[wgi] = w_cache_valid[wgi] && (w_cache_addr[wgi] == w_addr);
end
endgenerate
reg w_hit_found_c;
reg [WEIDXW-1:0] w_hit_idx_c;
integer ei;
generate
if (W_ENTRIES == 4) begin : GEN_WHIT_FLAT
// real, measured configuration (see ERR-0029) -- flat,
// single-level priority encode over the parallel one-hot
// compare above, no serial inter-entry dependency.
always @(*) begin
w_hit_found_c = |w_match_oh;
casez (w_match_oh)
4'b1???: w_hit_idx_c = 2'd3;
4'b01??: w_hit_idx_c = 2'd2;
4'b001?: w_hit_idx_c = 2'd1;
4'b0001: w_hit_idx_c = 2'd0;
default: w_hit_idx_c = {WEIDXW{1'b0}};
endcase
end
end else begin : GEN_WHIT_FALLBACK
// any other W_ENTRIES value: fall back to the original,
// functionally-equivalent (but serially-dependent) scan --
// not the measured/optimized configuration this project
// actually builds, kept only for parametric safety.
always @(*) begin
w_hit_found_c = 1'b0;
w_hit_idx_c = {WEIDXW{1'b0}};
for (ei = 0; ei < W_ENTRIES; ei = ei + 1) begin
if (w_cache_valid[ei] && w_cache_addr[ei] == w_addr) begin
w_hit_found_c = 1'b1;
w_hit_idx_c = ei[WEIDXW-1:0];
end
end
end
end
endgenerate
wire w_cache_hit = w_hit_found_c && w_req;
// ============================================================
// Shared physical controller, BURST_LEN=8 (128-bit/16-byte real
// SDRAM transactions), reused UNCHANGED from STEP16-18.
// ============================================================
reg ctrl_req;
reg ctrl_wr;
reg [ADDR_WIDTH-2:0] ctrl_addr;
reg [127:0] ctrl_wdata;
reg [15:0] ctrl_wmask;
wire [127:0] ctrl_rdata;
wire ctrl_ready;
wire ctrl_busy;
sdram_cdc_bridge #(
.CLK_FREQ_MHZ_FAST(115), .BURST_LEN(8),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_sdram_ctrl (
.clk_slow(clk), .rst_slow(rst),
.clk_fast(clk_fast), .rst_fast(rst_fast),
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr),
.wdata(ctrl_wdata), .wmask(ctrl_wmask),
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy),
.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)
);
localparam S_IDLE = 3'd0,
S_W_WAIT = 3'd1,
S_AR_RD_WAIT = 3'd2,
S_AR_WR_WAIT = 3'd3;
reg [2:0] state;
reg w_pending_upper_half;
reg [ADDR_WIDTH-1:0] w_pending_addr;
reg [2:0] ar_pending_word;
// ---- req_pending latches (same fix class as sdram_controller.v's
// own ERR-0019/ERR-0020): this backend's own top-level S_IDLE
// arbitration can only START a new transaction when it is
// genuinely idle. A single-cycle w_req/ar_req pulse (this
// project's own established mem_req convention) arriving on a
// cycle this backend happens to be busy servicing the OTHER port
// would otherwise be silently dropped -- the caller has no idea,
// waits forever for a `ready` that never comes. Found the hard way
// (STEP19, EXP-0048): the first real N=4 D-Stress run deadlocked
// at 0/256 neurons, jobs_allocated stuck at 12, because the very
// first activation-fill read raced against weight-prefetch traffic
// and was lost exactly this way. Fix: latch EVERY req's own fields
// unconditionally, every cycle, regardless of current state (not
// just from S_IDLE), mirroring sdram_controller.v's own corrected
// fix exactly (ERR-0020: the FIRST attempt only latched from
// S_IDLE, which was still not enough -- latch unconditionally).
reg w_req_pending;
reg [ADDR_WIDTH-1:0] w_req_addr_lat;
reg ar_req_pending;
reg ar_req_wr_lat;
reg [ADDR_WIDTH-1:0] ar_req_addr_lat;
reg [15:0] ar_req_wdata_lat;
reg ar_req_lbn_lat, ar_req_ubn_lat;
wire w_eff_req = w_req || w_req_pending;
wire [ADDR_WIDTH-1:0] w_eff_addr = w_req ? w_addr : w_req_addr_lat;
wire ar_eff_req = ar_req || ar_req_pending;
wire ar_eff_wr = ar_req ? ar_wr : ar_req_wr_lat;
wire [ADDR_WIDTH-1:0] ar_eff_addr = ar_req ? ar_addr : ar_req_addr_lat;
wire [15:0] ar_eff_wdata= ar_req ? ar_wdata : ar_req_wdata_lat;
wire ar_eff_lbn = ar_req ? ar_lb_n : ar_req_lbn_lat;
wire ar_eff_ubn = ar_req ? ar_ub_n : ar_req_ubn_lat;
wire [ADDR_WIDTH-2:0] w_eff_aligned_word_addr = {w_eff_addr[ADDR_WIDTH-1:4], 3'b000};
wire w_eff_addr_is_upper_half = w_eff_addr[3];
wire [ADDR_WIDTH-2:0] ar_eff_block_base = {ar_eff_addr[ADDR_WIDTH-2:3], 3'b000};
wire [2:0] ar_eff_word_in_blk = ar_eff_addr[2:0];
integer ri;
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
for (ri = 0; ri < W_ENTRIES; ri = ri + 1) w_cache_valid[ri] <= 1'b0;
w_alloc_ptr <= {WEIDXW{1'b0}};
ctrl_req <= 1'b0; ctrl_wr <= 1'b0; ctrl_addr <= {(ADDR_WIDTH-1){1'b0}};
ctrl_wdata <= 128'h0; ctrl_wmask <= 16'hFFFF;
w_ready <= 1'b0; w_rdata <= 64'h0;
ar_ready <= 1'b0; ar_rdata <= 16'h0;
w_pending_upper_half <= 1'b0; w_pending_addr <= {ADDR_WIDTH{1'b0}};
ar_pending_word <= 3'h0;
w_req_pending <= 1'b0; w_req_addr_lat <= {ADDR_WIDTH{1'b0}};
ar_req_pending <= 1'b0; ar_req_wr_lat <= 1'b0;
ar_req_addr_lat <= {ADDR_WIDTH{1'b0}}; ar_req_wdata_lat <= 16'h0;
ar_req_lbn_lat <= 1'b1; ar_req_ubn_lat <= 1'b1;
end else begin
ctrl_req <= 1'b0;
w_ready <= 1'b0;
ar_ready <= 1'b0;
// latch fresh requests unconditionally, every cycle,
// regardless of state (see req_pending's own comment above)
if (w_req) begin
w_req_addr_lat <= w_addr;
w_req_pending <= 1'b1;
end
if (ar_req) begin
ar_req_wr_lat <= ar_wr;
ar_req_addr_lat <= ar_addr;
ar_req_wdata_lat <= ar_wdata;
ar_req_lbn_lat <= ar_lb_n;
ar_req_ubn_lat <= ar_ub_n;
ar_req_pending <= 1'b1;
end
case (state)
S_IDLE: begin
// W has priority when both are pending (real
// measured traffic: weight >>> activation+result,
// STEP17 EXP-0045 -- AR is never starved since W's
// own real access pattern idles between tiles).
if (w_cache_hit) begin
// fully serviced THIS cycle -- must also cancel
// the unconditional latch above, which just set
// w_req_pending<=1 for this SAME w_req pulse
// (real bug found via full regression, EXP-0048
// /ERR-0023: without this the latch survives
// uncontested, and next cycle w_eff_req reads
// true from STALE w_req_pending/w_req_addr_lat,
// issuing a bogus extra fetch that shifts every
// subsequent response by one).
w_rdata <= w_cache_data[w_hit_idx_c];
w_ready <= 1'b1;
w_cache_valid[w_hit_idx_c] <= 1'b0;
w_req_pending <= 1'b0;
end else if (w_eff_req) begin
ctrl_req <= 1'b1;
ctrl_wr <= 1'b0;
ctrl_addr <= w_eff_aligned_word_addr;
ctrl_wmask <= 16'h0000;
w_pending_upper_half <= w_eff_addr_is_upper_half;
w_pending_addr <= w_eff_addr;
w_req_pending <= 1'b0;
state <= S_W_WAIT;
end else if (ar_eff_req && !ar_eff_wr) begin
ctrl_req <= 1'b1;
ctrl_wr <= 1'b0;
ctrl_addr <= ar_eff_block_base;
ctrl_wmask <= 16'h0000;
ar_pending_word <= ar_eff_word_in_blk;
ar_req_pending <= 1'b0;
state <= S_AR_RD_WAIT;
end else if (ar_eff_req && ar_eff_wr) begin
// mask every word except the target one; within
// the target word, pass ar_lb_n/ar_ub_n through
// directly (same active-low "write this byte"
// polarity as real SDRAM DQM: lb_n=0 -> DQM=0
// -> byte written; lb_n=1 -> DQM=1 -> masked).
ctrl_req <= 1'b1;
ctrl_wr <= 1'b1;
ctrl_addr <= ar_eff_block_base;
ctrl_wdata <= {8{ar_eff_wdata}}; // replicate; only the target word's mask bits matter
ctrl_wmask <= {16{1'b1}} & ~(16'h0003 << (ar_eff_word_in_blk*2)) | ({14'b0, ar_eff_ubn, ar_eff_lbn} << (ar_eff_word_in_blk*2));
ar_req_pending <= 1'b0;
state <= S_AR_WR_WAIT;
end
end
S_W_WAIT: begin
if (ctrl_ready) begin
if (w_pending_upper_half) begin
w_rdata <= ctrl_rdata[127:64];
w_cache_data[w_alloc_ptr] <= ctrl_rdata[63:0];
w_cache_addr[w_alloc_ptr] <= w_pending_addr - {{(ADDR_WIDTH-4){1'b0}}, 4'd8};
end else begin
w_rdata <= ctrl_rdata[63:0];
w_cache_data[w_alloc_ptr] <= ctrl_rdata[127:64];
w_cache_addr[w_alloc_ptr] <= w_pending_addr + {{(ADDR_WIDTH-4){1'b0}}, 4'd8};
end
w_cache_valid[w_alloc_ptr] <= 1'b1;
w_alloc_ptr <= (w_alloc_ptr == W_ENTRIES[WEIDXW-1:0]-1'b1) ? {WEIDXW{1'b0}} : w_alloc_ptr + 1'b1;
w_ready <= 1'b1;
state <= S_IDLE;
end
end
S_AR_RD_WAIT: begin
if (ctrl_ready) begin
ar_rdata <= ctrl_rdata[ar_pending_word*16 +: 16];
ar_ready <= 1'b1;
state <= S_IDLE;
end
end
S_AR_WR_WAIT: begin
if (ctrl_ready) begin
ar_ready <= 1'b1;
state <= S_IDLE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule
@@ -0,0 +1,404 @@
`timescale 1ns/1ps
// ============================================================
// EXP-0055 -- combined fork of sdram_unified_backend.v (STEP19) -- the
// ONLY changes are: (1) two new clk_fast/rst_fast ports, (2) the
// physical sdram_controller.v instance replaced by sdram_cdc_bridge_
// openrow.v (EXP-0053 CDC + EXP-0054 page-hit, composed together).
// W-port cache, arbitration, and the W/AR top-level FSM are ALL
// byte-for-byte unchanged, per the same fork discipline sdram_
// unified_backend_pipelined.v (EXP-0052) already established. See
// hardware/v2/logs/experiments.log (search "EXP-0055") for why this
// fork exists and its own isolated (tb_sdram_cdc_bridge_openrow.v)
// measured number before this integration step.
//
// Everything below this point is STEP19's own original header,
// preserved for the W/AR port contract description (still accurate --
// only the physical controller behind ctrl_req/.../ctrl_ready changed):
//
// NMS STEP19 -- UNIFIED single-SDRAM memory backend.
//
// Replaces BOTH physical memory paths that existed through STEP18
// (sdram_weight_backend_pack128.v for weights, and hardware/v1/rtl/
// memory_interface.v + psram_controller.v for activation-fill/result-
// writeback) with ONE physical AS4C4M16SA-6TIN SDRAM chip, ONE
// sdram_controller.v instance (BURST_LEN=8), serving THREE logical
// traffic classes through TWO external ports that exactly match what
// the existing, UNCHANGED consumers already drive:
//
// W port (64-bit): weight_prefetch_engine_wide.v's own real
// traffic, via slot_mem_arbiter_wide.v -- IDENTICAL external
// contract to STEP18's sdram_weight_backend_pack128.v (byte
// address in, 64-bit mem_rdata out), and internally reuses that
// module's own validated N_ENTRIES=4 "other half" cache
// unchanged (EXP-0046/ERR-0022's own fix, not re-derived here).
//
// AR port (16-bit, byte-maskable): nms_activation_fill_ctrl_v3.v's
// own activation reads AND every per-slot nms_memory_manager_
// stream_wide.v's own result writes, via slot_mem_arbiter.v --
// IDENTICAL external contract to the real V1 psram_controller.v
// port it replaces (word address in, 16-bit mem_wdata/mem_rdata,
// mem_lb_n/mem_ub_n byte-lane write masking). Neither
// nms_activation_fill_ctrl_v3.v nor nms_memory_manager_stream_
// wide.v needed ANY change -- they already produce a WORD
// address and already drive lb_n/ub_n exactly as the real V1
// PSRAM controller expected.
//
// Neither weight_prefetch_engine_wide.v, nms_activation_fill_ctrl_v3.
// v, nms_memory_manager_stream_wide.v, nor neural_processor.v changed
// AT ALL for this step -- this is a pure memory-side substitution,
// per the governing spec's own explicit instruction.
//
// KEY ENABLING FACT: real SDR SDRAM's own DQM pins are a per-BYTE
// write mask (STEP19's own real, tested extension to sdram_
// controller.v's `wmask` port) -- this lets a single-BYTE result
// write happen INSIDE a shared BURST_LEN=8 (128-bit) transaction by
// masking out every byte except the one/two the caller actually wants
// written, with NO read-modify-write needed at all (the real SDRAM
// chip itself leaves masked bytes untouched, by JEDEC definition).
// Activation reads need no such trick -- a full 128-bit block is
// fetched and the caller's own requested 16-bit word is extracted
// combinationally from it.
//
// Arbitration: simple, correctness-first 2-way priority (weight
// traffic strongly dominates real measured traffic -- STEP17 showed
// the activation/result path at <=7.2% of all external-memory
// activity -- so W is granted priority when both are pending, AR is
// never starved since W's own real traffic pattern always eventually
// idles between tiles/jobs). Exactly one physical SDRAM transaction
// in flight at a time (matches sdram_controller.v's own inherent
// single-transaction design, STEP18 Part E's own documented, accepted
// scope boundary -- not revisited here).
// ============================================================
module sdram_unified_backend_combined #(
parameter ADDR_WIDTH = 26, // byte address width (W port convention)
parameter CLK_FREQ_MHZ = 64,
parameter W_ENTRIES = 4, // weight-cache depth, >= real N_SLOTS
// physical SDRAM geometry, forwarded directly to sdram_controller.v
// (AS4C32M16SA defaults: 13 row bits/A0-A12, 10 col bits/A0-A9,
// 2 bank bits/BA0-BA1) -- must satisfy ADDR_WIDTH-1 ==
// BANK_BITS+ROW_BITS+COL_BITS (byte address = word address + 1 bit),
// asserted at elaboration below.
parameter ROW_BITS = 13,
parameter COL_BITS = 10,
parameter BANK_BITS = 2
)(
input wire clk,
input wire rst,
// ---- EXP-0055: fast SDRAM clock domain (115.2MHz-class), see
// sdram_cdc_bridge_openrow.v -- everything else in this module
// (W/AR ports, W-cache, top-level FSM) stays on clk/rst exactly
// as before ----
input wire clk_fast,
input wire rst_fast,
// ---- W: weight fetch (64-bit, byte address, read-only) ----
input wire w_req,
input wire [ADDR_WIDTH-1:0] w_addr,
output reg [63:0] w_rdata,
output reg w_ready,
// ---- AR: activation-fill (read) + result-writeback (write),
// 16-bit, WORD address (matches the real V1 psram_controller.v
// convention this port replaces exactly) ----
input wire ar_req,
input wire ar_wr,
input wire [ADDR_WIDTH-1:0] ar_addr, // word address, low 22 bits meaningful
// (matches slot_mem_arbiter.v's own
// m_addr width convention exactly --
// that arbiter's real callers only ever
// drive a 22-bit-significant word
// address into an ADDR_WIDTH-wide bus)
input wire [15:0] ar_wdata,
input wire ar_lb_n,
input wire ar_ub_n,
output reg [15:0] ar_rdata,
output reg ar_ready,
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
);
initial if (ADDR_WIDTH != BANK_BITS + ROW_BITS + COL_BITS + 1) begin
$display("FATAL sdram_unified_backend_combined: ADDR_WIDTH(%0d) != BANK_BITS(%0d)+ROW_BITS(%0d)+COL_BITS(%0d)+1",
ADDR_WIDTH, BANK_BITS, ROW_BITS, COL_BITS);
$finish;
end
// ============================================================
// W-port cache (identical logic to sdram_weight_backend_pack128.v
// -- an N_ENTRIES-deep, fully-associative "other half" cache,
// round-robin allocated; safe under any sizing, see that module's
// own header/ERR-0022 for the full rationale, not repeated here)
// ============================================================
localparam WEIDXW = (W_ENTRIES <= 1) ? 1 : $clog2(W_ENTRIES);
reg w_cache_valid [0:W_ENTRIES-1];
reg [ADDR_WIDTH-1:0] w_cache_addr [0:W_ENTRIES-1];
reg [63:0] w_cache_data [0:W_ENTRIES-1];
reg [WEIDXW-1:0] w_alloc_ptr;
// ERR-0029 fix (N=8 @64MHz critical-path, measured via real P&R:
// worst seed1 total delay 17.909ns, 84% routing, dominant hop
// 2.5-2.8ns): the original RTL used a sequential for-loop that
// overwrites w_hit_idx_c on every match ("last valid+matching entry
// wins"), which Yosys/nextpnr synthesized as a serially-dependent
// cascade of PFUMX/OFX fast-mux primitives -- each entry's result
// depends on the previous one, forcing nextpnr to place the whole
// chain along one physical path with no freedom to shorten it. This
// is the same architectural fix class as ERR-0028 (activation_fill_
// ctrl's max-tree): replace the serial dependency chain with a flat
// one-hot compare (fully parallel, W_ENTRIES=4 comparators, no
// inter-entry dependency) followed by a single-level priority-encode
// casez, preserving the EXACT original "highest index wins" semantics
// bit-for-bit (verified: original loop always ends on the highest ei
// that matched, since ei counts up without break).
wire [W_ENTRIES-1:0] w_match_oh;
genvar wgi;
generate
for (wgi = 0; wgi < W_ENTRIES; wgi = wgi + 1) begin : GEN_WMATCH
assign w_match_oh[wgi] = w_cache_valid[wgi] && (w_cache_addr[wgi] == w_addr);
end
endgenerate
reg w_hit_found_c;
reg [WEIDXW-1:0] w_hit_idx_c;
integer ei;
generate
if (W_ENTRIES == 4) begin : GEN_WHIT_FLAT
// real, measured configuration (see ERR-0029) -- flat,
// single-level priority encode over the parallel one-hot
// compare above, no serial inter-entry dependency.
always @(*) begin
w_hit_found_c = |w_match_oh;
casez (w_match_oh)
4'b1???: w_hit_idx_c = 2'd3;
4'b01??: w_hit_idx_c = 2'd2;
4'b001?: w_hit_idx_c = 2'd1;
4'b0001: w_hit_idx_c = 2'd0;
default: w_hit_idx_c = {WEIDXW{1'b0}};
endcase
end
end else begin : GEN_WHIT_FALLBACK
// any other W_ENTRIES value: fall back to the original,
// functionally-equivalent (but serially-dependent) scan --
// not the measured/optimized configuration this project
// actually builds, kept only for parametric safety.
always @(*) begin
w_hit_found_c = 1'b0;
w_hit_idx_c = {WEIDXW{1'b0}};
for (ei = 0; ei < W_ENTRIES; ei = ei + 1) begin
if (w_cache_valid[ei] && w_cache_addr[ei] == w_addr) begin
w_hit_found_c = 1'b1;
w_hit_idx_c = ei[WEIDXW-1:0];
end
end
end
end
endgenerate
wire w_cache_hit = w_hit_found_c && w_req;
// ============================================================
// Shared physical controller, BURST_LEN=8 (128-bit/16-byte real
// SDRAM transactions), reused UNCHANGED from STEP16-18.
// ============================================================
reg ctrl_req;
reg ctrl_wr;
reg [ADDR_WIDTH-2:0] ctrl_addr;
reg [127:0] ctrl_wdata;
reg [15:0] ctrl_wmask;
wire [127:0] ctrl_rdata;
wire ctrl_ready;
wire ctrl_busy;
sdram_cdc_bridge_openrow #(
.CLK_FREQ_MHZ_FAST(115), .BURST_LEN(8),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_sdram_ctrl (
.clk_slow(clk), .rst_slow(rst),
.clk_fast(clk_fast), .rst_fast(rst_fast),
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr),
.wdata(ctrl_wdata), .wmask(ctrl_wmask),
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy),
.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)
);
localparam S_IDLE = 3'd0,
S_W_WAIT = 3'd1,
S_AR_RD_WAIT = 3'd2,
S_AR_WR_WAIT = 3'd3;
reg [2:0] state;
reg w_pending_upper_half;
reg [ADDR_WIDTH-1:0] w_pending_addr;
reg [2:0] ar_pending_word;
// ---- req_pending latches (same fix class as sdram_controller.v's
// own ERR-0019/ERR-0020): this backend's own top-level S_IDLE
// arbitration can only START a new transaction when it is
// genuinely idle. A single-cycle w_req/ar_req pulse (this
// project's own established mem_req convention) arriving on a
// cycle this backend happens to be busy servicing the OTHER port
// would otherwise be silently dropped -- the caller has no idea,
// waits forever for a `ready` that never comes. Found the hard way
// (STEP19, EXP-0048): the first real N=4 D-Stress run deadlocked
// at 0/256 neurons, jobs_allocated stuck at 12, because the very
// first activation-fill read raced against weight-prefetch traffic
// and was lost exactly this way. Fix: latch EVERY req's own fields
// unconditionally, every cycle, regardless of current state (not
// just from S_IDLE), mirroring sdram_controller.v's own corrected
// fix exactly (ERR-0020: the FIRST attempt only latched from
// S_IDLE, which was still not enough -- latch unconditionally).
reg w_req_pending;
reg [ADDR_WIDTH-1:0] w_req_addr_lat;
reg ar_req_pending;
reg ar_req_wr_lat;
reg [ADDR_WIDTH-1:0] ar_req_addr_lat;
reg [15:0] ar_req_wdata_lat;
reg ar_req_lbn_lat, ar_req_ubn_lat;
wire w_eff_req = w_req || w_req_pending;
wire [ADDR_WIDTH-1:0] w_eff_addr = w_req ? w_addr : w_req_addr_lat;
wire ar_eff_req = ar_req || ar_req_pending;
wire ar_eff_wr = ar_req ? ar_wr : ar_req_wr_lat;
wire [ADDR_WIDTH-1:0] ar_eff_addr = ar_req ? ar_addr : ar_req_addr_lat;
wire [15:0] ar_eff_wdata= ar_req ? ar_wdata : ar_req_wdata_lat;
wire ar_eff_lbn = ar_req ? ar_lb_n : ar_req_lbn_lat;
wire ar_eff_ubn = ar_req ? ar_ub_n : ar_req_ubn_lat;
wire [ADDR_WIDTH-2:0] w_eff_aligned_word_addr = {w_eff_addr[ADDR_WIDTH-1:4], 3'b000};
wire w_eff_addr_is_upper_half = w_eff_addr[3];
wire [ADDR_WIDTH-2:0] ar_eff_block_base = {ar_eff_addr[ADDR_WIDTH-2:3], 3'b000};
wire [2:0] ar_eff_word_in_blk = ar_eff_addr[2:0];
integer ri;
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
for (ri = 0; ri < W_ENTRIES; ri = ri + 1) w_cache_valid[ri] <= 1'b0;
w_alloc_ptr <= {WEIDXW{1'b0}};
ctrl_req <= 1'b0; ctrl_wr <= 1'b0; ctrl_addr <= {(ADDR_WIDTH-1){1'b0}};
ctrl_wdata <= 128'h0; ctrl_wmask <= 16'hFFFF;
w_ready <= 1'b0; w_rdata <= 64'h0;
ar_ready <= 1'b0; ar_rdata <= 16'h0;
w_pending_upper_half <= 1'b0; w_pending_addr <= {ADDR_WIDTH{1'b0}};
ar_pending_word <= 3'h0;
w_req_pending <= 1'b0; w_req_addr_lat <= {ADDR_WIDTH{1'b0}};
ar_req_pending <= 1'b0; ar_req_wr_lat <= 1'b0;
ar_req_addr_lat <= {ADDR_WIDTH{1'b0}}; ar_req_wdata_lat <= 16'h0;
ar_req_lbn_lat <= 1'b1; ar_req_ubn_lat <= 1'b1;
end else begin
ctrl_req <= 1'b0;
w_ready <= 1'b0;
ar_ready <= 1'b0;
// latch fresh requests unconditionally, every cycle,
// regardless of state (see req_pending's own comment above)
if (w_req) begin
w_req_addr_lat <= w_addr;
w_req_pending <= 1'b1;
end
if (ar_req) begin
ar_req_wr_lat <= ar_wr;
ar_req_addr_lat <= ar_addr;
ar_req_wdata_lat <= ar_wdata;
ar_req_lbn_lat <= ar_lb_n;
ar_req_ubn_lat <= ar_ub_n;
ar_req_pending <= 1'b1;
end
case (state)
S_IDLE: begin
// W has priority when both are pending (real
// measured traffic: weight >>> activation+result,
// STEP17 EXP-0045 -- AR is never starved since W's
// own real access pattern idles between tiles).
if (w_cache_hit) begin
// fully serviced THIS cycle -- must also cancel
// the unconditional latch above, which just set
// w_req_pending<=1 for this SAME w_req pulse
// (real bug found via full regression, EXP-0048
// /ERR-0023: without this the latch survives
// uncontested, and next cycle w_eff_req reads
// true from STALE w_req_pending/w_req_addr_lat,
// issuing a bogus extra fetch that shifts every
// subsequent response by one).
w_rdata <= w_cache_data[w_hit_idx_c];
w_ready <= 1'b1;
w_cache_valid[w_hit_idx_c] <= 1'b0;
w_req_pending <= 1'b0;
end else if (w_eff_req) begin
ctrl_req <= 1'b1;
ctrl_wr <= 1'b0;
ctrl_addr <= w_eff_aligned_word_addr;
ctrl_wmask <= 16'h0000;
w_pending_upper_half <= w_eff_addr_is_upper_half;
w_pending_addr <= w_eff_addr;
w_req_pending <= 1'b0;
state <= S_W_WAIT;
end else if (ar_eff_req && !ar_eff_wr) begin
ctrl_req <= 1'b1;
ctrl_wr <= 1'b0;
ctrl_addr <= ar_eff_block_base;
ctrl_wmask <= 16'h0000;
ar_pending_word <= ar_eff_word_in_blk;
ar_req_pending <= 1'b0;
state <= S_AR_RD_WAIT;
end else if (ar_eff_req && ar_eff_wr) begin
// mask every word except the target one; within
// the target word, pass ar_lb_n/ar_ub_n through
// directly (same active-low "write this byte"
// polarity as real SDRAM DQM: lb_n=0 -> DQM=0
// -> byte written; lb_n=1 -> DQM=1 -> masked).
ctrl_req <= 1'b1;
ctrl_wr <= 1'b1;
ctrl_addr <= ar_eff_block_base;
ctrl_wdata <= {8{ar_eff_wdata}}; // replicate; only the target word's mask bits matter
ctrl_wmask <= {16{1'b1}} & ~(16'h0003 << (ar_eff_word_in_blk*2)) | ({14'b0, ar_eff_ubn, ar_eff_lbn} << (ar_eff_word_in_blk*2));
ar_req_pending <= 1'b0;
state <= S_AR_WR_WAIT;
end
end
S_W_WAIT: begin
if (ctrl_ready) begin
if (w_pending_upper_half) begin
w_rdata <= ctrl_rdata[127:64];
w_cache_data[w_alloc_ptr] <= ctrl_rdata[63:0];
w_cache_addr[w_alloc_ptr] <= w_pending_addr - {{(ADDR_WIDTH-4){1'b0}}, 4'd8};
end else begin
w_rdata <= ctrl_rdata[63:0];
w_cache_data[w_alloc_ptr] <= ctrl_rdata[127:64];
w_cache_addr[w_alloc_ptr] <= w_pending_addr + {{(ADDR_WIDTH-4){1'b0}}, 4'd8};
end
w_cache_valid[w_alloc_ptr] <= 1'b1;
w_alloc_ptr <= (w_alloc_ptr == W_ENTRIES[WEIDXW-1:0]-1'b1) ? {WEIDXW{1'b0}} : w_alloc_ptr + 1'b1;
w_ready <= 1'b1;
state <= S_IDLE;
end
end
S_AR_RD_WAIT: begin
if (ctrl_ready) begin
ar_rdata <= ctrl_rdata[ar_pending_word*16 +: 16];
ar_ready <= 1'b1;
state <= S_IDLE;
end
end
S_AR_WR_WAIT: begin
if (ctrl_ready) begin
ar_ready <= 1'b1;
state <= S_IDLE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule
@@ -0,0 +1,382 @@
`timescale 1ns/1ps
// ============================================================
// NMS STEP19 -- UNIFIED single-SDRAM memory backend.
//
// Replaces BOTH physical memory paths that existed through STEP18
// (sdram_weight_backend_pack128.v for weights, and hardware/v1/rtl/
// memory_interface.v + psram_controller.v for activation-fill/result-
// writeback) with ONE physical AS4C4M16SA-6TIN SDRAM chip, ONE
// sdram_controller.v instance (BURST_LEN=8), serving THREE logical
// traffic classes through TWO external ports that exactly match what
// the existing, UNCHANGED consumers already drive:
//
// W port (64-bit): weight_prefetch_engine_wide.v's own real
// traffic, via slot_mem_arbiter_wide.v -- IDENTICAL external
// contract to STEP18's sdram_weight_backend_pack128.v (byte
// address in, 64-bit mem_rdata out), and internally reuses that
// module's own validated N_ENTRIES=4 "other half" cache
// unchanged (EXP-0046/ERR-0022's own fix, not re-derived here).
//
// AR port (16-bit, byte-maskable): nms_activation_fill_ctrl_v3.v's
// own activation reads AND every per-slot nms_memory_manager_
// stream_wide.v's own result writes, via slot_mem_arbiter.v --
// IDENTICAL external contract to the real V1 psram_controller.v
// port it replaces (word address in, 16-bit mem_wdata/mem_rdata,
// mem_lb_n/mem_ub_n byte-lane write masking). Neither
// nms_activation_fill_ctrl_v3.v nor nms_memory_manager_stream_
// wide.v needed ANY change -- they already produce a WORD
// address and already drive lb_n/ub_n exactly as the real V1
// PSRAM controller expected.
//
// Neither weight_prefetch_engine_wide.v, nms_activation_fill_ctrl_v3.
// v, nms_memory_manager_stream_wide.v, nor neural_processor.v changed
// AT ALL for this step -- this is a pure memory-side substitution,
// per the governing spec's own explicit instruction.
//
// KEY ENABLING FACT: real SDR SDRAM's own DQM pins are a per-BYTE
// write mask (STEP19's own real, tested extension to sdram_
// controller.v's `wmask` port) -- this lets a single-BYTE result
// write happen INSIDE a shared BURST_LEN=8 (128-bit) transaction by
// masking out every byte except the one/two the caller actually wants
// written, with NO read-modify-write needed at all (the real SDRAM
// chip itself leaves masked bytes untouched, by JEDEC definition).
// Activation reads need no such trick -- a full 128-bit block is
// fetched and the caller's own requested 16-bit word is extracted
// combinationally from it.
//
// Arbitration: simple, correctness-first 2-way priority (weight
// traffic strongly dominates real measured traffic -- STEP17 showed
// the activation/result path at <=7.2% of all external-memory
// activity -- so W is granted priority when both are pending, AR is
// never starved since W's own real traffic pattern always eventually
// idles between tiles/jobs). Exactly one physical SDRAM transaction
// in flight at a time (matches sdram_controller.v's own inherent
// single-transaction design, STEP18 Part E's own documented, accepted
// scope boundary -- not revisited here).
// ============================================================
module sdram_unified_backend_openrow #(
parameter ADDR_WIDTH = 26, // byte address width (W port convention)
parameter CLK_FREQ_MHZ = 64,
parameter W_ENTRIES = 4, // weight-cache depth, >= real N_SLOTS
// physical SDRAM geometry, forwarded directly to sdram_controller.v
// (AS4C32M16SA defaults: 13 row bits/A0-A12, 10 col bits/A0-A9,
// 2 bank bits/BA0-BA1) -- must satisfy ADDR_WIDTH-1 ==
// BANK_BITS+ROW_BITS+COL_BITS (byte address = word address + 1 bit),
// asserted at elaboration below.
parameter ROW_BITS = 13,
parameter COL_BITS = 10,
parameter BANK_BITS = 2
)(
input wire clk,
input wire rst,
// ---- W: weight fetch (64-bit, byte address, read-only) ----
input wire w_req,
input wire [ADDR_WIDTH-1:0] w_addr,
output reg [63:0] w_rdata,
output reg w_ready,
// ---- AR: activation-fill (read) + result-writeback (write),
// 16-bit, WORD address (matches the real V1 psram_controller.v
// convention this port replaces exactly) ----
input wire ar_req,
input wire ar_wr,
input wire [ADDR_WIDTH-1:0] ar_addr, // word address, low 22 bits meaningful
// (matches slot_mem_arbiter.v's own
// m_addr width convention exactly --
// that arbiter's real callers only ever
// drive a 22-bit-significant word
// address into an ADDR_WIDTH-wide bus)
input wire [15:0] ar_wdata,
input wire ar_lb_n,
input wire ar_ub_n,
output reg [15:0] ar_rdata,
output reg ar_ready,
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
);
initial if (ADDR_WIDTH != BANK_BITS + ROW_BITS + COL_BITS + 1) begin
$display("FATAL sdram_unified_backend: ADDR_WIDTH(%0d) != BANK_BITS(%0d)+ROW_BITS(%0d)+COL_BITS(%0d)+1",
ADDR_WIDTH, BANK_BITS, ROW_BITS, COL_BITS);
$finish;
end
// ============================================================
// W-port cache (identical logic to sdram_weight_backend_pack128.v
// -- an N_ENTRIES-deep, fully-associative "other half" cache,
// round-robin allocated; safe under any sizing, see that module's
// own header/ERR-0022 for the full rationale, not repeated here)
// ============================================================
localparam WEIDXW = (W_ENTRIES <= 1) ? 1 : $clog2(W_ENTRIES);
reg w_cache_valid [0:W_ENTRIES-1];
reg [ADDR_WIDTH-1:0] w_cache_addr [0:W_ENTRIES-1];
reg [63:0] w_cache_data [0:W_ENTRIES-1];
reg [WEIDXW-1:0] w_alloc_ptr;
// ERR-0029 fix (N=8 @64MHz critical-path, measured via real P&R:
// worst seed1 total delay 17.909ns, 84% routing, dominant hop
// 2.5-2.8ns): the original RTL used a sequential for-loop that
// overwrites w_hit_idx_c on every match ("last valid+matching entry
// wins"), which Yosys/nextpnr synthesized as a serially-dependent
// cascade of PFUMX/OFX fast-mux primitives -- each entry's result
// depends on the previous one, forcing nextpnr to place the whole
// chain along one physical path with no freedom to shorten it. This
// is the same architectural fix class as ERR-0028 (activation_fill_
// ctrl's max-tree): replace the serial dependency chain with a flat
// one-hot compare (fully parallel, W_ENTRIES=4 comparators, no
// inter-entry dependency) followed by a single-level priority-encode
// casez, preserving the EXACT original "highest index wins" semantics
// bit-for-bit (verified: original loop always ends on the highest ei
// that matched, since ei counts up without break).
wire [W_ENTRIES-1:0] w_match_oh;
genvar wgi;
generate
for (wgi = 0; wgi < W_ENTRIES; wgi = wgi + 1) begin : GEN_WMATCH
assign w_match_oh[wgi] = w_cache_valid[wgi] && (w_cache_addr[wgi] == w_addr);
end
endgenerate
reg w_hit_found_c;
reg [WEIDXW-1:0] w_hit_idx_c;
integer ei;
generate
if (W_ENTRIES == 4) begin : GEN_WHIT_FLAT
// real, measured configuration (see ERR-0029) -- flat,
// single-level priority encode over the parallel one-hot
// compare above, no serial inter-entry dependency.
always @(*) begin
w_hit_found_c = |w_match_oh;
casez (w_match_oh)
4'b1???: w_hit_idx_c = 2'd3;
4'b01??: w_hit_idx_c = 2'd2;
4'b001?: w_hit_idx_c = 2'd1;
4'b0001: w_hit_idx_c = 2'd0;
default: w_hit_idx_c = {WEIDXW{1'b0}};
endcase
end
end else begin : GEN_WHIT_FALLBACK
// any other W_ENTRIES value: fall back to the original,
// functionally-equivalent (but serially-dependent) scan --
// not the measured/optimized configuration this project
// actually builds, kept only for parametric safety.
always @(*) begin
w_hit_found_c = 1'b0;
w_hit_idx_c = {WEIDXW{1'b0}};
for (ei = 0; ei < W_ENTRIES; ei = ei + 1) begin
if (w_cache_valid[ei] && w_cache_addr[ei] == w_addr) begin
w_hit_found_c = 1'b1;
w_hit_idx_c = ei[WEIDXW-1:0];
end
end
end
end
endgenerate
wire w_cache_hit = w_hit_found_c && w_req;
// ============================================================
// Shared physical controller, BURST_LEN=8 (128-bit/16-byte real
// SDRAM transactions), reused UNCHANGED from STEP16-18.
// ============================================================
reg ctrl_req;
reg ctrl_wr;
reg [ADDR_WIDTH-2:0] ctrl_addr;
reg [127:0] ctrl_wdata;
reg [15:0] ctrl_wmask;
wire [127:0] ctrl_rdata;
wire ctrl_ready;
wire ctrl_busy;
sdram_controller_openrow #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .BURST_LEN(8),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_sdram_ctrl (
.clk(clk), .rst(rst),
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr),
.wdata(ctrl_wdata), .wmask(ctrl_wmask),
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy),
.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)
);
localparam S_IDLE = 3'd0,
S_W_WAIT = 3'd1,
S_AR_RD_WAIT = 3'd2,
S_AR_WR_WAIT = 3'd3;
reg [2:0] state;
reg w_pending_upper_half;
reg [ADDR_WIDTH-1:0] w_pending_addr;
reg [2:0] ar_pending_word;
// ---- req_pending latches (same fix class as sdram_controller.v's
// own ERR-0019/ERR-0020): this backend's own top-level S_IDLE
// arbitration can only START a new transaction when it is
// genuinely idle. A single-cycle w_req/ar_req pulse (this
// project's own established mem_req convention) arriving on a
// cycle this backend happens to be busy servicing the OTHER port
// would otherwise be silently dropped -- the caller has no idea,
// waits forever for a `ready` that never comes. Found the hard way
// (STEP19, EXP-0048): the first real N=4 D-Stress run deadlocked
// at 0/256 neurons, jobs_allocated stuck at 12, because the very
// first activation-fill read raced against weight-prefetch traffic
// and was lost exactly this way. Fix: latch EVERY req's own fields
// unconditionally, every cycle, regardless of current state (not
// just from S_IDLE), mirroring sdram_controller.v's own corrected
// fix exactly (ERR-0020: the FIRST attempt only latched from
// S_IDLE, which was still not enough -- latch unconditionally).
reg w_req_pending;
reg [ADDR_WIDTH-1:0] w_req_addr_lat;
reg ar_req_pending;
reg ar_req_wr_lat;
reg [ADDR_WIDTH-1:0] ar_req_addr_lat;
reg [15:0] ar_req_wdata_lat;
reg ar_req_lbn_lat, ar_req_ubn_lat;
wire w_eff_req = w_req || w_req_pending;
wire [ADDR_WIDTH-1:0] w_eff_addr = w_req ? w_addr : w_req_addr_lat;
wire ar_eff_req = ar_req || ar_req_pending;
wire ar_eff_wr = ar_req ? ar_wr : ar_req_wr_lat;
wire [ADDR_WIDTH-1:0] ar_eff_addr = ar_req ? ar_addr : ar_req_addr_lat;
wire [15:0] ar_eff_wdata= ar_req ? ar_wdata : ar_req_wdata_lat;
wire ar_eff_lbn = ar_req ? ar_lb_n : ar_req_lbn_lat;
wire ar_eff_ubn = ar_req ? ar_ub_n : ar_req_ubn_lat;
wire [ADDR_WIDTH-2:0] w_eff_aligned_word_addr = {w_eff_addr[ADDR_WIDTH-1:4], 3'b000};
wire w_eff_addr_is_upper_half = w_eff_addr[3];
wire [ADDR_WIDTH-2:0] ar_eff_block_base = {ar_eff_addr[ADDR_WIDTH-2:3], 3'b000};
wire [2:0] ar_eff_word_in_blk = ar_eff_addr[2:0];
integer ri;
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
for (ri = 0; ri < W_ENTRIES; ri = ri + 1) w_cache_valid[ri] <= 1'b0;
w_alloc_ptr <= {WEIDXW{1'b0}};
ctrl_req <= 1'b0; ctrl_wr <= 1'b0; ctrl_addr <= {(ADDR_WIDTH-1){1'b0}};
ctrl_wdata <= 128'h0; ctrl_wmask <= 16'hFFFF;
w_ready <= 1'b0; w_rdata <= 64'h0;
ar_ready <= 1'b0; ar_rdata <= 16'h0;
w_pending_upper_half <= 1'b0; w_pending_addr <= {ADDR_WIDTH{1'b0}};
ar_pending_word <= 3'h0;
w_req_pending <= 1'b0; w_req_addr_lat <= {ADDR_WIDTH{1'b0}};
ar_req_pending <= 1'b0; ar_req_wr_lat <= 1'b0;
ar_req_addr_lat <= {ADDR_WIDTH{1'b0}}; ar_req_wdata_lat <= 16'h0;
ar_req_lbn_lat <= 1'b1; ar_req_ubn_lat <= 1'b1;
end else begin
ctrl_req <= 1'b0;
w_ready <= 1'b0;
ar_ready <= 1'b0;
// latch fresh requests unconditionally, every cycle,
// regardless of state (see req_pending's own comment above)
if (w_req) begin
w_req_addr_lat <= w_addr;
w_req_pending <= 1'b1;
end
if (ar_req) begin
ar_req_wr_lat <= ar_wr;
ar_req_addr_lat <= ar_addr;
ar_req_wdata_lat <= ar_wdata;
ar_req_lbn_lat <= ar_lb_n;
ar_req_ubn_lat <= ar_ub_n;
ar_req_pending <= 1'b1;
end
case (state)
S_IDLE: begin
// W has priority when both are pending (real
// measured traffic: weight >>> activation+result,
// STEP17 EXP-0045 -- AR is never starved since W's
// own real access pattern idles between tiles).
if (w_cache_hit) begin
// fully serviced THIS cycle -- must also cancel
// the unconditional latch above, which just set
// w_req_pending<=1 for this SAME w_req pulse
// (real bug found via full regression, EXP-0048
// /ERR-0023: without this the latch survives
// uncontested, and next cycle w_eff_req reads
// true from STALE w_req_pending/w_req_addr_lat,
// issuing a bogus extra fetch that shifts every
// subsequent response by one).
w_rdata <= w_cache_data[w_hit_idx_c];
w_ready <= 1'b1;
w_cache_valid[w_hit_idx_c] <= 1'b0;
w_req_pending <= 1'b0;
end else if (w_eff_req) begin
ctrl_req <= 1'b1;
ctrl_wr <= 1'b0;
ctrl_addr <= w_eff_aligned_word_addr;
ctrl_wmask <= 16'h0000;
w_pending_upper_half <= w_eff_addr_is_upper_half;
w_pending_addr <= w_eff_addr;
w_req_pending <= 1'b0;
state <= S_W_WAIT;
end else if (ar_eff_req && !ar_eff_wr) begin
ctrl_req <= 1'b1;
ctrl_wr <= 1'b0;
ctrl_addr <= ar_eff_block_base;
ctrl_wmask <= 16'h0000;
ar_pending_word <= ar_eff_word_in_blk;
ar_req_pending <= 1'b0;
state <= S_AR_RD_WAIT;
end else if (ar_eff_req && ar_eff_wr) begin
// mask every word except the target one; within
// the target word, pass ar_lb_n/ar_ub_n through
// directly (same active-low "write this byte"
// polarity as real SDRAM DQM: lb_n=0 -> DQM=0
// -> byte written; lb_n=1 -> DQM=1 -> masked).
ctrl_req <= 1'b1;
ctrl_wr <= 1'b1;
ctrl_addr <= ar_eff_block_base;
ctrl_wdata <= {8{ar_eff_wdata}}; // replicate; only the target word's mask bits matter
ctrl_wmask <= {16{1'b1}} & ~(16'h0003 << (ar_eff_word_in_blk*2)) | ({14'b0, ar_eff_ubn, ar_eff_lbn} << (ar_eff_word_in_blk*2));
ar_req_pending <= 1'b0;
state <= S_AR_WR_WAIT;
end
end
S_W_WAIT: begin
if (ctrl_ready) begin
if (w_pending_upper_half) begin
w_rdata <= ctrl_rdata[127:64];
w_cache_data[w_alloc_ptr] <= ctrl_rdata[63:0];
w_cache_addr[w_alloc_ptr] <= w_pending_addr - {{(ADDR_WIDTH-4){1'b0}}, 4'd8};
end else begin
w_rdata <= ctrl_rdata[63:0];
w_cache_data[w_alloc_ptr] <= ctrl_rdata[127:64];
w_cache_addr[w_alloc_ptr] <= w_pending_addr + {{(ADDR_WIDTH-4){1'b0}}, 4'd8};
end
w_cache_valid[w_alloc_ptr] <= 1'b1;
w_alloc_ptr <= (w_alloc_ptr == W_ENTRIES[WEIDXW-1:0]-1'b1) ? {WEIDXW{1'b0}} : w_alloc_ptr + 1'b1;
w_ready <= 1'b1;
state <= S_IDLE;
end
end
S_AR_RD_WAIT: begin
if (ctrl_ready) begin
ar_rdata <= ctrl_rdata[ar_pending_word*16 +: 16];
ar_ready <= 1'b1;
state <= S_IDLE;
end
end
S_AR_WR_WAIT: begin
if (ctrl_ready) begin
ar_ready <= 1'b1;
state <= S_IDLE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule