Skip to content
VLSI Mentor

I²C · Module 5

The STOP Condition and Releasing the Bus

STOP is SDA rising while SCL is high — the mirror edge of START, with obligations that are not mirrored at all. Releasing the bus is not the same as making it available, and the interval between the two is where a whole class of intermittent failure lives.

Chapter 5.2 took the falling edge out of the reserved space. This chapter takes the rising one — and then spends most of its length on something the edge itself does not tell you.

The temptation with STOP is to treat it as START with the direction flipped and move on. The definitions genuinely are mirror images, and if the obligations were mirrored too this would be a short chapter. They are not. A START hands the bus to one controller in a single instant; a STOP hands it back over an interval, and the difference between the instant the conductors go high and the instant the bus is actually available is the subject of Chapter 5.1's debugging case.

1. The Definition

UM10204 §3.1.4, the sentence after the one Chapter 5.2 started from:

A LOW to HIGH transition on the SDA line while SCL is HIGH defines a STOP condition.

Same structure, opposite direction. As with START, the level of SCL across the transition is what separates the framing event from ordinary data — an SDA rise during a low phase is the perfectly ordinary act of preparing to transmit a one.

And as with START, the specification adds that START and STOP conditions are always generated by the master. A target cannot end a transfer, for the same mechanical reason it cannot begin one: producing an edge inside a high phase requires owning the clock. A target that has nothing more to say has to say so within the protocol — by not acknowledging, which is Module 7 — and cannot simply hang up.

The last low phase, then STOP

10 cycles
Ten intervals. SCL is low and SDA is low at the end of a transfer. SCL is released and goes high while SDA stays low. SDA is then released and rises while SCL is high, which is the STOP condition. Both lines remain high afterwards.bus busybus busyreleased, not yet freereleased, not yet freefreefreeSCL released — SDA still lowSCL released — SDA stilllowP — STOP: SDA rises, SCL highP — STOP: SDA rises, SCLhighfree — a START is allowedfree — a START is allowedsclsda0011111111t0t1t2t3t4t5t6t7t8t9
Figure 1 — the STOP condition. The transfer's last low phase ends, the controller releases SCL so the line goes high, and only then does it release SDA. That SDA rise, occurring while SCL is high, is the STOP. Note the order: SCL must already be high, so the interval between the two releases is a specified minimum rather than an implementation choice — Chapter 5.5 measures it. Conceptual figure: interval widths are for legibility and carry no specification proportion.

2. Where the Symmetry Stops

It is worth laying the two events side by side, because the asymmetries are the content of this chapter.

STARTSTOP
SDA edge while SCL is highfallingrising
generated bycontroller onlycontroller only
bus state afterwardsbusy, immediatelyreleased — then free, after an interval
what the controller does nextpulls SCL low for the first clock pulsenothing; both lines stay released
effect on every targetbegin listening for an addressreturn to idle and watch for the next START
can a transfer follow immediately?n/ano
the design object it needsan edge detectoran edge detector and a timer

The last two rows are the whole story. A START is complete the moment its edge occurs — the bus is busy, and nothing further is required for that to be true. A STOP's edge is only the beginning of returning the bus, and what completes it is the passage of time on a bus that nobody disturbs.

That asymmetry exists because of what the two events ask of other devices. A START asks every target to start doing something, and a target can begin listening in the next bit period. A STOP asks every device to finish — to return its protocol state machine to idle, clear whatever it was tracking, and get ready to watch for the next START. Finishing takes a bounded but non-zero amount of time in a real device, and the specification's bus-free interval is what guarantees the slowest such device gets it.

3. Released Is Not Free

UM10204 says the bus is considered free again a certain time after the STOP condition. Three states follow, and the middle one is the one designs omit.

busy — a transfer is in progress. Either conductor may be low. No other controller may start.

released — both conductors are high, because the STOP put them there, but the interval has not yet elapsed. A level test cannot distinguish this state from free; only elapsed time can.

free — both conductors have been high for the whole required interval. A START is now permitted.

The trap is that released and free are electrically identical. There is no observable difference on either conductor. A controller checking scl_in == 1 && sda_in == 1 gets the same answer in both states, which is why Chapter 5.1's debugging case reproduced only on a busy board: the test and the requirement agree whenever the previous transfer happens to have been a long time ago, and they disagree exactly when it was recent.

The parameter's name and its per-mode values belong to Module 11, which owns the timing table. What belongs here is the state machine, and one worked conversion to make the interval concrete.

4. The Interval Is Contiguous, Not Cumulative

Before converting anything, settle a question the specification's phrasing leaves to the reader: if the bus goes low part-way through the interval and comes back, does the elapsed time count?

It does not, and the reason follows from §2. The interval exists to give every device enough undisturbed quiet to return to idle. A device interrupted half way through that return has not finished — so the time it already had bought nothing, and the interval has to start over. What the requirement asks for is a contiguous stretch of released bus, not a total.

This is a design decision with a visible consequence in RTL: the counter reloads when the bus goes low, rather than pausing. A tracker that paused would declare the bus free after the right total amount of quiet distributed across interruptions, which is not the requirement. §9 injects exactly that fault and §7's testbench catches it.

5. From a Time Requirement to a Cycle Count

The requirement is a duration in microseconds. A synchronous design counts clock cycles. Somebody has to convert, and the direction of rounding is not a matter of taste.

For a minimum duration, the conversion must round up:

Azvya Education Pvt. Ltd.VLSI Mentor
MATHEMATICAL DERIVATION — converting a minimum duration to a cycle count
   required_cycles = ceil( t_required / T_clk )   =   ceil( t_required x f_clk )

   Round UP, always, and the reason is asymmetric consequence:

     rounding UP    -> the interval is slightly LONGER than required.
                       Costs a fraction of a microsecond of throughput.
                       Still legal: the requirement is a MINIMUM.

     rounding DOWN  -> the interval is SHORTER than required.
                       Costs nothing measurable and is ILLEGAL.
                       Fails intermittently, against some devices, on some boards.

   Floor division is the default behaviour of integer arithmetic in every HDL,
   so the unsafe direction is the one you get by not thinking about it.

Work it with the Standard-mode bus-free minimum, which UM10204 gives as 4.7 µs. Two cases, and the second is the one that matters:

Azvya Education Pvt. Ltd.VLSI Mentor
MATHEMATICAL DERIVATION — two conversions, one of them non-integer
   CASE 1 -- the arithmetic happens to be exact
     f_clk = 100 MHz  ->  T_clk = 10 ns
     t_required = 4.7 us = 4700 ns

     4700 ns / 10 ns = 470.0 exactly   ->   470 cycles
     No rounding decision arises, which is why an example like this one
     teaches nothing about rounding and should never be the only example.

   CASE 2 -- the arithmetic does not divide
     f_clk = 48 MHz   ->  T_clk = 20.8333... ns
     t_required = 4700 ns

     4700 ns / 20.8333 ns = 225.6 cycles

     ceil(225.6) = 226 cycles          <- the only legal choice
     floor(225.6) = 225 cycles         <- 225 x 20.8333 ns = 4687.5 ns
                                          which is 12.5 ns SHORT of 4.7 us

   12.5 ns is not a rounding error. It is a specification violation that will
   be reported as "works on the prototype, fails on the pilot run".

5a. Doing It Safely in Integer Arithmetic

The equation is easy; getting it into synthesizable RTL without stepping on an integer hazard takes a little care.

Azvya Education Pvt. Ltd.VLSI Mentor
ARCHITECTURAL PSEUDOCODE — the integer-safe ceiling idiom, and what it avoids
   // UNSAFE -- overflow. An elaboration-time constant is typically 32-bit
   // signed, and multiplying hertz by nanoseconds leaves that range at once:
   //
   //     FCLK_HZ * T_NS  =  100_000_000 * 4700  =  4.7e11     OVERFLOWS
   //
   // UNSAFE -- truncation. Integer division discards the remainder, which is
   // precisely the fraction that forces the round up:
   //
   //     T_NS / T_CLK_NS  =  4700 / 21  =  223                TOO SHORT, twice over
   //
   // UNSAFE -- real arithmetic. $ceil and real division are fine at elaboration
   // time in SystemVerilog, but a design that carries reals into anything a
   // synthesis tool must implement has stopped being portable RTL.

   // SAFE -- scale the units so the product stays small, and add the divisor
   // minus one before dividing, which is integer ceiling:
   //
   //     cycles = (T_NS * FCLK_MHZ + 999) / 1000
   //
   //   check the non-integer case:  (4700 * 48 + 999) / 1000
   //                             =  (225600 + 999) / 1000
   //                             =  226599 / 1000  =  226        CORRECT
   //
   //   check the exact case:        (4700 * 100 + 999) / 1000
   //                             =  (470000 + 999) / 1000
   //                             =  470999 / 1000  =  470        CORRECT
   //                                                  ^ does NOT over-round
   //
   // The +999 form rounds up if and only if there is a remainder, which is the
   // property that makes it safe for both cases. Largest intermediate here is
   // 4700 x 48 = 225,600 -- four orders of magnitude inside 32 bits.

The tracker in §6 takes the resulting cycle count as a parameter rather than performing this conversion internally, and that is deliberate: the conversion depends on the system clock frequency, which is a property of the instantiating design and not of the bus. Pushing it to the parameter keeps the block portable and makes the count reviewable at the point of instantiation, where the frequency is actually known.

6. Counter Semantics — Proving the Off-By-One

A timing block whose counter semantics are ambiguous is worthless, so state the convention and prove it rather than asserting it.

Convention: load N - 1 and transition when the count reaches zero. That produces exactly N cycles.

Azvya Education Pvt. Ltd.VLSI Mentor
MATHEMATICAL DERIVATION — the timeline, for BUS_FREE_CYCLES = 5
   At cycle k the STOP is detected: state <= WAIT_FREE, cnt <= N-1 = 4.

   cycle    cnt on entry   test           action              free cycles counted
   -----    ------------   ------------   -----------------   -------------------
   k+1          4          cnt != 0       cnt <= 3                    1
   k+2          3          cnt != 0       cnt <= 2                    2
   k+3          2          cnt != 0       cnt <= 1                    3
   k+4          1          cnt != 0       cnt <= 0                    4
   k+5          0          cnt == 0       state <= FREE               5
                                                                     ^
                                            five counted cycles, as required

   Load N instead, and the bus stays unavailable for 6 cycles.
   Load N-2, and it becomes available after 4 -- one cycle short of legal.
   Both faults are injected in section 9, and both are caught by MEASUREMENT
   rather than by inspection.

7. The Tracker in Three Languages

The design is a three-state machine with one counter, an edge detector for each framing direction, and two status outputs. Both framing detectors reuse Chapter 5.2's four-term guard — the only difference between them is the direction of the SDA change, which is why one block can find both.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_state_tracker.sv — SYNTHESIZABLE RTL. Three states, because released and free are not the same.
   module i2c_bus_state_tracker #(
       // Cycles the bus must be observed released before a START is permitted
       // again. ILLUSTRATIVE default: the real requirement is a time, converted to
       // cycles by the ceiling rule. Module 11 owns the specified value.
       parameter int BUS_FREE_CYCLES = 5
   )(
       input  logic clk,
       input  logic rst_n,
       input  logic scl_in,             // observed bus level, not drive intent
       input  logic sda_in,             // observed bus level, not drive intent
       output logic start_detected,     // single-cycle event pulse
       output logic stop_detected,      // single-cycle event pulse
       output logic bus_busy,           // a transfer is in progress
       output logic bus_free            // released long enough that a START is allowed
   );
       typedef enum logic [1:0] { ST_FREE, ST_BUSY, ST_WAIT_FREE } state_e;
       state_e state;

       localparam int CW = (BUS_FREE_CYCLES <= 1) ? 1 : $clog2(BUS_FREE_CYCLES);
       logic [CW-1:0] cnt;

       logic scl_q, sda_q;
       logic saw_start, saw_stop;

       // Both framing edges are SDA transitions while SCL reads HIGH on BOTH
       // samples. They differ only in direction, which is the whole reason one
       // detector can find both.
       always_comb begin
           saw_start = scl_q && scl_in &&  sda_q && !sda_in;   // SDA fell
           saw_stop  = scl_q && scl_in && !sda_q &&  sda_in;   // SDA rose
       end

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               // Reset assumes an idle bus but NOT a free one: the tracker has not
               // yet observed the bus-free interval, so it must earn ST_FREE.
               state <= ST_WAIT_FREE;
               cnt   <= CW'(BUS_FREE_CYCLES - 1);
               scl_q <= 1'b1; sda_q <= 1'b1;
               start_detected <= 1'b0;
               stop_detected  <= 1'b0;
           end else begin
               start_detected <= saw_start;
               stop_detected  <= saw_stop;
               scl_q <= scl_in;
               sda_q <= sda_in;

               case (state)
                   ST_FREE:
                       if (saw_start) state <= ST_BUSY;

                   ST_BUSY:
                       if (saw_stop) begin
                           state <= ST_WAIT_FREE;
                           cnt   <= CW'(BUS_FREE_CYCLES - 1);
                       end

                   ST_WAIT_FREE:
                       // A START may legally arrive only after the interval, but the
                       // tracker reports what the bus DID, not what was allowed --
                       // legality is Chapter 5.5's job.
                       if (saw_start) state <= ST_BUSY;
                       else if (!(scl_in && sda_in))
                           // Not actually free: a line went low without being a
                           // START, so the contiguous interval restarts.
                           cnt <= CW'(BUS_FREE_CYCLES - 1);
                       else if (cnt != '0) cnt <= cnt - 1'b1;
                       else state <= ST_FREE;

                   default: state <= ST_WAIT_FREE;
               endcase
           end
       end

       always_comb begin
           bus_busy = (state == ST_BUSY);
           bus_free = (state == ST_FREE);
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_state_tracker_tb.sv — SELF-CHECKING TESTBENCH, SIMULATION ONLY. Two parameterisations; measures the interval; proves it is contiguous.
   module i2c_bus_state_tracker_tb;
       localparam int N_A = 5;
       localparam int N_B = 8;

       logic clk = 1'b0, rst_n, scl_in, sda_in;
       int   errors = 0;

       // Two differently-parameterised trackers on the SAME stimulus. Measuring
       // both proves the interval TRACKS the parameter instead of matching one
       // hand-tuned number -- which is what kills off-by-one mutations.
       logic sA, pA, bA, fA;
       logic sB, pB, bB, fB;

       i2c_bus_state_tracker #(.BUS_FREE_CYCLES(N_A)) dutA
           (.clk(clk), .rst_n(rst_n), .scl_in(scl_in), .sda_in(sda_in),
            .start_detected(sA), .stop_detected(pA), .bus_busy(bA), .bus_free(fA));
       i2c_bus_state_tracker #(.BUS_FREE_CYCLES(N_B)) dutB
           (.clk(clk), .rst_n(rst_n), .scl_in(scl_in), .sda_in(sda_in),
            .start_detected(sB), .stop_detected(pB), .bus_busy(bB), .bus_free(fB));

       always #5 clk = ~clk;
       initial begin #40000; $display("FAIL: watchdog expired"); $finish; end

       // Cycle-stamp every event. Timestamps, then arithmetic -- never "wait the
       // expected number of cycles and assume".
       int cyc = 0;
       int tA_stop, tA_free, tB_stop, tB_free;
       // Latched once, when measured: later stimulus reuses the timestamp
       // registers, so the reported figures must not be recomputed at the end.
       int gapA, gapB;
       logic fA_q, fB_q;
       always @(posedge clk) if (rst_n) begin
           cyc <= cyc + 1;
           if (pA) tA_stop <= cyc;
           if (pB) tB_stop <= cyc;
           if (fA && !fA_q) tA_free <= cyc;
           if (fB && !fB_q) tB_free <= cyc;
           fA_q <= fA; fB_q <= fB;
       end

       task automatic do_start();
           scl_in = 1'b1; sda_in = 1'b1;   repeat (2) @(negedge clk);
           sda_in = 1'b0;                  repeat (2) @(negedge clk);   // START edge
           scl_in = 1'b0;                  repeat (2) @(negedge clk);
       endtask

       task automatic do_bit(input logic v);
           scl_in = 1'b0;                  repeat (1) @(negedge clk);
           sda_in = v;                     repeat (1) @(negedge clk);
           scl_in = 1'b1;                  repeat (2) @(negedge clk);
           scl_in = 1'b0;                  repeat (1) @(negedge clk);
       endtask

       task automatic do_stop();
           scl_in = 1'b0; sda_in = 1'b0;   repeat (2) @(negedge clk);
           scl_in = 1'b1;                  repeat (2) @(negedge clk);
           sda_in = 1'b1;                  repeat (1) @(negedge clk);   // STOP edge
       endtask

       initial begin
           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1;
           fA_q = 1'b0; fB_q = 1'b0;
           tA_stop = 0; tA_free = 0; tB_stop = 0; tB_free = 0;
           repeat (3) @(negedge clk);

           // 1 -- after reset neither tracker may claim FREE yet: it has not seen
           //      the interval. Claiming free-on-reset is a real bus bug.
           if (fA !== 1'b0 || fB !== 1'b0) begin
               $display("FAIL: tracker claimed bus_free immediately after reset"); errors++; end
           rst_n = 1'b1;

           // 2 -- both must reach FREE once the bus stays released, and the SLOWER
           //      parameter must take strictly longer.
           repeat (N_B + 4) @(negedge clk);
           if (fA !== 1'b1 || fB !== 1'b1) begin
               $display("FAIL: trackers did not reach bus_free on a quiet bus"); errors++; end

           // 3 -- START makes the bus busy, and not free.
           do_start();
           if (bA !== 1'b1 || bB !== 1'b1) begin $display("FAIL: START did not set bus_busy"); errors++; end
           if (fA !== 1'b0 || fB !== 1'b0) begin $display("FAIL: bus reported free while busy"); errors++; end

           do_bit(1'b0); do_bit(1'b1);
           if (bA !== 1'b1) begin $display("FAIL: data bits cleared bus_busy"); errors++; end

           // 4 -- STOP ends the transfer but must NOT make the bus immediately free.
           do_stop();
           if (bA !== 1'b0 || bB !== 1'b0) begin $display("FAIL: STOP did not clear bus_busy"); errors++; end
           if (fA !== 1'b0 || fB !== 1'b0) begin
               $display("FAIL: bus reported free immediately at the STOP -- the interval was skipped"); errors++; end

           // 5 -- MEASURE the interval on both trackers.
           repeat (N_B + 6) @(negedge clk);
           if (fA !== 1'b1 || fB !== 1'b1) begin $display("FAIL: bus never became free after STOP"); errors++; end

           // Both stop_detected and bus_free are REGISTERED outputs, so each
           // timestamp carries one cycle of observation latency and the two cancel:
           // the measured gap is exactly BUS_FREE_CYCLES. Measuring between two
           // like-for-like observation points is what makes that true -- comparing a
           // registered flag against a combinational one would be off by one.
           gapA = tA_free - tA_stop;
           gapB = tB_free - tB_stop;
           if ((tA_free - tA_stop) != N_A) begin
               $display("FAIL: tracker A free gap = %0d cycles, expected %0d", tA_free - tA_stop, N_A); errors++; end
           if ((tB_free - tB_stop) != N_B) begin
               $display("FAIL: tracker B free gap = %0d cycles, expected %0d", tB_free - tB_stop, N_B); errors++; end
           // The difference must be exactly the parameter difference -- this is the
           // check that a single hand-tuned constant cannot satisfy.
           if (((tB_free - tB_stop) - (tA_free - tA_stop)) != (N_B - N_A)) begin
               $display("FAIL: interval does not track the parameter"); errors++; end

           // 6 -- RESTART: the interval must be CONTIGUOUS. Part-way through it, pull
           //      a line low without making a START, release it, and the whole
           //      interval starts again -- a tracker that merely paused its counter
           //      would declare the bus free too early.
           do_start(); do_stop();
           repeat (N_A - 1) @(negedge clk);
           if (fA !== 1'b0) begin $display("FAIL: free before the interval elapsed"); errors++; end
           scl_in = 1'b0;                  repeat (1) @(negedge clk);   // not a START
           scl_in = 1'b1;
           repeat (N_A - 1) @(negedge clk);
           if (fA !== 1'b0) begin
               $display("FAIL: interval did not restart after the bus went low"); errors++; end
           repeat (3) @(negedge clk);
           if (fA !== 1'b1) begin $display("FAIL: never became free after the restart"); errors++; end

           if (errors == 0)
               $display("PASS: free gaps %0d and %0d cycles track BUS_FREE_CYCLES %0d and %0d",
                        gapA, gapB, N_A, N_B);
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_state_tracker.v — SYNTHESIZABLE RTL. The same tracker in Verilog-2005.
   module i2c_bus_state_tracker #(
       // ILLUSTRATIVE default: the real requirement is a time, converted by the
       // ceiling rule. Module 11 owns the specified value.
       parameter integer BUS_FREE_CYCLES = 5
   )(
       input  wire clk,
       input  wire rst_n,
       input  wire scl_in,             // observed bus level, not drive intent
       input  wire sda_in,             // observed bus level, not drive intent
       output reg  start_detected,     // single-cycle event pulse
       output reg  stop_detected,      // single-cycle event pulse
       output wire bus_busy,
       output wire bus_free
   );
       localparam ST_FREE      = 2'd0;
       localparam ST_BUSY      = 2'd1;
       localparam ST_WAIT_FREE = 2'd2;

       localparam integer CW = (BUS_FREE_CYCLES <= 1) ? 1 : $clog2(BUS_FREE_CYCLES);

       reg [1:0]    state;
       reg [CW-1:0] cnt;
       reg          scl_q, sda_q;
       wire         saw_start, saw_stop;

       // Both framing edges are SDA transitions while SCL reads HIGH on BOTH
       // samples; they differ only in direction.
       assign saw_start = scl_q & scl_in &  sda_q & ~sda_in;   // SDA fell
       assign saw_stop  = scl_q & scl_in & ~sda_q &  sda_in;   // SDA rose

       always @(posedge clk) begin
           if (!rst_n) begin
               // Idle, but not yet FREE: the interval has to be earned.
               state <= ST_WAIT_FREE;
               cnt   <= BUS_FREE_CYCLES - 1;
               scl_q <= 1'b1; sda_q <= 1'b1;
               start_detected <= 1'b0;
               stop_detected  <= 1'b0;
           end else begin
               start_detected <= saw_start;
               stop_detected  <= saw_stop;
               scl_q <= scl_in;
               sda_q <= sda_in;

               case (state)
                   ST_FREE:
                       if (saw_start) state <= ST_BUSY;

                   ST_BUSY:
                       if (saw_stop) begin
                           state <= ST_WAIT_FREE;
                           cnt   <= BUS_FREE_CYCLES - 1;
                       end

                   ST_WAIT_FREE:
                       if (saw_start) state <= ST_BUSY;
                       else if (!(scl_in & sda_in)) cnt <= BUS_FREE_CYCLES - 1;
                       else if (cnt != 0) cnt <= cnt - 1'b1;
                       else state <= ST_FREE;

                   default: state <= ST_WAIT_FREE;
               endcase
           end
       end

       assign bus_busy = (state == ST_BUSY);
       assign bus_free = (state == ST_FREE);
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_state_tracker_tb.v — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same measurements in Verilog idiom.
   module i2c_bus_state_tracker_tb;
       localparam integer N_A = 5;
       localparam integer N_B = 8;

       reg  clk, rst_n, scl_in, sda_in;
       integer errors;

       // Two differently-parameterised trackers on the SAME stimulus: the interval
       // must TRACK the parameter, not match one hand-tuned number.
       wire sA, pA, bA, fA;
       wire sB, pB, bB, fB;

       i2c_bus_state_tracker #(.BUS_FREE_CYCLES(N_A)) dutA
           (.clk(clk), .rst_n(rst_n), .scl_in(scl_in), .sda_in(sda_in),
            .start_detected(sA), .stop_detected(pA), .bus_busy(bA), .bus_free(fA));
       i2c_bus_state_tracker #(.BUS_FREE_CYCLES(N_B)) dutB
           (.clk(clk), .rst_n(rst_n), .scl_in(scl_in), .sda_in(sda_in),
            .start_detected(sB), .stop_detected(pB), .bus_busy(bB), .bus_free(fB));

       initial clk = 1'b0;
       always #5 clk = ~clk;
       initial begin #40000; $display("FAIL: watchdog expired"); $finish; end

       integer cyc;
       integer tA_stop, tA_free, tB_stop, tB_free;
       // Latched once, when measured: later stimulus reuses these registers.
       integer gapA, gapB;
       reg fA_q, fB_q;
       always @(posedge clk) if (rst_n) begin
           cyc <= cyc + 1;
           if (pA) tA_stop <= cyc;
           if (pB) tB_stop <= cyc;
           if (fA && !fA_q) tA_free <= cyc;
           if (fB && !fB_q) tB_free <= cyc;
           fA_q <= fA; fB_q <= fB;
       end

       task do_start; begin
           scl_in = 1'b1; sda_in = 1'b1;   repeat (2) @(negedge clk);
           sda_in = 1'b0;                  repeat (2) @(negedge clk);
           scl_in = 1'b0;                  repeat (2) @(negedge clk);
       end endtask

       task do_bit; input v; begin
           scl_in = 1'b0;                  repeat (1) @(negedge clk);
           sda_in = v;                     repeat (1) @(negedge clk);
           scl_in = 1'b1;                  repeat (2) @(negedge clk);
           scl_in = 1'b0;                  repeat (1) @(negedge clk);
       end endtask

       task do_stop; begin
           scl_in = 1'b0; sda_in = 1'b0;   repeat (2) @(negedge clk);
           scl_in = 1'b1;                  repeat (2) @(negedge clk);
           sda_in = 1'b1;                  repeat (1) @(negedge clk);
       end endtask

       initial begin
           errors = 0; cyc = 0;
           tA_stop = 0; tA_free = 0; tB_stop = 0; tB_free = 0;
           fA_q = 1'b0; fB_q = 1'b0;
           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1;
           repeat (3) @(negedge clk);

           if (fA !== 1'b0 || fB !== 1'b0) begin
               $display("FAIL: tracker claimed bus_free immediately after reset"); errors = errors + 1; end
           rst_n = 1'b1;

           repeat (N_B + 4) @(negedge clk);
           if (fA !== 1'b1 || fB !== 1'b1) begin
               $display("FAIL: trackers did not reach bus_free on a quiet bus"); errors = errors + 1; end

           do_start();
           if (bA !== 1'b1 || bB !== 1'b1) begin $display("FAIL: START did not set bus_busy"); errors = errors + 1; end
           if (fA !== 1'b0 || fB !== 1'b0) begin $display("FAIL: bus reported free while busy"); errors = errors + 1; end

           do_bit(1'b0); do_bit(1'b1);
           if (bA !== 1'b1) begin $display("FAIL: data bits cleared bus_busy"); errors = errors + 1; end

           do_stop();
           if (bA !== 1'b0 || bB !== 1'b0) begin $display("FAIL: STOP did not clear bus_busy"); errors = errors + 1; end
           if (fA !== 1'b0 || fB !== 1'b0) begin
               $display("FAIL: bus free immediately at the STOP -- interval skipped"); errors = errors + 1; end

           repeat (N_B + 6) @(negedge clk);
           if (fA !== 1'b1 || fB !== 1'b1) begin $display("FAIL: bus never became free after STOP"); errors = errors + 1; end

           // Both timestamps come from REGISTERED outputs, so the one-cycle
           // observation latencies cancel and the gap is exactly BUS_FREE_CYCLES.
           gapA = tA_free - tA_stop;
           gapB = tB_free - tB_stop;
           if ((tA_free - tA_stop) != N_A) begin
               $display("FAIL: tracker A free gap = %0d, expected %0d", tA_free - tA_stop, N_A); errors = errors + 1; end
           if ((tB_free - tB_stop) != N_B) begin
               $display("FAIL: tracker B free gap = %0d, expected %0d", tB_free - tB_stop, N_B); errors = errors + 1; end
           if (((tB_free - tB_stop) - (tA_free - tA_stop)) != (N_B - N_A)) begin
               $display("FAIL: interval does not track the parameter"); errors = errors + 1; end

           // RESTART: the interval must be CONTIGUOUS. A tracker that merely paused
           // its counter would declare the bus free too early.
           do_start(); do_stop();
           repeat (N_A - 1) @(negedge clk);
           if (fA !== 1'b0) begin $display("FAIL: free before the interval elapsed"); errors = errors + 1; end
           scl_in = 1'b0;                  repeat (1) @(negedge clk);
           scl_in = 1'b1;
           repeat (N_A - 1) @(negedge clk);
           if (fA !== 1'b0) begin
               $display("FAIL: interval did not restart after the bus went low"); errors = errors + 1; end
           repeat (3) @(negedge clk);
           if (fA !== 1'b1) begin $display("FAIL: never became free after the restart"); errors = errors + 1; end

           if (errors == 0)
               $display("PASS: free gaps %0d and %0d cycles track BUS_FREE_CYCLES %0d and %0d",
                        gapA, gapB, N_A, N_B);
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_state_tracker.vhd — SYNTHESIZABLE RTL. Enumerated states; a constrained natural forbids a negative count.
   library ieee;
   use ieee.std_logic_1164.all;

   entity i2c_bus_state_tracker is
       generic (
           -- ILLUSTRATIVE default: the real requirement is a time, converted by the
           -- ceiling rule. Module 11 owns the specified value. `positive` forbids a
           -- zero-length interval at elaboration.
           BUS_FREE_CYCLES : positive := 5
       );
       port (
           clk            : in  std_logic;
           rst_n          : in  std_logic;
           scl_in         : in  std_logic;   -- observed bus level, not drive intent
           sda_in         : in  std_logic;   -- observed bus level, not drive intent
           start_detected : out std_logic;   -- single-cycle event pulse
           stop_detected  : out std_logic;   -- single-cycle event pulse
           bus_busy       : out std_logic;
           bus_free       : out std_logic
       );
   end entity;

   architecture rtl of i2c_bus_state_tracker is
       type state_t is (ST_FREE, ST_BUSY, ST_WAIT_FREE);
       signal state : state_t := ST_WAIT_FREE;
       signal cnt   : natural range 0 to BUS_FREE_CYCLES - 1 := BUS_FREE_CYCLES - 1;
       signal scl_q : std_logic := '1';
       signal sda_q : std_logic := '1';
       signal saw_start, saw_stop : std_logic;
   begin
       -- Both framing edges are SDA transitions while SCL reads HIGH on BOTH
       -- samples; they differ only in direction.
       saw_start <= scl_q and scl_in and sda_q and (not sda_in);       -- SDA fell
       saw_stop  <= scl_q and scl_in and (not sda_q) and sda_in;       -- SDA rose

       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   -- Idle, but not yet FREE: the interval has to be earned.
                   state <= ST_WAIT_FREE;
                   cnt   <= BUS_FREE_CYCLES - 1;
                   scl_q <= '1'; sda_q <= '1';
                   start_detected <= '0';
                   stop_detected  <= '0';
               else
                   start_detected <= saw_start;
                   stop_detected  <= saw_stop;
                   scl_q <= scl_in;
                   sda_q <= sda_in;

                   case state is
                       when ST_FREE =>
                           if saw_start = '1' then state <= ST_BUSY; end if;

                       when ST_BUSY =>
                           if saw_stop = '1' then
                               state <= ST_WAIT_FREE;
                               cnt   <= BUS_FREE_CYCLES - 1;
                           end if;

                       when ST_WAIT_FREE =>
                           if saw_start = '1' then
                               state <= ST_BUSY;
                           elsif not (scl_in = '1' and sda_in = '1') then
                               -- Not actually free: the contiguous interval restarts.
                               cnt <= BUS_FREE_CYCLES - 1;
                           elsif cnt /= 0 then
                               cnt <= cnt - 1;
                           else
                               state <= ST_FREE;
                           end if;
                   end case;
               end if;
           end if;
       end process;

       bus_busy <= '1' when state = ST_BUSY else '0';
       bus_free <= '1' when state = ST_FREE else '0';
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_state_tracker_tb.vhd — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same measurements with assert report severity.
   library ieee;
   use ieee.std_logic_1164.all;

   entity i2c_bus_state_tracker_tb is
   end entity;

   architecture sim of i2c_bus_state_tracker_tb is
       constant N_A : positive := 5;
       constant N_B : positive := 8;

       signal clk    : std_logic := '0';
       signal rst_n  : std_logic := '0';
       signal scl_in : std_logic := '1';
       signal sda_in : std_logic := '1';

       signal sA, pA, bA, fA : std_logic;
       signal sB, pB, bB, fB : std_logic;

       -- All timestamps are driven by one process and read by the checker.
       signal cyc     : natural := 0;
       signal tA_stop : natural := 0;
       signal tA_free : natural := 0;
       signal tB_stop : natural := 0;
       signal tB_free : natural := 0;
       -- Set by the checker just before it suspends, so the watchdog can tell a
       -- finished run from a stalled one.
       signal test_done : std_logic := '0';
   begin
       -- Two differently-parameterised trackers on the SAME stimulus: the interval
       -- must TRACK the generic, not match one hand-tuned number.
       dutA : entity work.i2c_bus_state_tracker
           generic map (BUS_FREE_CYCLES => N_A)
           port map (clk => clk, rst_n => rst_n, scl_in => scl_in, sda_in => sda_in,
                     start_detected => sA, stop_detected => pA, bus_busy => bA, bus_free => fA);
       dutB : entity work.i2c_bus_state_tracker
           generic map (BUS_FREE_CYCLES => N_B)
           port map (clk => clk, rst_n => rst_n, scl_in => scl_in, sda_in => sda_in,
                     start_detected => sB, stop_detected => pB, bus_busy => bB, bus_free => fB);

       clk <= not clk after 5 ns;

       -- Watchdog. A broken design must produce a REPORTED FAILURE, not a silent
       -- stop: without this, a `wait until` against a stalled DUT simply runs to the
       -- simulator's time limit and prints nothing that identifies the problem.
       watchdog : process
       begin
           wait for 40 us;
           if test_done = '0' then
               report "watchdog expired -- the design never reached the expected state"
                   severity failure;
           end if;
           wait;
       end process;

       stamp : process (clk)
           variable fA_q, fB_q : std_logic := '0';
       begin
           if rising_edge(clk) then
               if rst_n = '1' then
                   cyc <= cyc + 1;
                   if pA = '1' then tA_stop <= cyc; end if;
                   if pB = '1' then tB_stop <= cyc; end if;
                   if fA = '1' and fA_q = '0' then tA_free <= cyc; end if;
                   if fB = '1' and fB_q = '0' then tB_free <= cyc; end if;
                   fA_q := fA; fB_q := fB;
               end if;
           end if;
       end process;

       stim : process
           variable errs : natural := 0;
           -- Latched once, when measured: later stimulus reuses these signals.
           variable gapA, gapB : integer := 0;

           procedure waitn (n : in positive) is
           begin
               for i in 1 to n loop wait until falling_edge(clk); end loop;
           end procedure;

           procedure do_start is
           begin
               scl_in <= '1'; sda_in <= '1'; waitn(2);
               sda_in <= '0';                waitn(2);
               scl_in <= '0';                waitn(2);
           end procedure;

           procedure do_bit (v : in std_logic) is
           begin
               scl_in <= '0';   waitn(1);
               sda_in <= v;     waitn(1);
               scl_in <= '1';   waitn(2);
               scl_in <= '0';   waitn(1);
           end procedure;

           procedure do_stop is
           begin
               scl_in <= '0'; sda_in <= '0'; waitn(2);
               scl_in <= '1';                waitn(2);
               sda_in <= '1';                waitn(1);
           end procedure;
       begin
           waitn(3);
           if fA /= '0' or fB /= '0' then
               report "tracker claimed bus_free immediately after reset" severity error; errs := errs + 1; end if;
           rst_n <= '1';

           waitn(N_B + 4);
           if fA /= '1' or fB /= '1' then
               report "trackers did not reach bus_free on a quiet bus" severity error; errs := errs + 1; end if;

           do_start;
           if bA /= '1' or bB /= '1' then
               report "START did not set bus_busy" severity error; errs := errs + 1; end if;
           if fA /= '0' or fB /= '0' then
               report "bus reported free while busy" severity error; errs := errs + 1; end if;

           do_bit('0'); do_bit('1');
           if bA /= '1' then
               report "data bits cleared bus_busy" severity error; errs := errs + 1; end if;

           do_stop;
           if bA /= '0' or bB /= '0' then
               report "STOP did not clear bus_busy" severity error; errs := errs + 1; end if;
           if fA /= '0' or fB /= '0' then
               report "bus free immediately at the STOP -- interval skipped" severity error; errs := errs + 1; end if;

           waitn(N_B + 6);
           if fA /= '1' or fB /= '1' then
               report "bus never became free after STOP" severity error; errs := errs + 1; end if;

           -- Both timestamps come from REGISTERED outputs, so the one-cycle
           -- observation latencies cancel and the gap is exactly BUS_FREE_CYCLES.
           gapA := tA_free - tA_stop;
           gapB := tB_free - tB_stop;
           if (tA_free - tA_stop) /= N_A then
               report "tracker A free gap = " & integer'image(tA_free - tA_stop) &
                      ", expected " & integer'image(N_A) severity error; errs := errs + 1; end if;
           if (tB_free - tB_stop) /= N_B then
               report "tracker B free gap = " & integer'image(tB_free - tB_stop) &
                      ", expected " & integer'image(N_B) severity error; errs := errs + 1; end if;
           if ((tB_free - tB_stop) - (tA_free - tA_stop)) /= (N_B - N_A) then
               report "interval does not track the generic" severity error; errs := errs + 1; end if;

           -- RESTART: the interval must be CONTIGUOUS.
           do_start; do_stop;
           waitn(N_A - 1);
           if fA /= '0' then
               report "free before the interval elapsed" severity error; errs := errs + 1; end if;
           scl_in <= '0'; waitn(1);
           scl_in <= '1';
           waitn(N_A - 1);
           if fA /= '0' then
               report "interval did not restart after the bus went low" severity error; errs := errs + 1; end if;
           waitn(3);
           if fA /= '1' then
               report "never became free after the restart" severity error; errs := errs + 1; end if;

           if errs = 0 then
               report "i2c_bus_state_tracker self-check complete: free gaps " &
                      integer'image(gapA) & " and " &
                      integer'image(gapB) & " cycles track the generics" severity note;
           else
               report "i2c_bus_state_tracker self-check FAILED" severity error;
           end if;
           test_done <= '1';
           wait;
       end process;
   end architecture;

7a. What the Testbench Measures

The requirement is a duration, so the testbench measures a duration. It does not wait the expected number of cycles and then check a flag — that test would pass against a design that hard-coded the same number, which is the thing most worth ruling out.

Instead it instantiates two trackers with different parameters on the same stimulus and timestamps the events:

checkwhat it establishes
neither tracker claims bus_free immediately after resetthe interval has to be earned; a tracker that resets into FREE permits an instant START
both reach bus_free on a quiet busthe state machine makes progress at all
bus_busy asserted after START, cleared after STOPthe busy tracking is real
not free at the STOPthe interval is not skipped — the §3 bug
measured gap == BUS_FREE_CYCLES, for boththe interval tracks the parameter
gap difference == parameter differenceno single hard-coded constant can satisfy both
interval restarts after a mid-interval lowit is contiguous, not cumulative — §4

The two-parameterisation trick is what gives the suite its strength. A design with an off-by-one is wrong by one cycle at both settings, so measuring both and checking the difference isolates the parameter-tracking property from the absolute value.

One measurement subtlety worth naming. Both stop_detected and bus_free are registered outputs, so each timestamp carries one cycle of observation latency — and because both are observed the same way, the two latencies cancel and the measured gap is exactly BUS_FREE_CYCLES. Comparing a registered flag against a combinational one would be off by one, and the error would look like an RTL bug rather than a measurement bug. Measuring between like-for-like observation points is what makes the arithmetic clean.

Verified execution. All three run, and all three complete at the same simulated time with the same measurements:

languagesimulatorresultmeasured gapscompletes at
SystemVerilogIcarus Verilog, -g2012PASS5 and 8 cycles730 ns
Verilog-2005Icarus Verilog, -g2005PASS5 and 8 cycles730 ns
VHDLnvc 1.23.0PASS5 and 8 cycles730 ns

i2c_bus_state_tracker — busy, then released, then free

10 cycles
Ten internal clock cycles. Both observed bus lines are high throughout except SDA, which is low in the first cycle and rises in the second while SCL is high. The stop detected output pulses one cycle later and bus busy falls at the same time. The bus free output stays low for five further cycles and then rises, showing the bus-free interval between release and availability.bus-free interval, 5 cyclesbus-free interval, 5 cyclesSDA rises while SCL is highSDA rises while SCL is highSTOP reported; busy clearsSTOP reported; busy clearsfree — 5 cycles after the STOPfree — 5 cycles after theSTOPclkscl_insda_instop_detectedbus_busybus_freet0t1t2t3t4t5t6t7t8t9
Figure 2 — the tracker's own view, from the simulation, with BUS_FREE_CYCLES set to 5. SDA rises while SCL is high, the STOP is reported one cycle later, and bus_busy drops at once. bus_free does not follow it: five counted cycles of released bus elapse first, and only then does the tracker permit a START. The gap between the two registered outputs is exactly the parameter. SIMULATION-DERIVED figure — the sampled model the RTL implements, not the analog bus.

8. What a STOP Does to Every Device

Symmetrically to Chapter 5.2's broadcast, a STOP is unconditional and everyone acts on it.

Every target returns to idle. Whatever phase it was in — waiting for more address bits, mid-byte, holding an acknowledge — it stops and resets its protocol state. This is the mechanism that makes a STOP the universal escape: a controller that has confused a target can always end the transfer, and the target will let go.

Every target stops driving SDA. A target transmitting a byte, or asserting an acknowledge, must release. This matters more than it sounds: if a target kept driving after a STOP, the bus would never return to the released state and no subsequent transfer could begin. The unconditional-release rule is what makes the bus-free interval measurable at all.

Other controllers may begin competing again — but not yet. The bus becomes available only after the interval, which is what makes the interval a multi-controller fairness property as well as a per-device recovery one. Module 13 owns arbitration.

A target that was clock-stretching must let SCL go. A STOP cannot even be generated while a target holds SCL low, because the framing edge requires SCL high. This is a real deadlock shape rather than a theoretical one, and it is Module 12.

9. Mutation Testing

Five faults, each injected into the verified RTL, with the testbench run against each.

mutationwhat it breaksresult
reload the counter with BUS_FREE_CYCLESinterval one cycle too long — legal but slowFAIL — gap measured 6, required 5
reload with BUS_FREE_CYCLES - 2interval one cycle too short — illegalFAIL — gap measured 4, required 5
pause the counter instead of reloading itinterval becomes cumulative, not contiguousFAIL — caught by the restart test
report free while still waitingthe §3 bug, in the tracker itselfFAIL — free immediately after reset
reset straight into FREEpermits a START before any intervalFAIL — free immediately after reset

All five caught. Two observations worth keeping.

The two off-by-one mutations are caught by measurement and would not be caught by inspection. Both produce a design that looks right, runs, and passes any test that waits a while and then checks a flag. The only thing that distinguishes them is a number, and the only way to get the number is to timestamp two events and subtract. This is why §7a's suite measures rather than waits.

The direction of the off-by-one determines how bad it is, and the testbench does not care. Loading N makes the bus slower than necessary and remains legal; loading N-2 makes it illegal. A testbench that only checked gap >= N would pass the first and catch the second, which is arguably the "correct" specification check — and it would also silently accept a design that had drifted to N + 50. Checking equality catches both the illegal fault and the throughput regression, and on a block whose whole purpose is a specified interval, that is the right trade.

10. Verification Connection — Cycle Counting and Timestamping Are Different Tools

This chapter is the natural place to separate two verification techniques that get used interchangeably and should not be.

A cycle-based checker counts internal clock cycles. The testbench in §7a is one: it timestamps in cycles, subtracts, and compares against a parameter expressed in cycles. It is the right tool here because the thing under test is a cycle count — the tracker's contract is "this many cycles of released bus", and checking it in cycles compares like with like.

A real-time checker timestamps in physical time and does arithmetic on nanoseconds. It is the right tool when the requirement is a duration in the specification and the signals are asynchronous external edges, because that is what the specification actually constrains.

The same parameter can need both, at different stages:

stagewhat is being verifiedright tool
block level, this chapterthe tracker counts the configured number of cyclescycle-based — the contract is in cycles
block level, Chapter 5.5a generated framing margin lasts the configured number of cyclescycle-based
system level, pin edgesthe bus-free interval on the real bus is at least 4.7 µsreal-time — the requirement is in µs

The reason to be deliberate about this is that each tool is blind to the other's failures. A cycle-based check on the tracker cannot detect a wrong clock frequency, a wrong conversion, or a wrong parameter at the instantiation — it will happily confirm that 225 cycles is 225 cycles. Only a real-time check against the pins catches "225 cycles was the wrong number". Conversely, a real-time check on a block whose contract is in cycles introduces a frequency assumption the block does not have.

This is also the honest limitation of every design in this module, and it is worth stating plainly: these are sampled designs verified with cycle-based checks, and they establish protocol and sequencing correctness. They do not establish that any interval met a specification in nanoseconds. Module 21 builds the timestamp-based checker that does.

Azvya Education Pvt. Ltd.VLSI Mentor
UVM CONCEPT — VERIFICATION ONLY. Why the pin-level check needs timestamps, not cycles.
   // Not sampled on an internal clock: real-time observations of the bus, which
   // is what the specification's microseconds are a statement about.
   time t_stop_edge;
   time t_start_edge;

   // A pin-level monitor records WHEN each framing edge happened.
   always @(posedge sda_bus) if (scl_bus) t_stop_edge  = $time;   // STOP
   always @(negedge sda_bus) if (scl_bus) begin                   // START
       t_start_edge = $time;
       if (t_stop_edge != 0 && (t_start_edge - t_stop_edge) < T_BUF_MIN)
           `uvm_error("I2C_TIMING", $sformatf(
               "tBUF violation: measured %0t, required minimum %0t (STOP at %0t, START at %0t)",
               t_start_edge - t_stop_edge, T_BUF_MIN, t_stop_edge, t_start_edge))
   end

   // Note the message. "I2C timing error" would be useless; a violation report
   // has to carry the measurement, the limit, and both anchor timestamps, or the
   // engineer reading it at 2am cannot tell a marginal bus from a broken one.

11. FPGA and ASIC Implications

The counter is wider than people expect, and the width is set by the slowest mode. A bus-free interval of 4.7 µs at a 100 MHz internal clock is 470 cycles — nine bits. A design parameterised for a fast mode and then reconfigured for Standard-mode discovers this at the worst possible moment, because a counter too narrow to hold the count does not fail loudly; it wraps, and produces an interval that is short by a multiple of its range. The VHDL version in §7 uses a natural range 0 to BUS_FREE_CYCLES - 1, which turns that class of bug into an elaboration-time or runtime range error rather than silent wrapping — one of the few places where VHDL's type system buys something the Verilog versions have to get right by review.

On an FPGA the tracker usually shares the controller's clock, and that ties the interval to the clock frequency. Nothing in the design notices a clock-frequency change; the parameter is a cycle count and cycle counts do not know their own duration. A design that is retargeted to a different FPGA family with a different reference clock will silently violate the interval unless the parameter is recomputed. This is a strong argument for computing the count with the §5a idiom at elaboration, from a frequency constant that lives with the clock declaration, rather than writing 470 in the instantiation.

On an ASIC the interval is usually programmable, and that is a hazard rather than a convenience. A peripheral whose bus-free count comes from a software register can be programmed with an illegal value by a driver that does not know the mode, and hardware that accepts it will produce an illegal bus. Chapter 5.5 builds the configuration check that refuses.

Bus-free tracking must survive the controller being idle. A controller that gates off its own clock when it has nothing to do stops counting, and then cannot know whether the interval elapsed. Either the tracker stays in an always-on domain or the controller must conservatively restart the interval on wake — and restarting is the safe choice, because it errs towards waiting too long.

12. Debugging — The Transfer That Ended Twice

A bus that went quiet for exactly as long as nobody was looking

Pitfall — a bus-free counter that pauses on activity instead of restarting
Buggy Code
// A controller correctly implements the three states, correctly rounds its
// conversion up, and correctly refuses to start until the interval elapses. The
// tracker is genuinely good. It has one line wrong:
//
//   ST_WAIT_FREE:
//       if (saw_start)                    state <= ST_BUSY;
//       else if (!(scl_in && sda_in))     cnt   <= cnt;        // "hold while busy"
//       else if (cnt != 0)                cnt   <= cnt - 1;
//       else                              state <= ST_FREE;
//
// The intent reads reasonably: while the bus is disturbed, do not make progress;
// resume counting when it goes quiet again. It even sounds MORE careful than
// reloading, because it refuses to count disturbed time.
//
// It passes every test that lets the bus go quiet after a STOP and stay quiet,
// which is every test anyone writes first, because that is what a STOP normally
// leads to.
Symptom

Intermittent NACKs on the first transfer after bursts of activity from a second controller on the same segment -- a sensor hub that occasionally talks to the same PMIC. Single-controller operation is flawless.

The failure rate correlates with how often the two controllers' traffic happens to interleave, which makes it look like an arbitration bug and sends the investigation into Module 13 territory. Arbitration is in fact working correctly: captures show clean loss detection, clean back-off, no double-driving.

What the capture does show, if you measure it rather than read it, is a STOP-to-START gap shorter than the specification minimum -- but only sometimes, and only when there was intervening activity. A gap measured on a quiet bus is always correct, so a spot check of "does this controller respect bus-free time" passes.

Root Cause

The interval was implemented as a TOTAL rather than as a CONTIGUOUS stretch.

Section 4 is the specification point: the bus-free requirement exists to give every device an undisturbed stretch of quiet in which to return to idle. A device interrupted part way through that return has not finished, so the time it already accumulated bought nothing. Pausing the counter preserves that worthless accumulated time and then adds the remainder to it, producing a design that waits the right TOTAL amount of quiet spread across interruptions and the wrong contiguous amount.

Concretely, with a 470-cycle interval: the controller sees a STOP, counts down 300 cycles, the other controller's transfer begins and ends, and this controller resumes from 170 -- so the slowest target on the bus got 300 cycles of quiet, then a transfer, then 170 cycles, and never the 470 contiguous cycles it needed. The target that was slowest to return to idle is the one that misses the next START, which is why the victim device is not the same one every time and why swapping parts "fixes" it temporarily.

The reason it reads as an arbitration bug is that it only fires when two controllers interleave, and interleaving is arbitration's job. But arbitration is about who wins a simultaneous start; this is about a timer that measured the wrong thing. The distinction is visible in one measurement -- the STOP-to-START gap -- and invisible in the arbitration waveform, which is entirely correct.

Fix
// One line. RELOAD, do not pause:
//
//   ST_WAIT_FREE:
//       if (saw_start)                 state <= ST_BUSY;
//       else if (!(scl_in && sda_in))  cnt   <= BUS_FREE_CYCLES - 1;   // RESTART
//       else if (cnt != 0)             cnt   <= cnt - 1;
//       else                           state <= ST_FREE;
//
// The verification fix is the one that generalises, because the RTL fix is
// obvious once the requirement is stated correctly and the test gap is what let
// it through. The suite in section 7a had to gain a case that does not occur in
// normal operation at all:
//
//   issue a STOP, wait PART of the interval, disturb the bus WITHOUT making a
//   START, release it, and require that the full interval elapses again.
//
// That test is the only one in the suite that distinguishes reload from pause,
// and section 9 confirms it: injecting the pause fault into the corrected design
// produces a FAIL from exactly this case and from nothing else.
//
// The diagnostic habit: when a requirement is a duration, ask whether it is
// CONTIGUOUS or CUMULATIVE before writing the counter, and put the answer in a
// comment. The specification usually implies it rather than stating it, and the
// implication follows from WHY the interval exists -- here, from the fact that a
// device's return to idle cannot be paused and resumed either.
//
// The measurement habit: measure the gap between the STOP and the next START on
// a BUSY bus, not a quiet one. A specification interval verified only in the
// easy case is verified in the case that never fails.

13. Common Misconceptions

"A STOP is SDA going high." A STOP is SDA going high while SCL is high. SDA goes high routinely to transmit a one and to release after an acknowledge.

"STOP is just START with the edge reversed, so the same code works with a sign flip." The definitions mirror; the obligations do not. A START completes at its edge and a STOP begins an interval. §2 tabulates the differences and §12 is what conflating them costs.

"The bus is free when both lines are high." Released and free are electrically identical and temporally different. This is the single most productive misconception on this bus, and it produces failures that reproduce only on busy boards.

"Rounding the cycle count down by one is negligible." It is a specification violation. §5's Case 2 is 12.5 ns short, which is invisible on the bench and fails against particular devices in production.

"Waiting the expected number of cycles and checking a flag is a timing test." It is a test that passes against a design that hard-coded the same number. Measure the interval and compare; §7a instantiates two parameterisations precisely so a constant cannot pass.

"A target can end a transfer it cannot continue." It cannot generate a STOP, because that needs SCL high at a chosen instant. Its options are to NACK or to stretch the clock.

"If the bus-free interval is respected on a quiet bus, it is respected." §12 depends on exactly this false inference: the interesting case is a disturbed interval, which normal operation rarely produces and a test has to construct deliberately.

14. Reason It Through

A requirement is 4.7 µs and the internal clock is 48 MHz. Your colleague computes 225 cycles. What is wrong, by how much, and why will the bench not show it?

The division gives 225.6, and a minimum must round up, so the legal count is 226. At 225 cycles the interval is 4687.5 ns — 12.5 ns short. The bench will not show it because 12.5 ns of missing bus-free time only matters to a device that needed nearly all of the interval to return to idle, and whether any device on a given board is that slow depends on the parts, the temperature and the loading. It is a violation that manifests as a yield problem rather than a functional one.

Why does the tracker reload its counter rather than pausing it, and what test distinguishes the two?

Because the requirement is a contiguous stretch of released bus, not a total quantity of quiet — a device interrupted part way through returning to idle has to start over, so the time it had accumulated is worthless. The only test that distinguishes them disturbs the bus part-way through the interval without issuing a START and then requires the full interval to elapse again. That case does not arise in normal operation, which is why §12's bug survived to production.

Both stop_detected and bus_free are registered. Why does that make the measured gap exactly BUS_FREE_CYCLES rather than one more or one less?

Because each timestamp is taken one cycle after the event it reports, and the two offsets cancel when you subtract. The measurement is between like-for-like observation points. If bus_free were combinational from the state while stop_detected stayed registered, the gap would come out one short — and the resulting "off-by-one" would be in the measurement, not the design, which is a genuinely hard bug to see because the RTL under suspicion is correct.

A controller gates off its clock while idle and wakes on bus activity. What must it assume about the bus-free interval on wake?

That it has not elapsed. While the clock was stopped the counter was not running, so the controller has no evidence about how long the bus was quiet — and the safe assumption errs towards waiting, because waiting too long costs throughput while starting too early is illegal. Restarting the full interval on wake is the correct conservative choice; the alternative is keeping the tracker in an always-on domain so it never loses count.

Your tracker passes a cycle-based check confirming it counts 226 cycles. What has that not established?

That 226 was the right number. A cycle-based check compares a cycle count against a cycle count and is structurally incapable of noticing a wrong clock frequency, a wrong conversion, or a wrong parameter at the instantiation. Only a real-time check against the pins — measuring the STOP-to-START gap in nanoseconds against the specification minimum — can catch "the block correctly counts the wrong number". §10 tabulates which tool belongs at which stage.

15. Understanding Check

16. Summary

A STOP is a low-to-high SDA transition while SCL is high, generated only by the controller, and it requires releasing SCL before SDA.

The definitions mirror START; the obligations do not. A START is complete at its edge. A STOP begins an interval, and the bus is not available until that interval has passed.

Released and free are electrically identical and temporally different. No level test can tell them apart, which is why conflating them produces failures that appear only when the previous transfer was recent.

The interval must be contiguous, not cumulative, because its purpose is to give the slowest device an undisturbed stretch in which to return to idle. In RTL that means the counter reloads rather than pauses — one line, and §12 is what the other line costs.

A minimum duration converts to cycles by rounding up, and floor is what integer arithmetic gives you by default. The safe idiom scales the units to avoid overflow and adds the divisor minus one to get a true ceiling.

Loading N - 1 and transitioning at zero produces exactly N cycles, and §6 proves it on a timeline rather than asserting it.

Measure intervals; do not wait them. Two parameterisations and a timestamp subtraction; both off-by-one faults in §9 are caught by measurement and by nothing else.

Cycle-based checking and real-time checking answer different questions. The first confirms a block counts what it was told to; only the second can confirm that what it was told was right.

17. What Comes Next

The bus can now be claimed and returned, and the interval that separates one transfer from the next is accounted for. Which raises the question this module has been circling since Chapter 5.1: if returning the bus is expensive — an interval during which nobody may transmit — is there a way to keep a conversation going without paying it?

There is, and it reuses the START edge rather than adding anything. Chapter 5.4 builds the classifier that tells the two apart, and the state it needs is exactly the bus_busy this chapter's tracker already maintains.

Browse the full path on the I²C tutorials index. For the opposite edge, see The START Condition; for the three states this chapter separates, Bus Idle and the Framing Primitives.

Continue learning