An escape is a bug that verification should have found and did not. The fix in the RTL is the easy part. The valuable work is finding which part of the verification flow failed, so the same class of bug cannot get through again.
This is a made-up scenario. A chip link uses credit-based flow control: the sender starts with 4 credits, spends one per packet, and the receiver returns one when it frees a buffer. In the lab, the link runs at full rate for hours, then throughput drops and the link stops. A reset brings it back.
Lab debug reads the credit counter over the debug bus: it shows 0 while the receiver reports all buffers free. Credits are leaking. The RTL had this:
// snippet: the escaped counter update
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) credits <= 4'(MAX);
else if (consume) credits <= credits - 1;
else if (ret) credits <= credits + 1; // lost when consume is also highWhen a packet leaves in the same cycle a credit comes back, the else if drops the return. Each overlap leaks one credit. Four overlaps and the link is dead.
Each question names one layer of the flow. The first layer that answers "no" is where the escape happened, but check all four: escapes usually pass through more than one gap.
Question | In this case | Gap |
|---|---|---|
Was the feature in the plan, with the right closure criterion? | The plan row said "credits reach 0 and MAX". It did not mention same-cycle spend and return. | Plan |
Could the stimulus create the condition? | The receiver model returned credits only in idle cycles, to keep the model simple. Spend and return never met. | Stimulus |
Did coverage measure the condition? | Coverpoints on the credit value and on each signal alone. No cross of consume and ret. | Coverage |
Would a checker have seen the symptom? | Tests were short, so one leaked credit never stalled anything. No end-of-test audit that all credits came home. | Check |
All four failed. That is typical. A single gap is usually covered by another layer; an escape needs the gaps to line up.
The bench below is the regression that closes the escape. It does three things the original did not: it lets spend and return overlap, it counts how often they overlap and fails if they never do, and it drains the link at the end and audits the credit count.
module credit_ctr #(parameter int MAX = 4) (
input logic clk,
input logic rst_n,
input logic consume, // a packet leaves: spend one credit
input logic ret, // the far side frees a buffer: credit back
output logic [3:0] credits,
output logic can_send
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) credits <= 4'(MAX);
else credits <= credits - 4'(consume) + 4'(ret); // both in one cycle: net zero
assign can_send = (credits != 0);
endmodule
The OLD_STIM switch rebuilds the original bench inside the new one, so each gap can be shown on its own. These results come from running the bench with Verilator 5.048:
Change | Result | What it shows |
|---|---|---|
None (fixed DUT, new stimulus) | PASS, 95 overlaps in 400 cycles | The condition is exercised and checked. |
Put the | FAIL at cycle 6, the first overlap | The model compare catches the bug as soon as the stimulus reaches it. |
Buggy DUT and | Model compare and drain audit pass; only "consume and return never met" fires | The original stimulus could not reach the bug. Without the coverage check, this run would be green. |
Fixed DUT and | FAIL on the same coverage line | The hole is reported even when the RTL is correct. That is the point: a hole is a defect in the bench. |
The third row is the escape, reproduced: a green regression on buggy RTL. The coverage check is written as a pass condition, not as a report someone has to read.
The RTL fix is one line. The useful output of an escape review is a list of actions that each close a gap for every similar design:
Plan: every counter with independent increment and decrement gets a plan row for the same-cycle case.
Stimulus: reference models of the far side may not serialise events the real design can overlap. Review every model shortcut for this.
Coverage: cross the inputs of every up/down counter. The cross is cheap and the hole is visible.
Check: every resource that is borrowed and returned (credits, tags, buffers, IDs) gets an end-of-test audit after a drain.
Sweep: search the RTL for else if chains that update one register from two independent events, and review each.
The sweep matters most. If one engineer wrote this pattern once, it probably exists elsewhere in the same codebase.
Do not stop at "add a directed test for the overlap". That test protects one counter. The audit and the cross protect all of them.
Do not blame the designer. The verification flow is meant to catch design mistakes; the review is about the flow.
Do not accept "it needed hours of lab time to show up" as a reason. Hours of runtime were needed to exhaust the credits, but the first leak happened within microseconds. An audit sees the first leak.
No. It proves one directed case. The review closes when the plan, stimulus models, coverage and audits are fixed for every borrowed-and-returned resource, and the pattern sweep is done.
OLD_STIM = 1 even with correct RTL?Because the overlap count is a pass condition. A stimulus that cannot create a required condition is a bench defect, whether or not the RTL happens to be correct.