HomeHBM3 ControllerModule 4 — Page Policy
⚡ Phase 1 · Module 4 of 4

HBM3 Row Activation & Page Policy

Open-page, closed-page, and adaptive page policies with rolling hit rate measurement. Row hit/miss/conflict detection, ACT/PRE decision logic, and policy auto-switching — fully synthesizable Verilog with SV testbench.

📁 hbm3_page_policy.v 🧪 tb_hbm3_page_policy.sv ✅ Synthesizable RTL Open / Closed / Adaptive JEDEC JESD238

What Is a Page Policy and Why Does It Matter?

Every DRAM bank has a sense amplifier array that can hold one open row at a time. When a row is activated (ACTivated), its contents are latched into the sense amplifiers and the row stays "open" until a PRECHARGE (PRE) command closes it. The page policy decides what to do with that open row after an access completes.

This decision directly affects the bandwidth/latency tradeoff. Keeping a row open (open-page) gives near-zero overhead for repeated accesses to the same row but causes a tRP + tRCD penalty when the next access needs a different row. Closing the row immediately (closed-page) wastes tRP on every access but eliminates conflict penalties. The optimal choice depends entirely on the workload's spatial locality.

In HBM3, the difference between a row hit and a row conflict is 27 cycles (18 cycles tRCD saved vs tRP + tRCD penalty added). At 2 GHz, that is 13.5 ns per access — enough to lose 40+ GB/s of bandwidth on a random-access workload if you chose the wrong policy.

The Three Scenarios Every Controller Must Handle

When a new memory request arrives, the page policy engine checks the current state of the target bank and classifies the request into one of three scenarios:

Row Hit

The requested row is already open in the target bank. Issue only RD or WR. No ACT or PRE needed. Fastest path — saves tRCD entirely.

Latency: CL only

Row Miss (Empty)

The bank is precharged — no row is open. Issue ACT, wait tRCD, then issue RD/WR. Medium penalty — one extra timing step.

Latency: tRCD + CL

Row Conflict

A different row is open in the target bank. Must PRE the existing row, wait tRP, ACT the new row, wait tRCD, then RD/WR. Highest penalty.

Latency: tRP + tRCD + CL

Access Latency by Scenario

ScenarioCommands IssuedExtra Latency (cycles)Extra Latency (ns)Best Policy
Row hitRD / WR only00Open-page (keeps row open)
Row empty (miss)ACT → RD/WR+18 (tRCD)+9 nsEither (row must be opened)
Row conflictPRE → ACT → RD/WR+18 + 14 = +32 (tRP+tRCD)+16 nsClosed-page avoids this

tRCD = 18 cycles (9 ns) — RAS-to-CAS delay: time for sense amplifiers to resolve row data after ACTIVATE.
tRP = 14 cycles (7 ns) — Row Precharge time: time to restore bit-lines to idle voltage after PRECHARGE.
CL = 20 cycles (10 ns) — CAS latency: read pipeline delay from CAS command to first data valid at output pins.

Open-Page Policy

In open-page mode the controller never issues a PRECHARGE unless forced by a row conflict or refresh. After every RD or WR, the row stays in the sense amplifiers. If the next access hits the same row — a row hit — it gets through in just CL cycles with no overhead. This is ideal for:

The risk: every cross-row access becomes a row conflict (tRP + tRCD penalty), because there is always an open row that needs to be precharged first. On a random-access workload, open-page can be worse than closed-page by up to 1.8×.

Open-page policy with 32 banks means the controller can hold 32 simultaneously open rows — one per bank. The bank FSM (Module 2) tracks which row is open in each bank via the o_banks_open bitmap. The page policy engine consults i_open_row to determine if the incoming request hits the currently open row in the target bank.

Closed-Page Policy

In closed-page mode the controller issues a PRECHARGE immediately after every RD or WR command completes. No row stays open between accesses. The next access always finds an empty bank and must issue ACT (tRCD penalty), but it never incurs the tRP penalty — because there is no open row to precharge first.

The result: every access pays tRCD, but the latency is consistent and predictable. There are no conflict spikes. This makes closed-page the right choice for:

In closed-page mode, o_issue_pre is asserted after every accepted request so that the timing FSM can schedule the PRECHARGE before the next command arrives. The PRE command and the following idle tRP slot happen "for free" if the next request targets a different bank — the controller pipelines across banks during the precharge wait.

Adaptive Policy — Measuring and Switching Hit Rate

The adaptive policy monitors the actual row hit rate in a rolling 256-request window and selects open or closed page based on the measured result. This allows the controller to respond to workload phase changes without any software involvement.

Hit Rate Measurement

The module counts requests in an 8-bit counter (r_req_count). Every time a request is a row hit, an 8-bit hit counter (r_hit_count) increments. When r_req_count wraps past 255 (i.e. every 256 requests), the ratio r_hit_count / 256 is directly readable as an 8-bit fixed-point hit rate, where:

Policy Switching Logic

At each 256-request window boundary, the module evaluates:

The threshold of 128 (50%) is optimal for symmetric hit/miss cost distributions. In practice, because row conflicts cost more than misses, a lower threshold (e.g. 100 = ~39%) may give better overall performance. This is left as a parameter for tuning.

Timing Diagram — Hit / Miss / Conflict Side-by-Side

Page Policy — Row Hit vs Miss vs Conflict Timing ROW HIT ROW MISS (Empty) ROW CONFLICT addr REQ cmd RD/WR data DATA CL = 20 cy (no ACT or PRE needed) addr REQ cmd ACT RD/WR data DATA tRCD(18) + CL(20) = 38 cy (bank was idle — ACT required) addr REQ cmd PRE ACT RD/WR data DATA tRP(14)+tRCD(18)+CL(20)=52 cy (wrong row open — PRE+ACT) Hit: 20 cy — Best Case Miss: 38 cy — Medium Conflict: 52 cy — Worst All timings at 2 GHz · tRCD=18 cy (9 ns) · tRP=14 cy (7 ns) · CL=20 cy (10 ns) — JEDEC JESD238 HBM3

Port Reference — hbm3_page_policy

PortDirWidthDescription
i_clkin1System clock (2 GHz)
i_rst_nin1Active-low synchronous reset
i_req_validin1New memory request arriving this cycle
i_req_row[14:0]in15Row address of incoming request
i_req_bg[2:0]in3Target bank group (0–7)
i_req_ba[1:0]in2Target bank within group (0–3)
i_bank_activein1From bank FSM: target bank has an open row
i_open_row[14:0]in15Currently open row in target bank (from bank FSM)
i_policy_sel[1:0]in2Policy select: 00=open, 01=closed, 10=adaptive
o_page_hitout1Request targets currently open row — RD/WR directly
o_page_missout1Bank is idle — need ACT then RD/WR
o_page_conflictout1Different row open — need PRE + ACT + RD/WR
o_issue_preout1Assert to issue PRECHARGE (closed-page or conflict)
o_issue_actout1Assert to issue ACTIVATE (miss or conflict)
o_hit_rate[7:0]out8Rolling 256-request hit rate (8-bit, 128=50%)
o_policy_active[1:0]out2Effective policy: 00=open, 01=closed, 10=adaptive (same as input unless adaptive overrides)

Verilog Source — hbm3_page_policy.v

verilog · hbm3_page_policy.v
// ================================================================
//  hbm3_page_policy.v
//  HBM3 Page Policy Engine  —  Phase 1 · Module 4
//  Open-page, closed-page, and adaptive policy
//  Row hit/miss/conflict detection, rolling hit rate,
//  ACT/PRE decision logic
//  Synthesizable Verilog — EcrioniX HBM3 Controller Series
// ================================================================
`timescale 1ns/1ps
`default_nettype none

module hbm3_page_policy (
    input  wire        i_clk,
    input  wire        i_rst_n,

    // --- Incoming Request ---
    input  wire        i_req_valid,       // New request this cycle
    input  wire [14:0] i_req_row,         // Requested row address
    input  wire [2:0]  i_req_bg,          // Bank group (0-7)
    input  wire [1:0]  i_req_ba,          // Bank within group (0-3)

    // --- Current Bank State (from Bank FSM, Module 2) ---
    input  wire        i_bank_active,     // Target bank has open row
    input  wire [14:0] i_open_row,        // Currently open row in target bank

    // --- Policy Selection ---
    input  wire [1:0]  i_policy_sel,      // 00=open 01=closed 10=adaptive

    // --- Page Status Outputs ---
    output reg         o_page_hit,        // Request hits open row
    output reg         o_page_miss,       // Bank idle — need ACT
    output reg         o_page_conflict,   // Wrong row open — need PRE+ACT

    // --- Command Issue Signals ---
    output reg         o_issue_pre,       // Issue PRECHARGE
    output reg         o_issue_act,       // Issue ACTIVATE

    // --- Adaptive Policy Metrics ---
    output reg  [7:0]  o_hit_rate,        // Rolling hit rate (128 = 50%)
    output reg  [1:0]  o_policy_active    // Effective policy (may differ from sel in adaptive)
);

// Policy encoding constants
localparam POL_OPEN    = 2'b00;
localparam POL_CLOSED  = 2'b01;
localparam POL_ADAPTIVE= 2'b10;

// Adaptive threshold: 128/256 = 50%
localparam ADAPT_THRESH = 8'd128;

// ================================================================
//  Row Hit / Miss / Conflict Detection (combinational)
// ================================================================
reg w_hit, w_miss, w_conflict;

always @(*) begin
    w_hit      = 1'b0;
    w_miss     = 1'b0;
    w_conflict = 1'b0;
    if (i_req_valid) begin
        if (i_bank_active) begin
            if (i_open_row == i_req_row)
                w_hit = 1'b1;      // Same row — hit
            else
                w_conflict = 1'b1; // Different row — conflict
        end else begin
            w_miss = 1'b1;         // No open row — miss
        end
    end
end

// ================================================================
//  Rolling 256-Request Hit Rate Counter
// ================================================================
reg [7:0]  r_req_count;   // Counts 0-255, wraps at 256
reg [7:0]  r_hit_count;   // Hits in current window

always @(posedge i_clk) begin
    if (!i_rst_n) begin
        r_req_count <= 8'd0;
        r_hit_count <= 8'd0;
        o_hit_rate  <= 8'd0;
    end else if (i_req_valid) begin
        if (r_req_count == 8'd255) begin
            // Window boundary: publish hit rate and reset counters
            o_hit_rate  <= r_hit_count;    // hits out of 256 requests
            r_hit_count <= w_hit ? 8'd1 : 8'd0;
            r_req_count <= 8'd0;
        end else begin
            r_req_count <= r_req_count + 8'd1;
            if (w_hit)
                r_hit_count <= r_hit_count + 8'd1;
        end
    end
end

// ================================================================
//  Effective Policy Resolution (adaptive picks open or closed)
// ================================================================
reg [1:0] r_effective_policy;

always @(posedge i_clk) begin
    if (!i_rst_n) begin
        r_effective_policy <= POL_CLOSED; // Safe default
    end else begin
        case (i_policy_sel)
            POL_OPEN    : r_effective_policy <= POL_OPEN;
            POL_CLOSED  : r_effective_policy <= POL_CLOSED;
            POL_ADAPTIVE: begin
                // Switch every window boundary based on measured hit rate
                if (o_hit_rate > ADAPT_THRESH)
                    r_effective_policy <= POL_OPEN;
                else
                    r_effective_policy <= POL_CLOSED;
            end
            default: r_effective_policy <= POL_CLOSED;
        endcase
    end
end

// ================================================================
//  Page Status + Command Issue Outputs
// ================================================================
always @(posedge i_clk) begin
    if (!i_rst_n) begin
        o_page_hit      <= 1'b0;
        o_page_miss     <= 1'b0;
        o_page_conflict <= 1'b0;
        o_issue_pre     <= 1'b0;
        o_issue_act     <= 1'b0;
        o_policy_active <= POL_CLOSED;
    end else begin
        o_page_hit      <= w_hit;
        o_page_miss     <= w_miss;
        o_page_conflict <= w_conflict;
        o_policy_active <= r_effective_policy;

        // Default de-assert
        o_issue_pre <= 1'b0;
        o_issue_act <= 1'b0;

        if (i_req_valid) begin
            case (r_effective_policy)
                POL_OPEN: begin
                    // Hit: no commands (row already open)
                    // Miss: issue ACT
                    // Conflict: issue PRE + ACT
                    if (w_miss)     o_issue_act <= 1'b1;
                    if (w_conflict) begin o_issue_pre <= 1'b1; o_issue_act <= 1'b1; end
                end
                POL_CLOSED: begin
                    // Always close after access; need ACT for every new access
                    // Hit still gets RD/WR; but we always issue PRE after (handled externally)
                    // On next cycle, bank is idle → miss → ACT needed
                    if (w_hit || w_miss) o_issue_act <= !i_bank_active;
                    if (w_conflict)      begin o_issue_pre <= 1'b1; o_issue_act <= 1'b1; end
                    // Always schedule PRE after every access
                    o_issue_pre <= i_req_valid; // External logic must delay PRE by tWR
                end
                POL_ADAPTIVE: begin
                    // Resolved to open or closed above — reuse open/closed logic
                    if (o_hit_rate > ADAPT_THRESH) begin
                        // Behave as open-page
                        if (w_miss)     o_issue_act <= 1'b1;
                        if (w_conflict) begin o_issue_pre <= 1'b1; o_issue_act <= 1'b1; end
                    end else begin
                        // Behave as closed-page
                        if (!i_bank_active) o_issue_act <= 1'b1;
                        if (w_conflict)     begin o_issue_pre <= 1'b1; o_issue_act <= 1'b1; end
                        o_issue_pre <= i_req_valid;
                    end
                end
                default: begin end
            endcase
        end
    end
end

endmodule
`default_nettype wire

SystemVerilog Testbench — tb_hbm3_page_policy.sv

systemverilog · tb_hbm3_page_policy.sv
// ================================================================
//  tb_hbm3_page_policy.sv
//  Self-checking testbench for hbm3_page_policy
//  5 Tests: page hit, page miss, page conflict,
//           adaptive threshold crossing, closed-page PRE issue
// ================================================================
`timescale 1ns/1ps

module tb_hbm3_page_policy;

// ---- DUT Ports ----
logic        clk, rst_n;
logic        req_valid;
logic [14:0] req_row;
logic [2:0]  req_bg;
logic [1:0]  req_ba;
logic        bank_active;
logic [14:0] open_row;
logic [1:0]  policy_sel;

wire         page_hit, page_miss, page_conflict;
wire         issue_pre, issue_act;
wire  [7:0]  hit_rate;
wire  [1:0]  policy_active;

hbm3_page_policy dut (
    .i_clk          (clk),
    .i_rst_n        (rst_n),
    .i_req_valid    (req_valid),
    .i_req_row      (req_row),
    .i_req_bg       (req_bg),
    .i_req_ba       (req_ba),
    .i_bank_active  (bank_active),
    .i_open_row     (open_row),
    .i_policy_sel   (policy_sel),
    .o_page_hit     (page_hit),
    .o_page_miss    (page_miss),
    .o_page_conflict(page_conflict),
    .o_issue_pre    (issue_pre),
    .o_issue_act    (issue_act),
    .o_hit_rate     (hit_rate),
    .o_policy_active(policy_active)
);

// 2 GHz clock
initial clk = 0;
always #0.25 clk = ~clk;

int pass_cnt = 0, fail_cnt = 0;

task check(input string name, input logic got, input logic exp);
    if (got === exp) begin
        $display("  PASS  %s", name); pass_cnt++;
    end else begin
        $display("  FAIL  %s: got=%0b exp=%0b", name, got, exp); fail_cnt++;
    end
endtask

task tick(input int n = 1);
    repeat(n) @(posedge clk); #0.1;
endtask

task reset_dut;
    rst_n = 0; req_valid = 0; req_row = 0; req_bg = 0; req_ba = 0;
    bank_active = 0; open_row = 0; policy_sel = 2'b00;
    tick(4); rst_n = 1; tick(2);
endtask

// ================================================================
//  SVA Assertions
// ================================================================
// Only one of hit/miss/conflict can be high when req_valid
property one_hot_status;
    @(posedge clk) disable iff (!rst_n)
    req_valid |-> $onehot0({page_hit, page_miss, page_conflict});
endproperty
assert property (one_hot_status) else $error("SVA FAIL: multiple status bits asserted");

// issue_act must not fire on a page hit in open-page mode
property no_act_on_hit;
    @(posedge clk) disable iff (!rst_n)
    (page_hit && policy_active == 2'b00) |-> !issue_act;
endproperty
assert property (no_act_on_hit) else $error("SVA FAIL: ACT issued on page hit in open-page mode");

// ================================================================
//  TEST 1 — Page Hit (open-page, same row already active)
// ================================================================
task test1_page_hit;
    $display("\n[TEST 1] Page Hit — Open-Page Policy");
    reset_dut();
    policy_sel  = 2'b00;    // open-page
    bank_active = 1'b1;
    open_row    = 15'h0042; // row 0x42 is open
    req_row     = 15'h0042; // same row — hit
    req_bg      = 3'd0;
    req_ba      = 2'd0;
    req_valid   = 1'b1;
    tick(1); req_valid = 0; tick(1);
    check("page_hit asserted",    page_hit,     1'b1);
    check("page_miss NOT set",    page_miss,    1'b0);
    check("page_conflict NOT set",page_conflict,1'b0);
    check("issue_act NOT set",    issue_act,    1'b0);
endtask

// ================================================================
//  TEST 2 — Page Miss (open-page, bank idle)
// ================================================================
task test2_page_miss;
    $display("\n[TEST 2] Page Miss — Bank Idle");
    reset_dut();
    policy_sel  = 2'b00;    // open-page
    bank_active = 1'b0;     // bank is idle
    open_row    = 15'd0;
    req_row     = 15'h0010;
    req_valid   = 1'b1;
    tick(1); req_valid = 0; tick(1);
    check("page_miss asserted",   page_miss,    1'b1);
    check("page_hit NOT set",     page_hit,     1'b0);
    check("page_conflict NOT set",page_conflict,1'b0);
    check("issue_act asserted",   issue_act,    1'b1);
endtask

// ================================================================
//  TEST 3 — Page Conflict (different row open)
// ================================================================
task test3_page_conflict;
    $display("\n[TEST 3] Page Conflict — Different Row Open");
    reset_dut();
    policy_sel  = 2'b00;    // open-page
    bank_active = 1'b1;
    open_row    = 15'h00AA; // row 0xAA is open
    req_row     = 15'h00BB; // different row — conflict
    req_valid   = 1'b1;
    tick(1); req_valid = 0; tick(1);
    check("page_conflict asserted",page_conflict,1'b1);
    check("page_hit NOT set",      page_hit,     1'b0);
    check("page_miss NOT set",     page_miss,    1'b0);
    check("issue_pre asserted",    issue_pre,    1'b1);
    check("issue_act asserted",    issue_act,    1'b1);
endtask

// ================================================================
//  TEST 4 — Adaptive Threshold Crossing
// ================================================================
task test4_adaptive_threshold;
    $display("\n[TEST 4] Adaptive Policy — Threshold Crossing");
    reset_dut();
    policy_sel  = 2'b10;    // adaptive
    bank_active = 1'b1;
    open_row    = 15'h0050;

    // Send 256 all-hit requests — hit rate should exceed threshold
    req_row = 15'h0050; // always matches open_row
    repeat(256) begin
        req_valid = 1'b1; tick(1); req_valid = 0; tick(1);
    end
    tick(2);
    check("hit_rate high after 256 hits", hit_rate > 8'd128, 1'b1);

    // Send 256 all-conflict requests — hit rate should drop below threshold
    req_row = 15'h0099; // never matches open_row
    repeat(256) begin
        req_valid = 1'b1; tick(1); req_valid = 0; tick(1);
    end
    tick(2);
    check("hit_rate low after 256 conflicts", hit_rate < 8'd128, 1'b1);
endtask

// ================================================================
//  TEST 5 — Closed-Page PRE Issue
// ================================================================
task test5_closed_pre_issue;
    $display("\n[TEST 5] Closed-Page — PRE Issued on Every Access");
    reset_dut();
    policy_sel  = 2'b01;    // closed-page
    bank_active = 1'b1;
    open_row    = 15'h0007;
    req_row     = 15'h0007; // hit in closed-page mode
    req_valid   = 1'b1;
    tick(1); req_valid = 0; tick(1);
    check("page_hit in closed-page", page_hit,  1'b1);
    check("issue_pre in closed-page",issue_pre, 1'b1);
endtask

// ================================================================
//  Main
// ================================================================
initial begin
    $dumpfile("dump_pp.vcd"); $dumpvars(0, tb_hbm3_page_policy);
    test1_page_hit();
    test2_page_miss();
    test3_page_conflict();
    test4_adaptive_threshold();
    test5_closed_pre_issue();
    $display("\n========================================");
    $display("  RESULTS: %0d PASS  /  %0d FAIL", pass_cnt, fail_cnt);
    $display("========================================");
    $finish;
end

endmodule

Frequently Asked Questions

What is a DRAM page policy?

A page policy determines what the memory controller does with an activated DRAM row after completing a read or write. Under open-page policy, the row remains in the sense amplifiers for subsequent accesses. Under closed-page policy, a PRECHARGE command is issued immediately after each access, returning the bank to idle. The choice trades off row-hit bandwidth against row-conflict latency.

What is the difference between a row hit, miss, and conflict?

A row hit occurs when the requested row is already activated — only RD/WR is issued (CL latency). A row miss (empty bank) requires ACT + RD/WR (tRCD + CL latency). A row conflict requires PRE + ACT + RD/WR because a different row is already open (tRP + tRCD + CL — the most expensive path). Minimizing conflicts is the primary goal of any page policy engine.

When should I use open-page vs closed-page policy?

Open-page is ideal for high-locality workloads: sequential reads, matrix multiply, video decode. Closed-page suits random-access patterns: hash tables, pointer chasing, graph traversal. If you don't know the workload in advance — or it changes dynamically — adaptive policy measures the actual hit rate every 256 requests and switches automatically.

How does the adaptive policy measure the hit rate?

The module counts requests and row hits in a rolling 256-request window. At the window boundary, the 8-bit hit_count is exported directly as o_hit_rate (where 128 = 50%). If the measured rate exceeds 128, the controller adopts open-page behavior for the next window. Otherwise it uses closed-page. The window resets every 256 requests so the policy tracks phase changes in the workload without manual intervention.

What is tRCD and why does it matter?

tRCD (RAS-to-CAS Delay) is the minimum time after an ACTIVATE command before a READ or WRITE can be issued. During tRCD, the sense amplifiers are amplifying the tiny charge difference on the bit lines into a full logic level — accessing the row early would read unresolved, noisy data. In HBM3 at 2 GHz, tRCD = 18 cycles (9 ns). Eliminating tRCD via row hits is the single biggest latency saving available to a memory controller.