HomeHBM3 ControllerModule 1 — Timing FSM
⚡ Phase 1 · Module 1 of 4

HBM3 DRAM Timing FSM

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.

📁 hbm3_timing_fsm.v 🧪 tb_hbm3_timing_fsm.sv ✅ Synthesizable RTL 🕐 2 GHz target clock JEDEC JESD238

Why the Timing FSM Is the Foundation

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.

A DRAM timing FSM is essentially a safety interlock. Once a command is accepted, no other command can be issued until the countdown expires. The FSM physically prevents protocol violations that would corrupt data.

The 7 FSM States

S_IDLE
Bank fully precharged. Accepts ACT or REF commands only. Default power-up state.
S_ACTIVATING
ACT issued. Counting down tRCD (28 cycles). Row wordline raising, cells stabilising.
S_ACTIVE
Row open. Accepts RD, WR, PRE (if tRAS met) or REF (if tRAS met).
S_READING
RD issued. Counting CL (70 cycles). DQ bus driven by DRAM at CL expiry.
S_WRITING
WR issued. Counting CWL (36 cycles). Controller drives DQ starting at CWL expiry.
S_PRECHARGE
PRE issued. Counting tRP (28 cycles). Bitlines equalising, row deactivating.
S_REFRESH
REF issued. Counting tRFC (440 cycles). All rows internally refreshed. No access.

State Transition Diagram

S_IDLE precharged S_ACTIVATING cnt: tRCD S_ACTIVE row open S_READING cnt: CL S_WRITING cnt: CWL S_PRECHARGE cnt: tRP S_REFRESH cnt: tRFC cmd_act cnt==0 cmd_rd cnt==0 cmd_wr cnt==0 cmd_pre & tRAS_ok cnt==0 cmd_ref cmd_ref&tRAS_ok cnt==0

JEDEC Timing Parameters

All defaults calibrated for a 2 GHz controller clock (0.5 ns per cycle). Override via module parameters for different clock speeds.

ParameterSymbolCycles (2 GHz)TimeMeaning
RAS-to-CAS delaytRCD2814 nsACT → first RD or WR allowed
Row active mintRAS6432 nsACT → PRE minimum window
Precharge timetRP2814 nsPRE → next ACT on same bank
CAS read latencyCL7035 nsRD command → first DQ data bit
CAS write latencyCWL3618 nsWR command → controller drives DQ
Refresh cycle timetRFC440220 nsREF command duration — no access
Refresh intervaltREFI78003.9 µsMaximum gap between REF commands
Row cycle timetRC9246 nstRAS + tRP — ACT to ACT same bank

Port Reference

PortDirWidthDescription
clkin1Controller clock (2 GHz target)
rst_nin1Active-low synchronous reset
cmd_actin1ACTIVATE command — open a row
cmd_rdin1READ command — read from open row
cmd_wrin1WRITE command — write to open row
cmd_prein1PRECHARGE command — close row
cmd_refin1REFRESH command — all-bank refresh
readyout1FSM accepts a new command this cycle
row_openout1A row is currently activated
rd_validout1CL elapsed — sample DQ bus now
wr_readyout1CWL elapsed — drive DQ bus now
refreshingout1Refresh in progress — no access
timer[9:0]out10Active countdown value (debug/ILA)
state_dbg[2:0]out3FSM state encoding (debug/assertions)

Verilog Source — hbm3_timing_fsm.v

verilog · hbm3_timing_fsm.v
// ================================================================
//  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
tRAS runs concurrently with tRCD and any read/write countdown. The 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.

Timing Waveform

CLK CMD STATE READY ROW_OPEN RD_VALID ◄ tRCD ► ◄────── CL ──────► ◄ tRP ► ACT RD PRE IDLE ACTIVATING ACT READING ACT PRECH IDLE valid

SystemVerilog Testbench — tb_hbm3_timing_fsm.sv

systemverilog · tb_hbm3_timing_fsm.sv
// ================================================================
//  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

Frequently Asked Questions

What is tRCD in DRAM and why does it matter?

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).

Why is tRAS tracked with a separate counter?

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.

What happens if the scheduler tries to issue PRE before tRAS is met?

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.

Why does CL = 70 cycles seem so large?

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.

What does rd_valid actually signal in the controller?

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.

Can this module handle back-to-back reads on the same row?

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.