Memory upgrade, at the user's own explicit request: Alliance Memory AS4C4M16SA-6TIN (64Mbit/8MB) -> AS4C32M16SB-7BIN (512Mbit/64MB, 54-ball TFBGA), the largest same-family SDR SDRAM Alliance Memory offers. Real-datasheet-driven (whole AS4C4M16SA/AS4C8M16SA/AS4C16M16SA/ AS4C32M16SA family investigated): 13 row bits (was 12, one new FPGA pin sdram_a[12]/ball F1), 10 column bits (was 8), real -7-grade AC timing (tRCD/tRP improved to 15ns, tREFI halved to 7.8us for the doubled row count). sdram_controller.v and sdram_model.v gained real ROW_BITS/COL_BITS/BANK_BITS parameters (was hardcoded 12/8/2). ADDR_WIDTH widened 23->26 bits across the live instantiation tree. This required a real SPI protocol change (spi_host_bridge.v): a 26-bit byte address no longer fits in 3 bytes -- every address field widened 3->4 bytes (WRITE_JOB 15->18 payload bytes, WRITE_MEM/READ_MEM header 5->6 bytes). Found and fixed two real timing regressions via nextpnr-ecp5 P&R (not assumed): neural_director.v's own runtime-indexed demux write (ERR-0027, was silently synthesizing an extra MULT18X18D) and nms_activation_fill_ctrl_v3.v's own linear N_SLOTS-wide max-scan (ERR-0028, became dominant at N_SLOTS=8) -- both replaced with constant-indexed/tree-based equivalents, bit-exact same behavior, confirmed via full D-Stress N=2/4/8 regression (identical cycle counts). N_SLOTS=4 now fully closes timing at 64MHz (8/8 seeds); N_SLOTS=8 significantly improved but not yet fully reliable (5/8 seeds) -- honestly disclosed, not claimed complete. Full regression re-verified: sdram_controller (461/461, 18 configs), tb_sdram_boundary (21/21), D-Stress N=2/4/8 (bit-exact), spi_host_bridge (18/18), board-level SPI smoke test (11/11), unified backend (40/40). See hardware/v2/docs/MEMORY_UPGRADE_64MB_N8.md for the full investigation, and errors.log/decisions.log (ERR-0027, ERR-0028, DEC-0039) for the complete root-cause writeups. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
228 lines
9.6 KiB
Verilog
228 lines
9.6 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// Isolated unit regression for spi_host_bridge.v (STEP20).
|
|
//
|
|
// Emulates: (1) dependency_manager.v's reg_ready contract (a level,
|
|
// asserted only when the target node is free -- here deliberately
|
|
// delayed for a few cycles on the first job to prove reg_valid is
|
|
// HELD, not pulsed blind); (2) the host-arb slot_mem_arbiter's
|
|
// mem_ready contract (one clean req/ready handshake, SDRAM-like fixed
|
|
// latency, backed by a simple associative model array standing in for
|
|
// real SDRAM content).
|
|
//
|
|
// Per spi_host_bridge.v's own documented protocol: CS must stay
|
|
// asserted (low) for the WHOLE WRITE_MEM/READ_MEM transaction,
|
|
// including the internal wait for mem_ready -- SCLK may be idled
|
|
// (held low, no toggling) during that wait without losing state. This
|
|
// testbench's SPI master BFM does exactly that.
|
|
// ================================================================
|
|
|
|
module tb_spi_host_bridge;
|
|
|
|
localparam ADDR_WIDTH = 26; // AS4C32M16SA memory upgrade: 25-bit word address + 1 byte-select bit
|
|
localparam N_NODES = 16;
|
|
localparam MAX_DEPS = 4;
|
|
localparam NODEW = $clog2(N_NODES);
|
|
localparam REQW = $clog2(MAX_DEPS+1);
|
|
|
|
reg clk = 0, rst = 1;
|
|
always #5 clk = ~clk; // 100MHz sim clock (arbitrary, faster than SPI)
|
|
|
|
reg sclk = 0, mosi = 0, cs_n = 1;
|
|
wire miso;
|
|
|
|
reg reg_ready_model = 0;
|
|
wire reg_valid;
|
|
wire [NODEW-1:0] reg_node_id;
|
|
wire [REQW-1:0] reg_required;
|
|
wire [MAX_DEPS*NODEW-1:0] reg_producer_ids;
|
|
wire [ADDR_WIDTH-1:0] reg_x_base, reg_w_base, reg_result_addr;
|
|
wire [15:0] reg_n_tiles;
|
|
|
|
wire mem_req, mem_wr, mem_lb_n, mem_ub_n;
|
|
wire [ADDR_WIDTH-1:0] mem_addr;
|
|
wire [15:0] mem_wdata;
|
|
reg [15:0] mem_rdata_model;
|
|
reg mem_ready_model = 0;
|
|
|
|
wire soft_rst_pulse;
|
|
|
|
spi_host_bridge #(
|
|
.ADDR_WIDTH(ADDR_WIDTH), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
|
|
.reg_valid(reg_valid), .reg_ready(reg_ready_model),
|
|
.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),
|
|
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr),
|
|
.mem_wdata(mem_wdata), .mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n),
|
|
.mem_rdata(mem_rdata_model), .mem_ready(mem_ready_model),
|
|
.soft_rst_pulse(soft_rst_pulse)
|
|
);
|
|
|
|
// ---- simple backing memory model: fixed 6-cycle mem_ready latency ----
|
|
reg [15:0] mem_model [0:1023];
|
|
integer mem_latency_cnt;
|
|
reg mem_pending;
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
mem_ready_model <= 1'b0; mem_pending <= 1'b0; mem_latency_cnt <= 0;
|
|
end else begin
|
|
mem_ready_model <= 1'b0;
|
|
if (mem_req && !mem_pending) begin
|
|
mem_pending <= 1'b1;
|
|
mem_latency_cnt <= 6;
|
|
end else if (mem_pending) begin
|
|
if (mem_latency_cnt == 0) begin
|
|
mem_pending <= 1'b0;
|
|
mem_ready_model <= 1'b1;
|
|
if (mem_wr) mem_model[mem_addr[9:0]] <= mem_wdata;
|
|
else mem_rdata_model <= mem_model[mem_addr[9:0]];
|
|
end else begin
|
|
mem_latency_cnt <= mem_latency_cnt - 1;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
// ---- SPI master BFM: mode 0, MSB-first ----
|
|
// Bit period = 500ns (2MHz SPI clock) against a 100MHz sim `clk`:
|
|
// a 50x margin over the ~4-clk-cycle CDC synchronizer latency,
|
|
// representative of a REAL deployment (system clock 64-80MHz vs a
|
|
// practical SPI clock in the low single-digit MHz -- see this
|
|
// module's own header for the documented minimum ratio). A torture
|
|
// rate close to the CDC latency (as an earlier draft of this
|
|
// testbench used) is not a realistic operating point and is not
|
|
// what this module is specified against.
|
|
task spi_byte(input [7:0] tx, output [7:0] rx);
|
|
integer i;
|
|
begin
|
|
rx = 8'h00;
|
|
for (i = 7; i >= 0; i = i - 1) begin
|
|
mosi = tx[i];
|
|
#200; sclk = 1; #50; rx = {rx[6:0], miso}; #50; sclk = 0; #200;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
integer errors = 0, tests = 0;
|
|
task check(input cond, input [255:0] name);
|
|
begin
|
|
tests = tests + 1;
|
|
if (!cond) begin errors = errors + 1; $display("FAIL: %0s", name); end
|
|
else $display("PASS: %0s", name);
|
|
end
|
|
endtask
|
|
|
|
reg [7:0] rxb;
|
|
reg [ADDR_WIDTH-1:0] exp_addr;
|
|
|
|
initial begin
|
|
rst = 1; cs_n = 1; sclk = 0; mosi = 0;
|
|
repeat (10) @(posedge clk);
|
|
rst = 0;
|
|
repeat (5) @(posedge clk);
|
|
|
|
// ================= Test A: WRITE_JOB, delayed reg_ready =====
|
|
reg_ready_model = 0;
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h10, rxb); // opcode WRITE_JOB
|
|
spi_byte(8'h05, rxb); // node_id=5
|
|
spi_byte(8'h02, rxb); // required=2
|
|
spi_byte(8'hAB, rxb); // producer_ids[15:8]
|
|
spi_byte(8'hCD, rxb); // producer_ids[7:0]
|
|
spi_byte(8'h00, rxb); // x_base[25:24]
|
|
spi_byte(8'h00, rxb); // x_base[23:16]
|
|
spi_byte(8'h10, rxb); // x_base[15:8]
|
|
spi_byte(8'h00, rxb); // x_base[7:0] -> x_base=0x001000
|
|
spi_byte(8'h00, rxb); // w_base[25:24]
|
|
spi_byte(8'h00, rxb); // w_base[23:16]
|
|
spi_byte(8'h20, rxb); // w_base[15:8]
|
|
spi_byte(8'h00, rxb); // w_base[7:0] -> w_base=0x002000
|
|
spi_byte(8'h00, rxb); // n_tiles[15:8]
|
|
spi_byte(8'h04, rxb); // n_tiles[7:0] -> n_tiles=4
|
|
spi_byte(8'h00, rxb); // result_addr[25:24]
|
|
spi_byte(8'h00, rxb); // result_addr[23:16]
|
|
spi_byte(8'h30, rxb); // result_addr[15:8]
|
|
spi_byte(8'h00, rxb); // result_addr[7:0] -> result_addr=0x003000
|
|
|
|
// reg_valid must now be held (reg_ready still 0). Allow for the
|
|
// CDC synchronizer latency on the LAST bit before sampling.
|
|
repeat (8) @(posedge clk);
|
|
check(reg_valid == 1'b1, "A: reg_valid asserted after 15th payload byte");
|
|
check(reg_node_id == 5, "A: reg_node_id");
|
|
check(reg_required == 2, "A: reg_required");
|
|
check(reg_producer_ids == 16'hABCD, "A: reg_producer_ids");
|
|
check(reg_x_base == 26'h001000, "A: reg_x_base");
|
|
check(reg_w_base == 26'h002000, "A: reg_w_base");
|
|
check(reg_n_tiles == 16'h0004, "A: reg_n_tiles");
|
|
check(reg_result_addr == 26'h003000, "A: reg_result_addr");
|
|
|
|
repeat (3) begin
|
|
@(posedge clk);
|
|
check(reg_valid == 1'b1, "A: reg_valid still held while reg_ready=0");
|
|
end
|
|
reg_ready_model = 1;
|
|
@(posedge clk);
|
|
#1;
|
|
check(reg_valid == 1'b0, "A: reg_valid drops the cycle after reg_ready seen");
|
|
reg_ready_model = 0;
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test B: STATUS after accepted job ========
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h20, rxb); // opcode STATUS
|
|
spi_byte(8'h00, rxb); // clocks out status byte
|
|
$monitoroff;
|
|
check(rxb[2] == 1'b1, "B: STATUS last_job_accepted=1");
|
|
check(rxb[0] == 1'b0, "B: STATUS job_busy=0 (already accepted)");
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test C: WRITE_MEM, single word ===========
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h01, rxb); // opcode WRITE_MEM
|
|
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h55, rxb); // addr=0x000055
|
|
spi_byte(8'h00, rxb); spi_byte(8'h01, rxb); // len_words=1
|
|
spi_byte(8'h12, rxb); spi_byte(8'h34, rxb); // data=0x1234
|
|
// hold CS low, idle SCLK, while the memory model latency elapses
|
|
#200;
|
|
cs_n = 1; #40;
|
|
check(mem_model[16'h0055] == 16'h1234, "C: WRITE_MEM wrote 0x1234 @ 0x000055");
|
|
|
|
// ================= Test D: READ_MEM, single word =============
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h02, rxb); // opcode READ_MEM
|
|
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h55, rxb); // addr=0x000055
|
|
spi_byte(8'h00, rxb); spi_byte(8'h01, rxb); // len_words=1
|
|
#200; // idle SCLK while the read latency elapses
|
|
spi_byte(8'h00, rxb); exp_addr = rxb; // MSB
|
|
check(rxb == 8'h12, "D: READ_MEM MSB byte == 0x12");
|
|
spi_byte(8'h00, rxb);
|
|
check(rxb == 8'h34, "D: READ_MEM LSB byte == 0x34");
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test E: RESET opcode ======================
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h0F, rxb); // opcode RESET
|
|
cs_n = 1;
|
|
begin : wait_soft_rst
|
|
integer wi; reg seen;
|
|
seen = 1'b0;
|
|
for (wi = 0; wi < 10; wi = wi + 1) begin
|
|
@(posedge clk);
|
|
if (soft_rst_pulse) seen = 1'b1;
|
|
end
|
|
check(seen, "E: soft_rst_pulse asserted after CS rises (within CDC latency)");
|
|
end
|
|
|
|
$display("=== tb_spi_host_bridge: %0d/%0d PASS ===", tests-errors, tests);
|
|
if (errors != 0) $display("*** %0d FAILURES ***", errors);
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|