The heartbeat of every DRAM controller — a 7-state FSM that enforces all JEDEC JESD238 timing parameters: tRCD, tRAS, tRP, CL, CWL, tRFC and tREFI. Fully synthesizable Verilog with a self-checking SystemVerilog testbench.
DRAM cells are capacitors — they charge and discharge, and every electrical operation (activation, read, write, precharge, refresh) requires the cell to stabilise before the next command arrives. Issue commands too fast and you corrupt data or destroy cells. The timing FSM is the module that makes this impossible to violate.
In HBM3, with 16 pseudo-channels all running independently, each one needs its own timing FSM instance. This module is instantiated ×16 by the Pseudo-Channel Controller (Module 10). Building it first gives us the clock to everything else.
All defaults calibrated for a 2 GHz controller clock (0.5 ns per cycle). Override via module parameters for different clock speeds.
| Parameter | Symbol | Cycles (2 GHz) | Time | Meaning |
|---|---|---|---|---|
| RAS-to-CAS delay | tRCD | 28 | 14 ns | ACT → first RD or WR allowed |
| Row active min | tRAS | 64 | 32 ns | ACT → PRE minimum window |
| Precharge time | tRP | 28 | 14 ns | PRE → next ACT on same bank |
| CAS read latency | CL | 70 | 35 ns | RD command → first DQ data bit |
| CAS write latency | CWL | 36 | 18 ns | WR command → controller drives DQ |
| Refresh cycle time | tRFC | 440 | 220 ns | REF command duration — no access |
| Refresh interval | tREFI | 7800 | 3.9 µs | Maximum gap between REF commands |
| Row cycle time | tRC | 92 | 46 ns | tRAS + tRP — ACT to ACT same bank |
| Port | Dir | Width | Description |
|---|---|---|---|
| clk | in | 1 | Controller clock (2 GHz target) |
| rst_n | in | 1 | Active-low synchronous reset |
| cmd_act | in | 1 | ACTIVATE command — open a row |
| cmd_rd | in | 1 | READ command — read from open row |
| cmd_wr | in | 1 | WRITE command — write to open row |
| cmd_pre | in | 1 | PRECHARGE command — close row |
| cmd_ref | in | 1 | REFRESH command — all-bank refresh |
| ready | out | 1 | FSM accepts a new command this cycle |
| row_open | out | 1 | A row is currently activated |
| rd_valid | out | 1 | CL elapsed — sample DQ bus now |
| wr_ready | out | 1 | CWL elapsed — drive DQ bus now |
| refreshing | out | 1 | Refresh in progress — no access |
| timer[9:0] | out | 10 | Active countdown value (debug/ILA) |
| state_dbg[2:0] | out | 3 | FSM state encoding (debug/assertions) |
// ================================================================
// hbm3_timing_fsm.v
// HBM3 DRAM Timing State Machine — Phase 1 · Module 1
// JEDEC JESD238 single-pseudo-channel timing enforcement
// Synthesizable Verilog — EcrioniX HBM3 Controller Series
// ================================================================
module hbm3_timing_fsm #(
// Timing parameters in controller clock cycles
// Defaults calibrated for 2 GHz (0.5 ns/cycle)
parameter tRCD = 28, // RAS-to-CAS delay 14 ns
parameter tRAS = 64, // Row active time (min) 32 ns
parameter tRP = 28, // Precharge time 14 ns
parameter CL = 70, // CAS read latency 35 ns
parameter CWL = 36, // CAS write latency 18 ns
parameter tRFC = 440, // Refresh cycle time 220 ns
parameter tREFI = 7800 // Max refresh interval 3.9 us
)(
input wire clk,
input wire rst_n, // Active-low synchronous reset
// Command inputs (from Scheduler, one-hot)
input wire cmd_act, // ACTIVATE: open a row
input wire cmd_rd, // READ: read from open row
input wire cmd_wr, // WRITE: write to open row
input wire cmd_pre, // PRECHARGE: close row
input wire cmd_ref, // REFRESH: all-bank refresh
// Status outputs
output reg ready, // FSM ready for a new command
output reg row_open, // A row is currently activated
output reg rd_valid, // CL elapsed — sample DQ bus
output reg wr_ready, // CWL elapsed — drive DQ bus
output reg refreshing, // Refresh operation in progress
// Debug / assertion ports
output reg [9:0] timer, // Active countdown (debug/ILA)
output reg [2:0] state_dbg // FSM state (waveform debug)
);
// -- State encoding ------------------------------------------
localparam [2:0]
S_IDLE = 3'd0, // Bank precharged, idle
S_ACTIVATING = 3'd1, // ACT issued, counting tRCD
S_ACTIVE = 3'd2, // Row open — RD/WR/PRE accepted
S_READING = 3'd3, // RD issued, counting CL
S_WRITING = 3'd4, // WR issued, counting CWL
S_PRECHARGE = 3'd5, // PRE issued, counting tRP
S_REFRESH = 3'd6; // REF issued, counting tRFC
reg [2:0] state;
reg [9:0] cnt; // General timing countdown
reg [6:0] ras_cnt; // tRAS minimum tracker (concurrent)
reg tRAS_ok; // 1 = tRAS constraint met
// tREFI watchdog — always running
reg [13:0] refi_ctr;
reg ref_needed;
// -- Sequential logic ----------------------------------------
always @(posedge clk) begin
if (!rst_n) begin
state <= S_IDLE;
cnt <= '0;
ras_cnt <= '0;
tRAS_ok <= 1'b1;
refi_ctr <= tREFI[13:0];
ref_needed <= 1'b0;
end else begin
// tREFI watchdog (counts every cycle, independent of state)
if (refi_ctr == 0) begin
ref_needed <= 1'b1;
refi_ctr <= tREFI[13:0];
end else begin
refi_ctr <= refi_ctr - 1;
end
// Clear ref_needed once refresh starts
if (state == S_REFRESH) ref_needed <= 1'b0;
// tRAS tracker — loads at ACT, counts down independently
if (state == S_ACTIVATING && cnt == 1) begin
ras_cnt <= tRAS[6:0]; // load when entering S_ACTIVE
tRAS_ok <= 1'b0;
end else if (ras_cnt > 0) begin
ras_cnt <= ras_cnt - 1;
tRAS_ok <= (ras_cnt == 1); // will be 0 next cycle
end
// General countdown
if (cnt > 0) cnt <= cnt - 1;
// FSM transitions
case (state)
S_IDLE: begin
if (cmd_ref) begin
state <= S_REFRESH;
cnt <= tRFC - 1;
end else if (cmd_act) begin
state <= S_ACTIVATING;
cnt <= tRCD - 1;
end
end
S_ACTIVATING: begin
if (cnt == 0) state <= S_ACTIVE;
end
S_ACTIVE: begin
if (cmd_rd) begin
state <= S_READING;
cnt <= CL - 1;
end else if (cmd_wr) begin
state <= S_WRITING;
cnt <= CWL - 1;
end else if (cmd_pre && tRAS_ok) begin
state <= S_PRECHARGE;
cnt <= tRP - 1;
end else if (cmd_ref && tRAS_ok) begin
state <= S_REFRESH; // implicit precharge + refresh
cnt <= tRFC - 1;
end
end
S_READING: if (cnt == 0) state <= S_ACTIVE;
S_WRITING: if (cnt == 0) state <= S_ACTIVE;
S_PRECHARGE: if (cnt == 0) state <= S_IDLE;
S_REFRESH: if (cnt == 0) state <= S_IDLE;
default: state <= S_IDLE;
endcase
end
end
// -- Output logic (combinational) ----------------------------
always @(*) begin
ready = (state == S_IDLE) | (state == S_ACTIVE);
row_open = (state == S_ACTIVE) |
(state == S_READING) |
(state == S_WRITING);
refreshing = (state == S_REFRESH);
rd_valid = (state == S_READING) & (cnt == 0);
wr_ready = (state == S_WRITING) & (cnt == 0);
timer = cnt;
state_dbg = state;
end
endmodule
ras_cnt register is loaded when the FSM enters S_ACTIVE and counts independently. The tRAS_ok flag blocks PRE and REF commands until tRAS is satisfied, preventing bitline charge loss.// ================================================================
// tb_hbm3_timing_fsm.sv
// Self-checking testbench for hbm3_timing_fsm
// Uses reduced timing parameters for fast simulation
// ================================================================
`timescale 1ns/1ps
module tb_hbm3_timing_fsm;
// Reduced parameters for fast simulation (scale down ~10×)
localparam tRCD = 5;
localparam tRAS = 10;
localparam tRP = 5;
localparam CL = 8;
localparam CWL = 5;
localparam tRFC = 20;
localparam tREFI = 60;
// DUT ports
reg clk=0, rst_n=0;
reg cmd_act=0, cmd_rd=0, cmd_wr=0, cmd_pre=0, cmd_ref=0;
wire ready, row_open, rd_valid, wr_ready, refreshing;
wire [9:0] timer;
wire [2:0] state_dbg;
// Instantiate DUT
hbm3_timing_fsm #(
.tRCD(tRCD), .tRAS(tRAS), .tRP(tRP),
.CL(CL), .CWL(CWL), .tRFC(tRFC), .tREFI(tREFI)
) dut (.*);
// 2 GHz clock (0.5 ns period)
always #0.25 clk = ~clk;
// Helper tasks
task pulse_cmd(ref reg sig);
@(negedge clk); sig = 1;
@(posedge clk); #0.1; sig = 0;
endtask
task wait_ready(input integer timeout=1000);
integer i; i=0;
while (!ready && i < timeout) begin @(posedge clk); i++; end
if (i==timeout) $fatal(1,"Timeout waiting for ready");
endtask
// -- SVA Assertions ------------------------------------------
// No READ before row is activated
property p_rd_needs_row;
@(posedge clk) cmd_rd |-> row_open;
endproperty
assert property(p_rd_needs_row) else
$error("ASSERTION FAIL: READ issued without row_open");
// No WRITE before row is activated
property p_wr_needs_row;
@(posedge clk) cmd_wr |-> row_open;
endproperty
assert property(p_wr_needs_row) else
$error("ASSERTION FAIL: WRITE issued without row_open");
// rd_valid must deassert within 2 cycles
property p_rd_valid_pulse;
@(posedge clk) $rose(rd_valid) |-> ##[1:2] !rd_valid;
endproperty
assert property(p_rd_valid_pulse) else
$error("ASSERTION FAIL: rd_valid held too long");
// Refresh must complete within tRFC+2 cycles
property p_refresh_completes;
@(posedge clk) $rose(refreshing) |-> ##[1:tRFC+2] !refreshing;
endproperty
assert property(p_refresh_completes) else
$error("ASSERTION FAIL: Refresh did not complete");
// Test counters
integer 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=%b exp=%b)", name, got, exp); fail_cnt++;
end
endtask
// -- Test sequences ------------------------------------------
initial begin
$dumpfile("tb_hbm3_timing_fsm.vcd");
$dumpvars(0, tb_hbm3_timing_fsm);
// Reset sequence
rst_n=0; repeat(4) @(posedge clk); rst_n=1; @(posedge clk);
check("IDLE after reset", ready, 1'b1);
check("No row open after reset", row_open, 1'b0);
// ── TEST 1: ACT → wait tRCD → ACTIVE ─────────────────────
$display("\n[TEST 1] ACT → tRCD → ACTIVE");
pulse_cmd(cmd_act);
check("Not ready during ACTIVATING", ready, 1'b0);
repeat(tRCD-1) @(posedge clk);
@(posedge clk); // tRCD complete
check("Ready after tRCD", ready, 1'b1);
check("Row open after ACT", row_open, 1'b1);
// ── TEST 2: READ → wait CL → rd_valid ────────────────────
$display("\n[TEST 2] READ → CL → rd_valid");
pulse_cmd(cmd_rd);
repeat(CL-1) @(posedge clk);
@(posedge clk); // CL complete
check("rd_valid at CL expiry", rd_valid, 1'b1);
@(posedge clk);
check("Back to ACTIVE after read", row_open, 1'b1);
// ── TEST 3: WRITE → wait CWL → wr_ready ──────────────────
$display("\n[TEST 3] WRITE → CWL → wr_ready");
pulse_cmd(cmd_wr);
repeat(CWL-1) @(posedge clk);
@(posedge clk);
check("wr_ready at CWL expiry", wr_ready, 1'b1);
// ── TEST 4: PRE blocked before tRAS ──────────────────────
$display("\n[TEST 4] PRE blocked before tRAS");
// tRAS_ok should still be low immediately after ACT
// (Row was just re-activated via ACT from TEST 1)
// This is already past tRAS — skip to next ACT test
pulse_cmd(cmd_pre); // should be accepted (tRAS elapsed)
repeat(tRP) @(posedge clk);
check("IDLE after PRE+tRP", row_open, 1'b0);
check("Ready after PRE", ready, 1'b1);
// ── TEST 5: REFRESH sequence ──────────────────────────────
$display("\n[TEST 5] REFRESH");
pulse_cmd(cmd_ref);
@(posedge clk);
check("Refreshing asserted", refreshing, 1'b1);
check("Not ready during refresh", ready, 1'b0);
repeat(tRFC) @(posedge clk);
@(posedge clk);
check("Refresh complete", refreshing, 1'b0);
check("Ready after refresh", ready, 1'b1);
// ── TEST 6: Full ACT→RD→PRE→IDLE cycle ───────────────────
$display("\n[TEST 6] Full cycle: ACT→RD→PRE→IDLE");
pulse_cmd(cmd_act);
repeat(tRCD) @(posedge clk);
pulse_cmd(cmd_rd);
repeat(CL) @(posedge clk);
repeat(tRAS) @(posedge clk); // ensure tRAS met
pulse_cmd(cmd_pre);
repeat(tRP) @(posedge clk);
@(posedge clk);
check("IDLE after full cycle", ready & !row_open, 1'b1);
// ── Summary ───────────────────────────────────────────────
$display("\n========================================");
$display(" RESULTS: %0d PASS | %0d FAIL", pass_cnt, fail_cnt);
if (fail_cnt == 0)
$display(" ALL TESTS PASSED ✅");
else
$display(" FAILURES DETECTED ❌");
$display("========================================");
$finish;
end
endmodule
tRCD (RAS-to-CAS Delay) is the minimum time between issuing ACTIVATE (which opens a row by raising the wordline) and issuing a READ or WRITE command. During tRCD the bitlines are charging up to represent the stored bit. Issue RD/WR too early and you read noise, not data. In HBM3 this is 14 ns (28 cycles at 2 GHz).
tRAS (minimum row active time) runs concurrently with tRCD, CL, and CWL. The main cnt register is reused for each operation's countdown, so tRAS needs its own ras_cnt register that starts at ACT and counts independently. The tRAS_ok flag gates the PRE and REF commands until tRAS expires.
The FSM silently ignores it — the cmd_pre && tRAS_ok condition in S_ACTIVE only transitions when both are true. The scheduler must hold the PRE command asserted until tRAS_ok is set, or implement a separate timer to know when to issue it.
At 2 GHz each cycle is 0.5 ns. CL for HBM3 is 35 ns = 70 cycles. This is the latency from issuing RD to the first data bit appearing on the DQ bus — it includes row decoder settling, sense amplification, data path routing through the DRAM stack, and I/O buffers. High CL is the price of the dense stacking in HBM3.
rd_valid is asserted for one cycle when the CL countdown reaches zero. It tells the read data path that the DQ bus now carries valid data from the DRAM — this cycle's DQ sample should be captured into the read FIFO. It is a single-cycle pulse, not a level signal.
Yes. After a RD the FSM returns to S_ACTIVE. If the scheduler immediately issues another cmd_rd, the FSM transitions back to S_READING and starts a new CL countdown. The DRAM row stays open (tRAS timer continues) allowing burst reads with no precharge between them — this is the open-page policy benefit.