Skip to content
VLSI Mentor

I²C · Module 22

Verifying Stretching and Arbitration Without Flaky Tests

A feature that was implemented, correct and never executed; how to place a behaviour instead of hoping for it; and why a test whose checks pass whether or not the case occurred is flaky even when it is green every time.

Clock stretching and arbitration have a reputation for flaky tests, and the reputation is earned — but the flakiness is rarely in the design. It is in stimulus that produces the situation sometimes, and in checks that cannot tell the difference between "the case did not arise" and "the case arose and was handled".

Both are fixable, and neither is fixed by running longer.

1. The Feature That Was Implemented, Correct, and Never Ran

The clearest case of accidental non-verification in this curriculum, and nothing failed to reveal it.

Clock stretching was present in three places and all three worked: the controller driver had a bounded wait with a timeout, the responder model had a STRETCH_CLKS policy, and Module 18's target had a stall_req input. Every piece correct.

The responder was instantiated with STRETCH_CLKS(0). stall_req was tied low. The feature had never executed.

2. Placing the Behaviour Instead of Hoping For It

Stretching is now produced deliberately in two places, and both required something structural rather than a random knob.

A second responder instance. The hold length is an elaboration-time parameter and cannot be raised mid-run, so the stretching case is a second instance of the same module at its own address, silent for every transfer aimed elsewhere:

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from Module 20's bfm_pair bench
   i2c_resp_bfm #(.MY_ADDR(STRETCH_ADDR), .ACK_ADDR(1'b1), .NACK_AFTER(0),
                  .STRETCH_CLKS(40), .READ_BASE(8'h00)) sresp (...);

Two inequalities make that test mean something, and both are checked in the source: 40 exceeds a half period, so the hold is unmistakably a stretch rather than bit-timing noise; and 40 is far shorter than the driver's timeout, so a correct driver waits it out. Break the first and the test proves nothing; break the second and it proves the opposite of what it claims.

A bounded stall window on the real target. stall_req is a level, not a pulse — held high for a whole transfer it does not mean "stretch once", it means "never become ready", and the first attempt produced a driver timeout that looked like a stuck bus. The window is bounded and timed off the monitor's observed byte count, so the stall lands inside a transfer rather than before it.

3. Prove the Case Occurred, Separately From Proving It Was Handled

This is the discipline that removes the flakiness, and it is the same one 22.3 and 22.7 arrive at from other directions.

A stretch test has two halves and the first is the one usually missing:

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from Module 20's responder bench — T5
         ck("T5 the responder really did stretch",       s_nstr > 0, 1);
         ck("T5 the controller observed the stretch",    c_stretched, 1);
         ck("T5 and did not give up waiting",            c_timeout, 0);
         ck("T5 the address was still acknowledged",     c_aacked, 1);
         ck("T5 the responder received the last byte intact", s_lastwr, 8'h5B);
         ck("T5 the monitor reconstructed three bytes anyway", m_nbytes - nb_before, 3);

The first two are the anti-flake checks. s_nstr > 0 says the stretch happened, from the responder's own counter. c_stretched says the controller noticed, derived independently from the resolved SCL line failing to rise. Without them, a run in which nothing stretched passes every remaining check — because a transfer with no stretch also completes correctly.

And the payload check at the end matters for a reason specific to stretching: a correct stretch changes when bits move and changes nothing about which bits move. The monitor reconstructs the same three bytes, because it is edge-driven and has no notion of a bit period — which is 20.7's design decision paying off here.

4. A Cover Point Makes a Silent Run Visible

Checks confirm the case when it occurs. A cover point reports whether it occurred at all, across a whole regression:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_props.vhd
      -- psl COV_STRETCH        : cover {scl_holders > 1};

Six hits on the assertion bench. A run reporting zero would mean the properties stayed quiet because nothing stretched — which is a different result from the properties holding, and the only way to tell them apart from a summary.

Note that this cover point reads the holder count, not the bus. The resolved line cannot reveal a stretch — SCL low is SCL low, whether one device holds it or two — so without drive intents the situation is not observable at all.

5. Arbitration: The Method, and What Is Missing

Arbitration is the harder case and this environment does not produce it. Stating the method is still worth doing, because the same anti-flake structure applies and the gap is then specific rather than vague.

What makes arbitration flaky by default. A loss occurs only when two masters transmit differing bits at the same moment. Left to random timing, that coincidence is rare, unrepeatable, and its absence is invisible.

What deliberate placement would look like. Two active agents with different drive indices — 21.7's agent already selects its role from configuration, so this is a topology change rather than new components — started at a controlled offset so that their address bytes differ at a chosen bit position. The loser is then determined by construction rather than by scheduling.

The two-halves discipline, applied. Prove the contention occurred: both agents transmitted, the holder count exceeded one, and the resolved line differed from the loser's intent on the deciding bit. Only then check the response: the loser stopped driving within a bit, did not corrupt the winner's transfer, and reported the loss.

The stretch test that passed on every run without ever stretching

Pitfall — a test that cannot tell 'handled' from 'never happened'
Buggy Code
// A clock-stretch test. It has passed on every run for eight months.
//
//    resp.stretch_enable = 1'b1;          // ask the responder to stretch
//    req_addr = ADDR; req_len = 3'd2;
//    run_txn;
//    ck("the address was acknowledged", c_aacked, 1);
//    ck("both data bytes acknowledged", c_nacked, 2);
//    ck("the byte landed",              resp_last_write, 8'h5B);
//
// Every check is about the TRANSFER COMPLETING CORRECTLY. A transfer with no
// stretch at all completes correctly too -- so all three pass whether or not the
// responder stretched.
//
// And it did not. stretch_enable is read at construction in this responder, and
// the test sets it after the component is built. The feature never executed.
//
// Eight months of green results carrying no information about stretching.
Root Cause

Every check in the original is satisfied by a transfer that never stretched, so the test's result is independent of the behaviour it names — which is what flakiness actually is, even when the result is consistently green. Asserting the occurrence from two independent sources makes the case a precondition of the test rather than a hope, and moving an elaboration-time parameter into a second instance is what makes the case reachable at all.

Fix
// Add the half that proves the case OCCURRED, from a source independent of the
// thing being tested:
//
//    nb_before = m_nbytes;  nack_before = r_nack;
//    run_txn;
//
//    // 1. DID IT HAPPEN?
//    ck("the responder really did stretch",    s_nstr > 0, 1);   // responder's own count
//    ck("the controller observed the stretch", c_stretched, 1);  // derived from the LINE
//
//    // 2. ONLY THEN: was it handled?
//    ck("and did not give up waiting",         c_timeout, 0);
//    ck("both data bytes acknowledged",        c_nacked, 2);
//    ck("the monitor reconstructed 3 bytes anyway", m_nbytes - nb_before, 3);
//
// The two sources matter: s_nstr is the responder saying it stretched, and
// c_stretched is the controller noticing SCL failed to rise when released. Either
// alone can be wrong; together they are the event.
//
// AND MAKE THE PARAMETER REACHABLE. If the hold length is elaboration-time, a
// run-time assignment silently does nothing -- so the stretching case becomes a
// SECOND INSTANCE at its own address:
//
//    i2c_resp_bfm #(.MY_ADDR(STRETCH_ADDR), .STRETCH_CLKS(40), ...) sresp (...);
//
// with 40 > HALF (so it is a stretch, not bit-timing noise) and 40 << TIMEOUT
// (so a CORRECT driver waits it out). Break the first and the test proves
// nothing; break the second and it proves the opposite of what it claims.
Pitfall — a level-triggered stall held for a whole transfer
Buggy Code
// Asking the real target to stretch, using its stall input:
//
//    stall_req = 1'b1;          // "stretch during this transfer"
//    run_txn;
//    stall_req = 1'b0;
//    ck("the driver waited",   c_timeout, 0);      // FAILS
//    ck("the byte landed",     dut_regs[1], 8'h7E); // FAILS
//
// The driver reports a timeout and the transfer never completes. The natural
// reading is that the driver's stretch support is broken.
//
// stall_req is a LEVEL, not a pulse. Held high for the whole transfer it does not
// mean "stretch once" -- it means "never become ready". The target holds SCL for
// as long as it is asserted, which is longer than any finite patience.
//
// So the test asked for an infinite stretch and then complained that the driver
// gave up, which is the T5 stuck-line result arriving under a stretch heading.
Root Cause

A level-triggered request held across a transfer requests an unbounded stall, so the correct driver behaviour is the timeout the test treats as a failure — the test and the design disagree about what was asked for. Bounding the window makes the request finite, timing it off an observed byte places it where it matters, and latching the evidence is necessary because the condition is gone by the time the assertions run.

Fix
// Bound the request, and place it INSIDE a transfer rather than before it:
//
//    logic   stall_arm;
//    integer stall_left;
//    logic   stall_fired;                    // evidence, LATCHED
//    wire    stall_req = (stall_left > 0);
//
//    always_ff @(posedge clk) begin
//       if (stall_arm && m_bvalid && m_bisaddr) stall_left <= 300;   // after an OBSERVED byte
//       else if (stall_left > 0)                stall_left <= stall_left - 1;
//       if (stall_left > 0 && dut_stretching)   stall_fired <= 1'b1;
//    end
//
//    ck("the target really did hold SCL while asked to", stall_fired, 1);
//    ck("the driver observed a stretch",                 c_stretched, 1);
//    ck("and did NOT report a timeout",                  c_timeout, 0);
//
// TWO THINGS THE FIX DEPENDS ON:
//
//   * the window is timed off the MONITOR's observed byte count, so the stall
//     lands inside a transfer -- armed beforehand it would stall an idle bus;
//   * stall_fired is LATCHED, because by the time the checks run the window has
//     closed. Checking a live signal after the event it describes reports that no
//     stall was requested during a transfer that was visibly stalled.

6. What 22.9 Settled

A feature can be implemented, correct, and never executed, and nothing but a status column asks. Six mutations would have survived against correct stretch code.

Place the behaviour rather than hoping for it — a second instance where a parameter is elaboration-time, a bounded window timed off an observed event where the input is a level.

Prove the case occurred, independently of proving it was handled. A test whose checks pass either way is flaky even when it is green every time.

Latch the evidence, because the condition is gone by the time the assertions run.

A cover point distinguishes "the properties held" from "nothing happened", which a summary otherwise cannot.

Arbitration is method here, not results, and the gap is named precisely rather than implied.

7. What Module 22 Settled

Nine chapters, and one measurement underneath all of them.

Eight defects were injected into Module 18's verified target and the property set caught two — both cases where the target itself drove the bus illegally. It missed three cases of the target misreading legal traffic, and all three data faults. Module 20's scoreboard caught the data ones. That three-way split is the module's spine: if a waveform alone can violate the rule, it is an assertion; if only a disagreement with a contract can, it is a scoreboard; and whether the situation ever arose is a third question that neither answers.

The module could make that measurement because, unlike Module 21, its checks actually run. SystemVerilog assertions are rejected outright here, but NVC evaluates PSL and Module 18 exists in VHDL — so the properties execute against the real design, and every result is a number rather than an argument.

What executing them cost was a list of findings that no amount of careful writing would have produced. Three PSL constructs that this tool accepts and never evaluates, so a property built on rose(scl) passes forever while checking nothing. A cover form that can never be hit, sitting in the report as a permanent hole for a case already being exercised. A contention property that fired 84 times on legal traffic — and whose investigation indicted the bench before the property. A bit counter that counted the STOP sequence's own clock edge, so every legal STOP was reported as truncation. And three negative tests out of five that violated nothing at all: one drove a STOP onto an already-released line, one inherited a legal gap from the stimulus library, one OR-ed both contenders into a single drive bit.

Every one of those presented identically — as a property that would not fire, or one that fired when it should not. What separated them was the same piece of instrumentation each time: count the event beside the violation. "Stops seen = 0" and "idle at the last START = 17" are one-line answers to questions that otherwise cost an afternoon of auditing correct logic.

The discipline that falls out is short. A property that has never fired is a property nobody has evidence for. A cover point that has never been hit may be unhittable. A negative test must prove the violation occurred before claiming anything about detection. And when a property fires on traffic you believe is legal, doubt the traffic first — one of this module's own "legal" traces turned out to be a single bit followed by a STOP.

Module 23 — I²C Debugging and Failure Analysis inverts the situation these nine chapters assume. Everything here depends on owning the environment: placing the fault, instrumenting the antecedent, counting the event. The next module is about the bus you did not instrument — a failure on somebody else's board, with a scope and no assertions — and what the waveform is able to tell you, in what order, before you have a theory.

Continue learning