Files
FPGA-Neural/hardware/v2/sim/tb_spi_host_bridge.v
micheleandClaude Sonnet 5 8d83d97bde feat: SDRAM 8MB->64MB upgrade (AS4C32M16SB-7BIN) + N_SLOTS=8 support
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
2026-09-07 00:20:53 +02:00

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