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. Sequencer Robustness

Part 4 · FSM & Control · Intermediate

Sequencer Robustness

A handshake-based read sequencer with timeout, abort handling and stale-response protection.

On this page

    Why this matters

    Real memories vary in latency and sometimes never answer. A sequencer that counts cycles hangs or, after an abort, returns the word from the aborted address: in the removed-DRAIN test the second read came back wrong.

    After this lesson you can: add a handshake, a timeout, an abort and a drain state to a sequencer, and state the contract it needs from the other side.

    Failure modes

    Real memories vary in latency and sometimes never answer. Callers also cancel requests.

    Problem

    Without protection

    Protection

    Latency varies

    A fixed wait captures the wrong cycle

    Wait for a rvalid handshake instead of counting

    The other side never answers

    busy stays high forever; the system hangs

    Timeout counter and an err flag

    The caller cancels a request

    The old response arrives later and is mistaken for the next request's answer

    Drain state: absorb the outstanding response before accepting a new request

    A request arrives while busy

    Operands overwritten

    Ignore req unless in IDLE; expose busy

    Handshake, timeout and drain

    The sequencer waits for rvalid instead of counting cycles. A timeout in WAIT or DRAIN latches an ERR fault with busy high, blocking reuse. DRAIN absorbs a late answer after an abort; if abort and rvalid coincide in WAIT, that answer is discarded immediately and no drain is needed.

    State diagram with ISSUE, WAIT, DONE, ERR and DRAIN states.
    DRAIN discards the outstanding response. WAIT or DRAIN timeout latches ERR until explicit recovery after the memory interface is quiesced.

    Timeout is a failure report, not a recovery. A memory that answers after the limit brings back the stale-response problem.

    Choose the timeout above the worst legal latency. Treat err as a system-level fault: ERR keeps busy = 1, err = 1, done = 0 and mem_en = 0. Assert recover only after resetting/quiescing the memory interface and proving no old response can arrive. A local sequencer reset alone does not cancel a peer's pending response. A late rvalid in ERR is ignored and does not clear the fault.

    TIMEOUT, AW and DW must each be at least 1. The procedural initial check reports invalid parameters at simulation time zero; malformed declarations may be rejected earlier by a tool.

    Peer contract: one outstanding request, one response per launch, no response sampled on the ISSUE launch edge (minimum latency one cycle), and no unsolicited responses. Abort cancels in ISSUE or WAIT, including a coincident WAIT response. Once WAIT accepts rvalid without abort, data is committed: a later abort in DONE cannot retract completion. Abort in DRAIN changes nothing; ERR requires recover after peer quiescence. Reset cancels local work but also requires peer quiescence before reuse.

    What the test covers

    • 300 reads at random addresses and random latencies from 1 to 6 cycles, all below the 8-cycle timeout.

    • A dead memory that never responds: the sequencer latches err and stays busy until explicit recovery after the memory is quiesced.

    • Late responses beyond both WAIT and DRAIN timeouts, with a new request held high: no new access or completion is allowed while faulted, even when the old response arrives.

    • Abort and rvalid on the same WAIT edge: discard the response, preserve data, never pulse done, and return directly to IDLE without a second response.

    • Abort timing swept across ISSUE, WAIT and completion, followed by a read of a different address; directed checks distinguish cancellation from late abort after commit.

    • Aborts in WAIT cycles 1 to 3 of a slow (latency 7) read, followed by a latency-4 read. The memory model queues every response, so the aborted word still arrives, and without DRAIN it lands in the new read's WAIT.

    Common mistakes

    Warning

    Remove the DRAIN state (go straight to IDLE on abort) and the same test fails: the second read returns the word from the aborted address. In this experiment the mutated design returned 0x5c where the correct value was 0x61, for the abort in ISSUE and for each abort in WAIT cycles 1 to 3. This is the classic stale-response bug: one block's late answer is consumed by the next request.

    Apply this checklist to any sequencer.

    • Reset leaves busy = 0, done = 0, err = 0.

    • start/req is ignored unless idle; operands are captured.

    • Every wait has an upper bound (timeout) or a proof that it terminates.

    • Abort cancels only before the response commits in WAIT. DONE is already committed; DRAIN retains peer ownership; ERR requires explicit recovery.

    • Repeated operations work: the test below runs hundreds of reads in a row. It always leaves at least one idle cycle between requests, so add a directed case if you also need an immediate restart from DONE.

    Interview

    “What if the other side never responds?” Answer with a timeout and an error flag. Follow up with the abort case and the stale response: this shows you think about the whole protocol and not only the happy path.

    Check yourself Why does the abort in the ISSUE state also go to DRAIN?

    In ISSUE the request is already leaving (mem_en is high this cycle), so a response will come even though the caller has cancelled. The sequencer must discard that response before accepting another request. If draining times out, it latches ERR and requires explicit recovery after the peer is quiesced.

    Testbench

    The handshake-based reader and the testbench covering random latency, dead memory and abort.

    rd_hs.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
    module rd_hs #(parameter int TIMEOUT = 8, parameter int AW = 4, parameter int DW = 8) (
      input  logic          clk, rst_n,
      input  logic          req, abort, recover, // recover only after peer reset/quiescence rules out stale responses
      input  logic [AW-1:0] addr,
      output logic          mem_en,
      output logic [AW-1:0] mem_addr,
      input  logic          rvalid,
      input  logic [DW-1:0] rdata,
      output logic          busy, done, err,
      output logic [DW-1:0] data
    );
      typedef enum logic [2:0] {IDLE, ISSUE, WAIT, DRAIN, DONE, ERR} state_t;
      state_t state;
      logic [AW-1:0] addr_r;
      // TIMEOUT must be at least 1: $clog2(0 + 1) is 0, which would give a [-1:0] counter.
      initial begin
        if (TIMEOUT < 1 || AW < 1 || DW < 1) $fatal(1, "rd_hs: TIMEOUT, AW and DW must be at least 1");
      end
      logic [$clog2(TIMEOUT+1)-1:0] t;
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin state <= IDLE; addr_r <= '0; t <= '0; data <= '0; end
        else begin
          unique case (state)
            IDLE:  if (req) begin addr_r <= addr; state <= ISSUE; end
            ISSUE: begin                                  // the request leaves this cycle
                     t <= '0;
                     if (abort) state <= DRAIN;           // a response is on its way even if we abort now
                     else       state <= WAIT;
                   end
            WAIT:  if (abort)                  begin t <= '0; if (rvalid) state <= IDLE; else state <= DRAIN; end
                   else if (rvalid)            begin data <= rdata; state <= DONE; end
                   else if (t == TIMEOUT - 1)  state <= ERR;
                   else                        t <= t + 1'b1;
            DRAIN: if (rvalid)                 state <= IDLE; // swallow the outstanding response
                   else if (t == TIMEOUT - 1) state <= ERR;
                   else                      t <= t + 1'b1;
            DONE:  state <= IDLE;
            ERR:   if (recover) state <= IDLE;         // fault stays latched even if a late response arrives
            default: state <= ERR;
          endcase
        end
    
      assign mem_en   = (state == ISSUE);
      assign mem_addr = addr_r;
      assign busy     = (state != IDLE);
      assign done     = (state == DONE);
      assign err      = (state == ERR);
    endmodule
    

    Remember

    • Wait for rvalid rather than counting cycles, and bound every wait with a timeout or a proof of termination.

    • A timeout is a failure report. The ERR state keeps busy high until the peer is quiet and recover is asserted.

    • DRAIN absorbs a late answer after an abort. Without it, the aborted word lands in the next read.

    Rule of thumb: design for the whole protocol, not only the happy path.

    Keep going

    • Practice: Waveform debug of a write transaction timeout

    • Interview: run a mock interview on this topic

    Related topics

    • UART transmitter FSM

    • Memory controller FSM

    Previous

    106 / 350

    Next: UART Transmitter

    Up next

    Up nextUART Transmitter

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Sequencer Mindset
    • Shift-Add Multiply