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 cycles2. Where the Symmetry Stops
It is worth laying the two events side by side, because the asymmetries are the content of this chapter.
| START | STOP | |
|---|---|---|
| SDA edge while SCL is high | falling | rising |
| generated by | controller only | controller only |
| bus state afterwards | busy, immediately | released — then free, after an interval |
| what the controller does next | pulls SCL low for the first clock pulse | nothing; both lines stay released |
| effect on every target | begin listening for an address | return to idle and watch for the next START |
| can a transfer follow immediately? | n/a | no |
| the design object it needs | an edge detector | an 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:
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:
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.
// 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.
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.
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 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 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 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 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; 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:
| check | what it establishes |
|---|---|
neither tracker claims bus_free immediately after reset | the interval has to be earned; a tracker that resets into FREE permits an instant START |
both reach bus_free on a quiet bus | the state machine makes progress at all |
bus_busy asserted after START, cleared after STOP | the busy tracking is real |
| not free at the STOP | the interval is not skipped — the §3 bug |
measured gap == BUS_FREE_CYCLES, for both | the interval tracks the parameter |
| gap difference == parameter difference | no single hard-coded constant can satisfy both |
| interval restarts after a mid-interval low | it 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:
| language | simulator | result | measured gaps | completes at |
|---|---|---|---|---|
| SystemVerilog | Icarus Verilog, -g2012 | PASS | 5 and 8 cycles | 730 ns |
| Verilog-2005 | Icarus Verilog, -g2005 | PASS | 5 and 8 cycles | 730 ns |
| VHDL | nvc 1.23.0 | PASS | 5 and 8 cycles | 730 ns |
i2c_bus_state_tracker — busy, then released, then free
10 cycles8. 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.
| mutation | what it breaks | result |
|---|---|---|
reload the counter with BUS_FREE_CYCLES | interval one cycle too long — legal but slow | FAIL — gap measured 6, required 5 |
reload with BUS_FREE_CYCLES - 2 | interval one cycle too short — illegal | FAIL — gap measured 4, required 5 |
| pause the counter instead of reloading it | interval becomes cumulative, not contiguous | FAIL — caught by the restart test |
| report free while still waiting | the §3 bug, in the tracker itself | FAIL — free immediately after reset |
| reset straight into FREE | permits a START before any interval | FAIL — 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:
| stage | what is being verified | right tool |
|---|---|---|
| block level, this chapter | the tracker counts the configured number of cycles | cycle-based — the contract is in cycles |
| block level, Chapter 5.5 | a generated framing margin lasts the configured number of cycles | cycle-based |
| system level, pin edges | the bus-free interval on the real bus is at least 4.7 µs | real-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.
// 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
// 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.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.
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.
// 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
Related tutorials
- Related topic
The START Condition
START is SDA falling while SCL is high, it is generated only by the controller, and it makes the bus busy. Derive what every device must do in response, then build a detector in three languages and find out why its two guard terms and its reset value are all load-bearing.
- Related topic
The Data-Valid Rule — SDA Stable While SCL Is High
One sentence governs every bit on an I²C bus, and it is derived rather than decreed: the receiver needs a settled value at the instant it looks. What falls out is that an SDA edge while SCL is HIGH cannot be data — which is why the bus reserves it for framing.
- Related topic
Repeated START — Holding the Bus Between Phases
A repeated START is not a new waveform. It is the START edge again, and what makes it a different event is that the bus was already busy. That single fact is why a classifier needs state and why a monitor that joins late cannot classify what it sees.
- Related topic
START/STOP Timing and Malformed Framing
Three framing margins, each with two anchor events, all of them minimums: the hold after a START, the setup before a repeated START, and the setup before a STOP. Build a sequencer that generates all three and refuses an illegal configuration, then catalogue the malformed framing the margins exist to prevent.
