Files
FPGA-Neural/rtl/psram_controller.v
T
micheleandClaude Sonnet 5 55c827bedf feat: PSRAM page-mode reads + graph engine (Type #2) + real pinout/IRQ pins
PSRAM page-mode read burst support in psram_controller.v: enables the
ISSI IS66WVE4M16EBLL-70BLI's page mode via its configuration-register
software-access sequence at boot (disabled by default on the real
chip), then keeps CE#/OE# asserted after a read so a same-page
continuation only pays tAPA (20ns) instead of a full tAA (70ns)
random access, with automatic tCEM-safe session closing. Only a WRITE
closes the page -- byte-enable changes do not, since
int8_memory_access.v alternates them on nearly every access and an
early implementation attempt that treated them as a close condition
measured a real regression (53.25->61.25 cycles/edge) before being
corrected (53.25->37.53 cycles/edge, +42% gather bandwidth).
sim/psram_model.v gained independent tAPA/tAA and tCEM enforcement
(with a real Verilog same-timestep event-ordering race found and
fixed via a #0 sync) so the regression proves real timing compliance,
not just data correctness. New sim/psram_page_mode_tb.v; full 26-file
regression suite re-run clean. Real nextpnr-ecp5 Fmax re-measured on
the full spi_neuron_top system: 75.73MHz (P2, up from 55.59MHz) and
65.13MHz (P8) -- still under the 80MHz target but not regressed, with
the critical path confirmed (not assumed) to remain entirely inside
neuron_parallel's accumulate chain, never psram_controller.

Also includes this session's other already-validated work: the graph
engine (Type #2 sparse-graph network: act_buffer, graph_engine,
netasm host assembler), real CABGA381 pinout (.lpf, place&route
verified) and physical IRQ_N/DATA_READY_N pins, and Phase 7 timing
closure logs -- all previously uncommitted, documented in WORKLOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LH3jPeJ3eFMfF2v8SQhpkk
2026-09-03 17:12:05 +02:00

783 lines
26 KiB
Verilog

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;
// ============================================================
// 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;
// ----------------------------------------------------
// 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;
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) begin
// ------------------------------------------------
// Latch transaction
// ------------------------------------------------
address_reg <= mem_addr;
wdata_reg <= mem_wdata;
wr_reg <= mem_wr;
// ------------------------------------------------
// Latch byte enables
// ------------------------------------------------
lb_reg <= mem_lb_n;
ub_reg <= mem_ub_n;
// ------------------------------------------------
// 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;
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
// ------------------------------------------------
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