I²C · Module 23
Reading a Capture — Framing, Address, ACK and the First Bad Byte
How to turn a few thousand samples of two wires into a sentence, and then into a number: the decode rules in the order they apply, the provisional-bit rule that stops every STOP decoding as a truncated byte, and a first-divergence diff that answers where a capture stops matching what was asked for. Includes a decoder whose first version had that exact bug, and the ten failures from one cause that found it.
Chapter 23.1 opens with "state the observation". On a real bus the observation does not arrive as a sentence. It arrives as a few thousand samples of two signals, with no annotation on them at all, and turning that into something you can reason about is a skill with rules.
This chapter is those rules, in the order they apply, and then the tool that makes the second question mechanical. Because once you can read a capture, the useful question is rarely "is this valid I²C" — most captures of a broken system are perfectly valid I²C. The useful question is where does this stop matching what I asked for, and that question has an answer which is a number.
1. What a Capture Contains, and What It Does Not
A capture is a time series of two levels. That is the whole of it.
It does not contain: which device drove any bit, what any device intended, what the software asked for, whether a byte was a register address or data, or what any value means. Every one of those is supplied by you, from outside the capture, and confusing the two is the mistake Chapter 23.1 §1 is about. A capture supports statements of the form "the ninth bit of the second byte was high". It does not support "the target NACKed the register address" — that sentence contains three interpretations, and two of them can be wrong while the observation stays true.
The discipline that keeps this straight is to decode in two passes, and to write down the first one before starting the second:
PASS 1 — OBSERVATION. Samples to events. Framing, bytes, acknowledge levels.
Needs nothing but the specification. Has exactly one
correct answer for any given capture.
PASS 2 — INTERPRETATION. Events to meaning. Which byte was the address, what
direction it selected, which register the second byte
pointed at, whose acknowledge that was. Needs the
datasheet, the driver source and the board. Can be
wrong while pass 1 is right.The decoder in §4 does pass 1 and refuses to do pass 2. That refusal is a design decision, not a limitation, and §6 shows what it buys.
2. The Decode Rules
Four rules, all from UM10204, and one of them is not obvious.
Sample SDA on the rising edge of SCL. The data-valid rule (§3.1.3) requires SDA to be stable for the whole SCL high period, so every instant in it reads the same bit. The rising edge is simply the first one, which makes it the natural choice — and on a marginal board it is also the least forgiving one, which is a feature when you are hunting a setup-time problem.
A START is SDA falling while SCL is high. A STOP is SDA rising while SCL is high. (§3.1.4.) These are the only two SDA transitions permitted during an SCL high period, which is exactly why they can carry meaning.
A byte is eight bits MSB first, then a ninth clock. The ninth clock is the acknowledge slot, in which the receiver drives. The direction of that drive reverses between a write and a read, which is why §3 spends time on it.
And the rule that is not obvious: a bit sampled at a rising edge is provisional until that high period ends.
3. The Provisional-Bit Rule
Here is the shape of a STOP, phase by phase:
The second rising edge is not a bit
8 cyclesCount the rising edges in that figure. There are two. The first is the ninth clock and carries the acknowledge. The second exists only to create a high period in which the SDA release is legal — a STOP is defined as an SDA rise while SCL is high, and SCL has to get high somehow.
A decoder that commits a bit at every rising edge counts that second one. Then SDA rises, framing is detected, and there is one bit sitting in the shift register. The decoder duly reports a byte truncated after one bit.
The symptom is unmistakable once you have seen it and baffling before:
Every STOP in the capture appears as a one-bit truncated byte.
Every repeated START appears as a one-bit truncated byte.
So a completely healthy transfer decodes as malformed at exactly the point
where it ends -- which is the point a reader is least likely to be looking,
because the interesting part is assumed to be earlier.The fix is to defer the commit to the falling edge. A bit becomes real when its high period ends without a framing condition having occurred in it. That costs one register, and it makes framing and data mutually exclusive by construction — which is what the specification already says they are.
4. The Decoder
// -----------------------------------------------------------------------------
// i2c_capture_decode.sv
// A streaming I2C capture decoder: two sampled wires in, a sequence of protocol
// EVENTS out. Chapter 23.2's instrument.
//
// WHAT IT IS AND IS NOT. This is not the monitor of Chapter 20.7. That monitor
// reconstructs transactions in order to CHECK a design under test, and it belongs
// to a verification environment that knows what the design was asked to do. This
// block knows nothing about any design, any register map or any intent. It turns
// samples into events, and stops.
//
// The separation is the chapter's subject. An event stream is an OBSERVATION: a
// START occurred, then a byte 0xA0 acknowledged, then a byte 0x00 not
// acknowledged, then a STOP. Whether that is RIGHT is a question about intent, and
// intent lives somewhere else. Merging the two is how a capture gets read as "the
// target NACKed the register address" when what was observed is "the second byte
// was not acknowledged".
//
// THE DECODE RULES, all from UM10204:
//
// sample point SDA is read on the RISING edge of SCL. The data-valid rule
// (§3.1.3) requires SDA to be stable for the whole SCL high time,
// so any instant in it reads the same bit; the rising edge is
// simply the first one.
//
// START SDA falls while SCL is HIGH (§3.1.4)
// STOP SDA rises while SCL is HIGH (§3.1.4)
// repeated START a START while a transfer is in progress (§3.1.10)
//
// byte 8 bits MSB first, then a ninth clock in which the receiver
// drives the acknowledge.
//
// ── THE RULE THAT IS NOT OBVIOUS, AND THE BUG IT PREVENTS ───────────────────
//
// A bit sampled at the rising edge of SCL is PROVISIONAL until that high period
// ends. It becomes a data bit only when SCL falls with no framing condition having
// occurred in between.
//
// The reason is in the shape of a STOP. After the last bit's SCL falls, the master
// pulls SDA low, RAISES SCL, and only then releases SDA. That SCL rise is a real
// rising edge on a real bus and it carries no data -- it exists to create the high
// period in which the SDA release is legal. A decoder that commits a bit at every
// SCL rise counts it, and then sees the SDA release and reports a byte truncated
// after one bit. The same thing happens at every repeated START.
//
// The symptom is unmistakable once you have seen it and baffling before: EVERY
// STOP in the capture appears as a one-bit truncated byte, so a perfectly healthy
// transfer decodes as malformed at exactly the point it ends. The first version of
// this block had that bug, and its testbench caught it as ten failures across five
// tests with a single cause.
//
// Deferring the commit to the falling edge costs one register and makes framing
// and data mutually exclusive by construction, which is what the specification
// already says they are.
//
// WHAT THE ACK BIT DOES NOT SAY. `ev_ack` is the level sampled in the ninth clock.
// It says the line was pulled low, or was not. It does NOT say which device pulled
// it -- a wire cannot say that, and on a 10-bit address prefix or a general call
// several devices may be pulling at once (Chapters 6.2 and 15.1). Attribution is
// an interpretation; the decoder reports the observation.
//
// TRUNCATION IS AN EVENT, NOT AN ERROR. When framing arrives part-way through a
// byte the decoder emits EV_TRUNC carrying how many bits had been COMMITTED, and
// then emits the framing event itself on the following cycle. Discarding the
// partial byte would erase the most diagnostic feature of a malformed capture:
// where it stopped being well formed.
//
// SYNCHRONISED INPUTS ASSUMED. `scl` and `sda` must already have crossed into this
// clock domain (Chapter 19.4). Feeding raw pins here samples metastable values, and
// no amount of decoding logic repairs that.
//
// MINIMUM OVERSAMPLING. At least two samples per bus phase, so that every edge is
// seen and the one-deep pending-event slot is always drained before the next
// framing condition. The bench runs the same capture at four and at nine.
// -----------------------------------------------------------------------------
module i2c_capture_decode (
input wire clk,
input wire rst_n,
// The captured bus, already synchronised to clk.
input wire scl,
input wire sda,
// One strobe per decoded event.
output reg ev_valid,
output reg [2:0] ev_kind,
output reg [7:0] ev_data, // EV_BYTE: the byte. EV_TRUNC: the partial, right-aligned.
output reg ev_ack, // EV_BYTE: the level sampled in the ninth clock.
output reg [3:0] ev_bits, // EV_TRUNC: how many bits had been committed.
// Running state, exposed so a reader can place an event without replaying the
// whole stream.
output reg in_transfer,
output reg [15:0] ev_index
);
localparam [2:0] EV_NONE = 3'd0,
EV_START = 3'd1,
EV_RSTART = 3'd2,
EV_STOP = 3'd3,
EV_BYTE = 3'd4,
EV_TRUNC = 3'd5;
reg scl_q, sda_q;
wire scl_rise = ~scl_q & scl;
wire scl_fall = scl_q & ~scl;
wire sda_fall = sda_q & ~sda;
wire sda_rise = ~sda_q & sda;
// Framing is decided on an SDA edge with SCL high in BOTH the previous and the
// current sample. Testing only the current `scl` would classify the SDA change
// that accompanies a rising SCL as framing, which is the most common way a
// hand-written decoder manufactures phantom STARTs.
wire scl_high_stable = scl_q & scl;
reg [7:0] shreg;
reg [3:0] bitcnt; // committed bits, 0..8; 8 is the acknowledge slot
reg pend_valid; // a provisional bit is held
reg pend_bit;
// One-deep queue for the framing event that follows a truncation report.
reg [2:0] queued_kind;
reg queued_valid;
always @(posedge clk) begin
if (!rst_n) begin
scl_q <= 1'b1;
sda_q <= 1'b1;
ev_valid <= 1'b0;
ev_kind <= EV_NONE;
ev_data <= 8'h00;
ev_ack <= 1'b1;
ev_bits <= 4'd0;
in_transfer <= 1'b0;
ev_index <= 16'd0;
shreg <= 8'h00;
bitcnt <= 4'd0;
pend_valid <= 1'b0;
pend_bit <= 1'b0;
queued_kind <= EV_NONE;
queued_valid <= 1'b0;
end else begin
scl_q <= scl;
sda_q <= sda;
ev_valid <= 1'b0;
ev_kind <= EV_NONE;
if (queued_valid) begin
// Drain the framing event that a truncation deferred.
ev_valid <= 1'b1;
ev_kind <= queued_kind;
ev_index <= ev_index + 16'd1;
queued_valid <= 1'b0;
end else if (scl_high_stable && (sda_fall || sda_rise)) begin
// Framing. The provisional bit belongs to this high period and is
// therefore NOT a data bit -- discard it. `bitcnt` already holds only
// committed bits, so the truncation count needs no adjustment.
pend_valid <= 1'b0;
if (in_transfer && bitcnt != 4'd0) begin
ev_valid <= 1'b1;
ev_kind <= EV_TRUNC;
ev_data <= shreg;
ev_bits <= bitcnt;
ev_index <= ev_index + 16'd1;
queued_kind <= sda_fall ? EV_RSTART : EV_STOP;
queued_valid <= 1'b1;
end else begin
ev_valid <= 1'b1;
ev_kind <= sda_fall ? (in_transfer ? EV_RSTART : EV_START) : EV_STOP;
ev_index <= ev_index + 16'd1;
end
in_transfer <= sda_fall; // a START opens a transfer, a STOP closes it
bitcnt <= 4'd0;
shreg <= 8'h00;
end else if (scl_rise && in_transfer) begin
// Provisional only. Nothing is committed and no event is emitted.
pend_valid <= 1'b1;
pend_bit <= sda;
end else if (scl_fall && in_transfer && pend_valid) begin
// The high period ended without framing, so the bit is real.
pend_valid <= 1'b0;
if (bitcnt == 4'd8) begin
ev_valid <= 1'b1;
ev_kind <= EV_BYTE;
ev_data <= shreg;
ev_ack <= ~pend_bit; // LOW in the ninth clock == acknowledged
ev_index <= ev_index + 16'd1;
bitcnt <= 4'd0;
shreg <= 8'h00;
end else begin
shreg <= {shreg[6:0], pend_bit}; // MSB first
bitcnt <= bitcnt + 4'd1;
end
end
end
end
endmoduleThree things it deliberately does not do, and each is a pass-2 question:
It does not say who acknowledged. ev_ack is the level sampled in the ninth clock: the line was pulled low, or it was not. Attribution needs to know which device was addressed and in which direction, and on a 10-bit address prefix (Chapter 6.2) or a general call (15.1) several devices may be pulling at once. The wire cannot count them and neither can the decoder.
It does not label the address byte. "The first byte after a START is the address" is true and it is an interpretation, one that stops being simple the moment a 10-bit address or a High-speed master code (14.2) is involved. The decoder emits bytes; you decide which one is the address.
It does not decide whether anything is wrong. A truncated byte is reported as an event, not an error, because on a capture of a multi-master bus a truncated byte is the expected result of arbitration loss (13.4).
5. The Bench, and How Ten Failures Had One Cause
// -----------------------------------------------------------------------------
// i2c_capture_decode_tb.sv
// The oracle for the decoder, and the chapter's second tool: a FIRST-DIVERGENCE
// diff between what was observed and what was intended.
//
// The bench has two halves and they do different jobs.
//
// THE GENERATOR drives scl and sda at sample-clock granularity to produce real
// I2C waveforms -- START, bits, the acknowledge slot, repeated START, STOP --
// and also the malformed ones a capture from a broken board actually contains.
// It is a waveform generator, not a protocol model: it will happily emit a STOP
// in the middle of a byte, because that is the interesting case.
//
// THE DIFF compares the decoded event list against an INTENDED event list and
// reports the first index at which they differ, and in what respect. This is the
// thing you actually do with a capture, made mechanical. "Where does this stop
// matching what I asked for" is a far better first question than "what is wrong
// with this capture", because it has an answer that is a number.
//
// Bounded: every wait is a fixed count of sample ticks. Nothing waits on a DUT
// condition, and a watchdog terminates the run regardless.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_capture_decode_tb;
// Sample ticks per bus phase. 4 is enough to separate every edge the decoder
// looks at while keeping the runs short; T7 re-runs the golden capture at 9 to
// show the decode does not depend on the oversampling ratio.
integer TICKS = 4;
localparam [2:0] EV_NONE = 3'd0, EV_START = 3'd1, EV_RSTART = 3'd2,
EV_STOP = 3'd3, EV_BYTE = 3'd4, EV_TRUNC = 3'd5;
reg clk = 1'b0;
reg rst_n = 1'b0;
reg scl = 1'b1, sda = 1'b1;
wire ev_valid, ev_ack, in_transfer;
wire [2:0] ev_kind;
wire [7:0] ev_data;
wire [3:0] ev_bits;
wire [15:0] ev_index;
integer errors = 0, checks = 0, neg_detected = 0, negative_mode = 0;
// An INVARIANT, checked continuously rather than asserted in a comment: a
// provisional bit can only exist inside a transfer. The decoder sets
// `pend_valid` only under `scl_rise && in_transfer`, and the framing branch that
// clears `in_transfer` also clears `pend_valid` — so the two can never disagree.
// That invariant is what makes the `in_transfer` term in the commit branch
// redundant, and a mutation removing that term is equivalent BECAUSE of it.
// Checking the invariant is what turns "equivalent" from an argument into a
// measurement: if it ever failed, the mutation would stop being equivalent.
integer invariant_violations = 0;
integer invariant_samples = 0;
always @(posedge clk) if (rst_n) begin
invariant_samples = invariant_samples + 1;
if (dut.pend_valid && !dut.in_transfer)
invariant_violations = invariant_violations + 1;
end
always #5 clk = ~clk;
i2c_capture_decode dut (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.ev_valid(ev_valid), .ev_kind(ev_kind), .ev_data(ev_data),
.ev_ack(ev_ack), .ev_bits(ev_bits),
.in_transfer(in_transfer), .ev_index(ev_index)
);
// ------------------------------------------------------------------ recorder
localparam MAXEV = 64;
reg [2:0] obs_kind [0:MAXEV-1];
reg [7:0] obs_data [0:MAXEV-1];
reg obs_ack [0:MAXEV-1];
reg [3:0] obs_bits [0:MAXEV-1];
integer obs_n = 0;
always @(posedge clk) if (rst_n && ev_valid && obs_n < MAXEV) begin
obs_kind[obs_n] <= ev_kind;
obs_data[obs_n] <= ev_data;
obs_ack [obs_n] <= ev_ack;
obs_bits[obs_n] <= ev_bits;
obs_n <= obs_n + 1;
end
// ------------------------------------------------------------------ intended
reg [2:0] exp_kind [0:MAXEV-1];
reg [7:0] exp_data [0:MAXEV-1];
reg exp_ack [0:MAXEV-1];
integer exp_n = 0;
task exp_clear; begin exp_n = 0; end endtask
task exp_add (input [2:0] k, input [7:0] d, input a);
begin exp_kind[exp_n] = k; exp_data[exp_n] = d; exp_ack[exp_n] = a; exp_n = exp_n + 1; end
endtask
// ------------------------------------------------------------- the diff tool
// Classes, and the order matters: a framing difference is reported in
// preference to a data difference at the same index, because once the framing
// diverges the byte boundaries after it are not comparable at all.
localparam integer D_MATCH = 0, D_FRAMING = 1, D_DATA = 2, D_ACK = 3,
D_EXTRA = 4, D_MISSING = 5;
integer div_index, div_class;
task first_divergence;
integer i;
begin
div_index = -1; div_class = D_MATCH;
for (i = 0; i < (obs_n < exp_n ? obs_n : exp_n); i = i + 1) begin
if (div_index == -1) begin
if (obs_kind[i] !== exp_kind[i]) begin
div_index = i; div_class = D_FRAMING;
end else if (obs_kind[i] == EV_BYTE && obs_data[i] !== exp_data[i]) begin
div_index = i; div_class = D_DATA;
end else if (obs_kind[i] == EV_BYTE && obs_ack[i] !== exp_ack[i]) begin
div_index = i; div_class = D_ACK;
end
end
end
// A length difference is only reported when the common prefix matched;
// otherwise the earlier divergence is the one worth having.
if (div_index == -1 && obs_n != exp_n) begin
div_index = (obs_n < exp_n) ? obs_n : exp_n;
div_class = (obs_n > exp_n) ? D_EXTRA : D_MISSING;
end
end
endtask
// ------------------------------------------------------------- bus generator
task tick (input integer n); integer i;
begin for (i = 0; i < n; i = i + 1) @(posedge clk); end
endtask
task bus_idle (input integer n);
begin scl = 1'b1; sda = 1'b1; tick(n); end
endtask
// SDA falls while SCL is high, then SCL goes low.
task gen_start;
begin scl = 1'b1; sda = 1'b1; tick(TICKS);
sda = 1'b0; tick(TICKS);
scl = 1'b0; tick(TICKS); end
endtask
// From SCL low: release SDA, raise SCL, drop SDA while SCL is high, drop SCL.
task gen_rstart;
begin scl = 1'b0; sda = 1'b1; tick(TICKS);
scl = 1'b1; tick(TICKS);
sda = 1'b0; tick(TICKS);
scl = 1'b0; tick(TICKS); end
endtask
// From SCL low: drive SDA low, raise SCL, release SDA while SCL is high.
task gen_stop;
begin scl = 1'b0; sda = 1'b0; tick(TICKS);
scl = 1'b1; tick(TICKS);
sda = 1'b1; tick(TICKS); end
endtask
// One bit: place it while SCL is low, pulse SCL high, return SCL low.
task gen_bit (input b);
begin scl = 1'b0; sda = b; tick(TICKS);
scl = 1'b1; tick(TICKS);
scl = 1'b0; tick(TICKS); end
endtask
// A byte plus its acknowledge slot. `ack` is what the RECEIVER does: 1 means
// the line is pulled low in the ninth clock.
task gen_byte (input [7:0] d, input ack); integer i;
begin for (i = 7; i >= 0; i = i - 1) gen_bit(d[i]);
gen_bit(~ack); end
endtask
// A bit whose SDA change coincides EXACTLY with the rising edge of SCL, i.e.
// with no setup time at all at this sample resolution. A real controller would
// be violating tSU;DAT, but a capture of a marginal board contains exactly this,
// and a decoder has to be defined at the boundary rather than only in the middle
// of a comfortable high period. It is also the only stimulus that can tell
// "sample SDA now" apart from "sample SDA as it was one tick ago", and the only
// one that can produce the phantom START a decoder gets if it tests framing on
// the current SCL sample alone.
task gen_bit_tight (input b);
begin scl = 1'b0; tick(TICKS);
scl = 1'b1; sda = b; tick(TICKS); // both change on the same tick
scl = 1'b0; tick(TICKS); end
endtask
task gen_byte_tight (input [7:0] d, input ack); integer i;
begin for (i = 7; i >= 0; i = i - 1) gen_bit_tight(d[i]);
gen_bit_tight(~ack); end
endtask
// A byte cut short: n bits, then nothing. Used to build malformed captures.
task gen_partial (input [7:0] d, input integer n); integer i;
begin for (i = 0; i < n; i = i + 1) gen_bit(d[7-i]); end
endtask
// ------------------------------------------------------------------- checking
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; scl = 1'b1; sda = 1'b1; tick(3); rst_n = 1'b1; obs_n = 0; tick(2); end
endtask
task dump; integer i;
begin
for (i = 0; i < obs_n; i = i + 1) begin
case (obs_kind[i])
EV_START : $display(" [%0d] START", i);
EV_RSTART: $display(" [%0d] repeated START", i);
EV_STOP : $display(" [%0d] STOP", i);
EV_BYTE : $display(" [%0d] byte 0x%02h %0s", i, obs_data[i],
obs_ack[i] ? "ACK" : "NACK");
EV_TRUNC : $display(" [%0d] TRUNCATED after %0d bits, partial 0x%02h",
i, obs_bits[i], obs_data[i]);
default : $display(" [%0d] ??", i);
endcase
end
end
endtask
integer i;
initial begin
$display("i2c_capture_decode_tb");
// ---------------------------------------------------------------------
// T1 -- the golden capture. A write of 0x5A to register 0x10 of the device
// at 7-bit address 0x48, which on the wire is:
// START, 0x90 ACK, 0x10 ACK, 0x5A ACK, STOP
// 0x90 is 0x48 shifted left with the R/W bit clear.
// ---------------------------------------------------------------------
$display("T1 golden capture -- a three-byte write");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_byte(8'h10, 1'b1); gen_byte(8'h5A, 1'b1); gen_stop;
bus_idle(TICKS);
dump;
chk("T1 event count", obs_n, 5);
chk("T1 [0] is START", obs_kind[0], EV_START);
chk("T1 [1] is a byte", obs_kind[1], EV_BYTE);
chk("T1 [1] value 0x90", obs_data[1], 8'h90);
chk("T1 [1] acknowledged", obs_ack[1], 1);
chk("T1 [2] value 0x10", obs_data[2], 8'h10);
chk("T1 [3] value 0x5A", obs_data[3], 8'h5A);
chk("T1 [4] is STOP", obs_kind[4], EV_STOP);
chk("T1 ev_index counts the events", ev_index, obs_n);
// ---------------------------------------------------------------------
// T2 -- a combined transfer, which is where repeated START must not be
// decoded as STOP-then-START. Write the pointer, repeated START, read back.
// ---------------------------------------------------------------------
$display("T2 combined transfer with a repeated START");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_byte(8'h10, 1'b1);
gen_rstart; gen_byte(8'h91, 1'b1); gen_byte(8'h5A, 1'b0); // master NACKs the last byte
gen_stop; bus_idle(TICKS);
dump;
chk("T2 event count", obs_n, 7);
chk("T2 [3] is a repeated START", obs_kind[3], EV_RSTART);
chk("T2 [3] is NOT a plain START", (obs_kind[3] == EV_START) ? 1 : 0, 0);
chk("T2 [4] read address 0x91", obs_data[4], 8'h91);
chk("T2 [5] final byte NACKed", obs_ack[5], 0);
chk("T2 no STOP before the read", (obs_kind[3] == EV_STOP) ? 1 : 0, 0);
// ---------------------------------------------------------------------
// T3 -- truncation. A STOP arrives after five bits of the second byte. The
// decoder must report WHERE, because that count is the diagnostic.
// ---------------------------------------------------------------------
$display("T3 a byte cut short by a STOP");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_partial(8'hA8, 5); gen_stop;
bus_idle(TICKS);
dump;
// FOUR events, not three. The truncation and the STOP that caused it are
// separate facts about the capture and are reported separately. An earlier
// version of this bench expected three, because an earlier version of the
// decoder folded them together -- the expectation had the defect written
// into it, which is the failure mode Chapter 23.1 section 11 is about.
chk("T3 event count", obs_n, 4);
chk("T3 [2] is TRUNCATED", obs_kind[2], EV_TRUNC);
chk("T3 truncated after 5 bits", obs_bits[2], 5);
chk("T3 partial value is 0x15", obs_data[2], 8'h15); // 10101 right-aligned
chk("T3 [3] is the STOP that truncated it", obs_kind[3], EV_STOP);
// ---------------------------------------------------------------------
// T4 -- the first-divergence diff, on a capture that is legal I2C and is
// NOT what was asked for. Intent was a write of 0x5A to register 0x10; the
// capture shows register 0x11. Everything before that matches.
// ---------------------------------------------------------------------
$display("T4 first divergence -- a legal capture that is the wrong transfer");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_byte(8'h11, 1'b1); gen_byte(8'h5A, 1'b1); gen_stop;
bus_idle(TICKS);
exp_clear;
exp_add(EV_START, 8'h00, 1'b0);
exp_add(EV_BYTE, 8'h90, 1'b1);
exp_add(EV_BYTE, 8'h10, 1'b1);
exp_add(EV_BYTE, 8'h5A, 1'b1);
exp_add(EV_STOP, 8'h00, 1'b0);
first_divergence;
$display(" first divergence: index %0d, class %0d", div_index, div_class);
chk("T4 divergence index is 2", div_index, 2);
chk("T4 divergence class is DATA", div_class, D_DATA);
chk("T4 the address byte matched", (obs_data[1] === 8'h90) ? 1 : 0, 1);
// ---------------------------------------------------------------------
// T5 -- the diff must prefer the EARLIER divergence, and must prefer a
// framing difference to a data difference at the same index. Here the
// capture NACKs the address, so the transfer ends early: the intended list
// has five events and the observed has three, and the first difference is
// an acknowledge at index 1, not the length at index 3.
// ---------------------------------------------------------------------
$display("T5 the diff reports the earliest difference, not the loudest");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b0); gen_stop; // address NACKed, transfer abandoned
bus_idle(TICKS);
exp_clear;
exp_add(EV_START, 8'h00, 1'b0);
exp_add(EV_BYTE, 8'h90, 1'b1);
exp_add(EV_BYTE, 8'h10, 1'b1);
exp_add(EV_BYTE, 8'h5A, 1'b1);
exp_add(EV_STOP, 8'h00, 1'b0);
first_divergence;
$display(" observed %0d events, intended %0d; first divergence: index %0d class %0d",
obs_n, exp_n, div_index, div_class);
chk("T5 divergence index is 1", div_index, 1);
chk("T5 divergence class is ACK", div_class, D_ACK);
chk("T5 length difference NOT reported", (div_class == D_MISSING) ? 1 : 0, 0);
// and the length case in isolation, so D_MISSING is reachable and tested
exp_clear;
exp_add(EV_START, 8'h00, 1'b0);
exp_add(EV_BYTE, 8'h90, 1'b0);
exp_add(EV_STOP, 8'h00, 1'b0);
exp_add(EV_START, 8'h00, 1'b0);
first_divergence;
chk("T5 length-only difference is MISSING", div_class, D_MISSING);
chk("T5 length-only index is 3", div_index, 3);
// ---------------------------------------------------------------------
// T6 -- a repeated START arriving mid-byte. Two facts, two events, in the
// order they happened: the byte was truncated after three bits, and what
// truncated it was a repeated START. Decoding must then resume cleanly,
// because a repeated START is a legal thing for a controller to do and the
// read that follows it is the part you are usually trying to look at.
// ---------------------------------------------------------------------
$display("T6 a repeated START arriving mid-byte");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_partial(8'hFF, 3);
gen_rstart; gen_byte(8'h91, 1'b1); gen_stop;
bus_idle(TICKS);
dump;
chk("T6 event count", obs_n, 6);
chk("T6 [2] is TRUNCATED", obs_kind[2], EV_TRUNC);
chk("T6 truncated after 3 bits", obs_bits[2], 3);
chk("T6 partial value is 0x07", obs_data[2], 8'h07);
chk("T6 [3] is the repeated START that truncated it", obs_kind[3], EV_RSTART);
chk("T6 decoding resumes correctly", obs_data[4], 8'h91);
chk("T6 [5] is STOP", obs_kind[5], EV_STOP);
chk("T6 ev_index counts the events", ev_index, obs_n);
// ---------------------------------------------------------------------
// T7 -- the decode must not depend on the oversampling ratio. The golden
// capture again, at a different number of samples per phase. A decoder
// whose bit alignment is a function of the sample rate would pass T1 and
// fail here, and on a real capture that dependency appears as a decode that
// works with one analyser and not another.
// ---------------------------------------------------------------------
$display("T7 the same capture at a different oversampling ratio");
TICKS = 9;
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_byte(8'h10, 1'b1); gen_byte(8'h5A, 1'b1); gen_stop;
bus_idle(TICKS);
chk("T7 event count unchanged", obs_n, 5);
chk("T7 [1] value 0x90", obs_data[1], 8'h90);
chk("T7 [2] value 0x10", obs_data[2], 8'h10);
chk("T7 [3] value 0x5A", obs_data[3], 8'h5A);
chk("T7 [4] is STOP", obs_kind[4], EV_STOP);
TICKS = 4;
// ---------------------------------------------------------------------
// T9 -- the bus between transfers. After a STOP, a captured bus is not
// silent: the controller may clock, another segment may be active, or the
// analyser may simply still be recording. None of it is part of a transfer,
// and the decoder must emit NOTHING until the next START.
//
// This is the stimulus the first version of this bench did not have, and two
// mutations survived because of it: one that left `in_transfer` asserted
// after a STOP, and one that committed bits whether or not a transfer was
// open. Both are silent on a bench that resets after every STOP.
// ---------------------------------------------------------------------
$display("T9 clock activity between transfers must decode to nothing");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_stop;
i = obs_n;
chk("T9 the first transfer decoded", i, 3);
// Eight clock pulses with SDA toggling, with no START anywhere.
gen_bit(1'b1); gen_bit(1'b0); gen_bit(1'b1); gen_bit(1'b1);
gen_bit(1'b0); gen_bit(1'b0); gen_bit(1'b1); gen_bit(1'b0);
bus_idle(TICKS);
chk("T9 no events from inter-transfer clocking", obs_n, i);
// And a fresh transfer after it still decodes.
gen_start; gen_byte(8'h91, 1'b1); gen_stop;
bus_idle(TICKS);
dump;
chk("T9 the second transfer decoded", obs_n, i + 3);
chk("T9 second START is a plain START", obs_kind[i], EV_START);
chk("T9 second address byte", obs_data[i + 1], 8'h91);
chk("T9 ev_index counts the events", ev_index, obs_n);
// ---------------------------------------------------------------------
// T10 -- a capture with no setup time. Every SDA change lands on the same
// sample tick as the rising edge of SCL. Two properties are under test and
// neither is reachable from a comfortably spaced capture:
//
// the bit sampled is the one PRESENT at the rising edge, not the one that
// was there a tick earlier while SCL was still low
//
// an SDA change that arrives WITH a rising SCL is not framing, because
// framing requires SCL to have been high already
// ---------------------------------------------------------------------
$display("T10 a capture with no setup time -- tight edges");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte_tight(8'hA5, 1'b1); gen_byte_tight(8'h3C, 1'b0); gen_stop;
bus_idle(TICKS);
dump;
chk("T10 event count", obs_n, 4);
chk("T10 [0] is START and not a phantom", obs_kind[0], EV_START);
chk("T10 [1] value 0xA5", obs_data[1], 8'hA5);
chk("T10 [1] acknowledged", obs_ack[1], 1);
chk("T10 [2] value 0x3C", obs_data[2], 8'h3C);
chk("T10 [2] NACKed", obs_ack[2], 0);
chk("T10 [3] is STOP", obs_kind[3], EV_STOP);
chk("T10 ev_index counts the events", ev_index, obs_n);
// ---------------------------------------------------------------------
// T8 -- negative proof. Five deliberately wrong expectations, one per check
// shape used above, all of which must be detected.
// ---------------------------------------------------------------------
$display("T8 negative proof -- six deliberately wrong expectations");
do_reset;
bus_idle(TICKS);
gen_start; gen_byte(8'h90, 1'b1); gen_stop;
bus_idle(TICKS);
exp_clear;
exp_add(EV_START, 8'h00, 1'b0);
exp_add(EV_BYTE, 8'h90, 1'b1);
exp_add(EV_STOP, 8'h00, 1'b0);
first_divergence;
negative_mode = 1;
chk("T8a wrong event count", obs_n, 9);
chk("T8b wrong event kind", obs_kind[0], EV_STOP);
chk("T8c wrong byte value", obs_data[1], 8'h91);
chk("T8d wrong ack", obs_ack[1], 0);
chk("T8e a divergence where there is none", div_index, 1);
chk("T8f a truncation count that is not there", obs_bits[1], 5);
negative_mode = 0;
chk("T8 all six negatives detected", neg_detected, 6);
chk("T8 the capture really does match intent", div_index, -1);
chk("invariant pend_valid implies in_transfer held", invariant_violations, 0);
chk("the invariant was actually sampled", (invariant_samples > 1000) ? 1 : 0, 1);
$display(" invariant sampled on %0d clocks, violations %0d",
invariant_samples, invariant_violations);
$display("");
$display("checks=%0d errors=%0d negatives_detected=%0d/6", checks, errors, neg_detected);
if (errors == 0) $display("RESULT: PASS"); else $display("RESULT: FAIL");
$finish;
end
initial begin
#4000000;
$display("RESULT: FAIL -- watchdog expired, the run did not terminate");
$finish;
end
endmoduleThe bench has two halves doing different jobs. The generator drives scl and sda at sample granularity to produce real I²C waveforms, including the malformed ones a capture of a broken board actually contains — it will happily emit a STOP in the middle of a byte, because that is the interesting case. The diff compares the decoded event list against an intended one, which is §6.
Reading the first run
The first version of the decoder produced this:
T1 golden capture -- a three-byte write
[0] START
[1] byte 0x90 ACK
[2] byte 0x10 ACK
[3] byte 0x5a ACK
[4] TRUNCATED after 1 bits, partial 0x00
FAIL T1 [4] is STOP
T2 combined transfer with a repeated START
[3] TRUNCATED after 1 bits, partial 0x01
FAIL T2 [3] is a repeated START
T3 a byte cut short by a STOP
FAIL T3 truncated after 5 bits: got 6 expected 5
FAIL T3 partial value is 0x15: got 42 expected 21
...
checks=43 errors=10Work the evidence rather than the failures. Three observations, in this order:
The first three bytes of T1 decoded perfectly. 0x90, 0x10, 0x5A, all acknowledged, all correct. Whatever is wrong is not in bit sampling, byte assembly, MSB ordering or acknowledge decoding — a fault in any of those could not produce three correct bytes in a row.
Every failure is at a framing boundary. T1 fails at the STOP. T2 fails at the repeated START. T3's count is one too high and its partial value is 0x2A instead of 0x15 — which is 0x15 shifted left by one, i.e. one extra bit. Five separate tests, and in each one the anomaly sits exactly where a byte meets a framing condition.
The one extra bit is the same bit every time. 0x2A is 0x15 with one more bit shifted in. A STOP after a complete byte gives "truncated after 1 bit". A repeated START after a complete byte gives "truncated after 1 bit". The count is always exactly one more than it should be, never two.
Three observations, one conclusion, and no guessing: something is contributing exactly one extra bit at every framing boundary. That is not five bugs, and it is not "the framing detector is broken" — a broken framing detector would miss the framing, not add a bit before it. It is one bit, arriving from the SCL rise that framing needs in order to happen.
The expectations had the defect written into them
Fixing the decoder left four failures, and they were mine:
FAIL T3 event count: got 4 expected 3
FAIL T6 no separate RSTART event: got 1 expected 0
FAIL T6 decoding resumes correctly: got 7 expected 145
FAIL T6 [4] is STOP: got 4 expected 3T3 now reports four events — a START, a byte, a truncation, and the STOP that caused the truncation — where the bench expected three. T6 expected no separate repeated-START event, because the old decoder folded the framing into the truncation, and the bench had a comment describing that as "the decoder's stated limitation, tested rather than only claimed".
It was not a limitation. It was the bug, and the bench had written it down as a specification.
That is a failure mode worth naming, because it is common and it is invisible from inside: an expectation written after observing the behaviour records the behaviour, not the requirement. The comment even sounded like good practice — testing a limitation rather than merely claiming it is normally exactly right. What made it wrong here is that the limitation was never independently justified; it was reverse-engineered from a run.
The defence is the one Chapter 19.7 §3 arrives at from a different direction: expectations are worth most when they are written against the specification, before the run. Every expectation in T1, T2 and T3 was, and that is why they caught the bug. The two in T6 were not, and they hid it.
6. The First-Divergence Diff
Most captures from a broken system are valid I²C. The decoder will happily confirm that, and it does not help.
What helps is comparing the event list against what was intended and reporting the first index where they part company. T4 is the canonical case: the intent was a write of 0x5A to register 0x10, and the capture is a write of 0x5A to register 0x11.
observed intended
[0] START [0] START
[1] byte 0x90 ACK [1] byte 0x90 ACK
[2] byte 0x11 ACK [2] byte 0x10 ACK <-- first divergence
[3] byte 0x5A ACK [3] byte 0x5A ACK
[4] STOP [4] STOP
first divergence: index 2, class DATAEverything about that capture is legal. Framing is correct, every byte is acknowledged, the transfer is well formed, and an analyser will decode it without a complaint. The diff says, in one number, that the address byte was right and the register pointer was not — which localises the fault to whatever produced the register index, and rules out addressing, framing and the target's acknowledge behaviour in the same breath.
The order of the classes is the design
Two details in first_divergence matter more than they look.
Framing outranks data at the same index. Once the framing has diverged, the byte boundaries after it are not comparable — comparing byte values across a framing difference produces a list of differences that are all consequences of the first one.
A length difference is only reported when the common prefix matched. T5 is the test: the capture NACKs the address and abandons the transfer, so three events were observed where five were intended. Two differences exist — an acknowledge at index 1 and a length at index 3 — and only the first is worth having, because the length difference is a consequence of the NACK.
observed 3 events, intended 5
first divergence: index 1, class ACK
The length difference at index 3 is real and is not reported, because the
transfer ending early is what a NACKed address MEANS. Reporting it as well
would present a consequence alongside its cause with equal weight.That is the same principle as first_div_cyc in Chapter 23.1: the earliest divergence localises, and every later one is downstream of it.
7. What Mutation Found
Sixteen mutations, each written to its own path, that path hashed, and iverilog invoked on that exact path — with the harness refusing to score any mutation whose anchor did not appear exactly once in the source.
16 valid 10 KILLED 4 SURVIVED (2 further mutations failed to generate)
C02 framing tested on the current SCL sample only SURVIVED
C07 a STOP does not end the transfer SURVIVED
C13 bits are committed outside a transfer too SURVIVED
C16 SDA sampled one tick late SURVIVEDNone of those four is equivalent. All four are real behavioural changes that the bench could not see, and three of them had the same cause.
The bench only produced comfortable captures
C02 removes the "SCL was already high" term from the framing test, which is what prevents an SDA change arriving with a rising SCL from being read as a START. C16 samples SDA one tick before the rising edge instead of at it.
Neither is observable on a capture whose SDA changes sit comfortably in the middle of the SCL low period — and every capture the generator produced did, because gen_bit sets SDA, waits, then moves SCL. Real captures of marginal boards do not look like that.
T10 is the missing stimulus: a byte whose every SDA change lands on the same sample tick as the rising edge of SCL. That violates tSU;DAT and a conforming controller would not emit it — which is exactly why it belongs in a bench for a capture reader, whose entire job is making sense of traffic that a conforming controller did not emit.
C07 leaves in_transfer asserted after a STOP; C13 commits bits regardless of whether a transfer is open. Neither is visible on a bench that resets immediately after every STOP, and the original one did. T9 is the missing stimulus: eight clock pulses with SDA toggling between two transfers, which must decode to exactly nothing, followed by a fresh transfer that must still decode.
16 valid 15 KILLED 1 SURVIVED 0 failed to generateThe one survivor, and turning its equivalence into a measurement
C13 removes the in_transfer term from the commit branch. It survives because pend_valid implies in_transfer: the provisional bit is only ever set under scl_rise && in_transfer, and the framing branch that clears in_transfer also clears pend_valid. The guard is therefore redundant, and the mutation is equivalent.
But "the guard is redundant because of an invariant" is an argument, and arguments about state machines are exactly the kind of thing that is right until somebody adds a state. So the invariant is checked rather than asserted:
always @(posedge clk) if (rst_n) begin
invariant_samples = invariant_samples + 1;
if (dut.pend_valid && !dut.in_transfer)
invariant_violations = invariant_violations + 1;
end
baseline: invariant sampled on 3413 clocks, violations 0
And the check is proven LIVE, not merely silent. Mutation C11 -- which stops
the framing branch clearing the provisional bit -- produces
invariant sampled on 3413 clocks, violations 188
so a run reporting zero is reporting a measurement, not an absence of code.The guard stays in the design. It costs nothing, and it documents the invariant at the point where a future edit would break it.
8. What a Capture Cannot Settle
It cannot attribute a low level. Every pull-down on the bus looks identical, which is the wired-AND doing its job (Chapter 2.5). An acknowledge in the capture means somebody pulled SDA low in the ninth clock.
It cannot distinguish a NACK from an absent device. Both are "nobody pulled SDA low". Chapter 23.4 is that problem in full.
It cannot show you a violated setup time unless it out-samples it. An analyser at four times the bit rate cannot resolve tSU;DAT. It will show clean bits and a decode that matches, and the marginal timing that is actually failing on some units will be invisible — which is why Chapter 23.1 §3 puts the instrument's capability on the checklist before the design.
It cannot tell you anything about drive intent. Intent is inside a chip. A capture is one of the three observation points in Chapter 23.1 §4 — the one furthest from the fault.
9. Misconceptions
10. Debug Lab
The analyser decodes the transfer perfectly and the sensor still reads zero
A capture with nothing wrong in it, and the one number that localises the fault
// A temperature sensor at 7-bit address 0x48. The driver does the standard
// register read: write the pointer, repeated START, read two bytes. It has worked
// on three previous boards with the same part.
//
// On this board every read returns 0x0000. The analyser's I2C decoder shows a
// well-formed combined transfer with every byte acknowledged and no protocol
// warnings of any kind.
//
// The tempting conclusion is that the sensor is faulty, and it is a hypothesis
// rather than an observation. Others that fit the same evidence:
//
// (a) the sensor is genuinely returning zero because it has not been
// configured -- many parts power up in a shutdown mode
// (b) the register pointer written is not the one the temperature lives at
// (c) the sensor is a different part from the one the driver was written for,
// with the same address and a different register map
// (d) the part is faulty
//
// All four produce a perfectly legal capture with every byte acknowledged, which
// is why "the capture looks fine" eliminates none of them.Pass 1, the OBSERVATION. Feeding the capture through the decoder:
[0] START [1] byte 0x90 ACK <- 0x48 << 1, write [2] byte 0x00 ACK [3] repeated START [4] byte 0x91 ACK <- 0x48 << 1, read [5] byte 0x00 ACK [6] byte 0x00 NACK [7] STOP
Eight events, well formed, every byte from the controller acknowledged by the target, and the final byte NACKed by the controller -- which is correct, because the controller ends a read by declining to acknowledge the last byte (Chapter 7.4).
Nothing here is a protocol fault, and that is the point: pass 1 is CLEAN.
Pass 2, the INTERPRETATION, against the driver source. The driver is written for this part's datasheet, in which the temperature register is 0x00, and it intends:
START, 0x90 ACK, 0x00 ACK, RSTART, 0x91 ACK, <data> ACK, <data> NACK, STOP
Running the diff against that intended list:
first divergence: index -1 (no divergence)
The capture is EXACTLY the transfer the driver intended to perform. Not merely legal -- identical, byte for byte, acknowledge for acknowledge.
That single result does a large amount of work. The bus, the framing, the addressing, the direction, the repeated START, the register pointer and the controller's own acknowledge policy are all doing precisely what the driver asked. Candidate (b) is dead: the pointer written is 0x00, which is what was intended.
The diff finding NO divergence is the localising evidence, and it is worth being precise about what it establishes and what it does not.
it ESTABLISHES that the controller side is behaving as designed, end to end. Every hypothesis about the driver, the controller RTL, the bus and the framing is eliminated in one measurement.
it does NOT establish that the design is correct. "The capture matches intent" and "the intent is right" are different claims, and the diff only ever checks the first.
So the fault is in the intent, and three candidates remain: (a), (c) and (d). They are distinguished by reading a DIFFERENT register, which is one more transfer:
read the part's identification register, if it has one -> distinguishes (c) read the configuration register and compare with the reset value in the datasheet -> distinguishes (a) if both agree with the datasheet and the temperature is still zero -> supports (d)
On this board it was (a). The configuration register read back 0x0100, and the datasheet's reset value for that field is "shutdown". The part powers up not converting, and a temperature register that has never been written by a conversion reads zero. The driver had been ported from a board whose bootloader happened to bring the sensor out of shutdown, so the initialisation had never been needed.
The correction is one transfer in the driver's init: write the configuration register to enable continuous conversion, then wait the datasheet's conversion time before the first read.
PROOF, and the order matters:
1. before: the configuration register reads 0x0100 (shutdown) and every temperature read returns 0x0000 2. after the init write: the configuration register reads back the enabled value -- which proves the WRITE reached the part, separately from whether the temperature is now right 3. after one conversion interval: the temperature register returns a plausible, CHANGING value, and cooling the part moves it in the right direction
Step 2 matters because it separates two things that step 3 would confuse. If the temperature became non-zero without step 2, the fix would be confirmed only by its symptom, and a symptom can improve for reasons unrelated to the change. Step 3's "changing, and in the right direction" is there for the same reason: a stuck non-zero value is also non-zero.
11. Reason It Through
12. Questions
13. What This Chapter Settled
Four decode rules, one of which is not obvious and costs a register: a bit sampled at a rising edge is provisional until that high period ends without framing. Skipping it makes every STOP and every repeated START in a healthy capture decode as a one-bit truncated byte, and the first version of the decoder here did exactly that.
Ten failures across five tests, resolved to one cause by looking at what the failures had in common rather than at what each one said — three correct bytes, every anomaly at a framing boundary, every count off by exactly one. Then four more failures that were the bench's own expectations, written after the fact and recording the defect as a specification.
A first-divergence diff that turns "where does this stop matching what I asked for" into an index and a class, with framing outranking data at the same index and length differences suppressed until the prefix matches — both so that a consequence is never presented alongside its cause.
Sixteen mutations: fifteen killed, one equivalent under an invariant that is measured on every clock and proven able to report a violation. The four first-campaign survivors were all stimulus gaps, and closing them took two tests that a comfortable capture can never produce: edges with no setup time, and clock activity between transfers.
What this chapter has assumed throughout is that the capture is digitally meaningful — that a low is a low and an edge is an edge. On a bus with a marginal pull-up that assumption is false, and the resulting symptoms are protocol-shaped: bytes with a bit wrong, acknowledges that come and go, transfers that fail at one temperature. Chapter 23.3 is the triage decision that tells those apart, and it is the highest-value split in the module.
Continue learning
Related tutorials
- Related topic
A Systematic I²C Debug Workflow
Turns “it doesn’t work” into an ordered narrowing: separate observation from interpretation, suspect the instrument before the design, eliminate a layer, then form competing hypotheses and pick the measurement that discriminates them. Builds the instrument step four depends on — three observation points, one layer verdict — in SystemVerilog and VHDL, and reports the saturation bound a mutation proved had never been tested.
- Related topic
Protocol Bug or Electrical Bug? Splitting the Search Space
The highest-value triage decision in I²C debugging, and the evidence that settles it. Why a wrong bit, an intermittent acknowledge and a failure that only appears at 400 kHz are produced identically by a shift-register bug and by a slow edge — and how one measurement, the time from release to a readable HIGH, separates them. Builds the classifier, runs one drive pattern against two rise times, and reports the bench race that produced an off-by-one at some latencies and not others.
- 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
Missing ACK and Address Faults
Seven causes of a missing acknowledge, one diagnostic procedure of at most five probes, and a confusion matrix that shows exactly which two it cannot separate. Built on a fault-injectable target so every verdict is checked against a known cause — including a target that acknowledges correctly and a controller that samples too early to see it.
