I²C · Module 13
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.
Chapter 13.3 ended the moment a master discovers it has lost. That discovery is one comparator and one flag.
What follows is not.
§3.1.8 places five distinct obligations on a losing master. Stopping is the second of them, and the fifth is the one that ships broken.
They are spread across a single paragraph, which is why they read as one instruction and are in fact five.
1. The Five Obligations
| # | obligation | force | when |
|---|---|---|---|
| ① | detect: sent HIGH, saw LOW, while SCL is HIGH | must | per bit |
| ② | turn off the SDA driver | must | the moment a difference appears |
| ③ | generate clock pulses to the end of the byte | may | until the byte boundary |
| ④ | restart the transaction | must | when the bus is free |
| ⑤ | switch to slave mode | must, conditionally | immediately, if addressing and it has a slave |
Two things about that table are worth noticing before the sections that follow.
Only one of the five is permissive. ③ says can, not must — and §3 is about why the specification bothers to grant permission for something that sounds like a courtesy.
Only one is conditional, and it is the one with two conditions that are easy to collapse into one. §5.
2. Obligation ②: "The Moment There Is a Difference"
The specification states this twice, and the second statement exists to nail down the timing: "the moment there is a difference … the DATA 1 output is switched off." Not at the end of the bit, not at the next falling edge — immediately.
There is a subtlety here that the design in §7 makes visible and that is worth working through, because it changes where the obligation actually bites.
On the losing bit itself, the driver is already off. A master loses only when it sent a one — which on an open-drain bus means it released the line. So at the instant of loss it is not driving anything, and "turn off the driver" is trivially satisfied.
Obligation ② binds on the bits after the loss, not on the losing bit.
That is where it has teeth. The loser's shift register keeps advancing, and some of its remaining bits are zeros. Without ②, the loser would drive those zeros onto the wire — corrupting the winner's message from the deciding bit onward, which is precisely the A & B failure Chapter 13.3 §4 warns about.
3. Obligation ③: Permission, Not Courtesy
The specification allows the loser to keep clocking to the end of the byte. Why would it bother to say so, and why is that permission rather than a requirement?
Because stopping mid-byte is harder than continuing. A master that must halt its clock the instant it loses has to abandon a bit cell partway through — and its SCL is wired-AND with everyone else's, so stopping means releasing SCL, which does nothing unless it was the one holding it low. If it was holding SCL low and simply stops, it leaves the line held: the stuck-clock condition Chapter 12.4 §3 shows has no protocol-level recovery.
And because continuing costs the bus nothing. By Chapter 13.2's rule the loser's clock pulses are already merged with the winner's by the wired-AND. Its low phase may lengthen the combined low phase; its high phase may shorten the combined high phase. Neither corrupts anything — the winner's data is on SDA, which the loser is no longer driving.
So obligation ③ is the specification declining to demand something expensive and useless. The loser is permitted to finish the bit cell it is in, and then to stop at a boundary where stopping is clean.
Why the byte boundary specifically? Because that is where the loser's SCL obligations naturally end: nine bits, and after the acknowledge there is no bit in progress. Stopping there leaves the bus in a state the winner can continue from without the loser's involvement.
And note what the permission does not extend to: driving SDA. The loser may clock; it may not transmit. §7's test 5 sweeps the entire remainder of the byte asserting the SDA driver stays off while the clock permission is still asserted — the two are independent and a design that conflated them would either corrupt data or stall the bus.
4. Obligation ④: The Transaction Is Owed
"…and must restart its transaction when the bus is free."
Three words in that clause each do work.
"Must" — this is not optional. The loser had something to do and it still has to do it. A design that dropped the transfer on losing would silently lose work, and the layer above would have no way to know: from its perspective it issued a transfer and got no error.
"Restart" — from the beginning, with a fresh START. Not resume. The loser's partial transfer left no state anywhere: the slave it was addressing never saw a complete address, and the winner's transfer has been using the bus since. There is nothing to resume to.
"When the bus is free" — not immediately. And "free" here is Chapter 13.1 §3's condition: both lines released for at least T_BUF, which cannot be satisfied until the winner has issued its STOP. A loser that retried immediately would be starting a transfer during the winner's transaction, which is the collision of Chapter 13.1 §2 — deliberately created this time.
5. Obligation ⑤: The One That Ships Broken
"If a master also incorporates a slave function and it loses arbitration during the addressing stage, it is possible that the winning master is trying to address it. The losing master must therefore switch over immediately to its slave mode."
This is the obligation that is missed, and the reasoning behind it is genuinely non-obvious. Work it through.
The losing master was transmitting an address. That is what "during the addressing stage" means — the bits on the wire were the first byte of a transfer, an address plus an R/W bit.
Arbitration separated the two addresses at some bit. Before the deciding bit the two masters' addresses were identical; from it onward they differ. So the winner's address shares a prefix with the loser's own intended target — and the winner's address is now on the wire, being clocked out to whichever device answers.
That device might be the loser. A combined master-slave has its own slave address. There is nothing to stop the winner's address from being it — and if this device was acting as a master, it was not listening as a slave.
So a device that does not switch over will NACK a transfer directed at itself, and the winner will conclude the device is absent. The symptom is a device that is intermittently missing, correlated with bus contention, and present whenever you probe it individually.
And "immediately" is load-bearing here too. The winner's address is being clocked out now; the acknowledge slot arrives a few bits later. A device that switched over at the end of the byte would miss the acknowledge it was supposed to give. §7's test 7 asserts the switch happens in the same cycle as the loss detection, and §7's design note records why the decision has to be made at that instant: the transfer phase has moved on by the time anything else could look at it.
6. The Loser's Timeline, Drawn
Stop driving now, clock to the byte boundary, restart when free
10 cyclesRead the two signal rows against each other. drives SDA drops at interval 1 and never returns; may clock continues for five more intervals. The two are independent, and that independence is obligation ③ against obligation ②: the loser is off the data line and still on the clock line, which is a state a design that conflated "stop" with "stop everything" cannot represent.
7. The Loss Handler in Three Languages
// LOSING ARBITRATION CORRECTLY. Section 3.1.8 does not simply say "the loser stops". It places FIVE
// distinct obligations on the losing master, and they are easy to read past because they are spread
// across a paragraph:
//
// 1. DETECT. "The first time a master tries to send a HIGH, but detects that the SDA level is LOW,
// the master knows that it has lost the arbitration" -- and the check is made "while SCL is
// HIGH".
//
// 2. STOP DRIVING, IMMEDIATELY. "...and turns off its SDA output driver." Restated later with the
// timing made explicit: "THE MOMENT there is a difference between the internal data level of
// the master generating DATA 1 and the actual level on the SDA line, the DATA 1 output is
// switched off."
//
// 3. MAY KEEP CLOCKING, to a bounded point. "A master that loses the arbitration CAN generate
// clock pulses UNTIL THE END OF THE BYTE in which it loses the arbitration." Permission, not
// obligation -- and it expires at a byte boundary, not at an arbitrary time.
//
// 4. MUST RESTART WHEN THE BUS IS FREE. "...and must restart its transaction when the bus is
// free." Not immediately, and not never: the transaction is owed, and it is owed later.
//
// 5. IF IT HAS A SLAVE FUNCTION AND LOST WHILE ADDRESSING, BECOME A SLAVE IMMEDIATELY. "If a
// master also incorporates a slave function and it loses arbitration during the addressing
// stage, IT IS POSSIBLE THAT THE WINNING MASTER IS TRYING TO ADDRESS IT. The losing master must
// therefore switch over immediately to its slave mode."
//
// Obligation 5 is the one that is almost always missed, and the reason is worth stating: the losing
// master was transmitting an address, so the bits it was putting on the wire were an address -- and
// the winner's address, which differs only from the losing bit onward, may be THIS DEVICE'S OWN. A
// combined master-slave that does not switch over will NACK a transfer directed at itself, and the
// winner will conclude the device is absent. Mutation D5 is that omission.
//
// Note what obligations 3 and 4 are NOT. The loser does not abort, does not signal an error on the
// bus, and does not drive anything further. Its clock pulses are indistinguishable from the winner's
// because the wired-AND has already merged them (Chapter 13.2), so "keep clocking" costs the bus
// nothing and saves the loser from having to stop mid-byte.
module i2c_arb_loss_handler #(
parameter int TICK_W = 16,
// Whether this master also implements a slave. Obligation 5 applies only if it does -- and a
// device WITHOUT a slave function must NOT switch, because it has no slave to switch to.
parameter bit HAS_SLAVE = 1'b1
)(
input logic clk,
input logic rst_n,
// the observed bus
input logic scl_in,
input logic sda_in,
// this master's intent for the current bit, and where it is in the transfer
input logic sda_intent, // 1 = wants to release (send a one)
input logic in_addressing, // 1 = the byte being sent is the address byte
input logic bus_free, // from the bus-free monitor of Chapter 13.1
input logic begin_transfer, // pulse: this master starts a transaction
// ---- what this master drives ----
output logic sda_drive_low,
output logic scl_may_clock, // obligation 3: permission, bounded by the byte
// ---- what it discovered and owes ----
output logic lost,
output logic loss_pulse,
output logic switch_to_slave, // obligation 5
output logic restart_pending, // obligation 4
output logic restart_pulse,
output logic [2:0] state,
output logic [3:0] bits_this_byte,
output logic [TICK_W-1:0] n_losses,
output logic [TICK_W-1:0] n_restarts
);
localparam logic [2:0] S_IDLE = 3'd0,
S_ACTIVE = 3'd1,
S_FINISH = 3'd2, // lost; clocking out the rest of the byte
S_WAIT = 3'd3, // byte done; owing a restart
S_SLAVE = 3'd4; // obligation 5
logic scl_q;
wire scl_rise = scl_in && !scl_q;
wire scl_fall = !scl_in && scl_q;
// Obligation 1: the asymmetric test, sampled while SCL is HIGH. Combinational, because it is a
// statement about the present state of two signals.
wire losing_now = (state == S_ACTIVE) && scl_in && sda_intent && !sda_in;
always_ff @(posedge clk) begin
if (!rst_n) begin
scl_q <= 1'b1;
state <= S_IDLE;
sda_drive_low <= 1'b0;
scl_may_clock <= 1'b0;
lost <= 1'b0;
loss_pulse <= 1'b0;
switch_to_slave <= 1'b0;
restart_pending <= 1'b0;
restart_pulse <= 1'b0;
bits_this_byte <= 4'd0;
n_losses <= '0;
n_restarts <= '0;
end else begin
scl_q <= scl_in;
loss_pulse <= 1'b0;
restart_pulse <= 1'b0;
// Obligation 2: the driver follows the intent ONLY while active. Expressed here rather
// than in a separate branch so that there is no state in which `lost` is set and the
// driver is still on -- mutation D2 delays this by a cycle and is caught at the pin.
sda_drive_low <= (state == S_ACTIVE) && !sda_intent && !losing_now;
case (state)
S_IDLE: begin
scl_may_clock <= 1'b0;
if (begin_transfer) begin
state <= S_ACTIVE;
scl_may_clock <= 1'b1;
lost <= 1'b0;
bits_this_byte <= 4'd0;
end
end
S_ACTIVE: begin
if (scl_fall) bits_this_byte <= bits_this_byte + 4'd1;
if (losing_now) begin
lost <= 1'b1;
loss_pulse <= 1'b1;
n_losses <= n_losses + 1'b1;
// Obligation 5, and it is decided HERE -- at the moment of loss, using
// where in the transfer the loss happened. Deciding later would need the
// phase to be remembered, and the phase has moved on by then.
if (HAS_SLAVE && in_addressing) begin
switch_to_slave <= 1'b1;
scl_may_clock <= 1'b0; // a slave does not drive the clock
state <= S_SLAVE;
end else begin
// Obligation 3: permission to keep clocking to the end of this byte.
scl_may_clock <= 1'b1;
state <= S_FINISH;
end
end
end
S_FINISH: begin
// Clocking out the remainder of the byte. Nothing is driven on SDA.
if (scl_fall) begin
bits_this_byte <= bits_this_byte + 4'd1;
// Nine bits per byte including the acknowledge: permission expires here.
if (bits_this_byte >= 4'd8) begin
scl_may_clock <= 1'b0;
restart_pending <= 1'b1; // obligation 4
state <= S_WAIT;
end
end
end
S_WAIT: begin
// Obligation 4: restart WHEN THE BUS IS FREE. Not before, and not never.
if (bus_free) begin
restart_pending <= 1'b0;
restart_pulse <= 1'b1;
n_restarts <= n_restarts + 1'b1;
lost <= 1'b0;
bits_this_byte <= 4'd0;
scl_may_clock <= 1'b1;
state <= S_ACTIVE;
end
end
S_SLAVE: begin
// Obligation 5's consequence: this device is now a slave for the rest of the
// transfer the winner is conducting. It leaves slave mode at a STOP, which the
// surrounding design signals by deasserting in_addressing and asserting
// bus_free -- at which point the owed transaction may be retried.
if (bus_free) begin
switch_to_slave <= 1'b0;
restart_pending <= 1'b0;
restart_pulse <= 1'b1;
n_restarts <= n_restarts + 1'b1;
lost <= 1'b0;
bits_this_byte <= 4'd0;
scl_may_clock <= 1'b1;
state <= S_ACTIVE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule `timescale 1ns/1ps
// TWO instances on the same stimulus, because obligation 5 is CONDITIONAL on the device having a
// slave function -- and a suite with only one configuration cannot tell "always switch" from
// "switch when appropriate" from "never switch".
//
// dut_ms : HAS_SLAVE = 1 -- a combined master-slave. Must switch to slave mode on an
// addressing-stage loss.
// dut_m : HAS_SLAVE = 0 -- a master only. Must NOT switch, because it has no slave to switch to.
module i2c_arb_loss_handler_tb;
localparam int TICK_W = 16;
logic clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
logic scl = 1'b1, sda = 1'b1;
logic sda_intent = 1'b1, in_addressing = 1'b0, bus_free = 1'b0, begin_transfer = 1'b0;
logic ms_drv, ms_clk, ms_lost, ms_lp, ms_slave, ms_rp, ms_rpul;
logic [2:0] ms_state;
logic [3:0] ms_bits;
logic [TICK_W-1:0] ms_nl, ms_nr;
i2c_arb_loss_handler #(.TICK_W(TICK_W), .HAS_SLAVE(1'b1)) dut_ms (
.clk(clk), .rst_n(rst_n), .scl_in(scl), .sda_in(sda),
.sda_intent(sda_intent), .in_addressing(in_addressing), .bus_free(bus_free),
.begin_transfer(begin_transfer),
.sda_drive_low(ms_drv), .scl_may_clock(ms_clk),
.lost(ms_lost), .loss_pulse(ms_lp), .switch_to_slave(ms_slave),
.restart_pending(ms_rp), .restart_pulse(ms_rpul),
.state(ms_state), .bits_this_byte(ms_bits), .n_losses(ms_nl), .n_restarts(ms_nr));
logic m_drv, m_clk, m_lost, m_lp, m_slave, m_rp, m_rpul;
logic [2:0] m_state;
logic [3:0] m_bits;
logic [TICK_W-1:0] m_nl, m_nr;
i2c_arb_loss_handler #(.TICK_W(TICK_W), .HAS_SLAVE(1'b0)) dut_m (
.clk(clk), .rst_n(rst_n), .scl_in(scl), .sda_in(sda),
.sda_intent(sda_intent), .in_addressing(in_addressing), .bus_free(bus_free),
.begin_transfer(begin_transfer),
.sda_drive_low(m_drv), .scl_may_clock(m_clk),
.lost(m_lost), .loss_pulse(m_lp), .switch_to_slave(m_slave),
.restart_pending(m_rp), .restart_pulse(m_rpul),
.state(m_state), .bits_this_byte(m_bits), .n_losses(m_nl), .n_restarts(m_nr));
localparam logic [2:0] S_IDLE=3'd0, S_ACTIVE=3'd1, S_FINISH=3'd2, S_WAIT=3'd3, S_SLAVE=3'd4;
int errors = 0;
task automatic tick(input int n); begin repeat (n) @(negedge clk); end endtask
// One bit: SDA settles during the low phase, is read back during the high phase.
// `other_low` models the winning master pulling SDA down.
task automatic bit_cell(input logic want, input logic other_low);
begin
scl = 1'b0;
sda_intent = want;
sda = other_low ? 1'b0 : want; // the wired-AND as seen on the pin
tick(5);
scl = 1'b1; tick(6);
scl = 1'b0; tick(3);
end
endtask
task automatic start_xfer(input logic addressing);
begin
in_addressing = addressing;
bus_free = 1'b0;
scl = 1'b0; sda = 1'b1; sda_intent = 1'b1; tick(3);
begin_transfer = 1'b1; tick(1); begin_transfer = 1'b0; tick(2);
end
endtask
task automatic go_free(); begin
scl = 1'b1; sda = 1'b1; sda_intent = 1'b1; in_addressing = 1'b0;
bus_free = 1'b1; tick(6);
end endtask
initial begin
tick(4); rst_n = 1'b1; tick(4);
// ---- 1. reset ------------------------------------------------------------------------
if (ms_state !== S_IDLE || m_state !== S_IDLE) begin
$display("FAIL: not idle out of reset"); errors++; end
if (ms_nl !== '0 || ms_nr !== '0) begin
$display("FAIL: counters nonzero out of reset"); errors++; end
if (ms_drv !== 1'b0 || ms_clk !== 1'b0) begin
$display("FAIL: driving or clocking out of reset"); errors++; end
// ---- 2. an uncontested transfer: no loss, still driving ------------------------------
// The negative case first. A handler that always declared a loss would pass every test below.
start_xfer(1'b0);
bit_cell(1'b0, 1'b0); // sends 0, sees 0 -- proves NOTHING, must not be a loss
bit_cell(1'b1, 1'b0); // sends 1, sees 1 -- fine
if (ms_lost !== 1'b0) begin
$display("FAIL: an uncontested transfer reported a loss"); errors++; end
if (ms_state !== S_ACTIVE) begin
$display("FAIL: left ACTIVE with no loss (state %0d)", ms_state); errors++; end
// ---- 3. sending 0 and seeing 0 is NEVER a loss ---------------------------------------
// The asymmetry of section 3.1.8. Another master pulling the line low while this one also
// wants it low is indistinguishable from being alone -- and is therefore not evidence.
begin
int l0; l0 = ms_nl;
bit_cell(1'b0, 1'b1); // this master sends 0; the other also pulls low
if (ms_nl != l0) begin
$display("FAIL: sending 0 while another master pulled low was reported as a loss -- the test must be ASYMMETRIC");
errors++; end
end
// ---- 4. OBLIGATION 1 and 2: sent 1, saw 0 -> lost, and the driver is off -------------
begin
int l0; l0 = ms_nl;
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; // we want 1; the winner holds it low
tick(4);
scl = 1'b1; tick(2);
if (ms_lost !== 1'b1) begin
$display("FAIL: sent 1 and observed 0 while SCL was high, but no loss was reported");
errors++; end
if (ms_nl != l0 + 1) begin
$display("FAIL: the loss was not counted"); errors++; end
if (ms_drv !== 1'b0) begin
$display("FAIL: the SDA driver was still on after losing -- section 3.1.8 says it is switched off THE MOMENT a difference appears");
errors++; end
end
// ---- 5. OBLIGATION 3: may keep clocking, to the END OF THE BYTE ----------------------
// Permission, and it expires at a byte boundary rather than immediately or indefinitely.
if (ms_clk !== 1'b1) begin
$display("FAIL: clocking permission was withdrawn immediately on loss -- the loser MAY clock to the end of the byte");
errors++; end
if (ms_state !== S_FINISH) begin
$display("FAIL: expected S_FINISH after a non-addressing loss, got %0d", ms_state);
errors++; end
// Clock out the rest of the byte. Nothing may be driven on SDA throughout.
begin
int k;
for (k = 0; k < 9; k++) begin
if (ms_drv !== 1'b0) begin
$display("FAIL: the loser drove SDA during the remainder of the byte"); errors++; end
bit_cell(1'b1, 1'b1);
end
if (ms_clk !== 1'b0) begin
$display("FAIL: clocking permission outlasted the byte -- it expires at the byte boundary");
errors++; end
if (ms_state !== S_WAIT) begin
$display("FAIL: expected S_WAIT after the byte completed, got %0d", ms_state);
errors++; end
if (ms_rp !== 1'b1) begin
$display("FAIL: restart_pending not set -- the transaction is still owed"); errors++; end
end
// ---- 6. OBLIGATION 4: restart only WHEN THE BUS IS FREE -------------------------------
begin
int r0; r0 = ms_nr;
bus_free = 1'b0; tick(20);
if (ms_nr != r0) begin
$display("FAIL: restarted while the bus was busy"); errors++; end
if (ms_state !== S_WAIT) begin
$display("FAIL: left S_WAIT with the bus busy"); errors++; end
go_free();
if (ms_nr != r0 + 1) begin
$display("FAIL: did not restart once the bus was free"); errors++; end
if (ms_state !== S_ACTIVE) begin
$display("FAIL: expected S_ACTIVE after restarting, got %0d", ms_state); errors++; end
if (ms_rp !== 1'b0) begin
$display("FAIL: restart_pending still set after restarting"); errors++; end
end
// ---- 7. OBLIGATION 5: lost while ADDRESSING, with a slave function ------------------
// "It is possible that the winning master is trying to address it. The losing master must
// therefore switch over immediately to its slave mode."
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b1); // in_addressing = 1
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(4);
scl = 1'b1; tick(2);
if (ms_slave !== 1'b1) begin
$display("FAIL: a combined master-slave that lost during ADDRESSING did not switch to slave mode -- the winner may be addressing it");
errors++; end
if (ms_state !== S_SLAVE) begin
$display("FAIL: expected S_SLAVE after an addressing-stage loss, got %0d", ms_state);
errors++; end
if (ms_clk !== 1'b0) begin
$display("FAIL: still asserting clocking permission in slave mode -- a slave does not drive the clock");
errors++; end
// ---- 8. the CONTROL for obligation 5: a master WITHOUT a slave must not switch --------
// Same stimulus, same cycle, different configuration. Without this the check could be
// "always switch when addressing", which would be wrong for a master-only device.
if (m_slave !== 1'b0) begin
$display("FAIL: a master-only device switched to slave mode -- it has no slave to switch to");
errors++; end
if (m_state !== S_FINISH) begin
$display("FAIL: the master-only device should be finishing its byte, got state %0d",
m_state); errors++; end
// ---- 9. a loss OUTSIDE addressing must not switch, even with a slave function ---------
// The other half of obligation 5's condition. A data-phase loss carries no risk that the
// winner is addressing this device, so switching would be wrong.
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b0); // NOT addressing
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(4);
scl = 1'b1; tick(2);
if (ms_slave !== 1'b0) begin
$display("FAIL: a combined master-slave switched to slave mode after a DATA-phase loss");
errors++; end
if (ms_state !== S_FINISH) begin
$display("FAIL: expected S_FINISH after a data-phase loss, got %0d", ms_state); errors++; end
// ---- 10. the slave-mode device rejoins and still owes its transaction -----------------
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b1);
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(4);
scl = 1'b1; tick(2);
begin
int r0; r0 = ms_nr;
go_free();
if (ms_nr != r0 + 1) begin
$display("FAIL: the device did not retry its owed transaction after leaving slave mode");
errors++; end
if (ms_slave !== 1'b0) begin
$display("FAIL: still in slave mode after the bus went free"); errors++; end
end
// ---- 11. the loser never drives SDA again before restarting ---------------------------
// Swept across the whole post-loss period rather than sampled once, because obligation 2 is
// about every cycle and a single sample would miss a one-cycle glitch.
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b0);
scl = 1'b0; sda_intent = 1'b0; sda = 1'b1; tick(3); // driving low legitimately
scl = 1'b1; tick(3);
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(3); // now lose
scl = 1'b1; tick(3);
begin
int k;
for (k = 0; k < 40; k++) begin
sda_intent = (k % 3 == 0) ? 1'b0 : 1'b1; // intent keeps changing; irrelevant
tick(1);
if (ms_drv !== 1'b0) begin
$display("FAIL: the loser drove SDA low %0d cycles after losing, with intent=%b",
k, sda_intent); errors++; k = 40; end
end
end
if (errors == 0)
$display("PASS: all five obligations hold -- asymmetric detection, the driver off the moment a difference appears, clocking permitted only to the byte boundary, a restart owed until the bus is free, and slave mode entered only for an addressing-stage loss by a device that has a slave");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
#2000000;
$display("FAIL: watchdog expired");
$finish;
end
endmodule // LOSING ARBITRATION CORRECTLY. Section 3.1.8 does not simply say "the loser stops". It places FIVE
// distinct obligations on the losing master, and they are easy to read past because they are spread
// across a paragraph:
//
// 1. DETECT. "The first time a master tries to send a HIGH, but detects that the SDA level is LOW,
// the master knows that it has lost the arbitration" -- and the check is made "while SCL is
// HIGH".
//
// 2. STOP DRIVING, IMMEDIATELY. "...and turns off its SDA output driver." Restated later with the
// timing made explicit: "THE MOMENT there is a difference between the internal data level of
// the master generating DATA 1 and the actual level on the SDA line, the DATA 1 output is
// switched off."
//
// 3. MAY KEEP CLOCKING, to a bounded point. "A master that loses the arbitration CAN generate
// clock pulses UNTIL THE END OF THE BYTE in which it loses the arbitration." Permission, not
// obligation -- and it expires at a byte boundary, not at an arbitrary time.
//
// 4. MUST RESTART WHEN THE BUS IS FREE. "...and must restart its transaction when the bus is
// free." Not immediately, and not never: the transaction is owed, and it is owed later.
//
// 5. IF IT HAS A SLAVE FUNCTION AND LOST WHILE ADDRESSING, BECOME A SLAVE IMMEDIATELY. "If a
// master also incorporates a slave function and it loses arbitration during the addressing
// stage, IT IS POSSIBLE THAT THE WINNING MASTER IS TRYING TO ADDRESS IT. The losing master must
// therefore switch over immediately to its slave mode."
//
// Obligation 5 is the one that is almost always missed, and the reason is worth stating: the losing
// master was transmitting an address, so the bits it was putting on the wire were an address -- and
// the winner's address, which differs only from the losing bit onward, may be THIS DEVICE'S OWN. A
// combined master-slave that does not switch over will NACK a transfer directed at itself, and the
// winner will conclude the device is absent. Mutation D5 is that omission.
//
// Note what obligations 3 and 4 are NOT. The loser does not abort, does not signal an error on the
// bus, and does not drive anything further. Its clock pulses are indistinguishable from the winner's
// because the wired-AND has already merged them (Chapter 13.2), so "keep clocking" costs the bus
// nothing and saves the loser from having to stop mid-byte.
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_arb_loss_handler #(
parameter TICK_W = 16,
// Whether this master also implements a slave. Obligation 5 applies only if it does -- and a
// device WITHOUT a slave function must NOT switch, because it has no slave to switch to.
parameter HAS_SLAVE = 1'b1
)(
input wire clk,
input wire rst_n,
// the observed bus
input wire scl_in,
input wire sda_in,
// this master's intent for the current bit, and where it is in the transfer
input wire sda_intent, // 1 = wants to release (send a one)
input wire in_addressing, // 1 = the byte being sent is the address byte
input wire bus_free, // from the bus-free monitor of Chapter 13.1
input wire begin_transfer, // pulse: this master starts a transaction
// ---- what this master drives ----
output reg sda_drive_low,
output reg scl_may_clock, // obligation 3: permission, bounded by the byte
// ---- what it discovered and owes ----
output reg lost,
output reg loss_pulse,
output reg switch_to_slave, // obligation 5
output reg restart_pending, // obligation 4
output reg restart_pulse,
output reg [2:0] state,
output reg [3:0] bits_this_byte,
output reg [TICK_W-1:0] n_losses,
output reg [TICK_W-1:0] n_restarts
);
localparam [2:0] S_IDLE = 3'd0,
S_ACTIVE = 3'd1,
S_FINISH = 3'd2, // lost; clocking out the rest of the byte
S_WAIT = 3'd3, // byte done; owing a restart
S_SLAVE = 3'd4; // obligation 5
reg scl_q;
wire scl_rise = scl_in && !scl_q;
wire scl_fall = !scl_in && scl_q;
// Obligation 1: the asymmetric test, sampled while SCL is HIGH. Combinational, because it is a
// statement about the present state of two signals.
wire losing_now = (state == S_ACTIVE) && scl_in && sda_intent && !sda_in;
always @(posedge clk) begin
if (!rst_n) begin
scl_q <= 1'b1;
state <= S_IDLE;
sda_drive_low <= 1'b0;
scl_may_clock <= 1'b0;
lost <= 1'b0;
loss_pulse <= 1'b0;
switch_to_slave <= 1'b0;
restart_pending <= 1'b0;
restart_pulse <= 1'b0;
bits_this_byte <= 4'd0;
n_losses <= {TICK_W{1'b0}};
n_restarts <= {TICK_W{1'b0}};
end else begin
scl_q <= scl_in;
loss_pulse <= 1'b0;
restart_pulse <= 1'b0;
// Obligation 2: the driver follows the intent ONLY while active. Expressed here rather
// than in a separate branch so that there is no state in which `lost` is set and the
// driver is still on -- mutation D2 delays this by a cycle and is caught at the pin.
sda_drive_low <= (state == S_ACTIVE) && !sda_intent && !losing_now;
case (state)
S_IDLE: begin
scl_may_clock <= 1'b0;
if (begin_transfer) begin
state <= S_ACTIVE;
scl_may_clock <= 1'b1;
lost <= 1'b0;
bits_this_byte <= 4'd0;
end
end
S_ACTIVE: begin
if (scl_fall) bits_this_byte <= bits_this_byte + 4'd1;
if (losing_now) begin
lost <= 1'b1;
loss_pulse <= 1'b1;
n_losses <= n_losses + 1'b1;
// Obligation 5, and it is decided HERE -- at the moment of loss, using
// where in the transfer the loss happened. Deciding later would need the
// phase to be remembered, and the phase has moved on by then.
if (HAS_SLAVE && in_addressing) begin
switch_to_slave <= 1'b1;
scl_may_clock <= 1'b0; // a slave does not drive the clock
state <= S_SLAVE;
end else begin
// Obligation 3: permission to keep clocking to the end of this byte.
scl_may_clock <= 1'b1;
state <= S_FINISH;
end
end
end
S_FINISH: begin
// Clocking out the remainder of the byte. Nothing is driven on SDA.
if (scl_fall) begin
bits_this_byte <= bits_this_byte + 4'd1;
// Nine bits per byte including the acknowledge: permission expires here.
if (bits_this_byte >= 4'd8) begin
scl_may_clock <= 1'b0;
restart_pending <= 1'b1; // obligation 4
state <= S_WAIT;
end
end
end
S_WAIT: begin
// Obligation 4: restart WHEN THE BUS IS FREE. Not before, and not never.
if (bus_free) begin
restart_pending <= 1'b0;
restart_pulse <= 1'b1;
n_restarts <= n_restarts + 1'b1;
lost <= 1'b0;
bits_this_byte <= 4'd0;
scl_may_clock <= 1'b1;
state <= S_ACTIVE;
end
end
S_SLAVE: begin
// Obligation 5's consequence: this device is now a slave for the rest of the
// transfer the winner is conducting. It leaves slave mode at a STOP, which the
// surrounding design signals by deasserting in_addressing and asserting
// bus_free -- at which point the owed transaction may be retried.
if (bus_free) begin
switch_to_slave <= 1'b0;
restart_pending <= 1'b0;
restart_pulse <= 1'b1;
n_restarts <= n_restarts + 1'b1;
lost <= 1'b0;
bits_this_byte <= 4'd0;
scl_may_clock <= 1'b1;
state <= S_ACTIVE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule `timescale 1ns/1ps
// TWO instances on the same stimulus, because obligation 5 is CONDITIONAL on the device having a
// slave function -- and a suite with only one configuration cannot tell "always switch" from
// "switch when appropriate" from "never switch".
//
// dut_ms : HAS_SLAVE = 1 -- a combined master-slave. Must switch to slave mode on an
// addressing-stage loss.
// dut_m : HAS_SLAVE = 0 -- a master only. Must NOT switch, because it has no slave to switch to.
// (Verilog-2001 testbench -- same stimulus, same checks.)
module i2c_arb_loss_handler_tb;
localparam TICK_W = 16;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg scl = 1'b1, sda = 1'b1;
reg sda_intent = 1'b1, in_addressing = 1'b0, bus_free = 1'b0, begin_transfer = 1'b0;
wire ms_drv, ms_clk, ms_lost, ms_lp, ms_slave, ms_rp, ms_rpul;
wire [2:0] ms_state;
wire [3:0] ms_bits;
wire [TICK_W-1:0] ms_nl, ms_nr;
i2c_arb_loss_handler #(.TICK_W(TICK_W), .HAS_SLAVE(1'b1)) dut_ms (
.clk(clk), .rst_n(rst_n), .scl_in(scl), .sda_in(sda),
.sda_intent(sda_intent), .in_addressing(in_addressing), .bus_free(bus_free),
.begin_transfer(begin_transfer),
.sda_drive_low(ms_drv), .scl_may_clock(ms_clk),
.lost(ms_lost), .loss_pulse(ms_lp), .switch_to_slave(ms_slave),
.restart_pending(ms_rp), .restart_pulse(ms_rpul),
.state(ms_state), .bits_this_byte(ms_bits), .n_losses(ms_nl), .n_restarts(ms_nr));
wire m_drv, m_clk, m_lost, m_lp, m_slave, m_rp, m_rpul;
wire [2:0] m_state;
wire [3:0] m_bits;
wire [TICK_W-1:0] m_nl, m_nr;
i2c_arb_loss_handler #(.TICK_W(TICK_W), .HAS_SLAVE(1'b0)) dut_m (
.clk(clk), .rst_n(rst_n), .scl_in(scl), .sda_in(sda),
.sda_intent(sda_intent), .in_addressing(in_addressing), .bus_free(bus_free),
.begin_transfer(begin_transfer),
.sda_drive_low(m_drv), .scl_may_clock(m_clk),
.lost(m_lost), .loss_pulse(m_lp), .switch_to_slave(m_slave),
.restart_pending(m_rp), .restart_pulse(m_rpul),
.state(m_state), .bits_this_byte(m_bits), .n_losses(m_nl), .n_restarts(m_nr));
localparam [2:0] S_IDLE=3'd0, S_ACTIVE=3'd1, S_FINISH=3'd2, S_WAIT=3'd3, S_SLAVE=3'd4;
integer errors = 0;
// Hoisted to module scope: Verilog-2001 permits a variable declaration only at
// module level or in a NAMED block, and every call site below is sequential.
integer l0 = 0;
integer r0 = 0;
integer k = 0;
task tick(input integer n); begin repeat (n) @(negedge clk); end endtask
// One bit: SDA settles during the low phase, is read back during the high phase.
// `other_low` models the winning master pulling SDA down.
task bit_cell(input logic want, input logic other_low);
begin
scl = 1'b0;
sda_intent = want;
sda = other_low ? 1'b0 : want; // the wired-AND as seen on the pin
tick(5);
scl = 1'b1; tick(6);
scl = 1'b0; tick(3);
end
endtask
task start_xfer(input logic addressing);
begin
in_addressing = addressing;
bus_free = 1'b0;
scl = 1'b0; sda = 1'b1; sda_intent = 1'b1; tick(3);
begin_transfer = 1'b1; tick(1); begin_transfer = 1'b0; tick(2);
end
endtask
task go_free(); begin
scl = 1'b1; sda = 1'b1; sda_intent = 1'b1; in_addressing = 1'b0;
bus_free = 1'b1; tick(6);
end endtask
initial begin
tick(4); rst_n = 1'b1; tick(4);
// ---- 1. reset ------------------------------------------------------------------------
if (ms_state !== S_IDLE || m_state !== S_IDLE) begin
$display("FAIL: not idle out of reset"); errors = errors + 1; end
if (ms_nl !== {TICK_W{1'b0}} || ms_nr !== {TICK_W{1'b0}}) begin
$display("FAIL: counters nonzero out of reset"); errors = errors + 1; end
if (ms_drv !== 1'b0 || ms_clk !== 1'b0) begin
$display("FAIL: driving or clocking out of reset"); errors = errors + 1; end
// ---- 2. an uncontested transfer: no loss, still driving ------------------------------
// The negative case first. A handler that always declared a loss would pass every test below.
start_xfer(1'b0);
bit_cell(1'b0, 1'b0); // sends 0, sees 0 -- proves NOTHING, must not be a loss
bit_cell(1'b1, 1'b0); // sends 1, sees 1 -- fine
if (ms_lost !== 1'b0) begin
$display("FAIL: an uncontested transfer reported a loss"); errors = errors + 1; end
if (ms_state !== S_ACTIVE) begin
$display("FAIL: left ACTIVE with no loss (state %0d)", ms_state); errors = errors + 1; end
// ---- 3. sending 0 and seeing 0 is NEVER a loss ---------------------------------------
// The asymmetry of section 3.1.8. Another master pulling the line low while this one also
// wants it low is indistinguishable from being alone -- and is therefore not evidence.
begin
l0 = ms_nl;
bit_cell(1'b0, 1'b1); // this master sends 0; the other also pulls low
if (ms_nl != l0) begin
$display("FAIL: sending 0 while another master pulled low was reported as a loss -- the test must be ASYMMETRIC");
errors = errors + 1; end
end
// ---- 4. OBLIGATION 1 and 2: sent 1, saw 0 -> lost, and the driver is off -------------
begin
l0 = ms_nl;
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; // we want 1; the winner holds it low
tick(4);
scl = 1'b1; tick(2);
if (ms_lost !== 1'b1) begin
$display("FAIL: sent 1 and observed 0 while SCL was high, but no loss was reported");
errors = errors + 1; end
if (ms_nl != l0 + 1) begin
$display("FAIL: the loss was not counted"); errors = errors + 1; end
if (ms_drv !== 1'b0) begin
$display("FAIL: the SDA driver was still on after losing -- section 3.1.8 says it is switched off THE MOMENT a difference appears");
errors = errors + 1; end
end
// ---- 5. OBLIGATION 3: may keep clocking, to the END OF THE BYTE ----------------------
// Permission, and it expires at a byte boundary rather than immediately or indefinitely.
if (ms_clk !== 1'b1) begin
$display("FAIL: clocking permission was withdrawn immediately on loss -- the loser MAY clock to the end of the byte");
errors = errors + 1; end
if (ms_state !== S_FINISH) begin
$display("FAIL: expected S_FINISH after a non-addressing loss, got %0d", ms_state);
errors = errors + 1; end
// Clock out the rest of the byte. Nothing may be driven on SDA throughout.
begin
for (k = 0; k < 9; k = k + 1) begin
if (ms_drv !== 1'b0) begin
$display("FAIL: the loser drove SDA during the remainder of the byte"); errors = errors + 1; end
bit_cell(1'b1, 1'b1);
end
if (ms_clk !== 1'b0) begin
$display("FAIL: clocking permission outlasted the byte -- it expires at the byte boundary");
errors = errors + 1; end
if (ms_state !== S_WAIT) begin
$display("FAIL: expected S_WAIT after the byte completed, got %0d", ms_state);
errors = errors + 1; end
if (ms_rp !== 1'b1) begin
$display("FAIL: restart_pending not set -- the transaction is still owed"); errors = errors + 1; end
end
// ---- 6. OBLIGATION 4: restart only WHEN THE BUS IS FREE -------------------------------
begin
r0 = ms_nr;
bus_free = 1'b0; tick(20);
if (ms_nr != r0) begin
$display("FAIL: restarted while the bus was busy"); errors = errors + 1; end
if (ms_state !== S_WAIT) begin
$display("FAIL: left S_WAIT with the bus busy"); errors = errors + 1; end
go_free();
if (ms_nr != r0 + 1) begin
$display("FAIL: did not restart once the bus was free"); errors = errors + 1; end
if (ms_state !== S_ACTIVE) begin
$display("FAIL: expected S_ACTIVE after restarting, got %0d", ms_state); errors = errors + 1; end
if (ms_rp !== 1'b0) begin
$display("FAIL: restart_pending still set after restarting"); errors = errors + 1; end
end
// ---- 7. OBLIGATION 5: lost while ADDRESSING, with a slave function ------------------
// "It is possible that the winning master is trying to address it. The losing master must
// therefore switch over immediately to its slave mode."
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b1); // in_addressing = 1
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(4);
scl = 1'b1; tick(2);
if (ms_slave !== 1'b1) begin
$display("FAIL: a combined master-slave that lost during ADDRESSING did not switch to slave mode -- the winner may be addressing it");
errors = errors + 1; end
if (ms_state !== S_SLAVE) begin
$display("FAIL: expected S_SLAVE after an addressing-stage loss, got %0d", ms_state);
errors = errors + 1; end
if (ms_clk !== 1'b0) begin
$display("FAIL: still asserting clocking permission in slave mode -- a slave does not drive the clock");
errors = errors + 1; end
// ---- 8. the CONTROL for obligation 5: a master WITHOUT a slave must not switch --------
// Same stimulus, same cycle, different configuration. Without this the check could be
// "always switch when addressing", which would be wrong for a master-only device.
if (m_slave !== 1'b0) begin
$display("FAIL: a master-only device switched to slave mode -- it has no slave to switch to");
errors = errors + 1; end
if (m_state !== S_FINISH) begin
$display("FAIL: the master-only device should be finishing its byte, got state %0d",
m_state); errors = errors + 1; end
// ---- 9. a loss OUTSIDE addressing must not switch, even with a slave function ---------
// The other half of obligation 5's condition. A data-phase loss carries no risk that the
// winner is addressing this device, so switching would be wrong.
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b0); // NOT addressing
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(4);
scl = 1'b1; tick(2);
if (ms_slave !== 1'b0) begin
$display("FAIL: a combined master-slave switched to slave mode after a DATA-phase loss");
errors = errors + 1; end
if (ms_state !== S_FINISH) begin
$display("FAIL: expected S_FINISH after a data-phase loss, got %0d", ms_state); errors = errors + 1; end
// ---- 10. the slave-mode device rejoins and still owes its transaction -----------------
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b1);
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(4);
scl = 1'b1; tick(2);
begin
r0 = ms_nr;
go_free();
if (ms_nr != r0 + 1) begin
$display("FAIL: the device did not retry its owed transaction after leaving slave mode");
errors = errors + 1; end
if (ms_slave !== 1'b0) begin
$display("FAIL: still in slave mode after the bus went free"); errors = errors + 1; end
end
// ---- 11. the loser never drives SDA again before restarting ---------------------------
// Swept across the whole post-loss period rather than sampled once, because obligation 2 is
// about every cycle and a single sample would miss a one-cycle glitch.
rst_n = 1'b0; tick(3); rst_n = 1'b1; tick(3);
start_xfer(1'b0);
scl = 1'b0; sda_intent = 1'b0; sda = 1'b1; tick(3); // driving low legitimately
scl = 1'b1; tick(3);
scl = 1'b0; sda_intent = 1'b1; sda = 1'b0; tick(3); // now lose
scl = 1'b1; tick(3);
begin
for (k = 0; k < 40; k = k + 1) begin
sda_intent = (k % 3 == 0) ? 1'b0 : 1'b1; // intent keeps changing; irrelevant
tick(1);
if (ms_drv !== 1'b0) begin
$display("FAIL: the loser drove SDA low %0d cycles after losing, with intent=%b",
k, sda_intent); errors = errors + 1; k = 40; end
end
end
if (errors == 0)
$display("PASS: all five obligations hold -- asymmetric detection, the driver off the moment a difference appears, clocking permitted only to the byte boundary, a restart owed until the bus is free, and slave mode entered only for an addressing-stage loss by a device that has a slave");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
#2000000;
$display("FAIL: watchdog expired");
$finish;
end
endmodule -- LOSING ARBITRATION CORRECTLY -- the VHDL form. The same five obligations section 3.1.8 places on a
-- losing master:
--
-- 1. DETECT: sent HIGH, observed LOW, while SCL is HIGH.
-- 2. STOP DRIVING, "THE MOMENT there is a difference".
-- 3. MAY keep clocking, "UNTIL THE END OF THE BYTE in which it loses".
-- 4. MUST "restart its transaction when the bus is free".
-- 5. If it has a slave function and lost during ADDRESSING, "switch over immediately to its slave
-- mode" -- because "it is possible that the winning master is trying to address it".
--
-- Obligation 5 is the one usually missed: the losing master was transmitting an address, and the
-- winner's address may be this device's own. A combined master-slave that does not switch over will
-- NACK a transfer directed at itself.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_arb_loss_handler is
generic (
TICK_W : natural := 16;
-- Obligation 5 applies only to a device that HAS a slave. A master-only device must not
-- switch, because it has no slave to switch to.
HAS_SLAVE : std_logic := '1'
);
port (
clk : in std_logic;
rst_n : in std_logic;
scl_in : in std_logic;
sda_in : in std_logic;
sda_intent : in std_logic; -- 1 = wants to release (send a one)
in_addressing : in std_logic;
bus_free : in std_logic;
begin_transfer : in std_logic;
sda_drive_low : out std_logic;
scl_may_clock : out std_logic;
lost : out std_logic;
loss_pulse : out std_logic;
switch_to_slave : out std_logic;
restart_pending : out std_logic;
restart_pulse : out std_logic;
state : out unsigned(2 downto 0);
bits_this_byte : out unsigned(3 downto 0);
n_losses : out unsigned(TICK_W-1 downto 0);
n_restarts : out unsigned(TICK_W-1 downto 0)
);
end entity;
architecture rtl of i2c_arb_loss_handler is
constant S_IDLE : unsigned(2 downto 0) := "000";
constant S_ACTIVE : unsigned(2 downto 0) := "001";
constant S_FINISH : unsigned(2 downto 0) := "010";
constant S_WAIT : unsigned(2 downto 0) := "011";
constant S_SLAVE : unsigned(2 downto 0) := "100";
-- NOTE: VHDL is case-INSENSITIVE, so a signal named s_slave would collide with the
-- constant S_SLAVE. SystemVerilog would have treated them as distinct names.
signal st : unsigned(2 downto 0) := S_IDLE;
signal scl_q : std_logic := '1';
signal fall_s : std_logic;
signal losing : std_logic;
signal s_drv, s_clk, s_lost, s_lp, s_to_slave, s_rp, s_rpul : std_logic := '0';
signal s_bits : unsigned(3 downto 0) := (others => '0');
signal r_nl, r_nr : unsigned(TICK_W-1 downto 0) := (others => '0');
begin
fall_s <= '1' when (scl_in = '0' and scl_q = '1') else '0';
-- Obligation 1: the asymmetric test, sampled while SCL is HIGH.
losing <= '1' when (st = S_ACTIVE and scl_in = '1' and sda_intent = '1' and sda_in = '0')
else '0';
sda_drive_low <= s_drv;
scl_may_clock <= s_clk;
lost <= s_lost;
loss_pulse <= s_lp;
switch_to_slave <= s_to_slave;
restart_pending <= s_rp;
restart_pulse <= s_rpul;
state <= st;
bits_this_byte <= s_bits;
n_losses <= r_nl;
n_restarts <= r_nr;
process (clk) is
begin
if rising_edge(clk) then
if rst_n = '0' then
scl_q <= '1';
st <= S_IDLE;
s_drv <= '0';
s_clk <= '0';
s_lost <= '0';
s_lp <= '0';
s_to_slave <= '0';
s_rp <= '0';
s_rpul <= '0';
s_bits <= (others => '0');
r_nl <= (others => '0');
r_nr <= (others => '0');
else
scl_q <= scl_in;
s_lp <= '0';
s_rpul <= '0';
-- Obligation 2: the driver follows the intent ONLY while active, and drops in the
-- same cycle a difference appears. There is no state in which `lost` is set and the
-- driver is still on.
if st = S_ACTIVE and sda_intent = '0' and losing = '0' then
s_drv <= '1';
else
s_drv <= '0';
end if;
if st = S_IDLE then
s_clk <= '0';
if begin_transfer = '1' then
st <= S_ACTIVE;
s_clk <= '1';
s_lost <= '0';
s_bits <= (others => '0');
end if;
elsif st = S_ACTIVE then
if fall_s = '1' then
s_bits <= s_bits + 1;
end if;
if losing = '1' then
s_lost <= '1';
s_lp <= '1';
r_nl <= r_nl + 1;
-- Obligation 5, decided HERE, using where in the transfer the loss happened.
if HAS_SLAVE = '1' and in_addressing = '1' then
s_to_slave <= '1';
s_clk <= '0'; -- a slave does not drive the clock
st <= S_SLAVE;
else
-- Obligation 3: permission to keep clocking to the end of this byte.
s_clk <= '1';
st <= S_FINISH;
end if;
end if;
elsif st = S_FINISH then
if fall_s = '1' then
s_bits <= s_bits + 1;
-- Nine bits per byte including the acknowledge: permission expires here.
if s_bits >= to_unsigned(8, 4) then
s_clk <= '0';
s_rp <= '1'; -- obligation 4
st <= S_WAIT;
end if;
end if;
elsif st = S_WAIT then
-- Obligation 4: restart WHEN THE BUS IS FREE. Not before, and not never.
if bus_free = '1' then
s_rp <= '0';
s_rpul <= '1';
r_nr <= r_nr + 1;
s_lost <= '0';
s_bits <= (others => '0');
s_clk <= '1';
st <= S_ACTIVE;
end if;
elsif st = S_SLAVE then
if bus_free = '1' then
s_to_slave <= '0';
s_rp <= '0';
s_rpul <= '1';
r_nr <= r_nr + 1;
s_lost <= '0';
s_bits <= (others => '0');
s_clk <= '1';
st <= S_ACTIVE;
end if;
else
st <= S_IDLE;
end if;
end if;
end if;
end process;
end architecture; -- The VHDL testbench. TWO instances on identical stimulus, because obligation 5 is CONDITIONAL on the
-- device having a slave function -- and one configuration cannot tell "always switch" from "switch
-- when appropriate" from "never switch".
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_arb_loss_handler_tb is
end entity;
architecture tb of i2c_arb_loss_handler_tb is
constant TICK_W : natural := 16;
constant S_IDLE : unsigned(2 downto 0) := "000";
constant S_ACTIVE : unsigned(2 downto 0) := "001";
constant S_FINISH : unsigned(2 downto 0) := "010";
constant S_WAIT : unsigned(2 downto 0) := "011";
constant S_SLV : unsigned(2 downto 0) := "100";
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal scl, sda : std_logic := '1';
signal sda_intent : std_logic := '1';
signal in_addressing: std_logic := '0';
signal bus_free : std_logic := '0';
signal begin_xfer : std_logic := '0';
signal ms_drv, ms_clk, ms_lost, ms_lp, ms_slv, ms_rp, ms_rpul : std_logic;
signal ms_state : unsigned(2 downto 0);
signal ms_bits : unsigned(3 downto 0);
signal ms_nl, ms_nr : unsigned(TICK_W-1 downto 0);
signal m_drv, m_clk, m_lost, m_lp, m_slv, m_rp, m_rpul : std_logic;
signal m_state : unsigned(2 downto 0);
signal m_bits : unsigned(3 downto 0);
signal m_nl, m_nr : unsigned(TICK_W-1 downto 0);
signal done : boolean := false;
signal errors : integer := 0;
begin
clk_gen : process is
begin
while not done loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
dut_ms : entity work.i2c_arb_loss_handler
generic map (TICK_W => TICK_W, HAS_SLAVE => '1')
port map (clk => clk, rst_n => rst_n, scl_in => scl, sda_in => sda,
sda_intent => sda_intent, in_addressing => in_addressing,
bus_free => bus_free, begin_transfer => begin_xfer,
sda_drive_low => ms_drv, scl_may_clock => ms_clk,
lost => ms_lost, loss_pulse => ms_lp, switch_to_slave => ms_slv,
restart_pending => ms_rp, restart_pulse => ms_rpul,
state => ms_state, bits_this_byte => ms_bits,
n_losses => ms_nl, n_restarts => ms_nr);
dut_m : entity work.i2c_arb_loss_handler
generic map (TICK_W => TICK_W, HAS_SLAVE => '0')
port map (clk => clk, rst_n => rst_n, scl_in => scl, sda_in => sda,
sda_intent => sda_intent, in_addressing => in_addressing,
bus_free => bus_free, begin_transfer => begin_xfer,
sda_drive_low => m_drv, scl_may_clock => m_clk,
lost => m_lost, loss_pulse => m_lp, switch_to_slave => m_slv,
restart_pending => m_rp, restart_pulse => m_rpul,
state => m_state, bits_this_byte => m_bits,
n_losses => m_nl, n_restarts => m_nr);
stim : process is
procedure tick(n : in integer) is
begin
for i in 1 to n loop
wait until falling_edge(clk);
end loop;
end procedure;
procedure chk(cond : in boolean; msg : in string) is
begin
if not cond then
report "FAIL: " & msg severity error;
errors <= errors + 1;
wait for 0 ns;
end if;
end procedure;
-- One bit. `other_low` models the winning master pulling SDA down.
procedure bit_cell(want : in std_logic; other_low : in std_logic) is
begin
scl <= '0';
sda_intent <= want;
if other_low = '1' then sda <= '0'; else sda <= want; end if;
tick(5);
scl <= '1'; tick(6);
scl <= '0'; tick(3);
end procedure;
procedure start_xfer(addressing : in std_logic) is
begin
in_addressing <= addressing;
bus_free <= '0';
scl <= '0'; sda <= '1'; sda_intent <= '1'; tick(3);
begin_xfer <= '1'; tick(1); begin_xfer <= '0'; tick(2);
end procedure;
procedure go_free is
begin
scl <= '1'; sda <= '1'; sda_intent <= '1'; in_addressing <= '0';
bus_free <= '1'; tick(6);
end procedure;
variable l0, r0 : integer;
begin
tick(4); rst_n <= '1'; tick(4);
-- 1. reset
chk(ms_state = S_IDLE and m_state = S_IDLE, "not idle out of reset");
chk(ms_nl = 0 and ms_nr = 0, "counters nonzero out of reset");
chk(ms_drv = '0' and ms_clk = '0', "driving or clocking out of reset");
-- 2. an uncontested transfer: no loss, still driving
start_xfer('0');
bit_cell('0', '0'); -- sends 0, sees 0 -- proves NOTHING
bit_cell('1', '0'); -- sends 1, sees 1 -- fine
chk(ms_lost = '0', "an uncontested transfer reported a loss");
chk(ms_state = S_ACTIVE, "left ACTIVE with no loss");
-- 3. sending 0 and seeing 0 is NEVER a loss
l0 := to_integer(ms_nl);
bit_cell('0', '1');
chk(to_integer(ms_nl) = l0,
"sending 0 while another master pulled low was reported as a loss -- the test must be ASYMMETRIC");
-- 4. OBLIGATIONS 1 and 2: sent 1, saw 0 -> lost, and the driver is off
l0 := to_integer(ms_nl);
scl <= '0'; sda_intent <= '1'; sda <= '0';
tick(4);
scl <= '1'; tick(2);
chk(ms_lost = '1', "sent 1 and observed 0 while SCL was high, but no loss was reported");
chk(to_integer(ms_nl) = l0 + 1, "the loss was not counted");
chk(ms_drv = '0',
"the SDA driver was still on after losing -- it is switched off THE MOMENT a difference appears");
-- 5. OBLIGATION 3: may keep clocking, to the END OF THE BYTE
chk(ms_clk = '1',
"clocking permission was withdrawn immediately -- the loser MAY clock to the end of the byte");
chk(ms_state = S_FINISH, "expected S_FINISH after a non-addressing loss");
for k in 0 to 8 loop
chk(ms_drv = '0', "the loser drove SDA during the remainder of the byte");
bit_cell('1', '1');
end loop;
chk(ms_clk = '0', "clocking permission outlasted the byte");
chk(ms_state = S_WAIT, "expected S_WAIT after the byte completed");
chk(ms_rp = '1', "restart_pending not set -- the transaction is still owed");
-- 6. OBLIGATION 4: restart only WHEN THE BUS IS FREE
r0 := to_integer(ms_nr);
bus_free <= '0'; tick(20);
chk(to_integer(ms_nr) = r0, "restarted while the bus was busy");
chk(ms_state = S_WAIT, "left S_WAIT with the bus busy");
go_free;
chk(to_integer(ms_nr) = r0 + 1, "did not restart once the bus was free");
chk(ms_state = S_ACTIVE, "expected S_ACTIVE after restarting");
chk(ms_rp = '0', "restart_pending still set after restarting");
-- 7. OBLIGATION 5: lost while ADDRESSING, with a slave function
rst_n <= '0'; tick(3); rst_n <= '1'; tick(3);
start_xfer('1');
scl <= '0'; sda_intent <= '1'; sda <= '0'; tick(4);
scl <= '1'; tick(2);
chk(ms_slv = '1',
"a combined master-slave that lost during ADDRESSING did not switch to slave mode");
chk(ms_state = S_SLV, "expected S_SLAVE after an addressing-stage loss");
chk(ms_clk = '0', "still asserting clocking permission in slave mode");
-- 8. the CONTROL: a master WITHOUT a slave must not switch
chk(m_slv = '0', "a master-only device switched to slave mode -- it has no slave to switch to");
chk(m_state = S_FINISH, "the master-only device should be finishing its byte");
-- 9. a loss OUTSIDE addressing must not switch, even with a slave function
rst_n <= '0'; tick(3); rst_n <= '1'; tick(3);
start_xfer('0');
scl <= '0'; sda_intent <= '1'; sda <= '0'; tick(4);
scl <= '1'; tick(2);
chk(ms_slv = '0', "a combined master-slave switched to slave mode after a DATA-phase loss");
chk(ms_state = S_FINISH, "expected S_FINISH after a data-phase loss");
-- 10. the slave-mode device rejoins and still owes its transaction
rst_n <= '0'; tick(3); rst_n <= '1'; tick(3);
start_xfer('1');
scl <= '0'; sda_intent <= '1'; sda <= '0'; tick(4);
scl <= '1'; tick(2);
r0 := to_integer(ms_nr);
go_free;
chk(to_integer(ms_nr) = r0 + 1,
"the device did not retry its owed transaction after leaving slave mode");
chk(ms_slv = '0', "still in slave mode after the bus went free");
-- 11. the loser never drives SDA again before restarting, swept rather than sampled
rst_n <= '0'; tick(3); rst_n <= '1'; tick(3);
start_xfer('0');
scl <= '0'; sda_intent <= '0'; sda <= '1'; tick(3);
scl <= '1'; tick(3);
scl <= '0'; sda_intent <= '1'; sda <= '0'; tick(3);
scl <= '1'; tick(3);
for k in 0 to 39 loop
if (k mod 3) = 0 then sda_intent <= '0'; else sda_intent <= '1'; end if;
tick(1);
chk(ms_drv = '0', "the loser drove SDA low after losing, whatever its intent");
end loop;
if errors = 0 then
report "i2c_arb_loss_handler self-check complete: all five obligations hold -- asymmetric detection, the driver off the moment a difference appears, clocking permitted only to the byte boundary, a restart owed until the bus is free, and slave mode entered only for an addressing-stage loss by a device that has a slave" severity note;
else
report "FAILURES in i2c_arb_loss_handler" severity error;
end if;
done <= true;
wait;
end process;
end architecture;7a. Five Decisions Worth Defending
The SDA driver is gated on being in the active state, and the loss condition is combinational. So there is no cycle in which lost is set and the driver is still on. §2 records that this is provably redundant on the losing bit and load-bearing on every bit after it.
Clocking permission and the SDA driver are separate outputs. §3's independence. A single "stop" signal would force a design to choose between corrupting data and stalling the bus.
Obligation ⑤ is decided at the instant of loss, using the phase at that instant. Deferring it would require remembering which phase the loss occurred in, and by the time anything else could look, the phase has moved on. The state machine branches at the loss and never revisits the question.
HAS_SLAVE is a parameter, not an input. Whether a device implements a slave is a property of the device, fixed at elaboration — and making it a parameter means the two configurations can be instantiated side by side on identical stimulus, which §7c's tests 7 and 8 require.
Clocking permission expires at nine bits, not eight. A byte is eight data bits plus the acknowledge, and the acknowledge is part of the byte the loser was transmitting. Stopping after eight would abandon the bit cell containing the winner's acknowledge — which the winner needs.
7b. Verified Execution
$ iverilog -g2012 -o d4 i2c_arb_loss_handler.sv i2c_arb_loss_handler_tb.sv && ./d4
PASS: all five obligations hold -- asymmetric detection, the driver off the moment a
difference appears, clocking permitted only to the byte boundary, a restart owed until the
bus is free, and slave mode entered only for an addressing-stage loss by a device that has a
slave
i2c_arb_loss_handler_tb.sv:248: $finish called at 3380000 (1ps)
$ iverilog -g2005 -o v4 i2c_arb_loss_handler.v i2c_arb_loss_handler_tb.v && ./v4
PASS: all five obligations hold -- asymmetric detection, the driver off the moment a
difference appears, clocking permitted only to the byte boundary, a restart owed until the
bus is free, and slave mode entered only for an addressing-stage loss by a device that has a
slave
i2c_arb_loss_handler_tb.v:253: $finish called at 3380000 (1ps)
$ nvc -a i2c_arb_loss_handler.vhd i2c_arb_loss_handler_tb.vhd
$ nvc -e i2c_arb_loss_handler_tb && nvc -r i2c_arb_loss_handler_tb --stop-time=500us
** Note: 3380ns+1: i2c_arb_loss_handler self-check complete: all five obligations hold --
asymmetric detection, the driver off the moment a difference appears, clocking permitted
only to the byte boundary, a restart owed until the bus is free, and slave mode entered only
for an addressing-stage loss by a device that has a slaveAll three at 3380 ns — the shortest run in either module, because five obligations are five branches.
7c. What the Testbench Proves
Two instances run on identical stimulus, because obligation ⑤ is conditional and one configuration cannot distinguish "always switch" from "switch when appropriate" from "never switch":
| instance | HAS_SLAVE | represents |
|---|---|---|
dut_ms | 1 | a combined master-slave |
dut_m | 0 | a master only |
| # | stimulus | what it establishes |
|---|---|---|
| 1 | reset | idle; not driving, not clocking |
| 2 | an uncontested transfer | no loss; still active |
| 3 | sending 0 while another master pulls low | not a loss — the test is asymmetric |
| 4 | sent 1, saw 0 while SCL high | lost, counted, and the driver is off |
| 5 | the remainder of the byte | clocking permitted, SDA driver off throughout, permission expires at the boundary |
| 6 | bus busy, then free | no restart while busy; restart once free |
| 7 | loss while addressing, HAS_SLAVE = 1 | switches to slave mode immediately; stops clocking |
| 8 | the same cycle, HAS_SLAVE = 0 | does not switch — the control for test 7 |
| 9 | loss not while addressing, HAS_SLAVE = 1 | does not switch — the other condition |
| 10 | leaving slave mode | the owed transaction is still retried |
| 11 | forty cycles of changing intent after the loss | the driver never comes back on |
Tests 7, 8 and 9 are the three-sided test that obligation ⑤ requires. Test 7 satisfies both conditions; test 8 removes the slave function; test 9 removes the addressing phase. A suite containing only test 7 is consistent with a design that switches unconditionally, and §8's mutations Q6 and Q7 are exactly those two wrong implementations.
Test 11 is a sweep, not a sample, and §2's callout is why. The obligation is about every cycle after the loss, and the loser's intent keeps changing as its shift register advances — so the test drives intent low on every third cycle and requires the driver to stay off through all forty. A single sampled check would pass a design that drove one stray zero.
Test 5 asserts two things at once and they are independent. Inside the loop it checks the SDA driver is off; outside it checks clocking permission was still granted and then expired. A design that withdrew clocking permission immediately passes the driver check and fails the permission check, and vice versa.
Test 3 carries Chapter 13.3's asymmetry into this chapter. A handler that treated any mismatch as a loss would declare a loss on the first zero of every address, so almost every transfer would end before it began.
Test 10 is the obligation-④-survives-obligation-⑤ check. A device that switched to slave mode still owes its transaction, and once the bus frees it must retry. A design that treated slave mode as a terminal state would silently drop the work — the same silent-loss failure §4 describes, reached by a different path.
8. Mutation Testing
Seven defects injected into the SystemVerilog handler, and one removed as provably equivalent.
| # | injected defect | outcome |
|---|---|---|
| Q1 | the loser keeps driving SDA during the remainder of the byte | killed — test 11 |
| Q2 | clocking permission is withdrawn immediately on loss | killed — test 5 |
| Q3 | clocking permission never expires at the byte boundary | killed — test 5 |
| Q4 | the restart happens immediately rather than when the bus is free | killed — test 5 |
| Q5 | obligation ⑤ omitted: never switches to slave mode | killed — test 7 |
| Q6 | switches to slave mode regardless of the transfer phase | killed — test 9 |
| Q7 | switches to slave mode even without a slave function | killed — test 8 |
| — | removed: the driver is turned off a cycle late | equivalent by construction — §2 |
Seven injected, seven killed. Three notes.
Q5, Q6 and Q7 are the complete set of ways to get a two-condition obligation wrong, and each dies to a different test. That is the argument for §5's callout stated as a result: with two conditions there are three wrong implementations besides the omission, and a suite needs a case per condition to separate them.
Q4 is killed by test 5 rather than by test 6, which is worth a moment. Making the restart immediate means the handler leaves the wait state at once — so it never reaches the state where clocking permission expires, and test 5's byte-boundary check fires first. The failure message is about clocking, not about restarting. An assertion failure names the first symptom, not the defect, and the same observation appears in Chapter 13.3 §8 where two different defects produce one message.
And Q1 is the replacement for the equivalent mutant. Letting the driver follow intent during the finish state is the real form of the obligation-② defect: the loser's remaining zeros reach the wire and corrupt the winner's message from the deciding bit onward. Test 11's sweep catches it on the first cycle where the loser's intent happens to be a zero.
9. Verification Connection — Five Obligations, Five Properties
// Obligation 1: detection, and the antecedent carries the asymmetry. Chapter 13.3 section 2.
property p_detect;
@(posedge clk) (scl_in && sda_intent && !sda_in && active) |=> arb_lost;
endproperty
assert property (p_detect)
else $error("sent a one and observed a zero while SCL was high, but no loss was declared");
// Obligation 2, and note it is written over the WHOLE post-loss interval rather than the losing
// cycle. Section 2: on the losing bit the driver is already off, so a property scoped to that
// cycle is vacuous. This one binds where the obligation bites.
property p_driver_stays_off;
@(posedge clk) $rose(arb_lost) |-> (!sda_drive_low) throughout (arb_lost)[*1:$];
endproperty
assert property (p_driver_stays_off)
else $error("the loser drove SDA after losing -- its remaining zeros are corrupting the winner");
// Obligation 3 is PERMISSION, so the property is about its EXPIRY rather than its existence. A
// property demanding the loser keep clocking would be asserting a requirement the specification
// deliberately did not impose -- the same error Chapter 12.4 section 2 warns about for timeouts.
property p_clocking_expires_at_the_byte;
@(posedge clk) (arb_lost && bits_this_byte >= 9) |-> ##[0:1] !scl_may_clock;
endproperty
assert property (p_clocking_expires_at_the_byte)
else $error("clocking permission outlasted the byte in which arbitration was lost");
// Obligation 4: the transaction is OWED, and the restart is gated on the bus being free.
property p_restart_only_when_free;
@(posedge clk) $rose(restart_pulse) |-> $past(bus_free);
endproperty
assert property (p_restart_only_when_free)
else $error("restarted while the bus was busy -- that creates a fresh collision");
property p_restart_is_owed;
@(posedge clk) $rose(restart_pending) |-> s_eventually (restart_pulse);
endproperty
assert property (p_restart_is_owed)
else $error("a pending restart was never issued -- the transaction was silently dropped");
// Obligation 5, and it needs TWO properties because it has two conditions. One says it must
// happen; the other says it must not happen otherwise. A suite with only the first accepts a
// design that switches unconditionally -- mutations Q6 and Q7.
property p_slave_switch_when_required;
@(posedge clk) ($rose(arb_lost) && in_addressing && HAS_SLAVE) |-> switch_to_slave;
endproperty
assert property (p_slave_switch_when_required)
else $error("lost while addressing with a slave function, but did not switch to slave mode");
property p_no_slave_switch_otherwise;
@(posedge clk) ($rose(arb_lost) && (!in_addressing || !HAS_SLAVE)) |-> !switch_to_slave;
endproperty
assert property (p_no_slave_switch_otherwise)
else $error("switched to slave mode without both conditions -- the winner is not addressing this device"); covergroup i2c_loss_cg with function sample(bit lost, bit in_addr, bit has_slave,
int bits_after_loss, int free_wait_ticks,
bit switched);
// THE cross of this chapter. Obligation 5 has two conditions, so the coverage target is the
// 2x2 square -- and three of its four cells are cases where the device must NOT switch. A
// suite that fills only the top-left corner has tested one of four behaviours.
addressing: coverpoint in_addr { bins address_phase = {1}; bins data_phase = {0}; }
slave_fn: coverpoint has_slave { bins combined = {1}; bins master_only = {0}; }
obligation5: cross addressing, slave_fn;
// WHERE in the byte the loss happened, because obligation 3's permission is bounded by the byte
// and a loss on the last bit leaves almost nothing to clock out. The extremes are the
// interesting bins: a loss on bit 7 exercises the full finish sequence, one on the ACK almost
// none of it.
loss_position: coverpoint bits_after_loss {
bins whole_byte = {[8:9]}; // lost on the first bit: the longest finish
bins most = {[4:7]};
bins few = {[1:3]};
bins none = {0}; // lost on the acknowledge: nothing left to clock
}
// How long the loser had to wait for a free bus. The zero bin must stay EMPTY in regression,
// because restarting with no wait is mutation Q4 and section 4's livelock.
wait_for_free: coverpoint free_wait_ticks {
bins immediate = {0}; // must not happen
bins short = {[1:200]};
bins long = {[201:$]}; // the winner had a long transfer
}
// And the negative case that must be covered: a transfer that was never contested at all, so
// none of the five obligations is reached. It is the state the bus is in almost always, and the
// one a loss handler is most likely to regress.
contested: coverpoint lost { bins never_lost = {0}; bins lost = {1}; }
endgroup10. FPGA and ASIC Implications
The handler is a five-state machine and two counters — around 60 flops. Small, and every state earns its place: remove S_FINISH and obligation ③ has nowhere to live; remove S_WAIT and obligation ④ becomes immediate; remove S_SLAVE and obligation ⑤ is unimplementable.
Obligation ⑤ needs the slave receiver to be startable mid-byte. This is the real implementation cost of the chapter, and it is easy to underestimate. The device was transmitting, so its slave logic was idle; it must now begin receiving an address that is already partly on the wire. Two workable approaches:
| approach | cost | caveat |
|---|---|---|
| run the slave address comparator always, in parallel with master activity | a shift register and a comparator, permanently clocked | the cheaper option, and it means no mid-byte start is needed |
| start the slave receiver at the loss point | no permanent logic | it has missed the bits before the deciding one, so it must reconstruct them from what it sent — since they were identical |
The second works because of a fact from Chapter 13.3 §4: the bits before the deciding bit were identical to what this device sent. So the loser already knows them. That is a genuinely elegant recovery and it is worth knowing about, but the first approach is what most designs should do, because it has no special case.
Clocking permission is easy to implement and easy to get wrong in the safe-looking direction. Withdrawing it immediately feels conservative and leaves SCL held low if the loser was the one holding it — the unrecoverable fault. Granting it forever is the other error and merely wastes bus time. If in doubt, release SCL and stop: that is always safe, even though it forfeits the permission.
The retry needs a backoff only if the addresses are equal. Two masters retrying after a loss re-arbitrate, and Chapter 13.3 resolves them deterministically by address — so the same master wins again, and the other retries again, and progress is made because the winner eventually finishes and stops contending. No backoff is needed and none is specified. The exception is two masters with identical addresses and payloads, which never separate (Chapter 13.3 §5) — and there a backoff does not help either, because they would collide identically after any equal delay.
And a master that also acts as a slave must not NACK while it owes a restart. Its slave function is live; its master function is waiting. Conflating the two — for instance by holding the whole device "busy" until the retry completes — turns obligation ④ into the intermittent-missing-device symptom of §5.
11. Debugging — The Device That Vanished Only When the Bus Was Busy
Pitfall — a combined master-slave that does not switch to slave mode on an addressing-stage loss
// A sensor hub that is BOTH a master (it polls its own sensors) and a slave (the host reads
// aggregated results from it). Address 0x42 as a slave.
//
// Its arbitration was implemented carefully and it looked complete:
//
// always_ff @(posedge clk) begin
// if (scl_high && sda_tx_bit && !sda_in_sync) begin
// arb_lost <= 1'b1;
// sda_oe <= 1'b0; // obligation 2: stop driving. Correct.
// mst_state <= MST_FINISH; // obligation 3: clock out the byte. Correct.
// end
// end
// // ... and in MST_FINISH, at the byte boundary:
// // retry_pending <= 1'b1; // obligation 4: owed. Correct.
// // mst_state <= MST_WAIT_FREE;
//
// Four of the five obligations, implemented correctly. The slave receiver was left alone -- it was
// idle because the device was transmitting, and nothing in the loss path touched it.
//
// Which is obligation 5, and its absence is invisible on any test where the device is not addressed
// by the master that beat it.The host could read the sensor hub reliably in the lab. In the field, on boards where a management controller shared the bus, reads of the hub returned "device not present" at a rate that tracked bus activity -- a few per cent when the bus was quiet, nearly a third under load.
Every diagnostic pointed at the hub being absent. The host's driver reported a NACK on the address byte, which is exactly what an unpopulated address looks like. So the investigation went to the hub's slave logic, its address decoder, and its power sequencing. All correct.
The hub was then probed in isolation with the management controller powered down: flawless, for hours, at every speed mode.
What resolved it was capturing the failing transfer in full, from the START rather than from the address. The host's read of 0x42 had been preceded, within tHD;STA, by the HUB's own START -- the hub was beginning a poll of one of its own sensors at address 0x44.
hub intends: 0x44 << 1 | 0 = 1000 1000 host intends: 0x42 << 1 | 1 = 1000 0101 ^^^^ identical ^ bit 3: hub sends 1, host sends 0 -> HOST WINS
The hub lost at bit 3, correctly stopped driving, correctly clocked out the byte, and correctly queued its retry. And the address that then completed on the wire was 1000 0101 -- the host reading 0x42, which is the hub's own slave address.
The hub was not listening. It was a master in MST_FINISH, obligation 3, clocking out a byte it had already lost. Its slave receiver was idle and had missed the address entirely, so it did not acknowledge, and the host correctly concluded that nothing at 0x42 had answered.
The device that "vanished" was the device that had just been told to listen and was not.
Obligation 5 was not implemented: "If a master also incorporates a slave function and it loses arbitration during the addressing stage, it is possible that the winning master is trying to address it. The losing master must therefore switch over immediately to its slave mode."
The reasoning the specification compresses into one sentence is the crux. Arbitration separates two addresses at the first differing bit, so every bit BEFORE that bit was identical -- which means the winner's address shares a prefix with the loser's intended target, and the winner's address may be the loser's OWN. Here it was: the hub was addressing 0x44, the host was addressing 0x42, and the two differ only from bit 3 down.
The correlation with bus load is the signature. Obligation 5 only bites when this device loses arbitration DURING ADDRESSING and the winner happens to be addressing it -- which requires contention, so the failure rate is a function of how often the two masters collide. Probing the hub in isolation removes the other master and therefore removes the only condition under which the bug can appear.
And the symptom pointed at the innocent subsystem: the hub's slave logic was correct and was simply never started.
12. Common Misconceptions
"The loser just stops." There are five obligations. Stopping is the second, and the fifth is conditional and usually missing.
"Turn off the driver immediately" is about the losing bit. On that bit the driver is already off, because you lose only when sending a one. The obligation binds on every bit after the loss.
"A loser must stop clocking at once." It may clock to the end of the byte — permission, not obligation — and stopping mid-byte can leave SCL held low, which has no protocol recovery.
"The loser may keep clocking, so it may keep driving." Clocking and driving are independent. It may clock; it may not transmit.
"The loser should retry immediately." It must retry when the bus is free. Retrying at once creates a fresh collision that can make the winner lose, and under contention that becomes a livelock.
"Restart means resume." From the beginning, with a fresh START. Nothing was left to resume: the slave never saw a complete address.
"Switching to slave mode is for tidiness." The winner's address may be this device's own, because the two addresses were identical up to the deciding bit. Not switching means NACKing a transfer aimed at you.
"A master-only device should switch to slave mode anyway, harmlessly." It has no slave to switch to, so it sits inert still owing a restart.
"A data-phase loss also needs the slave switch." The address was agreed before the contest, so the winner is addressing a device both masters had chosen — not this one.
13. Reason It Through
Why is "turn off the driver the moment a difference appears" trivially satisfied on the losing bit?
Because a master loses only when it sent a one, and on an open-drain bus sending a one means releasing. So at the instant of loss it is not driving anything. The obligation has teeth on the following bits, where the loser's shift register may present zeros.
Why does the specification grant permission to keep clocking rather than requiring the loser to stop?
Because stopping mid-byte is both harder and more dangerous: if the loser was the one holding SCL low, stopping leaves the line held — the stuck-clock condition with no protocol-level recovery. And continuing costs nothing, since the loser's clock is already merged with the winner's and it is no longer on SDA.
A losing master retries the instant it detects the loss. Trace what happens.
Its START pulls SDA low during the winner's transfer, which the winner reads back as its own arbitration loss — so the winner withdraws too, and the slave has seen a malformed sequence. Both masters now owe restarts and collide again. Under contention throughput collapses rather than degrading, which is why the obligation is to wait for a free bus.
Why might the winner be addressing the loser, and what follows?
Because arbitration separates the two addresses at their first differing bit, so every bit before it was identical — the winner's address shares a prefix with the loser's intended target and may be the loser's own slave address. It follows that a combined master-slave must start listening immediately, or it will NACK a transfer directed at itself.
Why must a data-phase loss not trigger the slave switch?
Because a data-phase loss means the two masters agreed on the address and diverged in the payload. The winner is therefore talking to the device both of them addressed, which is not this one. Switching would make the device stop owing its restart and listen for a transfer that will never come.
A fault's rate rises with bus load and disappears when the device is tested alone. What class of bug is that, and why does isolation testing hide it?
An arbitration bug, because contention is the precondition for every one of the five obligations to be exercised. Testing the device alone removes the second master, which removes the only circumstance in which any of them can be reached.
14. Understanding Check
15. Summary
Five obligations, not one. Detect; stop driving; optionally clock to the byte boundary; restart when free; and conditionally become a slave at once.
"Stop driving immediately" binds after the losing bit, because on that bit the loser had already released. Its remaining zeros are what would corrupt the winner.
Clocking is permitted, not required, because stopping mid-byte is harder and can leave SCL held — and continuing costs the bus nothing.
Clocking and driving are independent. The loser is off SDA and still on SCL, which is a state a conflated design cannot represent.
The transaction is owed, and the retry waits for a free bus. Retrying immediately makes the winner lose and turns contention into livelock.
The winner may be addressing the loser, because the two addresses were identical up to the deciding bit — so a combined master-slave must start listening in the same cycle it loses.
Both conditions on that obligation are necessary, and there are three distinct ways to get it wrong, each needing its own test.
And a fault whose rate tracks bus load is an arbitration fault, because contention is the precondition for reaching any of these five paths at all.
16. What Comes Next
Four chapters have established the mechanism: why two masters exist, how they share a clock, how the wire decides between them, and what the loser owes.
Chapter 13.5 closes the module with the cases that do not fit the pattern — and with three combinations the specification explicitly declines to define:
"There is an undefined condition if the arbitration procedure is still in progress at the moment when one master sends a repeated START or a STOP condition while the other master is still sending data."
Three combinations, named individually, and undefined is not illegal. The specification does not forbid them and does not say what happens — so a monitor that reports them as violations is claiming a rule that does not exist, which is Chapter 12.4 §2's error in a new place.
The chapter also explains why those three and not others: arbitration works because competing masters are sending comparable things — data bits, which the wired-AND resolves and which each master can read back. A framing event is not a data bit; it is a transition of SDA while SCL is high, which is the one thing the data-valid rule forbids during a bit. So a framing event against a data bit has nothing for the read-back test to mean.
Continue learning
Related tutorials
- Related topic
I²C Transaction Atomicity and Bus Ownership Across Phases
What is and is not atomic on an I²C bus, stated precisely. Three things end bus ownership and one that looks like it should does not — and telling them apart needs one input the wire cannot supply.
- Related topic
Why Multiple I²C Masters Exist
The systems that end up with two masters on one bus, and the rule that is not enough to keep them apart. Includes the finding that a collision leaves no trace on the wire.
- Related topic
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.
- 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.
