I²C · Module 10
I²C Transaction Atomicity and Bus Ownership Across Phases
What is and is not atomic on an I²C bus, stated precisely. Three things end bus ownership and one that looks like it should does not — and telling them apart needs one input the wire cannot supply.
Chapter 10.2 ended on a limit: a bus monitor can report that the bus was released, and cannot report that releasing it was a mistake. On the wire, a STOP followed by a START is two transactions — and that is exactly what it is.
This chapter closes the gap by adding what the wire cannot supply, and in doing so has to answer a question the earlier chapters have been leaning on without defining: what does atomic actually mean on this bus?
The answer is narrower than people assume, and the cases it excludes are more interesting than the ones it includes.
1. Atomicity, Stated Precisely
Start by discarding the word's usual connotations. I²C offers no transactions in the database sense: nothing is journalled, nothing rolls back, and Chapter 8.1 §4 established that a partially-completed write stays partially completed forever. So "atomic" here cannot mean all-or-nothing.
What it does mean is this:
A multi-phase transaction is atomic if the bus was continuously owned by this master from its first START to its final STOP.
Nothing about the data. Nothing about success. Just ownership, and continuity of ownership. Because ownership is the only thing the protocol actually guarantees: while you hold the bus, no other master may address any device on it, so nothing can change underneath you.
And that guarantee is exactly what a combined transaction needs. Chapter 10.1 writes a pointer and then reads from it, and the correctness of the read depends entirely on the pointer still being what phase 1 set — which is guaranteed if and only if nobody else could have addressed the device in between.
2. What the Protocol Does and Does Not Guarantee
Worth tabulating, because the boundary is not where intuition puts it.
| question | guaranteed? | why |
|---|---|---|
| While I hold the bus, can another master address my device? | no | the bus is not free; a START requires an idle bus |
| Between my phases joined by an Sr, can the pointer change? | no | same reason — the bus was never released |
| Between my phases joined by a P and an S, can it change? | yes | the bus was free; any master was entitled to take it |
| If I am NACKed partway, do earlier bytes un-happen? | no | there is no rollback — 8.1 §4 |
| If a slave stretches SCL, do I still own the bus? | yes | SCL held low is the bus being held |
| If I lose arbitration, do I still own the bus? | no | it is now somebody else's |
| If I reset mid-transaction, is the bus released? | no | a reset stops driving; it does not emit a STOP |
| Is a single byte transfer atomic? | yes, trivially | it cannot be interrupted between its own bits |
Two rows deserve comment.
"If I reset mid-transaction, is the bus released?" — no, and this is the worst of the failure modes. Resetting a master clears its outputs; it does not generate a STOP, because generating a STOP is a bus operation and the block that would perform it is the block being reset. So the bus is left in whatever state the last driven bit put it — quite possibly with SDA low — and no master is finishing the transaction. Chapter 10.1 §10 flagged this; §3 classifies it.
"Is a single byte transfer atomic?" — yes, and that is why this module exists. Single-phase transfers are atomic for free: there is no junction at which anything could interpose. Atomicity only becomes a design concern when a transaction has more than one phase, which is precisely the combined format.
3. Three Ways Ownership Ends, and One That Does Not
| # | event | ownership | severity | what happens next |
|---|---|---|---|---|
| 1 | released — a STOP before the phases are done | lost — bus is free | high | another master may change the device's state |
| 2 | arbitration lost — another master won | lost — bus is theirs | high | the transaction never completed at all |
| 3 | abandoned — finished with no final STOP | stuck — bus is held | highest | nobody can use the bus until recovery |
| — | clock stretching — a slave holds SCL low | retained | none | the transfer continues, slower |
The ordering by severity is worth defending, because it is not the ordering by how wrong the code is.
Released is bad because it is quiet. Both resulting transactions are legal, every byte is acknowledged, and the damage is a plausible wrong value. Chapter 10.2 §10 is that fault in the field, once a week for months.
Arbitration loss is bad but honest. The master knows. Module 13 builds the detection; the point here is that a master that loses arbitration has been told, so it can retry the whole transaction from the start. The information exists.
Abandonment is the worst, because it takes the bus away from everybody. A released bus is at least usable. An abandoned transaction leaves SDA or SCL held with no master driving toward completion, and every other device on that bus is now locked out — a single-device bug that becomes a system-wide outage. Recovering needs the mechanism in Chapter 5.5, which is outside the protocol.
And clock stretching is not on the list. A slave holding SCL low has taken the bus from nobody; it has held it. No other master can start a transfer, because starting one requires an idle bus. So a transaction stretched for ten milliseconds is exactly as atomic as one that was not stretched at all — it merely took longer.
4. Where a Combined Transaction Can Still Be Interrupted
Given all that, walk the canonical write-then-read and ask at each interval whether anything can get in.
Ownership across a combined transaction
10 cyclesThe can interpose row reads no for eight consecutive intervals, and that is the guarantee. Notice what it is not saying:
It is not saying nothing can go wrong. The device can NACK the pointer. It can NACK the read addressing. It can return fill bytes past the end of its register file (Chapter 9.2 §3). It can stretch the clock for ten milliseconds. All of those are possible inside the owned span, and none of them is an atomicity failure — they are ordinary protocol outcomes.
It is not saying the master cannot break it itself. Every one of §3's three breaks is something this master does or suffers: it emits a STOP too early, it loses arbitration, or it stops driving. Atomicity is not something the bus enforces on your behalf; it is something a correct master maintains.
And it is not saying the span is short. With stretching, those eight intervals can take milliseconds. Ownership is about exclusivity, not duration.
5. The Tracker in Three Languages
The tracker takes the framing events from Chapter 10.2's monitor and three signals the wire cannot supply, and produces a verdict per transaction.
| input | why it cannot come from the wire |
|---|---|
txn_active | whether this master considers a transaction in progress — 10.1's bus_held |
expect_phases | how many addressings the transaction should contain; a write-then-read has two |
txn_aborted | the master is terminating early on purpose — which looks identical to a release |
arb_lost | this master lost arbitration (Module 13) |
scl_stretched | a slave is holding SCL low — counted, and deliberately not a break |
// An ATOMICITY tracker for multi-phase transactions.
//
// Chapter 10.2's monitor reports facts about the wire and deliberately cannot report
// intent: on the wire, a STOP followed by a START IS two separate transactions, and
// nothing distinguishes that from a driver that wrongly let the bus go. This block
// supplies the missing half by taking what the master BELIEVED it was doing and
// correlating it against what the wire actually showed.
//
// The definition it enforces is narrow and worth stating exactly:
//
// A multi-phase transaction is ATOMIC if the bus was continuously owned by this
// master from the first START to the final STOP.
//
// "Continuously owned" is not the same as "continuously busy", and that distinction is
// the whole content of this chapter. Three things end ownership and one does not:
//
// RELEASED a STOP before every phase had been done, with no abort to explain it.
// The bus is free and any other master may take it -- and in the
// register-pointer pattern of Chapter 10.1 may change the pointer this
// master just wrote.
// ARBITRATION this master lost the bus to another one. The bus did not become free,
// it became somebody else's, and the transaction never completed.
// ABANDONED the master finished without a final STOP at all -- a reset, a timeout,
// a driver that gave up. The bus is left HELD with nobody to finish it,
// which strands every other device on the bus.
//
// STRETCHING does NOT break atomicity. A slave holding SCL low slows the transfer
// and changes nothing about ownership: SCL held low IS the bus being
// owned. This is the case people most often get wrong, so it is an
// input here and an explicit test in the testbench.
//
// THREE INPUTS CARRY INTENT, and each exists because the wire cannot supply it:
//
// expect_phases how many addressings this transaction should contain. A write-then-
// read has two. Without it, a STOP after one phase is indistinguishable
// from a completed single-phase transfer.
// txn_aborted the master is terminating early ON PURPOSE -- Chapter 10.1's
// sequencer does exactly this when an address or pointer is NACKed. An
// abort MUST release the bus, so it produces the same wire event as the
// fault above and is only distinguishable by this signal.
// arb_lost this master lost arbitration (Module 11).
module i2c_atomicity_tracker #(
parameter int CNT_W = 16,
parameter int PH_W = 4 // phases per transaction, counter width
)(
input logic clk,
input logic rst_n,
// ---- framing events, from the monitor of Chapter 10.2 ----
input logic start_det, // pulse: a fresh START
input logic restart_det, // pulse: a repeated START -- a new phase, same ownership
input logic stop_det, // pulse: a STOP -- ownership ends here
// ---- what THIS master believes ----
input logic txn_active, // Chapter 10.1's bus_held
input logic [PH_W-1:0] expect_phases, // addressings this transaction should contain
input logic txn_aborted, // terminating early on purpose
input logic arb_lost, // pulse: lost arbitration (Module 11)
input logic scl_stretched, // a slave holds SCL low. NOT a break.
// ---- verdict ----
output logic [1:0] break_reason,
output logic atomic, // the transaction in progress is still atomic
output logic [PH_W-1:0] phases, // addressings seen in this transaction
output logic verdict_valid, // pulse: a transaction just concluded
// The consequence that matters in the register-pointer pattern: if ownership was
// lost AFTER a phase had already completed, the pointer this master wrote may have
// been changed by whoever took the bus. The data read afterwards is then from an
// unknown location -- and nothing on the bus reports that.
output logic pointer_suspect,
output logic [CNT_W-1:0] n_atomic,
output logic [CNT_W-1:0] n_broken,
output logic [CNT_W-1:0] n_aborted, // clean early terminations, not faults
output logic [CNT_W-1:0] n_stretch_events // counted, and deliberately harmless
);
localparam logic [1:0] BR_NONE = 2'd0;
localparam logic [1:0] BR_RELEASED = 2'd1;
localparam logic [1:0] BR_ARB_LOST = 2'd2;
localparam logic [1:0] BR_ABANDONED = 2'd3;
logic txn_active_q;
logic stretch_q; // so stretch EVENTS are counted, not stretched cycles
logic tracking; // inside a transaction this block is judging
logic saw_stop; // a STOP has been seen in this transaction
logic was_aborted; // the master declared an abort during this transaction
always_ff @(posedge clk) begin
if (!rst_n) begin
break_reason <= BR_NONE;
atomic <= 1'b1;
phases <= '0;
verdict_valid <= 1'b0;
pointer_suspect <= 1'b0;
n_atomic <= '0;
n_broken <= '0;
n_aborted <= '0;
n_stretch_events <= '0;
txn_active_q <= 1'b0;
stretch_q <= 1'b0;
tracking <= 1'b0;
saw_stop <= 1'b0;
was_aborted <= 1'b0;
end else begin
verdict_valid <= 1'b0;
txn_active_q <= txn_active;
stretch_q <= scl_stretched;
// Stretching is COUNTED so a transaction's duration can be explained, and it
// deliberately touches neither `atomic` nor `break_reason`. A slave holding
// SCL low has taken the bus from nobody -- it IS the bus being held, which is
// the opposite of the bus being free.
if (scl_stretched && !stretch_q) n_stretch_events <= n_stretch_events + 1'b1;
if (txn_active && !txn_active_q) begin
// ---- a transaction begins ----
tracking <= 1'b1;
atomic <= 1'b1;
break_reason <= BR_NONE;
phases <= '0;
pointer_suspect <= 1'b0;
saw_stop <= 1'b0;
was_aborted <= 1'b0;
end else if (tracking) begin
// Each addressing is a phase. A fresh START opens the first; each
// repeated START opens another WITHOUT surrendering ownership, which is
// exactly why a combined transaction is one transaction.
if (start_det || restart_det) phases <= phases + 1'b1;
if (txn_aborted) was_aborted <= 1'b1;
if (arb_lost) begin
// Losing arbitration ends ownership immediately. It takes priority
// over a release because it is the more specific explanation: the
// bus did not become free, it became somebody else's.
atomic <= 1'b0;
break_reason <= BR_ARB_LOST;
if (phases != '0) pointer_suspect <= 1'b1;
end else if (stop_det) begin
saw_stop <= 1'b1;
// A STOP before every phase has been done RELEASES the bus. Unless
// an abort explains it -- an abort must release the bus, so it
// produces the identical wire event and is distinguishable only by
// the master telling us.
if ((phases < expect_phases) && !(txn_aborted || was_aborted)) begin
atomic <= 1'b0;
break_reason <= BR_RELEASED;
if (phases != '0) pointer_suspect <= 1'b1;
end
end
end
// ---- a transaction concludes ----
if (tracking && !txn_active && txn_active_q) begin
tracking <= 1'b0;
verdict_valid <= 1'b1;
if (break_reason != BR_NONE) begin
n_broken <= n_broken + 1'b1;
end else if (!(saw_stop || stop_det)) begin
// Finished with no STOP at all. The bus is left HELD and nobody is
// going to finish the transaction -- the worst of the three, because
// it strands every other device rather than only corrupting this
// master's own data.
atomic <= 1'b0;
break_reason <= BR_ABANDONED;
n_broken <= n_broken + 1'b1;
if (phases != '0) pointer_suspect <= 1'b1;
end else if (was_aborted || txn_aborted) begin
// A clean early termination. Not atomic in the sense of having
// completed, and not a FAULT either -- counted separately so a
// report cannot confuse "the device was absent" with "the driver
// released the bus".
n_aborted <= n_aborted + 1'b1;
end else begin
n_atomic <= n_atomic + 1'b1;
end
end
end
end
endmodule `timescale 1ns/1ps
module i2c_atomicity_tracker_tb;
localparam int CNT_W = 16;
localparam int PH_W = 4;
localparam logic [1:0] BR_NONE = 2'd0;
localparam logic [1:0] BR_RELEASED = 2'd1;
localparam logic [1:0] BR_ARB_LOST = 2'd2;
localparam logic [1:0] BR_ABANDONED = 2'd3;
logic clk = 1'b0;
always #5 clk = ~clk;
logic rst_n = 1'b0;
logic start_det = 1'b0, restart_det = 1'b0, stop_det = 1'b0;
logic txn_active = 1'b0, txn_aborted = 1'b0, arb_lost = 1'b0, scl_stretched = 1'b0;
logic [PH_W-1:0] expect_phases = 4'd2;
logic [1:0] break_reason;
logic atomic, verdict_valid, pointer_suspect;
logic [PH_W-1:0] phases;
logic [CNT_W-1:0] n_atomic, n_broken, n_aborted, n_stretch_events;
int errors = 0;
i2c_atomicity_tracker #(.CNT_W(CNT_W), .PH_W(PH_W)) dut (.*);
initial begin #200000; $display("FAIL: watchdog expired"); $finish; end
int n_verdict;
always @(posedge clk) if (rst_n && verdict_valid) n_verdict++;
// ---- stimulus primitives -------------------------------------------------------
task automatic pulse_s(); start_det = 1'b1; @(negedge clk); start_det = 1'b0; @(negedge clk); endtask
task automatic pulse_sr(); restart_det = 1'b1; @(negedge clk); restart_det = 1'b0; @(negedge clk); endtask
task automatic pulse_p(); stop_det = 1'b1; @(negedge clk); stop_det = 1'b0; @(negedge clk); endtask
task automatic pulse_arb(); arb_lost = 1'b1; @(negedge clk); arb_lost = 1'b0; @(negedge clk); endtask
// A slave stretches SCL for n cycles. It is a real event and it is NOT a break.
task automatic stretch_for(input int n);
scl_stretched = 1'b1; repeat (n) @(negedge clk); scl_stretched = 1'b0; @(negedge clk);
endtask
task automatic begin_txn(input logic [PH_W-1:0] ph);
expect_phases = ph;
txn_active = 1'b1; @(negedge clk); @(negedge clk);
endtask
task automatic end_txn();
txn_active = 1'b0; @(negedge clk); @(negedge clk);
endtask
initial begin
repeat (3) @(negedge clk);
if (atomic !== 1'b1) begin $display("FAIL: atomic should default true"); errors++; end
rst_n = 1'b1; @(negedge clk);
// ---- 1: a HEALTHY write-then-read. Two phases, joined by a repeated START,
// ending with one STOP. This must be atomic.
begin_txn(4'd2);
pulse_s(); // phase 1: the write addressing
pulse_sr(); // phase 2: the read addressing -- ownership retained
pulse_p(); // the final STOP
end_txn();
if (atomic !== 1'b1) begin
$display("FAIL: a correct write-then-read was reported as non-atomic"); errors++; end
if (break_reason !== BR_NONE) begin
$display("FAIL: break_reason = %0d on a healthy transaction", break_reason);
errors++; end
if (phases !== 4'd2) begin
$display("FAIL: counted %0d phases, expected 2", phases); errors++; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect on a healthy transaction"); errors++; end
if (n_atomic !== 16'd1 || n_broken !== 16'd0) begin
$display("FAIL: counts atomic=%0d broken=%0d, expected 1,0", n_atomic, n_broken);
errors++; end
// ---- 2: CLOCK STRETCHING does not break atomicity. This is the case most
// often got wrong: the slave holding SCL low IS the bus being owned.
begin_txn(4'd2);
pulse_s();
stretch_for(20); // a long stall, mid-transaction
pulse_sr();
stretch_for(35); // and another, in the read phase
pulse_p();
end_txn();
if (atomic !== 1'b1) begin
$display("FAIL: clock stretching was treated as a loss of atomicity"); errors++; end
if (break_reason !== BR_NONE) begin
$display("FAIL: stretching set break_reason = %0d", break_reason); errors++; end
if (n_stretch_events !== 16'd2) begin
$display("FAIL: counted %0d stretch events, expected 2", n_stretch_events);
errors++; end
if (n_atomic !== 16'd2) begin
$display("FAIL: a stretched transaction was not counted as atomic"); errors++; end
// ---- 3: the BUS WAS RELEASED between the phases. A STOP arrived after only
// one of two phases, with no abort to explain it. This is the fault the
// whole module is about, and the pointer is now suspect.
begin_txn(4'd2);
pulse_s(); // phase 1 done: the pointer has been written
pulse_p(); // ... and the bus is RELEASED
pulse_s(); // re-taken as a fresh START
pulse_p();
end_txn();
if (atomic !== 1'b0) begin
$display("FAIL: a released transaction was reported atomic"); errors++; end
if (break_reason !== BR_RELEASED) begin
$display("FAIL: break_reason = %0d, expected %0d (released)",
break_reason, BR_RELEASED); errors++; end
// THE consequence. A phase had completed, so a pointer had been written, so
// whoever took the free bus could have changed it.
if (pointer_suspect !== 1'b1) begin
$display("FAIL: pointer_suspect not raised after a mid-transaction release");
errors++; end
if (n_broken !== 16'd1) begin
$display("FAIL: n_broken = %0d, expected 1", n_broken); errors++; end
// ---- 4: ARBITRATION LOST. Ownership did not become free, it became somebody
// else's -- a more specific explanation, so it must win over a release.
begin_txn(4'd2);
pulse_s();
pulse_arb();
end_txn();
if (atomic !== 1'b0) begin
$display("FAIL: an arbitration loss was reported atomic"); errors++; end
if (break_reason !== BR_ARB_LOST) begin
$display("FAIL: break_reason = %0d, expected %0d (arbitration)",
break_reason, BR_ARB_LOST); errors++; end
if (pointer_suspect !== 1'b1) begin
$display("FAIL: pointer_suspect not raised after an arbitration loss"); errors++; end
// ---- 5: ABANDONED. The master stopped without emitting a STOP at all, so the
// bus is left HELD -- the worst of the three, because it strands every
// other device rather than only corrupting this master's own data.
begin_txn(4'd2);
pulse_s();
pulse_sr();
end_txn(); // no STOP
if (atomic !== 1'b0) begin
$display("FAIL: an abandoned transaction was reported atomic"); errors++; end
if (break_reason !== BR_ABANDONED) begin
$display("FAIL: break_reason = %0d, expected %0d (abandoned)",
break_reason, BR_ABANDONED); errors++; end
if (n_broken !== 16'd3) begin
$display("FAIL: n_broken = %0d, expected 3", n_broken); errors++; end
// ---- 6: a CLEAN ABORT. The device was absent, so the master terminated early
// ON PURPOSE with a STOP. On the wire this is IDENTICAL to test 3 -- one
// phase, then a STOP -- and only the master's own signal distinguishes
// them. It must NOT be counted as a fault.
begin_txn(4'd2);
pulse_s();
txn_aborted = 1'b1; @(negedge clk);
pulse_p();
txn_aborted = 1'b0;
end_txn();
if (break_reason !== BR_NONE) begin
$display("FAIL: a clean abort was reported as break_reason %0d", break_reason);
errors++; end
if (n_aborted !== 16'd1) begin
$display("FAIL: n_aborted = %0d, expected 1", n_aborted); errors++; end
if (n_broken !== 16'd3) begin
$display("FAIL: a clean abort was counted as broken (n_broken = %0d)", n_broken);
errors++; end
// ---- 7: a THREE-phase transaction. Exposure is a property of the junctions,
// not of the length -- three phases held is exactly as atomic as two.
begin_txn(4'd3);
pulse_s(); pulse_sr(); pulse_sr(); pulse_p();
end_txn();
if (atomic !== 1'b1) begin
$display("FAIL: a three-phase held transaction was reported non-atomic"); errors++; end
if (phases !== 4'd3) begin
$display("FAIL: counted %0d phases, expected 3", phases); errors++; end
if (n_atomic !== 16'd3) begin
$display("FAIL: n_atomic = %0d, expected 3", n_atomic); errors++; end
// ---- 8: a release BEFORE any phase completed. The bus was let go, but no
// pointer had been written yet, so there is nothing to have been changed.
// pointer_suspect must stay CLEAR -- the flag means something specific.
begin_txn(4'd2);
pulse_p(); // a STOP with zero phases seen
end_txn();
if (break_reason !== BR_RELEASED) begin
$display("FAIL: a zero-phase release was not reported as released"); errors++; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect raised when no phase had completed"); errors++; end
// ---- 8b: ARBITRATION lost before any phase completed. Ownership was lost,
// so the transaction is broken -- but no pointer had been written, so
// pointer_suspect must stay CLEAR. The flag means one specific thing.
begin_txn(4'd2);
pulse_arb(); // zero phases seen
end_txn();
if (break_reason !== BR_ARB_LOST) begin
$display("FAIL: a zero-phase arbitration loss was not reported"); errors++; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect raised on an arbitration loss with no phase done");
errors++; end
// ---- 9: the verdict is a PULSE, one per transaction, and the flags clear on
// the next transaction so a stale verdict cannot be read as a fresh one.
begin_txn(4'd2);
pulse_s(); pulse_sr(); pulse_p();
end_txn();
if (break_reason !== BR_NONE || atomic !== 1'b1) begin
$display("FAIL: a verdict survived into the next transaction"); errors++; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect survived into the next transaction"); errors++; end
if (n_verdict !== 10) begin
$display("FAIL: %0d verdict pulses across 10 transactions", n_verdict); errors++; end
// ---- 10: stretching OUTSIDE a transaction is still counted and still harmless.
begin
logic [CNT_W-1:0] st_before;
st_before = n_stretch_events;
stretch_for(15);
if (n_stretch_events !== st_before + 16'd1) begin
$display("FAIL: a stretch outside a transaction was not counted"); errors++; end
if (break_reason !== BR_NONE) begin
$display("FAIL: an idle-bus stretch set a break reason"); errors++; end
end
if (errors == 0)
$display("PASS: held transactions atomic at any length, stretching harmless, release and arbitration and abandonment distinguished, a clean abort is not a fault");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule // An ATOMICITY tracker for multi-phase transactions. (Verilog-2001)
//
// Chapter 10.2's monitor reports facts about the wire and deliberately cannot report
// intent: on the wire, a STOP followed by a START IS two separate transactions, and
// nothing distinguishes that from a driver that wrongly let the bus go. This block
// supplies the missing half by taking what the master BELIEVED it was doing and
// correlating it against what the wire actually showed.
//
// The definition it enforces is narrow and worth stating exactly:
//
// A multi-phase transaction is ATOMIC if the bus was continuously owned by this
// master from the first START to the final STOP.
//
// "Continuously owned" is not the same as "continuously busy", and that distinction is
// the whole content of this chapter. Three things end ownership and one does not:
//
// RELEASED a STOP before every phase had been done, with no abort to explain it.
// The bus is free and any other master may take it -- and in the
// register-pointer pattern of Chapter 10.1 may change the pointer this
// master just wrote.
// ARBITRATION this master lost the bus to another one. The bus did not become free,
// it became somebody else's, and the transaction never completed.
// ABANDONED the master finished without a final STOP at all -- a reset, a timeout,
// a driver that gave up. The bus is left HELD with nobody to finish it,
// which strands every other device on the bus.
//
// STRETCHING does NOT break atomicity. A slave holding SCL low slows the transfer
// and changes nothing about ownership: SCL held low IS the bus being
// owned. This is the case people most often get wrong, so it is an
// input here and an explicit test in the testbench.
//
// THREE INPUTS CARRY INTENT, and each exists because the wire cannot supply it:
//
// expect_phases how many addressings this transaction should contain. A write-then-
// read has two. Without it, a STOP after one phase is indistinguishable
// from a completed single-phase transfer.
// txn_aborted the master is terminating early ON PURPOSE -- Chapter 10.1's
// sequencer does exactly this when an address or pointer is NACKed. An
// abort MUST release the bus, so it produces the same wire event as the
// fault above and is only distinguishable by this signal.
// arb_lost this master lost arbitration (Module 11).
module i2c_atomicity_tracker #(
parameter CNT_W = 16,
parameter PH_W = 4 // phases per transaction, counter width
)(
input wire clk,
input wire rst_n,
// ---- framing events, from the monitor of Chapter 10.2 ----
input wire start_det, // pulse: a fresh START
input wire restart_det, // pulse: a repeated START -- a new phase, same ownership
input wire stop_det, // pulse: a STOP -- ownership ends here
// ---- what THIS master believes ----
input wire txn_active, // Chapter 10.1's bus_held
input wire [PH_W-1:0] expect_phases, // addressings this transaction should contain
input wire txn_aborted, // terminating early on purpose
input wire arb_lost, // pulse: lost arbitration (Module 11)
input wire scl_stretched, // a slave holds SCL low. NOT a break.
// ---- verdict ----
output reg [1:0] break_reason,
output reg atomic, // the transaction in progress is still atomic
output reg [PH_W-1:0] phases, // addressings seen in this transaction
output reg verdict_valid, // pulse: a transaction just concluded
// The consequence that matters in the register-pointer pattern: if ownership was
// lost AFTER a phase had already completed, the pointer this master wrote may have
// been changed by whoever took the bus. The data read afterwards is then from an
// unknown location -- and nothing on the bus reports that.
output reg pointer_suspect,
output reg [CNT_W-1:0] n_atomic,
output reg [CNT_W-1:0] n_broken,
output reg [CNT_W-1:0] n_aborted, // clean early terminations, not faults
output reg [CNT_W-1:0] n_stretch_events // counted, and deliberately harmless
);
localparam [1:0] BR_NONE = 2'd0;
localparam [1:0] BR_RELEASED = 2'd1;
localparam [1:0] BR_ARB_LOST = 2'd2;
localparam [1:0] BR_ABANDONED = 2'd3;
reg txn_active_q;
reg stretch_q; // so stretch EVENTS are counted, not stretched cycles
reg tracking; // inside a transaction this block is judging
reg saw_stop; // a STOP has been seen in this transaction
reg was_aborted; // the master declared an abort during this transaction
always @(posedge clk) begin
if (!rst_n) begin
break_reason <= BR_NONE;
atomic <= 1'b1;
phases <= {PH_W{1'b0}};
verdict_valid <= 1'b0;
pointer_suspect <= 1'b0;
n_atomic <= {CNT_W{1'b0}};
n_broken <= {CNT_W{1'b0}};
n_aborted <= {CNT_W{1'b0}};
n_stretch_events <= {CNT_W{1'b0}};
txn_active_q <= 1'b0;
stretch_q <= 1'b0;
tracking <= 1'b0;
saw_stop <= 1'b0;
was_aborted <= 1'b0;
end else begin
verdict_valid <= 1'b0;
txn_active_q <= txn_active;
stretch_q <= scl_stretched;
// Stretching is COUNTED so a transaction's duration can be explained, and it
// deliberately touches neither `atomic` nor `break_reason`. A slave holding
// SCL low has taken the bus from nobody -- it IS the bus being held, which is
// the opposite of the bus being free.
if (scl_stretched && !stretch_q) n_stretch_events <= n_stretch_events + 1'b1;
if (txn_active && !txn_active_q) begin
// ---- a transaction begins ----
tracking <= 1'b1;
atomic <= 1'b1;
break_reason <= BR_NONE;
phases <= {PH_W{1'b0}};
pointer_suspect <= 1'b0;
saw_stop <= 1'b0;
was_aborted <= 1'b0;
end else if (tracking) begin
// Each addressing is a phase. A fresh START opens the first; each
// repeated START opens another WITHOUT surrendering ownership, which is
// exactly why a combined transaction is one transaction.
if (start_det || restart_det) phases <= phases + 1'b1;
if (txn_aborted) was_aborted <= 1'b1;
if (arb_lost) begin
// Losing arbitration ends ownership immediately. It takes priority
// over a release because it is the more specific explanation: the
// bus did not become free, it became somebody else's.
atomic <= 1'b0;
break_reason <= BR_ARB_LOST;
if (phases != {PH_W{1'b0}}) pointer_suspect <= 1'b1;
end else if (stop_det) begin
saw_stop <= 1'b1;
// A STOP before every phase has been done RELEASES the bus. Unless
// an abort explains it -- an abort must release the bus, so it
// produces the identical wire event and is distinguishable only by
// the master telling us.
if ((phases < expect_phases) && !(txn_aborted || was_aborted)) begin
atomic <= 1'b0;
break_reason <= BR_RELEASED;
if (phases != {PH_W{1'b0}}) pointer_suspect <= 1'b1;
end
end
end
// ---- a transaction concludes ----
if (tracking && !txn_active && txn_active_q) begin
tracking <= 1'b0;
verdict_valid <= 1'b1;
if (break_reason != BR_NONE) begin
n_broken <= n_broken + 1'b1;
end else if (!(saw_stop || stop_det)) begin
// Finished with no STOP at all. The bus is left HELD and nobody is
// going to finish the transaction -- the worst of the three, because
// it strands every other device rather than only corrupting this
// master's own data.
atomic <= 1'b0;
break_reason <= BR_ABANDONED;
n_broken <= n_broken + 1'b1;
if (phases != {PH_W{1'b0}}) pointer_suspect <= 1'b1;
end else if (was_aborted || txn_aborted) begin
// A clean early termination. Not atomic in the sense of having
// completed, and not a FAULT either -- counted separately so a
// report cannot confuse "the device was absent" with "the driver
// released the bus".
n_aborted <= n_aborted + 1'b1;
end else begin
n_atomic <= n_atomic + 1'b1;
end
end
end
end
endmodule `timescale 1ns/1ps
module i2c_atomicity_tracker_tb; // Verilog-2001
localparam CNT_W = 16;
localparam PH_W = 4;
localparam [1:0] BR_NONE = 2'd0;
localparam [1:0] BR_RELEASED = 2'd1;
localparam [1:0] BR_ARB_LOST = 2'd2;
localparam [1:0] BR_ABANDONED = 2'd3;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b0;
reg start_det = 1'b0, restart_det = 1'b0, stop_det = 1'b0;
reg txn_active = 1'b0, txn_aborted = 1'b0, arb_lost = 1'b0, scl_stretched = 1'b0;
reg [PH_W-1:0] expect_phases = 4'd2;
wire [1:0] break_reason;
wire atomic, verdict_valid, pointer_suspect;
wire [PH_W-1:0] phases;
wire [CNT_W-1:0] n_atomic, n_broken, n_aborted, n_stretch_events;
integer errors = 0;
reg [CNT_W-1:0] st_before;
i2c_atomicity_tracker #(.CNT_W(CNT_W), .PH_W(PH_W)) dut (
.clk(clk), .rst_n(rst_n), .start_det(start_det), .restart_det(restart_det),
.stop_det(stop_det), .txn_active(txn_active), .expect_phases(expect_phases),
.txn_aborted(txn_aborted), .arb_lost(arb_lost), .scl_stretched(scl_stretched),
.break_reason(break_reason), .atomic(atomic), .phases(phases),
.verdict_valid(verdict_valid), .pointer_suspect(pointer_suspect),
.n_atomic(n_atomic), .n_broken(n_broken), .n_aborted(n_aborted),
.n_stretch_events(n_stretch_events));
initial begin #200000; $display("FAIL: watchdog expired"); $finish; end
integer n_verdict = 0;
always @(posedge clk) if (rst_n && verdict_valid) n_verdict = n_verdict + 1;
// ---- stimulus primitives -------------------------------------------------------
task pulse_s; begin start_det = 1'b1; @(negedge clk); start_det = 1'b0; @(negedge clk); end endtask
task pulse_sr; begin restart_det = 1'b1; @(negedge clk); restart_det = 1'b0; @(negedge clk); end endtask
task pulse_p; begin stop_det = 1'b1; @(negedge clk); stop_det = 1'b0; @(negedge clk); end endtask
task pulse_arb; begin arb_lost = 1'b1; @(negedge clk); arb_lost = 1'b0; @(negedge clk); end endtask
// A slave stretches SCL for n cycles. It is a real event and it is NOT a break.
task stretch_for;
input integer n;
begin
scl_stretched = 1'b1; repeat (n) @(negedge clk);
scl_stretched = 1'b0; @(negedge clk);
end
endtask
task begin_txn;
input [PH_W-1:0] ph;
begin
expect_phases = ph;
txn_active = 1'b1; @(negedge clk); @(negedge clk);
end
endtask
task end_txn; begin
txn_active = 1'b0; @(negedge clk); @(negedge clk);
end endtask
initial begin
repeat (3) @(negedge clk);
if (atomic !== 1'b1) begin $display("FAIL: atomic should default true"); errors = errors + 1; end
rst_n = 1'b1; @(negedge clk);
// ---- 1: a HEALTHY write-then-read. Two phases, joined by a repeated START,
// ending with one STOP. This must be atomic.
begin_txn(4'd2);
pulse_s; // phase 1: the write addressing
pulse_sr; // phase 2: the read addressing -- ownership retained
pulse_p; // the final STOP
end_txn;
if (atomic !== 1'b1) begin
$display("FAIL: a correct write-then-read was reported as non-atomic"); errors = errors + 1; end
if (break_reason !== BR_NONE) begin
$display("FAIL: break_reason = %0d on a healthy transaction", break_reason);
errors = errors + 1; end
if (phases !== 4'd2) begin
$display("FAIL: counted %0d phases, expected 2", phases); errors = errors + 1; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect on a healthy transaction"); errors = errors + 1; end
if (n_atomic !== 16'd1 || n_broken !== 16'd0) begin
$display("FAIL: counts atomic=%0d broken=%0d, expected 1,0", n_atomic, n_broken);
errors = errors + 1; end
// ---- 2: CLOCK STRETCHING does not break atomicity. This is the case most
// often got wrong: the slave holding SCL low IS the bus being owned.
begin_txn(4'd2);
pulse_s;
stretch_for(20); // a long stall, mid-transaction
pulse_sr;
stretch_for(35); // and another, in the read phase
pulse_p;
end_txn;
if (atomic !== 1'b1) begin
$display("FAIL: clock stretching was treated as a loss of atomicity"); errors = errors + 1; end
if (break_reason !== BR_NONE) begin
$display("FAIL: stretching set break_reason = %0d", break_reason); errors = errors + 1; end
if (n_stretch_events !== 16'd2) begin
$display("FAIL: counted %0d stretch events, expected 2", n_stretch_events);
errors = errors + 1; end
if (n_atomic !== 16'd2) begin
$display("FAIL: a stretched transaction was not counted as atomic"); errors = errors + 1; end
// ---- 3: the BUS WAS RELEASED between the phases. A STOP arrived after only
// one of two phases, with no abort to explain it. This is the fault the
// whole module is about, and the pointer is now suspect.
begin_txn(4'd2);
pulse_s; // phase 1 done: the pointer has been written
pulse_p; // ... and the bus is RELEASED
pulse_s; // re-taken as a fresh START
pulse_p;
end_txn;
if (atomic !== 1'b0) begin
$display("FAIL: a released transaction was reported atomic"); errors = errors + 1; end
if (break_reason !== BR_RELEASED) begin
$display("FAIL: break_reason = %0d, expected %0d (released)",
break_reason, BR_RELEASED); errors = errors + 1; end
// THE consequence. A phase had completed, so a pointer had been written, so
// whoever took the free bus could have changed it.
if (pointer_suspect !== 1'b1) begin
$display("FAIL: pointer_suspect not raised after a mid-transaction release");
errors = errors + 1; end
if (n_broken !== 16'd1) begin
$display("FAIL: n_broken = %0d, expected 1", n_broken); errors = errors + 1; end
// ---- 4: ARBITRATION LOST. Ownership did not become free, it became somebody
// else's -- a more specific explanation, so it must win over a release.
begin_txn(4'd2);
pulse_s;
pulse_arb;
end_txn;
if (atomic !== 1'b0) begin
$display("FAIL: an arbitration loss was reported atomic"); errors = errors + 1; end
if (break_reason !== BR_ARB_LOST) begin
$display("FAIL: break_reason = %0d, expected %0d (arbitration)",
break_reason, BR_ARB_LOST); errors = errors + 1; end
if (pointer_suspect !== 1'b1) begin
$display("FAIL: pointer_suspect not raised after an arbitration loss"); errors = errors + 1; end
// ---- 5: ABANDONED. The master stopped without emitting a STOP at all, so the
// bus is left HELD -- the worst of the three, because it strands every
// other device rather than only corrupting this master's own data.
begin_txn(4'd2);
pulse_s;
pulse_sr;
end_txn; // no STOP
if (atomic !== 1'b0) begin
$display("FAIL: an abandoned transaction was reported atomic"); errors = errors + 1; end
if (break_reason !== BR_ABANDONED) begin
$display("FAIL: break_reason = %0d, expected %0d (abandoned)",
break_reason, BR_ABANDONED); errors = errors + 1; end
if (n_broken !== 16'd3) begin
$display("FAIL: n_broken = %0d, expected 3", n_broken); errors = errors + 1; end
// ---- 6: a CLEAN ABORT. The device was absent, so the master terminated early
// ON PURPOSE with a STOP. On the wire this is IDENTICAL to test 3 -- one
// phase, then a STOP -- and only the master's own signal distinguishes
// them. It must NOT be counted as a fault.
begin_txn(4'd2);
pulse_s;
txn_aborted = 1'b1; @(negedge clk);
pulse_p;
txn_aborted = 1'b0;
end_txn;
if (break_reason !== BR_NONE) begin
$display("FAIL: a clean abort was reported as break_reason %0d", break_reason);
errors = errors + 1; end
if (n_aborted !== 16'd1) begin
$display("FAIL: n_aborted = %0d, expected 1", n_aborted); errors = errors + 1; end
if (n_broken !== 16'd3) begin
$display("FAIL: a clean abort was counted as broken (n_broken = %0d)", n_broken);
errors = errors + 1; end
// ---- 7: a THREE-phase transaction. Exposure is a property of the junctions,
// not of the length -- three phases held is exactly as atomic as two.
begin_txn(4'd3);
pulse_s; pulse_sr; pulse_sr; pulse_p;
end_txn;
if (atomic !== 1'b1) begin
$display("FAIL: a three-phase held transaction was reported non-atomic"); errors = errors + 1; end
if (phases !== 4'd3) begin
$display("FAIL: counted %0d phases, expected 3", phases); errors = errors + 1; end
if (n_atomic !== 16'd3) begin
$display("FAIL: n_atomic = %0d, expected 3", n_atomic); errors = errors + 1; end
// ---- 8: a release BEFORE any phase completed. The bus was let go, but no
// pointer had been written yet, so there is nothing to have been changed.
// pointer_suspect must stay CLEAR -- the flag means something specific.
begin_txn(4'd2);
pulse_p; // a STOP with zero phases seen
end_txn;
if (break_reason !== BR_RELEASED) begin
$display("FAIL: a zero-phase release was not reported as released"); errors = errors + 1; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect raised when no phase had completed"); errors = errors + 1; end
// ---- 8b: ARBITRATION lost before any phase completed. Ownership was lost,
// so the transaction is broken -- but no pointer had been written, so
// pointer_suspect must stay CLEAR. The flag means one specific thing.
begin_txn(4'd2);
pulse_arb; // zero phases seen
end_txn;
if (break_reason !== BR_ARB_LOST) begin
$display("FAIL: a zero-phase arbitration loss was not reported");
errors = errors + 1; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect raised on an arbitration loss with no phase done");
errors = errors + 1; end
// ---- 9: the verdict is a PULSE, one per transaction, and the flags clear on
// the next transaction so a stale verdict cannot be read as a fresh one.
begin_txn(4'd2);
pulse_s; pulse_sr; pulse_p;
end_txn;
if (break_reason !== BR_NONE || atomic !== 1'b1) begin
$display("FAIL: a verdict survived into the next transaction"); errors = errors + 1; end
if (pointer_suspect !== 1'b0) begin
$display("FAIL: pointer_suspect survived into the next transaction"); errors = errors + 1; end
if (n_verdict !== 10) begin
$display("FAIL: %0d verdict pulses across 10 transactions", n_verdict); errors = errors + 1; end
// ---- 10: stretching OUTSIDE a transaction is still counted and still harmless.
begin
st_before = n_stretch_events;
stretch_for(15);
if (n_stretch_events !== st_before + 16'd1) begin
$display("FAIL: a stretch outside a transaction was not counted"); errors = errors + 1; end
if (break_reason !== BR_NONE) begin
$display("FAIL: an idle-bus stretch set a break reason"); errors = errors + 1; end
end
if (errors == 0)
$display("PASS: held transactions atomic at any length, stretching harmless, release and arbitration and abandonment distinguished, a clean abort is not a fault");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- An ATOMICITY tracker for multi-phase transactions.
--
-- Chapter 10.2's monitor reports facts about the wire and deliberately cannot report
-- intent: on the wire, a STOP followed by a START IS two separate transactions, and
-- nothing distinguishes that from a driver that wrongly let the bus go. This block
-- supplies the missing half by taking what the master BELIEVED it was doing and
-- correlating it against what the wire actually showed.
--
-- The definition it enforces is narrow and worth stating exactly:
--
-- A multi-phase transaction is ATOMIC if the bus was continuously owned by this
-- master from the first START to the final STOP.
--
-- "Continuously owned" is not the same as "continuously busy", and that distinction is
-- the whole content of this chapter. Three things end ownership and one does not:
--
-- RELEASED a STOP before every phase had been done, with no abort to explain it.
-- The bus is free and any other master may take it -- and in the
-- register-pointer pattern of Chapter 10.1 may change the pointer this
-- master just wrote.
-- ARBITRATION this master lost the bus to another one. The bus did not become free,
-- it became somebody else's, and the transaction never completed.
-- ABANDONED the master finished without a final STOP at all -- a reset, a timeout,
-- a driver that gave up. The bus is left HELD with nobody to finish it,
-- which strands every other device on the bus.
--
-- STRETCHING does NOT break atomicity. A slave holding SCL low slows the transfer
-- and changes nothing about ownership: SCL held low IS the bus being
-- owned. This is the case people most often get wrong.
--
-- THREE INPUTS CARRY INTENT, and each exists because the wire cannot supply it:
-- expect_phases, txn_aborted and arb_lost. An abort MUST release the bus, so it produces
-- the identical wire event to the RELEASED fault and is distinguishable only by being
-- declared.
entity i2c_atomicity_tracker is
generic (
CNT_W : positive := 16;
PH_W : positive := 4 -- phases per transaction, counter width
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- framing events, from the monitor of Chapter 10.2
start_det : in std_logic; -- pulse: a fresh START
restart_det : in std_logic; -- pulse: a repeated START -- new phase, same owner
stop_det : in std_logic; -- pulse: a STOP -- ownership ends here
-- what THIS master believes
txn_active : in std_logic; -- Chapter 10.1's bus_held
expect_phases : in unsigned(PH_W - 1 downto 0); -- addressings expected
txn_aborted : in std_logic; -- terminating early on purpose
arb_lost : in std_logic; -- pulse: lost arbitration
scl_stretched : in std_logic; -- a slave holds SCL low
-- verdict
break_reason : out unsigned(1 downto 0);
atomic : out std_logic;
phases : out unsigned(PH_W - 1 downto 0);
verdict_valid : out std_logic;
-- If ownership was lost AFTER a phase had already completed, the pointer this
-- master wrote may have been changed by whoever took the bus. The data read
-- afterwards is then from an unknown location -- and nothing on the bus says so.
pointer_suspect : out std_logic;
n_atomic : out unsigned(CNT_W - 1 downto 0);
n_broken : out unsigned(CNT_W - 1 downto 0);
n_aborted : out unsigned(CNT_W - 1 downto 0); -- clean, not faults
n_stretch_events : out unsigned(CNT_W - 1 downto 0) -- deliberately harmless
);
end entity;
architecture rtl of i2c_atomicity_tracker is
constant BR_NONE : unsigned(1 downto 0) := to_unsigned(0, 2);
constant BR_RELEASED : unsigned(1 downto 0) := to_unsigned(1, 2);
constant BR_ARB_LOST : unsigned(1 downto 0) := to_unsigned(2, 2);
constant BR_ABANDONED : unsigned(1 downto 0) := to_unsigned(3, 2);
constant ZERO_PH : unsigned(PH_W - 1 downto 0) := (others => '0');
signal txn_active_q : std_logic := '0';
signal stretch_q : std_logic := '0'; -- count stretch EVENTS, not cycles
signal tracking : std_logic := '0';
signal saw_stop : std_logic := '0';
signal was_aborted : std_logic := '0';
signal br : unsigned(1 downto 0) := to_unsigned(0, 2);
signal at : std_logic := '1';
signal ph : unsigned(PH_W - 1 downto 0) := (others => '0');
begin
break_reason <= br;
atomic <= at;
phases <= ph;
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
br <= BR_NONE;
at <= '1';
ph <= (others => '0');
verdict_valid <= '0';
pointer_suspect <= '0';
n_atomic <= (others => '0');
n_broken <= (others => '0');
n_aborted <= (others => '0');
n_stretch_events <= (others => '0');
txn_active_q <= '0';
stretch_q <= '0';
tracking <= '0';
saw_stop <= '0';
was_aborted <= '0';
else
verdict_valid <= '0';
txn_active_q <= txn_active;
stretch_q <= scl_stretched;
-- Stretching is COUNTED so a transaction's duration can be explained,
-- and it deliberately touches neither `at` nor `br`. A slave holding SCL
-- low has taken the bus from nobody -- it IS the bus being held, which
-- is the opposite of the bus being free.
if scl_stretched = '1' and stretch_q = '0' then
n_stretch_events <= n_stretch_events + 1;
end if;
if txn_active = '1' and txn_active_q = '0' then
-- a transaction begins
tracking <= '1';
at <= '1';
br <= BR_NONE;
ph <= (others => '0');
pointer_suspect <= '0';
saw_stop <= '0';
was_aborted <= '0';
elsif tracking = '1' then
-- Each addressing is a phase. A fresh START opens the first; each
-- repeated START opens another WITHOUT surrendering ownership, which
-- is exactly why a combined transaction is one transaction.
if start_det = '1' or restart_det = '1' then ph <= ph + 1; end if;
if txn_aborted = '1' then was_aborted <= '1'; end if;
if arb_lost = '1' then
-- Losing arbitration ends ownership immediately. It takes
-- priority over a release because it is the more specific
-- explanation: the bus did not become free, it became somebody
-- else's.
at <= '0';
br <= BR_ARB_LOST;
if ph /= ZERO_PH then pointer_suspect <= '1'; end if;
elsif stop_det = '1' then
saw_stop <= '1';
-- A STOP before every phase has been done RELEASES the bus,
-- unless an abort explains it.
if ph < expect_phases
and not (txn_aborted = '1' or was_aborted = '1') then
at <= '0';
br <= BR_RELEASED;
if ph /= ZERO_PH then pointer_suspect <= '1'; end if;
end if;
end if;
end if;
-- a transaction concludes
if tracking = '1' and txn_active = '0' and txn_active_q = '1' then
tracking <= '0';
verdict_valid <= '1';
if br /= BR_NONE then
n_broken <= n_broken + 1;
elsif not (saw_stop = '1' or stop_det = '1') then
-- Finished with no STOP at all. The bus is left HELD and nobody
-- is going to finish the transaction -- the worst of the three,
-- because it strands every other device rather than only
-- corrupting this master's own data.
at <= '0';
br <= BR_ABANDONED;
n_broken <= n_broken + 1;
if ph /= ZERO_PH then pointer_suspect <= '1'; end if;
elsif was_aborted = '1' or txn_aborted = '1' then
-- A clean early termination: not atomic in the sense of having
-- completed, and not a FAULT either. Counted separately so a
-- report cannot confuse "the device was absent" with "the driver
-- released the bus".
n_aborted <= n_aborted + 1;
else
n_atomic <= n_atomic + 1;
end if;
end if;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_atomicity_tracker_tb is
end entity;
architecture sim of i2c_atomicity_tracker_tb is
constant CNT_W : positive := 16;
constant PH_W : positive := 4;
constant BR_NONE : unsigned(1 downto 0) := to_unsigned(0, 2);
constant BR_RELEASED : unsigned(1 downto 0) := to_unsigned(1, 2);
constant BR_ARB_LOST : unsigned(1 downto 0) := to_unsigned(2, 2);
constant BR_ABANDONED : unsigned(1 downto 0) := to_unsigned(3, 2);
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal start_det, restart_det, stop_det : std_logic := '0';
signal txn_active, txn_aborted, arb_lost, scl_stretched : std_logic := '0';
signal expect_phases : unsigned(PH_W - 1 downto 0) := to_unsigned(2, PH_W);
signal break_reason : unsigned(1 downto 0);
signal atomic, verdict_valid, pointer_suspect : std_logic;
signal phases : unsigned(PH_W - 1 downto 0);
signal n_atomic, n_broken, n_aborted, n_stretch_events : unsigned(CNT_W - 1 downto 0);
-- owned solely by the verdict-counting process
signal n_verdict : natural := 0;
signal test_done : std_logic := '0';
begin
dut : entity work.i2c_atomicity_tracker
generic map (CNT_W => CNT_W, PH_W => PH_W)
port map (clk => clk, rst_n => rst_n, start_det => start_det,
restart_det => restart_det, stop_det => stop_det,
txn_active => txn_active, expect_phases => expect_phases,
txn_aborted => txn_aborted, arb_lost => arb_lost,
scl_stretched => scl_stretched, break_reason => break_reason,
atomic => atomic, phases => phases, verdict_valid => verdict_valid,
pointer_suspect => pointer_suspect, n_atomic => n_atomic,
n_broken => n_broken, n_aborted => n_aborted,
n_stretch_events => n_stretch_events);
clk <= not clk after 5 ns;
watchdog : process
begin
wait for 400 us;
if test_done = '0' then
report "watchdog expired -- the design never reached the expected state"
severity failure;
end if;
wait;
end process;
counter : process (clk)
begin
if rising_edge(clk) and rst_n = '1' and verdict_valid = '1' then
n_verdict <= n_verdict + 1;
end if;
end process;
stim : process
variable errs : natural := 0;
variable st_before : 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;
procedure pulse_s is
begin
start_det <= '1'; waitn(1); start_det <= '0'; waitn(1);
end procedure;
procedure pulse_sr is
begin
restart_det <= '1'; waitn(1); restart_det <= '0'; waitn(1);
end procedure;
procedure pulse_p is
begin
stop_det <= '1'; waitn(1); stop_det <= '0'; waitn(1);
end procedure;
procedure pulse_arb is
begin
arb_lost <= '1'; waitn(1); arb_lost <= '0'; waitn(1);
end procedure;
-- A slave stretches SCL for n cycles. A real event, and NOT a break.
procedure stretch_for (n : in positive) is
begin
scl_stretched <= '1'; waitn(n); scl_stretched <= '0'; waitn(1);
end procedure;
procedure begin_txn (p : in positive) is
begin
expect_phases <= to_unsigned(p, PH_W);
txn_active <= '1'; waitn(2);
end procedure;
procedure end_txn is
begin
txn_active <= '0'; waitn(2);
end procedure;
begin
waitn(3);
if atomic /= '1' then
report "atomic should default true" severity error; errs := errs + 1; end if;
rst_n <= '1'; waitn(1);
-- 1: a HEALTHY write-then-read. Two phases joined by a repeated START, ending
-- with one STOP. This must be atomic.
begin_txn(2);
pulse_s; -- phase 1: the write addressing
pulse_sr; -- phase 2: the read addressing -- ownership retained
pulse_p; -- the final STOP
end_txn;
if atomic /= '1' then
report "a correct write-then-read was reported as non-atomic" severity error;
errs := errs + 1; end if;
if break_reason /= BR_NONE then
report "break_reason set on a healthy transaction" severity error;
errs := errs + 1; end if;
if phases /= to_unsigned(2, PH_W) then
report "wrong phase count, expected 2" severity error; errs := errs + 1; end if;
if pointer_suspect /= '0' then
report "pointer_suspect on a healthy transaction" severity error;
errs := errs + 1; end if;
if n_atomic /= to_unsigned(1, CNT_W) or n_broken /= to_unsigned(0, CNT_W) then
report "wrong atomic/broken counts, expected 1 and 0" severity error;
errs := errs + 1; end if;
-- 2: CLOCK STRETCHING does not break atomicity. This is the case most often got
-- wrong: the slave holding SCL low IS the bus being owned.
begin_txn(2);
pulse_s;
stretch_for(20); -- a long stall, mid-transaction
pulse_sr;
stretch_for(35); -- and another, in the read phase
pulse_p;
end_txn;
if atomic /= '1' then
report "clock stretching was treated as a loss of atomicity" severity error;
errs := errs + 1; end if;
if break_reason /= BR_NONE then
report "stretching set a break reason" severity error; errs := errs + 1; end if;
if n_stretch_events /= to_unsigned(2, CNT_W) then
report "wrong stretch-event count, expected 2" severity error;
errs := errs + 1; end if;
if n_atomic /= to_unsigned(2, CNT_W) then
report "a stretched transaction was not counted as atomic" severity error;
errs := errs + 1; end if;
-- 3: the BUS WAS RELEASED between the phases. A STOP arrived after only one of
-- two phases, with no abort to explain it. This is the fault the whole module is
-- about, and the pointer is now suspect.
begin_txn(2);
pulse_s; -- phase 1 done: the pointer has been written
pulse_p; -- ... and the bus is RELEASED
pulse_s; -- re-taken as a fresh START
pulse_p;
end_txn;
if atomic /= '0' then
report "a released transaction was reported atomic" severity error;
errs := errs + 1; end if;
if break_reason /= BR_RELEASED then
report "break_reason should be RELEASED" severity error; errs := errs + 1; end if;
-- THE consequence. A phase had completed, so a pointer had been written, so
-- whoever took the free bus could have changed it.
if pointer_suspect /= '1' then
report "pointer_suspect not raised after a mid-transaction release"
severity error; errs := errs + 1; end if;
if n_broken /= to_unsigned(1, CNT_W) then
report "wrong broken count, expected 1" severity error; errs := errs + 1; end if;
-- 4: ARBITRATION LOST. Ownership did not become free, it became somebody else's
-- -- a more specific explanation, so it must win over a release.
begin_txn(2);
pulse_s;
pulse_arb;
end_txn;
if atomic /= '0' then
report "an arbitration loss was reported atomic" severity error;
errs := errs + 1; end if;
if break_reason /= BR_ARB_LOST then
report "break_reason should be ARBITRATION" severity error; errs := errs + 1; end if;
if pointer_suspect /= '1' then
report "pointer_suspect not raised after an arbitration loss" severity error;
errs := errs + 1; end if;
-- 5: ABANDONED. The master stopped without emitting a STOP at all, so the bus is
-- left HELD -- the worst of the three, because it strands every other device
-- rather than only corrupting this master's own data.
begin_txn(2);
pulse_s;
pulse_sr;
end_txn; -- no STOP
if atomic /= '0' then
report "an abandoned transaction was reported atomic" severity error;
errs := errs + 1; end if;
if break_reason /= BR_ABANDONED then
report "break_reason should be ABANDONED" severity error; errs := errs + 1; end if;
if n_broken /= to_unsigned(3, CNT_W) then
report "wrong broken count, expected 3" severity error; errs := errs + 1; end if;
-- 6: a CLEAN ABORT. The device was absent, so the master terminated early ON
-- PURPOSE with a STOP. On the wire this is IDENTICAL to test 3 -- one phase,
-- then a STOP -- and only the master's own signal distinguishes them. It must
-- NOT be counted as a fault.
begin_txn(2);
pulse_s;
txn_aborted <= '1'; waitn(1);
pulse_p;
txn_aborted <= '0';
end_txn;
if break_reason /= BR_NONE then
report "a clean abort was reported as a break" severity error;
errs := errs + 1; end if;
if n_aborted /= to_unsigned(1, CNT_W) then
report "wrong aborted count, expected 1" severity error; errs := errs + 1; end if;
if n_broken /= to_unsigned(3, CNT_W) then
report "a clean abort was counted as broken" severity error; errs := errs + 1; end if;
-- 7: a THREE-phase transaction. Exposure is a property of the junctions, not of
-- the length -- three phases held is exactly as atomic as two.
begin_txn(3);
pulse_s; pulse_sr; pulse_sr; pulse_p;
end_txn;
if atomic /= '1' then
report "a three-phase held transaction was reported non-atomic" severity error;
errs := errs + 1; end if;
if phases /= to_unsigned(3, PH_W) then
report "wrong phase count, expected 3" severity error; errs := errs + 1; end if;
if n_atomic /= to_unsigned(3, CNT_W) then
report "wrong atomic count, expected 3" severity error; errs := errs + 1; end if;
-- 8: a release BEFORE any phase completed. The bus was let go, but no pointer
-- had been written yet, so there is nothing to have been changed.
-- pointer_suspect must stay CLEAR -- the flag means something specific.
begin_txn(2);
pulse_p; -- a STOP with zero phases seen
end_txn;
if break_reason /= BR_RELEASED then
report "a zero-phase release was not reported as released" severity error;
errs := errs + 1; end if;
if pointer_suspect /= '0' then
report "pointer_suspect raised when no phase had completed" severity error;
errs := errs + 1; end if;
-- 8b: ARBITRATION lost before any phase completed. Ownership was lost, so the
-- transaction is broken -- but no pointer had been written, so pointer_suspect
-- must stay CLEAR. The flag means one specific thing.
begin_txn(2);
pulse_arb; -- zero phases seen
end_txn;
if break_reason /= BR_ARB_LOST then
report "a zero-phase arbitration loss was not reported" severity error;
errs := errs + 1; end if;
if pointer_suspect /= '0' then
report "pointer_suspect raised on an arbitration loss with no phase done"
severity error; errs := errs + 1; end if;
-- 9: the verdict is a PULSE, one per transaction, and the flags clear on the next
-- transaction so a stale verdict cannot be read as a fresh one.
begin_txn(2);
pulse_s; pulse_sr; pulse_p;
end_txn;
if break_reason /= BR_NONE or atomic /= '1' then
report "a verdict survived into the next transaction" severity error;
errs := errs + 1; end if;
if pointer_suspect /= '0' then
report "pointer_suspect survived into the next transaction" severity error;
errs := errs + 1; end if;
if n_verdict /= 10 then
report "wrong number of verdict pulses across ten transactions" severity error;
errs := errs + 1; end if;
-- 10: stretching OUTSIDE a transaction is still counted and still harmless.
st_before := n_stretch_events;
stretch_for(15);
if n_stretch_events /= st_before + 1 then
report "a stretch outside a transaction was not counted" severity error;
errs := errs + 1; end if;
if break_reason /= BR_NONE then
report "an idle-bus stretch set a break reason" severity error;
errs := errs + 1; end if;
if errs = 0 then
report "i2c_atomicity_tracker self-check complete: held transactions atomic at "
& "any length, stretching harmless, release and arbitration and "
& "abandonment distinguished, a clean abort is not a fault" severity note;
else
report "i2c_atomicity_tracker self-check FAILED" severity error;
end if;
test_done <= '1';
wait;
end process;
end architecture;5a. Six Decisions Worth Defending
Stretching is counted and deliberately excluded from the verdict. §3's callout gives the argument in both directions: excluding it from the judgement stops false positives, counting it preserves the ability to explain a transaction's duration. Mutation C1 makes stretching set a break reason and dies with a message that says a well-formed transaction was reported broken — a false positive, which is the failure direction that destroys a checker's usefulness.
expect_phases is an input because a STOP after one phase is ambiguous without it. One phase then a STOP is a completed single-phase transfer, and it is also a released two-phase one. The only thing that distinguishes them is how many phases the master intended. Mutation C3 removes the comparison and reports every healthy transaction as broken, because the final STOP of any transaction arrives while the master still considers itself active.
txn_aborted exists because an abort and a release are the same wire event. Chapter 10.1 §5 established that an abort must release the bus — so a legitimate abort produces exactly the pattern a mid-transaction release does: fewer phases than expected, then a STOP. No amount of wire-watching separates them; only the master declaring its intent does. This is the single clearest example in the module of §4's point that the bus records events and not intentions, and mutation C2 drops the guard and reports clean aborts as faults.
Arbitration loss takes priority over a release. Both can be true in the same cycle — losing arbitration means somebody else's START won, and a START is not a STOP, but the sequencing can put both events close together. When both apply, arbitration is the more specific explanation: the bus did not become free, it became somebody else's, and a retry is a different action from a retry after a release. Mutation C4 removes the priority and the tracker reports abandoned where arbitration belongs.
pointer_suspect is guarded on a phase having completed, and that guard is the point of the flag. Ownership lost before any addressing finished means no pointer was ever written, so there is nothing that could have been changed — the transaction failed, but the device's state is untouched. Ownership lost after phase 1 means a pointer exists and somebody may have overwritten it. Those are different problems with different recovery, and a flag that fired on both would mean only "something went wrong", which the break reason already says. Mutations C5 and C10 remove the guard and remove the flag respectively; both die.
The verdict clears when a transaction begins, not when one ends. The same reasoning as Chapter 8.3 §7a: a result must survive past the moment it is produced, because the end of the transaction is when a consumer reads it. Clearing at the start of the next transaction is the latest point that cannot destroy a result somebody is entitled to. Mutation C8 stops the clearing and a stale verdict is read as a fresh one.
5b. Verified Execution
$ iverilog -g2012 -o c0 i2c_atomicity_tracker.sv i2c_atomicity_tracker_tb.sv && ./c0
PASS: held transactions atomic at any length, stretching harmless, release and
arbitration and abandonment distinguished, a clean abort is not a fault
i2c_atomicity_tracker_tb.sv:229: $finish called at 1680 (1ps)
$ iverilog -g2005 -o c1 i2c_atomicity_tracker.v i2c_atomicity_tracker_tb.v && ./c1
PASS: held transactions atomic at any length, stretching harmless, release and
arbitration and abandonment distinguished, a clean abort is not a fault
i2c_atomicity_tracker_tb.v:243: $finish called at 1680 (1ps)
$ nvc -a i2c_atomicity_tracker.vhd i2c_atomicity_tracker_tb.vhd
$ nvc -e i2c_atomicity_tracker_tb && nvc -r i2c_atomicity_tracker_tb --stop-time=500us
** Note: 1680ns+0: i2c_atomicity_tracker self-check complete: held transactions atomic
at any length, stretching harmless, release and arbitration and abandonment
distinguished, a clean abort is not a faultAll three at 1680 ns.
6. What the Testbench Proves
| # | scenario | verdict required |
|---|---|---|
| 1 | two phases, Sr junction, one STOP | atomic; no break; pointer_suspect clear |
| 2 | the same, with two long stretches | atomic — stretching counted, not judged |
| 3 | a STOP after one of two phases | released, and pointer_suspect set |
| 4 | arbitration lost after phase 1 | arbitration, not released; suspect set |
| 5 | finished with no STOP | abandoned |
| 6 | a clean abort with a STOP | no break, counted as an abort, not as broken |
| 7 | three phases, two Sr's | atomic — exposure is about junctions, not length |
| 8 | a release with zero phases done | released, but pointer_suspect clear |
| 8b | arbitration lost with zero phases done | arbitration, and pointer_suspect clear |
| 9 | a healthy transaction after all of the above | the verdict and flags cleared |
| 10 | stretching on an idle bus | counted, and still no break |
Tests 3 and 6 are the pair that matters most, and they are identical on the wire. Both are: one phase completed, then a STOP, then the master stops. The only difference is that test 6 asserts txn_aborted. Test 3 must be a fault and test 6 must not, and no monitor watching only SDA and SCL could tell you which is which — which is §4's thesis reduced to two test cases three lines apart.
Tests 8 and 8b are the zero-phase versions of 3 and 4, and they exist because pointer_suspect means something specific. Losing the bus is bad in all four cases; the pointer is only in question when one was actually written. Test 8b was added after a mutation survived, because the original suite covered the zero-phase case for a release and not for an arbitration loss — the same guard, on a different code path.
Test 2 is the one to read if you read only one. Two stretches of twenty and thirty-five cycles, in the middle of a transaction, and the required verdict is atomic. That is the chapter's central claim as an executable assertion.
7. Mutation Testing
Ten defects injected into the SystemVerilog tracker.
| # | injected defect | outcome |
|---|---|---|
| C1 | clock stretching breaks atomicity | killed — stretching treated as a loss |
| C2 | a clean abort is reported as a release | killed — an abort reported as break reason 1 |
| C3 | a release is flagged without the phase comparison | killed — every healthy transaction reported broken |
| C4 | arbitration loss does not take priority | killed — reported abandoned, expected arbitration |
| C5 | pointer_suspect raised with no phase completed | killed |
| C6 | the abandoned case is not detected | killed — an abandoned transaction reported atomic |
| C7 | a repeated START is not counted as a phase | killed — a healthy transaction reported non-atomic |
| C8 | the verdict is not cleared at the start | killed — a stale verdict read as fresh |
| C9 | a clean abort is counted as a fault | killed — n_aborted 0, expected 1 |
| C10 | pointer_suspect is never raised on a release | killed |
Ten injected, ten killed — and across the whole module 28 injected, 28 killed, no survivors and no invalid mutants, with three survivors from the first run closed by new tests rather than excused.
C1 and C3 both fail as false positives, and that is the direction worth dwelling on. C1 reports a violation on every stretched transaction; C3 reports one on every transaction at all. Neither misses a fault — both invent them. A checker that misses faults is inadequate; a checker that fires on correct behaviour is actively harmful, because it trains people to ignore its output, and then it occupies the slot where a working checker would have been. This is why the testbench asserts atomic == 1 on healthy traffic rather than only atomic == 0 on broken traffic, and it is the same argument Chapter 9.3 §8 made about the hang predictor.
C4's message is the interesting one: it reports abandoned where arbitration belongs. Follow it through. Removing the arbitration branch means the loss is not recorded, so when the master drops txn_active the tracker finds no STOP and no break reason — and concludes the transaction was abandoned. The verdict is wrong in a specific and misleading way: it blames the master for leaving the bus held when in fact another master took it. A recovery routine acting on that would attempt a bus-recovery sequence on a bus that is working fine.
C7 is a reminder that the phase count is load-bearing. Not counting repeated STARTs as phases means a two-phase transaction reports one phase, so its final STOP arrives with phases < expect_phases and is classified as a release. A healthy transaction reported broken — again a false positive, from a mutation that looks like it would only affect a status counter.
8. Verification Connection — Atomicity Is a Property, Not a Check
Everything in this chapter is a statement about intervals, which is what makes it assertion territory rather than checker territory. Chapter 10.1 §9 established the shape: a property about an interval cannot be verified at its endpoints.
// THE property. From the first START of a multi-phase transaction to its final STOP,
// no STOP may appear. `throughout` is essential: an endpoint check passes on a design
// that released the bus and re-took it, because the extra STOP is balanced by the
// extra START.
property p_ownership_continuous;
@(posedge clk) disable iff (!rst_n)
(txn_begin && expect_phases > 1)
|=> ((!stop_det || txn_last_phase) throughout (txn_end[->1]));
endproperty
assert property (p_ownership_continuous)
else $error("the bus was released mid-transaction -- ownership was not continuous");
// Clock stretching must NOT affect the verdict. Asserting the NEGATIVE is the point:
// it is the property that stops a future well-meaning change from treating a stall as
// a break, which is the single most common misreading of atomicity on this bus.
property p_stretching_is_not_a_break;
@(posedge clk) disable iff (!rst_n)
scl_stretched |-> ##1 $stable(break_reason);
endproperty
assert property (p_stretching_is_not_a_break)
else $error("clock stretching changed the atomicity verdict");
// A transaction must END with a STOP. Abandonment is the failure that strands every
// other device, so it deserves its own property rather than being folded into the
// ownership one -- the two have different recoveries.
property p_transaction_ends_with_stop;
@(posedge clk) disable iff (!rst_n)
txn_end |-> (stop_det || $past(stop_det, 1) || arb_lost);
endproperty
assert property (p_transaction_ends_with_stop)
else $error("a transaction ended without a STOP -- the bus is left HELD");
// pointer_suspect means one specific thing. Asserting the implication in BOTH
// directions is what keeps it meaningful: it must be set when ownership was lost
// after a phase, and clear otherwise. A flag that is merely "often right" is a flag
// people stop trusting.
property p_suspect_iff_lost_after_a_phase;
@(posedge clk) disable iff (!rst_n)
verdict_valid |-> (pointer_suspect == ((break_reason != BR_NONE) && (phases > 0)));
endproperty
assert property (p_suspect_iff_lost_after_a_phase)
else $error("pointer_suspect does not match 'ownership lost after a phase'");And the coverage. For atomicity the interesting axis is where in the transaction ownership was lost, because that is what determines the recovery.
covergroup i2c_atomicity_cg with function sample(int break_reason, int phases,
int expect_phases, bit stretched);
// The four verdicts. `none` must be covered too -- a suite that only sampled
// failures could not show that healthy traffic passes, which is the false-positive
// direction that matters most (section 7).
verdict: coverpoint break_reason {
bins none = {0};
bins released = {1};
bins arb_lost = {2};
bins abandoned = {3};
}
// WHERE ownership was lost. Before any phase completed, nothing of the device's
// state is in question; after one, the pointer is. These need separate recovery,
// so they need separate bins.
when_lost: coverpoint phases {
bins before_any_phase = {0};
bins after_first = {1};
bins mid_transaction = {[2:14]};
}
// Stretching crossed with the verdict is how you demonstrate the claim of section
// 3 rather than merely asserting it: stretched transactions must appear in the
// `none` verdict bin.
stretched_cp: coverpoint stretched;
// A multi-phase transaction is the only kind where atomicity is a design concern
// (section 2), so the crosses restrict attention to it.
multi_phase: coverpoint (expect_phases > 1);
verdict_x_when: cross verdict, when_lost;
verdict_x_stretch: cross verdict, stretched_cp;
verdict_x_multi: cross verdict, multi_phase;
endgroup9. FPGA and ASIC Implications
The tracker is about 60 flops. Two bits of break reason, a phase counter, four sixteen-bit counters and six state bits. There is no arithmetic beyond increments and one magnitude compare against expect_phases.
It composes with Chapter 10.2's monitor rather than duplicating it. The framing detection lives in one place and both blocks use it — the monitor produces start_det, restart_det and stop_det from the wires, and the tracker consumes them. Re-deriving framing inside the tracker would double the cost and, worse, create the possibility of the two blocks disagreeing about what the bus did.
The intent inputs are the integration work, and they are cheap. txn_active is Chapter 10.1's bus_held, already an output. expect_phases is a constant per transaction type — two for a write-then-read. txn_aborted is the OR of that sequencer's three NACK flags. arb_lost comes from Module 13. None of these needs new logic; they need wiring, and the wiring is what turns two independently useless observations into a verdict.
pointer_suspect is the output worth routing to software. It is the one signal here that tells a driver something it can act on: the data you just read may be from the wrong location. A driver that sees it can re-issue the whole transaction rather than trusting the result — which is a recovery that is impossible without it, because everything else about the transfer looked fine.
On an ASIC this belongs in the debug register block, not the datapath. It observes and drives nothing, so it can be clock-gated off in production if area matters, at the cost of losing exactly the diagnosis that is hardest to get any other way. The trade is worth stating explicitly rather than making by default: this block costs 60 flops and answers "why did the bus hang" — a question that otherwise costs a board respin to instrument.
10. Debugging — The Reset That Locked Out Every Device on the Bus
Pitfall — a watchdog reset between the phases of a combined transaction
// A controller reads a configuration block from an EEPROM at boot, using a proper
// write-then-read with a repeated START. The junction is correct; section 10.2's
// bug is not present here.
//
// S 0xA0 A ptr A Sr 0xA1 A d0 A d1 A ... d31 N P
//
// The EEPROM occasionally stretches SCL while servicing an internal operation, so
// the read can take several milliseconds. The firmware's watchdog window is 5 ms.
//
// When the stretch is long enough, the watchdog fires MID-TRANSACTION -- between the
// repeated START and the final STOP -- and resets the controller.
//
// The reset clears the I2C master's outputs. It does NOT emit a STOP, because
// emitting a STOP is a bus operation and the block that would do it is the block
// being reset.After a reset, nothing on the I2C bus worked. Not the EEPROM -- NOTHING. The temperature sensor, the power monitor, the display controller: every device unreachable, every transfer timing out.
And the controller itself was fine. It booted, ran, logged, and reported that all six I2C devices had failed simultaneously. Six independent devices failing at once is not a plausible hardware fault, which sent the investigation toward the controller's own pins and pin-mux configuration -- all correct.
A scope settled it in seconds and the picture was unmistakable: SDA was LOW. Continuously. Not toggling, not noisy -- held. With SCL released high.
That combination cannot occur during any legal transfer, because SDA low with SCL high for an extended period is not a state the protocol has. It is the EEPROM, still mid-byte in a read it was never told had ended, driving a zero bit and waiting for a clock pulse that is never coming.
And the bus in that state is unusable by every device, because a START requires an idle bus -- both wires high. No master could begin a transfer, including the one that had just rebooted and was trying to.
The reset ABANDONED a transaction rather than ending it. The master stopped driving without emitting a STOP, so no device learned the transfer was over.
The EEPROM was mid-read as a slave-transmitter. It had been told "send another byte" by the master's last acknowledge, it was driving the MSB of that byte, and it had no way to conclude anything had gone wrong -- a slave-transmitter has no timeout, no NACK available to it, and nothing to say (Chapter 9.2 section 3). It simply waited, holding SDA at whatever the current bit was.
Because that bit happened to be a zero, SDA was held LOW, which locks the bus for everybody. Had it been a one the bus would have looked idle and the whole thing would have gone unnoticed -- which is why this appeared to strike at random.
The stretch was not the bug. The stretch was legal, and section 3 is explicit that stretching does not break atomicity. The bug was a watchdog window shorter than the worst-case legal transaction, and a reset path that left the bus held.
11. Common Misconceptions
"Atomic means all-or-nothing." Not on this bus. There is no rollback: a partially completed write stays partially completed. Atomic here means the bus was continuously owned from the first START to the final STOP.
"Clock stretching breaks atomicity." It does not. A slave holding SCL low has taken the bus from nobody — no other master can start a transfer on a bus that is not idle. A transaction stretched for ten milliseconds is exactly as atomic as one that was not.
"A busy bus is an owned bus." Busy and owned come apart in both directions. An idle bus is neither busy nor owned — and that is the dangerous state, because it is the one another master may take.
"A reset releases the bus." It stops the master driving; it does not emit a STOP. A slave stranded mid-byte holds SDA at whatever bit it was on, and if that bit is a zero the bus is locked for every device. §10 is that failure.
"Losing arbitration and releasing the bus are the same kind of problem." Losing arbitration is reported — the master knows and can retry the whole transaction. A release is silent, and both resulting transactions are legal. Different information, different recovery, which is why arbitration takes priority in the verdict.
"A monitor on the bus can detect a mid-transaction release." It cannot. An abort and a release produce the identical wire event, and only the master's own declaration separates them. Tests 3 and 6 in §6 are that fact as two test cases.
"pointer_suspect means the transaction failed." The break reason means that. pointer_suspect means something narrower and more actionable: a pointer had been written before ownership was lost, so the data may be from the wrong location.
"A longer transaction is more exposed." Exposure is a property of the junctions, not the length. A three-phase held transaction is exactly as atomic as a two-phase one, which is §6's test 7.
12. Reason It Through
A combined transaction takes 14 ms because a slave stretched SCL twice. Was it atomic, and how would you justify the answer to somebody who says 14 ms cannot possibly be atomic?
It was atomic. Ownership, not duration, is the criterion — and while SCL is held low the bus is not idle, so no other master could begin a transfer. Nothing could have interposed at any point, which is exactly the guarantee atomicity provides. The objection is really about latency, which is a real concern and a different one: a 14 ms transaction may well violate a timing budget or trip a watchdog (§10), and both are worth fixing. Neither makes it non-atomic.
A master resets between the phases of a write-then-read. Classify the failure and explain why it is worse than a mid-transaction release.
Abandoned. The master stopped without a STOP, so no device learned the transfer ended, and the slave — a slave-transmitter mid-read — holds SDA at whatever bit it was driving. If that is a zero, the bus is locked for every device, because a START requires both wires high. A release at least leaves a usable bus and corrupts only this master's own data; abandonment converts a single-master bug into a system-wide outage, and recovery needs the out-of-band sequence from Chapter 5.5.
Two transactions both lose the bus. One had completed no phases; the other had completed its pointer write. Same break reason. What differs and why does it matter?
pointer_suspect. With no phase completed, no pointer was written, so the device's state is untouched — the transaction failed and nothing else is in question. With phase 1 done, a pointer exists and whoever took the bus may have overwritten it, so any data subsequently read is from an unknown location. The recoveries differ: the first can simply be retried, while the second means data already returned to a caller may be wrong. A single "it failed" flag cannot express that, which is why the guard on pointer_suspect is the point of the flag rather than an optimisation.
Why does the tracker need txn_aborted when it already has expect_phases?
Because a legitimate abort produces exactly the pattern a release does: fewer phases than expected, then a STOP. Chapter 10.1 §5 established that an abort must release the bus, so the wire event is identical and expect_phases alone cannot separate them. Without txn_aborted, every absent device would be reported as a driver bug — which is mutation C2.
A checker fires on every access to a device that stretches the clock. What is the real cost, and why is it worse than a checker that misses faults?
It gets ignored. Within a few weeks a monitor that reports violations on healthy traffic is one nobody reads, and at that point it is worse than absent, because it occupies the slot a working checker would have had and creates a false sense that the property is being verified. That is why §6's tests assert the verdict is clean on healthy traffic, not merely that it is dirty on broken traffic — and why a negative property guards the stretching case specifically.
13. Understanding Check
14. Summary
Atomic means continuously owned, from the first START to the final STOP. Not all-or-nothing — there is no rollback on this bus. Ownership is the only exclusivity the protocol provides, and it is exactly what a combined transaction's correctness rests on.
Owned and busy are different properties. A clocked bus is busy; a bus with SCL held low is busy and owned; an idle bus is neither — and idle is the dangerous state, because it is the one another master may take.
Three things end ownership: a release, an arbitration loss, and an abandonment. Ranked by severity, abandonment is worst, because it strands every other device rather than only corrupting this master's data. A release is second because it is silent and both resulting transactions are legal. An arbitration loss is at least reported.
Clock stretching is not one of them. A stretched transaction is exactly as atomic as an unstretched one; it merely takes longer. Counting stretch events preserves the ability to explain a duration without letting it contaminate the verdict.
Three inputs carry intent, and each exists because the wire cannot supply it. How many phases were intended, whether an abort was deliberate, and whether arbitration was lost. An abort and a release are the same wire event — which is §4's thesis reduced to two adjacent test cases.
A flag that means everything means nothing. pointer_suspect fires only when ownership was lost after a phase completed, because that is the only case where the device's state is in question. The break reason already says a transaction failed.
A checker that fires on healthy traffic is worse than no checker. Two of this chapter's mutations fail as false positives, and that is the direction that destroys a monitor's usefulness — which is why the tests assert the verdict is clean on correct traffic, not only dirty on broken traffic.
15. What Comes Next
Module 10 is complete, and with it the transaction layer. Modules 8, 9 and 10 have built the write, the read, and the composition of both into the shape real devices actually use — every design compiled and run in three languages at exact timing parity, and every mutation suite closed with its survivors fixed rather than excused.
What this module could not do is judge the thing it kept deferring. arb_lost is an input here, taken on faith, and Module 13 builds the mechanism behind it — how two masters that start simultaneously discover the fact, why the loser always finds out before it corrupts anything, and why on a wired-AND bus the arbitration winner is decided bit by bit with no separate protocol at all. It is the last mechanism that can take the bus away from a master mid-transaction, and it is the one that makes §3's second row real.
Before that, Module 11 returns to the timing table with everything the transaction layer has established, and Module 12 builds clock stretching properly — the mechanism this chapter has taken as an input and insisted is harmless, derived from the wired-AND rule that Chapter 2.5 established and applied to the one line a slave is otherwise not allowed to touch.
Continue learning
Related tutorials
- Related topic
Losing I²C Arbitration — Detection, Correct Behaviour and Bus Ownership
The specification places five distinct obligations on a losing master, and stopping is only the second. Includes the one almost always missed — why a combined master-slave must become a slave immediately.
- Related topic
Asserting Clock-Stretching, Arbitration and Bus-Idle Behavior
The behaviours where two devices legitimately drive one line, a property that fired 84 times on legal traffic until its threshold was measured rather than guessed, and the temporal shape a restricted assertion language pushes back into ordinary logic.
- Related topic
Clock-Stretching and Arbitration Failures in the Field
Why a timeout is the least informative evidence an I²C controller produces, and what to record instead. Six situations that all report the same timeout, a classifier that separates them, and the one failure in the set that never times out at all — a controller that does not honour stretching, corrupting data silently and blaming the target.
- Related topic
The Data-Valid Rule — SDA Stable While SCL Is High
One sentence governs every bit on an I²C bus, and it is derived rather than decreed: the receiver needs a settled value at the instant it looks. What falls out is that an SDA edge while SCL is HIGH cannot be data — which is why the bus reserves it for framing.
