I²C · Module 23
Clock-Stretching and Arbitration Failures in the Field
Why a timeout is the least informative evidence an I²C controller produces, and what to record instead. Six situations that all report the same timeout, a classifier that separates them, and the one failure in the set that never times out at all — a controller that does not honour stretching, corrupting data silently and blaming the target.
Two I²C mechanisms exist specifically so that a participant can legitimately stop the bus. A target may hold SCL low for as long as it needs (Chapter 12); a controller that loses arbitration must stop driving immediately (Chapter 13.4). Both are designed behaviour, and both look exactly like a bus that has broken.
That ambiguity is this chapter's subject, and it has a specific consequence: a timeout is almost the least informative thing an I²C controller can report.
1. Six Situations, One Report
A controller logs "I2C timeout". That observation is consistent with at least six situations:
| situation | is anything wrong? | what fixes it |
|---|---|---|
| a target is stretching, legally, and the transfer will complete | no | nothing — wait |
| a target is stretching longer than this system budgeted for | maybe | a longer budget, or a faster target |
| this controller does not honour stretching and has run ahead of the bus | yes, here | the controller |
| this controller lost arbitration and did not act on it | yes, here | the controller's arbitration path |
| this controller is itself holding a line | yes, here | the controller's error path |
| nothing is held and the controller's own logic has stopped | yes, here | the controller's state machine |
A timeout value distinguishes none of them. It fires the same way in all six, and the number it fired at is a property of the threshold rather than of the bus. Worse, four of the six are faults in the controller doing the reporting — so the instrument is, statistically, most likely to be indicting the bus for its own behaviour.
The useful instrument is therefore not a better timeout. It is one that records what the bus was doing while no progress was being made.
2. The Failure That Never Times Out
Before the classifier, the situation that motivates the whole chapter — and the one a timeout can never detect, because nothing ever times out.
A target stretches by holding SCL low after the controller releases it. A conforming controller responds by waiting: it releases SCL, watches the line, and does not begin counting its high period until the line is actually high (Chapter 13.2). A controller that does not implement this releases SCL and counts its high period from that moment.
A clock pulse the bus never saw
10 cyclesNothing in that figure produces a timeout. The controller is making progress on its own schedule; it simply is not on the bus's schedule. The consequences are silent and varied:
A bit that was never clocked. The target's shift register never advanced, so the byte it assembles is short and shifted — and the acknowledge that follows may still arrive, because acknowledging is a separate decision.
A byte that appears corrupted. Which sends a debug session straight to Chapter 23.3's electrical/protocol split, where both answers are wrong: the bus is electrically sound and the protocol engine's logic is correct. The fault is in the relationship between them.
And a failure that depends on the target. A target that never stretches works perfectly with this controller. Put a slower one on the same bus and it fails — which gets blamed on the device that was added.
3. The Classifier
// -----------------------------------------------------------------------------
// i2c_stall_classifier.sv
// Chapter 23.7's instrument. Its subject is a single, very common, and almost
// useless piece of evidence: a timeout.
//
// A controller reports "I2C timeout". That observation is consistent with at
// least six situations, three of which are not faults in the controller and one
// of which is not a fault at all:
//
// a target is stretching SCL, legally, and the transfer will complete
// a target is stretching for longer than this system budgeted for
// THIS controller is not honouring stretching and has run ahead of the bus
// this controller lost arbitration and did not notice
// this controller is itself holding the line
// nothing is holding anything and the controller's own logic has stopped
//
// A timeout value distinguishes none of them. It fires the same way in all six,
// and the number it fired at is a property of the threshold rather than of the
// bus. So the useful instrument is not a better timeout -- it is one that records
// WHAT THE BUS WAS DOING while no progress was being made.
//
// ── THE INPUTS, AND WHY EACH IS NEEDED ──────────────────────────────────────
//
// my_scl_low, my_sda_low this controller's drive intent. Without them the
// instrument cannot tell "somebody is holding SCL"
// from "I am holding SCL", which are opposite
// findings with the same appearance on the wire.
//
// scl, sda the resolved bus, read back.
//
// progress a strobe from the controller whenever its
// transaction state advances. This is what makes a
// STALL definable at all: a bus can be quiet for a
// long time legitimately, and what makes a quiet bus
// a problem is that the controller wanted to move.
//
// xmit_high asserted while this controller is transmitting a
// ONE, i.e. has released SDA intending it to be high.
// Arbitration loss is defined exactly here (Chapter
// 13.3): the line reads zero where this controller
// transmitted a one.
//
// scl_phase_advance a strobe when the controller moves on from an SCL
// high phase. Comparing it against the LINE is what
// detects a controller that never supported
// stretching: it advances while the clock it released
// is still being held low by somebody else, so the
// high period it thinks it gave never existed.
//
// ── WHAT IT CANNOT DO ────────────────────────────────────────────────────────
//
// It cannot name the device that is stretching. The bus resolves every pull-down
// identically, and `C_FOREIGN` means only "not this controller". That is the same
// limit as every other instrument in this module, and it is a property of a
// wired-AND rather than of any implementation.
// -----------------------------------------------------------------------------
module i2c_stall_classifier #(
// Clocks without a progress strobe before the situation is called a stall.
parameter integer STALL_LIMIT = 40,
// The longest stretch this system has budgeted for, in clocks. A stretch
// longer than this is not illegal -- the specification sets no limit -- it is
// longer than this system can absorb, which is a different and clearer
// statement than "the target is broken" (Chapter 12.4).
parameter integer STRETCH_BUDGET = 20,
parameter integer CNT_W = 16
) (
input wire clk,
input wire rst_n,
input wire my_scl_low,
input wire my_sda_low,
input wire scl,
input wire sda,
input wire progress,
input wire xmit_high,
input wire scl_phase_advance,
// Classification of the FIRST stall, latched. Later stalls are counted but do
// not overwrite it -- the first divergence localises, and everything after it
// may be a consequence.
output reg [2:0] stall_class,
output reg stalled,
// Stretching, measured whether or not it ever caused a stall. A system that
// never stalls but stretches for 90% of its budget is one device away from
// stalling, and only a measurement says so.
output reg [CNT_W-1:0] n_stretch,
output reg [CNT_W-1:0] max_stretch,
output reg [CNT_W-1:0] n_over_budget,
// Protocol violations by THIS controller, counted separately from anything
// the bus did.
output reg [CNT_W-1:0] n_ignored_stretch,
output reg [CNT_W-1:0] n_arb_loss,
output reg [CNT_W-1:0] n_stalls
);
localparam [2:0] C_NONE = 3'd0,
C_FOREIGN = 3'd1, // somebody else holds SCL, within budget
C_EXCESSIVE = 3'd2, // somebody else holds SCL, beyond budget
C_SELF = 3'd3, // this controller is holding a line
C_ARB = 3'd4, // arbitration was lost and not acted on
C_DEADLOCK = 3'd5; // nothing is held and nothing is moving
localparam [CNT_W-1:0] CNT_MAX = {CNT_W{1'b1}};
// Released and still low: the primitive of Chapter 17.10, used here to
// recognise a foreign hold on either line.
wire scl_foreign = ~my_scl_low & ~scl;
wire sda_foreign = ~my_sda_low & ~sda;
// Arbitration loss, exactly as Chapter 13.3 defines it: this controller
// transmitted a one and the line reads zero.
wire arb_lost = xmit_high & ~sda;
reg [CNT_W-1:0] idle_cnt; // clocks since the last progress strobe
reg [CNT_W-1:0] stretch_cnt; // clocks in the current foreign SCL hold
reg in_stretch;
reg arb_seen; // arbitration was lost since the last progress
always @(posedge clk) begin
if (!rst_n) begin
stall_class <= C_NONE;
stalled <= 1'b0;
n_stretch <= 0;
max_stretch <= 0;
n_over_budget <= 0;
n_ignored_stretch <= 0;
n_arb_loss <= 0;
n_stalls <= 0;
idle_cnt <= 0;
stretch_cnt <= 0;
in_stretch <= 1'b0;
arb_seen <= 1'b0;
end else begin
// ── stretching, measured continuously ───────────────────────────────
if (scl_foreign) begin
if (!in_stretch) begin
in_stretch <= 1'b1;
stretch_cnt <= 1;
if (n_stretch != CNT_MAX) n_stretch <= n_stretch + 1'b1;
end else if (stretch_cnt != CNT_MAX) begin
stretch_cnt <= stretch_cnt + 1'b1;
end
end else if (in_stretch) begin
in_stretch <= 1'b0;
if (stretch_cnt > max_stretch) max_stretch <= stretch_cnt;
if (stretch_cnt > STRETCH_BUDGET[CNT_W-1:0])
if (n_over_budget != CNT_MAX) n_over_budget <= n_over_budget + 1'b1;
end
// ── this controller ignoring a stretch ──────────────────────────────
// It advanced past an SCL high phase while the clock it had released was
// still being held low by somebody else. Whatever high period it thinks
// it provided never appeared on the wire.
if (scl_phase_advance && scl_foreign)
if (n_ignored_stretch != CNT_MAX) n_ignored_stretch <= n_ignored_stretch + 1'b1;
// ── arbitration ─────────────────────────────────────────────────────
if (arb_lost) begin
arb_seen <= 1'b1;
if (n_arb_loss != CNT_MAX) n_arb_loss <= n_arb_loss + 1'b1;
end
// ── the stall itself ────────────────────────────────────────────────
if (progress) begin
idle_cnt <= 0;
stalled <= 1'b0;
arb_seen <= 1'b0;
end else if (idle_cnt >= STALL_LIMIT[CNT_W-1:0]) begin
if (!stalled) begin
stalled <= 1'b1;
if (n_stalls != CNT_MAX) n_stalls <= n_stalls + 1'b1;
// The classification, as a priority over conditions rather than
// over latched flags. The order is: what THIS controller is doing
// wrong first, because a fault here makes every observation of
// the bus a consequence; then arbitration, which is a legal event
// this controller has failed to act on; then the bus.
if (my_scl_low || my_sda_low)
stall_class_upd(C_SELF);
else if (arb_seen)
stall_class_upd(C_ARB);
else if (scl_foreign)
stall_class_upd(stretch_cnt > STRETCH_BUDGET[CNT_W-1:0]
? C_EXCESSIVE : C_FOREIGN);
else if (sda_foreign)
stall_class_upd(C_FOREIGN);
else
stall_class_upd(C_DEADLOCK);
end
end else begin
idle_cnt <= idle_cnt + 1'b1;
end
end
end
// Latch the FIRST classification only. Written as a task so the guard lives in
// exactly one place: five call sites each carrying their own copy of it is
// five chances for one of them to be edited and the others not.
task stall_class_upd (input [2:0] c);
begin
if (stall_class == C_NONE) stall_class <= c;
end
endtask
endmoduleThree of its inputs deserve comment, because each exists to answer a question the wire cannot.
my_scl_low and my_sda_low — this controller's drive intent. Without them the instrument cannot tell "somebody is holding SCL" from "I am holding SCL", which are opposite findings with identical appearances. This is Chapter 23.1's three-observation-point argument reaching its natural conclusion: an instrument inside a controller has access to the intent that an analyser does not.
progress — a strobe when the transaction state advances. This is what makes a stall definable. A bus can be quiet for a long time legitimately; what makes a quiet bus a problem is that the controller wanted to move and could not. Without a progress signal, "stalled" is indistinguishable from "idle".
scl_phase_advance — a strobe when the controller moves past an SCL high phase. Comparing it against the line is what detects §2's failure. If the controller advances while the clock it released is still being held low, the high period it believes it provided never existed.
The priority order, and why it is not arbitrary
Five classifications, and the order is: this controller's own misbehaviour first, then arbitration, then the bus.
A controller holding its own bus makes every observation of that bus a consequence of its own behaviour, so reporting "somebody is holding SCL" when the somebody is you is worse than useless — it sends a reader to the wrong board. Arbitration comes next because it is a legal event this controller has failed to act on: the bus is behaving correctly and the controller is not. Only when both are clear is a foreign hold a finding about the bus.
4. Measuring Stretching Even When It Is Not a Problem
n_stretch, max_stretch and n_over_budget are recorded whether or not a stall ever occurs, and that is the design decision most likely to be left out.
A system that never stalls and routinely stretches to 90% of its budget is one device away from stalling, one temperature away, one firmware revision away. Nothing in its behaviour reports that, because nothing has gone wrong yet. Only a measurement says so — and it is the same argument as Chapter 23.5's rise-time distribution: the margin is invisible until it is gone.
5. The Matrix
// -----------------------------------------------------------------------------
// i2c_stall_classifier_tb.sv
// Six situations that all produce "I2C timeout", one classifier, and a matrix
// showing that it separates them.
//
// The bench drives the classifier's inputs directly. That is deliberate: two of
// the six situations are protocol violations BY THE CONTROLLER, and a bench built
// around a conforming controller model cannot produce them at all. An instrument
// whose job is partly to indict its own host has to be testable against a host
// that misbehaves.
//
// Bounded: every wait is a fixed number of edges and a watchdog terminates the
// run. Every stimulus assignment lands 1 ns after a clock edge.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_stall_classifier_tb;
localparam integer STALL_LIMIT = 40, STRETCH_BUDGET = 20, CNT_W = 16;
localparam [2:0] C_NONE = 3'd0, C_FOREIGN = 3'd1, C_EXCESSIVE = 3'd2,
C_SELF = 3'd3, C_ARB = 3'd4, C_DEADLOCK = 3'd5;
reg clk = 1'b0;
reg rst_n = 1'b0;
always #5 clk = ~clk;
reg my_scl_low = 1'b0, my_sda_low = 1'b0;
reg foreign_scl_low = 1'b0, foreign_sda_low = 1'b0;
reg progress = 1'b0, xmit_high = 1'b0, scl_phase_advance = 1'b0;
// A wired-AND of this controller and one other participant. Nothing drives
// high anywhere.
wire scl = ~(my_scl_low | foreign_scl_low);
wire sda = ~(my_sda_low | foreign_sda_low);
wire [2:0] stall_class;
wire stalled;
wire [CNT_W-1:0] n_stretch, max_stretch, n_over_budget,
n_ignored_stretch, n_arb_loss, n_stalls;
integer errors = 0, checks = 0, neg_detected = 0, negative_mode = 0;
i2c_stall_classifier #(.STALL_LIMIT(STALL_LIMIT), .STRETCH_BUDGET(STRETCH_BUDGET),
.CNT_W(CNT_W))
dut (
.clk(clk), .rst_n(rst_n),
.my_scl_low(my_scl_low), .my_sda_low(my_sda_low), .scl(scl), .sda(sda),
.progress(progress), .xmit_high(xmit_high),
.scl_phase_advance(scl_phase_advance),
.stall_class(stall_class), .stalled(stalled),
.n_stretch(n_stretch), .max_stretch(max_stretch), .n_over_budget(n_over_budget),
.n_ignored_stretch(n_ignored_stretch), .n_arb_loss(n_arb_loss), .n_stalls(n_stalls)
);
// ------------------------------------------------------------------ helpers
task step; begin @(posedge clk); #1; end endtask
task tick (input integer n); integer k; begin for (k=0;k<n;k=k+1) step; end endtask
task pulse_progress; begin progress = 1'b1; step; progress = 1'b0; end endtask
task chk (input [255:0] name, input integer got, input integer exp);
begin
checks = checks + 1;
if (got !== exp) begin
if (negative_mode) neg_detected = neg_detected + 1;
else begin
errors = errors + 1;
$display(" FAIL %0s: got %0d expected %0d", name, got, exp);
end
end else if (negative_mode) begin
errors = errors + 1;
$display(" FAIL negative proof did not fire: %0s", name);
end
end
endtask
task do_reset;
begin
rst_n = 1'b0;
my_scl_low = 1'b0; my_sda_low = 1'b0;
foreign_scl_low = 1'b0; foreign_sda_low = 1'b0;
progress = 1'b0; xmit_high = 1'b0; scl_phase_advance = 1'b0;
tick(3); rst_n = 1'b1; tick(2);
end
endtask
// Normal traffic: a progress strobe every few clocks, so the stall detector
// has something to stop seeing.
task run_normal (input integer n); integer k;
begin for (k = 0; k < n; k = k + 1) begin pulse_progress; tick(5); end end
endtask
// A foreign SCL hold exactly `n` samples wide. `n` is what the classifier
// must report: the first edge at which the hold is visible counts as one, so
// a hold seen at n consecutive edges measures n.
task stretch_for (input integer n); integer k;
begin
foreign_scl_low = 1'b1;
for (k = 0; k < n; k = k + 1) step;
foreign_scl_low = 1'b0;
tick(3);
end
endtask
function [63:0] cname (input integer c);
begin
case (c)
C_NONE: cname = "NONE ";
C_FOREIGN: cname = "FOREIGN";
C_EXCESSIVE: cname = "EXCESS ";
C_SELF: cname = "SELF ";
C_ARB: cname = "ARB ";
C_DEADLOCK: cname = "DEADLCK";
default: cname = "? ";
endcase
end
endfunction
integer i;
initial begin
$display("i2c_stall_classifier_tb");
// ---------------------------------------------------------------------
// T1 -- traffic with no stall. The control: everything below is a
// departure from this, and a classifier that reported a stall here would
// make every other result meaningless.
// ---------------------------------------------------------------------
$display("T1 normal traffic produces no stall");
do_reset;
run_normal(10);
chk("T1 no stall", n_stalls, 0);
chk("T1 no class latched", stall_class, C_NONE);
chk("T1 no stretches", n_stretch, 0);
chk("T1 no violations", n_ignored_stretch + n_arb_loss, 0);
// ---------------------------------------------------------------------
// T2 -- a stretch WITHIN budget that never causes a stall. Measured
// anyway, because a system that never stalls and routinely stretches to
// 90% of its budget is one device away from stalling, and only a
// measurement says so.
// ---------------------------------------------------------------------
$display("T2 a stretch inside budget, measured but not a fault");
do_reset;
for (i = 0; i < 5; i = i + 1) begin
pulse_progress;
foreign_scl_low = 1'b1; tick(STRETCH_BUDGET - 5);
foreign_scl_low = 1'b0; tick(3);
end
pulse_progress; tick(3);
chk("T2 five stretches counted", n_stretch, 5);
chk("T2 none over budget", n_over_budget, 0);
chk("T2 max stretch is below budget",
(max_stretch > 0 && max_stretch <= STRETCH_BUDGET) ? 1 : 0, 1);
chk("T2 no stall", n_stalls, 0);
// ---------------------------------------------------------------------
// T2b -- the budget boundary, on both sides. A budget whose boundary is
// never tested at the boundary is a number the instrument has never
// actually applied, and it also pins down what the stretch timer counts:
// an off-by-one in the timer and an off-by-one in the comparison are
// indistinguishable anywhere except here.
// ---------------------------------------------------------------------
$display("T2b the budget boundary");
do_reset;
pulse_progress;
stretch_for(STRETCH_BUDGET);
chk("T2b a stretch of exactly the budget measures the budget",
max_stretch, STRETCH_BUDGET);
chk("T2b and is NOT over budget", n_over_budget, 0);
pulse_progress;
stretch_for(STRETCH_BUDGET + 1);
chk("T2b one sample beyond measures one beyond", max_stretch, STRETCH_BUDGET + 1);
chk("T2b and IS over budget", n_over_budget, 1);
chk("T2b two stretches counted", n_stretch, 2);
// ---------------------------------------------------------------------
// T2c -- max_stretch is the GREATEST, not the most recent. Every stretch
// so far has been equal or increasing, where "last" and "greatest" are the
// same number.
// ---------------------------------------------------------------------
$display("T2c max stretch is the greatest, not the last");
do_reset;
pulse_progress; stretch_for(STRETCH_BUDGET - 2);
chk("T2c the long one set the maximum", max_stretch, STRETCH_BUDGET - 2);
pulse_progress; stretch_for(3);
chk("T2c a later SHORT stretch does not lower it", max_stretch, STRETCH_BUDGET - 2);
pulse_progress; stretch_for(4);
chk("T2c nor does another", max_stretch, STRETCH_BUDGET - 2);
// ---------------------------------------------------------------------
// T3 -- a foreign stretch long enough to stall, but still inside budget at
// the moment the stall is declared. This is the case where the answer is
// "wait longer", and the classifier has to say so rather than report a
// fault.
// ---------------------------------------------------------------------
$display("T3 a stall caused by a stretch, within budget");
do_reset;
pulse_progress;
foreign_scl_low = 1'b1;
tick(STALL_LIMIT + 5);
chk("T3 a stall was declared", n_stalls, 1);
chk("T3 classified as EXCESSIVE, since it outran the budget",
stall_class, C_EXCESSIVE);
foreign_scl_low = 1'b0; tick(3);
chk("T3 and the stretch is recorded as over budget", n_over_budget, 1);
// ---------------------------------------------------------------------
// T4 -- THIS controller is the one holding a line. Same symptom -- no
// progress -- and the opposite finding. This must outrank everything
// else, because a controller holding its own bus makes every observation
// of that bus a consequence of its own behaviour.
// ---------------------------------------------------------------------
$display("T4 the controller is holding its own bus");
do_reset;
pulse_progress;
my_scl_low = 1'b1;
tick(STALL_LIMIT + 5);
chk("T4 a stall was declared", n_stalls, 1);
chk("T4 classified as SELF", stall_class, C_SELF);
chk("T4 not blamed on the bus", (stall_class == C_FOREIGN) ? 1 : 0, 0);
chk("T4 and no foreign stretch was recorded", n_stretch, 0);
// ---------------------------------------------------------------------
// T5 -- arbitration lost and not acted on. The controller transmitted a
// one and the line read zero, which is the definition; it then made no
// further progress. The stall is a CONSEQUENCE of an event the controller
// should have handled, not a bus fault.
// ---------------------------------------------------------------------
$display("T5 arbitration lost and not acted on");
do_reset;
pulse_progress;
xmit_high = 1'b1; foreign_sda_low = 1'b1; // we send a 1, somebody sends a 0
tick(STALL_LIMIT + 5);
chk("T5 arbitration loss was detected", (n_arb_loss > 0) ? 1 : 0, 1);
chk("T5 a stall was declared", n_stalls, 1);
chk("T5 classified as ARBITRATION", stall_class, C_ARB);
chk("T5 not as a foreign hold", (stall_class == C_FOREIGN) ? 1 : 0, 0);
// And the memory of it must not outlive the transaction. After progress,
// a LATER stall with a completely different cause must be classified by
// that cause -- an arbitration event that is never forgotten turns every
// subsequent stall into an arbitration report.
xmit_high = 1'b0; foreign_sda_low = 1'b0;
pulse_progress; tick(3);
do_reset;
pulse_progress;
xmit_high = 1'b1; foreign_sda_low = 1'b1; tick(5);
xmit_high = 1'b0; foreign_sda_low = 1'b0;
pulse_progress; tick(3);
chk("T5 the arbitration event was recorded", (n_arb_loss > 0) ? 1 : 0, 1);
tick(STALL_LIMIT + 5);
chk("T5 a later stall on a clean bus is DEADLOCK", stall_class, C_DEADLOCK);
chk("T5 not still reported as arbitration", (stall_class == C_ARB) ? 1 : 0, 0);
// ---------------------------------------------------------------------
// T6 -- nothing is holding anything and nothing is moving. Both lines idle
// high, no stretching, no arbitration event, and no progress. That is the
// controller's own logic having stopped, and it is the one case where the
// bus is entirely exonerated.
// ---------------------------------------------------------------------
$display("T6 a clean bus and no progress");
do_reset;
pulse_progress;
tick(STALL_LIMIT + 5);
chk("T6 a stall was declared", n_stalls, 1);
chk("T6 classified as DEADLOCK", stall_class, C_DEADLOCK);
chk("T6 the bus reads idle", (scl && sda) ? 1 : 0, 1);
chk("T6 nothing was held", n_stretch, 0);
// ---------------------------------------------------------------------
// T7 -- a controller that does not honour stretching. It advances past an
// SCL high phase while the clock it released is still held low. There is
// NO STALL: the controller carries on happily, and the bus is corrupted
// silently. This is the situation a timeout can never detect, because
// nothing ever times out.
// ---------------------------------------------------------------------
$display("T7 a controller that ignores stretching -- and never times out");
do_reset;
for (i = 0; i < 6; i = i + 1) begin
pulse_progress;
foreign_scl_low = 1'b1; // the target stretches
tick(3);
scl_phase_advance = 1'b1; step; // and we advance anyway
scl_phase_advance = 1'b0;
tick(2);
foreign_scl_low = 1'b0; tick(3);
end
pulse_progress; tick(3);
chk("T7 six violations counted", n_ignored_stretch, 6);
chk("T7 and NO stall was ever declared", n_stalls, 0);
chk("T7 so no class was latched", stall_class, C_NONE);
chk("T7 the stretches were seen", n_stretch, 6);
// The control: the same advance with no foreign hold is not a violation.
do_reset;
for (i = 0; i < 6; i = i + 1) begin
pulse_progress;
scl_phase_advance = 1'b1; step; scl_phase_advance = 1'b0; tick(3);
end
chk("T7 advancing on a free clock is not a violation", n_ignored_stretch, 0);
// ---------------------------------------------------------------------
// T8 -- the first classification is kept. A stall that begins as a foreign
// hold and is followed by the controller grabbing a line must still report
// the first cause; the second is downstream of it.
// ---------------------------------------------------------------------
$display("T8 the first classification is the one kept");
do_reset;
pulse_progress;
foreign_scl_low = 1'b1;
tick(STALL_LIMIT + 5);
chk("T8 first class is EXCESSIVE", stall_class, C_EXCESSIVE);
my_scl_low = 1'b1; tick(STALL_LIMIT + 5);
chk("T8 still EXCESSIVE after a later SELF condition", stall_class, C_EXCESSIVE);
chk("T8 and only one stall was counted", n_stalls, 1);
pulse_progress; tick(2);
my_scl_low = 1'b0; foreign_scl_low = 1'b0; tick(2);
chk("T8 progress clears the stalled flag", stalled, 0);
chk("T8 but not the latched class", stall_class, C_EXCESSIVE);
// A SECOND stall, after progress cleared the first. The classification
// latch is what is under test here: the second stall has a different and
// higher-priority cause, and the report must still name the first.
my_scl_low = 1'b1;
tick(STALL_LIMIT + 5);
chk("T8 a second stall was counted", n_stalls, 2);
chk("T8 and the FIRST class is still the one reported", stall_class, C_EXCESSIVE);
chk("T8 not overwritten by the later SELF cause",
(stall_class == C_SELF) ? 1 : 0, 0);
// ---------------------------------------------------------------------
// T9 -- the matrix. Five stall causes, five classes, each reached only by
// its own cause. The off-diagonal is what makes it a discrimination
// rather than five plausible answers.
// ---------------------------------------------------------------------
$display("T9 the matrix");
begin : matrix
integer r [0:4];
integer a, b, collisions;
// 0 EXCESSIVE stretch, 1 SELF, 2 ARB, 3 DEADLOCK, 4 foreign SDA hold
do_reset; pulse_progress; foreign_scl_low = 1'b1; tick(STALL_LIMIT+5);
r[0] = stall_class;
do_reset; pulse_progress; my_sda_low = 1'b1; tick(STALL_LIMIT+5);
r[1] = stall_class;
do_reset; pulse_progress; xmit_high = 1'b1; foreign_sda_low = 1'b1;
tick(STALL_LIMIT+5); r[2] = stall_class;
do_reset; pulse_progress; tick(STALL_LIMIT+5);
r[3] = stall_class;
do_reset; pulse_progress; foreign_sda_low = 1'b1; tick(STALL_LIMIT+5);
r[4] = stall_class;
for (a = 0; a <= 4; a = a + 1)
$display(" cause %0d -> %0s", a, cname(r[a]));
chk("T9 excessive stretch", r[0], C_EXCESSIVE);
chk("T9 self-held", r[1], C_SELF);
chk("T9 arbitration", r[2], C_ARB);
chk("T9 deadlock", r[3], C_DEADLOCK);
chk("T9 foreign SDA hold", r[4], C_FOREIGN);
collisions = 0;
for (a = 0; a <= 4; a = a + 1)
for (b = a + 1; b <= 4; b = b + 1)
if (r[a] == r[b]) collisions = collisions + 1;
chk("T9 no two causes share a class", collisions, 0);
end
// ---------------------------------------------------------------------
// T10 -- negative proof. Nine deliberately wrong expectations.
// ---------------------------------------------------------------------
$display("T10 negative proof -- nine deliberately wrong expectations");
do_reset;
pulse_progress; foreign_scl_low = 1'b1; tick(STALL_LIMIT + 5);
foreign_scl_low = 1'b0; tick(3);
chk("T10 setup: one stall", n_stalls, 1);
chk("T10 setup: excessive", stall_class, C_EXCESSIVE);
negative_mode = 1;
chk("T10a wrong stall count", n_stalls, 0);
chk("T10b wrong class", stall_class, C_SELF);
chk("T10c wrong stretch count", n_stretch, 0);
chk("T10d wrong over-budget count",n_over_budget, 0);
chk("T10e wrong max stretch", max_stretch, 0);
chk("T10f wrong violation count", n_ignored_stretch, 4);
chk("T10g wrong arbitration count",n_arb_loss, 4);
chk("T10h wrong stalled flag", stalled, 0);
chk("T10i wrong class distinctness",(stall_class == C_NONE) ? 1 : 0, 1);
negative_mode = 0;
chk("T10 all nine negatives detected", neg_detected, 9);
$display("");
$display("checks=%0d errors=%0d negatives_detected=%0d/9", checks, errors, neg_detected);
if (errors == 0) $display("RESULT: PASS"); else $display("RESULT: FAIL");
$finish;
end
initial begin
#2000000;
$display("RESULT: FAIL -- watchdog expired, the run did not terminate");
$finish;
end
endmodule cause class
a foreign hold on SCL, past the budget EXCESSIVE
this controller holding SDA SELF
arbitration lost and not acted on ARB
both lines idle, no progress DEADLOCK
a foreign hold on SDA FOREIGN
and the bench asserts that no two causes share a class.Then the sixth situation, which produces no class at all:
six clock phases advanced while the target was holding SCL low
n_ignored_stretch = 6 the violations, counted
n_stalls = 0 no stall was ever declared
stall_class = NONE so nothing was ever classified
n_stretch = 6 the stretches themselves were seen
control: the same six advances with the clock FREE
n_ignored_stretch = 0 advancing on a free clock is not a violationThat control is not decoration. Without it, a counter that increments on every scl_phase_advance regardless of the line would produce the same six, and the test would be measuring the stimulus rather than the design. The mutation that makes exactly that change is killed by exactly that control.
6. Where the Survivors Were
FIRST CAMPAIGN 15 valid 10 KILLED 5 SURVIVED
AFTER T2b, T2c, and extensions to T5 and T8
15 valid 15 KILLED 0 SURVIVEDAll five survivors were in the same two places, and neither is the classification logic that the chapter is about.
Two were the budget boundary. >= instead of >, and a stretch timer starting at zero instead of one. Neither is observable unless a stretch of exactly the budget is measured, and the first version of the bench used budget − 5. The two mutations are also indistinguishable from each other anywhere except at the boundary, which is worth noticing: an off-by-one in a counter and an off-by-one in the comparison that reads it produce identical behaviour everywhere else.
Three were sequence. max_stretch recording the last rather than the greatest, the classification latch taking the last cause rather than the first, and an arbitration event that is never forgotten. Each needed the same shape of test: do the thing, then do a different thing, and check that the first is still reported.
7. What This Cannot Settle
It cannot name the stretching device. FOREIGN means "not this controller", and the wired-AND supplies nothing further. Two targets that both stretch are, to this instrument, one.
It cannot tell a stretch from a stuck bus except by duration. Both are SCL held low by somebody else. The budget is the only boundary, and it is a number somebody chose rather than a property of the bus — so EXCESSIVE at a 1 ms budget and FOREIGN at a 10 ms budget are the same physical event.
It cannot detect arbitration loss on SCL. Arbitration is decided on SDA (Chapter 13.3); SCL synchronises rather than arbitrates, and a controller whose SCL is pulled low by a peer is being synchronised, not defeated. The classifier reports that as a foreign hold, correctly.
And it cannot see a second controller that behaves correctly. Two controllers, one of which loses arbitration and withdraws properly, produce no stall and no event — which is the system working. The absence of a report is the right answer and it is indistinguishable from the instrument being switched off, which is why n_stretch and the progress strobe both have to be non-zero for a clean result to mean anything.
8. Misconceptions
9. Debug Lab
A new sensor works on the evaluation board and fails on the product
Two systems, one device, and the measurement that belongs to neither of them
// A humidity sensor, qualified on the vendor's evaluation board and then fitted
// to a product. On the product it returns corrupted readings: the first byte of
// each two-byte read is correct and the second is shifted -- the value looks like
// the right data with its bits moved along by one or two positions.
//
// On the evaluation board, at the same bus speed, it is perfect.
//
// The observation that the FIRST byte is fine and the SECOND is shifted is doing
// real work already. A wholesale addressing or wiring fault would corrupt both.
// Something changes between the first byte and the second.
//
// Candidates:
//
// (a) the product's controller mishandles the second byte of a read -- a
// distinct code path from the first
// (b) the sensor needs a conversion interval between bytes and the product
// reads faster than the evaluation board
// (c) the sensor stretches SCL between bytes and the product's controller does
// not honour it
// (d) the product's bus is electrically worse and the second byte is where
// the degradation shows
// (e) the two systems run different sensor firmware revisionsStep 3 of Chapter 23.1's workflow first, because it is cheap. Chapter 23.5's profiler on the product's bus:
SDA, 40,000 releases: b0=39,994 b3=0 glitch=0 hold=0 max=147 ns SCL, 40,000 releases: b0=39,997 b3=0 glitch=0 hold=0 max=139 ns
Both lines comfortably inside a 300 ns Fast-mode budget, no spikes, no holds. Candidate (d) is dead, and it is worth noting how quickly: two numbers, and an entire layer eliminated before any protocol reasoning.
Now the stall classifier, instantiated in the product's controller, over 500 sensor reads:
n_stalls = 0 stall_class = NONE n_stretch = 1,000 max_stretch = 31 us n_over_budget = 0 n_arb_loss = 0 n_ignored_stretch = 1,000
Read the last two lines together, because the pair is the finding.
n_stalls = 0 and stall_class = NONE. Nothing ever timed out. There is no error to look up, no log entry, no status bit -- which is exactly why this had been attributed to the sensor: the controller reports that everything is fine.
n_ignored_stretch = 1,000, against 500 reads. The controller advanced past an SCL high phase while the clock was still held low, exactly TWICE per read.
Two per read, on a two-byte read. Once between the address and the first byte, once between the first and second.
And the evaluation board, with the same sensor and the same bus speed:
n_stretch = 1,000 the sensor stretches there too n_ignored_stretch = 0 and that controller waits
The product's controller does not honour clock stretching. The sensor stretches between bytes -- 31 us, which is well within any reasonable budget and is the reason nothing ever times out -- and the product's controller releases SCL, counts its high period from that moment, and drives low again while the line is still being held down by the sensor.
The high period it believes it provided never appears on the wire, so the sensor never clocks that bit in. Its shift register is one bit behind for the rest of the byte, which is precisely the "correct data shifted along" symptom.
The evaluation board's controller waits for the line, so the same sensor works.
Candidates (a), (b) and (e) all die on one observation: the sensor's behaviour is IDENTICAL in both systems. n_stretch = 1,000 on both, and 31 us on both. The sensor is not doing anything different; the controllers are.
It is worth being explicit about why the natural experiment -- swapping the sensor for one from the evaluation board -- would have proved nothing. It would have failed too, because the fault is not in any sensor. And it would have consumed the afternoon that the 1,000-versus-0 comparison took ten minutes to settle.
The discriminating measurement was made on the party COMMON to both systems only to establish that it was the same; the finding itself came from a counter that exists in neither product and had to be added. That is the shape of this class of bug: the evidence is inside the controller, the controller reports no error, and the only visible symptom is on a third device.
CORRECTION, and there are two levels:
the immediate fix is in the product's controller: release SCL, wait for the line to read HIGH, and only then begin counting the high period. That is Chapter 13.2's clock synchronisation, and it is the same change Chapter 23.6's recovery sequencer needed for the same reason.
the systemic fix is that n_ignored_stretch belongs in the controller's status register permanently. It cost two flip-flops and a comparator, and it is the difference between this taking ten minutes and taking a fortnight.
PROOF:
1. before: n_ignored_stretch = 1,000 over 500 reads, second byte shifted 2. after the controller change: n_ignored_stretch = 0 over the same 500 reads, with n_stretch UNCHANGED at 1,000 -- the sensor still stretches exactly as much, and the controller now waits 3. and the readings are correct
Step 2 is the one that proves the mechanism. n_stretch unchanged says the sensor's behaviour did not move, so the change in outcome is attributable entirely to the controller. If n_stretch had also changed, the fix would have altered two things at once and neither could be credited.
10. Reason It Through
11. Questions
12. What This Chapter Settled
Six situations that produce the same timeout, four of them faults in the controller reporting it, one of them not a fault at all. A timeout distinguishes none of them, and the value it fired at is a property of the threshold rather than of the bus.
A classifier that records what the bus was doing while no progress was being made, with five causes mapping to five classes and the off-diagonal asserted. Sixty-five checks, zero errors, nine negative proofs, fifteen mutations all killed. Its priority order is the module's recurring one: this participant's own behaviour first, because a fault there makes every observation of the bus a consequence.
Stretching measured whether or not it ever caused a stall, because a system at 90% of its budget has no symptoms and no margin. And "longer than budgeted" rather than "too long", because the specification sets no limit and the useful claim is about a number somebody chose.
The five mutation survivors sat in two places, neither of them the classification logic: a budget boundary never tested at the boundary, and three latches tested with one event where two were needed.
And the failure that never times out — a controller that advances past a high period the bus never granted, corrupting data silently and blaming the target. Its evidence exists at no single observation point: the line is on the wire, the phase is inside the chip, and only an instrument holding both can form the conjunction. That instrument costs two flip-flops and has to be designed in before the fault appears.
Which is the thread running through every remaining hardware-only failure: not that hardware is harder than simulation, but that the evidence lives somewhere simulation never had to look. Chapter 23.8 is the last of them.
Continue learning
Related tutorials
- Related topic
I²C Transaction Atomicity and Bus Ownership Across Phases
What is and is not atomic on an I²C bus, stated precisely. Three things end bus ownership and one that looks like it should does not — and telling them apart needs one input the wire cannot supply.
- Related topic
I²C Stretch Bounds, Timeouts and the No-Assumption Rule
The specification places no limit on how long a slave may hold the clock, so every timeout is a system policy rather than a compliance check. Includes the recovery asymmetry most engineers know only half of.
- Related topic
Bus Feedback — Clock Stretching and Arbitration From One Comparison
Clock stretching and arbitration loss are not two features. They are one comparison — a line this master released that reads back low — applied to two wires, differing only in a timing qualifier and an intent gate. Builds both from a single comparator and shows why a master can only ever lose by trying to send a one.
- Related topic
Stuck Bus — Diagnosis and Recovery
A bus held low, who is holding it, and why the standard recovery works for exactly one of its seven causes. Builds a clear sequencer whose pulse count is evidence rather than a boolean, runs it against seven holders including a legally stretching target, and reports the four bugs it took to get there — two in the design and two in the bench.
