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 |
The other side never answers |
| Timeout counter and an |
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 |
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.
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.
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.
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.
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.
The handshake-based reader and the testbench covering random latency, dead memory and abort.
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