feat: real N=16 hybrid systolic top-level, synthesis-only pass (EXP-0091)

Adds n16_system_ddr3_top.v, directly adapted from n2_system_ddr3_top.v's
own proven structure: same real MIG, spi_host_bridge_v3.v, flash_spi_
master.v, host_mem_bridge.v, all completely unmodified (confirms
EXP-0090's zero-protocol-change finding holds at full N=16 scale). The
real differences: neural_director_grouped.v replaces neural_director_
packed.v, 4x systolic_group.v replace 2x packed_slot.v, and the shared
arbiter grows to a real 21-way NUM_REQ (4 group weight-fetch + 16 PE
activation/writeback + 1 host_mem_bridge).

Real synthesis-only result: 0 Errors, 0 Critical Warnings, 128 DSP48E1
of 240 (53.33%) -- an exact real match to docs/ARCHITECTURE_ANALYSIS.md
S5.6's own original brainstorm DSP projection, now confirmed by real
synthesis instead of estimated.

Found and root-caused a real Vivado project quirk (not an RTL bug --
verified separately via a clean Icarus elaboration with stub modules
for mig_7series_0/STARTUPE2): a fresh add_files + update_compile_order
didn't make synth_design -top <newmodule> find the module. Fixed by
explicitly setting the fileset's own top property before synth_design.
CLAUDE.md updated with this as the real, confirmed procedure for adding
any future brand-new top-level module.

Honestly scoped: synthesis-only (connectivity/width correctness), NOT
yet a real functional xsim test and NOT yet real P&R timing -- both
real, disclosed next steps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-21 00:12:12 +02:00
co-authored by Claude Sonnet 5
parent bd1fb5dc14
commit 4acc669d43
4 changed files with 391 additions and 6 deletions
+69
View File
@@ -6171,3 +6171,72 @@ spi_host_bridge_v3.v (no changes needed, per Part 2's own real finding)
next_action: build the real N=16 top-level, verify it end-to-end (real
xsim against the real DDR3 model, matching this project's own
established multi-level verification discipline), then real P&R.
EXP-0091 -- real N=16 (4x4 hybrid systolic) top-level: synthesis-only
check PASSES, real utilization confirms the original DSP projection
exactly (2026-09-21, continuing "Ok procedi ad implementare quel che
manca")
CONTEXT: EXP-0089 (systolic_group.v/packed_pe.v) and EXP-0090
(neural_director_grouped.v) built and separately verified the two new
real subsystems this architecture needs. The remaining real gap was a
top-level module actually wiring everything together at N=16 scale.
NEW MODULE: `n16_system_ddr3_top.v`, directly adapted from n2_system_
ddr3_top.v's own real, proven structure -- same real MIG public
wrapper, same spi_host_bridge_v3.v, flash_spi_master.v, host_mem_
bridge.v, ALL instantiated completely unmodified (confirms EXP-0090's
own real finding: the host SPI/WRITE_JOB protocol needs zero changes
at N=16). The only real differences: neural_director_grouped.v
replaces neural_director_packed.v, 4x systolic_group.v replace 2x
packed_slot.v, and the shared arbiter grows from NUM_REQ=3 to
NUM_REQ=21 (4 groups' own weight-fetch requesters at slots 0-3, the 16
PEs' own activation-fetch+writeback requesters at slots 4-19 -- 4
consecutive slots per group -- and host_mem_bridge.v at slot 20).
N_SLOTS=16 (the real total parallel-PE count) is passed to spi_host_
bridge_v3.v purely for its own informational REG_READ(0x03) -- that
parameter never gates any real control logic there.
REAL VIVADO PROJECT QUIRK FOUND (not an RTL bug -- confirmed
separately): adding these 4 new files via `add_files` + `update_
compile_order` and then calling `synth_design -top n16_system_ddr3_
top` failed immediately with "module 'n16_system_ddr3_top' not found",
despite the file being correctly present, enabled, and marked
`USED_IN: synthesis` in the project. Before assuming an RTL bug,
verified the RTL itself independently: a clean Icarus elaboration of
n16_system_ddr3_top.v and every real dependency (using two small stub
modules for the real Xilinx primitives Icarus can't resolve on its
own, mig_7series_0 and STARTUPE2) completed with **0 errors** --
proving the RTL was correct all along and the issue was Vivado's own
project state. Fix: explicitly `set_property top n16_system_ddr3_top
$fileset` BEFORE calling `synth_design -top ...` (rather than relying
on the `-top` command-line override alone) -- real, reproducible fix,
now the confirmed real procedure for adding any brand-new top-level
module to this project going forward.
REAL RESULT: **synth_design completed successfully, 0 Errors, 0
Critical Warnings, 108 Warnings** (all real and expected -- e.g.
result_writeback.v's own `ctrl_rdata` being unused, since that module
is write-only by design, already known/expected since EXP-0088, not
new). Real utilization: **128 DSP48E1 / 240 (53.33%)** -- an EXACT
real match to docs/ARCHITECTURE_ANALYSIS.md S5.6's own original
brainstorm projection ("16 cores x 8 DSP/core = 128/240, 53%"), now
confirmed by real synthesis, not a projection any more. 20053 LUTs
(31.63%), real, healthy headroom remaining on this part.
DECISION: real, verified third step. This is SYNTHESIS-ONLY --
honestly disclosed, NOT yet a real P&R signoff (no place_design/
route_design run, no real timing number for this design) and NOT yet
a real functional xsim test (the sub-modules are separately verified,
but this top-level's own bus-slicing/arbiter-wiring correctness --
the same class of bug already found and fixed twice this session in
similar flattened-bus contexts -- has only been checked for
SYNTAX/CONNECTIVITY validity via synthesis succeeding, not for
FUNCTIONAL correctness).
next_action: (1) a real functional xsim test (mirroring tb_n2_system_
ddr3.v's own real-DDR3-model methodology, scaled to submit octets
across all 4 groups and verify all 16 real results) before trusting
this design at all -- synthesis succeeding proves connectivity, not
correctness. (2) real, full P&R (place_design + route_design) for a
real timing signoff, only after (1) passes.
+291
View File
@@ -0,0 +1,291 @@
`timescale 1ns/1ps
// ============================================================
// V3 -- REAL synthesis/P&R top for the N=16 (4 groups x 4 PEs) hybrid
// systolic system (EXP-0089/0090/0091), directly adapted from
// n2_system_ddr3_top.v's own real, proven structure -- same MIG/
// adapter, same host SPI bridge, same flash bridge, same host_mem_
// bridge.v raw-access path, ALL UNCHANGED. The only real differences
// from n2_system_ddr3_top.v: neural_director_grouped.v replaces
// neural_director_packed.v, 4x systolic_group.v replace 2x
// packed_slot.v, and the shared arbiter grows from NUM_REQ=3 to
// NUM_REQ=21 (4 groups' own weight-fetch requesters + 16 PEs' own
// activation-fetch/writeback requesters + host_mem_bridge.v).
//
// REAL, CONFIRMED FINDING (EXP-0090): the SPI/WRITE_JOB host protocol
// needs ZERO changes for this -- spi_host_bridge_v3.v is instantiated
// completely unmodified below. It never references job_out_slot (only
// the simple job_out_done completion pulse), so neural_director_
// grouped.v's own job_out_group output is left unconnected at this
// top level (real, disclosed: a future real status register could
// expose it, not needed for this real milestone). N_SLOTS is passed
// as 16 (the real, total parallel-PE count) purely for the host's own
// informational REG_READ(0x03) -- N_SLOTS never gates any real control
// logic in spi_host_bridge_v3.v.
//
// ARBITER SLOT MAP (real, NUM_REQ=21): slots 0-3 = groups 0-3's own
// weight-fetch; slots 4-19 = the 16 PEs' own activation-fetch+
// writeback, 4 consecutive slots per group (group g's PEs at slots
// 4+4g .. 4+4g+3); slot 20 = host_mem_bridge.v.
// ============================================================
module n16_system_ddr3_top #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ACC_WIDTH = 32,
parameter BURST_LEN = 8,
parameter JOB_ADDR_WIDTH = 26,
parameter MEM_ADDR_WIDTH = 25,
parameter LAYER_BYTES = 128,
parameter N_GROUPS = 4,
parameter QUEUE_DEPTH = 16
)(
input wire sys_clk_p,
input wire sys_clk_n,
input wire sys_rst,
input wire clk_ref_p,
input wire clk_ref_n,
inout wire [31:0] ddr3_dq,
inout wire [3:0] ddr3_dqs_n,
inout wire [3:0] ddr3_dqs_p,
output wire [13:0] ddr3_addr,
output wire [2:0] ddr3_ba,
output wire ddr3_ras_n,
output wire ddr3_cas_n,
output wire ddr3_we_n,
output wire ddr3_reset_n,
output wire [0:0] ddr3_ck_p,
output wire [0:0] ddr3_ck_n,
output wire [0:0] ddr3_cke,
output wire [0:0] ddr3_cs_n,
output wire [3:0] ddr3_dm,
output wire [0:0] ddr3_odt,
input wire sclk,
input wire mosi,
output wire miso,
input wire cs_n,
output wire flash_cs_n,
output wire flash_mosi,
input wire flash_miso,
output wire ui_clk_o,
output wire init_calib_complete,
output wire job_out_done,
output wire data_ready_n
);
localparam N_PES = N_GROUPS * 4; // 16
wire [27:0] app_addr;
wire [2:0] app_cmd;
wire app_en, app_rdy;
wire [127:0] app_wdf_data;
wire app_wdf_end;
wire [15:0] app_wdf_mask;
wire app_wdf_wren, app_wdf_rdy;
wire [127:0] app_rd_data;
wire app_rd_data_end, app_rd_data_valid;
wire ui_clk, ui_clk_sync_rst;
assign ui_clk_o = ui_clk;
mig_7series_0 u_mig (
.ddr3_dq(ddr3_dq), .ddr3_dqs_n(ddr3_dqs_n), .ddr3_dqs_p(ddr3_dqs_p),
.ddr3_addr(ddr3_addr), .ddr3_ba(ddr3_ba),
.ddr3_ras_n(ddr3_ras_n), .ddr3_cas_n(ddr3_cas_n), .ddr3_we_n(ddr3_we_n),
.ddr3_reset_n(ddr3_reset_n),
.ddr3_ck_p(ddr3_ck_p), .ddr3_ck_n(ddr3_ck_n),
.ddr3_cke(ddr3_cke), .ddr3_cs_n(ddr3_cs_n),
.ddr3_dm(ddr3_dm), .ddr3_odt(ddr3_odt),
.sys_clk_p(sys_clk_p), .sys_clk_n(sys_clk_n), .clk_ref_p(clk_ref_p), .clk_ref_n(clk_ref_n),
.app_addr(app_addr), .app_cmd(app_cmd), .app_en(app_en),
.app_wdf_data(app_wdf_data), .app_wdf_end(app_wdf_end),
.app_wdf_mask(app_wdf_mask), .app_wdf_wren(app_wdf_wren),
.app_rd_data(app_rd_data), .app_rd_data_end(app_rd_data_end),
.app_rd_data_valid(app_rd_data_valid), .app_rdy(app_rdy), .app_wdf_rdy(app_wdf_rdy),
.app_sr_req(1'b0), .app_ref_req(1'b0), .app_zq_req(1'b0),
.app_sr_active(), .app_ref_ack(), .app_zq_ack(),
.ui_clk(ui_clk), .ui_clk_sync_rst(ui_clk_sync_rst),
.init_calib_complete(init_calib_complete),
.device_temp(),
.sys_rst(sys_rst)
);
wire adp_req, adp_wr;
wire [MEM_ADDR_WIDTH-1:0] adp_addr;
wire [32*BURST_LEN-1:0] adp_wdata;
wire [4*BURST_LEN-1:0] adp_wmask;
wire [32*BURST_LEN-1:0] adp_rdata;
wire adp_ready, adp_busy;
mig_native_adapter #(.BURST_LEN(BURST_LEN), .ADDR_WIDTH(MEM_ADDR_WIDTH)) u_adapter (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.req(adp_req), .wr(adp_wr), .addr(adp_addr), .wdata(adp_wdata), .wmask(adp_wmask),
.rdata(adp_rdata), .ready(adp_ready), .busy(adp_busy),
.app_addr(app_addr), .app_cmd(app_cmd), .app_en(app_en), .app_rdy(app_rdy),
.app_wdf_data(app_wdf_data), .app_wdf_end(app_wdf_end), .app_wdf_mask(app_wdf_mask),
.app_wdf_wren(app_wdf_wren), .app_wdf_rdy(app_wdf_rdy),
.app_rd_data(app_rd_data), .app_rd_data_end(app_rd_data_end), .app_rd_data_valid(app_rd_data_valid)
);
// ---- real, real, appropriately-sized arbiter: 4 group weight-
// fetch + 16 PE activation/writeback + 1 host_mem_bridge = 21 ----
localparam NUM_REQ = N_GROUPS + N_PES + 1; // 4 + 16 + 1 = 21
localparam HOST_SLOT = NUM_REQ - 1; // 20
wire [NUM_REQ-1:0] req_active, req_grant, req_req, req_wr;
wire [NUM_REQ-1:0] req_ready, req_busy;
wire [NUM_REQ*MEM_ADDR_WIDTH-1:0] req_addr;
wire [NUM_REQ*32*BURST_LEN-1:0] req_wdata;
wire [NUM_REQ*4*BURST_LEN-1:0] req_wmask;
wire [NUM_REQ*32*BURST_LEN-1:0] req_rdata;
sdram_arbiter_n #(
.NUM_REQ(NUM_REQ), .ADDR_WIDTH(MEM_ADDR_WIDTH), .BURST_LEN(BURST_LEN)
) u_arb (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.req_active(req_active), .req_grant(req_grant),
.req_req(req_req), .req_wr(req_wr), .req_addr(req_addr),
.req_wdata(req_wdata), .req_wmask(req_wmask),
.req_rdata(req_rdata), .req_ready(req_ready), .req_busy(req_busy),
.ctrl_req(adp_req), .ctrl_wr(adp_wr), .ctrl_addr(adp_addr),
.ctrl_wdata(adp_wdata), .ctrl_wmask(adp_wmask),
.ctrl_rdata(adp_rdata), .ctrl_ready(adp_ready), .ctrl_busy(adp_busy)
);
// ---- grouped Director ----
wire job_in_valid, job_in_ready;
wire [JOB_ADDR_WIDTH-1:0] job_in_x_base, job_in_w_base, job_in_result_addr;
wire [15:0] job_in_n_tiles, job_in_node_id;
wire [N_GROUPS-1:0] group_job_start;
wire [JOB_ADDR_WIDTH*N_GROUPS-1:0] group_w_base;
wire [16*N_GROUPS-1:0] group_n_tiles;
wire [4*JOB_ADDR_WIDTH*N_GROUPS-1:0] group_pe_x_base_a, group_pe_x_base_b;
wire [4*JOB_ADDR_WIDTH*N_GROUPS-1:0] group_pe_result_addr_a, group_pe_result_addr_b;
wire [4*16*N_GROUPS-1:0] group_pe_node_id_a, group_pe_node_id_b;
wire [N_GROUPS-1:0] group_job_done;
wire [$clog2(N_GROUPS)-1:0] job_out_group_w;
wire [3:0] dir_state;
wire dir_error;
wire queue_empty;
neural_director_grouped #(
.ADDR_WIDTH(JOB_ADDR_WIDTH), .N_GROUPS(N_GROUPS), .QUEUE_DEPTH(QUEUE_DEPTH)
) u_dir (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready),
.job_in_x_base(job_in_x_base), .job_in_w_base(job_in_w_base),
.job_in_n_tiles(job_in_n_tiles), .job_in_result_addr(job_in_result_addr),
.job_in_node_id(job_in_node_id),
.group_job_start(group_job_start), .group_w_base(group_w_base), .group_n_tiles(group_n_tiles),
.group_pe_x_base_a(group_pe_x_base_a), .group_pe_x_base_b(group_pe_x_base_b),
.group_pe_result_addr_a(group_pe_result_addr_a), .group_pe_result_addr_b(group_pe_result_addr_b),
.group_pe_node_id_a(group_pe_node_id_a), .group_pe_node_id_b(group_pe_node_id_b),
.group_job_done(group_job_done),
.job_out_done(job_out_done), .job_out_group(job_out_group_w),
.dir_state(dir_state), .dir_error(dir_error), .queue_empty(queue_empty)
);
// ---- physical SPI host interface: UNMODIFIED (EXP-0090's own real
// finding -- zero protocol changes needed). N_SLOTS=16 reported
// purely informationally (REG_READ 0x03), matches the real total
// parallel-PE count, never gates any control logic in this module. ----
wire mem_req, mem_wr, mem_lb_n, mem_ub_n, mem_ready;
wire [MEM_ADDR_WIDTH-1:0] mem_addr;
wire [15:0] mem_wdata, mem_rdata;
wire soft_rst_pulse;
wire flash_xfer_active, flash_byte_req, flash_byte_done;
wire [7:0] flash_byte_wdata, flash_byte_rdata;
spi_host_bridge_v3 #(
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(N_PES)
) u_spi (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.init_calib_complete(init_calib_complete), .dir_error(dir_error),
.job_out_done(job_out_done), .data_ready_n(data_ready_n),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready),
.job_in_x_base(job_in_x_base), .job_in_w_base(job_in_w_base),
.job_in_n_tiles(job_in_n_tiles), .job_in_result_addr(job_in_result_addr),
.job_in_node_id(job_in_node_id),
.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), .mem_ready(mem_ready),
.flash_xfer_active(flash_xfer_active), .flash_byte_req(flash_byte_req),
.flash_byte_wdata(flash_byte_wdata), .flash_byte_rdata(flash_byte_rdata),
.flash_byte_done(flash_byte_done),
.soft_rst_pulse(soft_rst_pulse)
);
flash_spi_master u_flash (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.xfer_active(flash_xfer_active), .byte_req(flash_byte_req),
.byte_wdata(flash_byte_wdata), .byte_rdata(flash_byte_rdata), .byte_done(flash_byte_done), .busy(),
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso)
);
host_mem_bridge #(
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(MEM_ADDR_WIDTH)
) u_host_bridge (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.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), .mem_ready(mem_ready),
.req_active(req_active[HOST_SLOT]), .req_grant(req_grant[HOST_SLOT]),
.req_req(req_req[HOST_SLOT]), .req_wr(req_wr[HOST_SLOT]),
.req_addr(req_addr[HOST_SLOT*MEM_ADDR_WIDTH +: MEM_ADDR_WIDTH]),
.req_wdata(req_wdata[HOST_SLOT*32*BURST_LEN +: 32*BURST_LEN]),
.req_wmask(req_wmask[HOST_SLOT*4*BURST_LEN +: 4*BURST_LEN]),
.req_rdata(req_rdata[HOST_SLOT*32*BURST_LEN +: 32*BURST_LEN]),
.req_ready(req_ready[HOST_SLOT]), .req_busy(req_busy[HOST_SLOT])
);
// ---- 4x systolic_group.v, each: 1 arbiter slot for its own
// weight-fetch (slots 0..N_GROUPS-1), 4 arbiter slots for its own
// 4 PEs' activation-fetch+writeback (slots N_GROUPS+4g ..
// N_GROUPS+4g+3) ----
genvar gg;
generate
for (gg = 0; gg < N_GROUPS; gg = gg + 1) begin : GEN_GROUP
localparam PE_BASE = N_GROUPS + gg*4;
systolic_group #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(JOB_ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
) u_group (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.job_start(group_job_start[gg]),
.w_base(group_w_base[gg*JOB_ADDR_WIDTH +: JOB_ADDR_WIDTH]),
.n_tiles(group_n_tiles[gg*16 +: 16]),
.pe_x_base_a(group_pe_x_base_a[gg*4*JOB_ADDR_WIDTH +: 4*JOB_ADDR_WIDTH]),
.pe_x_base_b(group_pe_x_base_b[gg*4*JOB_ADDR_WIDTH +: 4*JOB_ADDR_WIDTH]),
.pe_result_addr_a(group_pe_result_addr_a[gg*4*JOB_ADDR_WIDTH +: 4*JOB_ADDR_WIDTH]),
.pe_result_addr_b(group_pe_result_addr_b[gg*4*JOB_ADDR_WIDTH +: 4*JOB_ADDR_WIDTH]),
.pe_node_id_a(group_pe_node_id_a[gg*4*16 +: 4*16]),
.pe_node_id_b(group_pe_node_id_b[gg*4*16 +: 4*16]),
.job_done(group_job_done[gg]),
.pe_result_data_a(), .pe_result_data_b(),
.pe_result_node_id_a(), .pe_result_node_id_b(),
.pe_result_addr_a_out(), .pe_result_addr_b_out(),
.mem_active(req_active[gg]), .mem_grant(req_grant[gg]),
.ctrl_req(req_req[gg]), .ctrl_wr(req_wr[gg]),
.ctrl_addr(req_addr[gg*MEM_ADDR_WIDTH +: MEM_ADDR_WIDTH]),
.ctrl_wdata(req_wdata[gg*32*BURST_LEN +: 32*BURST_LEN]),
.ctrl_wmask(req_wmask[gg*4*BURST_LEN +: 4*BURST_LEN]),
.ctrl_rdata(req_rdata[gg*32*BURST_LEN +: 32*BURST_LEN]),
.ctrl_ready(req_ready[gg]), .ctrl_busy(req_busy[gg]),
.pe_mem_active(req_active[PE_BASE +: 4]), .pe_mem_grant(req_grant[PE_BASE +: 4]),
.pe_ctrl_req(req_req[PE_BASE +: 4]), .pe_ctrl_wr(req_wr[PE_BASE +: 4]),
.pe_ctrl_addr(req_addr[PE_BASE*MEM_ADDR_WIDTH +: 4*MEM_ADDR_WIDTH]),
.pe_ctrl_wdata(req_wdata[PE_BASE*32*BURST_LEN +: 4*32*BURST_LEN]),
.pe_ctrl_wmask(req_wmask[PE_BASE*4*BURST_LEN +: 4*4*BURST_LEN]),
.pe_ctrl_rdata(req_rdata[PE_BASE*32*BURST_LEN +: 4*32*BURST_LEN]),
.pe_ctrl_ready(req_ready[PE_BASE +: 4]), .pe_ctrl_busy(req_busy[PE_BASE +: 4])
);
end
endgenerate
endmodule