I²C · Module 10
Repeated START in Practice — Why Not STOP Then START
Two sequences that look nearly identical in a driver's source are completely different on the wire. This chapter measures the difference, builds the passive monitor that tells them apart from two wires alone, and names exactly what a STOP costs on a shared bus.
Chapter 10.1 asserted that the junction between the two phases of a combined transaction must be a repeated START. This chapter earns that claim.
The two candidate sequences are, in a driver's source, about four characters apart:
i2c_write(dev, ptr); i2c_write(dev, ptr);
i2c_restart(); i2c_stop();
i2c_read(dev, buf, n); i2c_start();
i2c_read(dev, buf, n);Both put identical bytes on the wire in identical order. Both work perfectly on a bench with one master. And one of them is correct while the other has a failure mode that is intermittent, bus-wide, and essentially impossible to reproduce deliberately.
So the useful thing is not the advice — everyone has heard "use a repeated START". It is knowing exactly what the difference is on the wire, exactly what it costs, and how to see it in a capture.
1. The Difference Is One Framing Event
Chapter 5.1 established the rule that makes this measurable: an SDA edge while SCL is high is reserved for framing. Everything follows from it.
| event | wire signature | effect on ownership |
|---|---|---|
| START / repeated START | SDA falls while SCL is high | takes the bus, or keeps it |
| STOP | SDA rises while SCL is high | releases the bus |
A START and a repeated START are the same electrical event. There is no difference whatsoever in what the wires do — the distinction is entirely contextual:
If a transfer is already open, an SDA fall while SCL is high is a repeated START and the bus was never released. If no transfer is open, the same edge is a fresh START and the bus had been free.
That is why the monitor in §5 has to track state to classify one. It also means the two sequences differ by exactly one thing: whether a STOP appears between the phases. Not the bytes, not the addresses, not the acknowledges — one framing event.
Note 4 is the reason this works at all. Every device on the bus, on seeing an Sr, resets its bus logic and expects an address — so the repeated START unambiguously re-opens the addressing phase for everybody, with no device left believing the previous phase is still running. Chapter 9.2 §4 covered the one deliberate exception: the register pointer survives, because note 1 requires it to.
2. The Two Sequences, Side by Side
Repeated START versus STOP-then-START — the same bytes, one framing event apart
10 cyclesThe bus owned row is the whole argument. In the correct sequence it never reads N until the final STOP. In the broken one there are intervals in the middle where the answer is no — and during those intervals, any other master on the bus is not merely able to start a transfer, it is entitled to. The bus is free; that is what free means.
Three costs follow, and they are worth separating because only one of them is about correctness.
Time — real but minor. A STOP and a START have setup and hold requirements, and between them the mandatory bus-free interval tBUF must elapse (Chapter 5.5). Plus a whole extra address byte: nine pulses. At 100 kHz that is roughly 90 µs plus the framing. Measurable, rarely decisive.
Slave state — usually survivable, sometimes not. Note 4 says devices reset their bus logic on an S or Sr. Many devices also discard the register pointer on a STOP, because a STOP ends the transaction in every sense the protocol recognises. If yours does, the second phase reads from wherever the pointer defaults to, and the failure is deterministic and immediate — which, perversely, makes it the good outcome, because you find it on the first run.
The exposure window — the one that matters. Another master takes the free bus, addresses the same device, writes its own pointer, and completes. Your read phase then returns data from its location. Every byte is acknowledged, the framing is clean, the transaction reports success, and the data is plausible and wrong.
3. Reading It in a Capture
The diagnostic is structural and takes one pass. From Chapter 7.5 §2's method, on pass 1 you already scan for SDA edges while SCL is high. Classify them and count:
One logical access should produce ONE start and k repeated starts. If it produced two starts, the bus was released.
That is it. A write-then-read should show S, Sr, P — one fresh start, one repeated start, one stop. The broken version shows S, P, S, P — two fresh starts and no repeated start at all.
| what you see | what it was |
|---|---|
S … Sr … P | one combined transaction, bus held throughout |
S … P … S … P | two transactions; the bus was released in between |
S … Sr … Sr … P | a three-phase transaction, still held throughout |
S … P only | a single-phase transfer, or an abort (Chapter 10.1 §5) |
Two refinements make this reliable in practice.
Count starts, not stops. A missing repeated START is easy to overlook — it is one edge in a dense capture. Two fresh starts where you expected one is much more visible, because it means the capture contains two frames where the source contains one function call.
Measure the gap. Between the STOP and the next START, the bus is idle. That interval is the exposure window, and its length is worth knowing: a driver that releases the bus for 2 µs and one that releases it for 2 ms have the same bug and wildly different probabilities of being bitten by it. The monitor in §5 measures it.
4. What the Monitor Can and Cannot Tell You
Before the code, a limit that is genuinely important and is the reason Chapter 10.3 exists.
A monitor reads the wire. The wire records events, not intentions.
On the wire, S … P … S … P is two transactions. Not "a combined transaction that was done wrongly" — two transactions, and the first one is over in every sense the protocol recognises: the bus was released, every device reset its bus logic, and any master was free to begin. There is no field, no flag, and no timing signature that distinguishes:
- a driver that wrongly released the bus mid-access, from
- two separate accesses that happened to be adjacent.
So the monitor below reports facts: how many fresh starts, how many repeated starts, how many stops, and how long the bus was free. It does not report "you have a bug", because it cannot know. Turning those facts into a verdict requires one extra input — what the master believed it was doing — and that is the next chapter.
This is not a weakness of the design; it is a property of the bus, and it is worth internalising because it recurs. Chapter 10.1 §5 noted that an abort and a mid-transaction release produce the same wire event too. Whenever intent matters, the wire is not enough.
5. The Monitor in Three Languages
The monitor detects framing from the two wires using only the rule in §1, classifies each START by whether a transfer was already open, and times the gaps.
| output | what it reports |
|---|---|
start_det, restart_det, stop_det | framing events, classified |
in_transfer | between an S/Sr and the next P |
junction_kind | how the current addressing was reached: repeated START, or fresh |
bus_was_free | pulse: a fresh START re-took a bus that had been released |
free_cycles | the gap in progress |
last_free_cycles | the measured length of the last gap — the exposure window |
// A PASSIVE bus monitor that classifies the JUNCTION between two phases of a transfer
// and measures what a release costs. It drives nothing.
//
// The two sequences this chapter compares look almost identical in a driver's source
// and are completely different on the wire:
//
// REPEATED START S ... A [Sr] addr+R ... bus NEVER released
// STOP then START S ... A [P] ...... [S] addr+R bus released
//
// And they are distinguishable from the two wires alone, using nothing but the framing
// rule of Chapter 5.1 -- an SDA edge while SCL is HIGH is reserved for framing:
//
// SDA falling while SCL high -> a START. If a transfer is already open it is a
// REPEATED start, and the bus was never released.
// SDA rising while SCL high -> a STOP. The bus is now free, and every device
// on it is entitled to start a transfer.
//
// So "was the bus released between the phases" is not a matter of intent or of reading
// the driver's source. It is a fact on the wire, and this block reads it.
//
// The measurement that matters is free_cycles: how long the bus sat idle between a STOP
// and the next START. That interval is the EXPOSURE WINDOW -- the time during which any
// other master on the bus could legitimately have begun its own transfer and, in the
// register-pointer pattern of Chapter 10.1, changed the pointer this master had just
// written. A repeated START makes that window exactly zero, which is the whole reason
// the specification's combined format exists.
//
// ONE LIMIT IS WORTH STATING PLAINLY, because it is the bridge to Chapter 10.3.
// This block reports FACTS about the wire and cannot report INTENT. A STOP followed by
// a START is, on the wire, exactly two separate transactions -- and that is precisely
// what it IS. There is no field, flag or timing that distinguishes "a driver that
// wrongly released the bus mid-transaction" from "two transactions that happened to be
// adjacent", because after the STOP the first transaction is over in every sense the
// protocol recognises. So the diagnostic here is structural rather than semantic:
//
// one logical access should produce ONE start and k RESTARTS.
// If it produced two STARTS, the bus was released, whatever the driver intended.
//
// Correlating that with what a master believed it was doing needs a second input, and
// Chapter 10.3 supplies it.
module i2c_phase_junction_monitor #(
parameter int CNT_W = 16
)(
input logic clk,
input logic rst_n,
// The two wires, already synchronised to clk by the input stage of Module 4.
input logic sda_in,
input logic scl_in,
// ---- framing, detected from the wire ----
output logic start_det, // pulse: a START (first of a transfer)
output logic restart_det, // pulse: a REPEATED START -- bus retained
output logic stop_det, // pulse: a STOP -- bus released
output logic in_transfer, // a transfer is open: between an S/Sr and the next P
// ---- junction classification ----
// How the most recent addressing was reached: by a repeated START, with the bus
// retained, or by a fresh START after the bus had been free.
output logic [1:0] junction_kind, // 0 = none yet, 1 = repeated START, 2 = fresh
output logic bus_was_free, // pulse: a fresh START re-took a released bus
// ---- the cost of a release ----
// Cycles the bus spent idle between a STOP and the next START. In a combined
// transaction this is the window another master could have used.
output logic [CNT_W-1:0] free_cycles,
output logic [CNT_W-1:0] last_free_cycles, // the measurement of the last gap
output logic [CNT_W-1:0] n_starts,
output logic [CNT_W-1:0] n_restarts,
output logic [CNT_W-1:0] n_stops
);
localparam logic [1:0] JK_NONE = 2'd0;
localparam logic [1:0] JK_RESTART = 2'd1;
localparam logic [1:0] JK_FRESH = 2'd2;
logic sda_q, scl_q;
logic sda_fall, sda_rise, scl_stable_high;
assign sda_fall = sda_q && !sda_in;
assign sda_rise = !sda_q && sda_in;
// SCL must be high ACROSS the SDA edge, not merely high after it. Requiring both
// the previous and the current sample to be high is what stops a coincident SCL
// rise from being misread as framing -- the defect Module 4's mutation suite found
// and the reason that chapter's detector uses both terms.
assign scl_stable_high = scl_q && scl_in;
// A STOP has just released the bus, so the gap timer runs until the next START.
// "Idle" means both lines released: an undriven bus is high on both wires.
logic gap_running;
logic bus_idle;
assign bus_idle = sda_in && scl_in;
always_ff @(posedge clk) begin
if (!rst_n) begin
sda_q <= 1'b1; // an idle bus is high on both wires
scl_q <= 1'b1;
start_det <= 1'b0;
restart_det <= 1'b0;
stop_det <= 1'b0;
in_transfer <= 1'b0;
junction_kind <= JK_NONE;
bus_was_free <= 1'b0;
free_cycles <= '0;
last_free_cycles <= '0;
n_starts <= '0;
n_restarts <= '0;
n_stops <= '0;
gap_running <= 1'b0;
end else begin
sda_q <= sda_in;
scl_q <= scl_in;
start_det <= 1'b0;
restart_det <= 1'b0;
stop_det <= 1'b0;
bus_was_free <= 1'b0;
if (sda_fall && scl_stable_high) begin
// A START. Whether it is a plain one or a REPEATED one depends entirely
// on whether a transfer was already open -- there is no difference in
// the electrical event itself, which is why a monitor must track state
// to classify it.
if (in_transfer) begin
restart_det <= 1'b1;
n_restarts <= n_restarts + 1'b1;
junction_kind <= JK_RESTART;
end else begin
start_det <= 1'b1;
n_starts <= n_starts + 1'b1;
junction_kind <= JK_FRESH;
// A fresh START that follows a STOP re-takes a bus that was
// genuinely free. The gap timer was running, so its value is the
// window during which any other master could have taken it.
if (gap_running) begin
bus_was_free <= 1'b1;
last_free_cycles <= free_cycles;
end
end
in_transfer <= 1'b1;
gap_running <= 1'b0;
free_cycles <= '0;
end else if (sda_rise && scl_stable_high) begin
// A STOP. The bus is released from this instant, and the gap timer
// starts -- it is measuring the window, not waiting to see if anyone
// uses it.
stop_det <= 1'b1;
n_stops <= n_stops + 1'b1;
in_transfer <= 1'b0;
gap_running <= 1'b1;
free_cycles <= '0;
end else if (gap_running && bus_idle) begin
free_cycles <= free_cycles + 1'b1;
end
end
end
endmodule `timescale 1ns/1ps
// The testbench drives the two WIRES, not an abstract command interface, because the
// claim under test is about what the wire shows. Both sequences are built from the same
// primitives so the only difference between them is the junction.
module i2c_phase_junction_monitor_tb;
localparam int CNT_W = 16;
logic clk = 1'b0;
always #5 clk = ~clk;
logic rst_n = 1'b0;
logic sda_in = 1'b1, scl_in = 1'b1; // an idle bus is high on both wires
logic start_det, restart_det, stop_det, in_transfer, bus_was_free;
logic [1:0] junction_kind;
logic [CNT_W-1:0] free_cycles, last_free_cycles;
logic [CNT_W-1:0] n_starts, n_restarts, n_stops;
localparam logic [1:0] JK_NONE = 2'd0, JK_RESTART = 2'd1, JK_FRESH = 2'd2;
int errors = 0;
i2c_phase_junction_monitor #(.CNT_W(CNT_W)) dut (.*);
initial begin #400000; $display("FAIL: watchdog expired"); $finish; end
// ---- bus primitives, built strictly from the framing rules of Module 5 ----------
// A START: SDA falls while SCL is HIGH.
task automatic bus_start();
sda_in = 1'b1; scl_in = 1'b1; @(negedge clk); @(negedge clk);
sda_in = 1'b0; @(negedge clk); // the falling edge, SCL high
scl_in = 1'b0; @(negedge clk); // SCL drops: the transfer begins
endtask
// A STOP: SDA rises while SCL is HIGH.
task automatic bus_stop();
sda_in = 1'b0; scl_in = 1'b0; @(negedge clk);
scl_in = 1'b1; @(negedge clk); // SCL up with SDA still low
sda_in = 1'b1; @(negedge clk); // the rising edge, SCL high
endtask
// A data bit: SDA changes only while SCL is LOW, then one SCL pulse.
task automatic bus_bit(input logic v);
scl_in = 1'b0; @(negedge clk);
sda_in = v; @(negedge clk);
scl_in = 1'b1; @(negedge clk);
scl_in = 1'b0; @(negedge clk);
endtask
// One byte and its acknowledge slot -- nine bits, exactly as Chapter 7.1 requires.
task automatic bus_byte(input logic [7:0] v, input logic ninth);
for (int i = 7; i >= 0; i--) bus_bit(v[i]);
bus_bit(ninth);
endtask
// Let the bus sit IDLE for n cycles. Both wires released, which is what an
// unowned bus looks like.
task automatic bus_idle_for(input int n);
sda_in = 1'b1; scl_in = 1'b1;
repeat (n) @(negedge clk);
endtask
initial begin
repeat (3) @(negedge clk);
if (in_transfer !== 1'b0) begin $display("FAIL: in_transfer out of reset"); errors++; end
if (junction_kind !== JK_NONE) begin
$display("FAIL: junction_kind out of reset"); errors++; end
rst_n = 1'b1; @(negedge clk);
// ================================================================
// SEQUENCE 1 — the CORRECT combined transaction: repeated START.
// S addr+W A ptr A Sr addr+R A data N P
// ================================================================
bus_start();
bus_byte(8'hA0, 1'b0); // addr+W, slave ACKs (SDA low in the 9th)
bus_byte(8'h12, 1'b0); // the pointer, ACKed
bus_start(); // THE JUNCTION -- a repeated START
bus_byte(8'hA1, 1'b0); // addr+R, ACKed
bus_byte(8'h5A, 1'b1); // data, master NACKs (SDA high in the 9th)
bus_stop();
bus_idle_for(10);
// THE structural diagnostic. One logical access, ONE start: a repeated START
// does not release the bus, so there is nothing to re-take.
if (n_starts !== 16'd1) begin
$display("FAIL: a held transaction produced %0d STARTs -- one access, one START",
n_starts); errors++; end
if (n_restarts !== 16'd1) begin
$display("FAIL: counted %0d repeated STARTs, expected 1", n_restarts); errors++; end
if (n_stops !== 16'd1) begin
$display("FAIL: counted %0d STOPs, expected 1", n_stops); errors++; end
if (junction_kind !== JK_RESTART) begin
$display("FAIL: junction_kind = %0d, expected %0d (repeated START)",
junction_kind, JK_RESTART); errors++; end
// No gap to measure, and therefore no window in which another master could
// have interposed.
if (bus_was_free !== 1'b0) begin
$display("FAIL: a repeated START reported the bus as having been free");
errors++; end
if (in_transfer !== 1'b0) begin
$display("FAIL: still in a transfer after the STOP"); errors++; end
// ================================================================
// SEQUENCE 2 — the SAME ACCESS written as STOP then START.
// S addr+W A ptr A P [bus free] S addr+R A data N P
// Byte for byte identical. Only the junction differs.
// ================================================================
begin
logic [CNT_W-1:0] sr_before, s_before;
sr_before = n_restarts; s_before = n_starts;
bus_start();
bus_byte(8'hA0, 1'b0);
bus_byte(8'h12, 1'b0);
bus_stop(); // THE JUNCTION -- the bus is RELEASED here
bus_idle_for(24); // the mandatory bus-free interval, and then some
bus_start(); // ... and re-taken
bus_byte(8'hA1, 1'b0);
bus_byte(8'h5A, 1'b1);
bus_stop();
bus_idle_for(10);
// THE structural difference: no repeated START at all, and TWO fresh
// STARTs where one logical access should have produced one.
if (n_restarts !== sr_before) begin
$display("FAIL: a STOP-then-START junction produced a repeated START");
errors++; end
if (n_starts !== s_before + 16'd2) begin
$display("FAIL: a released junction produced %0d fresh STARTs, expected 2",
n_starts - s_before); errors++; end
end
if (junction_kind !== JK_FRESH) begin
$display("FAIL: junction_kind = %0d, expected %0d (fresh START)",
junction_kind, JK_FRESH); errors++; end
// THE cost. The bus was measurably free, and every cycle of it is a window in
// which another master could have taken the bus and changed the pointer.
if (last_free_cycles === '0) begin
$display("FAIL: a release junction measured ZERO free cycles"); errors++; end
if (last_free_cycles < 16'd20) begin
$display("FAIL: the gap measured %0d cycles, expected at least 20",
last_free_cycles); errors++; end
// ================================================================
// 3 — the gap is MEASURED, not assumed. A longer idle must read longer.
// ================================================================
begin
logic [CNT_W-1:0] short_gap, long_gap;
bus_start(); bus_byte(8'hA0, 1'b0); bus_stop();
bus_idle_for(12);
bus_start(); bus_byte(8'hA1, 1'b0); bus_stop();
short_gap = last_free_cycles;
bus_idle_for(60);
bus_start(); bus_byte(8'hA1, 1'b0); bus_stop();
long_gap = last_free_cycles;
if (!(long_gap > short_gap)) begin
$display("FAIL: a 60-cycle gap measured %0d, a 12-cycle gap measured %0d",
long_gap, short_gap); errors++; end
if (short_gap < 16'd8 || short_gap > 16'd20) begin
$display("FAIL: the 12-cycle gap measured %0d, expected roughly 12",
short_gap); errors++; end
end
// ================================================================
// 3b — free_cycles is the RUNNING gap, and there is no gap during a transfer.
// A consumer that read it mid-transfer must see zero, not the previous
// gap's length left over.
// ================================================================
bus_start();
if (free_cycles !== '0) begin
$display("FAIL: free_cycles reads %0d inside a transfer -- a stale measurement",
free_cycles); errors++; end
bus_byte(8'hA0, 1'b0);
if (free_cycles !== '0) begin
$display("FAIL: free_cycles reads %0d mid-byte, expected 0", free_cycles);
errors++; end
bus_stop();
bus_idle_for(10);
// ================================================================
// 3c — the gap measures time the bus was ACTUALLY FREE. If somebody is holding
// SCL low after the STOP, the bus is not available, and those cycles must
// not be counted as though another master could have used them.
// ================================================================
begin
logic [CNT_W-1:0] gap_with_hold;
bus_start(); bus_byte(8'hA0, 1'b0); bus_stop();
// SCL held LOW by somebody -- the bus is busy, not free
sda_in = 1'b1; scl_in = 1'b0;
repeat (40) @(negedge clk);
// now genuinely released
bus_idle_for(12);
bus_start(); bus_byte(8'hA1, 1'b0); bus_stop();
gap_with_hold = last_free_cycles;
// 40 held cycles plus 12 free ones. Only the free ones are a window.
if (gap_with_hold >= 16'd40) begin
$display("FAIL: the gap measured %0d cycles, counting the 40 in which SCL was HELD LOW",
gap_with_hold); errors++; end
if (gap_with_hold === '0) begin
$display("FAIL: the genuinely free cycles were not counted at all"); errors++; end
end
// ================================================================
// 4 — THREE phases joined by two repeated STARTs. The bus is held across the
// whole thing, so a three-phase transaction is no more exposed than a
// two-phase one -- the exposure is a property of the JUNCTIONS, not of
// the length.
// ================================================================
begin
logic [CNT_W-1:0] s_before, sr_before;
s_before = n_starts; sr_before = n_restarts;
bus_start();
bus_byte(8'hA0, 1'b0); bus_byte(8'h12, 1'b0);
bus_start(); // junction 1
bus_byte(8'hA1, 1'b0); bus_byte(8'h5A, 1'b0);
bus_start(); // junction 2
bus_byte(8'hA1, 1'b0); bus_byte(8'h6B, 1'b1);
bus_stop();
bus_idle_for(10);
// Three phases, ONE start, TWO repeated STARTs. Exposure is a property of
// the junctions, not of the transaction's length: a three-phase held
// transaction is exactly as exposed as a two-phase one, namely not at all.
if (n_starts !== s_before + 16'd1) begin
$display("FAIL: a three-phase held transaction produced %0d STARTs, expected 1",
n_starts - s_before); errors++; end
if (n_restarts !== sr_before + 16'd2) begin
$display("FAIL: a three-phase transaction produced %0d repeated STARTs, expected 2",
n_restarts - sr_before); errors++; end
if (junction_kind !== JK_RESTART) begin
$display("FAIL: the last junction of a held transaction was not an Sr");
errors++; end
end
// ================================================================
// 5 — an SDA edge while SCL is LOW is DATA, never framing. This is the check
// that stops the monitor from inventing junctions inside a byte.
// ================================================================
begin
logic [CNT_W-1:0] s_before, p_before;
s_before = n_starts; p_before = n_stops;
// a byte whose bits move SDA up and down many times, all with SCL low
bus_start();
bus_byte(8'hAA, 1'b0); // 1010 1010 -- eight SDA transitions
bus_byte(8'h55, 1'b0); // 0101 0101 -- eight more
bus_stop();
bus_idle_for(10);
// exactly one S and one P from the framing above, and nothing from the data
if (n_starts !== s_before + 16'd1) begin
$display("FAIL: data transitions produced %0d spurious STARTs",
n_starts - s_before - 1); errors++; end
if (n_stops !== p_before + 16'd1) begin
$display("FAIL: data transitions produced %0d spurious STOPs",
n_stops - p_before - 1); errors++; end
end
// ================================================================
// 6 — a long IDLE with no framing at all must not open a transfer or count
// anything. The bus spends most of its life here.
// ================================================================
begin
logic [CNT_W-1:0] s_before, sr_before, p_before;
s_before = n_starts; sr_before = n_restarts; p_before = n_stops;
bus_idle_for(80);
if (n_starts !== s_before || n_restarts !== sr_before || n_stops !== p_before) begin
$display("FAIL: an idle bus produced framing events"); errors++; end
if (in_transfer !== 1'b0) begin
$display("FAIL: an idle bus opened a transfer"); errors++; end
end
if (errors == 0)
$display("PASS: Sr and STOP-then-START distinguished from the wire, the release window measured, data edges never mistaken for framing");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule // A PASSIVE bus monitor (Verilog-2001)
// that classifies the JUNCTION between two phases of a transfer
// and measures what a release costs. It drives nothing.
//
// The two sequences this chapter compares look almost identical in a driver's source
// and are completely different on the wire:
//
// REPEATED START S ... A [Sr] addr+R ... bus NEVER released
// STOP then START S ... A [P] ...... [S] addr+R bus released
//
// And they are distinguishable from the two wires alone, using nothing but the framing
// rule of Chapter 5.1 -- an SDA edge while SCL is HIGH is reserved for framing:
//
// SDA falling while SCL high -> a START. If a transfer is already open it is a
// REPEATED start, and the bus was never released.
// SDA rising while SCL high -> a STOP. The bus is now free, and every device
// on it is entitled to start a transfer.
//
// So "was the bus released between the phases" is not a matter of intent or of reading
// the driver's source. It is a fact on the wire, and this block reads it.
//
// The measurement that matters is free_cycles: how long the bus sat idle between a STOP
// and the next START. That interval is the EXPOSURE WINDOW -- the time during which any
// other master on the bus could legitimately have begun its own transfer and, in the
// register-pointer pattern of Chapter 10.1, changed the pointer this master had just
// written. A repeated START makes that window exactly zero, which is the whole reason
// the specification's combined format exists.
//
// ONE LIMIT IS WORTH STATING PLAINLY, because it is the bridge to Chapter 10.3.
// This block reports FACTS about the wire and cannot report INTENT. A STOP followed by
// a START is, on the wire, exactly two separate transactions -- and that is precisely
// what it IS. There is no field, flag or timing that distinguishes "a driver that
// wrongly released the bus mid-transaction" from "two transactions that happened to be
// adjacent", because after the STOP the first transaction is over in every sense the
// protocol recognises. So the diagnostic here is structural rather than semantic:
//
// one logical access should produce ONE start and k RESTARTS.
// If it produced two STARTS, the bus was released, whatever the driver intended.
//
// Correlating that with what a master believed it was doing needs a second input, and
// Chapter 10.3 supplies it.
module i2c_phase_junction_monitor #(
parameter CNT_W = 16
)(
input wire clk,
input wire rst_n,
// The two wires, already synchronised to clk by the input stage of Module 4.
input wire sda_in,
input wire scl_in,
// ---- framing, detected from the wire ----
output reg start_det, // pulse: a START (first of a transfer)
output reg restart_det, // pulse: a REPEATED START -- bus retained
output reg stop_det, // pulse: a STOP -- bus released
output reg in_transfer, // a transfer is open: between an S/Sr and the next P
// ---- junction classification ----
// How the most recent addressing was reached: by a repeated START, with the bus
// retained, or by a fresh START after the bus had been free.
output reg [1:0] junction_kind, // 0 = none yet, 1 = repeated START, 2 = fresh
output reg bus_was_free, // pulse: a fresh START re-took a released bus
// ---- the cost of a release ----
// Cycles the bus spent idle between a STOP and the next START. In a combined
// transaction this is the window another master could have used.
output reg [CNT_W-1:0] free_cycles,
output reg [CNT_W-1:0] last_free_cycles, // the measurement of the last gap
output reg [CNT_W-1:0] n_starts,
output reg [CNT_W-1:0] n_restarts,
output reg [CNT_W-1:0] n_stops
);
localparam [1:0] JK_NONE = 2'd0;
localparam [1:0] JK_RESTART = 2'd1;
localparam [1:0] JK_FRESH = 2'd2;
reg sda_q, scl_q;
wire sda_fall, sda_rise, scl_stable_high;
assign sda_fall = sda_q && !sda_in;
assign sda_rise = !sda_q && sda_in;
// SCL must be high ACROSS the SDA edge, not merely high after it. Requiring both
// the previous and the current sample to be high is what stops a coincident SCL
// rise from being misread as framing -- the defect Module 4's mutation suite found
// and the reason that chapter's detector uses both terms.
assign scl_stable_high = scl_q && scl_in;
// A STOP has just released the bus, so the gap timer runs until the next START.
// "Idle" means both lines released: an undriven bus is high on both wires.
reg gap_running;
wire bus_idle;
assign bus_idle = sda_in && scl_in;
always @(posedge clk) begin
if (!rst_n) begin
sda_q <= 1'b1; // an idle bus is high on both wires
scl_q <= 1'b1;
start_det <= 1'b0;
restart_det <= 1'b0;
stop_det <= 1'b0;
in_transfer <= 1'b0;
junction_kind <= JK_NONE;
bus_was_free <= 1'b0;
free_cycles <= {CNT_W{1'b0}};
last_free_cycles <= {CNT_W{1'b0}};
n_starts <= {CNT_W{1'b0}};
n_restarts <= {CNT_W{1'b0}};
n_stops <= {CNT_W{1'b0}};
gap_running <= 1'b0;
end else begin
sda_q <= sda_in;
scl_q <= scl_in;
start_det <= 1'b0;
restart_det <= 1'b0;
stop_det <= 1'b0;
bus_was_free <= 1'b0;
if (sda_fall && scl_stable_high) begin
// A START. Whether it is a plain one or a REPEATED one depends entirely
// on whether a transfer was already open -- there is no difference in
// the electrical event itself, which is why a monitor must track state
// to classify it.
if (in_transfer) begin
restart_det <= 1'b1;
n_restarts <= n_restarts + 1'b1;
junction_kind <= JK_RESTART;
end else begin
start_det <= 1'b1;
n_starts <= n_starts + 1'b1;
junction_kind <= JK_FRESH;
// A fresh START that follows a STOP re-takes a bus that was
// genuinely free. The gap timer was running, so its value is the
// window during which any other master could have taken it.
if (gap_running) begin
bus_was_free <= 1'b1;
last_free_cycles <= free_cycles;
end
end
in_transfer <= 1'b1;
gap_running <= 1'b0;
free_cycles <= {CNT_W{1'b0}};
end else if (sda_rise && scl_stable_high) begin
// A STOP. The bus is released from this instant, and the gap timer
// starts -- it is measuring the window, not waiting to see if anyone
// uses it.
stop_det <= 1'b1;
n_stops <= n_stops + 1'b1;
in_transfer <= 1'b0;
gap_running <= 1'b1;
free_cycles <= {CNT_W{1'b0}};
end else if (gap_running && bus_idle) begin
free_cycles <= free_cycles + 1'b1;
end
end
end
endmodule `timescale 1ns/1ps
// The testbench drives the two WIRES, not an abstract command interface, because the
// claim under test is about what the wire shows. Both sequences are built from the same
// primitives so the only difference between them is the junction.
module i2c_phase_junction_monitor_tb; // Verilog-2001
localparam CNT_W = 16;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b0;
reg sda_in = 1'b1, scl_in = 1'b1; // an idle bus is high on both wires
wire start_det, restart_det, stop_det, in_transfer, bus_was_free;
wire [1:0] junction_kind;
wire [CNT_W-1:0] free_cycles, last_free_cycles;
wire [CNT_W-1:0] n_starts, n_restarts, n_stops;
localparam [1:0] JK_NONE = 2'd0, JK_RESTART = 2'd1, JK_FRESH = 2'd2;
integer errors = 0;
integer i;
reg [CNT_W-1:0] sr_before, s_before, p_before, short_gap, long_gap, gap_with_hold;
i2c_phase_junction_monitor #(.CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n), .sda_in(sda_in), .scl_in(scl_in),
.start_det(start_det), .restart_det(restart_det), .stop_det(stop_det),
.in_transfer(in_transfer), .junction_kind(junction_kind),
.bus_was_free(bus_was_free), .free_cycles(free_cycles),
.last_free_cycles(last_free_cycles), .n_starts(n_starts),
.n_restarts(n_restarts), .n_stops(n_stops));
initial begin #400000; $display("FAIL: watchdog expired"); $finish; end
// ---- bus primitives, built strictly from the framing rules of Module 5 ----------
// A START: SDA falls while SCL is HIGH.
task bus_start; begin
sda_in = 1'b1; scl_in = 1'b1; @(negedge clk); @(negedge clk);
sda_in = 1'b0; @(negedge clk); // the falling edge, SCL high
scl_in = 1'b0; @(negedge clk); // SCL drops: the transfer begins
end endtask
// A STOP: SDA rises while SCL is HIGH.
task bus_stop; begin
sda_in = 1'b0; scl_in = 1'b0; @(negedge clk);
scl_in = 1'b1; @(negedge clk); // SCL up with SDA still low
sda_in = 1'b1; @(negedge clk); // the rising edge, SCL high
end endtask
// A data bit: SDA changes only while SCL is LOW, then one SCL pulse.
task bus_bit;
input v;
begin
scl_in = 1'b0; @(negedge clk);
sda_in = v; @(negedge clk);
scl_in = 1'b1; @(negedge clk);
scl_in = 1'b0; @(negedge clk);
end
endtask
// One byte and its acknowledge slot -- nine bits, exactly as Chapter 7.1 requires.
task bus_byte;
input [7:0] v;
input ninth;
begin
for (i = 7; i >= 0; i = i - 1) bus_bit(v[i]);
bus_bit(ninth);
end
endtask
// Let the bus sit IDLE for n cycles. Both wires released, which is what an
// unowned bus looks like.
task bus_idle_for;
input integer n;
begin
sda_in = 1'b1; scl_in = 1'b1;
repeat (n) @(negedge clk);
end
endtask
initial begin
repeat (3) @(negedge clk);
if (in_transfer !== 1'b0) begin $display("FAIL: in_transfer out of reset"); errors = errors + 1; end
if (junction_kind !== JK_NONE) begin
$display("FAIL: junction_kind out of reset"); errors = errors + 1; end
rst_n = 1'b1; @(negedge clk);
// ================================================================
// SEQUENCE 1 — the CORRECT combined transaction: repeated START.
// S addr+W A ptr A Sr addr+R A data N P
// ================================================================
bus_start;
bus_byte(8'hA0, 1'b0); // addr+W, slave ACKs (SDA low in the 9th)
bus_byte(8'h12, 1'b0); // the pointer, ACKed
bus_start; // THE JUNCTION -- a repeated START
bus_byte(8'hA1, 1'b0); // addr+R, ACKed
bus_byte(8'h5A, 1'b1); // data, master NACKs (SDA high in the 9th)
bus_stop;
bus_idle_for(10);
// THE structural diagnostic. One logical access, ONE start: a repeated START
// does not release the bus, so there is nothing to re-take.
if (n_starts !== 16'd1) begin
$display("FAIL: a held transaction produced %0d STARTs -- one access, one START",
n_starts); errors = errors + 1; end
if (n_restarts !== 16'd1) begin
$display("FAIL: counted %0d repeated STARTs, expected 1", n_restarts); errors = errors + 1; end
if (n_stops !== 16'd1) begin
$display("FAIL: counted %0d STOPs, expected 1", n_stops); errors = errors + 1; end
if (junction_kind !== JK_RESTART) begin
$display("FAIL: junction_kind = %0d, expected %0d (repeated START)",
junction_kind, JK_RESTART); errors = errors + 1; end
// No gap to measure, and therefore no window in which another master could
// have interposed.
if (bus_was_free !== 1'b0) begin
$display("FAIL: a repeated START reported the bus as having been free");
errors = errors + 1; end
if (in_transfer !== 1'b0) begin
$display("FAIL: still in a transfer after the STOP"); errors = errors + 1; end
// ================================================================
// SEQUENCE 2 — the SAME ACCESS written as STOP then START.
// S addr+W A ptr A P [bus free] S addr+R A data N P
// Byte for byte identical. Only the junction differs.
// ================================================================
begin
sr_before = n_restarts; s_before = n_starts;
bus_start;
bus_byte(8'hA0, 1'b0);
bus_byte(8'h12, 1'b0);
bus_stop; // THE JUNCTION -- the bus is RELEASED here
bus_idle_for(24); // the mandatory bus-free interval, and then some
bus_start; // ... and re-taken
bus_byte(8'hA1, 1'b0);
bus_byte(8'h5A, 1'b1);
bus_stop;
bus_idle_for(10);
// THE structural difference: no repeated START at all, and TWO fresh
// STARTs where one logical access should have produced one.
if (n_restarts !== sr_before) begin
$display("FAIL: a STOP-then-START junction produced a repeated START");
errors = errors + 1; end
if (n_starts !== s_before + 16'd2) begin
$display("FAIL: a released junction produced %0d fresh STARTs, expected 2",
n_starts - s_before); errors = errors + 1; end
end
if (junction_kind !== JK_FRESH) begin
$display("FAIL: junction_kind = %0d, expected %0d (fresh START)",
junction_kind, JK_FRESH); errors = errors + 1; end
// THE cost. The bus was measurably free, and every cycle of it is a window in
// which another master could have taken the bus and changed the pointer.
if (last_free_cycles === {CNT_W{1'b0}}) begin
$display("FAIL: a release junction measured ZERO free cycles"); errors = errors + 1; end
if (last_free_cycles < 16'd20) begin
$display("FAIL: the gap measured %0d cycles, expected at least 20",
last_free_cycles); errors = errors + 1; end
// ================================================================
// 3 — the gap is MEASURED, not assumed. A longer idle must read longer.
// ================================================================
begin
bus_start; bus_byte(8'hA0, 1'b0); bus_stop;
bus_idle_for(12);
bus_start; bus_byte(8'hA1, 1'b0); bus_stop;
short_gap = last_free_cycles;
bus_idle_for(60);
bus_start; bus_byte(8'hA1, 1'b0); bus_stop;
long_gap = last_free_cycles;
if (!(long_gap > short_gap)) begin
$display("FAIL: a 60-cycle gap measured %0d, a 12-cycle gap measured %0d",
long_gap, short_gap); errors = errors + 1; end
if (short_gap < 16'd8 || short_gap > 16'd20) begin
$display("FAIL: the 12-cycle gap measured %0d, expected roughly 12",
short_gap); errors = errors + 1; end
end
// ================================================================
// 3b — free_cycles is the RUNNING gap, and there is no gap during a transfer.
// A consumer that read it mid-transfer must see zero, not the previous
// gap's length left over.
// ================================================================
bus_start;
if (free_cycles !== {CNT_W{1'b0}}) begin
$display("FAIL: free_cycles reads %0d inside a transfer -- a stale measurement",
free_cycles); errors = errors + 1; end
bus_byte(8'hA0, 1'b0);
if (free_cycles !== {CNT_W{1'b0}}) begin
$display("FAIL: free_cycles reads %0d mid-byte, expected 0", free_cycles);
errors = errors + 1; end
bus_stop;
bus_idle_for(10);
// ================================================================
// 3c — the gap measures time the bus was ACTUALLY FREE. If somebody is holding
// SCL low after the STOP, the bus is not available, and those cycles must
// not be counted as though another master could have used them.
// ================================================================
begin
bus_start; bus_byte(8'hA0, 1'b0); bus_stop;
// SCL held LOW by somebody -- the bus is busy, not free
sda_in = 1'b1; scl_in = 1'b0;
repeat (40) @(negedge clk);
// now genuinely released
bus_idle_for(12);
bus_start; bus_byte(8'hA1, 1'b0); bus_stop;
gap_with_hold = last_free_cycles;
// 40 held cycles plus 12 free ones. Only the free ones are a window.
if (gap_with_hold >= 16'd40) begin
$display("FAIL: the gap measured %0d cycles, counting the 40 in which SCL was HELD LOW",
gap_with_hold); errors = errors + 1; end
if (gap_with_hold === {CNT_W{1'b0}}) begin
$display("FAIL: the genuinely free cycles were not counted at all");
errors = errors + 1; end
end
// ================================================================
// 4 — THREE phases joined by two repeated STARTs. The bus is held across the
// whole thing, so a three-phase transaction is no more exposed than a
// two-phase one -- the exposure is a property of the JUNCTIONS, not of
// the length.
// ================================================================
begin
s_before = n_starts; sr_before = n_restarts;
bus_start;
bus_byte(8'hA0, 1'b0); bus_byte(8'h12, 1'b0);
bus_start; // junction 1
bus_byte(8'hA1, 1'b0); bus_byte(8'h5A, 1'b0);
bus_start; // junction 2
bus_byte(8'hA1, 1'b0); bus_byte(8'h6B, 1'b1);
bus_stop;
bus_idle_for(10);
// Three phases, ONE start, TWO repeated STARTs. Exposure is a property of
// the junctions, not of the transaction's length: a three-phase held
// transaction is exactly as exposed as a two-phase one, namely not at all.
if (n_starts !== s_before + 16'd1) begin
$display("FAIL: a three-phase held transaction produced %0d STARTs, expected 1",
n_starts - s_before); errors = errors + 1; end
if (n_restarts !== sr_before + 16'd2) begin
$display("FAIL: a three-phase transaction produced %0d repeated STARTs, expected 2",
n_restarts - sr_before); errors = errors + 1; end
if (junction_kind !== JK_RESTART) begin
$display("FAIL: the last junction of a held transaction was not an Sr");
errors = errors + 1; end
end
// ================================================================
// 5 — an SDA edge while SCL is LOW is DATA, never framing. This is the check
// that stops the monitor from inventing junctions inside a byte.
// ================================================================
begin
s_before = n_starts; p_before = n_stops;
// a byte whose bits move SDA up and down many times, all with SCL low
bus_start;
bus_byte(8'hAA, 1'b0); // 1010 1010 -- eight SDA transitions
bus_byte(8'h55, 1'b0); // 0101 0101 -- eight more
bus_stop;
bus_idle_for(10);
// exactly one S and one P from the framing above, and nothing from the data
if (n_starts !== s_before + 16'd1) begin
$display("FAIL: data transitions produced %0d spurious STARTs",
n_starts - s_before - 1); errors = errors + 1; end
if (n_stops !== p_before + 16'd1) begin
$display("FAIL: data transitions produced %0d spurious STOPs",
n_stops - p_before - 1); errors = errors + 1; end
end
// ================================================================
// 6 — a long IDLE with no framing at all must not open a transfer or count
// anything. The bus spends most of its life here.
// ================================================================
begin
s_before = n_starts; sr_before = n_restarts; p_before = n_stops;
bus_idle_for(80);
if (n_starts !== s_before || n_restarts !== sr_before || n_stops !== p_before) begin
$display("FAIL: an idle bus produced framing events"); errors = errors + 1; end
if (in_transfer !== 1'b0) begin
$display("FAIL: an idle bus opened a transfer"); errors = errors + 1; end
end
if (errors == 0)
$display("PASS: Sr and STOP-then-START distinguished from the wire, the release window measured, data edges never mistaken for framing");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- A PASSIVE bus monitor that classifies the JUNCTION between two phases of a transfer
-- and measures what a release costs. It drives nothing.
--
-- The two sequences this chapter compares look almost identical in a driver's source
-- and are completely different on the wire:
--
-- REPEATED START S ... A [Sr] addr+R ... bus NEVER released
-- STOP then START S ... A [P] ...... [S] addr+R bus released
--
-- And they are distinguishable from the two wires alone, using nothing but the framing
-- rule of Chapter 5.1 -- an SDA edge while SCL is HIGH is reserved for framing:
--
-- SDA falling while SCL high -> a START. If a transfer is already open it is a
-- REPEATED start, and the bus was never released.
-- SDA rising while SCL high -> a STOP. The bus is now free, and every device
-- on it is entitled to start a transfer.
--
-- The measurement that matters is free_cycles: how long the bus sat idle between a STOP
-- and the next START. That interval is the EXPOSURE WINDOW -- the time during which any
-- other master could legitimately have begun its own transfer and, in the
-- register-pointer pattern of Chapter 10.1, changed the pointer this master had just
-- written. A repeated START makes that window exactly zero.
--
-- ONE LIMIT IS WORTH STATING PLAINLY, because it is the bridge to Chapter 10.3.
-- This block reports FACTS about the wire and cannot report INTENT. A STOP followed by
-- a START is, on the wire, exactly two separate transactions -- and that is precisely
-- what it IS. So the diagnostic here is structural rather than semantic:
--
-- one logical access should produce ONE start and k RESTARTS.
-- If it produced two STARTS, the bus was released, whatever the driver intended.
entity i2c_phase_junction_monitor is
generic (
CNT_W : positive := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- The two wires, already synchronised to clk by the input stage of Module 4.
sda_in : in std_logic;
scl_in : in std_logic;
-- framing, detected from the wire
start_det : out std_logic; -- pulse: a START (first of a transfer)
restart_det : out std_logic; -- pulse: a REPEATED START -- bus retained
stop_det : out std_logic; -- pulse: a STOP -- bus released
in_transfer : out std_logic; -- open: between an S/Sr and the next P
-- How the most recent addressing was reached: by a repeated START, with the bus
-- retained, or by a fresh START after the bus had been free.
junction_kind : out unsigned(1 downto 0); -- 0 none, 1 repeated START, 2 fresh
bus_was_free : out std_logic; -- pulse: a fresh START re-took a free bus
-- the cost of a release
free_cycles : out unsigned(CNT_W - 1 downto 0);
last_free_cycles : out unsigned(CNT_W - 1 downto 0);
n_starts : out unsigned(CNT_W - 1 downto 0);
n_restarts : out unsigned(CNT_W - 1 downto 0);
n_stops : out unsigned(CNT_W - 1 downto 0)
);
end entity;
architecture rtl of i2c_phase_junction_monitor is
constant JK_NONE : unsigned(1 downto 0) := to_unsigned(0, 2);
constant JK_RESTART : unsigned(1 downto 0) := to_unsigned(1, 2);
constant JK_FRESH : unsigned(1 downto 0) := to_unsigned(2, 2);
signal sda_q, scl_q : std_logic := '1'; -- an idle bus is high on both wires
signal sda_fall, sda_rise, scl_stable_high : std_logic;
signal gap_running : std_logic := '0';
signal bus_idle : std_logic;
signal gap : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal open_xfer : std_logic := '0';
begin
sda_fall <= sda_q and (not sda_in);
sda_rise <= (not sda_q) and sda_in;
-- SCL must be high ACROSS the SDA edge, not merely high after it. Requiring both
-- the previous and the current sample to be high is what stops a coincident SCL
-- rise from being misread as framing -- the defect Module 4's mutation suite found.
scl_stable_high <= scl_q and scl_in;
-- "Idle" means both lines released: an undriven bus is high on both wires.
bus_idle <= sda_in and scl_in;
free_cycles <= gap;
in_transfer <= open_xfer;
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
sda_q <= '1';
scl_q <= '1';
start_det <= '0';
restart_det <= '0';
stop_det <= '0';
open_xfer <= '0';
junction_kind <= JK_NONE;
bus_was_free <= '0';
gap <= (others => '0');
last_free_cycles <= (others => '0');
n_starts <= (others => '0');
n_restarts <= (others => '0');
n_stops <= (others => '0');
gap_running <= '0';
else
sda_q <= sda_in;
scl_q <= scl_in;
start_det <= '0';
restart_det <= '0';
stop_det <= '0';
bus_was_free <= '0';
if sda_fall = '1' and scl_stable_high = '1' then
-- A START. Whether it is a plain one or a REPEATED one depends
-- entirely on whether a transfer was already open -- there is no
-- difference in the electrical event itself, which is why a monitor
-- must track state to classify it.
if open_xfer = '1' then
restart_det <= '1';
n_restarts <= n_restarts + 1;
junction_kind <= JK_RESTART;
else
start_det <= '1';
n_starts <= n_starts + 1;
junction_kind <= JK_FRESH;
-- A fresh START that follows a STOP re-takes a bus that was
-- genuinely free. The gap timer was running, so its value is the
-- window during which any other master could have taken it.
if gap_running = '1' then
bus_was_free <= '1';
last_free_cycles <= gap;
end if;
end if;
open_xfer <= '1';
gap_running <= '0';
gap <= (others => '0');
elsif sda_rise = '1' and scl_stable_high = '1' then
-- A STOP. The bus is released from this instant, and the gap timer
-- starts -- it is measuring the window, not waiting to see if
-- anyone uses it.
stop_det <= '1';
n_stops <= n_stops + 1;
open_xfer <= '0';
gap_running <= '1';
gap <= (others => '0');
elsif gap_running = '1' and bus_idle = '1' then
gap <= gap + 1;
end if;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- The testbench drives the two WIRES, not an abstract command interface, because the
-- claim under test is about what the wire shows. Both sequences are built from the same
-- primitives so the only difference between them is the junction.
entity i2c_phase_junction_monitor_tb is
end entity;
architecture sim of i2c_phase_junction_monitor_tb is
constant CNT_W : positive := 16;
constant JK_NONE : unsigned(1 downto 0) := to_unsigned(0, 2);
constant JK_RESTART : unsigned(1 downto 0) := to_unsigned(1, 2);
constant JK_FRESH : unsigned(1 downto 0) := to_unsigned(2, 2);
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal sda_in, scl_in : std_logic := '1'; -- an idle bus is high on both wires
signal start_det, restart_det, stop_det, in_transfer, bus_was_free : std_logic;
signal junction_kind : unsigned(1 downto 0);
signal free_cycles, last_free_cycles : unsigned(CNT_W - 1 downto 0);
signal n_starts, n_restarts, n_stops : unsigned(CNT_W - 1 downto 0);
signal test_done : std_logic := '0';
begin
dut : entity work.i2c_phase_junction_monitor
generic map (CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n, sda_in => sda_in, scl_in => scl_in,
start_det => start_det, restart_det => restart_det,
stop_det => stop_det, in_transfer => in_transfer,
junction_kind => junction_kind, bus_was_free => bus_was_free,
free_cycles => free_cycles, last_free_cycles => last_free_cycles,
n_starts => n_starts, n_restarts => n_restarts, n_stops => n_stops);
clk <= not clk after 5 ns;
watchdog : process
begin
wait for 800 us;
if test_done = '0' then
report "watchdog expired -- the design never reached the expected state"
severity failure;
end if;
wait;
end process;
stim : process
variable errs : natural := 0;
variable sr_before, s_before, p_before : unsigned(CNT_W - 1 downto 0);
variable short_gap, long_gap, gap_with_hold : unsigned(CNT_W - 1 downto 0);
procedure waitn (n : in positive) is
begin
for i in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
-- A START: SDA falls while SCL is HIGH.
procedure bus_start is
begin
sda_in <= '1'; scl_in <= '1'; waitn(2);
sda_in <= '0'; waitn(1); -- the falling edge, SCL high
scl_in <= '0'; waitn(1); -- SCL drops: the transfer begins
end procedure;
-- A STOP: SDA rises while SCL is HIGH.
procedure bus_stop is
begin
sda_in <= '0'; scl_in <= '0'; waitn(1);
scl_in <= '1'; waitn(1); -- SCL up with SDA still low
sda_in <= '1'; waitn(1); -- the rising edge, SCL high
end procedure;
-- A data bit: SDA changes only while SCL is LOW, then one SCL pulse.
procedure bus_bit (v : in std_logic) is
begin
scl_in <= '0'; waitn(1);
sda_in <= v; waitn(1);
scl_in <= '1'; waitn(1);
scl_in <= '0'; waitn(1);
end procedure;
-- One byte and its acknowledge slot -- nine bits, as Chapter 7.1 requires.
procedure bus_byte (v : in std_logic_vector(7 downto 0); ninth : in std_logic) is
begin
for i in 7 downto 0 loop bus_bit(v(i)); end loop;
bus_bit(ninth);
end procedure;
-- Let the bus sit IDLE for n cycles: both wires released, which is what an
-- unowned bus looks like.
procedure bus_idle_for (n : in positive) is
begin
sda_in <= '1'; scl_in <= '1';
waitn(n);
end procedure;
begin
waitn(3);
if in_transfer /= '0' then
report "in_transfer out of reset" severity error; errs := errs + 1; end if;
if junction_kind /= JK_NONE then
report "junction_kind out of reset" severity error; errs := errs + 1; end if;
rst_n <= '1'; waitn(1);
-- ================================================================
-- SEQUENCE 1 -- the CORRECT combined transaction: repeated START.
-- S addr+W A ptr A Sr addr+R A data N P
-- ================================================================
bus_start;
bus_byte(x"A0", '0'); -- addr+W, slave ACKs (SDA low in the 9th)
bus_byte(x"12", '0'); -- the pointer, ACKed
bus_start; -- THE JUNCTION -- a repeated START
bus_byte(x"A1", '0'); -- addr+R, ACKed
bus_byte(x"5A", '1'); -- data, master NACKs (SDA high in the 9th)
bus_stop;
bus_idle_for(10);
-- THE structural diagnostic. One logical access, ONE start: a repeated START
-- does not release the bus, so there is nothing to re-take.
if n_starts /= to_unsigned(1, CNT_W) then
report "a held transaction produced more than one START" severity error;
errs := errs + 1; end if;
if n_restarts /= to_unsigned(1, CNT_W) then
report "wrong number of repeated STARTs, expected 1" severity error;
errs := errs + 1; end if;
if n_stops /= to_unsigned(1, CNT_W) then
report "wrong number of STOPs, expected 1" severity error; errs := errs + 1; end if;
if junction_kind /= JK_RESTART then
report "junction_kind should be repeated START" severity error;
errs := errs + 1; end if;
-- No gap to measure, and therefore no window in which another master could
-- have interposed.
if bus_was_free /= '0' then
report "a repeated START reported the bus as having been free" severity error;
errs := errs + 1; end if;
if in_transfer /= '0' then
report "still in a transfer after the STOP" severity error; errs := errs + 1; end if;
-- ================================================================
-- SEQUENCE 2 -- the SAME ACCESS written as STOP then START.
-- S addr+W A ptr A P [bus free] S addr+R A data N P
-- Byte for byte identical. Only the junction differs.
-- ================================================================
sr_before := n_restarts; s_before := n_starts;
bus_start;
bus_byte(x"A0", '0');
bus_byte(x"12", '0');
bus_stop; -- THE JUNCTION -- the bus is RELEASED here
bus_idle_for(24); -- the mandatory bus-free interval, and then some
bus_start; -- ... and re-taken
bus_byte(x"A1", '0');
bus_byte(x"5A", '1');
bus_stop;
bus_idle_for(10);
-- THE structural difference: no repeated START at all, and TWO fresh STARTs
-- where one logical access should have produced one.
if n_restarts /= sr_before then
report "a STOP-then-START junction produced a repeated START" severity error;
errs := errs + 1; end if;
if n_starts /= s_before + 2 then
report "a released junction did not produce two fresh STARTs" severity error;
errs := errs + 1; end if;
if junction_kind /= JK_FRESH then
report "junction_kind should be a fresh START" severity error;
errs := errs + 1; end if;
-- THE cost. The bus was measurably free, and every cycle of it is a window in
-- which another master could have taken the bus and changed the pointer.
if last_free_cycles = to_unsigned(0, CNT_W) then
report "a release junction measured ZERO free cycles" severity error;
errs := errs + 1; end if;
if last_free_cycles < to_unsigned(20, CNT_W) then
report "the measured gap was shorter than the idle that produced it"
severity error; errs := errs + 1; end if;
-- ================================================================
-- 3 -- the gap is MEASURED, not assumed. A longer idle must read longer.
-- ================================================================
bus_start; bus_byte(x"A0", '0'); bus_stop;
bus_idle_for(12);
bus_start; bus_byte(x"A1", '0'); bus_stop;
short_gap := last_free_cycles;
bus_idle_for(60);
bus_start; bus_byte(x"A1", '0'); bus_stop;
long_gap := last_free_cycles;
if not (long_gap > short_gap) then
report "a 60-cycle gap did not measure longer than a 12-cycle gap"
severity error; errs := errs + 1; end if;
if short_gap < to_unsigned(8, CNT_W) or short_gap > to_unsigned(20, CNT_W) then
report "the 12-cycle gap measured outside the expected range" severity error;
errs := errs + 1; end if;
-- ================================================================
-- 3b -- free_cycles is the RUNNING gap, and there is no gap during a transfer.
-- A consumer that read it mid-transfer must see zero, not the previous
-- gap's length left over.
-- ================================================================
bus_start;
if free_cycles /= to_unsigned(0, CNT_W) then
report "free_cycles is nonzero inside a transfer -- a stale measurement"
severity error; errs := errs + 1; end if;
bus_byte(x"A0", '0');
if free_cycles /= to_unsigned(0, CNT_W) then
report "free_cycles is nonzero mid-byte, expected 0" severity error;
errs := errs + 1; end if;
bus_stop;
bus_idle_for(10);
-- ================================================================
-- 3c -- the gap measures time the bus was ACTUALLY FREE. If somebody is holding
-- SCL low after the STOP, the bus is not available, and those cycles must
-- not be counted as though another master could have used them.
-- ================================================================
bus_start; bus_byte(x"A0", '0'); bus_stop;
-- SCL held LOW by somebody -- the bus is busy, not free
sda_in <= '1'; scl_in <= '0';
waitn(40);
-- now genuinely released
bus_idle_for(12);
bus_start; bus_byte(x"A1", '0'); bus_stop;
gap_with_hold := last_free_cycles;
-- 40 held cycles plus 12 free ones. Only the free ones are a window.
if gap_with_hold >= to_unsigned(40, CNT_W) then
report "the gap counted the cycles in which SCL was HELD LOW" severity error;
errs := errs + 1; end if;
if gap_with_hold = to_unsigned(0, CNT_W) then
report "the genuinely free cycles were not counted at all" severity error;
errs := errs + 1; end if;
-- ================================================================
-- 4 -- THREE phases joined by two repeated STARTs. The bus is held across the
-- whole thing, so a three-phase transaction is no more exposed than a
-- two-phase one -- the exposure is a property of the JUNCTIONS, not of
-- the length.
-- ================================================================
s_before := n_starts; sr_before := n_restarts;
bus_start;
bus_byte(x"A0", '0'); bus_byte(x"12", '0');
bus_start; -- junction 1
bus_byte(x"A1", '0'); bus_byte(x"5A", '0');
bus_start; -- junction 2
bus_byte(x"A1", '0'); bus_byte(x"6B", '1');
bus_stop;
bus_idle_for(10);
if n_starts /= s_before + 1 then
report "a three-phase held transaction produced more than one START"
severity error; errs := errs + 1; end if;
if n_restarts /= sr_before + 2 then
report "a three-phase transaction did not produce two repeated STARTs"
severity error; errs := errs + 1; end if;
if junction_kind /= JK_RESTART then
report "the last junction of a held transaction was not an Sr" severity error;
errs := errs + 1; end if;
-- ================================================================
-- 5 -- an SDA edge while SCL is LOW is DATA, never framing. This is the check
-- that stops the monitor from inventing junctions inside a byte.
-- ================================================================
s_before := n_starts; p_before := n_stops;
bus_start;
bus_byte(x"AA", '0'); -- 1010 1010 -- eight SDA transitions
bus_byte(x"55", '0'); -- 0101 0101 -- eight more
bus_stop;
bus_idle_for(10);
if n_starts /= s_before + 1 then
report "data transitions produced spurious STARTs" severity error;
errs := errs + 1; end if;
if n_stops /= p_before + 1 then
report "data transitions produced spurious STOPs" severity error;
errs := errs + 1; end if;
-- ================================================================
-- 6 -- a long IDLE with no framing at all must not open a transfer or count
-- anything. The bus spends most of its life here.
-- ================================================================
s_before := n_starts; sr_before := n_restarts; p_before := n_stops;
bus_idle_for(80);
if n_starts /= s_before or n_restarts /= sr_before or n_stops /= p_before then
report "an idle bus produced framing events" severity error;
errs := errs + 1; end if;
if in_transfer /= '0' then
report "an idle bus opened a transfer" severity error; errs := errs + 1; end if;
if errs = 0 then
report "i2c_phase_junction_monitor self-check complete: Sr and STOP-then-START "
& "distinguished from the wire, the release window measured, data edges "
& "never mistaken for framing" severity note;
else
report "i2c_phase_junction_monitor self-check FAILED" severity error;
end if;
test_done <= '1';
wait;
end process;
end architecture;5a. Five Decisions Worth Defending
scl_stable_high requires SCL high on both samples, not just the current one. A framing event is an SDA edge while SCL is high, which means SCL must be high across the edge. Testing only the current sample admits a case where SCL and SDA moved together — and a coincident SCL rise would then be read as framing. Module 4's mutation suite found exactly this defect, and mutation B3 re-injects it here: the result is a held transaction reporting two STARTs, because a data-phase transition gets misclassified.
A repeated START is detected by state, not by the wire. if (in_transfer) is the entire classifier, because §1 established the two events are electrically identical. This is worth noticing as a design shape: when two protocol events share a physical signature, the classifier is necessarily stateful, and the state it needs is exactly the thing the protocol uses to disambiguate them. Mutation B1 removes the test and every repeated START is counted as a fresh one.
The gap timer requires the bus to be genuinely idle, not merely "after a STOP". gap_running && bus_idle, where idle means both wires released. If somebody is holding SCL low after the STOP — a stuck slave, another master's aborted attempt — the bus is not available, and counting those cycles would overstate the window. The measurement is meant to answer "how long could another master have used this bus", and a held-low SCL is time nobody could have used. Mutation B6 drops the bus_idle term and is killed by a test that holds SCL low for forty cycles in the middle of a gap.
free_cycles and last_free_cycles are separate outputs. One is the gap in progress, the other the completed measurement, and a consumer needs different things from each. Keeping them separate also means free_cycles reads zero during a transfer rather than holding a stale value — which is a real requirement and the one mutation B5 violates. Nothing in the design consumes either; both exist to be read.
It is genuinely passive. No output goes near SDA or SCL. That is what makes it safe to leave enabled in production silicon, and a debug block that could perturb the bus would be switched off in precisely the situations where it was wanted.
5b. Verified Execution
$ iverilog -g2012 -o b0 i2c_phase_junction_monitor.sv i2c_phase_junction_monitor_tb.sv && ./b0
PASS: Sr and STOP-then-START distinguished from the wire, the release window measured,
data edges never mistaken for framing
i2c_phase_junction_monitor_tb.sv:272: $finish called at 11630 (1ps)
$ iverilog -g2005 -o b1 i2c_phase_junction_monitor.v i2c_phase_junction_monitor_tb.v && ./b1
PASS: Sr and STOP-then-START distinguished from the wire, the release window measured,
data edges never mistaken for framing
i2c_phase_junction_monitor_tb.v:285: $finish called at 11630 (1ps)
$ nvc -a i2c_phase_junction_monitor.vhd i2c_phase_junction_monitor_tb.vhd
$ nvc -e i2c_phase_junction_monitor_tb && nvc -r i2c_phase_junction_monitor_tb --stop-time=900us
** Note: 11630ns+0: i2c_phase_junction_monitor self-check complete: Sr and
STOP-then-START distinguished from the wire, the release window measured, data
edges never mistaken for framingAll three at 11630 ns.
6. What the Testbench Proves
This testbench drives the two wires, not a command interface, because the claim under test is about what the wire shows. Its primitives are built strictly from the framing rules: bus_start moves SDA down while SCL is high, bus_stop moves it up while SCL is high, bus_bit changes SDA only while SCL is low.
That matters more than it sounds. Building the stimulus from the rules rather than from a recording means the two sequences in §2 are assembled from the identical primitives in the identical order, with one substitution — so the test genuinely isolates the junction rather than comparing two hand-written waveforms that might differ in other ways too.
| # | stimulus | what it establishes |
|---|---|---|
| 1 | the correct sequence, with an Sr | one fresh START, one repeated START, one STOP; no gap |
| 2 | the same bytes, with a STOP-then-START | two fresh STARTs, no repeated START, and a measured gap |
| 3 | a 12-cycle gap and a 60-cycle gap | the gap is measured, not assumed — longer reads longer |
| 3b | mid-transfer | free_cycles reads zero, not the previous gap's length |
| 3c | SCL held low for 40 cycles inside a gap | only the genuinely free cycles are counted |
| 4 | three phases, two repeated STARTs | one START, two Sr's — exposure is about junctions, not length |
| 5 | bytes 0xAA and 0x55 | sixteen SDA transitions with SCL low produce no framing |
| 6 | 80 idle cycles | nothing counted, no transfer opened |
Test 5 is the one that keeps the monitor honest. 0xAA is 1010 1010 and 0x55 is 0101 0101, so between them they move SDA sixteen times — every one with SCL low, every one legal data. A monitor that got the SCL condition wrong would invent eight starts and eight stops inside two bytes, and the failure would look like a wildly broken capture rather than like a detector bug.
Test 3c was added because a mutation survived without it, and the scenario it covers is real: after a STOP, SCL held low means somebody is driving the bus, so those cycles are not a window anyone could have used. Measuring them would overstate the exposure — and the whole value of the measurement is that it is honest about size.
7. Mutation Testing
Eight defects injected into the SystemVerilog monitor.
| # | injected defect | outcome |
|---|---|---|
| B1 | a repeated START is counted as a fresh START | killed — a held transaction reported 2 STARTs |
| B2 | framing detected on an SDA edge while SCL is low | killed — reported 11 STARTs in one transaction |
| B3 | only the current SCL sample is required, not both | killed — a held transaction reported 2 STARTs |
| B4 | START and STOP swapped | killed — 0 repeated STARTs, expected 1 |
| B5 | the gap timer is not reset by a START | killed — free_cycles stale inside a transfer |
| B6 | the gap counts even while the bus is not idle | killed — counted the 40 held-low cycles |
| B7 | the gap measurement is never captured | killed — a release measured zero free cycles |
| B8 | in_transfer is not cleared by a STOP | killed — still in a transfer after the STOP |
Eight injected, eight killed — with B5 and B6 requiring two new tests, both recorded here rather than smoothed over.
B5 and B6 survived the first version of the testbench and their common shape is instructive. Both concern the gap timer in situations the original stimulus never created: B5 needs somebody to read free_cycles during a transfer (the original only read it after a gap), and B6 needs a gap in which the bus is not idle (the original made every gap perfectly idle). Neither is exotic — a stale measurement and an inflated one are both ordinary instrument defects — and neither was reachable.
The generalisation is one this course has hit before, in Chapter 8.3 §9: a suite that only exercises the clean case cannot test the guards that define it. The gap timer has two guards, gap_running and bus_idle, and testing it only during clean idle gaps exercises neither.
B2 and B3 are the same mistake at two severities, which is why both are worth injecting. B2 inverts the SCL condition entirely and produces eleven STARTs in a two-byte transaction — unmistakable. B3 merely weakens it, requiring SCL high only on the current sample, and produces two STARTs where one belongs. The second is the realistic bug: it is a plausible simplification, it works for most edges, and it fails only when SCL and SDA move in the same cycle. Injecting the loud version alone would have left the quiet one untested.
8. Verification Connection — Assert the Junction, Cover the Gap
The junction rule is a property: it holds at every moment of every combined transaction, including ones written later by somebody who has not read this chapter.
// A direction change requires a re-addressing, and a re-addressing inside a
// transaction is a REPEATED START. So a transaction whose direction changes must
// show an Sr, never a STOP followed by a START.
//
// Note what this does NOT say: it says nothing about two adjacent transactions,
// because on the wire those are legal and indistinguishable (section 4). The
// property is only assertable because `txn_multi_phase` comes from the MASTER.
property p_junction_is_restart;
@(posedge clk) disable iff (!rst_n)
(txn_multi_phase && phase_complete && !txn_last_phase) |=> restart_det;
endproperty
assert property (p_junction_is_restart)
else $error("a phase junction used a STOP -- the bus was released mid-transaction");
// A repeated START is only meaningful inside a transfer. One detected while no
// transfer is open means the monitor's state and the bus disagree, which is worth
// an error in its own right: every count after it will be wrong.
property p_restart_only_inside_a_transfer;
@(posedge clk) disable iff (!rst_n)
restart_det |-> $past(in_transfer);
endproperty
assert property (p_restart_only_inside_a_transfer)
else $error("a repeated START was detected with no transfer open");
// Framing NEVER happens while SCL is low. This is the rule the whole detector rests
// on, asserted directly so a detector bug is caught at the source rather than as a
// wrong byte count several layers up.
property p_framing_requires_scl_high;
@(posedge clk) disable iff (!rst_n)
(start_det || restart_det || stop_det) |-> $past(scl_in) && scl_in;
endproperty
assert property (p_framing_requires_scl_high)
else $error("a framing event was detected while SCL was low");And the coverage, which for this chapter is about gap sizes — because the bug's probability is a function of the window.
covergroup i2c_gap_cg with function sample(int free_cycles, int n_starts_in_access,
bit multi_phase);
// THE coverpoint. A combined access that produced more than one fresh START
// released the bus. One is correct; two is the bug; more than two means several
// releases in one logical access.
starts_per_access: coverpoint n_starts_in_access {
bins held = {1}; // correct: one START, junctions are Sr
bins released = {2}; // the bug of this chapter
bins repeated = {[3:16]}; // released more than once
}
// Gap size buckets, chosen because the FAILURE PROBABILITY scales with them, not
// because they are evenly spaced. A sub-microsecond release is a latent bug that
// will bite rarely; a millisecond release will bite in the field this week.
gap: coverpoint free_cycles {
bins none = {0}; // a repeated START: no window at all
bins tiny = {[1:20]};
bins short = {[21:200]};
bins long = {[201:2000]};
bins very_long = {[2001:$]};
}
multi: coverpoint multi_phase;
// The cross is the point: a single-phase transfer legitimately has one START and
// a gap before it, while a MULTI-phase access with a gap in the middle is the
// bug. Covering gap size and phase count separately cannot tell them apart.
gap_x_multi: cross gap, multi;
starts_x_multi: cross starts_per_access, multi;
endgroup9. FPGA and ASIC Implications
The monitor is four counters and an edge detector. Two synchroniser flops for the wires, three event counters, a gap counter and a captured measurement, plus three state bits. At CNT_W = 16 that is roughly 90 flops — small enough to leave permanently enabled on any FPGA and negligible on an ASIC.
Size the gap counter for the longest gap you care to measure, not the longest possible. A gap counter that saturates reports a small number, which is the worst failure mode a measurement has (Chapter 9.3 §10 made the same point about pulse counters). At a 50 MHz system clock, sixteen bits is 1.3 ms — plenty for a junction gap and far too short for "how long has this bus been idle", so if you want the second measurement, use a second wider counter rather than stretching this one.
The two wires must be synchronised before they reach this block. It samples sda_in and scl_in on the system clock and compares consecutive samples, so both must already be clean single-clock-domain signals — the input stage Module 4 describes. Feeding raw pads in would let a metastable sample produce a phantom framing event, and a phantom START is exactly the kind of error that makes every subsequent count wrong.
bus_was_free is the output worth wiring to something. It pulses when a fresh START re-takes a bus that had been released, with the measured window on last_free_cycles beside it. On a single-master system that is normal and uninteresting. On a multi-master system, correlated with a master's own transaction state, it is the signal that catches the bug of this chapter — which is what Chapter 10.3 does with it.
This is a genuinely useful thing to ship. A permanently enabled monitor answers questions that are otherwise a logic-analyser session: is the bus being clocked, how many transactions per second, how long is it idle, and — the one specific to this chapter — did any access release the bus in the middle. None of that needs a probe on the board.
10. Debugging — The Sensor That Read Wrong Once a Week
Pitfall — a STOP-then-START junction on a bus that later gained a second master
// A temperature sensor driver, written and validated two years earlier on a board
// with a single I2C master. The register read was implemented as two calls:
//
// i2c_write(SENSOR, ®, 1); // phase 1: the pointer -- ends with a STOP
// i2c_read(SENSOR, buf, 2); // phase 2: a fresh START
//
// Both are ordinary library calls and each is individually correct. Together they
// put this on the wire:
//
// S 0xA0 A 0x00 A P <bus free, ~40 us> S 0xA1 A d0 A d1 N P
// ^ ^
// released here ... and re-taken here
//
// It worked flawlessly for two years, because there was only one master.
//
// Then a revision added a second controller on the same bus for a power-management
// IC -- which also, occasionally, reads that same sensor for a thermal limit.Roughly once a week, in the field, a unit logged a temperature about forty degrees off. One sample, never two in a row, then perfectly normal readings for days.
Every diagnostic said the bus was healthy. The driver reported success on every read. Retry counters were zero. The sensor passed every self-test. Swapping the sensor changed nothing, and neither did swapping the board.
It was not reproducible. A test rig ran the read in a tight loop for four days -- about thirty million reads -- with no bad sample at all, because the rig did not run the power-management firmware that was the other master.
The theory that held longest was electrical: a marginal pull-up, a noise event corrupting one byte. That theory is very hard to disprove from a log, and it predicts exactly what was observed -- a rare single-sample error.
What actually resolved it was a capture triggered on the sensor's address with a long buffer, looked at STRUCTURALLY rather than for signal quality. Most accesses showed S, P, S, P for what the source called one register read. And one capture showed, in the 40 us gap, the OTHER master's complete transaction: address, its own pointer write, a STOP. Then our read phase resumed, addressed the sensor, and read two bytes -- from the pointer the other master had just written.
Both transactions were legal. Nothing was corrupted. Our read returned a perfectly good value from the wrong register.
The junction between the two phases was a STOP, so the bus was released for about 40 microseconds. During that window the other master was entitled to take the bus, and occasionally did -- addressing the same device and overwriting the register pointer that phase 1 had just established.
The latent bug was two years old and harmless while the bus had one master. The hardware revision that added the second master did not introduce the bug; it made an existing one reachable. That is why nothing in the change log pointed at it.
11. Common Misconceptions
"A repeated START is a special kind of START." It is the same electrical event — SDA falling while SCL is high. What makes it "repeated" is that a transfer was already open. This is why a monitor must be stateful to classify one.
"STOP-then-START is just slower." It is also a release. On a single-master bus the difference is only time; on a shared one it opens a window in which another master may legitimately change the device's state, and both transactions remain perfectly legal.
"The bug would show up in testing." It needs two masters contending on the same device within tens of microseconds. That is rare enough to survive an entire development cycle and appear only in the field, with no error reported anywhere.
"Make the gap shorter and the race goes away." It becomes less likely, which is not the same thing. A repeated START makes the window exactly zero — a different kind of answer from reducing a probability.
"A monitor can detect a mid-transaction release from the wire." It cannot. S … P … S … P is two transactions on the wire, and nothing distinguishes that from two adjacent accesses. Detecting the fault needs the master's own intent, which is Chapter 10.3.
"All those SDA transitions inside a byte are near-misses for framing." They are legal data and are not near-misses at all, because they occur while SCL is low. The framing condition is an SDA edge while SCL is high, and §6's test 5 drives sixteen data transitions to prove the detector is not fooled.
"Any gap after a STOP is the exposure window." Only the genuinely idle part is. If SCL is being held low, nobody could have used the bus, so counting those cycles overstates the exposure — which matters because the measurement's value is that it is honest about size.
12. Reason It Through
A capture of what the source calls one register read shows two fresh STARTs and no repeated START. How confident can you be that this is a bug, and what would make you certain?
Structurally it is certain that the bus was released — two fresh STARTs means the first frame ended with a STOP. Whether that is a bug depends on intent: if the source performs one logical access, yes. The wire alone cannot settle it (§4), which is why the certain answer comes from comparing the capture against the driver's source, or from the tracker in Chapter 10.3 that takes the master's own transaction state as an input.
Why does the monitor need SCL high on two consecutive samples rather than one?
Because a framing event is an SDA edge while SCL is high, so SCL must be high across the edge. With only the current sample tested, a cycle in which SCL and SDA moved together reads as framing — and mutation B3 shows the consequence: a held transaction reporting two STARTs, because a data transition got promoted to a framing event. The quiet version of this bug is much more dangerous than the loud one.
After a STOP, SCL is held low for 200 µs, then the bus goes fully idle for 5 µs, then a START. What is the exposure window, and why?
5 µs. While SCL was held low the bus was being driven — somebody owned it — so no other master could have started a transfer. The window is the time the bus was genuinely available, which is why the gap timer requires both wires released rather than merely counting from the STOP.
A driver releases the bus for 2 µs between phases; another for 2 ms. Do they have the same bug?
Yes — identical defect, wildly different probability of being bitten. Both produce two fresh STARTs for one logical access. The 2 ms version will be found; the 2 µs version may sit latent for years and then surface as a "hardware regression" when a board revision adds a second master. This is why §8's coverage bins gap sizes by order of magnitude: the failure rate scales with the window, so the bins should too.
Why is free_cycles required to read zero during a transfer, when nothing in the design uses it?
Because it is an output, and its meaning is "the gap currently in progress". During a transfer there is no gap, so any nonzero value is a stale measurement that a consumer would misread as a live one. Mutation B5 leaves it stale and survives every check placed after a gap — the test that kills it has to read the output at a moment the original stimulus never examined.
13. Understanding Check
14. Summary
A START and a repeated START are the same electrical event. SDA falling while SCL is high. The difference is whether a transfer was already open, which is why classifying one requires state and why the two sequences in this chapter differ by exactly one framing event.
The difference is a STOP, and a STOP releases the bus. Three costs follow: a little time, possible loss of the device's register pointer, and — the one that matters — a window in which another master may legitimately take the bus and change the device's state, producing plausible wrong data with nothing reported.
The diagnostic is structural and costs one pass. One logical access should produce one fresh START and k repeated STARTs. Two fresh STARTs means the bus was released. Count STARTs, not STOPs, and measure the gap.
The wire records events, not intentions. S … P … S … P is two transactions, and no field, flag or timing distinguishes that from two adjacent accesses. A monitor can report facts and a measured window; turning those into a verdict needs the master's own state.
Measure only the time the bus was genuinely free. Cycles in which SCL is held low are time nobody could have used, and counting them overstates the exposure that the measurement exists to quantify.
A repeated START makes the window zero, not small. That is the difference between removing a race and making it rarer — and since framing costs no clock pulses, it is free.
15. What Comes Next
Chapter 10.3 closes the module by taking the input this chapter could not get from the wire. With the master's own transaction state alongside the framing events, "the bus was released mid-transaction" becomes a verdict rather than an observation — and the chapter defines atomicity precisely enough to enumerate everything that can break it.
Three things end bus ownership: a release, an arbitration loss (Module 13), and an abandonment where a master stops without a final STOP. One thing that looks as though it should does not: clock stretching, where a slave holding SCL low is the bus being owned rather than lost — the case most often got wrong, and the reason the tracker takes it as an explicit input and deliberately ignores it.
There is also a fourth condition the chapter can express that this one cannot: whether the register pointer written in phase 1 is still trustworthy. That turns out to depend not on whether ownership was lost, but on when — and the distinction is worth one flip-flop.
Continue learning
Related tutorials
- Related topic
Repeated START — Holding the Bus Between Phases
A repeated START is not a new waveform. It is the START edge again, and what makes it a different event is that the bus was already busy. That single fact is why a classifier needs state and why a monitor that joins late cannot classify what it sees.
- Related topic
10-Bit Addressing
A two-byte addressing mode built entirely out of reserved space, coexisting with seven-bit devices on the same wires. Its first byte is deliberately not unique, and a read has to re-address with only one byte — which is why a 10-bit slave needs memory a 7-bit slave does not.
- Related topic
tSU;STA and tHD;STA — START and Repeated-START Margins
The first parameters measured between two different signals rather than against a clock edge — and the timing-level reason a repeated START is not simply a START in the middle of a transfer.
- Related topic
tSU;STO and tBUF — STOP and Bus-Free Time
The only parameter in Table 10 measured between two transactions rather than inside one — which makes it the only one a single transfer cannot violate, and the one a busy multi-master bus violates most often.
