FASE #1 hardware freeze for FPGA-Neural V2, N4/P8, single external SDRAM (Alliance Memory AS4C4M16SA-6TIN) serving weights, activations, and results through one physical sdram_controller.v instance. Removes the PSRAM dependency (hardware/v1/rtl/psram_controller.v + memory_interface.v) from the V2 physical path entirely -- V1 itself remains fully unmodified, the golden reference. New RTL: sdram_unified_backend.v (2-way W/AR arbitration over one SDRAM controller, real per-byte DQM write masking added to sdram_controller.v for correct single-byte result writes with no read-modify-write), nms_neural_multiprocessor_sdram_unified.v (the frozen top-level). Two real bugs found and fixed via full-system testing before being accepted (ERR-0023): a deadlock and an off-by-one data-shift bug in the new arbitration logic. Real results: N=4 and N=2 D-Stress bit-exact (256/256 neurons), 40 real AUTO REFRESH events interleaved with zero corruption, real Yosys+nextpnr-ecp5 synthesis/P&R for LFE5U-45F-8CABGA381 (149/245 TRELLIS_IO, a real 45-pin reduction from the prior dual-memory design). Timing is MARGINAL (1/8 P&R seeds >=80MHz), reported honestly rather than masked by the best seed. Real, sourced ball-level pinout for the SDRAM bus + clk/rst (39/149 signals, P&R-verified) using the official Lattice ECP5U-45 pinout CSV found on disk during this step's own pre-commit review -- corrects an earlier draft that wrongly assumed no real pinout data was available. Chip readiness: NO. Real, disclosed blockers remain (no physical host interface exists yet -- the RTL's own reg_* ports are a 110-pin raw test-harness bus; clock source/PLL decision; power/configuration component selection) -- see hardware/v2/docs/{HARDWARE_FREEZE, CHIP_READINESS,OPEN_ITEMS}.md for the complete, itemized status. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
317 lines
14 KiB
Verilog
317 lines
14 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// NMS STEP16 -- behavioral model of Alliance Memory AS4C4M16SA-6TIN
|
|
// (SDR SDRAM, 64Mbit/8MB, x16, 4 banks x 4096 rows x 256 cols).
|
|
//
|
|
// Real JEDEC command decode (CS#/RAS#/CAS#/WE#), real per-bank
|
|
// state tracking (IDLE / ACTIVE with an open row), and REAL timing-
|
|
// violation assertions (tRCD, tRP, tRAS-min, tRC, tMRD) -- this model
|
|
// does not merely "accept whatever the controller sends"; it actively
|
|
// checks the controller's own real compliance with datasheet timing,
|
|
// same rigor this project applies to psram_model.v elsewhere. A
|
|
// timing violation here is a genuine controller bug, not tolerated
|
|
// silently.
|
|
//
|
|
// Refresh is tracked per-row (a real 4096-row array of "last
|
|
// refreshed at cycle N" timestamps) and checked against tREFI --
|
|
// data itself is not modeled as decaying (unnecessary complexity for
|
|
// this validation), but an insufficiently-refreshed row is flagged
|
|
// via a real, visible warning/assertion, not silently ignored.
|
|
//
|
|
// CAS_LATENCY and BURST_LEN are read back from the real LOAD MODE
|
|
// REGISTER command's own address bits (not assumed equal to the
|
|
// controller's own parameters) -- this model independently decodes
|
|
// the mode register exactly as real silicon would, so a controller
|
|
// bug in the MRS encoding would be caught here too.
|
|
// ============================================================
|
|
module sdram_model #(
|
|
parameter CLK_FREQ_MHZ = 166
|
|
)(
|
|
input wire clk,
|
|
input wire cke,
|
|
input wire cs_n,
|
|
input wire ras_n,
|
|
input wire cas_n,
|
|
input wire we_n,
|
|
input wire [1:0] ba,
|
|
input wire [11:0] a,
|
|
inout wire [15:0] dq,
|
|
input wire [1:0] dqm
|
|
);
|
|
localparam BANKS = 4;
|
|
localparam ROWS = 4096;
|
|
localparam COLS = 256;
|
|
|
|
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(18);
|
|
localparam T_RP = ns_to_cycles(18);
|
|
localparam T_RAS_MIN= ns_to_cycles(42);
|
|
localparam T_RC = ns_to_cycles(60);
|
|
localparam T_MRD = ns_to_cycles(12);
|
|
localparam T_REFI = ns_to_cycles(15625);
|
|
|
|
reg [15:0] mem [0:BANKS*ROWS*COLS-1];
|
|
|
|
// per-bank state
|
|
reg bank_active [0:BANKS-1];
|
|
reg [11:0] bank_row [0:BANKS-1];
|
|
integer bank_active_since [0:BANKS-1]; // cycle ACTIVATE was issued
|
|
integer bank_precharge_since [0:BANKS-1]; // cycle last PRECHARGE completed
|
|
|
|
// mode register (decoded from a real LOAD MODE REGISTER command)
|
|
reg [2:0] mr_burst_code;
|
|
reg [2:0] mr_cas_latency;
|
|
integer burst_len;
|
|
integer cas_latency;
|
|
|
|
integer cycle;
|
|
always @(posedge clk) cycle <= cycle + 1;
|
|
|
|
// refresh bookkeeping: last-refreshed cycle per row (across all
|
|
// banks -- real SDRAM refreshes one row address across ALL banks
|
|
// per AUTO REFRESH command)
|
|
integer last_refresh_cycle [0:ROWS-1];
|
|
integer last_any_refresh_cycle;
|
|
|
|
// ---- command decode ----
|
|
wire cmd_active = !cs_n && !ras_n && cas_n && we_n;
|
|
wire cmd_read = !cs_n && ras_n && !cas_n && we_n;
|
|
wire cmd_write = !cs_n && ras_n && !cas_n && !we_n;
|
|
wire cmd_precharge= !cs_n && !ras_n && cas_n && !we_n;
|
|
wire cmd_refresh = !cs_n && !ras_n && !cas_n && we_n;
|
|
wire cmd_mrs = !cs_n && !ras_n && !cas_n && !we_n;
|
|
|
|
// ---- read burst delivery (CAS-latency-delayed shift pipeline) ----
|
|
reg [15:0] rd_pipe [0:15]; // generous depth, only first `cas_latency` entries meaningful
|
|
reg rd_valid_pipe [0:15];
|
|
// combinational output stage: rd_pipe[0] IS the chip's own output
|
|
// register in real SDR SDRAM, already counted inside CAS_LATENCY --
|
|
// driving dq_out through one more registered NBA stage here would
|
|
// silently add an extra cycle of latency the controller doesn't
|
|
// expect (a real bug found by tracing a stubborn one-cycle-late
|
|
// read-data mismatch against the controller's own CAS_LATENCY-cycle
|
|
// wait_cnt derivation)
|
|
wire [15:0] dq_out = rd_pipe[0];
|
|
wire dq_out_en = rd_valid_pipe[0];
|
|
assign dq = dq_out_en ? dq_out : 16'hzzzz;
|
|
|
|
// active read-burst tracking (for auto-precharge/address auto-increment)
|
|
reg rd_burst_active;
|
|
reg [1:0] rd_bank;
|
|
reg [11:0] rd_row;
|
|
reg [7:0] rd_col;
|
|
integer rd_remaining;
|
|
reg rd_autoprecharge;
|
|
|
|
reg wr_burst_active;
|
|
reg [1:0] wr_bank;
|
|
reg [11:0] wr_row;
|
|
reg [7:0] wr_col;
|
|
integer wr_remaining;
|
|
reg wr_autoprecharge;
|
|
|
|
integer i;
|
|
|
|
initial begin
|
|
cycle = 0;
|
|
last_any_refresh_cycle = 0;
|
|
burst_len = 1; cas_latency = 3;
|
|
rd_burst_active = 0; wr_burst_active = 0;
|
|
for (i = 0; i < BANKS; i = i + 1) begin
|
|
bank_active[i] = 0; bank_active_since[i] = -1000000; bank_precharge_since[i] = 0;
|
|
end
|
|
for (i = 0; i < ROWS; i = i + 1) last_refresh_cycle[i] = 0;
|
|
for (i = 0; i < 16; i = i + 1) begin rd_pipe[i] = 0; rd_valid_pipe[i] = 0; end
|
|
end
|
|
|
|
// Combinational "about to insert a freshly-read word into the
|
|
// CAS-latency pipe this cycle" signals, computed from the
|
|
// CURRENT (pre-edge) burst-tracking registers -- kept OUTSIDE the
|
|
// always block and fed into a single, conflict-free per-index
|
|
// shift-or-insert assignment below (an earlier draft assigned
|
|
// rd_pipe[cas_latency-1] from BOTH the generic shift loop AND a
|
|
// separate insert statement in the same always block -- two NBAs
|
|
// to the same array element in the same time step, undefined/
|
|
// tool-dependent behavior, a real bug caught before simulation
|
|
// even ran, by inspection).
|
|
wire pipe_insert = rd_burst_active;
|
|
wire [15:0] pipe_insert_val = mem[rd_bank*ROWS*COLS + rd_row*COLS + rd_col];
|
|
|
|
always @(posedge clk) begin
|
|
// ---- shift the CAS-latency read pipeline every cycle, with
|
|
// AT MOST one insertion point per cycle (index cas_latency-1),
|
|
// never both a shift-in and an insert targeting the same
|
|
// index ----
|
|
for (i = 0; i < 15; i = i + 1) begin
|
|
if (pipe_insert && i == cas_latency-1) begin
|
|
rd_pipe[i] <= pipe_insert_val;
|
|
rd_valid_pipe[i] <= 1'b1;
|
|
end else begin
|
|
rd_pipe[i] <= rd_pipe[i+1];
|
|
rd_valid_pipe[i] <= rd_valid_pipe[i+1];
|
|
end
|
|
end
|
|
rd_pipe[15] <= 16'h0000;
|
|
rd_valid_pipe[15] <= 1'b0;
|
|
|
|
if (!cke) begin
|
|
// CKE low: real part would be in power-down/self-refresh;
|
|
// not exercised by this controller (CKE held high always)
|
|
end else begin
|
|
|
|
if (cmd_active) begin
|
|
// real timing check: bank must be idle, and the
|
|
// PREVIOUS precharge (if any) must satisfy tRP before
|
|
// this activate.
|
|
if (bank_active[ba])
|
|
$display("SDRAM_MODEL VIOLATION @%0t: ACTIVATE to bank %0d while already active (row %0d)", $time, ba, bank_row[ba]);
|
|
if ((cycle - bank_precharge_since[ba]) < T_RP && bank_precharge_since[ba] != 0)
|
|
$display("SDRAM_MODEL VIOLATION @%0t: tRP violated on bank %0d (%0d cycles since precharge, need %0d)",
|
|
$time, ba, cycle-bank_precharge_since[ba], T_RP);
|
|
bank_active[ba] <= 1'b1;
|
|
bank_row[ba] <= a;
|
|
bank_active_since[ba] <= cycle;
|
|
end
|
|
|
|
if (cmd_precharge) begin
|
|
// A10=1 -> precharge all banks; else just `ba`
|
|
for (i = 0; i < BANKS; i = i + 1) begin
|
|
if (a[10] || i == ba) begin
|
|
if (bank_active[i] && ((cycle - bank_active_since[i]) < T_RAS_MIN))
|
|
$display("SDRAM_MODEL VIOLATION @%0t: tRAS(min) violated on bank %0d (%0d cycles since activate, need %0d)",
|
|
$time, i, cycle-bank_active_since[i], T_RAS_MIN);
|
|
bank_active[i] <= 1'b0;
|
|
bank_precharge_since[i] <= cycle;
|
|
end
|
|
end
|
|
end
|
|
|
|
if (cmd_refresh) begin
|
|
if ((cycle - last_any_refresh_cycle) > T_REFI && last_any_refresh_cycle != 0)
|
|
$display("SDRAM_MODEL WARNING @%0t: AUTO REFRESH spacing %0d cycles exceeds tREFI=%0d",
|
|
$time, cycle-last_any_refresh_cycle, T_REFI);
|
|
last_any_refresh_cycle <= cycle;
|
|
end
|
|
|
|
if (cmd_mrs) begin
|
|
mr_burst_code <= a[2:0];
|
|
mr_cas_latency <= a[6:4];
|
|
burst_len <= (a[2:0]==3'b000) ? 1 : (a[2:0]==3'b001) ? 2 :
|
|
(a[2:0]==3'b010) ? 4 : (a[2:0]==3'b011) ? 8 : 1;
|
|
cas_latency <= (a[6:4]==3'b011) ? 3 : (a[6:4]==3'b010) ? 2 : 3;
|
|
end
|
|
|
|
if (cmd_read || cmd_write) begin
|
|
if (!bank_active[ba])
|
|
$display("SDRAM_MODEL VIOLATION @%0t: %s to bank %0d with no active row", $time, cmd_read?"READ":"WRITE", ba);
|
|
else if (bank_row[ba] !== a[11:0] && 1'b0) begin
|
|
// column command doesn't carry a row -- nothing to
|
|
// check here beyond bank-active, real row match is
|
|
// implicit (the address IS the column within the
|
|
// already-open row)
|
|
end
|
|
if ((cycle - bank_active_since[ba]) < T_RCD)
|
|
$display("SDRAM_MODEL VIOLATION @%0t: tRCD violated on bank %0d (%0d cycles since activate, need %0d)",
|
|
$time, ba, cycle-bank_active_since[ba], T_RCD);
|
|
|
|
if (cmd_read) begin
|
|
// real SDR SDRAM: CAS latency counts cycles from the
|
|
// READ command itself -- insert the first word's mem
|
|
// lookup right here (command-decode cycle) instead of
|
|
// waiting for rd_burst_active to become visible one
|
|
// cycle later, which added a spurious extra pipeline
|
|
// stage and made every read arrive one cycle late
|
|
// (the same bug class as the write-side fix below,
|
|
// found by tracing the cycle-exact mismatch against
|
|
// the controller's own CAS_LATENCY-cycle wait_cnt)
|
|
rd_pipe[cas_latency-1] <= mem[ba*ROWS*COLS + bank_row[ba]*COLS + a[7:0]];
|
|
rd_valid_pipe[cas_latency-1] <= 1'b1;
|
|
if (burst_len == 1) begin
|
|
rd_burst_active <= 1'b0;
|
|
if (a[10]) begin
|
|
bank_active[ba] <= 1'b0;
|
|
bank_precharge_since[ba] <= cycle;
|
|
end
|
|
end else begin
|
|
rd_bank <= ba; rd_row <= bank_row[ba]; rd_col <= a[7:0] + 1'b1;
|
|
rd_remaining <= burst_len - 1'b1; rd_autoprecharge <= a[10];
|
|
rd_burst_active <= 1'b1;
|
|
end
|
|
end else begin
|
|
// real SDR SDRAM: the first write word is presented on
|
|
// DQ CONCURRENTLY with the WRITE command itself, not one
|
|
// cycle later -- capture it right here (in the same
|
|
// cycle the command is decoded) instead of waiting for
|
|
// wr_burst_active, which would silently drop word0
|
|
if (dqm[0] == 1'b0) mem[ba*ROWS*COLS + bank_row[ba]*COLS + a[7:0]][7:0] <= dq[7:0];
|
|
if (dqm[1] == 1'b0) mem[ba*ROWS*COLS + bank_row[ba]*COLS + a[7:0]][15:8] <= dq[15:8];
|
|
if (burst_len == 1) begin
|
|
wr_burst_active <= 1'b0;
|
|
if (a[10]) begin
|
|
bank_active[ba] <= 1'b0;
|
|
bank_precharge_since[ba] <= cycle;
|
|
end
|
|
end else begin
|
|
wr_bank <= ba; wr_row <= bank_row[ba]; wr_col <= a[7:0] + 1'b1;
|
|
wr_remaining <= burst_len - 1'b1; wr_autoprecharge <= a[10];
|
|
wr_burst_active <= 1'b1;
|
|
end
|
|
end
|
|
end
|
|
|
|
// ---- service an in-progress read burst: one word/cycle,
|
|
// the actual mem[] read + pipe insertion happens above
|
|
// (pipe_insert/pipe_insert_val, combinational from THIS
|
|
// cycle's rd_bank/rd_row/rd_col) -- here we only advance
|
|
// the column pointer and burst-remaining bookkeeping ----
|
|
if (rd_burst_active) begin
|
|
rd_col <= rd_col + 1'b1;
|
|
rd_remaining <= rd_remaining - 1;
|
|
if (rd_remaining == 1) begin
|
|
rd_burst_active <= 1'b0;
|
|
if (rd_autoprecharge) begin
|
|
bank_active[rd_bank] <= 1'b0;
|
|
bank_precharge_since[rd_bank] <= cycle;
|
|
end
|
|
end
|
|
end
|
|
|
|
// ---- service an in-progress write burst: one word/cycle
|
|
// from dq ----
|
|
if (wr_burst_active) begin
|
|
if (dqm[0] == 1'b0) mem[wr_bank*ROWS*COLS + wr_row*COLS + wr_col][7:0] <= dq[7:0];
|
|
if (dqm[1] == 1'b0) mem[wr_bank*ROWS*COLS + wr_row*COLS + wr_col][15:8] <= dq[15:8];
|
|
wr_col <= wr_col + 1'b1;
|
|
wr_remaining <= wr_remaining - 1;
|
|
if (wr_remaining == 1) begin
|
|
wr_burst_active <= 1'b0;
|
|
if (wr_autoprecharge) begin
|
|
bank_active[wr_bank] <= 1'b0;
|
|
bank_precharge_since[wr_bank] <= cycle;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
// testbench-only backdoor access (poke/peek), matching this
|
|
// project's own established convention elsewhere (psram_model.v)
|
|
task automatic backdoor_write(input [1:0] tb_bank, input [11:0] tb_row, input [7:0] tb_col, input [15:0] val);
|
|
begin
|
|
mem[tb_bank*ROWS*COLS + tb_row*COLS + tb_col] = val;
|
|
end
|
|
endtask
|
|
function automatic [15:0] backdoor_read(input [1:0] tb_bank, input [11:0] tb_row, input [7:0] tb_col);
|
|
begin
|
|
backdoor_read = mem[tb_bank*ROWS*COLS + tb_row*COLS + tb_col];
|
|
end
|
|
endfunction
|
|
|
|
endmodule
|