Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
    • Controller and Datapath
    • Moore vs Mealy
    • State Encoding
    • Multi-Cycle Sequencers
      • The Sequencer Mindset
      • Shift-Add Multiply
      • Memory Read Sequencer with Latency
      • Sequencer Robustness
    • UART Transmitter
    • Memory Controller
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Multi-Cycle Sequencers
  1. Topics
  2. RTL Design
  3. FSM & Control
  4. Multi-Cycle Sequencers
  5. Memory Read Sequencer with Latency

Part 4 · FSM & Control · Intermediate

Memory Read Sequencer with Latency

A parameterized sequencer that issues an address, waits out RAM latency and captures data.

On this page

    Why this matters

    A synchronous memory answers L cycles after the address. If the sequencer captures one cycle early it returns the previous read's data, and that is the only off-by-one that is dangerous.

    After this lesson you can: build a read sequencer with a parameterised wait, and work out from the edge table when the data is available and when a consumer samples it.

    Why reads need a sequencer

    A synchronous memory returns data L cycles after the address is applied. Plain block RAM has L = 1.

    An output register or a bus adds cycles. The sequencer issues the address, waits a further L − 1 cycles, then captures the word it asked for.

    A parameterized wait

    The waveform shows a LAT = 3 read of address 5. The address goes out in ISSUE and the word returns in CAPTURE.

    • ISSUE: mem_en rises and address 5 goes out.

    • WAIT: two cycles pass while the RAM answers.

    • CAPTURE: mem_rdata holds 0x58 and data loads it at the end of this cycle.

    • Next cycle: done is high and data shows 0x58, so a consumer that registers data when it sees done gets the new word. The sequencer is back in IDLE and can accept the next req in this same cycle.

    Latency L

    States visited

    Cycles from ISSUE to CAPTURE

    1

    ISSUE → CAPTURE

    1

    2

    ISSUE → WAIT → CAPTURE

    2

    3

    ISSUE → WAIT → WAIT → CAPTURE

    3

    L

    ISSUE, L − 1 × WAIT, CAPTURE

    L

    Tip

    Make latency a parameter and test several values (the test below runs 1 to 5 together). The usual maintenance bug is changing the RAM's output register, which changes L, and forgetting the sequencer; the parameter turns that into a one-line change that the test checks.

    Common pitfall

    A done that is combinational from the state can announce a result one cycle before the data register holds it. The consumer that registers data on the edge where done is high then captures the previous read. Register done with the data, so that both change on the same edge.

    Common pitfall

    Off-by-one on the wait counter either captures one cycle too early (stale data from the previous read) or too late (wasted cycle, still correct). Only the early case is dangerous, so test it: shortening the wait by one cycle makes the test fail for every latency above 1.

    Check yourself For L = 4, how many WAIT cycles are there, and in which cycle after ISSUE is the data captured?

    Three WAIT cycles (L − 1). CAPTURE occurs four cycles after ISSUE, the cycle in which the RAM's output holds the requested word. done and the registered data then appear one cycle later, five cycles after ISSUE.

    Edge-by-edge timing

    Use acceptance as edge 0: ISSUE launches the RAM read at edge 1, the first of L registered pipeline edges; RAM data is available after edge L. CAPTURE registers data and done after edge L + 1. A downstream register consumes them at edge L + 2, which can also accept the next request. Availability latency is L + 1 periods; consumer latency and minimum initiation interval are L + 2. Counting the accepting slot inclusively gives L + 2 slots, a different observation.

    Edge (LAT = 3)

    Action / post-edge result

    0

    Accept address; enter ISSUE

    1

    RAM launches read; enter WAIT

    2

    WAIT; RAM pipeline advances

    3

    Enter CAPTURE; RAM word becomes available

    4

    Register data and done; return IDLE

    5

    Consumer captures data; next request may be accepted

    Legal parameters are LAT >= 1, AW >= 1 and DW >= 1. The initial diagnostic is a time-zero simulation check; it cannot guarantee that a tool accepts malformed ranges before simulation. Zero-latency combinational memory needs a different contract.

    Pipelined reads

    This sequencer starts one read at a time. For one word per cycle, issue a new address every cycle instead. Delay a valid bit and the destination tag by L stages to meet the returning data, as in the pipeline pattern.

    Testbench

    The sequencer, a latency-parameterized memory model, and the sweep over latencies 1 to 5 plus an AW=DW=1 boundary instance. Each starts with addresses zero and all-ones, then random addresses. The model launches reads only when mem_en is high, and the test requires exactly one enabled access per request. It checks both data availability with done and the downstream register's capture. Every instance must complete all 200 reads.

    rd_seq.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    module rd_seq #(parameter int LAT = 3, parameter int AW = 4, parameter int DW = 8) (
      input  logic          clk, rst_n,
      input  logic          req,
      input  logic [AW-1:0] addr,
      output logic          mem_en,
      output logic [AW-1:0] mem_addr,
      input  logic [DW-1:0] mem_rdata,       // LAT registered stages, counting the launch edge as stage 1
      output logic          busy,
      output logic          done,            // registered with data: high in the cycle data first shows the new word
      output logic [DW-1:0] data
    );
      typedef enum logic [1:0] {IDLE, ISSUE, WAIT, CAPTURE} state_t;
      initial if (LAT < 1 || AW < 1 || DW < 1)
        $fatal(1, "rd_seq: LAT, AW and DW must be at least 1");
      state_t state;
      logic [AW-1:0] addr_r;
      logic [$clog2(LAT+1):0] wait_cnt;
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin state <= IDLE; addr_r <= '0; wait_cnt <= '0; data <= '0; done <= 1'b0; end
        else begin
          done <= (state == CAPTURE);                          // set together with data, one pulse
          unique case (state)
            IDLE:    if (req) begin addr_r <= addr; state <= ISSUE; end
            ISSUE:   begin                                      // address is presented this cycle
                       wait_cnt <= LAT - 1;
                       state    <= (LAT == 1) ? CAPTURE : WAIT;
                     end
            WAIT:    begin
                       wait_cnt <= wait_cnt - 1'b1;
                       if (wait_cnt == 1) state <= CAPTURE;
                     end
            CAPTURE: begin data <= mem_rdata; state <= IDLE; end  // data is valid this cycle
            default: state <= IDLE;
          endcase
        end
    
      assign mem_en   = (state == ISSUE);
      assign mem_addr = addr_r;
      assign busy     = (state != IDLE);
    endmodule
    
    module rd_env #(parameter int LAT = 3, AW = 4, DW = 8)
      (input logic clk, input logic rst_n, output int errors, output int reads);
      logic req = 0, mem_en, busy, done;
      logic [AW-1:0] addr = 0, mem_addr;
      logic [DW-1:0] mem_rdata, data, got;
      logic [DW-1:0] mem [2**AW];
      logic [DW-1:0] pipe [LAT];
      int accesses = 0;
    
      initial for (int i = 0; i < 2**AW; i++) mem[i] = DW'(i * 17 + 3);
    
      // synchronous RAM with LAT cycles of read latency
      always_ff @(posedge clk) begin
        if (rst_n && mem_en) begin
          pipe[0] <= mem[mem_addr];
          accesses <= accesses + 1;
        end
        for (int i = 1; i < LAT; i++) pipe[i] <= pipe[i-1];
      end
      assign mem_rdata = pipe[LAT-1];
    
      rd_seq #(.LAT(LAT), .AW(AW), .DW(DW)) dut (.*);
    
      // a downstream consumer that registers data on the edge where done is high
      always_ff @(posedge clk) if (done) got <= data;
    
      int cycles;
      initial begin
        errors = 0; reads = 0;
        @(posedge rst_n);
        repeat (200) begin
          @(negedge clk) begin
            req = 1;
            if (reads == 0) addr = 0;
            else if (reads == 1) addr = '1;
            else addr = AW'($urandom_range(0, 2**AW-1));
          end
          @(negedge clk) req = 0;
          cycles = 0;
          while (!done && cycles < 20) begin @(negedge clk); cycles++; end
          reads++;
          if (accesses != reads) errors++;                     // exactly one enabled RAM access per request
          if (!done) errors++;
          else begin
            if (data !== mem[addr]) errors++;                   // data is already valid in the cycle where done is high
            if (cycles != LAT + 1) errors++;                    // ISSUE + (LAT-1) WAIT + CAPTURE, then the done cycle
            @(posedge clk); #1;                                 // the consumer registered data on this edge
            if (got !== mem[addr]) errors++;                    // it must hold the new word, not the previous read
          end
        end
      end
    endmodule
    
    module tb_rd;
      logic clk = 0, rst_n = 0;
      always #5 clk = ~clk;
      int e0, e1, e2, e3, e4, e5, r0, r1, r2, r3, r4, r5;
      rd_env #(1, 1, 1) u0 (clk, rst_n, e0, r0);
      rd_env #(1) u1 (clk, rst_n, e1, r1);
      rd_env #(2) u2 (clk, rst_n, e2, r2);
      rd_env #(3) u3 (clk, rst_n, e3, r3);
      rd_env #(4) u4 (clk, rst_n, e4, r4);
      rd_env #(5) u5 (clk, rst_n, e5, r5);
      initial begin
        #12 rst_n = 1;
        #30000;
        if (e0 + e1 + e2 + e3 + e4 + e5 == 0 && r0 == 200 && r1 == 200 && r2 == 200 && r3 == 200 && r4 == 200 && r5 == 200)
          $display("PASS: read sequencer LAT=1..5 and AW=DW=1 boundary, 200 reads each");
        else $fatal(1, "Read sweep FAIL: errors %0d %0d %0d %0d %0d %0d; reads %0d %0d %0d %0d %0d %0d",
                    e0, e1, e2, e3, e4, e5, r0, r1, r2, r3, r4, r5);
        $finish;
      end
    endmodule

    Remember

    • Issue the address, wait L - 1 cycles, then capture. Data is available L + 1 periods after acceptance and the consumer samples at L + 2.

    • Register done together with the data. A combinational done can announce a result a cycle before the data register holds it.

    • Make the latency a parameter and test several values.

    Rule of thumb: if the RAM output register changes, the sequencer's latency parameter changes with it.

    Keep going

    • Interview: run a mock interview on this topic

    Related topics

    • Sequencer robustness

    Previous

    105 / 350

    Next: Sequencer Robustness

    Up next

    Up nextSequencer Robustness

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Sequencer Mindset
    LAT = 3 read of address 5 (mem[5] = 0x58), sampled just before each edge, hexclkreqstateIDLEISSUEWAITCAPTUREIDLEmem_enmem_addr05mem_rdatax58donedata058
    The address is presented in ISSUE. Two WAIT cycles follow, and in CAPTURE the RAM output is the word at address 5 (0x58). The data register loads at the end of CAPTURE, and done is a flop set at the same edge, so both change in the same cycle: when done is high, data already holds 0x58.