`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