feat(v2): scaffold hardware/v1 frozen baseline + M1 Neural Processor

Begins the V2 Neural Multiprocessor / Dataflow architecture per
docs/v2-description.md, per explicit user request to freeze V1 and
start V2 development, copying from V1 what's needed.

Scaffold:
- hardware/v1/: byte-exact, read-only copy of the current V1 codebase
  (rtl, testbenches, tools, constraints, a representative subset of
  synthesis results, and reference docs) -- verified identical via
  diff/cmp against the live top-level tree before being made
  filesystem-read-only. The live top-level tree is untouched and
  remains the project's "production" V1 (see hardware/v1/README.md
  and hardware/v2/logs/decisions.log DEC-0001 for why copy-not-move).
- hardware/v2/: mandatory structure (rtl/sim/constraints/synthesis/
  reports/scripts/logs/docs) plus the full logging system required by
  the spec (development/architecture/simulation/synthesis/timing/
  benchmark/decisions/experiments/errors.log).

M1 -- Neural Processor (hardware/v2/rtl/neural_processor.v):
- 8-stage pipelined perceptron unit (P_IN=8): input align, 8
  multipliers, 3-level adder tree, accumulator, bias+activation, INT8
  saturation. Genuine 1-tile/cycle throughput, not just a wider
  combinational datapath.
- 7-state FSM (NP_IDLE..NP_ERROR per docs/v2-description.md §6, with
  4 baseline states merged into NP_WAIT_OPERANDS -- see
  decisions.log DEC-0002); valid/ready/data/last stream interfaces
  per §7.
- Bit-exact vs the frozen hardware/v1/rtl/neuron_parallel.v + mac8.v
  + mac_unit.v: 7/7 tests pass (hardware/v2/sim/tb_neural_processor.v),
  covering regular/mixed-sign/extreme-INT8 vectors, both activations,
  a zero-idle-gap back-to-back-tiles throughput check, and an 8-tile
  job -- verified with Verilator (see below for why).
- Real synthesis + place&route (Yosys + nextpnr-ecp5): 0 CHECK
  problems, Fmax 183.12 MHz at ACC_WIDTH=32 (PASS at 80MHz, ~3x V1's
  isolated PARALLEL=8 Fmax of 61.71 MHz) and 176.21 MHz at ACC_WIDTH=24
  (a user-requested comparison experiment, also bit-exact-verified;
  see experiments.log EXP-0001/EXP-0002 and benchmark.log).

Three real bugs found and resolved during M1 development (full
diagnostic record in errors.log):
- Two independent, reproducible Icarus Verilog v13.0 scheduling
  defects (ERR-0001, ERR-0002) that silently produced wrong simulation
  results for standard sequential Verilog -- confirmed via Verilator
  5.050 giving correct results on the same minimal repros. Verilator
  is now the trusted simulator for hardware/v2/ (decisions.log
  DEC-0004); Icarus's affected protocol-violation check was removed
  from the RTL and deferred architecturally to the Neural Director
  (DEC-0003) rather than chased further.
- One real RTL bug (ERR-0003): last0 wasn't gated like valid0,
  letting a "last tile" tag leak into the pipeline ahead of its
  actual valid tile on back-to-back jobs. Fixed and verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-05 14:06:53 +02:00
co-authored by Claude Sonnet 5
parent 07a48e401f
commit dc0b331d3e
161 changed files with 1151210 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
`timescale 1ns/1ps
// ================================================================
// ACT_BUFFER (Phase G1 -- Graph engine, Type #2 network)
//
// Global activation buffer for the sparse-graph datapath: one INT8
// slot per signal id, id 0..N_in-1 are the network's external
// inputs, every other id is the output of exactly one neuron (its
// out_id). graph_engine's gather stage reads this buffer by src_id;
// each neuron's result is written back here at its own out_id.
//
// Dual-port, byte-addressed:
// Port A - synchronous write (neuron output / input copy-in).
// Port B - synchronous READ, REGISTERED: rd_data reflects rd_addr
// from the PREVIOUS clock edge, not the current one (one
// cycle of latency). Callers must account for this --
// see graph_engine.v's gather stage.
//
// This is the plain always-block-per-port inference idiom Yosys'
// ECP5 memory_bram pass maps onto a Lattice DP16KD block RAM (single
// clock, independent read/write address, synchronous read with no
// output reset): no `rst` port is provided on purpose, matching what
// a real DP16KD offers and keeping this off the LUT-RAM path that a
// register-array-with-reset idiom would force it down.
//
// N_TOTAL is the V1 ceiling on distinct signal ids (§2 of the spec:
// 4096, 16-bit id space would allow up to 65536 without a format
// change). ADDR_WIDTH is derived, not passed in, so every caller
// stays consistent with N_TOTAL automatically.
// ================================================================
module act_buffer #(
parameter N_TOTAL = 4096,
parameter DATA_WIDTH = 8,
localparam ADDR_WIDTH = $clog2(N_TOTAL)
)(
input wire clk,
// ------------------------------------------------------------
// Port A: write
// ------------------------------------------------------------
input wire wr_en,
input wire [ADDR_WIDTH-1:0] wr_addr,
input wire signed [DATA_WIDTH-1:0] wr_data,
// ------------------------------------------------------------
// Port B: read (registered, 1-cycle latency)
// ------------------------------------------------------------
input wire [ADDR_WIDTH-1:0] rd_addr,
output reg signed [DATA_WIDTH-1:0] rd_data
);
reg signed [DATA_WIDTH-1:0] mem [0:N_TOTAL-1];
always @(posedge clk) begin
if (wr_en)
mem[wr_addr] <= wr_data;
end
always @(posedge clk) begin
rd_data <= mem[rd_addr];
end
endmodule
+50
View File
@@ -0,0 +1,50 @@
`timescale 1ns/1ps
// ================================================================
// CRC32_BYTE - combinational one-byte CRC32 update (IEEE 802.3 /
// zlib.crc32 algorithm: reflected polynomial 0xEDB88320, MSB-first
// byte order, init 0xFFFFFFFF, final XOR 0xFFFFFFFF applied by the
// CALLER when reading out the finished CRC, not baked in here so
// this block is a pure, reusable byte-update step).
//
// This exact reflected-polynomial bit-serial-per-byte construction
// is the standard, widely-documented way to compute the same CRC32
// zlib/PNG/Ethernet use; it is NOT re-derived from the flash
// subsystem's own design -- the independent oracle for this
// project is tools/flash_catalog/oracle.py's `crc32()`, which calls
// Python's stdlib `zlib.crc32` (a completely separate implementation
// in a different language). The two are cross-checked bit-for-bit
// by sim/crc32_tb.v -- see WORKLOG.md's F4 entry for the numbers.
//
// Usage: register `crc_out` into your own accumulator on the cycle
// a new byte is valid, seeded at 32'hFFFFFFFF before the first byte
// of a message; XOR the final accumulated value with 32'hFFFFFFFF
// to get the conventional CRC32 result.
// ================================================================
module crc32_byte (
input wire [31:0] crc_in,
input wire [7:0] data,
output wire [31:0] crc_out
);
function [31:0] next_crc;
input [31:0] c_in;
input [7:0] d;
reg [31:0] c;
integer k;
begin
c = c_in ^ {24'h0, d};
for (k = 0; k < 8; k = k + 1) begin
if (c[0])
c = (c >> 1) ^ 32'hEDB88320;
else
c = c >> 1;
end
next_crc = c;
end
endfunction
assign crc_out = next_crc(crc_in, data);
endmodule
+608
View File
@@ -0,0 +1,608 @@
`timescale 1ns/1ps
// ================================================================
// FLASH_COPY_ENGINE
//
// DMA-style block-streaming engine between the boot/persistence
// flash (via rtl/spi_flash_master.v, which it owns internally) and
// the shared PSRAM (via a new low-priority Port D on
// rtl/mem_arbiter.v -- see that file's updated header). This is the
// module the flash-subsystem phase-plan's §3 describes: the host
// issues a high-level command and walks away; there is no
// byte-at-a-time host involvement, only the FPGA streaming at
// whatever rate the flash/PSRAM actually allow.
//
// F2 SCOPE: LOAD direction, flash -> PSRAM. `op_start` with
// `op_dir = DIR_LOAD` copies `len` bytes from `flash_addr` to
// `psram_addr`. The Read Data (03h) instruction has no page-boundary
// restriction (unlike Page Program, §8 intro p.24: only
// WRITE/PROGRAM/ERASE instructions must land on a byte/page boundary
// or get ignored -- READ just streams and auto-increments, wrapping
// at the top of the array), so a LOAD needs no internal
// erase/program looping.
//
// F3 SCOPE: SAVE direction, PSRAM -> flash. `op_start` with
// `op_dir = DIR_SAVE` copies `len` bytes from `psram_addr` to
// `flash_addr`, doing erase-before-write internally:
//
// - Design decision (phase-plan §2.1 explicitly asks for one,
// with a stated reason): `flash_addr` MUST be 4KB-sector-aligned
// (low 12 bits zero) -- rejected as a bounds/alignment error
// otherwise (§A.3 "blocco non allineato al settore"), rather
// than silently doing a read-modify-erase-write of a partial
// sector. Reason: this project has NO scratch buffer large
// enough to hold a whole 4KB sector's unrelated surrounding data
// while erasing/reprogramming it, and the flash-subsystem's own
// design (fixed-size catalog slots, §4 of the phase-plan) means
// every real SAVE_SLOT call (F5) already writes whole,
// sector-aligned slots -- so alignment is not a real-world
// restriction here, only a rejected pathological case.
// - Erase phase: every 4KB sector overlapping [flash_addr,
// flash_addr+len) is erased (WREN + SE + poll RDSR-1 bit0/WIP
// until clear) before any programming starts. A `len` that
// isn't itself a sector multiple still erases the WHOLE last
// (partial) sector -- erase has no finer granularity (Table 1
// p.26) -- leaving the unwritten tail of that sector at 0xFF
// (erased state), which is correct/expected, not a bug: the
// catalog's own valid_flag+CRC (F4) is what marks the meaningful
// length of a slot, not "everything in the sector is meaningful".
// F5 SCOPE: DIR_ERASE, a standalone sector erase (the phase-plan's
// §5 FLASH_ERASE opcode, exposed once F5 wires this engine to
// spi_engine). `op_start` with `op_dir = DIR_ERASE` erases exactly
// the one 4KB sector at `flash_addr` (which must be sector-aligned,
// same check/reason as DIR_SAVE) -- `psram_addr` and `len` are
// ignored. Reuses DIR_SAVE's own erase-phase states verbatim (WREN +
// SE + poll RDSR-1/WIP), just stopping after that one sector instead
// of falling through to the program phase -- see `erase_only` below.
//
// - Program phase: looped Page Program (02h) calls of up to 256B
// each (never crossing a page boundary -- guaranteed by
// `flash_addr` being sector-, hence page-, aligned, and by
// capping every chunk at 256B), each individually WREN'd and
// RDSR-polled to completion before the next, exactly as the
// phase-plan's §2.2 requires ("Il loop lo fa la FPGA"). Each
// page's bytes are sourced live from PSRAM one at a time via
// Port D reads, driven by spi_flash_master's own `wdata_req`
// handshake (F1) -- no local buffer needed, matching this
// engine's LOAD-side "stream through" style.
//
// spi_flash_master's `n_data` is 16 bits (max 65535 bytes per SPI
// transaction); this engine's own `len` is 24 bits (matching the
// flash-subsystem opcode draft's 3-byte length field, §5 of the
// phase-plan), so a LOAD larger than 65535 bytes is split into
// multiple back-to-back spi_flash_master READ transactions
// (CHUNK_MAX bytes each) rather than assumed to fit in one -- closes
// a latent bug rather than leaving an untested edge case.
//
// Bounds checking (§A.3 "len fuori range" negative case, enforced
// here rather than deferred entirely to the F5 opcode layer, in
// case a future caller other than spi_engine ever drives this
// module directly): a request whose flash_addr+len would exceed the
// modeled 16MB (2^24) flash address space, or whose psram_addr+len
// would exceed the 8MB (2^23, ADDR_WIDTH) PSRAM address space,
// completes immediately with `err` asserted and does not touch the
// flash or PSRAM at all.
// ================================================================
module flash_copy_engine #(
parameter PSRAM_ADDR_WIDTH = 23,
parameter CLK_FREQ_MHZ = 80,
parameter SCLK_DIV = 2
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Physical flash pins (this module owns spi_flash_master)
// ------------------------------------------------------------
output wire mosi,
input wire miso,
output wire cs_n,
output wire sclk, // ordinary GPIO, real in both sim and synthesis
// ------------------------------------------------------------
// Command interface
// ------------------------------------------------------------
input wire op_start,
input wire [1:0] op_dir, // DIR_LOAD (F2, flash->PSRAM) or DIR_SAVE (F3, PSRAM->flash)
input wire [23:0] flash_addr,
input wire [PSRAM_ADDR_WIDTH-1:0] psram_addr,
input wire [23:0] len,
output wire busy,
output reg done, // one-cycle pulse
output reg err, // held until next op_start; see bounds check above
// ------------------------------------------------------------
// PSRAM arbiter master port (mem_arbiter.v Port D)
//
// d_req is a LEVEL signal (held for the whole ST_PSRAM_WAIT
// state, see below), not a one-cycle pulse like the other
// ports' requesters (spi_engine.v etc.) use. Found necessary
// during F2 bring-up (see WORKLOG.md): mem_arbiter.v only
// samples a requester's `req` while `owner==SEL_NONE`, so a
// ONE-CYCLE pulse that happens to land on the exact same cycle
// a higher-priority port (A/B/C) also requests is granted to
// that other port and simply never retried -- Port D is lowest
// priority by design (see mem_arbiter.v's header), so under any
// real, sustained contention from Port A a one-shot pulse would
// eventually get "unlucky" and hang this engine forever waiting
// for a d_ready that will never come. Holding d_req at the
// arbiter continuously (not just once) makes the wait exactly
// what the design intends -- "gets stretched out", never lost --
// without needing to change mem_arbiter.v itself (which serves
// the three already-validated masters too).
// ------------------------------------------------------------
output wire d_req,
output reg d_wr,
output reg [PSRAM_ADDR_WIDTH-1:0] d_addr,
output reg signed [7:0] d_wdata,
input wire signed [7:0] d_rdata,
input wire d_ready
);
localparam DIR_LOAD = 2'd0;
localparam DIR_SAVE = 2'd1;
localparam DIR_ERASE = 2'd2;
// FLASH_SPACE_BYTES = 16MB = 2^24 does NOT fit in 24 bits (24 bits
// only reaches 2^24-1) -- needs 25. Caught by iverilog's own
// "numeric constant truncated" warning on the first compile
// attempt (it silently became 0, which would have broken every
// bounds check below into "always in range"); widened here.
localparam [24:0] FLASH_SPACE_BYTES = 25'h100_0000; // 16MB, 2^24
localparam [23:0] CHUNK_MAX = 24'h00_FFFF; // spi_flash_master's n_data is 16b
localparam [23:0] SECTOR_BYTES = 24'd4096; // Sector Erase (4KB), Table 1 p.26 "20h"
localparam [23:0] PAGE_BYTES = 24'd256; // Page Program max, Table 1 p.26/note 3 p.29
// ============================================================
// spi_flash_master instance (F1 primitive)
// ============================================================
reg fm_start;
reg [7:0] fm_opcode;
reg fm_has_addr;
reg [23:0] fm_addr;
reg [1:0] fm_dir;
reg [15:0] fm_n_data;
wire fm_wdata_req;
reg [7:0] fm_wdata; // F3: driven from a live PSRAM read during Page Program
reg fm_wdata_valid;
wire fm_rdata_valid;
wire [7:0] fm_rdata;
reg fm_rdata_ack;
wire fm_busy;
wire fm_done;
localparam [1:0] FM_DIR_NONE = 2'd0;
localparam [1:0] FM_DIR_WRITE = 2'd1;
localparam [1:0] FM_DIR_READ = 2'd2;
spi_flash_master #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ),
.SCLK_DIV(SCLK_DIV)
) u_spi_flash_master (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n), .sclk(sclk),
.start(fm_start), .opcode(fm_opcode), .has_addr(fm_has_addr),
.addr(fm_addr), .dir(fm_dir), .n_data(fm_n_data),
.wdata_req(fm_wdata_req), .wdata(fm_wdata), .wdata_valid(fm_wdata_valid),
.rdata_valid(fm_rdata_valid), .rdata(fm_rdata), .rdata_ack(fm_rdata_ack),
.busy(fm_busy), .done(fm_done)
);
localparam [7:0] OP_READ = 8'h03;
localparam [7:0] OP_WREN = 8'h06;
localparam [7:0] OP_PP = 8'h02;
localparam [7:0] OP_SE = 8'h20;
localparam [7:0] OP_RDSR1 = 8'h05;
// ============================================================
// Main FSM
// ============================================================
localparam ST_IDLE = 5'd0;
localparam ST_CHUNK_ISSUE = 5'd1; // LOAD: starts one spi_flash_master READ for up to CHUNK_MAX bytes
localparam ST_CHUNK_WAIT = 5'd2; // LOAD: services rdata_valid -> PSRAM write, one byte at a time
localparam ST_PSRAM_WAIT = 5'd3; // LOAD: waiting for d_ready after issuing a PSRAM write
localparam ST_DONE = 5'd4;
// F3 (SAVE) states
localparam ST_SAVE_ERASE_WREN = 5'd5; // WREN before this sector's erase
localparam ST_SAVE_ERASE_WWAIT = 5'd6; // wait for WREN's own fm_done
localparam ST_SAVE_ERASE_ISSUE = 5'd7; // issue SE for the current sector
localparam ST_SAVE_ERASE_EWAIT = 5'd8; // wait for SE's own fm_done (command accepted, not WIP clear)
localparam ST_SAVE_PROG_WREN = 5'd9; // WREN before this page's Page Program
localparam ST_SAVE_PROG_WWAIT = 5'd10; // wait for WREN's own fm_done
localparam ST_SAVE_PROG_ISSUE = 5'd11; // issue PP header for the current page
localparam ST_SAVE_PROG_BYTE = 5'd12; // waiting for fm_wdata_req or fm_done (page byte loop)
localparam ST_SAVE_PROG_PWAIT = 5'd13; // waiting for d_ready on the PSRAM source read (presents wdata_valid the same cycle d_ready arrives)
// Shared RDSR (WIP) poll, used after both SE and PP -- which one
// is in progress, and what to do once WIP clears, is tracked by
// `save_phase` (below) rather than duplicated poll logic.
localparam ST_RDSR_ISSUE = 5'd15;
localparam ST_RDSR_BYTE = 5'd16; // waiting for the single response byte
localparam ST_RDSR_DWAIT = 5'd17; // wait for RDSR's own fm_done
reg [4:0] state;
reg [23:0] remaining;
reg [23:0] cur_flash_addr;
reg [PSRAM_ADDR_WIDTH-1:0] cur_psram_addr;
// F3 (SAVE) bookkeeping
localparam SAVE_PHASE_ERASE = 1'b0;
localparam SAVE_PHASE_PROG = 1'b1;
reg save_phase;
reg [24:0] save_end; // flash_addr + len, one extra bit of headroom
reg [23:0] erase_addr; // current sector cursor during the erase phase
reg [23:0] prog_addr; // current flash address cursor during the program phase
reg [PSRAM_ADDR_WIDTH-1:0] prog_psram_addr; // current PSRAM source cursor
reg [23:0] prog_remaining; // bytes left to program overall
reg wip_busy; // RDSR-1 bit0, captured by the poll
reg erase_only; // F5 (DIR_ERASE): stop after the erase phase, no program phase
assign busy = (state != ST_IDLE);
// d_req must drop the SAME cycle d_ready is observed, not wait
// for the state transition out of ST_PSRAM_WAIT (which only
// takes effect the following cycle): mem_arbiter's grant of Port
// D also completes (owner -> SEL_NONE) that same cycle, and its
// SEL_NONE case is combinational logic re-evaluated that very
// cycle -- if d_req were still 1 (as it would be with a plain
// `state == ST_PSRAM_WAIT` here, since `state` itself hasn't
// updated yet), the arbiter would immediately re-grant Port D a
// SECOND time using stale d_addr/d_wdata, which this engine is
// no longer driving meaningfully. Found during F2 bring-up (see
// WORKLOG.md) as a hang after the last byte of a LOAD. Same
// reasoning applies to F3's SAVE-side PSRAM source reads
// (ST_SAVE_PROG_PWAIT) -- same Port D, same arbiter, same race.
assign d_req = ((state == ST_PSRAM_WAIT) || (state == ST_SAVE_PROG_PWAIT)) && !d_ready;
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
done <= 1'b0;
err <= 1'b0;
fm_start <= 1'b0;
fm_opcode <= 8'h00;
fm_has_addr <= 1'b0;
fm_addr <= 24'h0;
fm_dir <= 2'd0;
fm_n_data <= 16'h0;
fm_rdata_ack <= 1'b0;
d_wr <= 1'b0;
d_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
d_wdata <= 8'sd0;
remaining <= 24'h0;
cur_flash_addr <= 24'h0;
cur_psram_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
fm_wdata <= 8'h00;
fm_wdata_valid <= 1'b0;
save_phase <= SAVE_PHASE_ERASE;
save_end <= 25'h0;
erase_addr <= 24'h0;
prog_addr <= 24'h0;
prog_psram_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
prog_remaining <= 24'h0;
wip_busy <= 1'b0;
erase_only <= 1'b0;
end else begin
fm_start <= 1'b0;
fm_rdata_ack <= 1'b0;
fm_wdata_valid <= 1'b0;
done <= 1'b0;
case (state)
// --------------------------------------------
ST_IDLE: begin
if (op_start) begin
if (op_dir == DIR_ERASE) begin
// F5: standalone sector erase. `len` and
// `psram_addr` are not meaningful here
// (ignored) -- only flash_addr's own
// sector-alignment and range matter.
if ( ({8'h0, flash_addr} + {8'h0, SECTOR_BYTES}) > {7'h0, FLASH_SPACE_BYTES} ||
flash_addr[11:0] != 12'h000
) begin
err <= 1'b1;
done <= 1'b1;
end else begin
err <= 1'b0;
save_phase <= SAVE_PHASE_ERASE;
erase_addr <= flash_addr;
save_end <= {1'b0, flash_addr} + {1'b0, SECTOR_BYTES}; // exactly one sector
erase_only <= 1'b1;
state <= ST_SAVE_ERASE_WREN;
end
// §A.3 negative cases (LOAD/SAVE): length
// pushes either address space past its own
// size (compared in generously-wide 32b
// temporaries so no same-width overflow can
// hide the very condition being checked
// for); an unknown direction; and, for SAVE
// only, a non-sector-aligned flash_addr (the
// design decision documented in the module
// header).
end else if ( ({8'h0, flash_addr} + {8'h0, len}) > {7'h0, FLASH_SPACE_BYTES} ||
({{(32-PSRAM_ADDR_WIDTH){1'b0}}, psram_addr} + {8'h0, len})
> (32'h1 << PSRAM_ADDR_WIDTH) ||
len == 24'h0 ||
(op_dir != DIR_LOAD && op_dir != DIR_SAVE) ||
(op_dir == DIR_SAVE && flash_addr[11:0] != 12'h000)
) begin
err <= 1'b1;
done <= 1'b1;
end else if (op_dir == DIR_LOAD) begin
err <= 1'b0;
remaining <= len;
cur_flash_addr <= flash_addr;
cur_psram_addr <= psram_addr;
state <= ST_CHUNK_ISSUE;
end else begin // DIR_SAVE
err <= 1'b0;
erase_only <= 1'b0;
save_phase <= SAVE_PHASE_ERASE;
save_end <= {1'b0, flash_addr} + {1'b0, len};
erase_addr <= flash_addr;
prog_addr <= flash_addr;
prog_psram_addr <= psram_addr;
prog_remaining <= len;
state <= ST_SAVE_ERASE_WREN;
end
end
end
// --------------------------------------------
ST_CHUNK_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_READ;
fm_has_addr <= 1'b1;
fm_addr <= cur_flash_addr;
fm_dir <= FM_DIR_READ;
fm_n_data <= (remaining > {8'h0, CHUNK_MAX}) ? CHUNK_MAX[15:0] : remaining[15:0];
state <= ST_CHUNK_WAIT;
end
// --------------------------------------------
// Services one byte at a time: spi_flash_master
// pauses (holds CS low, sclk idle) with rdata_valid
// asserted until fm_rdata_ack; meanwhile this state
// issues the matching PSRAM write and waits for
// d_ready before acking, giving natural backpressure
// -- the flash is never read faster than PSRAM can
// absorb.
// --------------------------------------------
ST_CHUNK_WAIT: begin
if (fm_rdata_valid) begin
d_wr <= 1'b1;
d_addr <= cur_psram_addr;
d_wdata <= $signed(fm_rdata);
state <= ST_PSRAM_WAIT; // d_req becomes 1 combinationally, see assign above
end else if (fm_done) begin
// Chunk's spi_flash_master transaction fully
// complete (`remaining` already decremented,
// one per byte, in ST_PSRAM_WAIT below --
// for each byte of THIS chunk). Zero means
// the whole request is done; nonzero means
// another chunk is still needed.
if (remaining == 24'h0) begin
state <= ST_DONE;
end else begin
state <= ST_CHUNK_ISSUE;
end
end
end
// --------------------------------------------
ST_PSRAM_WAIT: begin
if (d_ready) begin
fm_rdata_ack <= 1'b1;
cur_flash_addr <= cur_flash_addr + 24'd1;
cur_psram_addr <= cur_psram_addr + 1'b1;
remaining <= remaining - 24'd1;
state <= ST_CHUNK_WAIT;
end
end
// --------------------------------------------
ST_DONE: begin
done <= 1'b1;
state <= ST_IDLE;
end
// ============================================
// F3 (SAVE): erase phase
// ============================================
ST_SAVE_ERASE_WREN: begin
fm_start <= 1'b1;
fm_opcode <= OP_WREN;
fm_has_addr <= 1'b0;
fm_dir <= FM_DIR_NONE;
fm_n_data <= 16'd0;
state <= ST_SAVE_ERASE_WWAIT;
end
ST_SAVE_ERASE_WWAIT: begin
if (fm_done)
state <= ST_SAVE_ERASE_ISSUE;
end
ST_SAVE_ERASE_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_SE;
fm_has_addr <= 1'b1;
fm_addr <= erase_addr;
fm_dir <= FM_DIR_NONE;
fm_n_data <= 16'd0;
state <= ST_SAVE_ERASE_EWAIT;
end
ST_SAVE_ERASE_EWAIT: begin
// SE's own fm_done only means the command was
// clocked in, NOT that the erase has physically
// completed -- §2.1/§2.4 of the phase-plan: must
// poll RDSR-1's WIP bit next.
if (fm_done) begin
save_phase <= SAVE_PHASE_ERASE;
state <= ST_RDSR_ISSUE;
end
end
// ============================================
// F3 (SAVE): program phase, one page (<=256B) at a
// time, each individually WREN'd and WIP-polled.
// ============================================
ST_SAVE_PROG_WREN: begin
fm_start <= 1'b1;
fm_opcode <= OP_WREN;
fm_has_addr <= 1'b0;
fm_dir <= FM_DIR_NONE;
fm_n_data <= 16'd0;
state <= ST_SAVE_PROG_WWAIT;
end
ST_SAVE_PROG_WWAIT: begin
if (fm_done)
state <= ST_SAVE_PROG_ISSUE;
end
ST_SAVE_PROG_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_PP;
fm_has_addr <= 1'b1;
fm_addr <= prog_addr;
fm_dir <= FM_DIR_WRITE;
// Never crosses a 256B page boundary: prog_addr
// is always page-aligned when a page starts
// (flash_addr was required sector-, hence page-,
// aligned at DIR_SAVE dispatch, and every page
// before this one consumed exactly PAGE_BYTES
// bytes -- only the LAST page of the whole
// request can be shorter).
fm_n_data <= (prog_remaining > PAGE_BYTES) ? PAGE_BYTES[15:0] : prog_remaining[15:0];
state <= ST_SAVE_PROG_BYTE;
end
// Services spi_flash_master's wdata_req one byte at
// a time by reading the next PSRAM source byte
// (Port D) and handing it straight through -- no
// local page buffer, same "stream through" style as
// the LOAD path. prog_addr/prog_psram_addr/
// prog_remaining are advanced per-byte in
// ST_SAVE_PROG_PWAIT below, so by the time fm_done
// fires here they already reflect the post-page
// state (mirrors ST_PSRAM_WAIT's own bookkeeping on
// the LOAD side).
ST_SAVE_PROG_BYTE: begin
if (fm_wdata_req) begin
d_wr <= 1'b0;
d_addr <= prog_psram_addr;
state <= ST_SAVE_PROG_PWAIT;
end else if (fm_done) begin
save_phase <= SAVE_PHASE_PROG;
state <= ST_RDSR_ISSUE;
end
end
ST_SAVE_PROG_PWAIT: begin
if (d_ready) begin
fm_wdata <= d_rdata;
fm_wdata_valid <= 1'b1;
prog_psram_addr <= prog_psram_addr + 1'b1;
prog_addr <= prog_addr + 24'd1;
prog_remaining <= prog_remaining - 24'd1;
state <= ST_SAVE_PROG_BYTE;
end
end
// ============================================
// Shared RDSR-1 (WIP) poll, used after both SE and
// PP. `save_phase` (latched by the caller just
// before entering here) decides what "WIP cleared"
// means next: advance to the next sector / finish
// the erase phase, or advance to the next page /
// finish the whole SAVE.
// ============================================
ST_RDSR_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_RDSR1;
fm_has_addr <= 1'b0;
fm_dir <= FM_DIR_READ;
fm_n_data <= 16'd1;
state <= ST_RDSR_BYTE;
end
ST_RDSR_BYTE: begin
if (fm_rdata_valid) begin
wip_busy <= fm_rdata[0]; // RDSR-1 bit0 = BUSY/WIP, sim/flash_model.v header
fm_rdata_ack <= 1'b1;
state <= ST_RDSR_DWAIT;
end
end
ST_RDSR_DWAIT: begin
if (fm_done) begin
if (wip_busy) begin
state <= ST_RDSR_ISSUE; // still busy: poll again
end else if (save_phase == SAVE_PHASE_ERASE) begin
erase_addr <= erase_addr + SECTOR_BYTES;
if (({1'b0, erase_addr} + {1'b0, SECTOR_BYTES}) >= save_end) begin
// Erase phase covers the whole requested
// range: F5's standalone DIR_ERASE stops
// here (erase_only); DIR_SAVE falls
// through to programming.
state <= erase_only ? ST_DONE : ST_SAVE_PROG_WREN;
end else begin
state <= ST_SAVE_ERASE_WREN; // next sector
end
end else begin // SAVE_PHASE_PROG
if (prog_remaining == 24'h0) begin
state <= ST_DONE;
end else begin
state <= ST_SAVE_PROG_WREN; // next page
end
end
end
end
default: state <= ST_IDLE;
endcase
end
end
endmodule
+651
View File
@@ -0,0 +1,651 @@
`timescale 1ns/1ps
// ================================================================
// FLASH_SLOT_MANAGER
//
// Fixed-slot catalog layer on top of rtl/flash_copy_engine.v (F2/F3,
// reused UNCHANGED -- zero modifications, zero regression risk to
// its already-verified LOAD/SAVE paths). This is the module the
// flash-subsystem phase-plan's §4 describes: NOT a filesystem --
// a small fixed-size table mapping slot_id -> (offset, length,
// type, valid, CRC), read at boot and resolved before every
// LOAD_SLOT/SAVE_SLOT, no dynamic allocation, no GC.
//
// ----------------------------------------------------------------
// Catalog layout (matches tools/flash_catalog/oracle.py exactly --
// that script is the independent §A.1 oracle for both the byte
// layout and the CRC32 value; kept in sync by hand, cross-checked
// by the testbenches comparing actual bytes, not by either side
// trusting the other's prose)
// ----------------------------------------------------------------
// - N_SLOTS = 16 entries x ENTRY_BYTES = 16 bytes = 256 bytes
// total, comfortably inside one 4KB sector (CATALOG_SECTOR_ADDR,
// flash sector 0 -- RESERVED, never used for slot data).
// - Per-entry layout, MSB-first: offset[24b] | length[24b] |
// type[8b] | valid[8b] (0x01=valid, else invalid) | crc32[32b]
// | reserved[32b, always 0].
// - A freshly-erased catalog sector is all-0xFF, so every
// unwritten slot decodes as valid=0xFF != 0x01 => invalid,
// without needing any format/init step -- matches flash_model.v
// and the real Winbond erase behavior (§8.2.18 p.56).
//
// ----------------------------------------------------------------
// How the catalog gets to/from flash without touching PSRAM as user
// data (design decision, documented per the phase-plan's own
// "traccia ogni scelta" requirement):
// ----------------------------------------------------------------
// flash_copy_engine already knows how to move bytes flash<->PSRAM
// (F2/F3). Rather than teach it a THIRD destination (on-chip
// registers), this module stages the 256-byte catalog through a
// small reserved PSRAM region (CATALOG_PSRAM_ADDR) using
// flash_copy_engine's EXISTING, unmodified DIR_LOAD/DIR_SAVE, then
// itself reads/writes that staging region byte-by-byte via the
// SAME Port D connection flash_copy_engine uses -- safe because
// the two uses are TEMPORALLY DISJOINT by construction (this
// module's own sequential FSM never drives Port D itself while an
// internal flash_copy_engine operation is in flight, and vice
// versa): a static mux on `fce_busy` selects the owner, no new
// arbiter port needed.
// ASSUMPTION flagged (§A.5): CATALOG_PSRAM_ADDR is a convention,
// not enforced anywhere else -- nothing else in this design may
// use that PSRAM range while a catalog operation is in flight.
//
// ----------------------------------------------------------------
// CRC32 (rtl/crc32.v, IEEE 802.3/zlib -- verified independently
// against tools/flash_catalog/oracle.py + a textbook check-value,
// see sim/crc32_tb.v / WORKLOG.md's F4 entry) is computed by TAPPING
// flash_copy_engine's own Port D traffic while it is the active
// owner during a LOAD_SLOT or SAVE_SLOT (fce_d_wdata for a LOAD's
// PSRAM writes, fce_d_rdata for a SAVE's PSRAM reads) -- again zero
// modification to flash_copy_engine.v itself.
// ----------------------------------------------------------------
//
// Command interface:
// op_start, op_code (OP_CAT_READ / OP_CAT_WRITE_SLOT /
// OP_LOAD_SLOT / OP_SAVE_SLOT), slot_id, and per-opcode fields
// below. busy/done/err mirror flash_copy_engine's own convention.
//
// OP_CAT_READ: reloads the on-chip catalog register file from the
// flash catalog sector (e.g. at boot, or to force a refresh).
// OP_CAT_WRITE_SLOT: registers/updates slot_id's (offset, length,
// type) in the on-chip table, marks it INVALID (no verified
// data behind it yet -- SAVE_SLOT is what marks it valid), and
// persists the WHOLE catalog table to flash.
// OP_LOAD_SLOT(slot_id, psram_addr): resolves slot_id's offset+
// length from the on-chip catalog; if invalid, `err`+`done`
// immediately, no flash/PSRAM touched. Otherwise streams the
// slot's data flash->PSRAM (reusing flash_copy_engine's DIR_LOAD
// verbatim) while recomputing the CRC32 live; if it does not
// match the catalog's stored CRC, `err` is asserted (the data
// still lands in PSRAM -- the host is being told "unreliable",
// not given a transactional rollback, matching this project's
// existing STATUS-flag error convention rather than introducing
// a new one).
// OP_SAVE_SLOT(slot_id, psram_addr, length): resolves slot_id's
// already-registered offset (from a prior CAT_WRITE_SLOT);
// streams `length` bytes PSRAM->flash at that offset (reusing
// flash_copy_engine's DIR_SAVE verbatim) while computing the
// CRC32 live; on completion updates the on-chip entry (length,
// crc, valid=1) and persists the whole catalog to flash.
// ================================================================
module flash_slot_manager #(
parameter PSRAM_ADDR_WIDTH = 23,
parameter CLK_FREQ_MHZ = 80,
parameter SCLK_DIV = 2,
parameter [PSRAM_ADDR_WIDTH-1:0] CATALOG_PSRAM_ADDR = {PSRAM_ADDR_WIDTH{1'b0}}
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Physical flash pins (owns flash_copy_engine, which owns
// spi_flash_master -- same ownership chain as F2/F3)
// ------------------------------------------------------------
output wire mosi,
input wire miso,
output wire cs_n,
output wire sclk, // ordinary GPIO, real in both sim and synthesis
// ------------------------------------------------------------
// Command interface. op_code 0-3 are the F4 catalog/slot ops;
// 4-6 (added in F5) are raw, non-slot, non-CRC block ops that
// just forward straight to the internal flash_copy_engine --
// the phase-plan's §5 FLASH_READ_BLOCK/FLASH_WRITE_BLOCK/
// FLASH_ERASE, which take an explicit flash address rather than
// resolving one from the catalog.
// ------------------------------------------------------------
input wire op_start,
input wire [2:0] op_code,
input wire [3:0] slot_id,
input wire [23:0] new_offset, // CAT_WRITE_SLOT
input wire [23:0] new_length, // CAT_WRITE_SLOT
input wire [7:0] new_type, // CAT_WRITE_SLOT
input wire [PSRAM_ADDR_WIDTH-1:0] ext_psram_addr, // LOAD_SLOT / SAVE_SLOT / raw ops
input wire [23:0] ext_length, // SAVE_SLOT / raw block ops
input wire [23:0] raw_flash_addr, // FLASH_READ_BLOCK / FLASH_WRITE_BLOCK / FLASH_ERASE
output wire busy,
output reg done, // one-cycle pulse
output reg err, // held until next op_start
// ------------------------------------------------------------
// Catalog inspection (combinational read port, e.g. for a
// future CAT_READ SPI response -- F5 -- or this module's own
// testbench)
// ------------------------------------------------------------
input wire [3:0] cat_read_sel,
output wire [23:0] cat_out_offset,
output wire [23:0] cat_out_length,
output wire [7:0] cat_out_type,
output wire cat_out_valid,
output wire [31:0] cat_out_crc,
// ------------------------------------------------------------
// PSRAM arbiter master port (mem_arbiter.v Port D) -- muxed
// between the internal flash_copy_engine's own use and this
// module's own catalog-staging-buffer access, see header.
// ------------------------------------------------------------
output wire d_req,
output wire d_wr,
output wire [PSRAM_ADDR_WIDTH-1:0] d_addr,
output wire signed [7:0] d_wdata,
input wire signed [7:0] d_rdata,
input wire d_ready
);
localparam OP_CAT_READ = 3'd0;
localparam OP_CAT_WRITE_SLOT = 3'd1;
localparam OP_LOAD_SLOT = 3'd2;
localparam OP_SAVE_SLOT = 3'd3;
localparam OP_FLASH_READ_BLOCK = 3'd4; // F5, raw: flash -> PSRAM, explicit flash_addr
localparam OP_FLASH_WRITE_BLOCK = 3'd5; // F5, raw: PSRAM -> flash, explicit flash_addr
localparam OP_FLASH_ERASE = 3'd6; // F5, raw: standalone sector erase
localparam N_SLOTS = 16;
localparam ENTRY_BYTES = 16;
localparam CATALOG_BYTES = N_SLOTS * ENTRY_BYTES; // 256
localparam [23:0] CATALOG_SECTOR_ADDR = 24'h000000; // reserved, see header
localparam [7:0] VALID_MARK = 8'h01;
// ============================================================
// On-chip catalog register file
// ============================================================
reg [23:0] cat_offset [0:N_SLOTS-1];
reg [23:0] cat_length [0:N_SLOTS-1];
reg [7:0] cat_type [0:N_SLOTS-1];
reg cat_valid [0:N_SLOTS-1];
reg [31:0] cat_crc [0:N_SLOTS-1];
assign cat_out_offset = cat_offset[cat_read_sel];
assign cat_out_length = cat_length[cat_read_sel];
assign cat_out_type = cat_type[cat_read_sel];
assign cat_out_valid = cat_valid[cat_read_sel];
assign cat_out_crc = cat_crc[cat_read_sel];
// ============================================================
// flash_copy_engine instance (F2/F3 primitive, unmodified)
// ============================================================
reg fce_start;
reg [1:0] fce_dir;
reg [23:0] fce_flash_addr;
reg [PSRAM_ADDR_WIDTH-1:0] fce_psram_addr;
reg [23:0] fce_len;
wire fce_busy, fce_done, fce_err;
wire fce_d_req, fce_d_wr;
wire [PSRAM_ADDR_WIDTH-1:0] fce_d_addr;
wire signed [7:0] fce_d_wdata;
localparam FCE_DIR_LOAD = 2'd0;
localparam FCE_DIR_SAVE = 2'd1;
localparam FCE_DIR_ERASE = 2'd2;
flash_copy_engine #(
.PSRAM_ADDR_WIDTH(PSRAM_ADDR_WIDTH),
.CLK_FREQ_MHZ(CLK_FREQ_MHZ),
.SCLK_DIV(SCLK_DIV)
) u_fce (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n), .sclk(sclk),
.op_start(fce_start), .op_dir(fce_dir),
.flash_addr(fce_flash_addr), .psram_addr(fce_psram_addr), .len(fce_len),
.busy(fce_busy), .done(fce_done), .err(fce_err),
.d_req(fce_d_req), .d_wr(fce_d_wr), .d_addr(fce_d_addr), .d_wdata(fce_d_wdata),
.d_rdata(d_rdata), .d_ready(d_ready)
);
// ============================================================
// Port D mux: fce owns it whenever busy; otherwise this module
// drives it directly for catalog-staging-buffer access. See
// header for why this is safe without a new arbiter port.
// ============================================================
reg fsm_d_req;
reg fsm_d_wr;
reg [PSRAM_ADDR_WIDTH-1:0] fsm_d_addr;
reg signed [7:0] fsm_d_wdata;
assign d_req = fce_busy ? fce_d_req : fsm_d_req;
assign d_wr = fce_busy ? fce_d_wr : fsm_d_wr;
assign d_addr = fce_busy ? fce_d_addr : fsm_d_addr;
assign d_wdata = fce_busy ? fce_d_wdata : fsm_d_wdata;
// ============================================================
// CRC32 accumulator, tapped from fce's own Port D traffic while
// it is the active owner (see header). Active only during
// LOAD_SLOT/SAVE_SLOT's data-transfer phase (`crc_active`).
// ============================================================
reg crc_active;
reg [31:0] crc_acc;
wire [31:0] crc_next;
wire [7:0] crc_tap_byte = fce_d_wr ? fce_d_wdata : d_rdata; // LOAD writes vs SAVE reads
crc32_byte u_crc32 (
.crc_in(crc_acc),
.data(crc_tap_byte),
.crc_out(crc_next)
);
// NOTE: fce's own `d_req` is combinationally defined (in
// flash_copy_engine.v) as `(waiting_state) && !d_ready` -- so
// `fce_d_req && d_ready` is a contradiction, always false, and
// would never fire. The correct "a byte transfer through fce
// just completed" condition is simply `fce_busy && d_ready`: at
// that exact cycle fce's own state hasn't advanced out of its
// waiting state yet (same one-cycle-late FSM update reasoning
// documented in flash_copy_engine.v), so fce_busy is still 1,
// and this is the only reason d_ready would be high while fce
// owns Port D (see the mux above).
always @(posedge clk) begin
if (rst) begin
crc_acc <= 32'hFFFFFFFF;
end else if (crc_active && fce_busy && d_ready) begin
crc_acc <= crc_next;
end else if (!crc_active) begin
crc_acc <= 32'hFFFFFFFF; // re-armed for the next operation
end
end
// ============================================================
// Main FSM
// ============================================================
localparam ST_IDLE = 4'd0;
localparam ST_CATRD_FCE_GO = 4'd1;
localparam ST_CATRD_FCE_WAIT = 4'd2;
localparam ST_CATRD_RD_ISSUE = 4'd3;
localparam ST_CATRD_RD_WAIT = 4'd4;
localparam ST_CATWR_SER_ISSUE = 4'd5;
localparam ST_CATWR_SER_WAIT = 4'd6;
localparam ST_CATWR_FCE_GO = 4'd7;
localparam ST_CATWR_FCE_WAIT = 4'd8;
localparam ST_SLOT_FCE_GO = 4'd9;
localparam ST_SLOT_FCE_WAIT = 4'd10;
localparam ST_SLOT_CATWR_KICK = 4'd11; // SAVE_SLOT only: fall into the CAT_WRITE_SLOT persist sequence
localparam ST_DONE = 4'd15;
reg [3:0] state;
reg [8:0] byte_idx; // 0..255, position within the 256B catalog
reg [3:0] cur_slot; // slot_id of the entry currently being (de)serialized
reg [127:0] entry_shift; // 16-byte (de)serialization shift register, see header
reg [2:0] active_op;
reg [3:0] active_slot;
integer rst_i;
assign busy = (state != ST_IDLE);
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
done <= 1'b0;
err <= 1'b0;
fce_start <= 1'b0;
fce_dir <= 2'd0;
fce_flash_addr <= 24'h0;
fce_psram_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
fce_len <= 24'h0;
fsm_d_req <= 1'b0;
fsm_d_wr <= 1'b0;
fsm_d_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
fsm_d_wdata <= 8'sd0;
crc_active <= 1'b0;
byte_idx <= 9'h0;
cur_slot <= 4'h0;
entry_shift <= 128'h0;
active_op <= 2'd0;
active_slot <= 4'h0;
for (rst_i = 0; rst_i < N_SLOTS; rst_i = rst_i + 1) begin
cat_offset[rst_i] <= 24'h0;
cat_length[rst_i] <= 24'h0;
cat_type[rst_i] <= 8'h0;
cat_valid[rst_i] <= 1'b0;
cat_crc[rst_i] <= 32'h0;
end
end else begin
fce_start <= 1'b0;
fsm_d_req <= 1'b0;
done <= 1'b0;
case (state)
// ========================================
ST_IDLE: begin
if (op_start) begin
active_op <= op_code;
active_slot <= slot_id;
case (op_code)
OP_CAT_READ: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_LOAD;
fce_flash_addr <= CATALOG_SECTOR_ADDR;
fce_psram_addr <= CATALOG_PSRAM_ADDR;
fce_len <= CATALOG_BYTES[23:0];
state <= ST_CATRD_FCE_WAIT;
end
OP_CAT_WRITE_SLOT: begin
err <= 1'b0;
cat_offset[slot_id] <= new_offset;
cat_length[slot_id] <= new_length;
cat_type[slot_id] <= new_type;
cat_valid[slot_id] <= 1'b0; // no verified data yet, see header
cat_crc[slot_id] <= 32'h0;
byte_idx <= 9'h0;
cur_slot <= 4'h0;
state <= ST_CATWR_SER_ISSUE;
end
OP_LOAD_SLOT: begin
if (!cat_valid[slot_id]) begin
err <= 1'b1;
done <= 1'b1;
end else begin
err <= 1'b0;
crc_active <= 1'b1;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_LOAD;
fce_flash_addr <= cat_offset[slot_id];
fce_psram_addr <= ext_psram_addr;
fce_len <= cat_length[slot_id];
state <= ST_SLOT_FCE_WAIT;
end
end
OP_SAVE_SLOT: begin
err <= 1'b0;
crc_active <= 1'b1;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_SAVE;
fce_flash_addr <= cat_offset[slot_id]; // registered by a prior CAT_WRITE_SLOT
fce_psram_addr <= ext_psram_addr;
fce_len <= ext_length;
state <= ST_SLOT_FCE_WAIT;
end
// F5: raw block ops, no catalog/CRC
// involvement at all -- straight pass-
// through to flash_copy_engine with an
// explicit flash address from the host.
OP_FLASH_READ_BLOCK: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_LOAD;
fce_flash_addr <= raw_flash_addr;
fce_psram_addr <= ext_psram_addr;
fce_len <= ext_length;
state <= ST_SLOT_FCE_WAIT;
end
OP_FLASH_WRITE_BLOCK: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_SAVE;
fce_flash_addr <= raw_flash_addr;
fce_psram_addr <= ext_psram_addr;
fce_len <= ext_length;
state <= ST_SLOT_FCE_WAIT;
end
OP_FLASH_ERASE: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_ERASE;
fce_flash_addr <= raw_flash_addr;
state <= ST_SLOT_FCE_WAIT;
end
default: begin
err <= 1'b1;
done <= 1'b1;
end
endcase
end
end
// ========================================
// CAT_READ: load the catalog sector into the
// PSRAM staging buffer (fce, unmodified), then
// parse it 16 bytes (one entry) at a time.
// ========================================
ST_CATRD_FCE_WAIT: begin
if (fce_done) begin
byte_idx <= 9'h0;
state <= ST_CATRD_RD_ISSUE;
end
end
ST_CATRD_RD_ISSUE: begin
fsm_d_req <= 1'b1;
fsm_d_wr <= 1'b0;
fsm_d_addr <= CATALOG_PSRAM_ADDR + byte_idx;
state <= ST_CATRD_RD_WAIT;
end
ST_CATRD_RD_WAIT: begin
if (d_ready) begin
if (byte_idx[3:0] == 4'hF) begin
// 16th byte of this entry (bytes 12-15
// are reserved/ignored, so `d_rdata`
// itself -- this 16th byte -- never
// actually feeds any decoded field, only
// the PRE-update `entry_shift`, which at
// this point holds bytes 0-14 from the
// 15 shifts so far: entry_shift[119:112]
// = byte0 ... entry_shift[7:0] = byte14.
// See the module header for the full
// derivation.
cat_offset[byte_idx[8:4]] <= entry_shift[119:96]; // bytes 0-2
cat_length[byte_idx[8:4]] <= entry_shift[95:72]; // bytes 3-5
cat_type[byte_idx[8:4]] <= entry_shift[71:64]; // byte 6
cat_valid[byte_idx[8:4]] <= (entry_shift[63:56] == VALID_MARK); // byte 7
cat_crc[byte_idx[8:4]] <= entry_shift[55:24]; // bytes 8-11
end
entry_shift <= {entry_shift[119:0], d_rdata};
if (byte_idx == 9'd255) begin
state <= ST_DONE;
end else begin
byte_idx <= byte_idx + 9'd1;
state <= ST_CATRD_RD_ISSUE;
end
end
end
// ========================================
// CAT_WRITE_SLOT: serialize all N_SLOTS entries
// (from the just-updated on-chip table) into the
// PSRAM staging buffer, then persist via fce's
// unmodified DIR_SAVE (erase + page-program loop
// + WIP poll, all F3).
// ========================================
ST_CATWR_SER_ISSUE: begin
fsm_d_req <= 1'b1;
fsm_d_wr <= 1'b1;
fsm_d_addr <= CATALOG_PSRAM_ADDR + byte_idx;
// Byte to send: byte_idx[3:0] selects which of the
// 16 bytes of cur_slot's entry, MSB-first, same
// layout as the CAT_READ decode.
case (byte_idx[3:0])
4'h0: fsm_d_wdata <= cat_offset[cur_slot][23:16];
4'h1: fsm_d_wdata <= cat_offset[cur_slot][15:8];
4'h2: fsm_d_wdata <= cat_offset[cur_slot][7:0];
4'h3: fsm_d_wdata <= cat_length[cur_slot][23:16];
4'h4: fsm_d_wdata <= cat_length[cur_slot][15:8];
4'h5: fsm_d_wdata <= cat_length[cur_slot][7:0];
4'h6: fsm_d_wdata <= cat_type[cur_slot];
4'h7: fsm_d_wdata <= cat_valid[cur_slot] ? VALID_MARK : 8'h00;
4'h8: fsm_d_wdata <= cat_crc[cur_slot][31:24];
4'h9: fsm_d_wdata <= cat_crc[cur_slot][23:16];
4'hA: fsm_d_wdata <= cat_crc[cur_slot][15:8];
4'hB: fsm_d_wdata <= cat_crc[cur_slot][7:0];
default: fsm_d_wdata <= 8'h00; // reserved, bytes C-F
endcase
state <= ST_CATWR_SER_WAIT;
end
ST_CATWR_SER_WAIT: begin
if (d_ready) begin
if (byte_idx == 9'd255) begin
fce_start <= 1'b1;
fce_dir <= FCE_DIR_SAVE;
fce_flash_addr <= CATALOG_SECTOR_ADDR;
fce_psram_addr <= CATALOG_PSRAM_ADDR;
fce_len <= CATALOG_BYTES[23:0];
state <= ST_CATWR_FCE_WAIT;
end else begin
byte_idx <= byte_idx + 9'd1;
cur_slot <= (byte_idx[3:0] == 4'hF) ? (byte_idx[8:4] + 4'd1) : byte_idx[8:4];
state <= ST_CATWR_SER_ISSUE;
end
end
end
ST_CATWR_FCE_WAIT: begin
// Reached either directly from a CAT_WRITE_SLOT
// op, or via ST_SLOT_CATWR_KICK after a
// SAVE_SLOT's own data transfer already updated
// the catalog entry -- either way, this state is
// just "persist the catalog sector, then done".
if (fce_done)
state <= ST_DONE;
end
// ========================================
// LOAD_SLOT / SAVE_SLOT: run fce's existing
// DIR_LOAD/DIR_SAVE verbatim, CRC accumulated via
// the tap above.
// ========================================
ST_SLOT_FCE_WAIT: begin
if (fce_done) begin
crc_active <= 1'b0;
case (active_op)
OP_LOAD_SLOT: begin
// fce_err here would mean the
// catalog's own (offset,length) is
// somehow out of range -- shouldn't
// happen for a slot that passed the
// CAT_WRITE_SLOT/SAVE_SLOT bounds
// checks, but checked anyway rather
// than assumed.
if (fce_err || ((crc_acc ^ 32'hFFFFFFFF) != cat_crc[active_slot]))
err <= 1'b1;
state <= ST_DONE;
end
OP_SAVE_SLOT: begin
// BUG FIXED HERE (found during F5
// integration review, before ever
// running the F5 testbench): this
// branch previously updated+persisted
// the catalog as "valid" unconditionally
// on fce_done, without checking
// fce_err first -- a SAVE_SLOT whose
// underlying flash_copy_engine call
// itself failed (e.g. an out-of-range
// length) would still have been
// marked valid in the catalog. Now
// skips the catalog update entirely
// on a failed transfer.
if (fce_err) begin
err <= 1'b1;
state <= ST_DONE;
end else begin
cat_length[active_slot] <= fce_len;
cat_crc[active_slot] <= crc_acc ^ 32'hFFFFFFFF;
cat_valid[active_slot] <= 1'b1;
byte_idx <= 9'h0;
cur_slot <= 4'h0;
state <= ST_SLOT_CATWR_KICK;
end
end
default: begin
// F5 raw block ops (FLASH_READ_BLOCK/
// FLASH_WRITE_BLOCK/FLASH_ERASE): no
// catalog/CRC involvement, just
// forward fce's own error status.
err <= fce_err;
state <= ST_DONE;
end
endcase
end
end
// One-cycle bridge: the cat_* writes above need a
// cycle to land before ST_CATWR_SER_ISSUE reads them
// back out for serialization.
ST_SLOT_CATWR_KICK: begin
state <= ST_CATWR_SER_ISSUE;
end
// ========================================
ST_DONE: begin
done <= 1'b1;
state <= ST_IDLE;
end
default: state <= ST_IDLE;
endcase
end
end
endmodule
+611
View File
@@ -0,0 +1,611 @@
`timescale 1ns/1ps
// ================================================================
// GRAPH_ENGINE (Phase G3 -- Type #2 sparse-graph network)
//
// Orchestrates an arbitrary feed-forward DAG of neurons on top of
// the validated neuron_parallel core, exactly the way neuron_memory
// / layer_sequencer orchestrate the dense Type #1 path -- the
// compute datapath itself (neuron_parallel/mac8/mac_unit) is
// instantiated unmodified, never edited. See docs spec §2-§7 for
// the full design rationale; this header only covers what a reader
// of THIS file needs.
//
// REGISTER REUSE (no new SET_BASE selectors beyond sel 9/10):
// dense (#1) and graph (#2) modes are mutually exclusive at runtime
// (net_type dispatch, see spi_engine.v), so this module reuses three
// existing SET_BASE-driven registers for a different purpose while
// in graph mode -- documented here since it is not spelled out
// verbatim in the data-format spec:
// x_base -> base address of the N_in input bytes in PSRAM
// (same register/opcode host already uses to
// preload inputs for dense mode)
// table_base -> base of the graph descriptor table (§4.2),
// instead of the dense layer descriptor table
// buf_a_base -> out_base: where the n_out output bytes are
// copied to in PSRAM at the end of the run
// (dense mode's ping-pong buffer A is unused
// while graph mode runs, so no conflict)
// n_inputs_real -> N_in: number of graph input ids (0..N_in-1),
// instead of a dense layer's real input count
//
// OUTPUT COPY, FOLDED INTO THE MAIN LOOP: §6 describes a WRITE_OUTPUTS
// state as a separate pass after all neurons are computed. Since the
// spec's own out_id ordering guarantee (§4.4: "out_ids ... finiscono
// naturalmente con gli id piu alti") makes the output set EXACTLY
// the last n_out entries of the descriptor table, this engine copies
// a neuron's result to out_base the moment it computes it, if that
// neuron is one of the last n_out (WRITE_ACT / WRITE_OUT below) --
// no second pass over act_buf or extra id bookkeeping needed.
//
// GUARD (§7): a load-time check that src_id < out_id and
// src_id < N_TOTAL is done per-edge, right as each edge's src_id is
// read. On violation, `err` is raised (sticky until `rst`) and the
// run stops immediately -- no further memory traffic, no result
// written -- instead of silently computing a wrong answer. This
// module ALSO checks out_id < N_TOTAL and n_conn_padded (post-
// PARALLEL-padding) <= MAX_CONN for the same reason (out-of-range
// addressing / MAX_CONN overflow are just as unsafe as the two
// checks the spec calls out by name, and "stop instead of silently
// misbehaving" is the whole point of this section) -- an addition
// beyond the literal spec text, noted here for anyone diffing
// against it.
//
// PARALLEL-multiple padding (§2.6) is entirely a HOST/assembler
// responsibility: the edge blocks in PSRAM already physically
// contain the zero-weight padding edges tools/netasm inserts, so
// this engine just reads n_conn_padded = ceil(n_conn/PARALLEL)*
// PARALLEL physical edges per neuron -- no special-casing here, and
// neuron_parallel's own n_inputs_real convention (must already be a
// PARALLEL multiple) is satisfied by construction. A neuron with 0
// real connections still needs padding to a full PARALLEL group from
// the host: n_conn_padded==0 is treated as a load-time error (see
// guard above) rather than being forwarded to neuron_parallel, which
// would hang on it (documented hang mode of neuron_parallel/GROUPS=0,
// see rtl/neuron_parallel.v).
//
// ACTIVATION BUFFER GATHER TIMING: act_buffer's read port is
// registered (1-cycle latency, see rtl/act_buffer.v). This engine
// drives act_buf's read address directly (combinationally) from the
// edge's src_id as soon as it is known (after the edge's 2nd byte),
// and only consumes rd_data one FULL state later (ST_GATHER), by
// which point at least two more PSRAM byte round-trips (weight +
// reserved bytes of the same edge) have elapsed -- comfortably more
// than the 1 cycle act_buffer needs, on any realistic memory. See
// the ST_EDGE_WAIT / ST_GATHER case comments below.
// ================================================================
module graph_engine #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 8,
parameter ACC_WIDTH = 32,
parameter PARALLEL = 8,
parameter MAX_CONN = 32, // build-time max real+padding connections per neuron (must be a PARALLEL multiple -- enforced by neuron_parallel's own elaboration-time guard)
parameter N_TOTAL = 4096 // activation buffer depth / signal id space ceiling (§2)
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Trigger / status (from spi_engine's RUN_NETWORK dispatch when
// net_type == graph)
// ------------------------------------------------------------
input wire run_start, // one-cycle pulse
output reg busy,
output reg done, // one-cycle pulse
output reg err, // sticky guard-violation flag (§7), cleared on rst or the next run_start
// ------------------------------------------------------------
// Config registers (see REGISTER REUSE note above for x_base /
// table_base / buf_a_base / n_inputs_real)
// ------------------------------------------------------------
input wire [ADDR_WIDTH-1:0] x_base,
input wire [ADDR_WIDTH-1:0] table_base,
input wire [ADDR_WIDTH-1:0] out_base,
input wire [15:0] n_inputs_graph, // N_in
input wire [15:0] num_neurons_graph, // SET_BASE sel 9
input wire [15:0] n_out, // SET_BASE sel 10
// ------------------------------------------------------------
// Byte-level RAM master port (own arbiter port; shared with
// layer_sequencer's at the top level -- the two run modes are
// mutually exclusive, see rtl/spi_neuron_top.v)
// ------------------------------------------------------------
output reg ram_req,
output reg ram_wr,
output reg [ADDR_WIDTH-1:0] ram_addr,
output reg signed [7:0] ram_wdata,
input wire signed [7:0] ram_rdata,
input wire ram_ready
);
// ============================================================
// DERIVED WIDTHS
// ============================================================
localparam BUF_ADDR_WIDTH = $clog2(N_TOTAL);
localparam CONN_IDX_WIDTH = (MAX_CONN <= 1) ? 1 : $clog2(MAX_CONN + 1);
localparam PAR_SHIFT = $clog2(PARALLEL);
// ============================================================
// STATES / FSM REGISTERS
//
// Declared before the submodule instantiations below because
// act_buffer's read address is wired directly (combinationally)
// to src_id_acc.
// ============================================================
localparam ST_IDLE = 4'd0;
localparam ST_COPY_IN_RD = 4'd1;
localparam ST_COPY_IN_WAIT = 4'd2;
localparam ST_DESC_RD = 4'd3;
localparam ST_DESC_WAIT = 4'd4;
localparam ST_EDGE_RD = 4'd5;
localparam ST_EDGE_WAIT = 4'd6;
localparam ST_GATHER = 4'd7;
localparam ST_START_N = 4'd8;
localparam ST_WAIT_N = 4'd9;
localparam ST_WRITE_OUT_ISS = 4'd10;
localparam ST_WRITE_OUT_WAIT = 4'd11;
localparam ST_ERROR = 4'd12;
reg [3:0] state;
reg [15:0] in_idx;
reg [15:0] neuron_idx;
reg [ADDR_WIDTH-1:0] desc_addr;
reg [3:0] desc_byte_idx;
reg [23:0] conn_ptr_acc;
reg [15:0] n_conn_acc;
reg [15:0] out_id_acc;
reg [CONN_IDX_WIDTH-1:0] conn_i;
reg [1:0] edge_byte_idx;
reg [15:0] src_id_acc;
reg [7:0] weight_acc;
// Last n_out descriptor-table entries are the output sinks (§4.4):
// entries are in ascending out_id order, so this is a static
// range check on neuron_idx, no separate id lookup needed.
wire [15:0] out_threshold = num_neurons_graph - n_out;
wire is_output_sink = (neuron_idx >= out_threshold);
wire [15:0] out_offset = neuron_idx - out_threshold;
// ============================================================
// ACTIVATION BUFFER (private to this engine -- dense mode does
// not use it at all)
// ============================================================
reg act_wr_en;
reg [BUF_ADDR_WIDTH-1:0] act_wr_addr;
reg signed [DATA_WIDTH-1:0] act_wr_data;
wire signed [DATA_WIDTH-1:0] act_rd_data;
act_buffer #(
.N_TOTAL(N_TOTAL),
.DATA_WIDTH(DATA_WIDTH)
) u_act_buffer (
.clk(clk),
.wr_en(act_wr_en), .wr_addr(act_wr_addr), .wr_data(act_wr_data),
.rd_addr(src_id_acc[BUF_ADDR_WIDTH-1:0]), .rd_data(act_rd_data)
);
// ============================================================
// NEURON (private instance, reused across every graph neuron --
// same "one neuron computed at a time, memory-bound" design as
// neuron_memory.v)
// ============================================================
reg signed [DATA_WIDTH-1:0] x_mem [0:MAX_CONN-1];
reg signed [DATA_WIDTH-1:0] w_mem [0:MAX_CONN-1];
wire signed [DATA_WIDTH*MAX_CONN-1:0] x_bus;
wire signed [DATA_WIDTH*MAX_CONN-1:0] w_bus;
genvar gi;
generate
for (gi = 0; gi < MAX_CONN; gi = gi + 1) begin : GEN_BUS
assign x_bus[gi*DATA_WIDTH +: DATA_WIDTH] = x_mem[gi];
assign w_bus[gi*DATA_WIDTH +: DATA_WIDTH] = w_mem[gi];
end
endgenerate
reg neuron_start;
reg signed [7:0] bias_reg;
reg [1:0] activation_reg;
reg [15:0] n_conn_padded_reg;
wire signed [7:0] neuron_y;
wire neuron_busy;
wire neuron_done;
neuron_parallel #(
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(MAX_CONN),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH)
) u_neuron (
.clk(clk), .rst(rst), .start(neuron_start),
.x_bus(x_bus), .w_bus(w_bus), .bias(bias_reg),
.activation(activation_reg),
.n_inputs_real(n_conn_padded_reg),
.y(neuron_y), .busy(neuron_busy), .done(neuron_done)
);
// ============================================================
// MAIN FSM
// ============================================================
integer ri;
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
busy <= 1'b0;
done <= 1'b0;
err <= 1'b0;
in_idx <= 16'h0;
neuron_idx <= 16'h0;
desc_addr <= {ADDR_WIDTH{1'b0}};
desc_byte_idx <= 4'h0;
conn_ptr_acc <= 24'h0;
n_conn_acc <= 16'h0;
out_id_acc <= 16'h0;
n_conn_padded_reg <= 16'h0;
conn_i <= {CONN_IDX_WIDTH{1'b0}};
edge_byte_idx <= 2'h0;
src_id_acc <= 16'h0;
weight_acc <= 8'h0;
bias_reg <= 8'sd0;
activation_reg <= 2'd1;
act_wr_en <= 1'b0;
act_wr_addr <= {BUF_ADDR_WIDTH{1'b0}};
act_wr_data <= 8'sd0;
neuron_start <= 1'b0;
ram_req <= 1'b0;
ram_wr <= 1'b0;
ram_addr <= {ADDR_WIDTH{1'b0}};
ram_wdata <= 8'sd0;
for (ri = 0; ri < MAX_CONN; ri = ri + 1) begin
x_mem[ri] <= 8'sd0;
w_mem[ri] <= 8'sd0;
end
end else begin
// --------------------------------------------------
// Default pulses
// --------------------------------------------------
ram_req <= 1'b0;
act_wr_en <= 1'b0;
neuron_start <= 1'b0;
done <= 1'b0;
case (state)
// =============================================
// IDLE / ERROR: both accept run_start identically.
// A fresh run_start clears a stale `err` and gives
// the engine another attempt.
// =============================================
ST_IDLE, ST_ERROR: begin
if (run_start) begin
busy <= 1'b1;
err <= 1'b0;
in_idx <= 16'h0;
neuron_idx <= 16'h0;
desc_addr <= table_base;
state <= ST_COPY_IN_RD;
end
end
// =============================================
// COPY_INPUTS: act_buf[0..N_in-1] <- PSRAM[x_base..]
// =============================================
ST_COPY_IN_RD: begin
ram_req <= 1'b1;
ram_wr <= 1'b0;
ram_addr <= x_base + in_idx;
state <= ST_COPY_IN_WAIT;
end
ST_COPY_IN_WAIT: begin
if (ram_ready) begin
act_wr_en <= 1'b1;
act_wr_addr <= in_idx[BUF_ADDR_WIDTH-1:0];
act_wr_data <= ram_rdata;
if (in_idx == n_inputs_graph - 16'd1) begin
// BUG-006 fix (docs/validation/bugs.md):
// num_neurons_graph==0 has no neuron to
// process at all -- the original code
// always computed at least "neuron 0"
// before its own termination check
// (`neuron_idx == num_neurons_graph-1`,
// further down) could even run, and that
// check had the same unguarded-wraparound
// structure as BUG-002/003/004/005. Report
// done immediately instead of entering the
// descriptor-read loop (real-edge-guard
// protection incidentally limited the
// practical damage here, per
// docs/validation/06-graph-engine.md, but
// the structural gap is closed properly
// now rather than left to that
// side-effect).
if (num_neurons_graph == 16'h0) begin
busy <= 1'b0;
done <= 1'b1;
state <= ST_IDLE;
end else begin
desc_byte_idx <= 4'h0;
state <= ST_DESC_RD;
end
end else begin
in_idx <= in_idx + 16'd1;
state <= ST_COPY_IN_RD;
end
end
end
// =============================================
// READ_DESC: 11-byte graph descriptor (§4.2)
// =============================================
ST_DESC_RD: begin
ram_req <= 1'b1;
ram_wr <= 1'b0;
ram_addr <= desc_addr + desc_byte_idx;
state <= ST_DESC_WAIT;
end
ST_DESC_WAIT: begin
if (ram_ready) begin
case (desc_byte_idx)
4'd0: conn_ptr_acc[23:16] <= ram_rdata;
4'd1: conn_ptr_acc[15:8] <= ram_rdata;
4'd2: conn_ptr_acc[7:0] <= ram_rdata;
4'd3: n_conn_acc[15:8] <= ram_rdata;
4'd4: n_conn_acc[7:0] <= ram_rdata;
4'd5: out_id_acc[15:8] <= ram_rdata;
4'd6: out_id_acc[7:0] <= ram_rdata;
4'd7: activation_reg <= ram_rdata[1:0];
4'd8: bias_reg <= ram_rdata;
default: ; // reserved bytes 9-10, ignored
endcase
if (desc_byte_idx == 4'd10) begin
// n_conn_acc is fully committed (bytes
// 3-4, several cycles ago) by now.
n_conn_padded_reg <=
((n_conn_acc + (PARALLEL - 1)) >> PAR_SHIFT) << PAR_SHIFT;
conn_i <= {CONN_IDX_WIDTH{1'b0}};
edge_byte_idx <= 2'h0;
state <= ST_EDGE_RD;
end else begin
desc_byte_idx <= desc_byte_idx + 4'd1;
state <= ST_DESC_RD;
end
end
end
// =============================================
// EDGE stream + gather (§4.3, §7)
// =============================================
ST_EDGE_RD: begin
// n_conn_padded_reg == 0 means the host sent a
// neuron with no padded connections at all --
// would forward n_inputs_real=0 to neuron_parallel,
// which hangs (GROUPS=0 failure mode). Treated as
// a load-time error instead (§7 rationale).
if (n_conn_padded_reg == 16'h0) begin
err <= 1'b1;
busy <= 1'b0;
state <= ST_ERROR;
end else begin
ram_req <= 1'b1;
ram_wr <= 1'b0;
ram_addr <= conn_ptr_acc[ADDR_WIDTH-1:0] + (conn_i * 4) + edge_byte_idx;
state <= ST_EDGE_WAIT;
end
end
ST_EDGE_WAIT: begin
if (ram_ready) begin
case (edge_byte_idx)
2'd0: src_id_acc[15:8] <= ram_rdata;
2'd1: src_id_acc[7:0] <= ram_rdata;
2'd2: weight_acc <= ram_rdata;
default: ; // reserved byte, ignored
endcase
if (edge_byte_idx == 2'd3) begin
edge_byte_idx <= 2'h0;
// §7 guard: src_id must be a strictly
// earlier-computed signal, and both ids
// must fit the activation buffer.
if (src_id_acc >= out_id_acc ||
src_id_acc >= N_TOTAL[15:0] ||
out_id_acc >= N_TOTAL[15:0]) begin
err <= 1'b1;
busy <= 1'b0;
state <= ST_ERROR;
end else begin
// act_buf.rd_addr is wired directly
// to src_id_acc (see instantiation
// above); it has been stable since
// this cycle already (both bytes of
// src_id are committed) and will
// still be stable through the
// ST_GATHER cycle below.
state <= ST_GATHER;
end
end else begin
edge_byte_idx <= edge_byte_idx + 2'd1;
state <= ST_EDGE_RD;
end
end
end
// =============================================
// GATHER: consume act_buf's registered read data
// (valid: rd_addr has been stable since the START
// of this same edge's byte 1, i.e. well over a full
// PSRAM byte round-trip ago -- see header comment).
// =============================================
ST_GATHER: begin
x_mem[conn_i] <= act_rd_data;
w_mem[conn_i] <= $signed(weight_acc);
if (conn_i == n_conn_padded_reg[CONN_IDX_WIDTH-1:0] - 1'b1) begin
conn_i <= {CONN_IDX_WIDTH{1'b0}};
state <= ST_START_N;
end else begin
conn_i <= conn_i + 1'b1;
state <= ST_EDGE_RD;
end
end
// =============================================
// START / WAIT neuron_parallel
// =============================================
ST_START_N: begin
neuron_start <= 1'b1;
state <= ST_WAIT_N;
end
ST_WAIT_N: begin
if (neuron_done) begin
act_wr_en <= 1'b1;
act_wr_addr <= out_id_acc[BUF_ADDR_WIDTH-1:0];
act_wr_data <= neuron_y;
if (is_output_sink) begin
state <= ST_WRITE_OUT_ISS;
end else if (neuron_idx == num_neurons_graph - 16'd1) begin
busy <= 1'b0;
done <= 1'b1;
state <= ST_IDLE;
end else begin
neuron_idx <= neuron_idx + 16'd1;
desc_addr <= desc_addr + 11;
desc_byte_idx <= 4'h0;
state <= ST_DESC_RD;
end
end
end
// =============================================
// Fold WRITE_OUTPUTS into the loop for sink neurons
// =============================================
ST_WRITE_OUT_ISS: begin
ram_req <= 1'b1;
ram_wr <= 1'b1;
ram_addr <= out_base + out_offset;
ram_wdata <= neuron_y;
state <= ST_WRITE_OUT_WAIT;
end
ST_WRITE_OUT_WAIT: begin
if (ram_ready) begin
if (neuron_idx == num_neurons_graph - 16'd1) begin
busy <= 1'b0;
done <= 1'b1;
state <= ST_IDLE;
end else begin
neuron_idx <= neuron_idx + 16'd1;
desc_addr <= desc_addr + 11;
desc_byte_idx <= 4'h0;
state <= ST_DESC_RD;
end
end
end
default: begin
state <= ST_IDLE;
end
endcase
end
end
endmodule
+179
View File
@@ -0,0 +1,179 @@
module int8_memory_access #(
parameter ADDR_WIDTH = 23
)(
input wire clk,
input wire rst,
// ============================================================
// INT8 interface
//
// addr is BYTE address
// ============================================================
input wire req,
input wire wr,
input wire [ADDR_WIDTH-1:0] addr,
input wire signed [7:0] wdata,
output reg signed [7:0] rdata,
output reg ready,
// ============================================================
// 16-bit memory interface
// ============================================================
output reg mem_req,
output reg mem_wr,
output reg [ADDR_WIDTH-1:0] mem_addr,
output reg [15:0] mem_wdata,
output reg mem_lb_n,
output reg mem_ub_n,
input wire [15:0] mem_rdata,
input wire mem_ready
);
// ============================================================
// State machine
// ============================================================
localparam STATE_IDLE = 2'd0;
localparam STATE_WAIT = 2'd1;
reg [1:0] state;
// ============================================================
// Latched byte address
// ============================================================
reg [ADDR_WIDTH-1:0] addr_reg;
// ============================================================
// Main state machine
// ============================================================
always @(posedge clk) begin
if (rst) begin
state <= STATE_IDLE;
addr_reg <= {ADDR_WIDTH{1'b0}};
rdata <= 8'sd0;
ready <= 1'b0;
mem_req <= 1'b0;
mem_wr <= 1'b0;
mem_addr <= {ADDR_WIDTH{1'b0}};
mem_wdata <= 16'h0000;
// Active-low byte enables:
// 1 = disabled
mem_lb_n <= 1'b1;
mem_ub_n <= 1'b1;
end else begin
// ready is a one-cycle pulse
ready <= 1'b0;
// mem_req is a one-cycle pulse
mem_req <= 1'b0;
case (state)
// =================================================
// IDLE
// =================================================
STATE_IDLE: begin
if (req) begin
addr_reg <= addr;
mem_req <= 1'b1;
mem_wr <= wr;
// ------------------------------------------------
// Byte address -> 16-bit word address
//
// addr[0] = 0 -> low byte
// addr[0] = 1 -> high byte
// ------------------------------------------------
mem_addr <= addr >> 1;
// ------------------------------------------------
// Select byte
// ------------------------------------------------
if (addr[0] == 1'b0) begin
// Low byte
mem_lb_n <= 1'b0;
mem_ub_n <= 1'b1;
// Data goes into DQ[7:0]
mem_wdata <= {8'h00, wdata};
end else begin
// High byte
mem_lb_n <= 1'b1;
mem_ub_n <= 1'b0;
// Data goes into DQ[15:8]
mem_wdata <= {wdata, 8'h00};
end
state <= STATE_WAIT;
end
end
// =================================================
// WAIT
// =================================================
STATE_WAIT: begin
if (mem_ready) begin
// ------------------------------------------------
// Extract requested byte
// ------------------------------------------------
if (addr_reg[0] == 1'b0)
rdata <= mem_rdata[7:0];
else
rdata <= mem_rdata[15:8];
ready <= 1'b1;
state <= STATE_IDLE;
end
end
// =================================================
// Default
// =================================================
default: begin
state <= STATE_IDLE;
mem_req <= 1'b0;
mem_lb_n <= 1'b1;
mem_ub_n <= 1'b1;
end
endcase
end
end
endmodule
+81
View File
@@ -0,0 +1,81 @@
module layer #(
parameter DATA_WIDTH = 16,
parameter N_INPUTS = 64,
parameter N_NEURONS = 8,
parameter PARALLEL = 8,
parameter ACC_WIDTH = 40
)(
input clk,
input rst,
input start,
input signed [DATA_WIDTH*N_INPUTS-1:0] x_bus,
input signed [DATA_WIDTH*N_INPUTS*N_NEURONS-1:0]
weights_bus,
input signed [DATA_WIDTH*N_NEURONS-1:0]
bias_bus,
output signed [DATA_WIDTH*N_NEURONS-1:0]
y_bus,
output busy,
output done
);
wire [N_NEURONS-1:0] neuron_busy;
wire [N_NEURONS-1:0] neuron_done;
genvar n;
generate
for (n = 0; n < N_NEURONS; n = n + 1) begin : GEN_NEURON
neuron_parallel #(
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH)
) u_neuron (
.clk(clk),
.rst(rst),
.start(start),
.x_bus(x_bus),
.w_bus(
weights_bus[
n*N_INPUTS*DATA_WIDTH
+: N_INPUTS*DATA_WIDTH
]
),
.bias(
bias_bus[
n*DATA_WIDTH
+: DATA_WIDTH
]
),
.y(
y_bus[
n*DATA_WIDTH
+: DATA_WIDTH
]
),
.busy(neuron_busy[n]),
.done(neuron_done[n])
);
end
endgenerate
assign busy = |neuron_busy;
assign done = &neuron_done;
endmodule
+365
View File
@@ -0,0 +1,365 @@
`timescale 1ns/1ps
// ================================================================
// LAYER_SEQUENCER (Phase 5 - Multi-Layer Network)
//
// Chains up to N_LAYERS runs of a single, reused neuron_memory
// instance to execute a feedforward network of N_LAYERS dense
// layers, without touching neuron_memory.v or the validated compute
// core (neuron_parallel/mac8/mac_unit) at all.
//
// KEY DESIGN CHOICE: neuron_memory's N_INPUTS and N_NEURONS are both
// fixed at synthesis to the SAME value (this module's N_WIDTH
// parameter, e.g. 256). A logical layer with fewer real inputs or
// neurons than N_WIDTH is handled entirely by DATA convention, not
// RTL: the host zero-pads that layer's weight matrix beyond its
// real input count (so the extra MAC lanes contribute 0 regardless
// of input value) and its bias beyond its real neuron count. This
// sequencer then always reads/writes the FULL N_WIDTH-byte buffer
// for every layer transition -- it does not need to know any
// layer's "real" input/neuron count at all. Trade-off: a layer with
// few real inputs still takes as long as a full N_WIDTH-wide layer
// (wasted MAC cycles on zero-weighted padding); documented as a
// known Phase 7 (Optimization) follow-up, not solved here.
//
// Layer descriptor table (host-written via WRITE_RAM, read-only to
// this module): N_LAYERS entries of 11 bytes each, MSB-first,
// starting at `table_base`:
// w_base(3B), bias_addr(3B), activation(1B, low 2 bits --
// see rtl/neuron_parallel.v's ACT_* localparams),
// n_inputs_real(2B), n_neurons_real(2B)
// Layer 0's input is the external `x_base` (same register used for
// single-layer/manual mode). Layer k>0's input is the ping-pong
// output buffer (`buf_a_base`/`buf_b_base`) written by layer k-1.
// The final layer's output is left both in neuron_memory's own
// y_bus (readable via the existing READ_OUTPUT opcode, unchanged)
// and in the ping-pong buffer it was copied to.
//
// n_inputs_real/n_neurons_real let ONE synthesized bitstream (fixed
// N_WIDTH = neuron_memory's build-time max) serve any real network
// topology up to that width: forwarded to neuron_memory verbatim
// (see rtl/neuron_memory.v), and this sequencer copies exactly
// n_neurons_real bytes of y_bus into the ping-pong buffer -- NOT the
// full N_WIDTH -- so a narrower layer both computes AND is copied
// out faster, no zero-padding required in RAM.
// ================================================================
module layer_sequencer #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 8,
parameter N_WIDTH = 256, // = neuron_memory's N_INPUTS = N_NEURONS
parameter N_LAYERS = 4
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Trigger (from spi_engine's RUN_NETWORK opcode)
// ------------------------------------------------------------
input wire run_start, // one-cycle pulse
input wire [7:0] run_num_layers, // 1..N_LAYERS
output reg seq_busy,
output reg seq_done, // one-cycle pulse, mirrors neuron_memory.done
// ------------------------------------------------------------
// Config registers (from spi_engine's SET_BASE)
// ------------------------------------------------------------
input wire [ADDR_WIDTH-1:0] x_base, // layer 0's external input
input wire [ADDR_WIDTH-1:0] table_base,
input wire [ADDR_WIDTH-1:0] buf_a_base,
input wire [ADDR_WIDTH-1:0] buf_b_base,
// ------------------------------------------------------------
// neuron_memory control (sequencer-owned; only meaningful while
// seq_busy -- the top level muxes these against spi_engine's
// own direct-drive outputs based on seq_busy)
// ------------------------------------------------------------
output reg [ADDR_WIDTH-1:0] nm_x_base,
output reg [ADDR_WIDTH-1:0] nm_w_base,
output reg [ADDR_WIDTH-1:0] nm_bias_addr,
output reg [1:0] nm_activation,
output reg [15:0] nm_n_inputs,
output reg [15:0] nm_n_neurons,
output reg nm_start,
input wire nm_busy,
input wire nm_done, // one-cycle pulse
input wire signed [DATA_WIDTH*N_WIDTH-1:0] y_bus,
// ------------------------------------------------------------
// Byte-level RAM master port (own arbiter port)
// ------------------------------------------------------------
output reg ram_req,
output reg ram_wr,
output reg [ADDR_WIDTH-1:0] ram_addr,
output reg signed [7:0] ram_wdata,
input wire signed [7:0] ram_rdata,
input wire ram_ready
);
// ============================================================
// STATES
// ============================================================
localparam ST_IDLE = 4'd0;
localparam ST_READ_DESC = 4'd1;
localparam ST_READ_WAIT = 4'd2;
localparam ST_START_LAYER = 4'd3;
localparam ST_WAIT_LAYER = 4'd4;
localparam ST_COPY_ISSUE = 4'd5;
localparam ST_COPY_WAIT = 4'd6;
reg [3:0] state;
reg [7:0] layer_idx;
reg [7:0] num_layers_reg;
reg [ADDR_WIDTH-1:0] desc_table_addr;
reg [3:0] desc_byte_idx; // 0..10
reg [23:0] w_base_acc;
reg [23:0] bias_addr_acc;
reg [7:0] activation_acc;
reg [15:0] n_inputs_acc;
reg [15:0] n_neurons_acc;
reg cur_sel; // which ping-pong buffer to READ from for this layer (layer_idx>0)
reg write_sel; // which ping-pong buffer to WRITE this layer's output to
reg [$clog2(N_WIDTH+1)-1:0] copy_idx;
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
layer_idx <= 8'd0;
num_layers_reg <= 8'd0;
desc_table_addr <= {ADDR_WIDTH{1'b0}};
desc_byte_idx <= 4'd0;
w_base_acc <= 24'h0;
bias_addr_acc <= 24'h0;
activation_acc <= 8'h0;
n_inputs_acc <= 16'h0;
n_neurons_acc <= 16'h0;
cur_sel <= 1'b0;
write_sel <= 1'b0;
copy_idx <= 0;
nm_x_base <= {ADDR_WIDTH{1'b0}};
nm_w_base <= {ADDR_WIDTH{1'b0}};
nm_bias_addr <= {ADDR_WIDTH{1'b0}};
nm_activation <= 2'd1; // ACT_RELU
nm_n_inputs <= 16'h0;
nm_n_neurons <= 16'h0;
nm_start <= 1'b0;
ram_req <= 1'b0;
ram_wr <= 1'b0;
ram_addr <= {ADDR_WIDTH{1'b0}};
ram_wdata <= 8'sd0;
seq_busy <= 1'b0;
seq_done <= 1'b0;
end else begin
// --------------------------------------------------
// Default pulses
// --------------------------------------------------
nm_start <= 1'b0;
ram_req <= 1'b0;
seq_done <= 1'b0;
case (state)
// =================================================
// IDLE
// =================================================
ST_IDLE: begin
if (run_start) begin
// BUG-005 fix (docs/validation/bugs.md): with
// no guard, run_num_layers=0 made layer_idx
// (a full 8-bit counter) wrap the termination
// check to 255, reading and executing 256
// fabricated "layers" from PSRAM bytes far
// past the real descriptor table. There is
// nothing to sequence for zero layers -- treat
// it as an immediate, safe no-op completion
// instead (same convention as spi_engine.v's
// WRITE_RAM/READ_RAM len==0 guard: accept the
// command, do nothing, report done/no error).
if (run_num_layers == 8'd0) begin
seq_done <= 1'b1;
state <= ST_IDLE;
end else begin
seq_busy <= 1'b1;
layer_idx <= 8'd0;
num_layers_reg <= run_num_layers;
desc_table_addr <= table_base;
desc_byte_idx <= 4'd0;
write_sel <= 1'b0;
state <= ST_READ_DESC;
end
end
end
// =================================================
// READ DESCRIPTOR (6 bytes: w_base, bias_addr)
// =================================================
ST_READ_DESC: begin
ram_req <= 1'b1;
ram_wr <= 1'b0;
ram_addr <= desc_table_addr + desc_byte_idx;
state <= ST_READ_WAIT;
end
ST_READ_WAIT: begin
if (ram_ready) begin
case (desc_byte_idx)
4'd0: w_base_acc[23:16] <= ram_rdata;
4'd1: w_base_acc[15:8] <= ram_rdata;
4'd2: w_base_acc[7:0] <= ram_rdata;
4'd3: bias_addr_acc[23:16] <= ram_rdata;
4'd4: bias_addr_acc[15:8] <= ram_rdata;
4'd5: bias_addr_acc[7:0] <= ram_rdata;
4'd6: activation_acc <= ram_rdata;
4'd7: n_inputs_acc[15:8] <= ram_rdata;
4'd8: n_inputs_acc[7:0] <= ram_rdata;
4'd9: n_neurons_acc[15:8] <= ram_rdata;
4'd10: n_neurons_acc[7:0] <= ram_rdata;
endcase
if (desc_byte_idx == 4'd10) begin
desc_byte_idx <= 4'd0;
state <= ST_START_LAYER;
end else begin
desc_byte_idx <= desc_byte_idx + 4'd1;
state <= ST_READ_DESC;
end
end
end
// =================================================
// START LAYER
// =================================================
ST_START_LAYER: begin
nm_w_base <= w_base_acc[ADDR_WIDTH-1:0];
nm_bias_addr <= bias_addr_acc[ADDR_WIDTH-1:0];
nm_activation <= activation_acc[1:0];
nm_n_inputs <= n_inputs_acc;
nm_n_neurons <= n_neurons_acc;
nm_x_base <= (layer_idx == 8'd0)
? x_base
: (cur_sel ? buf_b_base : buf_a_base);
nm_start <= 1'b1;
state <= ST_WAIT_LAYER;
end
// =================================================
// WAIT FOR THIS LAYER TO FINISH
// =================================================
ST_WAIT_LAYER: begin
if (nm_done) begin
copy_idx <= 0;
state <= ST_COPY_ISSUE;
end
end
// =================================================
// COPY y_bus INTO THE PING-PONG OUTPUT BUFFER
// =================================================
ST_COPY_ISSUE: begin
ram_req <= 1'b1;
ram_wr <= 1'b1;
ram_addr <= (write_sel ? buf_b_base : buf_a_base) + copy_idx;
ram_wdata <= y_bus[copy_idx*DATA_WIDTH +: DATA_WIDTH];
state <= ST_COPY_WAIT;
end
ST_COPY_WAIT: begin
if (ram_ready) begin
if (copy_idx == n_neurons_acc[$clog2(N_WIDTH+1)-1:0] - 1'b1) begin
if (layer_idx == num_layers_reg - 8'd1) begin
// Last layer done.
seq_busy <= 1'b0;
seq_done <= 1'b1;
state <= ST_IDLE;
end else begin
// The buffer just written becomes
// the next layer's input.
cur_sel <= write_sel;
write_sel <= ~write_sel;
layer_idx <= layer_idx + 8'd1;
desc_table_addr <= desc_table_addr + 11;
desc_byte_idx <= 4'd0;
state <= ST_READ_DESC;
end
end else begin
copy_idx <= copy_idx + 1'b1;
state <= ST_COPY_ISSUE;
end
end
end
default: begin
state <= ST_IDLE;
end
endcase
end
end
endmodule
+117
View File
@@ -0,0 +1,117 @@
module mac8 #(
parameter DATA_WIDTH = 16,
parameter ACC_WIDTH = 40,
parameter PARALLEL = 8
)(
input signed [DATA_WIDTH*PARALLEL-1:0] x_bus,
input signed [DATA_WIDTH*PARALLEL-1:0] w_bus,
input signed [ACC_WIDTH-1:0] acc_in,
output signed [ACC_WIDTH-1:0] acc_out
);
/*
* Each MAC produces one sign-extended product.
*/
wire signed [ACC_WIDTH-1:0] products [0:PARALLEL-1];
genvar i;
generate
for (i = 0; i < PARALLEL; i = i + 1) begin : GEN_MAC
mac_unit #(
.DATA_WIDTH(DATA_WIDTH),
.ACC_WIDTH(ACC_WIDTH)
) u_mac (
.x(
x_bus[
i*DATA_WIDTH
+:
DATA_WIDTH
]
),
.w(
w_bus[
i*DATA_WIDTH
+:
DATA_WIDTH
]
),
.acc_in({ACC_WIDTH{1'b0}}),
.acc_out(products[i])
);
end
endgenerate
/*
* Balanced binary adder tree.
*
* PARALLEL is intended to be a power of two:
* 8 -> 3 levels
* 16 -> 4 levels
* 32 -> 5 levels
*
* This replaces the previous linear accumulator:
*
* (((p0+p1)+p2)+p3)+...
*
* with:
*
* sum
* / \
* ... ...
*
* reducing the combinational depth from O(PARALLEL)
* to O(log2(PARALLEL)).
*/
localparam TREE_LEVELS = $clog2(PARALLEL);
wire signed [ACC_WIDTH-1:0]
tree [0:TREE_LEVELS][0:PARALLEL-1];
generate
/*
* Level 0 = individual products
*/
for (i = 0; i < PARALLEL; i = i + 1) begin : GEN_TREE_INPUT
assign tree[0][i] = products[i];
end
endgenerate
genvar level;
genvar node;
generate
for (level = 0; level < TREE_LEVELS; level = level + 1) begin : GEN_TREE_LEVEL
for (
node = 0;
node < (PARALLEL >> (level + 1));
node = node + 1
) begin : GEN_TREE_NODE
assign tree[level + 1][node] =
tree[level][2*node] +
tree[level][2*node + 1];
end
end
endgenerate
/*
* Add the partial sum to the accumulator.
*/
assign acc_out =
acc_in + tree[TREE_LEVELS][0];
endmodule
+23
View File
@@ -0,0 +1,23 @@
module mac_unit #(
parameter DATA_WIDTH = 16,
parameter ACC_WIDTH = 40
)(
input signed [DATA_WIDTH-1:0] x,
input signed [DATA_WIDTH-1:0] w,
input signed [ACC_WIDTH-1:0] acc_in,
output signed [ACC_WIDTH-1:0] acc_out
);
localparam PROD_WIDTH = 2 * DATA_WIDTH;
wire signed [PROD_WIDTH-1:0] product;
wire signed [ACC_WIDTH-1:0] product_ext;
assign product = x * w;
assign product_ext =
{{(ACC_WIDTH-PROD_WIDTH){product[PROD_WIDTH-1]}}, product};
assign acc_out = acc_in + product_ext;
endmodule
+238
View File
@@ -0,0 +1,238 @@
`timescale 1ns/1ps
// ================================================================
// MEM_ARBITER
//
// Arbitrates a single shared byte-level memory master port (feeding
// a shared int8_memory_access -> memory_interface -> psram_controller
// chain) between three byte-level requesters:
//
// Port A: spi_engine.v (WRITE_RAM / READ_RAM opcodes)
// Port B: neuron_memory.v (its own X/W/bias reads during a run)
// Port C: layer_sequencer.v (Phase 5: descriptor reads + output
// buffer writes between layers)
// Port D: flash_copy_engine.v (flash-subsystem F2: flash<->PSRAM
// block DMA, LOWEST priority -- see
// below)
//
// Fixed priority B > C > A > D when more than one requests on the
// same idle cycle (an in-progress inference is treated as more
// time-critical than the sequencer's own bookkeeping, which in turn
// is treated as more time-critical than a newly-arriving manual SPI
// RAM access, which in turn is treated as more time-critical than
// the flash copy engine -- flash operations are ms-scale and never
// meant to compete with inference for memory bandwidth, per the
// flash-subsystem phase-plan's explicit "priorita bassa" requirement:
// a flash load/save simply waits its turn, one byte-transaction at a
// time, behind anything else that wants the shared PSRAM port).
// In normal operation B and C are temporally disjoint anyway --
// neuron_memory only requests while running, and layer_sequencer
// only requests in the gaps between layers -- so priority among
// A/B/C mostly matters for the edge case of a manual
// WRITE_RAM/READ_RAM arriving while a Phase 5 run is in progress.
// Port D is expected to be active only during flash load/save,
// which this design assumes does not overlap real-time inference
// (the same "not the hot path" assumption the flash phase-plan
// states explicitly) -- if it ever did overlap, its lowest-priority
// placement here means it simply gets stretched out, never starves
// or corrupts A/B/C.
// Once a port is granted, the arbiter holds ownership until that
// single transaction's m_ready pulse, then releases -- all four
// masters already issue `req` as a clean one-cycle pulse (matching
// int8_memory_access's own contract), so a simple grant-and-forward
// design is sufficient; no request queuing/pipelining is needed.
// ================================================================
module mem_arbiter #(
parameter ADDR_WIDTH = 23
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Port A - spi_engine
// ------------------------------------------------------------
input wire a_req,
input wire a_wr,
input wire [ADDR_WIDTH-1:0] a_addr,
input wire signed [7:0] a_wdata,
output reg signed [7:0] a_rdata,
output reg a_ready,
// ------------------------------------------------------------
// Port B - neuron_memory
// ------------------------------------------------------------
input wire b_req,
input wire b_wr,
input wire [ADDR_WIDTH-1:0] b_addr,
input wire signed [7:0] b_wdata,
output reg signed [7:0] b_rdata,
output reg b_ready,
// ------------------------------------------------------------
// Port C - layer_sequencer
// ------------------------------------------------------------
input wire c_req,
input wire c_wr,
input wire [ADDR_WIDTH-1:0] c_addr,
input wire signed [7:0] c_wdata,
output reg signed [7:0] c_rdata,
output reg c_ready,
// ------------------------------------------------------------
// Port D - flash_copy_engine (F2, lowest priority)
// ------------------------------------------------------------
input wire d_req,
input wire d_wr,
input wire [ADDR_WIDTH-1:0] d_addr,
input wire signed [7:0] d_wdata,
output reg signed [7:0] d_rdata,
output reg d_ready,
// ------------------------------------------------------------
// Shared master port
// ------------------------------------------------------------
output reg m_req,
output reg m_wr,
output reg [ADDR_WIDTH-1:0] m_addr,
output reg signed [7:0] m_wdata,
input wire signed [7:0] m_rdata,
input wire m_ready
);
localparam SEL_NONE = 3'd0;
localparam SEL_A = 3'd1;
localparam SEL_B = 3'd2;
localparam SEL_C = 3'd3;
localparam SEL_D = 3'd4;
reg [2:0] owner;
always @(posedge clk) begin
if (rst) begin
owner <= SEL_NONE;
m_req <= 1'b0;
m_wr <= 1'b0;
m_addr <= {ADDR_WIDTH{1'b0}};
m_wdata <= 8'sd0;
a_rdata <= 8'sd0;
a_ready <= 1'b0;
b_rdata <= 8'sd0;
b_ready <= 1'b0;
c_rdata <= 8'sd0;
c_ready <= 1'b0;
d_rdata <= 8'sd0;
d_ready <= 1'b0;
end else begin
m_req <= 1'b0;
a_ready <= 1'b0;
b_ready <= 1'b0;
c_ready <= 1'b0;
d_ready <= 1'b0;
case (owner)
SEL_NONE: begin
if (b_req) begin
owner <= SEL_B;
m_req <= 1'b1;
m_wr <= b_wr;
m_addr <= b_addr;
m_wdata <= b_wdata;
end else if (c_req) begin
owner <= SEL_C;
m_req <= 1'b1;
m_wr <= c_wr;
m_addr <= c_addr;
m_wdata <= c_wdata;
end else if (a_req) begin
owner <= SEL_A;
m_req <= 1'b1;
m_wr <= a_wr;
m_addr <= a_addr;
m_wdata <= a_wdata;
end else if (d_req) begin
owner <= SEL_D;
m_req <= 1'b1;
m_wr <= d_wr;
m_addr <= d_addr;
m_wdata <= d_wdata;
end
end
SEL_A: begin
if (m_ready) begin
a_rdata <= m_rdata;
a_ready <= 1'b1;
owner <= SEL_NONE;
end
end
SEL_B: begin
if (m_ready) begin
b_rdata <= m_rdata;
b_ready <= 1'b1;
owner <= SEL_NONE;
end
end
SEL_C: begin
if (m_ready) begin
c_rdata <= m_rdata;
c_ready <= 1'b1;
owner <= SEL_NONE;
end
end
SEL_D: begin
if (m_ready) begin
d_rdata <= m_rdata;
d_ready <= 1'b1;
owner <= SEL_NONE;
end
end
default: begin
owner <= SEL_NONE;
end
endcase
end
end
endmodule
+94
View File
@@ -0,0 +1,94 @@
module memory_interface #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 16
)(
input wire clk,
input wire rst,
input wire req,
input wire wr,
input wire [ADDR_WIDTH-1:0] addr,
input wire [DATA_WIDTH-1:0] wdata,
input wire lb_n,
input wire ub_n,
output reg [DATA_WIDTH-1:0] rdata,
output reg ready,
output reg mem_req,
output reg mem_wr,
output reg [ADDR_WIDTH-1:0] mem_addr,
output reg [DATA_WIDTH-1:0] mem_wdata,
output reg mem_lb_n,
output reg mem_ub_n,
input wire [DATA_WIDTH-1:0] mem_rdata,
input wire mem_ready
);
localparam STATE_IDLE = 2'd0;
localparam STATE_WAIT = 2'd1;
reg [1:0] state;
always @(posedge clk) begin
if (rst) begin
state <= STATE_IDLE;
rdata <= {DATA_WIDTH{1'b0}};
ready <= 1'b0;
mem_lb_n <= 1'b1;
mem_ub_n <= 1'b1;
mem_req <= 1'b0;
mem_wr <= 1'b0;
mem_addr <= {ADDR_WIDTH{1'b0}};
mem_wdata <= {DATA_WIDTH{1'b0}};
end else begin
// Default: pulses
ready <= 1'b0;
mem_req <= 1'b0;
case (state)
STATE_IDLE: begin
if (req) begin
// Latch transaction
mem_wr <= wr;
mem_addr <= addr;
mem_wdata <= wdata;
mem_lb_n <= lb_n;
mem_ub_n <= ub_n;
// Issue exactly one-cycle request
mem_req <= 1'b1;
state <= STATE_WAIT;
end
end
STATE_WAIT: begin
// Wait for memory completion
if (mem_ready) begin
if (!mem_wr)
rdata <= mem_rdata;
ready <= 1'b1;
state <= STATE_IDLE;
end
end
default: begin
state <= STATE_IDLE;
end
endcase
end
end
endmodule
+105
View File
@@ -0,0 +1,105 @@
module memory_model #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 16,
parameter DEPTH = 4096,
parameter READ_LATENCY = 2
)(
input wire clk,
input wire rst,
input wire req,
input wire wr,
input wire [ADDR_WIDTH-1:0] addr,
input wire [DATA_WIDTH-1:0] wdata,
output reg [DATA_WIDTH-1:0] rdata,
output reg ready
);
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg busy;
reg pending_wr;
reg [ADDR_WIDTH-1:0] pending_addr;
reg [DATA_WIDTH-1:0] pending_wdata;
integer delay_count;
integer i;
always @(posedge clk) begin
if (rst) begin
rdata <= {DATA_WIDTH{1'b0}};
ready <= 1'b0;
busy <= 1'b0;
pending_wr <= 1'b0;
pending_addr <= {ADDR_WIDTH{1'b0}};
pending_wdata <= {DATA_WIDTH{1'b0}};
delay_count <= 0;
for (i = 0; i < DEPTH; i = i + 1)
mem[i] <= {DATA_WIDTH{1'b0}};
end else begin
// ready is a one-cycle pulse
ready <= 1'b0;
// ----------------------------------------------------
// Accept request
// ----------------------------------------------------
if (!busy) begin
if (req) begin
busy <= 1'b1;
pending_wr <= wr;
pending_addr <= addr;
pending_wdata <= wdata;
delay_count <= READ_LATENCY;
end
end else begin
// ------------------------------------------------
// Wait
// ------------------------------------------------
if (delay_count > 0) begin
delay_count <= delay_count - 1;
end else begin
// --------------------------------------------
// Complete transaction
// --------------------------------------------
if (pending_wr) begin
// WRITE
if (pending_addr < DEPTH)
mem[pending_addr] <= pending_wdata;
end else begin
// READ
if (pending_addr < DEPTH)
rdata <= mem[pending_addr];
else
rdata <= {DATA_WIDTH{1'b0}};
end
ready <= 1'b1;
busy <= 1'b0;
end
end
end
end
endmodule
+550
View File
@@ -0,0 +1,550 @@
`timescale 1ns/1ps
module neuron_memory #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 8,
parameter N_INPUTS = 32,
parameter N_NEURONS = 1,
parameter PARALLEL = 8,
parameter ACC_WIDTH = 32
)(
input wire clk,
input wire rst,
input wire start,
// ------------------------------------------------------------
// Memory interface
//
// BYTE-ADDRESS / INT8 interface
// ------------------------------------------------------------
output wire mem_req,
output wire mem_wr,
output wire [ADDR_WIDTH-1:0] mem_addr,
output wire signed [7:0] mem_wdata,
input wire signed [7:0] mem_rdata,
input wire mem_ready,
// ------------------------------------------------------------
// Network memory layout
// ------------------------------------------------------------
input wire [ADDR_WIDTH-1:0] x_base,
input wire [ADDR_WIDTH-1:0] w_base,
input wire [ADDR_WIDTH-1:0] bias_addr,
// Activation function for this run, forwarded to neuron_parallel
// (see rtl/neuron_parallel.v's ACT_* localparams). Defaults to
// ACT_RELU (2'd1), neuron_parallel's own default, so any caller
// that leaves this unconnected is unaffected.
input wire [1:0] activation = 2'd1,
// Real (runtime) width for THIS run: how many of the N_INPUTS/
// N_NEURONS this instance was BUILT for are actually real for
// the network currently loaded. Lets one synthesized bitstream
// serve any network topology up to its build-time max width --
// X/W are read from RAM for n_inputs_real elements per neuron
// (not N_INPUTS), and only n_neurons_real neurons are computed
// (not N_NEURONS); n_inputs_real must be a multiple of PARALLEL
// (the caller's responsibility -- same constraint N_INPUTS
// itself is held to at elaboration time, see
// rtl/neuron_parallel.v's PARAMETER GUARD). Defaults to the
// full build-time width, so any caller that leaves these
// unconnected is completely unaffected.
input wire [15:0] n_inputs_real = N_INPUTS[15:0],
input wire [15:0] n_neurons_real = N_NEURONS[15:0],
// ------------------------------------------------------------
// Result
//
// One INT8 output per neuron, packed neuron-major (same
// convention as layer.v's y_bus): neuron n occupies
// y_bus[n*DATA_WIDTH +: DATA_WIDTH].
// ------------------------------------------------------------
output wire signed [DATA_WIDTH*N_NEURONS-1:0] y_bus,
output reg busy,
output reg done
);
// ============================================================
// STATES
// ============================================================
localparam STATE_IDLE = 4'd0;
localparam STATE_READ_X = 4'd1;
localparam STATE_READ_W = 4'd2;
localparam STATE_READ_BIAS = 4'd3;
localparam STATE_START_N = 4'd4;
localparam STATE_WAIT_N = 4'd5;
reg [3:0] state;
reg [$clog2(N_INPUTS+1)-1:0] index;
// ============================================================
// NEURON LOOP (Phase 3: multi-neuron memory integration)
//
// X is shared and read once per layer invocation. W and bias
// are re-read from memory for each neuron in turn and fed to a
// single, reused neuron_parallel instance (memory-bound design:
// one neuron computed at a time). w_group_base/bias_group_addr
// track the current neuron's base address and are advanced by
// N_INPUTS / 1 byte respectively between neurons, following the
// same neuron-major layout as layer.v's weights_bus/bias_bus.
// ============================================================
localparam NEURON_INDEX_WIDTH =
(N_NEURONS <= 1) ? 1 : $clog2(N_NEURONS);
reg [NEURON_INDEX_WIDTH-1:0] neuron_index;
reg [ADDR_WIDTH-1:0] w_group_base;
reg [ADDR_WIDTH-1:0] bias_group_addr;
reg signed [7:0] y_reg [0:N_NEURONS-1];
// ============================================================
// LOCAL MEMORY ARRAYS
// ============================================================
reg signed [7:0] x_mem [0:N_INPUTS-1];
reg signed [7:0] w_mem [0:N_INPUTS-1];
reg signed [7:0] bias_reg;
// ============================================================
// NEURON BUS
// ============================================================
wire signed [DATA_WIDTH*N_INPUTS-1:0] x_bus;
wire signed [DATA_WIDTH*N_INPUTS-1:0] w_bus;
genvar i;
generate
for (i = 0; i < N_INPUTS; i = i + 1) begin : GEN_BUS
assign x_bus[i*DATA_WIDTH +: DATA_WIDTH] = x_mem[i];
assign w_bus[i*DATA_WIDTH +: DATA_WIDTH] = w_mem[i];
end
endgenerate
genvar j;
generate
for (j = 0; j < N_NEURONS; j = j + 1) begin : GEN_Y_BUS
assign y_bus[j*DATA_WIDTH +: DATA_WIDTH] = y_reg[j];
end
endgenerate
// ============================================================
// INT8 MEMORY ACCESS
//
// This converts BYTE addresses into 16-bit word accesses.
//
// IMPORTANT:
// The memory side of this block is connected to the EXTERNAL
// memory_interface through the neuron_memory ports.
//
// It must NOT be connected directly to the PSRAM controller.
// ============================================================
reg access_req;
reg access_wr;
reg [ADDR_WIDTH-1:0] access_addr;
reg signed [7:0] access_wdata;
wire signed [7:0] access_rdata;
wire access_ready;
wire access_mem_req;
wire access_mem_wr;
wire [ADDR_WIDTH-1:0] access_mem_addr;
wire [15:0] access_mem_wdata;
wire access_mem_lb_n;
wire access_mem_ub_n;
// ------------------------------------------------------------
// Return data from the external memory interface.
//
// memory_interface returns a 16-bit word, while neuron_memory
// exposes only the requested INT8 byte.
//
// int8_memory_access expects the complete 16-bit word.
// ------------------------------------------------------------
wire [15:0] access_mem_rdata;
assign access_mem_rdata =
access_addr[0]
? {mem_rdata, 8'h00}
: {8'h00, mem_rdata};
// ------------------------------------------------------------
// IMPORTANT:
//
// mem_ready comes from the EXTERNAL memory_interface.
// mem_rdata comes from the EXTERNAL memory_interface.
//
// This fixes the previous deadlock where int8_memory_access
// was waiting for the PSRAM controller's mem_ready directly.
// ------------------------------------------------------------
int8_memory_access #(
.ADDR_WIDTH(ADDR_WIDTH)
) u_mem (
.clk (clk),
.rst (rst),
.req (access_req),
.wr (access_wr),
.addr (access_addr),
.wdata (access_wdata),
.rdata (access_rdata),
.ready (access_ready),
.mem_req (access_mem_req),
.mem_wr (access_mem_wr),
.mem_addr (access_mem_addr),
.mem_wdata (access_mem_wdata),
.mem_lb_n (access_mem_lb_n),
.mem_ub_n (access_mem_ub_n),
.mem_rdata (access_mem_rdata),
.mem_ready (mem_ready)
);
// ============================================================
// EXTERNAL MEMORY INTERFACE
//
// The external interface expects the INT8-level signals.
// The testbench converts these into its 16-bit bus.
//
// IMPORTANT:
// access_mem_addr is already a WORD address.
// However, the external neuron_memory interface is defined
// as a BYTE address.
//
// Therefore expose the original byte address here.
// ============================================================
assign mem_req = access_mem_req;
assign mem_wr = access_mem_wr;
assign mem_addr =
access_addr;
assign mem_wdata =
access_wdata;
// ============================================================
// NEURON
// ============================================================
reg neuron_start;
integer rst_i;
wire signed [7:0] neuron_y;
wire neuron_busy;
wire neuron_done;
neuron_parallel #(
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH)
) u_neuron (
.clk(clk),
.rst(rst),
.start(neuron_start),
.x_bus(x_bus),
.w_bus(w_bus),
.bias(bias_reg),
.activation(activation),
.n_inputs_real(n_inputs_real),
.y(neuron_y),
.busy(neuron_busy),
.done(neuron_done)
);
// ============================================================
// CONTROLLER
// ============================================================
always @(posedge clk) begin
if (rst) begin
state <= STATE_IDLE;
index <= 0;
neuron_index <= 0;
w_group_base <= 0;
bias_group_addr <= 0;
bias_reg <= 0;
access_req <= 1'b0;
access_wr <= 1'b0;
access_addr <= 0;
access_wdata <= 0;
neuron_start <= 1'b0;
for (rst_i = 0; rst_i < N_NEURONS; rst_i = rst_i + 1)
y_reg[rst_i] <= 0;
busy <= 1'b0;
done <= 1'b0;
end else begin
// ----------------------------------------------------
// Default pulse signals
// ----------------------------------------------------
access_req <= 1'b0;
neuron_start <= 1'b0;
done <= 1'b0;
case (state)
// =================================================
// IDLE
// =================================================
STATE_IDLE: begin
busy <= 1'b0;
if (start) begin
busy <= 1'b1;
index <= 0;
neuron_index <= 0;
w_group_base <= w_base;
bias_group_addr <= bias_addr;
// First X byte. (BUG-004 fix, docs/validation/
// bugs.md: STATE_READ_X/STATE_READ_W's own
// termination checks are patched below to
// treat n_inputs_real==0 as "done after this
// one byte" instead of wrapping -- see those
// states for the full rationale. One harmless
// extra byte is still read here for n_inputs_
// real==0 before the fixed check short-
// circuits; x_base/w_group_base are always
// valid addresses, so this costs one cycle,
// not correctness.)
access_addr <= x_base;
access_wr <= 1'b0;
access_req <= 1'b1;
state <= STATE_READ_X;
end
end
// =================================================
// READ X
// =================================================
STATE_READ_X: begin
if (access_ready) begin
x_mem[index] <= access_rdata;
// BUG-004 fix (docs/validation/bugs.md):
// `n_inputs_real[...]-1'b1` wraps for
// n_inputs_real==0 to a value `index` (sized
// to the SAME width) could reach by counting
// up from 0, reading well past the intended
// (empty) real region. Explicit `==0` check
// terminates after this one already-issued
// byte instead.
if (n_inputs_real == 16'h0 ||
index == n_inputs_real[$clog2(N_INPUTS+1)-1:0]-1'b1) begin
index <= 0;
// BUG-004 fix (docs/validation/bugs.md):
// n_neurons_real==0 means there is no
// neuron 0 to compute at all -- the
// original code always ran at least one
// full neuron (W/bias read + a real
// neuron_parallel invocation) before its
// own termination check could even be
// reached, and that check
// (`neuron_index == n_neurons_real-1`)
// had the same unguarded-wraparound issue
// as the other BUG-00x cases besides.
// Report done immediately, y_reg/y_bus
// left untouched (nothing was asked to be
// computed), instead of entering the
// neuron loop at all.
if (n_neurons_real == 16'h0) begin
busy <= 1'b0;
done <= 1'b1;
state <= STATE_IDLE;
end else begin
access_addr <= w_group_base;
access_wr <= 1'b0;
access_req <= 1'b1;
state <= STATE_READ_W;
end
end else begin
index <= index + 1'b1;
access_addr <= x_base + index + 1'b1;
access_req <= 1'b1;
end
end
end
// =================================================
// READ W
// =================================================
STATE_READ_W: begin
if (access_ready) begin
w_mem[index] <= access_rdata;
// BUG-004 fix (docs/validation/bugs.md): same
// wraparound issue and same fix as
// STATE_READ_X above -- this state is
// re-entered once per neuron (the per-neuron
// loop-back), so the guard matters on every
// iteration, not just the first.
if (n_inputs_real == 16'h0 ||
index == n_inputs_real[$clog2(N_INPUTS+1)-1:0]-1'b1) begin
access_addr <= bias_group_addr;
access_wr <= 1'b0;
access_req <= 1'b1;
state <= STATE_READ_BIAS;
end else begin
index <= index + 1'b1;
access_addr <= w_group_base + index + 1'b1;
access_req <= 1'b1;
end
end
end
// =================================================
// READ BIAS
// =================================================
STATE_READ_BIAS: begin
if (access_ready) begin
bias_reg <= access_rdata;
state <= STATE_START_N;
end
end
// =================================================
// START NEURON
// =================================================
STATE_START_N: begin
neuron_start <= 1'b1;
state <= STATE_WAIT_N;
end
// =================================================
// WAIT NEURON
// =================================================
STATE_WAIT_N: begin
if (neuron_done) begin
y_reg[neuron_index] <= neuron_y;
if (neuron_index == n_neurons_real[NEURON_INDEX_WIDTH-1:0]-1'b1) begin
// Last neuron of the layer: done.
busy <= 1'b0;
done <= 1'b1;
state <= STATE_IDLE;
end else begin
// Advance to the next neuron: X stays
// in x_mem (shared), reload W and bias
// for neuron_index+1 from memory.
neuron_index <= neuron_index + 1'b1;
w_group_base <= w_group_base + n_inputs_real;
bias_group_addr <= bias_group_addr + 1'b1;
index <= 0;
access_addr <= w_group_base + n_inputs_real;
access_wr <= 1'b0;
access_req <= 1'b1;
state <= STATE_READ_W;
end
end
end
// =================================================
// DEFAULT
// =================================================
default: begin
state <= STATE_IDLE;
busy <= 1'b0;
end
endcase
end
end
endmodule
+272
View File
@@ -0,0 +1,272 @@
module neuron_parallel #(
parameter DATA_WIDTH = 8,
parameter N_INPUTS = 32,
parameter PARALLEL = 8,
parameter ACC_WIDTH = 32
)(
input clk,
input rst,
input start,
input signed [DATA_WIDTH*N_INPUTS-1:0] x_bus,
input signed [DATA_WIDTH*N_INPUTS-1:0] w_bus,
input signed [DATA_WIDTH-1:0] bias,
// Activation function applied to the final accumulator before
// the INT8 saturate, see ACT_* localparams below. Defaults to
// ACT_RELU (2'd1) -- the ONLY behavior this module had before
// this port existed -- so every pre-existing caller that leaves
// it unconnected (rtl/layer.v and its testbenches) is completely
// unaffected.
input [1:0] activation = 2'd1,
// Real (runtime) input width for THIS run, in elements -- must
// be a multiple of PARALLEL (same constraint N_INPUTS itself is
// held to at elaboration time, just now the caller's runtime
// responsibility instead of a build-time guard: an n_inputs_real
// that isn't a PARALLEL multiple, or is 0, reproduces the same
// "wrong result" / "hangs forever" failure modes documented
// below for a bad N_INPUTS/PARALLEL pair). Defaults to N_INPUTS
// (the full build-time width), so any caller that leaves this
// unconnected processes every group exactly as before this port
// existed.
input [15:0] n_inputs_real = N_INPUTS[15:0],
output reg signed [DATA_WIDTH-1:0] y,
output reg busy,
output reg done
);
// ============================================================
// ACTIVATION ENCODING
// ============================================================
localparam ACT_NONE = 2'd0; // linear: saturate both directions, no clamp
localparam ACT_RELU = 2'd1; // max(0, x), then saturate positive (default)
// ============================================================
// PARAMETER GUARD
//
// PARALLEL must evenly divide N_INPUTS. If it does not:
//
// - GROUPS = N_INPUTS / PARALLEL truncates (integer division),
// and the remainder inputs are silently never read by the
// accumulator: WRONG result, no error, no warning.
//
// - If PARALLEL > N_INPUTS, GROUPS = 0 and the controller's
// terminal condition (group_index == GROUPS-1) is never
// satisfied: the neuron hangs forever (busy stays high,
// done is never asserted).
//
// Both failure modes were confirmed empirically in
// sim/parameter_sweep_tb.v (Phase 2 of the roadmap). Rather than
// changing the validated datapath, this forces an elaboration-
// time failure in BOTH simulation and synthesis by instantiating
// a deliberately undefined module when the condition is
// violated. When N_INPUTS % PARALLEL == 0 this generate branch
// is never elaborated, so valid configurations are unaffected.
// ============================================================
generate
// BUG-002 fix (docs/validation/bugs.md): N_INPUTS=0 satisfies
// `0 % PARALLEL == 0` for any PARALLEL, so the original
// modulo-only check never fired for this case -- yet GROUPS
// = 0/PARALLEL = 0 is exactly the degenerate condition this
// guard exists to prevent. x_bus/w_bus (declared
// [DATA_WIDTH*N_INPUTS-1:0]) also do not collapse to a true
// zero-width vector for N_INPUTS=0 ([-1:0] is treated as a
// real 2-bit vector by both Icarus and Yosys), confirmed to
// leave `start` silently ineffective on both simulation and
// real synthesis. Explicit `N_INPUTS == 0` check closes the
// gap without changing behavior for any N_INPUTS >= 1.
if (N_INPUTS == 0 || N_INPUTS % PARALLEL != 0) begin : PARAMETER_ERROR_N_INPUTS_NOT_MULTIPLE_OF_PARALLEL
neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL invalid_parameter_combination();
end
endgenerate
localparam GROUPS = N_INPUTS / PARALLEL;
localparam GROUP_INDEX_WIDTH =
(GROUPS <= 1) ? 1 : $clog2(GROUPS);
reg [GROUP_INDEX_WIDTH-1:0] group_index;
// Runtime group count for this run: n_inputs_real / PARALLEL.
// PARALLEL is a build-time constant, so this divide is a fixed
// combinational block sized once at synthesis (a shift when
// PARALLEL is a power of two, as in every config this project
// uses today), evaluated only at the start of a run -- not on
// the per-group critical path.
wire [15:0] groups_real = n_inputs_real / PARALLEL[15:0];
reg signed [ACC_WIDTH-1:0] acc;
wire signed [DATA_WIDTH*PARALLEL-1:0] x_group;
wire signed [DATA_WIDTH*PARALLEL-1:0] w_group;
wire signed [ACC_WIDTH-1:0] acc_next;
wire signed [ACC_WIDTH-1:0] bias_ext;
wire signed [ACC_WIDTH-1:0] final_acc;
assign x_group =
x_bus[
group_index*PARALLEL*DATA_WIDTH
+: PARALLEL*DATA_WIDTH
];
assign w_group =
w_bus[
group_index*PARALLEL*DATA_WIDTH
+: PARALLEL*DATA_WIDTH
];
mac8 #(
.DATA_WIDTH(DATA_WIDTH),
.ACC_WIDTH(ACC_WIDTH),
.PARALLEL(PARALLEL)
) u_mac8 (
.x_bus(x_group),
.w_bus(w_group),
.acc_in(acc),
.acc_out(acc_next)
);
// Sign extension INT8 -> INT32
assign bias_ext =
{{(ACC_WIDTH-DATA_WIDTH){bias[DATA_WIDTH-1]}}, bias};
// Accumulazione finale + bias
//
// PIPELINE STAGE (timing closure, step 2): this now adds bias to
// the ALREADY-REGISTERED `acc` (the complete sum, latched at the
// end of the last MAC group -- see the `finishing` stage below),
// not to `acc_next` combinationally chained onto the same cycle
// as the last group's own MAC-tree add. Measured on real
// place&route (see WORKLOG.md "Timing closure"): the previous
// same-cycle chain [mac8 tree add] -> [bias add] -> [saturate]
// was the critical path; splitting the bias-add+saturate half
// into its own cycle (operating on a value already settled in a
// register) breaks that chain by construction, independent of
// synthesis/placement heuristics -- unlike the bit-test rewrite
// below, which measurably shortens the logic but was swamped by
// seed-to-seed placement noise on its own. Adds exactly one
// cycle of latency per neuron (`start` to `done`), transparent
// to every caller's busy/done handshake (neuron_memory.v,
// layer_sequencer.v, graph_engine.v) -- no protocol change, no
// caller-visible interface change, only one extra `finishing`
// clock edge already absorbed by that handshake.
assign final_acc = acc + bias_ext;
// ============================================================
// SATURATION / ACTIVATION -- bit-test form (timing closure)
//
// final_acc (ACC_WIDTH bits) fits the signed DATA_WIDTH range
// iff bits [ACC_WIDTH-1:DATA_WIDTH-1] are all equal (all 0 ->
// non-negative and in range, all 1 -> negative and in range) --
// exactly what sign-extending the truncated DATA_WIDTH-bit value
// back up to ACC_WIDTH bits would reproduce. Bit-exact
// equivalent of the previous arithmetic comparisons in every
// case (verified in sim/neuron_parallel_saturation_bounds_tb.v);
// no ACT_NONE/ACT_RELU behavior change. NOTE (measured, see
// WORKLOG.md): on this device/toolchain, Yosys's abc9 mapper
// maps this wide AND/OR reduction onto CCU2C carry-chain cells
// too, not a shallow LUT tree -- so on its own this rewrite only
// trims the chain slightly (fewer CCU2C hops, ~0.2-0.9ns less
// logic delay measured) rather than eliminating it; kept anyway
// as a genuine, bit-exact-verified simplification, with the
// pipeline stage above doing the real timing-closure work.
// ============================================================
wire final_acc_sign = final_acc[ACC_WIDTH-1];
wire final_acc_upper_all0 = ~(|final_acc[ACC_WIDTH-1:DATA_WIDTH-1]);
wire final_acc_upper_all1 = &final_acc[ACC_WIDTH-1:DATA_WIDTH-1];
wire final_acc_in_range = final_acc_upper_all0 | final_acc_upper_all1;
wire final_acc_le_zero = final_acc_sign | ~(|final_acc);
// ACT_NONE: saturate to [-2^(DATA_WIDTH-1), 2^(DATA_WIDTH-1)-1]
wire signed [DATA_WIDTH-1:0] y_none =
final_acc_in_range ? final_acc[DATA_WIDTH-1:0]
: (final_acc_sign ? {1'b1, {(DATA_WIDTH-1){1'b0}}}
: {1'b0, {(DATA_WIDTH-1){1'b1}}});
// ACT_RELU: max(0, final_acc), then saturate positive
wire signed [DATA_WIDTH-1:0] y_relu =
final_acc_le_zero ? {DATA_WIDTH{1'b0}}
: (final_acc_upper_all0 ? final_acc[DATA_WIDTH-1:0]
: {1'b0, {(DATA_WIDTH-1){1'b1}}});
// `finishing`: one-cycle pipeline stage between the last MAC
// group's accumulate and the bias-add+saturate/activation step
// (see the `final_acc` comment above). `busy` stays high through
// it, so it is invisible to every existing start/busy/done
// caller -- just one extra clock of latency.
reg finishing;
always @(posedge clk) begin
if (rst) begin
group_index <= 0;
acc <= 0;
y <= 0;
busy <= 0;
done <= 0;
finishing <= 0;
end else begin
done <= 0;
if (start && !busy) begin
group_index <= 0;
acc <= 0;
busy <= 1;
// BUG-003 fix (docs/validation/bugs.md): with no
// guard, n_inputs_real=0 made groups_real=0 wrap the
// group-loop termination check to an unreachable (or,
// depending on GROUP_INDEX_WIDTH, silently
// full-width-processing) value -- confirmed
// inconsistent behavior across repeated runs, never a
// correct one. Zero real inputs has a well-defined
// correct answer (the empty sum is 0, so
// final_acc=bias_ext alone) -- go straight to
// `finishing` next cycle with acc still at its
// just-cleared 0, reusing the existing
// activation/saturation logic unchanged instead of
// entering the group-processing loop at all.
finishing <= (n_inputs_real == 16'h0);
end else if (finishing) begin
case (activation)
ACT_NONE: y <= y_none; // linear: saturate both directions
default: y <= y_relu; // ACT_RELU (also the fallback for any reserved encoding)
endcase
busy <= 0;
done <= 1;
finishing <= 0;
end else if (busy) begin
if (group_index == groups_real[GROUP_INDEX_WIDTH-1:0] - 1'b1) begin
acc <= acc_next; // complete sum, bias not yet added
finishing <= 1;
end else begin
acc <= acc_next;
group_index <= group_index + 1'b1;
end
end
end
end
endmodule
+858
View File
@@ -0,0 +1,858 @@
module psram_controller #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 16,
parameter CLK_FREQ_MHZ = 80
)(
input wire clk,
input wire rst,
// ============================================================
// Memory Interface side
// ============================================================
input wire mem_req,
input wire mem_wr,
input wire [ADDR_WIDTH-1:0] mem_addr,
input wire [DATA_WIDTH-1:0] mem_wdata,
input wire mem_lb_n,
input wire mem_ub_n,
output reg [DATA_WIDTH-1:0] mem_rdata,
output reg mem_ready,
// ============================================================
// PSRAM physical interface
// ============================================================
output reg [ADDR_WIDTH-1:0] psram_a,
inout wire [DATA_WIDTH-1:0] psram_dq,
output reg psram_ce_n,
output reg psram_oe_n,
output reg psram_we_n,
output reg psram_lb_n,
output reg psram_ub_n,
output reg psram_zz_n
);
// ============================================================
// Timing
// ============================================================
//
// ISSI IS66WVE4M16EBLL-70BLI (-70 speed grade): async random
// access is 70ns (tAA/tRC). The chip also supports PAGE MODE
// reads: once an initial tAA access has been done, further
// reads to the same 16-word page (address bits above A[3])
// only need to wait tAPA/tPC = 20ns before the next word is
// valid, because CE#/OE# stay asserted and only the low
// address bits change (datasheet Fig. 4). Page mode only
// applies to reads; writes always pay the full random-access
// time.
//
// Page mode read access is DISABLED at power-up (CR[7] = 0)
// and must be turned on with a configuration-register write
// before it can be relied on -- see STATE_CR_INIT below.
// ============================================================
localparam integer ACCESS_CYCLES =
((70 * CLK_FREQ_MHZ) + 999) / 1000;
localparam integer PAGE_CYCLES =
((20 * CLK_FREQ_MHZ) + 999) / 1000;
localparam integer INIT_CYCLES =
150 * CLK_FREQ_MHZ;
localparam integer COUNTER_WIDTH =
(INIT_CYCLES <= 1) ? 1 : $clog2(INIT_CYCLES + 1);
// A page is kept open (CE#/OE# held low between transactions)
// only up to a safety margin under tCEM (8us max CE# low
// pulse, refresh-related). 6us leaves comfortable headroom.
localparam integer PAGE_HOLD_NS = 6000;
localparam integer PAGE_TIMEOUT_CYCLES =
((PAGE_HOLD_NS * CLK_FREQ_MHZ) + 999) / 1000;
localparam integer HOLD_WIDTH =
(PAGE_TIMEOUT_CYCLES <= 1) ? 1 : $clog2(PAGE_TIMEOUT_CYCLES + 1);
// ============================================================
// Configuration register value
//
// Loaded once at power-up via the software-access sequence
// (datasheet Fig. 6/7 -- 2 dummy reads + 2 writes at the
// highest chip address; the first write is a required 0x0000
// "unlock", the second carries the real value). Bit layout is
// the standard ISSI CellularRAM CR (verified against the
// sibling IS66WVE1M16BLL datasheet -- same CR layout is used
// across the whole BLL family; re-check against the exact
// -EBLL datasheet at hardware bring-up):
//
// bit 7 Page 1 = page-mode reads enabled
// bits6:5 TCR 11 = +85C refresh (matches power-on default)
// bit 4 Sleep 1 = PAR on ZZ# (matches power-on default)
// bits2:0 PAR 000 = full-array refresh (default)
//
// i.e. power-on default (0x0070) with only the Page bit set.
// ============================================================
localparam [DATA_WIDTH-1:0] CR_VALUE = 16'h00F0;
// ============================================================
// State machine
// ============================================================
localparam [3:0]
STATE_INIT = 4'd0,
STATE_IDLE = 4'd1,
STATE_READ = 4'd2,
STATE_WRITE = 4'd3,
STATE_WRITE_WAIT = 4'd4,
STATE_CR_INIT = 4'd5,
STATE_PAGE_OPEN = 4'd6,
STATE_PAGE_CLOSE = 4'd7,
STATE_PAGE_REOPEN = 4'd8;
reg [3:0] state;
reg [COUNTER_WIDTH-1:0] counter;
// ============================================================
// Latched transaction
// ============================================================
reg [ADDR_WIDTH-1:0] address_reg;
reg [DATA_WIDTH-1:0] wdata_reg;
reg wr_reg;
// ============================================================
// Latched byte enables
//
// Active LOW:
// 0 = byte enabled
// 1 = byte disabled
// ============================================================
reg lb_reg;
reg ub_reg;
// ============================================================
// Page-mode bookkeeping
// ============================================================
reg page_hit_reg; // current READ: fast (page) vs slow (tAA)
reg [HOLD_WIDTH-1:0] hold_cycles; // cycles CE# has been held low this session
// ============================================================
// Configuration-register load sequence
// ============================================================
reg cr_init_active;
reg [2:0] cr_step;
// ============================================================
// Early-request latch (bug found + fixed 2026-09-04, see
// WORKLOG.md flash-subsystem F2 entry for the full writeup)
//
// STATE_INIT (150us power-up wait) and STATE_CR_INIT (the 4-step
// software-access sequence) do not check `mem_req` at all -- an
// external request arriving during that window was previously
// silently LOST (mem_req is a one-cycle pulse from
// int8_memory_access.v with no retry), while the caller sat in
// its own WAIT state watching for `mem_ready`. Meanwhile
// STATE_READ/STATE_WRITE_WAIT's completion unconditionally
// pulsed the SAME external `mem_ready` for CR_INIT's own 4
// internal dummy-read/write steps too -- so the waiting caller
// would see one of THOSE stray pulses, believe its own (never
// actually issued) request had completed, and move on with
// garbage/no data. Two independent effects of the same root
// cause (CR_INIT reusing the external-facing datapath for
// internal housekeeping): fixed together below (this latch) and
// at both `mem_ready <= 1'b1` sites (guarded on `!cr_init_active`).
// ============================================================
reg req_pending;
reg [ADDR_WIDTH-1:0] pending_addr;
reg [DATA_WIDTH-1:0] pending_wdata;
reg pending_wr;
reg pending_lb_n;
reg pending_ub_n;
// ============================================================
// PSRAM data bus control
// ============================================================
reg [DATA_WIDTH-1:0] dq_out;
reg dq_oe;
assign psram_dq =
dq_oe ? dq_out : {DATA_WIDTH{1'bz}};
// ============================================================
// Main state machine
// ============================================================
always @(posedge clk) begin
if (rst) begin
// ----------------------------------------------------
// State
// ----------------------------------------------------
state <= STATE_INIT;
counter <= 0;
// ----------------------------------------------------
// Transaction registers
// ----------------------------------------------------
address_reg <= {ADDR_WIDTH{1'b0}};
wdata_reg <= {DATA_WIDTH{1'b0}};
wr_reg <= 1'b0;
// Byte enables disabled during reset
lb_reg <= 1'b1;
ub_reg <= 1'b1;
// ----------------------------------------------------
// Page-mode bookkeeping
// ----------------------------------------------------
page_hit_reg <= 1'b0;
hold_cycles <= 0;
cr_init_active <= 1'b0;
cr_step <= 0;
req_pending <= 1'b0;
pending_addr <= {ADDR_WIDTH{1'b0}};
pending_wdata <= {DATA_WIDTH{1'b0}};
pending_wr <= 1'b0;
pending_lb_n <= 1'b1;
pending_ub_n <= 1'b1;
// ----------------------------------------------------
// Memory interface
// ----------------------------------------------------
mem_rdata <= {DATA_WIDTH{1'b0}};
mem_ready <= 1'b0;
// ----------------------------------------------------
// PSRAM address
// ----------------------------------------------------
psram_a <= {ADDR_WIDTH{1'b0}};
// ----------------------------------------------------
// PSRAM control
// ----------------------------------------------------
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_we_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
psram_zz_n <= 1'b1;
// ----------------------------------------------------
// Data bus
// ----------------------------------------------------
dq_out <= {DATA_WIDTH{1'b0}};
dq_oe <= 1'b0;
end else begin
// mem_ready is a one-cycle pulse
mem_ready <= 1'b0;
// Latch (don't drop) a request that arrives while the
// controller is still busy with its own power-up/CR-init
// sequence -- see the req_pending declaration above for
// why this is needed. Only ever latches ONE request (the
// arbiter + int8_memory_access contract guarantees no
// caller issues a second req before its first is
// acknowledged, so this window can only ever have at
// most one outstanding request to remember).
if ((state == STATE_INIT || state == STATE_CR_INIT) &&
mem_req && !req_pending) begin
req_pending <= 1'b1;
pending_addr <= mem_addr;
pending_wdata <= mem_wdata;
pending_wr <= mem_wr;
pending_lb_n <= mem_lb_n;
pending_ub_n <= mem_ub_n;
end
case (state)
// =================================================
// PSRAM power-up initialization
// =================================================
STATE_INIT: begin
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_we_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
psram_zz_n <= 1'b1;
dq_oe <= 1'b0;
if (counter == INIT_CYCLES - 1) begin
counter <= 0;
cr_step <= 0;
state <= STATE_CR_INIT;
end else begin
counter <= counter + 1'b1;
end
end
// =================================================
// Configuration-register load
//
// Software-access sequence (datasheet Fig. 6):
// 2 dummy reads + 2 writes at the highest chip
// address, each a fully separate CE# pulse. The
// first write clocks in 0x0000 (unlock), the
// second clocks in the real CR value. Reuses the
// ordinary STATE_READ/STATE_WRITE datapath so it
// is checked by the exact same timing as every
// other transaction.
// =================================================
STATE_CR_INIT: begin
if (cr_step == 3'd4) begin
cr_init_active <= 1'b0;
state <= STATE_IDLE;
end else begin
cr_init_active <= 1'b1;
address_reg <= {ADDR_WIDTH{1'b1}};
lb_reg <= 1'b0;
ub_reg <= 1'b0;
psram_a <= {ADDR_WIDTH{1'b1}};
psram_lb_n <= 1'b0;
psram_ub_n <= 1'b0;
psram_ce_n <= 1'b0;
psram_zz_n <= 1'b1;
counter <= 0;
page_hit_reg <= 1'b0;
if (cr_step < 3'd2) begin
// Dummy READ steps
wr_reg <= 1'b0;
dq_oe <= 1'b0;
psram_we_n <= 1'b1;
psram_oe_n <= 1'b0;
state <= STATE_READ;
end else begin
// WRITE steps: 0x0000 unlock, then real CR value
wr_reg <= 1'b1;
wdata_reg <= (cr_step == 3'd2) ?
{DATA_WIDTH{1'b0}} : CR_VALUE;
dq_out <= (cr_step == 3'd2) ?
{DATA_WIDTH{1'b0}} : CR_VALUE;
dq_oe <= 1'b1;
psram_we_n <= 1'b0;
psram_oe_n <= 1'b1;
state <= STATE_WRITE;
end
cr_step <= cr_step + 1'b1;
end
end
// =================================================
// Idle
// =================================================
STATE_IDLE: begin
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_we_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
psram_zz_n <= 1'b1;
dq_oe <= 1'b0;
if (mem_req || req_pending) begin
// ------------------------------------------------
// Latch transaction -- from the live port if a
// fresh request arrived this cycle, otherwise
// from the early-request latch (see above)
// captured while STATE_INIT/STATE_CR_INIT was
// still running. `mem_req` takes priority
// (cannot both be true from a real caller given
// the one-outstanding-request contract, but if
// they ever were, the live request is the more
// recent one).
// ------------------------------------------------
address_reg <= mem_req ? mem_addr : pending_addr;
wdata_reg <= mem_req ? mem_wdata : pending_wdata;
wr_reg <= mem_req ? mem_wr : pending_wr;
// ------------------------------------------------
// Latch byte enables
// ------------------------------------------------
lb_reg <= mem_req ? mem_lb_n : pending_lb_n;
ub_reg <= mem_req ? mem_ub_n : pending_ub_n;
req_pending <= 1'b0;
// ------------------------------------------------
// Address
// ------------------------------------------------
psram_a <= mem_addr;
// ------------------------------------------------
// Apply byte enables immediately
// ------------------------------------------------
psram_lb_n <= mem_lb_n;
psram_ub_n <= mem_ub_n;
psram_ce_n <= 1'b0;
counter <= 0;
// =================================================
// WRITE
// =================================================
if (mem_wr) begin
dq_out <= mem_wdata;
dq_oe <= 1'b1;
psram_we_n <= 1'b0;
psram_oe_n <= 1'b1;
state <= STATE_WRITE;
end
// =================================================
// READ (fresh session -- always full tAA)
// =================================================
else begin
dq_oe <= 1'b0;
psram_we_n <= 1'b1;
psram_oe_n <= 1'b0;
page_hit_reg <= 1'b0;
hold_cycles <= 0;
state <= STATE_READ;
end
end
end
// =================================================
// READ
//
// Wait ACCESS_CYCLES (tAA, fresh/random access) or
// PAGE_CYCLES (tAPA, same-page continuation) as
// selected by page_hit_reg.
// =================================================
STATE_READ: begin
psram_ce_n <= 1'b0;
psram_oe_n <= 1'b0;
psram_we_n <= 1'b1;
psram_lb_n <= lb_reg;
psram_ub_n <= ub_reg;
psram_zz_n <= 1'b1;
dq_oe <= 1'b0;
hold_cycles <= hold_cycles + 1'b1;
if (counter ==
(page_hit_reg ? PAGE_CYCLES : ACCESS_CYCLES) - 1) begin
// ------------------------------------------------
// Capture PSRAM data
// ------------------------------------------------
mem_rdata <= psram_dq;
// Only a REAL external transaction's
// completion may pulse the external
// mem_ready -- CR_INIT's own 2 dummy-read
// steps reuse this same state but must never
// be visible to whatever caller happens to
// be waiting (see req_pending's declaration
// above for the full incident writeup).
if (!cr_init_active)
mem_ready <= 1'b1;
counter <= 0;
if (cr_init_active) begin
// Close between CR software-access-sequence
// steps (datasheet Fig. 6 -- 4 separate CE#
// pulses).
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
state <= STATE_CR_INIT;
end else begin
// Keep the page open: CE#/OE# stay
// asserted so a following same-page read
// can skip straight to a fast PAGE_CYCLES
// access instead of a full tAA.
state <= STATE_PAGE_OPEN;
end
end else begin
counter <= counter + 1'b1;
end
end
// =================================================
// PAGE OPEN
//
// A read just completed and CE#/OE# were left
// asserted. From here:
// - a same-page READ continues immediately with
// only the address/byte-enable lines changing
// (fast PAGE_CYCLES access) -- byte enables are
// free to change here too, since
// int8_memory_access.v alternates LB#/UB# on
// nearly every byte-granular access and the
// datasheet's page timing (Fig. 4) is defined
// purely on the address bus and CE#/OE#;
// - a different-page READ can also continue
// without a CE# toggle, but pays the full
// ACCESS_CYCLES for that one word (real chip
// behaviour: any change at A[4] or above needs
// a fresh tAA);
// - a WRITE, or exceeding the tCEM safety margin,
// closes the page first.
// =================================================
STATE_PAGE_OPEN: begin
psram_ce_n <= 1'b0;
psram_oe_n <= 1'b0;
psram_we_n <= 1'b1;
psram_lb_n <= lb_reg;
psram_ub_n <= ub_reg;
psram_zz_n <= 1'b1;
dq_oe <= 1'b0;
if (mem_req) begin
if (mem_wr ||
(hold_cycles >= PAGE_TIMEOUT_CYCLES)) begin
// Latch the new transaction, then close
// the page before servicing it.
address_reg <= mem_addr;
wdata_reg <= mem_wdata;
wr_reg <= mem_wr;
lb_reg <= mem_lb_n;
ub_reg <= mem_ub_n;
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
state <= STATE_PAGE_CLOSE;
end else begin
// READ continuation: address and byte
// enables change freely, CE#/OE# stay
// low. int8_memory_access.v alternates
// LB#/UB# on essentially every access
// (byte-granular reads over the 16-bit
// bus) so byte-enable changes are the
// common case, not an exception -- the
// datasheet's page-mode timing (Fig. 4)
// is defined purely on the address bus
// and CE#/OE#, and says nothing that
// requires LB#/UB# to stay fixed.
page_hit_reg <=
(mem_addr[ADDR_WIDTH-1:4] ==
address_reg[ADDR_WIDTH-1:4]);
address_reg <= mem_addr;
psram_a <= mem_addr;
lb_reg <= mem_lb_n;
ub_reg <= mem_ub_n;
psram_lb_n <= mem_lb_n;
psram_ub_n <= mem_ub_n;
counter <= 0;
state <= STATE_READ;
end
end else begin
// Idle inside an open page -- respect tCEM.
if (hold_cycles >= PAGE_TIMEOUT_CYCLES) begin
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
state <= STATE_IDLE;
end else begin
hold_cycles <= hold_cycles + 1'b1;
end
end
end
// =================================================
// PAGE CLOSE
//
// One fully-deasserted cycle before reopening for a
// WRITE (or a timed-out page): guarantees OE# has
// been high for a full cycle (>= tHZ) before the
// controller starts driving DQ, avoiding bus
// contention with the PSRAM's own output buffer.
// =================================================
STATE_PAGE_CLOSE: begin
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_we_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
psram_zz_n <= 1'b1;
dq_oe <= 1'b0;
state <= STATE_PAGE_REOPEN;
end
// =================================================
// PAGE REOPEN
//
// Dispatches the transaction latched just before
// STATE_PAGE_CLOSE, exactly like STATE_IDLE would.
// =================================================
STATE_PAGE_REOPEN: begin
psram_a <= address_reg;
psram_lb_n <= lb_reg;
psram_ub_n <= ub_reg;
psram_zz_n <= 1'b1;
counter <= 0;
if (wr_reg) begin
dq_out <= wdata_reg;
dq_oe <= 1'b1;
psram_ce_n <= 1'b0;
psram_we_n <= 1'b0;
psram_oe_n <= 1'b1;
state <= STATE_WRITE;
end else begin
dq_oe <= 1'b0;
psram_ce_n <= 1'b0;
psram_we_n <= 1'b1;
psram_oe_n <= 1'b0;
page_hit_reg <= 1'b0;
hold_cycles <= 0;
state <= STATE_READ;
end
end
// =================================================
// WRITE
// =================================================
STATE_WRITE: begin
psram_ce_n <= 1'b0;
psram_oe_n <= 1'b1;
psram_we_n <= 1'b0;
psram_lb_n <= lb_reg;
psram_ub_n <= ub_reg;
psram_zz_n <= 1'b1;
dq_oe <= 1'b1;
if (counter == ACCESS_CYCLES - 1) begin
// ------------------------------------------------
// End WE# pulse
// ------------------------------------------------
psram_we_n <= 1'b1;
counter <= 0;
state <= STATE_WRITE_WAIT;
end else begin
counter <= counter + 1'b1;
end
end
// =================================================
// WRITE WAIT
//
// Keep CE#/LB#/UB# active for the final write hold
// interval before releasing the transaction.
// =================================================
STATE_WRITE_WAIT: begin
psram_ce_n <= 1'b0;
psram_oe_n <= 1'b1;
psram_we_n <= 1'b1;
psram_lb_n <= lb_reg;
psram_ub_n <= ub_reg;
psram_zz_n <= 1'b1;
dq_oe <= 1'b0;
// ------------------------------------------------
// Release PSRAM
// ------------------------------------------------
psram_ce_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
// ------------------------------------------------
// Transaction complete -- same cr_init_active
// guard as the STATE_READ completion above (this
// is CR_INIT's own 2 write steps reusing this
// state too).
// ------------------------------------------------
if (!cr_init_active)
mem_ready <= 1'b1;
state <= cr_init_active ? STATE_CR_INIT : STATE_IDLE;
end
// =================================================
// Default recovery
// =================================================
default: begin
state <= STATE_INIT;
counter <= 0;
psram_ce_n <= 1'b1;
psram_oe_n <= 1'b1;
psram_we_n <= 1'b1;
psram_lb_n <= 1'b1;
psram_ub_n <= 1'b1;
psram_zz_n <= 1'b1;
dq_oe <= 1'b0;
lb_reg <= 1'b1;
ub_reg <= 1'b1;
page_hit_reg <= 1'b0;
hold_cycles <= 0;
cr_init_active <= 1'b0;
cr_step <= 0;
end
endcase
end
end
endmodule
File diff suppressed because it is too large Load Diff
+371
View File
@@ -0,0 +1,371 @@
`timescale 1ns/1ps
// ================================================================
// SPI_FLASH_MASTER
//
// SPI MASTER toward the boot/persistence NOR flash (Winbond
// W25Q128JV, confirmed part per docs/FPGA-Neural-Hardware-Design.md
// §6/§7 -- see sim/flash_model.v's header for the JEDEC-ID variant
// caveat). This is the FPGA's *only* path to that flash: the host
// never touches these pins directly (see the phase-plan's §0
// constraint) -- it issues opcodes through spi_engine, which this
// module (and, in later phases, the copy engine built on top of it)
// serves.
//
// Everything the existing design talks to (rtl/spi_slave.v) is an
// SPI SLAVE toward the host. This module is the mirror image: an
// SPI MASTER toward the flash, mode 0 (CPOL=0, CPHA=0), MSB-first,
// matching every timing diagram in the W25Q128JV datasheet (Fig.
// 7/28/30/43a): MOSI driven on the falling edge of SCLK (one edge
// ahead of the flash's own rising-edge sample), MISO sampled on the
// rising edge (the flash drove it on the previous falling edge).
//
// ----------------------------------------------------------------
// DEDICATED BUS, NO CCLK/USRMCLK SHARING (revised 2026-09-04)
// ----------------------------------------------------------------
// This master's 4 pins (sclk/mosi/miso/cs_n) are ALL ordinary GPIO,
// wired to a second, independent connection on the same flash chip
// -- the runtime persistence path is fully separate from the boot
// config-SPI path (which still uses the dedicated CCLK/DQ0/DQ1/CS
// sysCONFIG pins on their own, untouched by this module). No pin is
// shared between the two, and no ECP5 config-primitive (`USRMCLK`)
// is involved: `sclk` is driven the same way `mosi`/`cs_n` already
// are, a plain synchronous output, real from simulation straight
// through to place&route -- one identical `.lpf` entry like every
// other signal in this design, not a special MCLK-site placement.
//
// This design was originally built reusing the CCLK pad via
// `USRMCLK` (see git history / WORKLOG.md's Phase F1 entry for that
// version) to save one pin. That coupling was dropped: sharing the
// boot clock pad made the "exclusive flash SPI bus" claim misleading
// (electrically it wasn't independent of the config engine at all),
// and it carried a real unresolved verification gap (`USRMCLKTS`
// pad-enable timing was never checked against the primary Lattice
// sysCONFIG Usage Guide, FPGA-TN-02039 -- not present in this
// project's local document set). A 4th ordinary GPIO ball costs
// nothing on this part (huge pin headroom, docs/FPGA-Neural-
// Hardware-Design.md §2) and removes the coupling and the
// verification gap entirely.
// ----------------------------------------------------------------
//
// Command interface (byte-oriented, req/valid handshakes matching
// this codebase's existing conventions -- see rtl/spi_slave.v's
// rx_valid/tx_byte_req and rtl/mem_arbiter.v's req/ready):
//
// start -- one-cycle pulse, transaction accepted iff !busy
// opcode[7:0] -- flash instruction byte (RDID/READ/WREN/PP/SE/RDSR1)
// has_addr -- 1: send 3 address bytes (A23-A0) after opcode
// addr[23:0] -- address, sent MSB-first (matches every W25Q128JV
// instruction diagram: A23-A16, A15-A8, A7-A0)
// dir[1:0] -- DIR_NONE (opcode/addr only, e.g. WREN/SE),
// DIR_WRITE (stream n_data bytes TO the flash,
// e.g. PP), DIR_READ (stream n_data bytes FROM
// the flash, e.g. READ/RDID/RDSR1)
// n_data[15:0] -- byte count for the data phase (0 for DIR_NONE)
//
// wdata_req -- one-cycle pulse: master needs the next write
// byte now; caller responds (same cycle or later,
// this module simply waits, sclk idles low with
// CS still held low -- a legal SPI technique, no
// deselect-time constraint applies mid-transaction)
// with wdata_valid+wdata.
// wdata_valid -- one-cycle pulse, wdata is valid this cycle
// wdata[7:0]
//
// rdata_valid -- one-cycle pulse: rdata holds a freshly-received
// byte; master pauses (sclk idle, CS still low)
// until the caller acks.
// rdata[7:0]
// rdata_ack -- one-cycle pulse from caller: byte consumed,
// resume shifting.
//
// busy, done (one-cycle pulse on transaction completion)
//
// SCLK RATE -- §1 of the phase-plan prompt requires citing timing:
// the W25Q128JV(-DTR) datasheet's §9.6 AC Electrical Characteristics
// (p.90) caps the Read Data (03h) instruction specifically at
// fR=50MHz (all OTHER standard-SPI instructions allow up to
// 104-133MHz depending on VCC). Since this master uses one fixed
// divider for every instruction, it must honor the TIGHTEST of
// those limits. Default SCLK_DIV=2 at CLK_FREQ_MHZ=80 gives
// sclk = 80/(2*2) = 20MHz, comfortably under the 50MHz Read Data cap
// with margin for the rise/fall-time and setup/hold non-idealities
// this digital model does not represent (§A.6) -- correctness over
// speed, per the phase-plan's own §A.6/§8 guidance (this is an
// init/persistence path, not the inference hot path).
// ================================================================
module spi_flash_master #(
parameter CLK_FREQ_MHZ = 80,
parameter SCLK_DIV = 2 // sclk = CLK_FREQ_MHZ / (2*SCLK_DIV) MHz
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Physical pins toward the flash
// ------------------------------------------------------------
output reg mosi,
input wire miso,
output reg cs_n,
output wire sclk, // ordinary GPIO, real in both sim and synthesis -- see header
// ------------------------------------------------------------
// Command interface
// ------------------------------------------------------------
input wire start,
input wire [7:0] opcode,
input wire has_addr,
input wire [23:0] addr,
input wire [1:0] dir,
input wire [15:0] n_data,
output reg wdata_req,
input wire [7:0] wdata,
input wire wdata_valid,
output reg rdata_valid,
output reg [7:0] rdata,
input wire rdata_ack,
output wire busy,
output reg done
);
localparam DIR_NONE = 2'd0;
localparam DIR_WRITE = 2'd1;
localparam DIR_READ = 2'd2;
// ============================================================
// SCLK generator: free-running divider, gated by `shifting`
// (asserted only while actively clocking a bit; held with sclk
// low and CS still low during the WAIT_W/EMIT_R handshake
// pauses between data bytes).
// ============================================================
reg [15:0] div_cnt;
reg sclk_reg;
reg shifting;
wire sclk_half_reached = (div_cnt == SCLK_DIV - 1);
always @(posedge clk) begin
if (rst || !shifting) begin
div_cnt <= 16'd0;
sclk_reg <= 1'b0;
end else if (sclk_half_reached) begin
div_cnt <= 16'd0;
sclk_reg <= ~sclk_reg;
end else begin
div_cnt <= div_cnt + 16'd1;
end
end
wire sclk_will_rise = shifting & sclk_half_reached & ~sclk_reg; // about to go 0->1
wire sclk_will_fall = shifting & sclk_half_reached & sclk_reg; // about to go 1->0
assign sclk = sclk_reg;
// ============================================================
// Main FSM
// ============================================================
localparam ST_IDLE = 4'd0;
localparam ST_CS_SETTLE = 4'd1; // one clk cycle: CS asserted, sclk still idle (setup margin)
localparam ST_HDR = 4'd2; // shifting opcode (+ addr) out
localparam ST_DATA_WAIT_W = 4'd3; // paused: need next write byte from caller
localparam ST_DATA_SHIFT = 4'd4; // shifting one data byte (either direction)
localparam ST_DATA_EMIT_R = 4'd5; // paused: present a received byte, wait ack
localparam ST_CS_RELEASE = 4'd6; // one clk cycle: CS deasserted, settle
localparam ST_DONE = 4'd7;
reg [3:0] state;
reg [31:0] hdr_shift; // up to 32 bits: 8 opcode + 24 addr
reg [5:0] hdr_len; // total header bits for this transaction
reg [5:0] bit_idx; // bit position within the current chunk (header or one data byte)
reg [7:0] byte_shift; // current data byte, shifting
reg [15:0] data_idx; // completed data bytes so far
reg [15:0] data_total;
reg [1:0] cur_dir;
assign busy = (state != ST_IDLE);
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
cs_n <= 1'b1;
mosi <= 1'b0;
shifting <= 1'b0;
wdata_req <= 1'b0;
rdata_valid <= 1'b0;
rdata <= 8'h00;
done <= 1'b0;
hdr_shift <= 32'h0;
hdr_len <= 6'd0;
bit_idx <= 6'd0;
byte_shift <= 8'h00;
data_idx <= 16'd0;
data_total <= 16'd0;
cur_dir <= DIR_NONE;
end else begin
wdata_req <= 1'b0;
rdata_valid <= 1'b0;
done <= 1'b0;
case (state)
// --------------------------------------------
ST_IDLE: begin
shifting <= 1'b0;
if (start) begin
cs_n <= 1'b0;
hdr_shift <= has_addr ? {opcode, addr} : {opcode, 24'h0};
hdr_len <= has_addr ? 6'd32 : 6'd8;
bit_idx <= 6'd0;
data_idx <= 16'd0;
data_total <= n_data;
cur_dir <= dir;
mosi <= opcode[7]; // bit index 0, preloaded ahead of the first rising edge
state <= ST_CS_SETTLE;
end
end
// --------------------------------------------
ST_CS_SETTLE: begin
shifting <= 1'b1;
state <= ST_HDR;
end
// --------------------------------------------
// Generic bit shifter for the header (opcode+addr).
// MOSI updated on the falling edge (one edge ahead
// of the flash's rising-edge sample); bit_idx
// advances on the rising edge (the edge on which
// the flash actually captures the bit we set up on
// the PRECEDING falling edge).
// --------------------------------------------
ST_HDR: begin
if (sclk_will_fall) begin
// At this point bit_idx already equals the
// number of bits sampled so far (updated by
// the preceding rising edge, below), which
// is exactly the index of the NEXT bit to
// put on MOSI ahead of its own rising-edge
// sample -- e.g. after the 1st rising edge
// samples bit 0, bit_idx==1 and this falling
// edge must prepare bit 1 = hdr_shift[31-1].
if (bit_idx < hdr_len)
mosi <= hdr_shift[31 - bit_idx];
end
if (sclk_will_rise) begin
if (bit_idx == hdr_len - 1) begin
// Header done. Move to data phase or
// straight to CS release (DIR_NONE).
bit_idx <= 6'd0;
if (cur_dir == DIR_NONE || data_total == 16'd0) begin
shifting <= 1'b0;
state <= ST_CS_RELEASE;
end else if (cur_dir == DIR_WRITE) begin
shifting <= 1'b0;
wdata_req <= 1'b1;
state <= ST_DATA_WAIT_W;
end else begin // DIR_READ
state <= ST_DATA_SHIFT;
end
end else begin
bit_idx <= bit_idx + 6'd1;
end
end
end
// --------------------------------------------
ST_DATA_WAIT_W: begin
if (wdata_valid) begin
byte_shift <= wdata;
mosi <= wdata[7];
bit_idx <= 6'd0;
shifting <= 1'b1;
state <= ST_DATA_SHIFT;
end
end
// --------------------------------------------
// One data byte, either direction.
// --------------------------------------------
ST_DATA_SHIFT: begin
if (sclk_will_rise) begin
if (cur_dir == DIR_READ)
byte_shift <= {byte_shift[6:0], miso};
if (bit_idx == 6'd7) begin
data_idx <= data_idx + 16'd1;
if (cur_dir == DIR_READ) begin
shifting <= 1'b0;
rdata <= {byte_shift[6:0], miso};
rdata_valid <= 1'b1;
state <= ST_DATA_EMIT_R;
end else begin
if (data_idx + 16'd1 == data_total) begin
shifting <= 1'b0;
state <= ST_CS_RELEASE;
end else begin
shifting <= 1'b0;
wdata_req <= 1'b1;
state <= ST_DATA_WAIT_W;
end
end
end else begin
bit_idx <= bit_idx + 6'd1;
end
end
if (sclk_will_fall && cur_dir == DIR_WRITE) begin
// Same indexing rationale as ST_HDR above.
if (bit_idx < 6'd8)
mosi <= byte_shift[7 - bit_idx];
end
end
// --------------------------------------------
ST_DATA_EMIT_R: begin
if (rdata_ack) begin
if (data_idx == data_total) begin
state <= ST_CS_RELEASE;
end else begin
bit_idx <= 6'd0;
shifting <= 1'b1;
state <= ST_DATA_SHIFT;
end
end
end
// --------------------------------------------
ST_CS_RELEASE: begin
cs_n <= 1'b1;
state <= ST_DONE;
end
// --------------------------------------------
ST_DONE: begin
done <= 1'b1;
state <= ST_IDLE;
end
default: state <= ST_IDLE;
endcase
end
end
endmodule
+572
View File
@@ -0,0 +1,572 @@
`timescale 1ns/1ps
// ================================================================
// SPI_NEURON_TOP
//
// Full Phase 3 + Phase 4 integration: SPI host interface (spi_slave
// + spi_engine, docs §8.1) driving neuron_memory.v (Phase 3,
// N_NEURONS>=1) through a shared PSRAM (memory_interface +
// psram_controller), arbitrated between spi_engine's own RAM access
// (WRITE_RAM/READ_RAM opcodes) and neuron_memory's own X/W/bias
// reads during a run.
//
// neuron_memory's own `rst` is the global reset OR'd with the
// RESET opcode's soft-reset pulse from spi_engine, so a host can
// recover the compute engine over SPI without a physical reset
// (RAM contents are untouched either way).
// ================================================================
module spi_neuron_top #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 8,
parameter N_INPUTS = 32,
parameter N_NEURONS = 1,
parameter PARALLEL = 8,
parameter ACC_WIDTH = 32,
parameter MEM_DATA_WIDTH = 16,
parameter CLK_FREQ_MHZ = 80,
parameter N_LAYERS = 4, // Phase 5: RUN_NETWORK, requires N_INPUTS==N_NEURONS
parameter GRAPH_MAX_CONN = 32, // Phase G5: graph_engine's build-time max connections/neuron
parameter GRAPH_N_TOTAL = 4096 // Phase G5: graph_engine's activation buffer depth
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// SPI host interface
// ------------------------------------------------------------
input wire sclk,
input wire mosi,
output wire miso,
input wire cs_n,
// ------------------------------------------------------------
// Host attention pins, active-LOW (open-drain-style naming, but
// driven push-pull here -- no other master shares these lines).
//
// irq_n -- low while graph_engine's `err` is set (the
// §7 load-time guard tripped; STATUS.bit2).
// Stays low until RESET or a fresh graph
// run_start clears it, exactly like the
// STATUS bit it mirrors.
// data_ready_n -- low while a run's result is waiting to be
// read (STATUS.bit1, done/sticky). Goes back
// high the moment the host reads STATUS (or
// on RESET) -- same flip-flop as the SPI
// status byte, just also wired to a pin so
// the host does not have to poll SPI to find
// out a result is ready.
//
// Both are level signals from already-registered sticky bits
// (spi_engine.v's status_done_sticky, graph_engine.v's err), so
// driving them straight onto a pin (just an inversion) needs no
// extra pipeline stage / debounce.
// ------------------------------------------------------------
output wire irq_n,
output wire data_ready_n,
// ------------------------------------------------------------
// Flash physical interface (Phase F5, revised 2026-09-04) --
// fully independent 4-wire SPI bus, separate from the host SPI
// above AND from the dedicated boot config-SPI pins: this is the
// APPLICATION-side master toward the boot/persistence flash
// (rtl/spi_flash_master.v, owned internally by flash_slot_manager).
// All 4 signals (sclk/mosi/miso/cs_n) are ordinary GPIO, real in
// both simulation and synthesis -- no ECP5 config-primitive
// (USRMCLK/CCLK) involved, so no pin is shared with the boot
// path. See docs/FPGA-Neural-Hardware-Design.md §6/§7 for the
// board-level implication (the flash chip needs its own second
// physical connection for this bus, separate from its boot-SPI
// wiring).
// ------------------------------------------------------------
output wire flash_mosi,
input wire flash_miso,
output wire flash_cs_n,
output wire flash_sclk,
// ------------------------------------------------------------
// PSRAM physical interface
// ------------------------------------------------------------
output wire [ADDR_WIDTH-1:0] psram_a,
inout wire [MEM_DATA_WIDTH-1:0] psram_dq,
output wire psram_ce_n,
output wire psram_oe_n,
output wire psram_we_n,
output wire psram_lb_n,
output wire psram_ub_n,
output wire psram_zz_n
);
// ============================================================
// SPI PHYSICAL LAYER
// ============================================================
wire [7:0] rx_byte;
wire rx_valid;
wire cs_start;
wire cs_end;
wire [7:0] tx_byte;
wire tx_byte_req;
spi_slave u_spi_slave (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.cs_active(), .cs_start(cs_start), .cs_end(cs_end)
);
// ============================================================
// SPI PROTOCOL ENGINE
// ============================================================
wire spi_ram_req;
wire spi_ram_wr;
wire [ADDR_WIDTH-1:0] spi_ram_addr;
wire signed [7:0] spi_ram_wdata;
wire signed [7:0] spi_ram_rdata;
wire spi_ram_ready;
wire [ADDR_WIDTH-1:0] x_base;
wire [ADDR_WIDTH-1:0] w_base;
wire [ADDR_WIDTH-1:0] bias_addr;
wire [1:0] activation;
wire [15:0] n_inputs_real;
wire [15:0] n_neurons_real;
wire nm_start;
wire nm_busy;
wire nm_done;
wire signed [DATA_WIDTH*N_NEURONS-1:0] y_bus;
wire nm_soft_rst;
// Phase 5: layer_sequencer control/status, driven by spi_engine's
// RUN_NETWORK opcode.
wire [ADDR_WIDTH-1:0] table_base;
wire [ADDR_WIDTH-1:0] buf_a_base;
wire [ADDR_WIDTH-1:0] buf_b_base;
wire run_start;
wire [7:0] run_num_layers;
wire seq_busy;
wire seq_done;
// Phase G5: net_type dispatch + graph_engine control/status.
wire [7:0] net_type;
wire [15:0] num_neurons_graph;
wire [15:0] n_out;
wire graph_busy;
wire graph_done;
wire graph_err;
wire data_ready;
// Phase F5: flash_slot_manager command interface, driven by
// spi_engine's flash-subsystem opcodes.
wire flash_op_start;
wire [2:0] flash_op_code;
wire [3:0] flash_slot_id;
wire [23:0] flash_new_offset;
wire [23:0] flash_new_length;
wire [7:0] flash_new_type;
wire [ADDR_WIDTH-1:0] flash_ext_psram_addr;
wire [23:0] flash_ext_length;
wire [23:0] flash_raw_flash_addr;
wire flash_busy;
wire flash_done;
wire flash_err;
wire [3:0] flash_cat_read_sel;
wire [23:0] flash_cat_out_offset;
wire [23:0] flash_cat_out_length;
wire [7:0] flash_cat_out_type;
wire flash_cat_out_valid;
wire [31:0] flash_cat_out_crc;
spi_engine #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL),
.N_TOTAL(GRAPH_N_TOTAL)
) u_spi_engine (
.clk(clk), .rst(rst),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.cs_start(cs_start), .cs_end(cs_end),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.ram_req(spi_ram_req), .ram_wr(spi_ram_wr),
.ram_addr(spi_ram_addr), .ram_wdata(spi_ram_wdata),
.ram_rdata(spi_ram_rdata), .ram_ready(spi_ram_ready),
.x_base(x_base), .w_base(w_base), .bias_addr(bias_addr),
.activation(activation),
.n_inputs_real(n_inputs_real), .n_neurons_real(n_neurons_real),
.nm_start(nm_start), .nm_busy(nm_busy), .nm_done(nm_done),
.y_bus(y_bus),
.nm_soft_rst(nm_soft_rst),
.table_base(table_base), .buf_a_base(buf_a_base), .buf_b_base(buf_b_base),
.run_start(run_start), .run_num_layers(run_num_layers),
.seq_busy(seq_busy), .seq_done(seq_done),
.net_type(net_type),
.num_neurons_graph(num_neurons_graph), .n_out(n_out),
.graph_busy(graph_busy), .graph_done(graph_done), .graph_err(graph_err),
.flash_op_start(flash_op_start), .flash_op_code(flash_op_code),
.flash_slot_id(flash_slot_id),
.flash_new_offset(flash_new_offset), .flash_new_length(flash_new_length),
.flash_new_type(flash_new_type),
.flash_ext_psram_addr(flash_ext_psram_addr), .flash_ext_length(flash_ext_length),
.flash_raw_flash_addr(flash_raw_flash_addr),
.flash_busy(flash_busy), .flash_done(flash_done), .flash_err(flash_err),
.flash_cat_read_sel(flash_cat_read_sel),
.flash_cat_out_offset(flash_cat_out_offset), .flash_cat_out_length(flash_cat_out_length),
.flash_cat_out_type(flash_cat_out_type), .flash_cat_out_valid(flash_cat_out_valid),
.flash_cat_out_crc(flash_cat_out_crc),
.data_ready(data_ready)
);
// ============================================================
// FLASH_SLOT_MANAGER (Phase F5): owns the flash-facing SPI
// master (rtl/spi_flash_master.v) internally through
// rtl/flash_copy_engine.v; PSRAM access goes through
// mem_arbiter's Port D below.
// ============================================================
wire flash_d_req;
wire flash_d_wr;
wire [ADDR_WIDTH-1:0] flash_d_addr;
wire signed [7:0] flash_d_wdata;
wire signed [7:0] flash_d_rdata;
wire flash_d_ready;
flash_slot_manager #(
.PSRAM_ADDR_WIDTH(ADDR_WIDTH),
.CLK_FREQ_MHZ(CLK_FREQ_MHZ)
) u_flash_slot_manager (
.clk(clk), .rst(rst),
.mosi(flash_mosi), .miso(flash_miso), .cs_n(flash_cs_n), .sclk(flash_sclk),
.op_start(flash_op_start), .op_code(flash_op_code), .slot_id(flash_slot_id),
.new_offset(flash_new_offset), .new_length(flash_new_length), .new_type(flash_new_type),
.ext_psram_addr(flash_ext_psram_addr), .ext_length(flash_ext_length),
.raw_flash_addr(flash_raw_flash_addr),
.busy(flash_busy), .done(flash_done), .err(flash_err),
.cat_read_sel(flash_cat_read_sel),
.cat_out_offset(flash_cat_out_offset), .cat_out_length(flash_cat_out_length),
.cat_out_type(flash_cat_out_type), .cat_out_valid(flash_cat_out_valid),
.cat_out_crc(flash_cat_out_crc),
.d_req(flash_d_req), .d_wr(flash_d_wr), .d_addr(flash_d_addr), .d_wdata(flash_d_wdata),
.d_rdata(flash_d_rdata), .d_ready(flash_d_ready)
);
// Physical attention pins: active-low, driven straight from the
// already-registered sticky bits (see the port declarations
// above for the full rationale).
assign data_ready_n = ~data_ready;
assign irq_n = ~graph_err;
// ============================================================
// LAYER SEQUENCER (Phase 5: RUN_NETWORK)
//
// Requires N_INPUTS == N_NEURONS (both equal N_WIDTH below) --
// see rtl/layer_sequencer.v header for why. neuron_memory is
// shared with the legacy single-layer path: the two mux_nm_*
// wires below select which master drives it, based on seq_busy.
// ============================================================
wire [ADDR_WIDTH-1:0] seq_nm_x_base;
wire [ADDR_WIDTH-1:0] seq_nm_w_base;
wire [ADDR_WIDTH-1:0] seq_nm_bias_addr;
wire [1:0] seq_nm_activation;
wire [15:0] seq_nm_n_inputs;
wire [15:0] seq_nm_n_neurons;
wire seq_nm_start;
wire seq_ram_req;
wire seq_ram_wr;
wire [ADDR_WIDTH-1:0] seq_ram_addr;
wire signed [7:0] seq_ram_wdata;
wire signed [7:0] seq_ram_rdata;
wire seq_ram_ready;
// Phase G5: net_type dispatch. RUN_NETWORK pulses spi_engine's
// single `run_start` output; route it to whichever engine
// net_type selects (the two are mutually exclusive by
// construction -- spi_engine only accepts a new RUN_NETWORK
// while !busy_all, so at most one of layer_sequencer/graph_engine
// is ever mid-run).
localparam NET_TYPE_GRAPH = 8'h02;
wire seq_run_start = (net_type == NET_TYPE_GRAPH) ? 1'b0 : run_start;
wire graph_run_start = (net_type == NET_TYPE_GRAPH) ? run_start : 1'b0;
layer_sequencer #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_WIDTH(N_NEURONS),
.N_LAYERS(N_LAYERS)
) u_layer_sequencer (
.clk(clk), .rst(rst),
.run_start(seq_run_start), .run_num_layers(run_num_layers),
.seq_busy(seq_busy), .seq_done(seq_done),
.x_base(x_base), .table_base(table_base),
.buf_a_base(buf_a_base), .buf_b_base(buf_b_base),
.nm_x_base(seq_nm_x_base), .nm_w_base(seq_nm_w_base),
.nm_bias_addr(seq_nm_bias_addr), .nm_activation(seq_nm_activation),
.nm_n_inputs(seq_nm_n_inputs), .nm_n_neurons(seq_nm_n_neurons),
.nm_start(seq_nm_start),
.nm_busy(nm_busy), .nm_done(nm_done),
.y_bus(y_bus),
.ram_req(seq_ram_req), .ram_wr(seq_ram_wr),
.ram_addr(seq_ram_addr), .ram_wdata(seq_ram_wdata),
.ram_rdata(seq_ram_rdata), .ram_ready(seq_ram_ready)
);
// ============================================================
// GRAPH ENGINE (Phase G5: RUN_NETWORK, net_type == graph)
//
// Owns its own private act_buffer and neuron_parallel instance
// (see rtl/graph_engine.v); shares layer_sequencer's arbiter
// port C below since the two never run concurrently. Register
// reuse (x_base/table_base/buf_a_base-as-out_base/n_inputs_real-
// as-N_in) documented in graph_engine.v's own header.
// ============================================================
wire graph_ram_req;
wire graph_ram_wr;
wire [ADDR_WIDTH-1:0] graph_ram_addr;
wire signed [7:0] graph_ram_wdata;
wire signed [7:0] graph_ram_rdata;
wire graph_ram_ready;
// rst is the global reset OR'd with the SPI RESET opcode pulse,
// same convention as neuron_memory's nm_rst -- a host can clear
// a stuck `err` without a physical reset.
wire graph_rst = rst | nm_soft_rst;
graph_engine #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.ACC_WIDTH(ACC_WIDTH),
.PARALLEL(PARALLEL),
.MAX_CONN(GRAPH_MAX_CONN),
.N_TOTAL(GRAPH_N_TOTAL)
) u_graph_engine (
.clk(clk), .rst(graph_rst),
.run_start(graph_run_start), .busy(graph_busy), .done(graph_done), .err(graph_err),
.x_base(x_base), .table_base(table_base), .out_base(buf_a_base),
.n_inputs_graph(n_inputs_real),
.num_neurons_graph(num_neurons_graph), .n_out(n_out),
.ram_req(graph_ram_req), .ram_wr(graph_ram_wr),
.ram_addr(graph_ram_addr), .ram_wdata(graph_ram_wdata),
.ram_rdata(graph_ram_rdata), .ram_ready(graph_ram_ready)
);
// Arbiter port C mux: static on net_type (not on busy) -- the two
// engines are mutually exclusive by construction (see above), so
// whichever one net_type currently selects is the only one ever
// driving a real request through this port.
wire portc_req = (net_type == NET_TYPE_GRAPH) ? graph_ram_req : seq_ram_req;
wire portc_wr = (net_type == NET_TYPE_GRAPH) ? graph_ram_wr : seq_ram_wr;
wire [ADDR_WIDTH-1:0] portc_addr = (net_type == NET_TYPE_GRAPH) ? graph_ram_addr : seq_ram_addr;
wire signed [7:0] portc_wdata = (net_type == NET_TYPE_GRAPH) ? graph_ram_wdata : seq_ram_wdata;
wire signed [7:0] portc_rdata_bus;
wire portc_ready_bus;
assign seq_ram_rdata = portc_rdata_bus;
assign seq_ram_ready = portc_ready_bus;
assign graph_ram_rdata = portc_rdata_bus;
assign graph_ram_ready = portc_ready_bus;
// neuron_memory master mux: the sequencer owns it for the whole
// duration of a RUN_NETWORK job (seq_busy), otherwise spi_engine
// drives it directly (legacy single-layer SET_BASE/START path).
wire [ADDR_WIDTH-1:0] mux_nm_x_base = seq_busy ? seq_nm_x_base : x_base;
wire [ADDR_WIDTH-1:0] mux_nm_w_base = seq_busy ? seq_nm_w_base : w_base;
wire [ADDR_WIDTH-1:0] mux_nm_bias_addr = seq_busy ? seq_nm_bias_addr : bias_addr;
wire [1:0] mux_nm_activation = seq_busy ? seq_nm_activation : activation;
wire [15:0] mux_nm_n_inputs = seq_busy ? seq_nm_n_inputs : n_inputs_real;
wire [15:0] mux_nm_n_neurons = seq_busy ? seq_nm_n_neurons : n_neurons_real;
wire mux_nm_start = seq_busy ? seq_nm_start : nm_start;
// ============================================================
// NEURON MEMORY
//
// rst is the global reset OR'd with the SPI RESET opcode pulse.
// ============================================================
wire nm_rst = rst | nm_soft_rst;
wire nm_ram_req;
wire nm_ram_wr;
wire [ADDR_WIDTH-1:0] nm_ram_addr;
wire signed [7:0] nm_ram_wdata;
wire signed [7:0] nm_ram_rdata;
wire nm_ram_ready;
neuron_memory #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH)
) u_neuron_memory (
.clk(clk), .rst(nm_rst),
.start(mux_nm_start),
.mem_req(nm_ram_req), .mem_wr(nm_ram_wr),
.mem_addr(nm_ram_addr), .mem_wdata(nm_ram_wdata),
.mem_rdata(nm_ram_rdata), .mem_ready(nm_ram_ready),
.x_base(mux_nm_x_base), .w_base(mux_nm_w_base), .bias_addr(mux_nm_bias_addr),
.activation(mux_nm_activation),
.n_inputs_real(mux_nm_n_inputs), .n_neurons_real(mux_nm_n_neurons),
.y_bus(y_bus), .busy(nm_busy), .done(nm_done)
);
// ============================================================
// SHARED MEMORY ARBITER
// ============================================================
wire arb_req;
wire arb_wr;
wire [ADDR_WIDTH-1:0] arb_addr;
wire signed [7:0] arb_wdata;
wire signed [7:0] arb_rdata;
wire arb_ready;
mem_arbiter #(
.ADDR_WIDTH(ADDR_WIDTH)
) u_arbiter (
.clk(clk), .rst(rst),
.a_req(spi_ram_req), .a_wr(spi_ram_wr),
.a_addr(spi_ram_addr), .a_wdata(spi_ram_wdata),
.a_rdata(spi_ram_rdata), .a_ready(spi_ram_ready),
.b_req(nm_ram_req), .b_wr(nm_ram_wr),
.b_addr(nm_ram_addr), .b_wdata(nm_ram_wdata),
.b_rdata(nm_ram_rdata), .b_ready(nm_ram_ready),
.c_req(portc_req), .c_wr(portc_wr),
.c_addr(portc_addr), .c_wdata(portc_wdata),
.c_rdata(portc_rdata_bus), .c_ready(portc_ready_bus),
// Port D: flash_slot_manager (Phase F5), lowest priority --
// see mem_arbiter.v's own header for the full rationale.
.d_req(flash_d_req), .d_wr(flash_d_wr),
.d_addr(flash_d_addr), .d_wdata(flash_d_wdata),
.d_rdata(flash_d_rdata), .d_ready(flash_d_ready),
.m_req(arb_req), .m_wr(arb_wr),
.m_addr(arb_addr), .m_wdata(arb_wdata),
.m_rdata(arb_rdata), .m_ready(arb_ready)
);
// ============================================================
// BYTE <-> WORD BRIDGE (shared, single instance)
// ============================================================
wire i8_mem_req;
wire i8_mem_wr;
wire [ADDR_WIDTH-1:0] i8_mem_addr;
wire [MEM_DATA_WIDTH-1:0] i8_mem_wdata;
wire i8_mem_lb_n;
wire i8_mem_ub_n;
wire [MEM_DATA_WIDTH-1:0] i8_mem_rdata;
wire i8_mem_ready;
int8_memory_access #(
.ADDR_WIDTH(ADDR_WIDTH)
) u_int8_access (
.clk(clk), .rst(rst),
.req(arb_req), .wr(arb_wr), .addr(arb_addr), .wdata(arb_wdata),
.rdata(arb_rdata), .ready(arb_ready),
.mem_req(i8_mem_req), .mem_wr(i8_mem_wr),
.mem_addr(i8_mem_addr), .mem_wdata(i8_mem_wdata),
.mem_lb_n(i8_mem_lb_n), .mem_ub_n(i8_mem_ub_n),
.mem_rdata(i8_mem_rdata), .mem_ready(i8_mem_ready)
);
// ============================================================
// MEMORY INTERFACE / PSRAM CONTROLLER
// ============================================================
wire [MEM_DATA_WIDTH-1:0] psram_mem_rdata;
wire psram_mem_ready;
wire psram_mem_req;
wire psram_mem_wr;
wire [ADDR_WIDTH-1:0] psram_mem_addr;
wire [MEM_DATA_WIDTH-1:0] psram_mem_wdata;
wire psram_mem_lb_n;
wire psram_mem_ub_n;
memory_interface #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(MEM_DATA_WIDTH)
) u_memory_if (
.clk(clk), .rst(rst),
.req(i8_mem_req), .wr(i8_mem_wr), .addr(i8_mem_addr), .wdata(i8_mem_wdata),
.lb_n(i8_mem_lb_n), .ub_n(i8_mem_ub_n),
.rdata(i8_mem_rdata), .ready(i8_mem_ready),
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready)
);
psram_controller #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(MEM_DATA_WIDTH),
.CLK_FREQ_MHZ(CLK_FREQ_MHZ)
) u_psram_ctrl (
.clk(clk), .rst(rst),
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready),
.psram_a(psram_a), .psram_dq(psram_dq),
.psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n),
.psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n)
);
endmodule
+206
View File
@@ -0,0 +1,206 @@
`timescale 1ns/1ps
// ================================================================
// SPI SLAVE - physical layer
//
// SPI Mode 0 (CPOL=0, CPHA=0), MSB-first, single SPI.
// Per docs/FPGA-NeuralNetwork-Engine.md §8.1: the FPGA is always
// SPI slave; one command per CS-low period.
//
// SCLK/MOSI/CS_N arrive from an external, clock-asynchronous SPI
// master, so they are double-flop synchronized into the `clk`
// domain before any edge detection. This module only implements
// the byte-level shift register and CS framing; opcode/protocol
// decoding lives in spi_engine.v.
//
// Mode 0 timing: MOSI is sampled on the RISING edge of SCLK; MISO
// is driven on the FALLING edge (so it is stable well before the
// master's next rising-edge sample).
// ================================================================
module spi_slave (
input wire clk,
input wire rst,
// ------------------------------------------------------------
// External SPI pins
// ------------------------------------------------------------
input wire sclk,
input wire mosi,
output reg miso,
input wire cs_n,
// ------------------------------------------------------------
// Byte-level interface to spi_engine
// ------------------------------------------------------------
output reg [7:0] rx_byte,
output reg rx_valid, // one clk pulse: rx_byte is valid
// IMPORTANT / load-bearing contract:
// tx_byte_req is a PREFETCH hint, not a "byte consumed" event.
// It fires once at cs_fell (to load byte 1) and once more after
// EVERY byte's last bit (to have the next byte ready in time
// for MISO, in case the master keeps clocking) -- including
// after the LAST byte of a transaction, since the slave cannot
// know in advance that no further byte will follow until CS
// actually deasserts. A consumer MUST NOT treat tx_byte_req as
// a destructive "advance/pop the next byte" trigger, or it will
// over-advance by exactly one byte on every transaction (e.g.
// over-incrementing a RAM read pointer). Use `rx_valid` instead
// to advance any stateful pointer: it pulses exactly once per
// REAL byte transferred, never an extra time, because it is
// driven purely by counted SCLK edges that actually happened.
input wire [7:0] tx_byte, // next byte to shift out on MISO
output reg tx_byte_req, // one clk pulse: refresh tx_byte now (prefetch hint, see above)
output wire cs_active, // level: transaction in progress
output reg cs_start, // one clk pulse: CS just went low
output reg cs_end // one clk pulse: CS just went high
);
// ============================================================
// CDC SYNCHRONIZERS (double flip-flop)
// ============================================================
reg [2:0] sclk_sync;
reg [2:0] mosi_sync;
reg [2:0] cs_n_sync;
always @(posedge clk) begin
if (rst) begin
sclk_sync <= 3'b000;
mosi_sync <= 3'b000;
cs_n_sync <= 3'b111;
end else begin
sclk_sync <= {sclk_sync[1:0], sclk};
mosi_sync <= {mosi_sync[1:0], mosi};
cs_n_sync <= {cs_n_sync[1:0], cs_n};
end
end
wire sclk_s = sclk_sync[2];
wire mosi_s = mosi_sync[2];
wire cs_n_s = cs_n_sync[2];
// Edge detects on the synchronized (2-deep) signal using one
// extra history bit, so "rising"/"falling" mean the edge that
// just became visible to `clk`.
reg sclk_prev;
reg cs_n_prev;
always @(posedge clk) begin
if (rst) begin
sclk_prev <= 1'b0;
cs_n_prev <= 1'b1;
end else begin
sclk_prev <= sclk_s;
cs_n_prev <= cs_n_s;
end
end
wire sclk_rise = sclk_s & ~sclk_prev;
wire sclk_fall = ~sclk_s & sclk_prev;
wire cs_fell = ~cs_n_s & cs_n_prev; // CS just went active (low)
wire cs_rose = cs_n_s & ~cs_n_prev; // CS just went inactive (high)
assign cs_active = ~cs_n_s;
// ============================================================
// BIT COUNTER / SHIFT REGISTERS
// ============================================================
reg [2:0] bit_count; // 0..7, counts bits received/sent within a byte
reg [7:0] rx_shift;
reg [7:0] tx_shift;
always @(posedge clk) begin
if (rst) begin
bit_count <= 3'd0;
rx_shift <= 8'h00;
tx_shift <= 8'h00;
rx_byte <= 8'h00;
rx_valid <= 1'b0;
tx_byte_req <= 1'b0;
miso <= 1'b0;
cs_start <= 1'b0;
cs_end <= 1'b0;
end else begin
// ------------------------------------------------
// Default pulses
// ------------------------------------------------
rx_valid <= 1'b0;
tx_byte_req <= 1'b0;
cs_start <= 1'b0;
cs_end <= 1'b0;
if (cs_fell) begin
// New transaction: reset bit counter, arm the
// first tx byte load and pre-load MISO with its
// MSB so it is valid before the first SCLK rise.
bit_count <= 3'd0;
tx_shift <= tx_byte;
tx_byte_req <= 1'b1;
miso <= tx_byte[7];
cs_start <= 1'b1;
end else if (cs_rose) begin
cs_end <= 1'b1;
end else if (cs_active) begin
if (sclk_rise) begin
// Sample MOSI (mode 0: data valid on rising edge)
rx_shift <= {rx_shift[6:0], mosi_s};
if (bit_count == 3'd7) begin
bit_count <= 3'd0;
rx_byte <= {rx_shift[6:0], mosi_s};
rx_valid <= 1'b1;
end else begin
bit_count <= bit_count + 3'd1;
end
end else if (sclk_fall) begin
// Drive next MISO bit (mode 0: output changes
// on the falling edge, ahead of the next
// master-side rising-edge sample).
if (bit_count == 3'd0) begin
// A byte boundary just completed on the
// matching rising edge above; load the
// next tx byte now.
tx_shift <= tx_byte;
tx_byte_req <= 1'b1;
miso <= tx_byte[7];
end else begin
tx_shift <= {tx_shift[6:0], 1'b0};
miso <= tx_shift[6];
end
end
end
end
end
endmodule