Skip to content

MEM_RAM_49_DDR2

Source: Verilog/fpga/nexys4ddr/ddr2/MEM_RAM_49_DDR2.v

Where it sits (Nexys): nd120_nexys4ddr_top > ND120_CORE > ND3202D > MEM_43 > MEM_RAM_49_DDR2 - instance path: CORE.CPU_BOARD.MEM.RAM

Used in: MEM_43 (Nexys, MEGA65 R3)

Contains: no other modules.

Module hierarchy - All modules

MEM_RAM_49_DDR2 symbol

Schematic

Schematic not generated: netlistsvg did not finish in 300 s (16856 cells, 483 wires).

Description

ND120 CPU, MM&M MEM/RAM - DDR2 backend (Nexys 4 DDR) Drop-in replacement for the sheet-49 RAM (MEM_RAM_49) that maps the ND-120 DRAM protocol onto the board's 128 MiB DDR2 through nd_ddr2_port, with a BRAM cache in front so the common case answers at BRAM speed (one sysclk) and only a cache miss stretches the cycle. Protocol contract (measured, docs/nd120-dram-memory.md section 4; N = OSC cycle of the RAS rising edge): N : row on AA_9_0, MWRITE50_n and BANKx valid N+1 : column on AA_9_0 N+2 : CAS rises, write data DD_17_0_IN valid N+4 : read data must be on DD_17_0_OUT (held while CAS high) N+5 : RAS falls, CAS tail one more cycle Next access no earlier than N+11. WHY A CACHE + HOLD, NOT A DIRECT BRIDGE: the deadline above is fixed by PAL_44902A ("NO WAIT STATE ON CPU TO MEMORY WRITE") and the MIG's read latency is variable (its internal DDR2 refresh alone is tRFC = 127.5 ns), so a direct bridge cannot promise the deadline - see fpga/nexys4ddr/EXTENSIONS-PLAN.md. Instead: HIT : the cache (BRAM, synchronous read) is looked up at N+1 and serves data at the start of N+3 - the same registered timing the proven MEM_RAM_49_BLOCKRAM path used. MISS: MEM_HOLD freezes the two registered control PALs (PAL_44803A grants, PAL_44902A RAS/CAS FSM) via their HOLD input, which stretches the memory cycle. This is legal: the CPU's cycle FSM (PAL_44601B state e) waits on CGNTCACT_n = ~(CGNT|CACT) (PAL_44302B), which tracks the frozen grant LEVEL, and DMA masters wait on BDRY_n (PAL_44310D), whose set terms need the post-RAS phases that the freeze postpones. The DGA TOUT watchdog is fed by every COMPLETING cycle through BDRY50, so sub-microsecond stretches never trip it. CACHE: direct-mapped, write-through, 8-word lines (one 128-bit DDR2 transfer), no allocate on write miss. 2^CACHE_IDX_BITS lines; the line data lives in eight parallel 16-bit BRAMs so a refill is a single-cycle write of the whole line. Write-through means DDR2 always holds the truth, so cache contents stay coherent across a soft reset and there are no dirty lines to evict. All memory traffic - CPU and DMA alike - flows through this one sheet-49 port, so there is no other master to stay coherent with. CAPACITY: 2M x 16-bit words = BANK0 + BANK2 (1M words each) = 4 MB, the same map as the Tang's MEM_RAM_49_SDRAM: the board decode PAL (PAL_44445B) orders the banks BANK0, BANK2, BANK1 in physical addresses, so the contiguous 4 MB is BANK0+BANK2 and BANK1 reports absent (reads 0, writes dropped) for the boot-time size probing. PARITY: pack16 policy (docs/nd120-parity-analysis.md): only the 16 data bits are stored; DD[8]/DD[17] are regenerated as odd parity on the read path and CORR_n always reports "correct". DDR2 MAP: ND word address {bank, row[9:0], col[9:0]} (21 bits) maps one word per 16-bit DDR2 unit at units 0 .. 0x1FFFFF (bottom 4 MiB). The nd_storage region sits at REGION_BASE_UNITS = 0x2000000 (64 MiB in), so the two can never collide. Both clients reach the single nd_ddr2_port through nd_ddr2_arb at the board top. CLOCK DOMAINS: the protocol side runs on sysclk (= clk_cpu). One operation at a time crosses to ui_clk (the MIG user clock) as a toggle with the payload held stable behind it, completion comes back as a second toggle - the same shape nd_ddr2_storage uses. Last reviewed: 25-AUG-2026 Ronny Hansen

Parameters

Parameter Default
CACHE_IDX_BITS 14
WFIFO_LOG2 2

Ports

Direction Width Name Description
input 1 sysclk System clock in FPGA (from MEM_43.sysclk)
input 1 sys_rst_n (active low) System reset in FPGA (from MEM_43.sys_rst_n)
input [9:0] AA_9_0 10 bits of LBD (including parity in bit 10)- 10 bit input to MEM/RAM (from MEM_ADDR_44.AA_9_0)
input 1 BANK0
input 1 BANK1 absent third 1M bank: never stored, reads 0
input 1 BANK2 2nd populated 1M bank (phys 1M-2M)
input 1 CAS Column Address Strobe (from MEM_RAMC_50.CAS)
input 1 RAS Row Address Strobe (from MEM_RAMC_50.RAS)
input 1 MWRITE50_n (active low) Memory Write (Delayed 50ns) (from MEM_LBDIF_48.MWRITE50_n)
input [17:0] DD_17_0_IN
output [17:0] DD_17_0_OUT
output 1 CORR_n (active low)
output 1 MEM_HOLD Freeze both control PALs (MAIN_RAM_DDR2 cache-miss (to MEM_RAMC_50.MEM_HOLD)
input 1 ui_clk
input 1 ui_rst
output 1 mm_req_valid
output 1 mm_req_we
output [26:0] mm_req_addr
output [127:0] mm_req_wdata
output [15:0] mm_req_wmask
input 1 mm_req_ready
input 1 mm_rsp_valid
input [127:0] mm_rsp_rdata
output [7:0] DBG_BRIDGE

Verilog source

Verilog/fpga/nexys4ddr/ddr2/MEM_RAM_49_DDR2.v on GitHub.

Show the Verilog of MEM_RAM_49_DDR2 (601 lines)
/**************************************************************************
** ND120 CPU, MM&M                                                       **
** MEM/RAM - DDR2 backend (Nexys 4 DDR)                                  **
** Drop-in replacement for the sheet-49 RAM (MEM_RAM_49) that maps the   **
** ND-120 DRAM protocol onto the board's 128 MiB DDR2 through            **
** nd_ddr2_port, with a BRAM cache in front so the common case answers   **
** at BRAM speed (one sysclk) and only a cache miss stretches the cycle. **
**                                                                       **
** Protocol contract (measured, docs/nd120-dram-memory.md section 4;     **
** N = OSC cycle of the RAS rising edge):                                **
**   N   : row on AA_9_0, MWRITE50_n and BANKx valid                     **
**   N+1 : column on AA_9_0                                              **
**   N+2 : CAS rises, write data DD_17_0_IN valid                        **
**   N+4 : read data must be on DD_17_0_OUT (held while CAS high)        **
**   N+5 : RAS falls, CAS tail one more cycle                            **
**   Next access no earlier than N+11.                                   **
**                                                                       **
** WHY A CACHE + HOLD, NOT A DIRECT BRIDGE: the deadline above is fixed  **
** by PAL_44902A ("NO WAIT STATE ON CPU TO MEMORY WRITE") and the MIG's  **
** read latency is variable (its internal DDR2 refresh alone is tRFC =   **
** 127.5 ns), so a direct bridge cannot promise the deadline - see       **
** fpga/nexys4ddr/EXTENSIONS-PLAN.md. Instead:                           **
**   HIT : the cache (BRAM, synchronous read) is looked up at N+1 and    **
**         serves data at the start of N+3 - the same registered timing  **
**         the proven MEM_RAM_49_BLOCKRAM path used.                     **
**   MISS: MEM_HOLD freezes the two registered control PALs             **
**         (PAL_44803A grants, PAL_44902A RAS/CAS FSM) via their HOLD    **
**         input, which stretches the memory cycle. This is legal: the   **
**         CPU's cycle FSM (PAL_44601B state e) waits on CGNTCACT_n =    **
**         ~(CGNT|CACT) (PAL_44302B), which tracks the frozen grant      **
**         LEVEL, and DMA masters wait on BDRY_n (PAL_44310D), whose set **
**         terms need the post-RAS phases that the freeze postpones. The **
**         DGA TOUT watchdog is fed by every COMPLETING cycle through    **
**         BDRY50, so sub-microsecond stretches never trip it.           **
**                                                                       **
** CACHE: direct-mapped, write-through, 8-word lines (one 128-bit DDR2   **
** transfer), no allocate on write miss. 2^CACHE_IDX_BITS lines; the     **
** line data lives in eight parallel 16-bit BRAMs so a refill is a       **
** single-cycle write of the whole line. Write-through means DDR2 always **
** holds the truth, so cache contents stay coherent across a soft reset  **
** and there are no dirty lines to evict. All memory traffic - CPU and   **
** DMA alike - flows through this one sheet-49 port, so there is no      **
** other master to stay coherent with.                                   **
**                                                                       **
** CAPACITY: 2M x 16-bit words = BANK0 + BANK2 (1M words each) = 4 MB,   **
** the same map as the Tang's MEM_RAM_49_SDRAM: the board decode PAL     **
** (PAL_44445B) orders the banks BANK0, BANK2, BANK1 in physical         **
** addresses, so the contiguous 4 MB is BANK0+BANK2 and BANK1 reports    **
** absent (reads 0, writes dropped) for the boot-time size probing.      **
**                                                                       **
** PARITY: pack16 policy (docs/nd120-parity-analysis.md): only the 16    **
** data bits are stored; DD[8]/DD[17] are regenerated as odd parity on   **
** the read path and CORR_n always reports "correct".                    **
**                                                                       **
** DDR2 MAP: ND word address {bank, row[9:0], col[9:0]} (21 bits) maps   **
** one word per 16-bit DDR2 unit at units 0 .. 0x1FFFFF (bottom 4 MiB).  **
** The nd_storage region sits at REGION_BASE_UNITS = 0x2000000 (64 MiB   **
** in), so the two can never collide. Both clients reach the single      **
** nd_ddr2_port through nd_ddr2_arb at the board top.                    **
**                                                                       **
** CLOCK DOMAINS: the protocol side runs on sysclk (= clk_cpu). One      **
** operation at a time crosses to ui_clk (the MIG user clock) as a       **
** toggle with the payload held stable behind it, completion comes back  **
** as a second toggle - the same shape nd_ddr2_storage uses.             **
**                                                                       **
** Last reviewed: 25-AUG-2026                                            **
** Ronny Hansen                                                          **
***************************************************************************/
`default_nettype none

module MEM_RAM_49_DDR2 #(
    // log2 of the number of cache lines. 14 = 16K lines x 8 words = 128K
    // words = 256 KB of BRAM data (plus a 16K x 5 tag array).
    parameter CACHE_IDX_BITS = 14,
    // Write-posting FIFO depth (power of two). A full FIFO holds the access
    // (MEM_HOLD) until a slot frees.
    parameter WFIFO_LOG2 = 2
) (
    // Sheet-49 interface (same as MEM_RAM_49)
    input wire sysclk,       //! System clock in FPGA (from MEM_43.sysclk)
    input wire sys_rst_n,    //! System reset in FPGA (from MEM_43.sys_rst_n)

    input wire [9:0] AA_9_0,  //! 10 bits of LBD (including parity in bit 10)- 10 bit input to MEM/RAM (from MEM_ADDR_44.AA_9_0)
    input wire       BANK0,
    input wire       BANK1,  // absent third 1M bank: never stored, reads 0
    input wire       BANK2,  // 2nd populated 1M bank (phys 1M-2M)

    input wire CAS,          //! Column Address Strobe (from MEM_RAMC_50.CAS)
    input wire RAS,          //! Row Address Strobe (from MEM_RAMC_50.RAS)

    input wire MWRITE50_n,   //! Memory Write (Delayed 50ns) (from MEM_LBDIF_48.MWRITE50_n)

    input  wire [17:0] DD_17_0_IN,
    output wire [17:0] DD_17_0_OUT,

    output wire CORR_n,

    //! Freeze request to the two registered control PALs (PAL_44803A,
    //! PAL_44902A via MEM_RAMC_50.MEM_HOLD). High = hold their state.
    output wire MEM_HOLD,    //! Freeze both control PALs (MAIN_RAM_DDR2 cache-miss (to MEM_RAMC_50.MEM_HOLD)

    // DDR2 client port (ui_clk domain) - wired to nd_ddr2_arb at the top
    input  wire         ui_clk,
    input  wire         ui_rst,
    output reg          mm_req_valid,
    output reg          mm_req_we,
    output reg  [ 26:0] mm_req_addr,
    output reg  [127:0] mm_req_wdata,
    output reg  [ 15:0] mm_req_wmask,
    input  wire         mm_req_ready,
    input  wire         mm_rsp_valid,
    input  wire [127:0] mm_rsp_rdata,

    // Raw bridge state for an ILA (see build.tcl -tclargs ila)
    output wire [7:0] DBG_BRIDGE
);

  localparam TAG_BITS = 21 - 3 - CACHE_IDX_BITS;  // word addr is 21 bits

  /*******************************************************************************
   ** Access tracking (sysclk, follows the proven MEM_RAM_49_BLOCKRAM shape)    **
   *******************************************************************************/
  reg        ras_d;
  reg        win_d;
  reg  [9:0] row_q;
  reg        wn_q;    // 1 = read (MWRITE50_n at RAS rise)
  reg        bsel_q;  // populated bank selected (one-hot decode only)
  reg        bank_q;  // 0 = BANK0, 1 = BANK2
  reg [17:0] dd_q;    // write-data capture (settled pre-CAS value)

  // The idle/refresh bus state parks BANK0=BANK1=BANK2=1 (the RN18 pull-ups
  // in MEM_ADEC_45 win when no grant is out), and the refresh grant runs the
  // RAS/CAS sequence with no bank granted. Real decodes are one-hot, so
  // all-three-high is NOT an access - without this term a refresh cycle
  // would fake a read miss and freeze the PALs for nothing.
  wire bank_onehot = (BANK0 | BANK2) & ~(BANK0 & BANK1 & BANK2);

  wire win = RAS & CAS & bsel_q;

  // Registered word address: row/bank at the RAS edge, column one cycle later
  reg  [9:0] col_q;
  wire [20:0] word_addr = {bank_q, row_q, col_q};

  /*******************************************************************************
   ** Cache arrays - synchronous-read BRAM                                      **
   ** Eight 16-bit data arrays (one per word-in-line) + one tag array.          **
   ** initial-zero valid bits: BRAM contents power up cleared, and a soft      **
   ** reset keeps them VALID because the cache is write-through (DDR2 still    **
   ** holds the same data).                                                    **
   *******************************************************************************/
  (* ram_style = "block" *) reg [TAG_BITS:0] tags[0:(1<<CACHE_IDX_BITS)-1];  // {valid, tag}
  (* ram_style = "block" *) reg [15:0] cd0[0:(1<<CACHE_IDX_BITS)-1];
  (* ram_style = "block" *) reg [15:0] cd1[0:(1<<CACHE_IDX_BITS)-1];
  (* ram_style = "block" *) reg [15:0] cd2[0:(1<<CACHE_IDX_BITS)-1];
  (* ram_style = "block" *) reg [15:0] cd3[0:(1<<CACHE_IDX_BITS)-1];
  (* ram_style = "block" *) reg [15:0] cd4[0:(1<<CACHE_IDX_BITS)-1];
  (* ram_style = "block" *) reg [15:0] cd5[0:(1<<CACHE_IDX_BITS)-1];
  (* ram_style = "block" *) reg [15:0] cd6[0:(1<<CACHE_IDX_BITS)-1];
  (* ram_style = "block" *) reg [15:0] cd7[0:(1<<CACHE_IDX_BITS)-1];

  integer ii;
  initial begin
    for (ii = 0; ii < (1 << CACHE_IDX_BITS); ii = ii + 1) tags[ii] = 0;
  end

  // Lookup address, combinational at N+1 (row registered, column live on AA)
  wire [20:0] la = {bank_q, row_q, AA_9_0};
  wire [CACHE_IDX_BITS-1:0] l_idx = la[CACHE_IDX_BITS+2:3];

  // BRAM read registers: continuous synchronous read, NO reset - a reset on
  // these blocks Vivado's BRAM inference (Synth 8-3391, measured 25-AUG).
  // The values are only consumed the cycle after the A_COL lookup, when they
  // hold the N+1 index's data.
  reg [TAG_BITS:0] tag_rd;
  reg [15:0] crd0, crd1, crd2, crd3, crd4, crd5, crd6, crd7;
  always @(posedge sysclk) begin
    tag_rd <= tags[l_idx];
    crd0   <= cd0[l_idx];
    crd1   <= cd1[l_idx];
    crd2   <= cd2[l_idx];
    crd3   <= cd3[l_idx];
    crd4   <= cd4[l_idx];
    crd5   <= cd5[l_idx];
    crd6   <= cd6[l_idx];
    crd7   <= cd7[l_idx];
  end

  // Registered copies of the looked-up index/tag/offset (stable through the
  // rest of the access, incl. a frozen miss window)
  reg [CACHE_IDX_BITS-1:0] idx_q;
  reg [TAG_BITS-1:0] atag_q;
  reg [2:0] off_q;

  wire hit = tag_rd[TAG_BITS] && (tag_rd[TAG_BITS-1:0] == atag_q);

  reg [15:0] line_word;
  always @(*) begin
    case (off_q)
      3'd0: line_word = crd0;
      3'd1: line_word = crd1;
      3'd2: line_word = crd2;
      3'd3: line_word = crd3;
      3'd4: line_word = crd4;
      3'd5: line_word = crd5;
      3'd6: line_word = crd6;
      default: line_word = crd7;
    endcase
  end

  /*******************************************************************************
   ** Write-posting FIFO (sysclk) - write-through ordering:                     **
   ** the ui engine drains every posted write BEFORE it issues a refill read,   **
   ** so a read miss always sees its own preceding writes in DDR2.             **
   *******************************************************************************/
  localparam WDEPTH = 1 << WFIFO_LOG2;
  reg [36:0] wfifo[0:WDEPTH-1];  // {addr[20:0], data[15:0]}
  reg [WFIFO_LOG2:0] wcnt;
  reg [WFIFO_LOG2-1:0] wrp, wwp;
  wire wfifo_full = wcnt[WFIFO_LOG2];
  wire wfifo_empty = (wcnt == 0);

  // Enqueue/dequeue can land in the same sysclk cycle; wcnt is updated in ONE
  // place from these two strobes so the two paths can never race each other.
  // exactly one word enters the FIFO whenever a slot exists and either a new
  // write lands (wr_edge) or a parked write waits (wpend_v) - including the
  // wr_edge&wpend_v case, where the PARKED write is the one enqueued.
  wire wf_enq = (wr_edge | wpend_v) & ~wfifo_full;
  wire wf_deq = ~op_busy & ~wfifo_empty;

  /*******************************************************************************
   ** sysclk <-> ui_clk operation channel (one op outstanding)                  **
   *******************************************************************************/
  reg         op_tgl;      // sysclk -> ui request
  reg         op_we;
  reg  [20:0] op_addr;
  reg  [15:0] op_wdata;
  reg         op_busy;     // sysclk-side: op in flight
  reg         op_is_refill;
  reg         dn_s0, dn_s1, dn_s2;  // done toggle synced into sysclk

  wire op_done_edge = dn_s1 ^ dn_s2;

  // ui-side registers (declared here, written in the ui block below)
  reg         done_tgl;
  reg [127:0] rd_line;     // captured refill line - stable before done_tgl flips

  /*******************************************************************************
   ** Protocol / cache state machine (sysclk)                                   **
   *******************************************************************************/
  localparam A_IDLE = 3'd0;
  localparam A_COL  = 3'd1;  // N+1: AA carries the column; cache lookup issued
  localparam A_CHK  = 3'd2;  // N+2: tag compare -> hit serve / miss hold
  localparam A_MISS = 3'd3;  // frozen: waiting for the refill line
  localparam A_TAIL = 3'd4;  // access done (or absent bank): wait out RAS

  reg [2:0] astate;
  reg [17:0] dd_hold;
  reg have_data;
  reg hold_r;        // read-miss freeze
  reg whold_r;       // write with FIFO full freeze
  reg refill_pend;   // refill wanted (granted to the op channel after drain)
  reg wpend_v;       // captured write waiting for a FIFO slot
  reg [36:0] wpend;
  reg last_hit;      // DBG
  reg hit_q;         // hit verdict latched at A_CHK (see whit below)
  reg wovr_r;        // sticky: a write landed with wpend full AND FIFO full (write LOST)

  assign MEM_HOLD = hold_r | whold_r;

  // Write execution point: first win edge with MWRITE50_n low (the BLOCKRAM
  // discipline). dd_q holds the settled pre-CAS data.
  wire wr_edge = win & ~win_d & ~wn_q;

  always @(posedge sysclk) begin
    if (!sys_rst_n) begin
      ras_d       <= 0;
      win_d       <= 0;
      row_q       <= 0;
      col_q       <= 0;
      wn_q        <= 1;
      bsel_q      <= 0;
      bank_q      <= 0;
      dd_q        <= 0;
      idx_q       <= 0;
      atag_q      <= 0;
      off_q       <= 0;
      astate      <= A_IDLE;
      dd_hold     <= 0;
      have_data   <= 0;
      hold_r      <= 0;
      whold_r     <= 0;
      refill_pend <= 0;
      wpend_v     <= 0;
      wpend       <= 0;
      last_hit    <= 0;
      hit_q       <= 0;
      wovr_r      <= 0;
      wcnt        <= 0;
      wrp         <= 0;
      wwp         <= 0;
      op_tgl      <= 0;
      op_we       <= 0;
      op_addr     <= 0;
      op_wdata    <= 0;
      op_busy     <= 0;
      op_is_refill<= 0;
      dn_s0       <= 0;
      dn_s1       <= 0;
      dn_s2       <= 0;
    end else begin
      ras_d <= RAS;
      win_d <= win;

      // done-toggle sync
      dn_s0 <= done_tgl;
      dn_s1 <= dn_s0;
      dn_s2 <= dn_s1;

      // write data: capture every edge until CAS is seen high - the final
      // capture (the CAS-fall edge) holds the settled pre-CAS value
      if (RAS && !CAS) dd_q <= DD_17_0_IN;

      /**********************************************************************
       * Access sequencing
       **********************************************************************/
      case (astate)
        A_IDLE: begin
          if (RAS && !ras_d) begin  // N: RAS rise, AA carries the row
            row_q     <= AA_9_0;
            wn_q      <= MWRITE50_n;
            bsel_q    <= bank_onehot;
            bank_q    <= BANK2;
            have_data <= 0;
            astate    <= A_COL;
          end
        end

        A_COL: begin  // N+1: AA carries the column -> cache lookup
          col_q  <= AA_9_0;
          idx_q  <= l_idx;
          atag_q <= la[20:20-(TAG_BITS-1)];
          off_q  <= AA_9_0[2:0];
          // (the cache arrays are read continuously - see the read block)
          astate <= bsel_q ? A_CHK : A_TAIL;
        end

        A_CHK: begin  // N+2: hit/miss known
          last_hit <= hit;
          hit_q    <= hit;  // stable copy for a LATE wr_edge (tag_rd drifts)
          if (wn_q) begin
            if (hit) begin
              dd_hold   <= {~(^line_word[15:8]), line_word[15:8],
                            ~(^line_word[7:0]), line_word[7:0]};
              have_data <= 1;
              astate    <= A_TAIL;
            end else begin
              hold_r      <= 1;  // freeze the control PALs from the next edge
              refill_pend <= 1;
              astate      <= A_MISS;
            end
          end else begin
            // write: executed at wr_edge (below, same or a later cycle);
            // the sequence itself never needs stretching for a write unless
            // the posting FIFO is full - that case is handled at wr_edge.
            astate <= A_TAIL;
          end
        end

        A_MISS: begin
          if (op_done_edge && op_is_refill) begin
            // the whole line is in rd_line (stable: captured ui-side before
            // the toggle flipped). The array writes fire from the dedicated
            // single-port write block below (refill_wr).
            dd_hold <= {~(^rd_word[15:8]), rd_word[15:8],
                        ~(^rd_word[7:0]), rd_word[7:0]};
            have_data <= 1;
            hold_r    <= 0;  // release: the PAL sequence resumes next edge
            astate    <= A_TAIL;
          end
        end

        A_TAIL: begin
          if (!RAS) astate <= A_IDLE;
        end

        default: astate <= A_IDLE;
      endcase

      /**********************************************************************
       * Write execution: post to DDR2 (write-through), update on cache hit.
       * hit/idx_q/off_q are valid from N+2 on; wr_edge is at N+2 or later,
       * in the same access (win requires bsel_q, so absent banks drop here).
       **********************************************************************/
      if (wr_edge) begin
        // cache update on hit happens in the dedicated write block below
        if (wpend_v) begin
          if (!wfifo_full) begin
            // A parked write already waits: it is OLDER than this one, so it
            // must enter the FIFO first and the NEW write takes its place in
            // wpend. Enqueuing the new write directly would reorder the two
            // (wrong final value on a same-address pair) - 25-AUG audit BUG 1.
            wfifo[wwp] <= wpend;
            wwp        <= wwp + 1'b1;
            wpend      <= {word_addr, dd_q[16:9], dd_q[7:0]};
            // wpend_v and whold_r stay 1 until wpend itself drains
          end else begin
            // Two parked writes and no slot: whold_r (MEM_HOLD) is supposed
            // to make this unreachable. If it happens anyway the write is
            // LOST - latch the sticky error flag rather than corrupt order.
            wovr_r <= 1;
          end
        end else if (!wfifo_full) begin
          wfifo[wwp] <= {word_addr, dd_q[16:9], dd_q[7:0]};
          wwp        <= wwp + 1'b1;
        end else begin
          // FIFO full: park the write and stretch the cycle until it fits
          wpend   <= {word_addr, dd_q[16:9], dd_q[7:0]};
          wpend_v <= 1;
          whold_r <= 1;
        end
      end else if (wpend_v && !wfifo_full) begin
        wfifo[wwp] <= wpend;
        wwp        <= wwp + 1'b1;
        wpend_v    <= 0;
        whold_r    <= 0;
      end

      /**********************************************************************
       * Operation channel: drain writes first, then a pending refill.
       * (Order matters: the refill must observe every earlier write.)
       **********************************************************************/
      if (!op_busy) begin
        if (!wfifo_empty) begin
          op_we        <= 1;
          op_addr      <= wfifo[wrp][36:16];
          op_wdata     <= wfifo[wrp][15:0];
          op_is_refill <= 0;
          wrp          <= wrp + 1'b1;
          op_busy      <= 1;
          op_tgl       <= ~op_tgl;
        end else if (refill_pend && !wpend_v) begin
          // !wpend_v: a parked write not yet in the FIFO is still an EARLIER
          // write; a refill issued past it would cache the pre-write line
          // FOREVER (write-through, no dirty state) - 25-AUG audit BUG 2.
          op_we        <= 0;
          op_addr      <= {word_addr[20:3], 3'b000};
          op_wdata     <= 0;
          op_is_refill <= 1;
          refill_pend  <= 0;
          op_busy      <= 1;
          op_tgl       <= ~op_tgl;
        end
      end else if (op_done_edge) begin
        op_busy <= 0;
      end

      // single-site FIFO count update (enqueue/dequeue may coincide)
      wcnt <= wcnt + {{WFIFO_LOG2{1'b0}}, wf_enq} - {{WFIFO_LOG2{1'b0}}, wf_deq};
    end
  end

  /*******************************************************************************
   ** Cache array writes - exactly ONE write statement per array (Vivado can  **
   ** only infer BRAM from a single write port; the refill and the write-hit  **
   ** update are mutually exclusive by construction: a refill only happens on **
   ** a READ miss). Address is idx_q for both.                                **
   *******************************************************************************/
  wire refill_wr = (astate == A_MISS) & op_done_edge & op_is_refill;
  wire [15:0] wr_data16 = {dd_q[16:9], dd_q[7:0]};
  // 25-AUG stale-word root cause: `hit` is combinational from tag_rd, and
  // tag_rd reloads EVERY posedge from l_idx = f(live AA bus). wr_edge lands
  // at N+2 *or later* (stretched cycles, refresh interleave); by then AA has
  // moved on, tag_rd holds a FOREIGN line's tag, `hit` reads falsely 0 and
  // the cache update is dropped while DDR2 takes the write - one stale word
  // stays cached forever (silicon: SINTRAN spins on 056063 reading old
  // buffer content its own load had overwritten). Use the hit verdict
  // LATCHED at A_CHK, when tag_rd provably belongs to this access; the
  // same-edge case (wr_edge at the A_CHK posedge itself) still sees the
  // valid live value.
  wire whit = wr_edge & ((astate == A_CHK) ? hit : hit_q);

  always @(posedge sysclk) begin
    if (refill_wr) tags[idx_q] <= {1'b1, atag_q};
    if (refill_wr | (whit & (off_q == 3'd0))) cd0[idx_q] <= refill_wr ? rd_line[15:0]    : wr_data16;
    if (refill_wr | (whit & (off_q == 3'd1))) cd1[idx_q] <= refill_wr ? rd_line[31:16]   : wr_data16;
    if (refill_wr | (whit & (off_q == 3'd2))) cd2[idx_q] <= refill_wr ? rd_line[47:32]   : wr_data16;
    if (refill_wr | (whit & (off_q == 3'd3))) cd3[idx_q] <= refill_wr ? rd_line[63:48]   : wr_data16;
    if (refill_wr | (whit & (off_q == 3'd4))) cd4[idx_q] <= refill_wr ? rd_line[79:64]   : wr_data16;
    if (refill_wr | (whit & (off_q == 3'd5))) cd5[idx_q] <= refill_wr ? rd_line[95:80]   : wr_data16;
    if (refill_wr | (whit & (off_q == 3'd6))) cd6[idx_q] <= refill_wr ? rd_line[111:96]  : wr_data16;
    if (refill_wr | (whit & (off_q == 3'd7))) cd7[idx_q] <= refill_wr ? rd_line[127:112] : wr_data16;
  end

  // Word the miss actually asked for, picked from the freshly fetched line
  reg [15:0] rd_word;
  always @(*) begin
    case (off_q)
      3'd0: rd_word = rd_line[15:0];
      3'd1: rd_word = rd_line[31:16];
      3'd2: rd_word = rd_line[47:32];
      3'd3: rd_word = rd_line[63:48];
      3'd4: rd_word = rd_line[79:64];
      3'd5: rd_word = rd_line[95:80];
      3'd6: rd_word = rd_line[111:96];
      default: rd_word = rd_line[127:112];
    endcase
  end

  /*******************************************************************************
   ** ui_clk side: one DDR2 transfer per op toggle                              **
   *******************************************************************************/
  reg rq_s0, rq_s1, rq_s2;
  // op_we/op_addr/op_wdata are stable from the toggle flip until done comes
  // back, so they are safe to sample here once the synced toggle lands.
  wire [26:0] ddr2_units = {6'd0, op_addr};              // one word per 16-bit unit
  wire [15:0] wr_mask = ~(16'h0003 << {op_addr[2:0], 1'b0});  // active-low, 2 bytes

  localparam U_IDLE = 2'd0;
  localparam U_REQ  = 2'd1;
  localparam U_WAIT = 2'd2;
  reg [1:0] ustate;

  always @(posedge ui_clk) begin
    if (ui_rst) begin
      rq_s0        <= 0;
      rq_s1        <= 0;
      rq_s2        <= 0;
      ustate       <= U_IDLE;
      mm_req_valid <= 0;
      mm_req_we    <= 0;
      mm_req_addr  <= 0;
      mm_req_wdata <= 0;
      mm_req_wmask <= 16'hFFFF;
      done_tgl     <= 0;
      rd_line      <= 0;
    end else begin
      rq_s0 <= op_tgl;
      rq_s1 <= rq_s0;
      rq_s2 <= rq_s1;

      case (ustate)
        U_IDLE:
        if (rq_s1 ^ rq_s2) begin
          mm_req_we    <= op_we;
          mm_req_addr  <= {ddr2_units[26:3], 3'b000};
          mm_req_wdata <= {8{op_wdata}};
          mm_req_wmask <= op_we ? wr_mask : 16'h0000;
          mm_req_valid <= 1;
          ustate       <= U_REQ;
        end

        U_REQ:
        if (mm_req_valid && mm_req_ready) begin
          mm_req_valid <= 0;
          ustate       <= U_WAIT;
        end

        U_WAIT:
        if (mm_rsp_valid) begin
          rd_line  <= mm_rsp_rdata;   // captured BEFORE the toggle flips
          done_tgl <= ~done_tgl;
          ustate   <= U_IDLE;
        end

        default: ustate <= U_IDLE;
      endcase
    end
  end

  /*******************************************************************************
   ** Sheet-49 outputs: same gating as the SIP1M9 chips (drive 0 / parity 1     **
   ** when not selected or not reading, so downstream OR-combining works)       **
   *******************************************************************************/
  wire read_active = CAS & wn_q & bsel_q & have_data;

  assign DD_17_0_OUT = read_active ? dd_hold : 18'b0;
  assign CORR_n = read_active ? ((^dd_hold[8:0]) & (^dd_hold[17:9])) : 1'b1;

`ifdef ND120_ILA_MARK_DEBUG
  // 25-AUG loop hunt: full word address + direction of every access, for the
  // ILA (pattern *s_ila_maddr* in build.tcl ilaslim). [21]=read [20:0]=word.
  (* mark_debug = "true" *) wire [21:0] s_ila_maddr = {wn_q, word_addr};
  // the 16-bit DATA word of the access (parity bits dropped): reads show the
  // served word (dd_hold); writes show the CAPTURED WRITE DATA (dd_q) - the
  // value that actually enters the cache and the write FIFO. 25-AUG stale-
  // word hunt: a corrupt written value was invisible while this only showed
  // dd_hold.
  (* mark_debug = "true" *) wire [15:0] s_ila_mdata =
      wn_q ? {dd_hold[16:9], dd_hold[7:0]} : {dd_q[16:9], dd_q[7:0]};
`endif

  assign DBG_BRIDGE = {astate[2:0],   // [7:5]
                       MEM_HOLD,      // [4]
                       last_hit,      // [3]
                       refill_pend,   // [2]
                       op_busy,       // [1]
                       have_data};    // [0]

endmodule

`default_nettype wire