Establishes the real ESP32<->ECP5 programming architecture: flash #1 (neural-network data, existing V1 subsystem, ball reserved not yet wired into V2) stays separate from flash #2 (boot bitstream, MSPI auto-boot, CFG[2:0]=[0,1,0]); ESP32 talks JTAG only (bit-banged, no hardware JTAG-master peripheral on S3/C6), updating flash #2 through the ECP5's own internal sysCONFIG-to-SPI bridge, never driving the flash pins directly -- zero bus contention, confirmed against the real Lattice hardware checklist and sysCONFIG user guide. Adds real, verified ball assignments (official Lattice CABGA381 CSV + Project Trellis iodb.json) for JTAG, PROGRAMN/INITN/DONE, CFG[2:0], and the MSPI dedicated pins -- all written to docs/pinouts.md. Implements FPGA_DATA_READY as real RTL: a system-idle detector (dependency_manager's any_pending OR neural_director's !queue_empty OR any active slot), sticky on the busy->idle edge, self-clearing on new work -- not a per-neuron completion pulse, which was confirmed too fine-grained. Bit-exact regression re-verified at N_SLOTS=4 and 8 (zero cycle-count change), new explicit data_ready assertion check added to the D-Stress testbench (PASS both configs), and a fresh Yosys+nextpnr-ecp5 placement check (0 errors, data_ready placed at G3). Also fixes a real, independently-found bug while editing an adjacent file: nms_neural_multiprocessor_sdram_unified.v's own sdram_a port was still [11:0] (12 bits), stale from before the 64MB/13-bit memory upgrade. Not exercised by the real board-level top (which wires SDRAM directly, bypassing this wrapper) but WAS silently truncating A12 in every D-Stress simulation this session, including today's earlier ERR-0029 verification runs. Assessed impact: all D-Stress test addresses used this session decode to rows under 4096 (bit 12 never actually needed), so no false-positive PASS is believed to have resulted -- but the full 64MB space was never actually exercised through this wrapper. Fixed; re-verified bit-exact with identical cycle counts. See decisions.log DEC-0041 for full detail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
298 lines
14 KiB
Verilog
298 lines
14 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// FPGA-Neural V2 -- Neural Director (M5, docs/v2-description.md §9)
|
|
//
|
|
// Dispatches job descriptors, arriving via a simple valid/ready
|
|
// producer interface, to whichever of N_SLOTS (memory_manager, M4)
|
|
// instances is currently free -- first-free scheduling (§9's initial
|
|
// policy; round-robin/least-loaded/etc are explicitly deferred to a
|
|
// later, experimentally-driven milestone, not this one).
|
|
//
|
|
// Each "slot" is one memory_manager's own job_start/x_base/w_base/
|
|
// n_tiles/result_addr/job_done interface (M4) -- the Director does
|
|
// not touch a Neural Processor directly, matching §34's division of
|
|
// labor ("Il Director gestisce WHAT deve essere eseguito... Il
|
|
// Memory Manager gestisce COME rendere disponibili i dati").
|
|
//
|
|
// Scope of THIS milestone (see hardware/v2/logs/decisions.log
|
|
// DEC-0007 for the full rationale): jobs are assumed already READY
|
|
// (no unresolved dependencies) -- dependency tracking, the waiting
|
|
// queue, and wake-up are explicitly the Dependency Manager's job
|
|
// (§10, M6, not yet built). §9's baseline FSM states
|
|
// DIR_WAIT_DEPENDENCY/DIR_COMPLETE/DIR_WAKEUP are therefore not
|
|
// separate states here; DIR_MONITOR's job (detecting a slot's
|
|
// completion) is handled by an always-active per-slot busy tracker,
|
|
// not a state the main allocate/scan loop must visit -- the same
|
|
// "don't gate concurrent per-unit progress behind a single shared
|
|
// FSM state" principle already applied to the Neural Processor's own
|
|
// FSM (DEC-0002).
|
|
// ================================================================
|
|
|
|
module neural_director #(
|
|
parameter ADDR_WIDTH = 26,
|
|
parameter N_SLOTS = 4,
|
|
parameter QUEUE_DEPTH = 8
|
|
)(
|
|
input wire clk,
|
|
input wire rst,
|
|
|
|
// ---- job submission (producer interface, e.g. a host or a
|
|
// future Dependency Manager, M6) ----
|
|
input wire job_in_valid,
|
|
output wire job_in_ready,
|
|
input wire [ADDR_WIDTH-1:0] job_in_x_base,
|
|
input wire [ADDR_WIDTH-1:0] job_in_w_base,
|
|
input wire [15:0] job_in_n_tiles,
|
|
input wire [ADDR_WIDTH-1:0] job_in_result_addr,
|
|
input wire [15:0] job_in_node_id,
|
|
|
|
// ---- per-slot memory_manager job control (arrayed, §9) ----
|
|
// 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
|
|
// map a slot's job_done back to the node_id that just completed,
|
|
// 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 wire [16*N_SLOTS-1:0] slot_node_id,
|
|
input wire [N_SLOTS-1:0] slot_job_done,
|
|
|
|
// ---- completion notification (§9 "rilevamento dei completamenti") ----
|
|
output reg job_out_done, // one-cycle pulse
|
|
output reg [$clog2(N_SLOTS)-1:0] job_out_slot,
|
|
|
|
output reg [3:0] dir_state,
|
|
output reg dir_error,
|
|
|
|
// FPGA_DATA_READY support: high when the dispatch queue is empty
|
|
// (no job waiting for a free slot) -- combined upstream with
|
|
// dependency_manager's any_pending and this module's own slot
|
|
// activity to detect true system-idle.
|
|
output wire queue_empty
|
|
);
|
|
|
|
localparam DIR_IDLE = 4'd0;
|
|
localparam DIR_SCAN_READY = 4'd1;
|
|
localparam DIR_ALLOCATE = 4'd2;
|
|
localparam DIR_ERROR = 4'd3;
|
|
|
|
// ---- ready queue: a plain circular FIFO of job descriptors.
|
|
// Depth is parametric (§9 implies no fixed size); pushing and
|
|
// popping are independent of the allocate FSM below so a new job
|
|
// can be accepted the same cycle an old one is dispatched. ----
|
|
localparam Q_ADDR_WIDTH = $clog2(QUEUE_DEPTH);
|
|
|
|
reg [ADDR_WIDTH-1:0] q_x_base [0:QUEUE_DEPTH-1];
|
|
reg [ADDR_WIDTH-1:0] q_w_base [0:QUEUE_DEPTH-1];
|
|
reg [15:0] q_n_tiles [0:QUEUE_DEPTH-1];
|
|
reg [ADDR_WIDTH-1:0] q_result_addr [0:QUEUE_DEPTH-1];
|
|
reg [15:0] q_node_id [0:QUEUE_DEPTH-1];
|
|
|
|
reg [Q_ADDR_WIDTH-1:0] q_head, q_tail;
|
|
reg [Q_ADDR_WIDTH:0] q_count; // one extra bit: 0..QUEUE_DEPTH inclusive
|
|
|
|
wire q_empty = (q_count == 0);
|
|
assign queue_empty = q_empty;
|
|
wire q_full = (q_count == QUEUE_DEPTH[Q_ADDR_WIDTH:0]);
|
|
|
|
assign job_in_ready = !q_full;
|
|
|
|
// ---- per-slot busy tracking: always-active, independent of the
|
|
// main allocate/scan FSM state (see file header). ----
|
|
reg [N_SLOTS-1:0] slot_busy;
|
|
|
|
wire [N_SLOTS-1:0] slot_free = ~slot_busy;
|
|
wire any_slot_free = |slot_free;
|
|
|
|
// first-free slot index (priority encoder, lowest index wins --
|
|
// "first-free", per §9's initial policy, not load-balanced).
|
|
// Reset/default values below use '0 rather than
|
|
// {$clog2(N_SLOTS){1'b0}} -- at N_SLOTS=1, $clog2(1)=0 makes that
|
|
// replication a ZERO-width replication, illegal outside a
|
|
// concatenation (IEEE 1800 11.4.12.1); found when this module was
|
|
// first synthesized/simulated at N_SLOTS=1 by the post-M10
|
|
// benchmark campaign (never exercised at N_SLOTS=1 through M5-M9).
|
|
// '0 self-sizes correctly for any width, including 0.
|
|
reg [$clog2(N_SLOTS)-1:0] free_slot_idx;
|
|
integer fi;
|
|
always @(*) begin
|
|
free_slot_idx = '0;
|
|
for (fi = N_SLOTS-1; fi >= 0; fi = fi - 1) begin
|
|
if (slot_free[fi]) free_slot_idx = fi[$clog2(N_SLOTS)-1:0];
|
|
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
|
|
// would read its own pre-edge value and not actually suppress a
|
|
// second same-cycle match, see file header/DEC-0007).
|
|
reg [$clog2(N_SLOTS)-1:0] done_slot_idx;
|
|
integer di;
|
|
always @(*) begin
|
|
done_slot_idx = '0;
|
|
for (di = N_SLOTS-1; di >= 0; di = di - 1) begin
|
|
if (slot_job_done[di]) done_slot_idx = di[$clog2(N_SLOTS)-1:0];
|
|
end
|
|
end
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
dir_state <= DIR_IDLE;
|
|
dir_error <= 1'b0;
|
|
q_head <= {Q_ADDR_WIDTH{1'b0}};
|
|
q_tail <= {Q_ADDR_WIDTH{1'b0}};
|
|
q_count <= {(Q_ADDR_WIDTH+1){1'b0}};
|
|
slot_busy <= {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
|
|
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
|
|
// of the allocate FSM's own state -- a producer must
|
|
// never be blocked just because the FSM is mid-allocate
|
|
// this cycle). ----
|
|
if (job_in_valid && job_in_ready) begin
|
|
q_x_base[q_tail] <= job_in_x_base;
|
|
q_w_base[q_tail] <= job_in_w_base;
|
|
q_n_tiles[q_tail] <= job_in_n_tiles;
|
|
q_result_addr[q_tail] <= job_in_result_addr;
|
|
q_node_id[q_tail] <= job_in_node_id;
|
|
q_tail <= (q_tail == QUEUE_DEPTH[Q_ADDR_WIDTH-1:0]-1'b1) ? {Q_ADDR_WIDTH{1'b0}} : q_tail + 1'b1;
|
|
end
|
|
|
|
// ---- Free a slot the instant its job_done pulses,
|
|
// regardless of the allocate FSM's own state (DIR_MONITOR
|
|
// absorbed here -- see file header). Cleared with a single
|
|
// vectorized AND-NOT of the whole slot_job_done bus (not a
|
|
// per-bit for-loop of individual NBA writes) so that TWO
|
|
// slots completing on the SAME cycle both get freed --a
|
|
// per-bit loop would have each iteration's non-blocking
|
|
// write use the same pre-edge slot_busy, so only the LAST
|
|
// matching bit would actually clear ("last write wins").
|
|
// job_out_done/job_out_slot still report at most one
|
|
// (the lowest-indexed) simultaneous completion per cycle
|
|
// -- a documented simplification (decisions.log DEC-0007),
|
|
// not a correctness issue for slot freeing itself. ----
|
|
slot_busy <= slot_busy & ~slot_job_done;
|
|
if (|slot_job_done) begin
|
|
job_out_done <= 1'b1;
|
|
job_out_slot <= done_slot_idx;
|
|
end
|
|
|
|
// ---- Main allocate/scan loop ----
|
|
case (dir_state)
|
|
|
|
DIR_IDLE: begin
|
|
dir_state <= DIR_SCAN_READY;
|
|
end
|
|
|
|
DIR_SCAN_READY: begin
|
|
if (!q_empty && any_slot_free) begin
|
|
dir_state <= DIR_ALLOCATE;
|
|
end
|
|
end
|
|
|
|
DIR_ALLOCATE: begin
|
|
// 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;
|
|
end
|
|
|
|
DIR_ERROR: begin
|
|
// Recoverable only via rst (§34: an error must not
|
|
// block the rest of the system).
|
|
end
|
|
|
|
default: dir_state <= DIR_ERROR;
|
|
|
|
endcase
|
|
|
|
// q_count tracks push/pop independently of which branch
|
|
// above fired, so it stays correct even when a push and a
|
|
// pop happen the same cycle.
|
|
case ({job_in_valid && job_in_ready, (dir_state == DIR_SCAN_READY) && !q_empty && any_slot_free})
|
|
2'b10: q_count <= q_count + 1'b1;
|
|
2'b01: q_count <= q_count - 1'b1;
|
|
default: q_count <= q_count; // 00: no change, 11: push+pop cancel out
|
|
endcase
|
|
end
|
|
end
|
|
|
|
endmodule
|