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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user