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.
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.
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:
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
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
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
| Scenario | Commands Issued | Extra Latency (cycles) | Extra Latency (ns) | Best Policy |
|---|---|---|---|---|
| Row hit | RD / WR only | 0 | 0 | Open-page (keeps row open) |
| Row empty (miss) | ACT → RD/WR | +18 (tRCD) | +9 ns | Either (row must be opened) |
| Row conflict | PRE → ACT → RD/WR | +18 + 14 = +32 (tRP+tRCD) | +16 ns | Closed-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.
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×.
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:
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.
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:
At each 256-request window boundary, the module evaluates:
r_hit_count > 128 → switch to or maintain open-page moder_hit_count ≤ 128 → switch to or maintain closed-page modeThe 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.
| Port | Dir | Width | Description |
|---|---|---|---|
| i_clk | in | 1 | System clock (2 GHz) |
| i_rst_n | in | 1 | Active-low synchronous reset |
| i_req_valid | in | 1 | New memory request arriving this cycle |
| i_req_row[14:0] | in | 15 | Row address of incoming request |
| i_req_bg[2:0] | in | 3 | Target bank group (0–7) |
| i_req_ba[1:0] | in | 2 | Target bank within group (0–3) |
| i_bank_active | in | 1 | From bank FSM: target bank has an open row |
| i_open_row[14:0] | in | 15 | Currently open row in target bank (from bank FSM) |
| i_policy_sel[1:0] | in | 2 | Policy select: 00=open, 01=closed, 10=adaptive |
| o_page_hit | out | 1 | Request targets currently open row — RD/WR directly |
| o_page_miss | out | 1 | Bank is idle — need ACT then RD/WR |
| o_page_conflict | out | 1 | Different row open — need PRE + ACT + RD/WR |
| o_issue_pre | out | 1 | Assert to issue PRECHARGE (closed-page or conflict) |
| o_issue_act | out | 1 | Assert to issue ACTIVATE (miss or conflict) |
| o_hit_rate[7:0] | out | 8 | Rolling 256-request hit rate (8-bit, 128=50%) |
| o_policy_active[1:0] | out | 2 | Effective policy: 00=open, 01=closed, 10=adaptive (same as input unless adaptive overrides) |
// ================================================================
// 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
// ================================================================
// 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
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.
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.
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.
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.
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.