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
This commit is contained in:
2026-09-07 00:20:53 +02:00
co-authored by Claude Sonnet 5
parent 9b5d1055b8
commit 8d83d97bde
21 changed files with 558 additions and 295 deletions
+1 -1
View File
@@ -43,7 +43,7 @@
module dependency_manager #(
parameter N_NODES = 16,
parameter MAX_DEPS = 4,
parameter ADDR_WIDTH = 23
parameter ADDR_WIDTH = 26
)(
input wire clk,
input wire rst,
+75 -22
View File
@@ -30,7 +30,7 @@
// ================================================================
module neural_director #(
parameter ADDR_WIDTH = 23,
parameter ADDR_WIDTH = 26,
parameter N_SLOTS = 4,
parameter QUEUE_DEPTH = 8
)(
@@ -48,11 +48,16 @@ module neural_director #(
input wire [15:0] job_in_node_id,
// ---- per-slot memory_manager job control (arrayed, §9) ----
output reg [N_SLOTS-1:0] slot_job_start,
output reg [ADDR_WIDTH*N_SLOTS-1:0] slot_x_base,
output reg [ADDR_WIDTH*N_SLOTS-1:0] slot_w_base,
output reg [16*N_SLOTS-1:0] slot_n_tiles,
output reg [ADDR_WIDTH*N_SLOTS-1:0] slot_result_addr,
// Ports are `wire`, driven by the GEN_SLOT_OUT generate block below
// from internal unpacked-array registers (see that block's own
// comment for why -- a real, measured timing regression found this
// session when ADDR_WIDTH grew from 23 to 26 bits, section
// "post-PRE-PCB-FREEZE memory upgrade").
output wire [N_SLOTS-1:0] slot_job_start,
output wire [ADDR_WIDTH*N_SLOTS-1:0] slot_x_base,
output wire [ADDR_WIDTH*N_SLOTS-1:0] slot_w_base,
output wire [16*N_SLOTS-1:0] slot_n_tiles,
output wire [ADDR_WIDTH*N_SLOTS-1:0] slot_result_addr,
// slot_node_id: which node_id is currently occupying each slot --
// not needed by memory_manager itself (it has no notion of node
// ids), but needed by a caller (dataflow_core.v, M7) that must
@@ -60,7 +65,7 @@ module neural_director #(
// to notify the Dependency Manager (M6). Purely additive: existing
// callers (hardware/v2/sim/tb_neural_director.v, M5) that don't
// connect it are unaffected.
output reg [16*N_SLOTS-1:0] slot_node_id,
output wire [16*N_SLOTS-1:0] slot_node_id,
input wire [N_SLOTS-1:0] slot_job_done,
// ---- completion notification (§9 "rilevamento dei completamenti") ----
@@ -121,6 +126,45 @@ module neural_director #(
end
end
// ---- per-slot output storage (unpacked arrays, one real register
// set per slot) + constant-indexed generate wiring out to the
// packed ports above. Found and fixed this session (post-PRE-PCB-
// FREEZE memory upgrade, ADDR_WIDTH 23->26): the PREVIOUS design
// used one wide packed `output reg` per field and wrote it with a
// RUNTIME-computed part-select (`slot_x_base[free_slot_idx*
// ADDR_WIDTH +: ADDR_WIDTH] <= ...`). A variable-indexed write into
// a wide packed register is not free logic -- Yosys/synth_ecp5
// synthesized the index computation (`free_slot_idx*ADDR_WIDTH`)
// as an actual MULT18X18D hard multiplier feeding a wide demux/
// crossbar into the destination slot, and this got measurably
// worse as ADDR_WIDTH grew (real nextpnr-ecp5 P&R: worst-seed Fmax
// collapsed from 68.51MHz at ADDR_WIDTH=23 to ~40-47MHz at
// ADDR_WIDTH=26, confirmed across 8 seeds, all failing the 64MHz
// target). The fix below replaces the runtime-indexed demux write
// with N_SLOTS parallel CONSTANT-indexed comparisons (`fi ==
// free_slot_idx`, each a cheap few-bit compare, no multiply) each
// gating its own slot's own narrow register -- functionally
// IDENTICAL behavior, bit-exact same external port semantics, only
// the internal implementation changed.
reg slot_job_start_r [0:N_SLOTS-1];
reg [ADDR_WIDTH-1:0] slot_x_base_r [0:N_SLOTS-1];
reg [ADDR_WIDTH-1:0] slot_w_base_r [0:N_SLOTS-1];
reg [15:0] slot_n_tiles_r [0:N_SLOTS-1];
reg [ADDR_WIDTH-1:0] slot_result_addr_r [0:N_SLOTS-1];
reg [15:0] slot_node_id_r [0:N_SLOTS-1];
genvar gs;
generate
for (gs = 0; gs < N_SLOTS; gs = gs + 1) begin : GEN_SLOT_OUT
assign slot_job_start[gs] = slot_job_start_r[gs];
assign slot_x_base[gs*ADDR_WIDTH +: ADDR_WIDTH] = slot_x_base_r[gs];
assign slot_w_base[gs*ADDR_WIDTH +: ADDR_WIDTH] = slot_w_base_r[gs];
assign slot_n_tiles[gs*16 +: 16] = slot_n_tiles_r[gs];
assign slot_result_addr[gs*ADDR_WIDTH +: ADDR_WIDTH] = slot_result_addr_r[gs];
assign slot_node_id[gs*16 +: 16] = slot_node_id_r[gs];
end
endgenerate
// Priority-encoded lowest-indexed slot reporting job_done this
// cycle (combinational, so it reflects THIS cycle's slot_job_done
// bus directly -- a register-based "already reported one" flag
@@ -143,16 +187,18 @@ module neural_director #(
q_tail <= {Q_ADDR_WIDTH{1'b0}};
q_count <= {(Q_ADDR_WIDTH+1){1'b0}};
slot_busy <= {N_SLOTS{1'b0}};
slot_job_start <= {N_SLOTS{1'b0}};
slot_x_base <= {(ADDR_WIDTH*N_SLOTS){1'b0}};
slot_w_base <= {(ADDR_WIDTH*N_SLOTS){1'b0}};
slot_n_tiles <= {(16*N_SLOTS){1'b0}};
slot_result_addr <= {(ADDR_WIDTH*N_SLOTS){1'b0}};
slot_node_id <= {(16*N_SLOTS){1'b0}};
for (fi = 0; fi < N_SLOTS; fi = fi + 1) begin
slot_job_start_r[fi] <= 1'b0;
slot_x_base_r[fi] <= {ADDR_WIDTH{1'b0}};
slot_w_base_r[fi] <= {ADDR_WIDTH{1'b0}};
slot_n_tiles_r[fi] <= 16'b0;
slot_result_addr_r[fi] <= {ADDR_WIDTH{1'b0}};
slot_node_id_r[fi] <= 16'b0;
end
job_out_done <= 1'b0;
job_out_slot <= '0;
end else begin
slot_job_start <= {N_SLOTS{1'b0}};
for (fi = 0; fi < N_SLOTS; fi = fi + 1) slot_job_start_r[fi] <= 1'b0;
job_out_done <= 1'b0;
// ---- Accept a new job into the ready queue (independent
@@ -201,14 +247,21 @@ module neural_director #(
end
DIR_ALLOCATE: begin
// Dispatch the head of the queue to the first
// free slot found this cycle.
slot_job_start[free_slot_idx] <= 1'b1;
slot_x_base[free_slot_idx*ADDR_WIDTH +: ADDR_WIDTH] <= q_x_base[q_head];
slot_w_base[free_slot_idx*ADDR_WIDTH +: ADDR_WIDTH] <= q_w_base[q_head];
slot_n_tiles[free_slot_idx*16 +: 16] <= q_n_tiles[q_head];
slot_result_addr[free_slot_idx*ADDR_WIDTH +: ADDR_WIDTH] <= q_result_addr[q_head];
slot_node_id[free_slot_idx*16 +: 16] <= q_node_id[q_head];
// Dispatch the head of the queue to the first free
// slot found this cycle -- N_SLOTS parallel
// constant-indexed compares (cheap) instead of one
// runtime-indexed wide demux write (see
// slot_x_base_r's own declaration comment for why).
for (fi = 0; fi < N_SLOTS; fi = fi + 1) begin
if (fi[$clog2(N_SLOTS)-1:0] == free_slot_idx) begin
slot_job_start_r[fi] <= 1'b1;
slot_x_base_r[fi] <= q_x_base[q_head];
slot_w_base_r[fi] <= q_w_base[q_head];
slot_n_tiles_r[fi] <= q_n_tiles[q_head];
slot_result_addr_r[fi] <= q_result_addr[q_head];
slot_node_id_r[fi] <= q_node_id[q_head];
end
end
slot_busy[free_slot_idx] <= 1'b1;
q_head <= (q_head == QUEUE_DEPTH[Q_ADDR_WIDTH-1:0]-1'b1) ? {Q_ADDR_WIDTH{1'b0}} : q_head + 1'b1;
dir_state <= DIR_SCAN_READY;
+1 -1
View File
@@ -52,7 +52,7 @@
// ================================================================
module slot_mem_arbiter #(
parameter ADDR_WIDTH = 23,
parameter ADDR_WIDTH = 26,
parameter N_PORTS = 4
)(
input wire clk,
+1 -1
View File
@@ -53,7 +53,7 @@
module slot_mem_arbiter_wide #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 23,
parameter ADDR_WIDTH = 26,
parameter N_PORTS = 4
)(
input wire clk,
+68 -51
View File
@@ -26,15 +26,17 @@
// 0x00 NOP -- 0 payload bytes.
// 0x0F RESET -- 0 payload bytes. Pulses soft_rst_pulse for
// one clk cycle after CS rises.
// 0x10 WRITE_JOB -- 15 payload bytes, registers one dependency-
// manager job (== one reg_valid/reg_* handshake):
// 0x10 WRITE_JOB -- 18 payload bytes (widened from 15 -- see
// "ADDRESS WIDTH" note below), registers one
// dependency-manager job (== one reg_valid/
// reg_* handshake):
// byte0 = {4'b0,node_id[3:0]}
// byte1 = {5'b0,required[2:0]}
// byte2:3 = producer_ids[15:0]
// byte4:6 = x_base[22:0] (byte4 msb={1'b0,x_base[22:16]})
// byte7:9 = w_base[22:0]
// byte10:11= n_tiles[15:0]
// byte12:14= result_addr[22:0]
// byte4:7 = x_base[25:0] (byte4 msb={6'b0,x_base[25:24]})
// byte8:11 = w_base[25:0]
// byte12:13= n_tiles[15:0]
// byte14:17= result_addr[25:0]
// reg_valid is asserted and HELD until the
// cycle reg_ready also reads 1 (same-cycle
// valid&&ready acceptance, matching
@@ -46,31 +48,42 @@
// bit1 = mem_busy (WRITE_MEM/READ_MEM waiting on mem_ready)
// bit2 = last_job_accepted (sticky, cleared by next WRITE_JOB)
// bits[7:3] = 0 (reserved)
// 0x01 WRITE_MEM -- 5 header bytes + 2*len_words payload bytes:
// byte0:2 = addr[22:0] (WORD address, matches
// 0x01 WRITE_MEM -- 6 header bytes (widened from 5) + 2*len_words
// payload bytes:
// byte0:3 = addr[25:0] (WORD address, matches
// sdram_unified_backend's AR port
// convention -- NOT a byte address)
// byte3:4 = len_words[15:0] (number of 16-bit
// convention -- NOT a byte address;
// byte0 msb={6'b0,addr[25:24]})
// byte4:5 = len_words[15:0] (number of 16-bit
// words to write, len_words>=1)
// then len_words * 2 bytes of data, MSB-first
// per word; each word is written via one
// mem_req/mem_ready handshake (lb_n=ub_n=0,
// full 16-bit write) before the next word's
// bytes are accepted.
// 0x02 READ_MEM -- 5 header bytes (addr + len_words, same shape
// 0x02 READ_MEM -- 6 header bytes (addr + len_words, same shape
// as WRITE_MEM), 0 further MOSI payload; the
// 2*len_words response bytes are clocked out
// on MISO starting at payload byte 6, MSB-
// on MISO starting at payload byte 7, MSB-
// first per word, one mem_req/mem_ready
// read per word.
//
// ADDRESS WIDTH (post-PRE-PCB-FREEZE memory upgrade): ADDR_WIDTH grew
// from 23 to 26 bits (SDRAM capacity upgrade, AS4C4M16SA-6TIN 8MB ->
// AS4C32M16SA-7TIN 64MB -- see sdram_controller.v's own header). A
// 26-bit address no longer fits in 3 bytes (24 bits) with a spare
// reserved bit the way the old 23-bit address did -- every address
// field below therefore widened from 3 to 4 bytes (6 reserved bits in
// the new top byte instead of 1), growing WRITE_JOB from 15 to 18
// payload bytes and the WRITE_MEM/READ_MEM header from 5 to 6 bytes.
//
// Any opcode byte not listed above is treated as NOP (0 payload,
// MISO drives 0x00) -- matches spi_engine.v's own "unknown opcode is
// inert, never wedges the bus" precedent.
// ================================================================
module spi_host_bridge #(
parameter ADDR_WIDTH = 23,
parameter ADDR_WIDTH = 26,
parameter N_NODES = 16,
parameter MAX_DEPS = 4
)(
@@ -226,9 +239,9 @@ module spi_host_bridge #(
localparam OP_STATUS = 8'h20;
localparam ST_OPCODE = 4'd0;
localparam ST_JOB = 4'd1; // collecting 15 WRITE_JOB payload bytes
localparam ST_JOB = 4'd1; // collecting 18 WRITE_JOB payload bytes
localparam ST_JOB_WAIT= 4'd2; // reg_valid held, waiting reg_ready
localparam ST_MEM_ADDR= 4'd3; // collecting 3 addr bytes
localparam ST_MEM_ADDR= 4'd3; // collecting 4 addr bytes
localparam ST_MEM_LEN = 4'd4; // collecting 2 length bytes
localparam ST_MEM_WD = 4'd5; // WRITE_MEM: collecting 2 data bytes/word
localparam ST_MEM_WISS= 4'd6; // WRITE_MEM: issue+wait mem_req
@@ -238,7 +251,7 @@ module spi_host_bridge #(
reg [3:0] state;
reg [7:0] opcode;
reg [3:0] byte_idx; // generic byte counter within a field
reg [4:0] byte_idx; // generic byte counter within a field (up to 17, WRITE_JOB)
reg [15:0] len_words;
reg [15:0] word_cnt;
reg [15:0] cur_word; // WRITE_MEM: assembling MSB,LSB; READ_MEM: holding readback
@@ -252,13 +265,13 @@ module spi_host_bridge #(
if (opcode == OP_STATUS)
tx_mux = {5'b0, last_job_accepted_r, mem_busy_r, job_busy_r};
else if (opcode == OP_READ_MEM && state == ST_MEM_ROUT)
tx_mux = (byte_idx == 4'd0) ? cur_word[15:8] : cur_word[7:0];
tx_mux = (byte_idx == 5'd0) ? cur_word[15:8] : cur_word[7:0];
end
assign tx_byte = tx_mux;
always @(posedge clk) begin
if (rst) begin
state <= ST_OPCODE; opcode <= 8'h00; byte_idx <= 4'd0;
state <= ST_OPCODE; opcode <= 8'h00; byte_idx <= 5'd0;
len_words <= 16'd0; word_cnt <= 16'd0; cur_word <= 16'd0;
reg_valid <= 1'b0; reg_node_id <= {NODEW{1'b0}}; reg_required <= {REQW{1'b0}};
reg_producer_ids <= {(MAX_DEPS*NODEW){1'b0}};
@@ -292,12 +305,12 @@ module spi_host_bridge #(
// to cs_rose below.
if (cs_fell && state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS) begin
state <= ST_OPCODE;
byte_idx <= 4'd0;
byte_idx <= 5'd0;
end else if (!cs_fell && rx_valid) begin
case (state)
ST_OPCODE: begin
opcode <= rx_byte;
byte_idx <= 4'd0;
byte_idx <= 5'd0;
case (rx_byte)
OP_WRITE_JOB: state <= ST_JOB;
OP_WRITE_MEM: state <= ST_MEM_ADDR;
@@ -309,59 +322,63 @@ module spi_host_bridge #(
ST_JOB: begin
case (byte_idx)
4'd0: reg_node_id <= rx_byte[NODEW-1:0];
4'd1: reg_required <= rx_byte[REQW-1:0];
4'd2: reg_producer_ids[15:8] <= rx_byte;
4'd3: reg_producer_ids[7:0] <= rx_byte;
4'd4: reg_x_base[22:16] <= rx_byte[6:0];
4'd5: reg_x_base[15:8] <= rx_byte;
4'd6: reg_x_base[7:0] <= rx_byte;
4'd7: reg_w_base[22:16] <= rx_byte[6:0];
4'd8: reg_w_base[15:8] <= rx_byte;
4'd9: reg_w_base[7:0] <= rx_byte;
4'd10: reg_n_tiles[15:8] <= rx_byte;
4'd11: reg_n_tiles[7:0] <= rx_byte;
4'd12: reg_result_addr[22:16] <= rx_byte[6:0];
4'd13: reg_result_addr[15:8] <= rx_byte;
4'd14: begin
5'd0: reg_node_id <= rx_byte[NODEW-1:0];
5'd1: reg_required <= rx_byte[REQW-1:0];
5'd2: reg_producer_ids[15:8] <= rx_byte;
5'd3: reg_producer_ids[7:0] <= rx_byte;
5'd4: reg_x_base[25:24] <= rx_byte[1:0];
5'd5: reg_x_base[23:16] <= rx_byte;
5'd6: reg_x_base[15:8] <= rx_byte;
5'd7: reg_x_base[7:0] <= rx_byte;
5'd8: reg_w_base[25:24] <= rx_byte[1:0];
5'd9: reg_w_base[23:16] <= rx_byte;
5'd10: reg_w_base[15:8] <= rx_byte;
5'd11: reg_w_base[7:0] <= rx_byte;
5'd12: reg_n_tiles[15:8] <= rx_byte;
5'd13: reg_n_tiles[7:0] <= rx_byte;
5'd14: reg_result_addr[25:24] <= rx_byte[1:0];
5'd15: reg_result_addr[23:16] <= rx_byte;
5'd16: reg_result_addr[15:8] <= rx_byte;
5'd17: begin
reg_result_addr[7:0] <= rx_byte;
reg_valid <= 1'b1;
last_job_accepted_r <= 1'b0;
state <= ST_JOB_WAIT;
end
endcase
if (byte_idx != 4'd14) byte_idx <= byte_idx + 4'd1;
if (byte_idx != 5'd17) byte_idx <= byte_idx + 5'd1;
end
ST_MEM_ADDR: begin
case (byte_idx)
4'd0: mem_addr[22:16] <= rx_byte[6:0];
4'd1: mem_addr[15:8] <= rx_byte;
4'd2: begin
5'd0: mem_addr[25:24] <= rx_byte[1:0];
5'd1: mem_addr[23:16] <= rx_byte;
5'd2: mem_addr[15:8] <= rx_byte;
5'd3: begin
mem_addr[7:0] <= rx_byte;
state <= ST_MEM_LEN;
end
endcase
if (byte_idx != 4'd2) byte_idx <= byte_idx + 4'd1;
else byte_idx <= 4'd0;
if (byte_idx != 5'd3) byte_idx <= byte_idx + 5'd1;
else byte_idx <= 5'd0;
end
ST_MEM_LEN: begin
if (byte_idx == 4'd0) begin
if (byte_idx == 5'd0) begin
len_words[15:8] <= rx_byte;
byte_idx <= 4'd1;
byte_idx <= 5'd1;
end else begin
len_words[7:0] <= rx_byte;
word_cnt <= {len_words[15:8], rx_byte};
byte_idx <= 4'd0;
byte_idx <= 5'd0;
state <= (opcode == OP_WRITE_MEM) ? ST_MEM_WD : ST_MEM_RISS;
end
end
ST_MEM_WD: begin
if (byte_idx == 4'd0) begin
if (byte_idx == 5'd0) begin
cur_word[15:8] <= rx_byte;
byte_idx <= 4'd1;
byte_idx <= 5'd1;
end else begin
cur_word[7:0] <= rx_byte;
state <= ST_MEM_WISS;
@@ -390,7 +407,7 @@ module spi_host_bridge #(
mem_busy_r <= 1'b0;
mem_addr <= mem_addr + 1'b1;
word_cnt <= word_cnt - 1'b1;
byte_idx <= 4'd0;
byte_idx <= 5'd0;
state <= (word_cnt == 16'd1) ? ST_IGNORE : ST_MEM_WD;
end
@@ -403,7 +420,7 @@ module spi_host_bridge #(
end else if (state == ST_MEM_RISS && mem_busy_r && mem_ready) begin
mem_busy_r <= 1'b0;
cur_word <= mem_rdata;
byte_idx <= 4'd0;
byte_idx <= 5'd0;
state <= ST_MEM_ROUT;
end
if (state == ST_MEM_ROUT && rx_valid) begin
@@ -412,12 +429,12 @@ module spi_host_bridge #(
// it is also the correct "advance" event for MISO-side
// bookkeeping (mirrors spi_slave's own documented
// rx_valid-drives-advancement convention).
if (byte_idx == 4'd0) begin
byte_idx <= 4'd1;
if (byte_idx == 5'd0) begin
byte_idx <= 5'd1;
end else begin
mem_addr <= mem_addr + 1'b1;
word_cnt <= word_cnt - 1'b1;
byte_idx <= 4'd0;
byte_idx <= 5'd0;
state <= (word_cnt == 16'd1) ? ST_IGNORE : ST_MEM_RISS;
end
end