Files
FPGA-Neural/hardware/v3/rtl/act_tile_fetch.v
T
micheleandClaude Sonnet 5 43a12379a5 feat: MILESTONE - real activation-fetch engine, full N=2 system verified on real DDR3 (EXP-0079)
Closes the last major disclosed functional gap: packed_slot.v's
activation data was read through a combinational stand-in since
EXP-0062. New act_tile_fetch.v reads activation tiles directly from
DDR3 (no on-chip buffering needed, unlike weights -- activation data
has no reuse), sharing each slot's existing ctrl port with its own
weight-prefetch engine. Real memory layout: one full BURST_LEN=8-word
burst per tile, deliberately avoiding any runtime-indexed part-select
given this project's thin P&R timing margin (EXP-0078).

Verified at three levels: act_tile_fetch.v alone (6/6), packed_slot.v
with real preloaded activation data (9/9), and the full N=2 system
against real DDR3 via xsim (8/8, 0 errors) -- the first time this
project's compute path has been verified end-to-end with real DDR3
for both weights and activations.

Retired hardware/v3/rtl/n2_system_top.v and its testbench (pre-DDR3
SDR-placeholder era, fully superseded by n2_system_ddr3_top.v).

Also: docs/PHYSICAL_REALIZATION.md (real pinout/parts/timing/protocol
reference for the physical board) and CLAUDE.md (persistent project
instructions for future Claude Code sessions).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
2026-09-20 08:13:15 +02:00

155 lines
6.6 KiB
Verilog

`timescale 1ns/1ps
// ============================================================
// V3 -- act_tile_fetch.v: REAL activation-tile fetch engine, closing
// the gap packed_slot.v's own header has disclosed since EXP-0062
// ("a real activation fetch engine ... is a separate, later
// deliverable, NOT built here"). This is that deliverable.
//
// WHY A SEPARATE, SIMPLE ENGINE (not a prefetch/buffer pair like the
// weight path): weights are reused across M reuse-positions per
// Director-dispatched pair, so prefetching them once into an on-chip
// buffer (layer_prefetch_ctrl.v/layer_weight_buffer.v) amortizes real
// DDR3 latency across many reads. Activation data has NO such reuse
// -- each position's activation tile is read exactly once per job --
// so buffering it on-chip would only add complexity for zero benefit.
// This engine reads DIRECTLY from DDR3 per tile instead.
//
// MEMORY LAYOUT CONVENTION (real, disclosed, and REQUIRED of whoever
// prepares activation data in DDR3 -- documented in the physical
// realization doc too): each activation tile (P_IN=8 INT8 values)
// occupies its OWN full BURST_LEN=8-word (128-bit) burst slot, in the
// LOW 64 bits, upper 64 bits unused padding. Tile index t's word
// address is therefore `base + t*BURST_LEN`, always burst-aligned by
// construction. This is DELIBERATELY wasteful of DDR3 capacity (2x)
// in exchange for AVOIDING a runtime-indexed part-select to pick
// which half of a shared burst holds the tile -- weight_tile_gather.v
// already established (EXP-0061) that pattern is a real Fmax risk,
// and this project's own P&R margin is currently thin (EXP-0078,
// WNS +0.013ns) -- not the moment to introduce a new critical path.
// A future denser packing (2 tiles/burst, real part-select) is a
// disclosed, deliberate follow-up, not done here.
//
// PROTOCOL: one request (`req` pulse + base_a/base_b/tcnt) triggers
// TWO SEQUENTIAL burst reads (lane A then lane B) over the SAME
// shared ctrl port packed_slot.v already owns -- reusing the EXACT
// port layer_prefetch_ctrl.v uses during S_PREFETCH, since that
// phase has already finished (weight data is on-chip by the time
// this engine runs) and the port is genuinely free. Follows the same
// combinational-first-grant discipline as every other one-shot-pulse
// requester in this project (EXP-0066): `mem_active` must be visible
// to the arbiter the SAME cycle it asserts, `ctrl_req` is only issued
// after `mem_grant` is observed, never blind.
// ============================================================
module act_tile_fetch #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter BURST_LEN = 8,
parameter ADDR_WIDTH = 25 // word address, matches the shared ctrl port's own convention
)(
input wire clk,
input wire rst,
input wire req, // one-shot pulse
input wire [ADDR_WIDTH-1:0] base_a,
input wire [ADDR_WIDTH-1:0] base_b,
input wire [15:0] tcnt,
output reg valid, // one-cycle pulse, data_a/data_b valid
output reg signed [DATA_WIDTH*P_IN-1:0] data_a,
output reg signed [DATA_WIDTH*P_IN-1:0] data_b,
output wire mem_active,
input wire mem_grant,
output reg ctrl_req,
output reg ctrl_wr,
output reg [ADDR_WIDTH-1:0] ctrl_addr,
output wire [16*BURST_LEN-1:0] ctrl_wdata,
output wire [2*BURST_LEN-1:0] ctrl_wmask,
input wire [16*BURST_LEN-1:0] ctrl_rdata,
input wire ctrl_ready,
input wire ctrl_busy
);
assign ctrl_wdata = {(16*BURST_LEN){1'b0}};
assign ctrl_wmask = {(2*BURST_LEN){1'b0}}; // read-only engine, mask unused
localparam S_IDLE = 3'd0,
S_MEMWAIT = 3'd1,
S_REQ_A = 3'd2,
S_GAP = 3'd3, // wait for ctrl_busy to clear before firing lane B's request
S_REQ_B = 3'd4;
reg [2:0] state;
reg [ADDR_WIDTH-1:0] base_a_lat, base_b_lat;
reg [15:0] tcnt_lat;
assign mem_active = (state != S_IDLE);
wire [ADDR_WIDTH-1:0] tile_offset = {{(ADDR_WIDTH-16){1'b0}}, tcnt_lat} * BURST_LEN[ADDR_WIDTH-1:0];
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
ctrl_req <= 1'b0; ctrl_wr <= 1'b0; ctrl_addr <= {ADDR_WIDTH{1'b0}};
valid <= 1'b0; data_a <= {(DATA_WIDTH*P_IN){1'b0}}; data_b <= {(DATA_WIDTH*P_IN){1'b0}};
base_a_lat <= {ADDR_WIDTH{1'b0}}; base_b_lat <= {ADDR_WIDTH{1'b0}}; tcnt_lat <= 16'd0;
end else begin
ctrl_req <= 1'b0;
valid <= 1'b0;
case (state)
S_IDLE: begin
if (req) begin
base_a_lat <= base_a;
base_b_lat <= base_b;
tcnt_lat <= tcnt;
state <= S_MEMWAIT;
end
end
S_MEMWAIT: begin
if (mem_grant) begin
ctrl_addr <= base_a_lat + tile_offset;
ctrl_wr <= 1'b0;
ctrl_req <= 1'b1;
state <= S_REQ_A;
end
end
S_REQ_A: begin
if (ctrl_ready) begin
data_a <= ctrl_rdata[0 +: DATA_WIDTH*P_IN];
ctrl_addr <= base_b_lat + tile_offset;
ctrl_wr <= 1'b0;
state <= S_GAP;
end
end
S_GAP: begin
// the shared controller may still be finishing its
// own internal completion sequence for lane A's
// request for one more cycle after ctrl_ready
// pulsed (mig_native_adapter.v's own S_DONE state
// keeps `busy` asserted through it) -- wait for
// !ctrl_busy before firing lane B's request,
// instead of assuming back-to-back is safe.
if (!ctrl_busy) begin
ctrl_req <= 1'b1;
state <= S_REQ_B;
end
end
S_REQ_B: begin
if (ctrl_ready) begin
data_b <= ctrl_rdata[0 +: DATA_WIDTH*P_IN];
valid <= 1'b1;
state <= S_IDLE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule