feat: integrate flash #1 (neural-network data) RTL into V2 top-level

Closes the flash #1 RTL gap flagged in DEC-0041: real, unmodified V1
subsystem (flash_slot_manager.v/flash_copy_engine.v/spi_flash_master.v/
crc32.v) now instantiated in fpga_neural_v2_top.v, bridged to the AR
memory bus via a new flash_mem_adapter.v (byte<->word, matches
nms_memory_manager_stream_wide.v's own real masking convention), and
commandable over SPI via a new spi_host_bridge.v opcode (OP_FLASH_CMD,
0x30) using the same byte-counting idiom as OP_WRITE_JOB. Real balls
now in the LPF: flash_sclk=B2, flash_mosi=E2, flash_miso=F2,
flash_cs_n=F3.

New tb_flash_integration_smoke.v: real SPI-triggered OP_FLASH_READ_BLOCK
verified bit-exact (64/64 bytes) against a real V1 flash_model.v
instance, through the new adapter and the widened (2->3 port) host-arb
arbiter; WRITE_JOB regression confirms the new 3rd port doesn't disturb
existing traffic. Full existing regression re-run clean: D-Stress N=4/
N=8 (bit-exact + data_ready PASS), board-level smoke test (11/11),
isolated spi_host_bridge test (18/18).

Honest, disclosed finding: a full 8-seed P&R re-verification shows
N_SLOTS=4 @ 64MHz regressed from 8/8 to 3/8 PASS (worst 60.18MHz).
Root cause traced via the real critical-path report: the SAME
pre-existing arbiter-to-sdram-backend bottleneck already documented all
session, made worse by flash's added die-area placement pressure --
not a new path through the flash logic itself. N_SLOTS=8 essentially
unchanged (6/8, was 5/8). See decisions.log DEC-0042 for full detail
and open decision points.

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 18:31:50 +02:00
co-authored by Claude Sonnet 5
parent b4f33388a9
commit 59901a4905
6 changed files with 605 additions and 20 deletions
+78 -5
View File
@@ -117,7 +117,22 @@ module spi_host_bridge #(
input wire [15:0] mem_rdata,
input wire mem_ready,
output reg soft_rst_pulse
output reg soft_rst_pulse,
// ---- flash #1 command interface (-> flash_slot_manager.v's own
// real V1 op_start/op_code/... port, unchanged shape/semantics) ----
output reg flash_op_start,
output reg [2:0] flash_op_code,
output reg [3:0] flash_slot_id,
output reg [23:0] flash_new_offset,
output reg [23:0] flash_new_length,
output reg [7:0] flash_new_type,
output reg [ADDR_WIDTH-1:0] flash_ext_addr,
output reg [23:0] flash_ext_length,
output reg [23:0] flash_raw_flash_addr,
input wire flash_busy,
input wire flash_done,
input wire flash_err
);
localparam NODEW = $clog2(N_NODES);
@@ -237,6 +252,7 @@ module spi_host_bridge #(
localparam OP_RESET = 8'h0F;
localparam OP_WRITE_JOB = 8'h10;
localparam OP_STATUS = 8'h20;
localparam OP_FLASH_CMD = 8'h30;
localparam ST_OPCODE = 4'd0;
localparam ST_JOB = 4'd1; // collecting 18 WRITE_JOB payload bytes
@@ -248,14 +264,17 @@ module spi_host_bridge #(
localparam ST_MEM_RISS= 4'd7; // READ_MEM: issue+wait mem_req
localparam ST_MEM_ROUT= 4'd8; // READ_MEM: shifting the 2 bytes of a word out
localparam ST_IGNORE = 4'd9; // opcode consumed / unknown, wait for cs_rose
localparam ST_FLASH = 4'd10; // collecting 19 OP_FLASH_CMD payload bytes
localparam ST_FLASH_WAIT = 4'd11; // flash_op_start held, waiting flash_busy to clear
reg [3:0] state;
reg [7:0] opcode;
reg [4:0] byte_idx; // generic byte counter within a field (up to 17, WRITE_JOB)
reg [4:0] byte_idx; // generic byte counter within a field (up to 18, OP_FLASH_CMD)
reg [15:0] len_words;
reg [15:0] word_cnt;
reg [15:0] cur_word; // WRITE_MEM: assembling MSB,LSB; READ_MEM: holding readback
reg job_busy_r, mem_busy_r, last_job_accepted_r;
reg flash_done_r, flash_err_r; // sticky, cleared by next OP_FLASH_CMD
// combinational tx byte mux -- STATUS response, READ_MEM data,
// everything else drives 0x00
@@ -263,7 +282,8 @@ module spi_host_bridge #(
always @(*) begin
tx_mux = 8'h00;
if (opcode == OP_STATUS)
tx_mux = {5'b0, last_job_accepted_r, mem_busy_r, job_busy_r};
tx_mux = {2'b0, flash_err_r, flash_done_r, flash_busy,
last_job_accepted_r, mem_busy_r, job_busy_r};
else if (opcode == OP_READ_MEM && state == ST_MEM_ROUT)
tx_mux = (byte_idx == 5'd0) ? cur_word[15:8] : cur_word[7:0];
end
@@ -281,6 +301,10 @@ module spi_host_bridge #(
mem_wdata <= 16'd0; mem_lb_n <= 1'b0; mem_ub_n <= 1'b0;
soft_rst_pulse <= 1'b0;
job_busy_r <= 1'b0; mem_busy_r <= 1'b0; last_job_accepted_r <= 1'b0;
flash_op_start <= 1'b0; flash_op_code <= 3'd0; flash_slot_id <= 4'd0;
flash_new_offset <= 24'd0; flash_new_length <= 24'd0; flash_new_type <= 8'd0;
flash_ext_addr <= {ADDR_WIDTH{1'b0}}; flash_ext_length <= 24'd0;
flash_raw_flash_addr <= 24'd0; flash_done_r <= 1'b0; flash_err_r <= 1'b0;
end else begin
mem_req <= 1'b0;
soft_rst_pulse <= 1'b0;
@@ -303,7 +327,7 @@ module spi_host_bridge #(
// root-caused via a full internal signal trace before
// this fix). Mirrors the same protection already applied
// to cs_rose below.
if (cs_fell && state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS) begin
if (cs_fell && state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS && state != ST_FLASH_WAIT) begin
state <= ST_OPCODE;
byte_idx <= 5'd0;
end else if (!cs_fell && rx_valid) begin
@@ -315,6 +339,7 @@ module spi_host_bridge #(
OP_WRITE_JOB: state <= ST_JOB;
OP_WRITE_MEM: state <= ST_MEM_ADDR;
OP_READ_MEM: state <= ST_MEM_ADDR;
OP_FLASH_CMD: state <= ST_FLASH;
OP_RESET: state <= ST_IGNORE;
default: state <= ST_IGNORE; // NOP, STATUS: no MOSI payload
endcase
@@ -385,7 +410,38 @@ module spi_host_bridge #(
end
end
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_IGNORE: no MOSI payload expected
ST_FLASH: begin
case (byte_idx)
5'd0: flash_op_code <= rx_byte[2:0];
5'd1: flash_slot_id <= rx_byte[3:0];
5'd2: flash_new_offset[23:16] <= rx_byte;
5'd3: flash_new_offset[15:8] <= rx_byte;
5'd4: flash_new_offset[7:0] <= rx_byte;
5'd5: flash_new_length[23:16] <= rx_byte;
5'd6: flash_new_length[15:8] <= rx_byte;
5'd7: flash_new_length[7:0] <= rx_byte;
5'd8: flash_new_type <= rx_byte;
5'd9: flash_ext_addr[ADDR_WIDTH-1:24] <= rx_byte[ADDR_WIDTH-25:0];
5'd10: flash_ext_addr[23:16] <= rx_byte;
5'd11: flash_ext_addr[15:8] <= rx_byte;
5'd12: flash_ext_addr[7:0] <= rx_byte;
5'd13: flash_ext_length[23:16] <= rx_byte;
5'd14: flash_ext_length[15:8] <= rx_byte;
5'd15: flash_ext_length[7:0] <= rx_byte;
5'd16: flash_raw_flash_addr[23:16] <= rx_byte;
5'd17: flash_raw_flash_addr[15:8] <= rx_byte;
5'd18: begin
flash_raw_flash_addr[7:0] <= rx_byte;
flash_op_start <= 1'b1;
flash_done_r <= 1'b0;
flash_err_r <= 1'b0;
state <= ST_FLASH_WAIT;
end
endcase
if (byte_idx != 5'd18) byte_idx <= byte_idx + 5'd1;
end
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_FLASH_WAIT/ST_IGNORE: no MOSI payload expected
endcase
end
@@ -439,6 +495,23 @@ module spi_host_bridge #(
end
end
// ---- OP_FLASH_CMD: hold flash_op_start until flash_busy
// rises (accepted, mirrors reg_valid/reg_ready and
// mem_req/mem_ready above), then wait for flash_busy to
// fall again (operation complete) before returning to
// ST_IGNORE, latching flash_done/flash_err into sticky
// STATUS bits (flash_slot_manager.v's own real done is a
// one-cycle pulse, err is held until the next op_start --
// both captured here, not re-derived).
if (state == ST_FLASH_WAIT && flash_op_start && flash_busy) begin
flash_op_start <= 1'b0;
end
if (flash_done) flash_done_r <= 1'b1;
if (flash_err) flash_err_r <= 1'b1;
if (state == ST_FLASH_WAIT && !flash_op_start && !flash_busy) begin
state <= ST_IGNORE;
end
job_busy_r <= (state == ST_JOB_WAIT);
if (cs_rose) begin