I²C · Module 13
I²C SDA Arbitration — Wired-AND Decides Bit by Bit
Arbitration with no arbiter, no priority and no protocol exchange — resolved by one asymmetric test each master performs on itself. Settles what 'no information is lost' actually means.
Two masters share a clock (Chapter 13.2) and neither knows the other exists (Chapter 13.1 §4). Both are now putting an address on SDA.
This is where the contest is decided, and the remarkable thing is how little machinery it takes:
There is no arbiter. No priority. No protocol exchange. Each master asks itself one question, once per bit, and the wire has already answered it.
1. The Rule
Four claims are packed in there, and each one is a section below.
| claim | where |
|---|---|
| the test is asymmetric — only "sent 1, saw 0" counts | §2 |
| there is no arbiter; each master decides about itself | §3 |
| no information is lost — and this is routinely misread | §4 |
| identical transmissions both complete | §5 |
2. The Asymmetric Test
The specification says a master checks whether the SDA level matches what it has sent. That sounds symmetric. It is not, and the asymmetry is the whole mechanism.
| I sent | the line reads | what I have learned |
|---|---|---|
| 1 (released) | 1 | nothing — I may be alone, or everyone else also sent 1 |
| 1 (released) | 0 | another device is pulling it down. I have lost. |
| 0 (driving low) | 0 | nothing — I am the one pulling it down |
| 0 (driving low) | 1 | impossible on a wired-AND bus |
Only the second row is conclusive, and the reason is the open-drain rule of Chapter 2.3: nothing on this bus can pull a line low except a device deciding to. So a low line while I released is positive evidence that somebody else is there and is sending a zero — and a zero beats a one, because the wire says so.
The first row is the interesting non-result. Seeing a high line tells me nothing at all, because a high line is what the bus does when nobody is driving it. Absence of evidence.
And the third row is why "matches what it has sent" must not be implemented literally: when I send a zero I am the cause of the zero, so comparing gives a match no matter who else is present. A master sending a zero learns nothing and must not conclude anything.
Sending a zero is never evidence. Only a released line that fails to rise is.
3. There Is No Arbiter
Nothing decides the winner. Read §6's design and look for the logic that picks one: it is not there, and there is nowhere to put it.
Each master runs the test of §2 on itself, using only its own intended bit and the pin. Neither master is told anything. Neither master tells anything. The two comparators do not communicate and do not need to, because the wire has already computed the answer — it carried a zero, and a zero means somebody sent one.
§3.1.8 states the consequence as a property of the bus rather than of any device: "control of the I²C-bus is decided solely on the address and data sent by competing masters, there is no central master, nor any order of priority on the bus."
Which has a corollary worth stating explicitly, because it surprises people:
The winner is determined by the message, not by the messenger. Whichever master is sending the numerically smaller byte wins — and that is a fact about the data, not about the device.
So a master cannot be "higher priority". It can only send lower-valued addresses, which is a design choice about address allocation rather than about arbitration. A device that must usually win should be given a low address; there is no other lever.
There is no path between the two comparators. That absence is the design.
4. What "No Information Is Lost" Actually Means
This is the claim that is most often restated incorrectly, and the incorrect version is plausible enough to survive a review.
The wrong reading: the bus carries the bitwise AND of the two messages, so both masters' data is merged and nothing is "lost" in the sense that the AND is a function of both.
The right reading: the bus carries the winner's message, unchanged. The loser stops driving the moment it loses, so it contributes nothing after that point — and before that point the two agreed, by definition of where the loss occurred.
Work it through. The first bit where the two differ is the deciding bit: one sends 1, the other 0, the zero wins. Before it they were identical, so the bus carried what both sent. After it only the winner drives. Therefore:
observed byte = the winner's byte, and since whoever sends 0 at the first difference wins, the winner is the master with the numerically smaller byte.
observed = min(A, B) — not
A & B.
And note what "no information is lost" is really asserting: not that the two messages are combined, but that the winner's transfer is completely unaffected by the contest. §3.1.8 says so twice — "the other master goes on to complete its transaction" and "this does not affect the data transfer initiated by the winning master." The winner does not retry, does not notice, and does not need to be told. From its side nothing happened.
5. Identical Transmissions Both Complete
One sentence of §3.1.8 is easy to skip and is a genuine surprise:
"This process may take many bits. Two masters can actually complete an entire transaction without error, as long as the transmissions are identical."
If two masters send the same address and the same data, neither ever loses. There is no differing bit, so the asymmetric test never fires, and both drive the whole transfer to completion. The slave sees one transfer. Both masters believe they conducted it. Both are right.
That is not a pathology — it is the direct consequence of §2's asymmetry. Two masters sending identical bits are indistinguishable from one master, because a wired-AND cannot tell two identical commands from one (Chapter 13.1 §4).
Three consequences, in increasing order of how much they matter.
It means arbitration can take the whole transfer. "This process may take many bits" — arbitration is not resolved at the START, or in the address, necessarily. Two masters writing the same value to the same register arbitrate for nine bytes and never separate.
It means a design must not assume a loser exists. A checker asserting "exactly one master loses per contested transfer" is wrong; §7's tests 2 and 10 assert the opposite case explicitly, and mutation P1 — which makes the test symmetric — is killed by test 2 rather than by any contested case.
And it is a real-world hazard for idempotent writes. Two masters both writing 0x00 to a reset register succeed, and both conclude they performed the reset. If the intended semantics were "exactly one of you does this", arbitration provides no help — it separates different messages, and these were the same.
6. Arbitration, Drawn
A sends 0xA0, B sends 0x9F: the bus carries 0x9F
9 cyclesRead the A intends row against the SDA line row. They agree at bits 7 and 6. At bit 5 A intends a one and the line reads zero — the one conclusive combination of §2's table — and from bit 4 onward A's row and the line row have no relationship at all, because A is no longer connected.
The bus carries 1001 1111 = 0x9F. A's four trailing zeros, which the AND reading would have put on the wire, are absent.
7. The Arbiter in Three Languages
The clock is supplied from outside this design, deliberately: its generation is Chapter 13.2's subject, and combining the two would make an arbitration bug indistinguishable from a synchronization bug.
// SDA ARBITRATION: THE WIRED-AND DECIDES, BIT BY BIT. UM10204 section 3.1.8:
//
// "Arbitration proceeds bit by bit. During every bit, WHILE SCL IS HIGH, each master checks to see
// if the SDA level matches what it has sent. This process may take many bits. Two masters can
// actually complete an entire transaction without error, as long as the transmissions are
// identical. 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 turns off its SDA output driver.
// The other master goes on to complete its transaction. NO INFORMATION IS LOST during the
// arbitration process."
//
// Four properties are packed into that paragraph, and this block exists to make each one testable.
//
// 1. THE TEST IS ASYMMETRIC. A master that sends 0 and sees 0 learns nothing -- it may be alone or
// it may be winning. Only "I sent 1 and the line is 0" is conclusive, because on a wired-AND bus
// nothing can pull a line low except another device. Sending 0 is never evidence of anything, and
// mutation C1 makes the comparison symmetric.
//
// 2. THE SAMPLE POINT IS SCL HIGH. That is where the data is valid (Chapter 4.2), so it is the only
// place a read-back means anything. Comparing during the low phase reads a line that is being
// driven to its next value.
//
// 3. THERE IS NO ARBITER. Nothing in this module decides the winner. Each master independently
// discovers whether it lost, and the wire has already resolved the question -- section 3.1.8:
// "control of the I2C-bus is decided solely on the address and data sent by competing masters,
// there is no central master, nor any order of priority on the bus."
//
// 4. NO INFORMATION IS LOST, and this is the property people state loosely. It does NOT mean the
// bus carries the bitwise AND of the two messages. It means the bus carries THE WINNER'S MESSAGE,
// UNCHANGED -- because the loser stops driving the moment it loses, so it contributes nothing
// after that point. Since the first differing bit is won by whoever sends 0, the winner is the
// master with the numerically SMALLER byte, and:
//
// observed byte == min(A, B) NOT A & B
//
// Those two are equal surprisingly often, which is what makes the mistake survivable. They differ
// whenever the loser had a 0 where the winner has a 1 in a later bit: A = 0x80, B = 0x7F gives
// min = 0x7F and AND = 0x00. Mutation C5 is the AND, and the testbench carries that exact pair.
module i2c_sda_arbiter #(
parameter int TICK_W = 16
)(
input logic clk,
input logic rst_n,
// The clock is supplied from outside: its generation is Chapter 13.2's subject, and mixing the
// two would make it impossible to tell an arbitration bug from a synchronization bug.
input logic scl_in,
input logic begin_byte, // pulse: both masters load and start driving
input logic [7:0] byte_a,
input logic [7:0] byte_b,
// ---- the wired-AND data line ----
output logic sda_line,
output logic a_drive_low,
output logic b_drive_low,
// ---- what each master discovered, independently ----
output logic a_lost,
output logic b_lost,
output logic a_active, // still driving
output logic b_active,
output logic [3:0] loss_bit, // 7..0, the bit index at which the first loss occurred
output logic loss_valid, // pulse when a loss is first detected
// ---- the byte the BUS actually carried ----
output logic byte_done,
output logic [7:0] observed_byte,
output logic [3:0] bits_sent,
// ---- totals ----
output logic [TICK_W-1:0] n_bits_compared,
output logic [TICK_W-1:0] n_losses
);
logic [7:0] sh_a, sh_b; // what each master still has to send, MSB first
logic [3:0] bit_idx; // 7 down to 0
logic running;
logic scl_q;
wire scl_rise = scl_in && !scl_q;
wire scl_fall = !scl_in && scl_q;
// Each master's intended bit for the current position.
wire a_bit = sh_a[7];
wire b_bit = sh_b[7];
// Open-drain: drive low for a 0, release for a 1 -- and only while still active.
assign a_drive_low = a_active && !a_bit;
assign b_drive_low = b_active && !b_bit;
assign sda_line = !a_drive_low && !b_drive_low;
logic [7:0] obs;
always_ff @(posedge clk) begin
if (!rst_n) begin
sh_a <= 8'h00;
sh_b <= 8'h00;
bit_idx <= 4'd0;
running <= 1'b0;
scl_q <= 1'b1;
a_lost <= 1'b0;
b_lost <= 1'b0;
a_active <= 1'b0;
b_active <= 1'b0;
loss_bit <= 4'd0;
loss_valid <= 1'b0;
byte_done <= 1'b0;
observed_byte <= 8'h00;
bits_sent <= 4'd0;
obs <= 8'h00;
n_bits_compared <= '0;
n_losses <= '0;
end else begin
scl_q <= scl_in;
loss_valid <= 1'b0;
byte_done <= 1'b0;
if (begin_byte) begin
sh_a <= byte_a;
sh_b <= byte_b;
bit_idx <= 4'd7;
running <= 1'b1;
a_active <= 1'b1;
b_active <= 1'b1;
a_lost <= 1'b0;
b_lost <= 1'b0;
obs <= 8'h00;
bits_sent <= 4'd0;
end else if (running) begin
// ---- the read-back, WHILE SCL IS HIGH ----
if (scl_rise) begin
n_bits_compared <= n_bits_compared + 1'b1;
// Record what the BUS carried, which is what a third party would have seen.
obs <= {obs[6:0], sda_line};
// Master A: the asymmetric test. Sending 0 proves nothing; sending 1 and
// observing 0 proves another device is pulling the line down.
if (a_active && a_bit && !sda_line) begin
a_lost <= 1'b1;
a_active <= 1'b0; // turn the driver off IMMEDIATELY
loss_bit <= bit_idx;
if (!a_lost && !b_lost) begin
loss_valid <= 1'b1;
n_losses <= n_losses + 1'b1;
end
end
// Master B: identical logic, no coordination with A whatsoever.
if (b_active && b_bit && !sda_line) begin
b_lost <= 1'b1;
b_active <= 1'b0;
loss_bit <= bit_idx;
if (!a_lost && !b_lost) begin
loss_valid <= 1'b1;
n_losses <= n_losses + 1'b1;
end
end
end
// ---- advance on the falling edge, where SDA is allowed to change ----
if (scl_fall) begin
sh_a <= {sh_a[6:0], 1'b1}; // shift in 1 = released
sh_b <= {sh_b[6:0], 1'b1};
bits_sent <= bits_sent + 4'd1;
if (bit_idx == 4'd0) begin
running <= 1'b0;
byte_done <= 1'b1;
observed_byte <= obs;
end else begin
bit_idx <= bit_idx - 4'd1;
end
end
end
end
end
endmodule `timescale 1ns/1ps
// The clock is driven by the testbench, deliberately: Chapter 13.2 owns clock generation, and mixing
// the two would make an arbitration failure indistinguishable from a synchronization failure.
//
// Every expectation is computed here from the two byte values, independently of the DUT:
//
// winner = the master with the numerically SMALLER byte (0 wins the first differing bit)
// observed byte = min(A, B) NOT A & B
// loss bit = the index of the highest bit where they differ
//
// The pair 0x80 / 0x7F is in the suite specifically because min = 0x7F while AND = 0x00, so it
// separates "the winner's message, unchanged" from "the bitwise AND of both messages".
module i2c_sda_arbiter_tb;
localparam int TICK_W = 16;
logic clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
logic scl = 1'b1;
logic begin_byte = 1'b0;
logic [7:0] byte_a = 8'h00, byte_b = 8'h00;
logic sda_line, a_drive_low, b_drive_low;
logic a_lost, b_lost, a_active, b_active, loss_valid, byte_done;
logic [3:0] loss_bit, bits_sent;
logic [7:0] observed_byte;
logic [TICK_W-1:0] n_bits_compared, n_losses;
i2c_sda_arbiter #(.TICK_W(TICK_W)) dut (
.clk(clk), .rst_n(rst_n), .scl_in(scl),
.begin_byte(begin_byte), .byte_a(byte_a), .byte_b(byte_b),
.sda_line(sda_line), .a_drive_low(a_drive_low), .b_drive_low(b_drive_low),
.a_lost(a_lost), .b_lost(b_lost), .a_active(a_active), .b_active(b_active),
.loss_bit(loss_bit), .loss_valid(loss_valid),
.byte_done(byte_done), .observed_byte(observed_byte), .bits_sent(bits_sent),
.n_bits_compared(n_bits_compared), .n_losses(n_losses)
);
int errors = 0;
task automatic tick(input int n); begin repeat (n) @(negedge clk); end endtask
// Independently computed expectations.
function automatic [7:0] exp_observed(input [7:0] a, input [7:0] b);
return (a < b) ? a : b; // the smaller message wins, unchanged
endfunction
function automatic int exp_loss_bit(input [7:0] a, input [7:0] b);
int k;
begin
exp_loss_bit = -1; // no difference -> no loss
for (k = 7; k >= 0; k--)
if (exp_loss_bit < 0 && a[k] !== b[k]) exp_loss_bit = k;
end
endfunction
// Drive one byte: eight SCL periods, with SDA changing on the low phase and read back on high.
task automatic send(input [7:0] a, input [7:0] b);
int k;
begin
byte_a = a; byte_b = b;
scl = 1'b0; tick(4);
begin_byte = 1'b1; tick(1); begin_byte = 1'b0;
for (k = 0; k < 8; k++) begin
tick(6); // low phase: SDA settles
scl = 1'b1; tick(8); // high phase: the read-back happens here
scl = 1'b0; tick(2); // falling edge: advance
end
tick(6);
scl = 1'b1; tick(6);
end
endtask
// Run one case and check everything about it.
task automatic check_case(input [7:0] a, input [7:0] b, input string label);
logic [7:0] want_obs;
int want_bit;
begin
want_obs = exp_observed(a, b);
want_bit = exp_loss_bit(a, b);
send(a, b);
if (observed_byte !== want_obs) begin
$display("FAIL: %s A=%02h B=%02h -- the bus carried %02h, expected %02h (the WINNER'S byte unchanged; A&B would be %02h)",
label, a, b, observed_byte, want_obs, a & b); errors++; end
if (want_bit < 0) begin
// Identical transmissions: section 3.1.8 says both masters complete without error.
if (a_lost !== 1'b0 || b_lost !== 1'b0) begin
$display("FAIL: %s identical bytes %02h produced a loss (a=%b b=%b) -- two masters sending the same thing must BOTH complete",
label, a, a_lost, b_lost); errors++; end
end else begin
if (loss_bit != want_bit[3:0]) begin
$display("FAIL: %s A=%02h B=%02h -- loss reported at bit %0d, expected %0d",
label, a, b, loss_bit, want_bit); errors++; end
// Exactly one master loses, and it is the one with the larger byte.
if (a < b) begin
if (a_lost !== 1'b0 || b_lost !== 1'b1) begin
$display("FAIL: %s A=%02h < B=%02h so B must lose, got a_lost=%b b_lost=%b",
label, a, b, a_lost, b_lost); errors++; end
end else begin
if (a_lost !== 1'b1 || b_lost !== 1'b0) begin
$display("FAIL: %s A=%02h > B=%02h so A must lose, got a_lost=%b b_lost=%b",
label, a, b, a_lost, b_lost); errors++; end
end
end
end
endtask
initial begin
tick(4); rst_n = 1'b1; tick(4);
// ---- 1. reset ------------------------------------------------------------------------
if (n_losses !== '0 || n_bits_compared !== '0) begin
$display("FAIL: counters nonzero out of reset"); errors++; end
if (a_lost !== 1'b0 || b_lost !== 1'b0) begin
$display("FAIL: a loss was reported out of reset"); errors++; end
if (sda_line !== 1'b1) begin
$display("FAIL: SDA not released out of reset"); errors++; end
// ---- 2. IDENTICAL transmissions: both masters complete, no loss ----------------------
// Section 3.1.8: "Two masters can actually complete an entire transaction without error, as
// long as the transmissions are identical." This is the case that proves the test is
// asymmetric -- a symmetric comparison would find a difference nowhere and yet a design
// that flagged "both drove the same bit" would fail here.
check_case(8'hA5, 8'hA5, "identical");
if (a_active !== 1'b1 || b_active !== 1'b1) begin
$display("FAIL: identical bytes left a=%b b=%b active -- both must still be driving",
a_active, b_active); errors++; end
// ---- 3. loss on the very FIRST bit ---------------------------------------------------
check_case(8'h00, 8'h80, "first-bit loss");
// ---- 4. loss on the LAST bit ---------------------------------------------------------
check_case(8'hA4, 8'hA5, "last-bit loss");
// ---- 5. THE pair that separates min from AND -----------------------------------------
// A = 1000_0000, B = 0111_1111. B wins at bit 7 and then sends seven ones, so the bus
// carries 0x7F. The bitwise AND is 0x00, which is what a design that merged both masters'
// contributions for the whole byte would report -- and it is wrong by every bit but one.
check_case(8'h80, 8'h7F, "min-not-AND");
if (observed_byte !== 8'h7F) begin
$display("FAIL: the bus carried %02h for 0x80 vs 0x7F, expected 0x7F -- 'no information is lost' means the WINNER'S message survives, not that the two are ANDed",
observed_byte); errors++; end
// ---- 6. the mirror of test 5 ---------------------------------------------------------
check_case(8'h7F, 8'h80, "min-not-AND mirrored");
// ---- 7. a mid-byte loss, with the loser holding 0s AFTER the loss point ---------------
// A = 1010_0000, B = 1001_1111. They agree on bits 7,6; at bit 5 A sends 1 and B sends 0,
// so B wins and the bus carries B = 0x9F. A's remaining zeros never reach the wire.
check_case(8'hA0, 8'h9F, "loser has zeros later");
// ---- 8. the loser stops driving IMMEDIATELY ------------------------------------------
// "The moment there is a difference ... the DATA 1 output is switched off." Checked at the
// level of the driver rather than the flag, because the driver is what the bus sees.
begin
byte_a = 8'hFF; byte_b = 8'h00;
scl = 1'b0; tick(4);
begin_byte = 1'b1; tick(1); begin_byte = 1'b0;
tick(6);
scl = 1'b1; tick(3);
// A sent 1, the line is 0 (B pulled it), so A must now be off.
if (a_active !== 1'b0) begin
$display("FAIL: master A was still active after losing on the first bit"); errors++; end
if (a_drive_low !== 1'b0) begin
$display("FAIL: master A was still driving SDA low after losing"); errors++; end
// And B is unaffected.
if (b_active !== 1'b1) begin
$display("FAIL: the WINNER stopped driving -- arbitration must not affect it"); errors++; end
scl = 1'b0; tick(4);
// finish the byte
for (int k = 1; k < 8; k++) begin
tick(6); scl = 1'b1; tick(8); scl = 1'b0; tick(2);
end
tick(6); scl = 1'b1; tick(6);
if (observed_byte !== 8'h00) begin
$display("FAIL: with A=FF and B=00 the bus carried %02h, expected 00", observed_byte);
errors++; end
end
// ---- 9. exactly ONE loss is reported per contested byte -------------------------------
begin
int l0;
l0 = n_losses;
check_case(8'hF0, 8'h0F, "one loss only");
if (n_losses != l0 + 1) begin
$display("FAIL: a contested byte produced %0d losses, expected exactly 1",
n_losses - l0); errors++; end
end
// ---- 10. an uncontested byte produces no loss ------------------------------------------
// The negative case: both masters sending the same thing is test 2; here only the shape of
// the check matters -- a design that always found a loser would pass tests 3 to 9.
begin
int l0;
l0 = n_losses;
check_case(8'h3C, 8'h3C, "uncontested");
if (n_losses != l0) begin
$display("FAIL: an uncontested byte reported %0d loss(es)", n_losses - l0);
errors++; end
end
// ---- 11. eight bits are compared per byte ----------------------------------------------
// The read-back happens once per bit, on the rising edge. A design comparing on both edges
// would report sixteen, and one comparing only when a loss occurred would report far fewer.
begin
int c0;
c0 = n_bits_compared;
check_case(8'h55, 8'h55, "bit count");
if (n_bits_compared != c0 + 8) begin
$display("FAIL: %0d bits compared in one byte, expected 8", n_bits_compared - c0);
errors++; end
end
if (errors == 0)
$display("PASS: the test is asymmetric so identical transmissions both complete, the loser stops driving immediately, the bus carries the winner's byte unchanged rather than the bitwise AND, and no arbiter decides anything");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
#2000000;
$display("FAIL: watchdog expired");
$finish;
end
endmodule // SDA ARBITRATION: THE WIRED-AND DECIDES, BIT BY BIT. UM10204 section 3.1.8:
//
// "Arbitration proceeds bit by bit. During every bit, WHILE SCL IS HIGH, each master checks to see
// if the SDA level matches what it has sent. This process may take many bits. Two masters can
// actually complete an entire transaction without error, as long as the transmissions are
// identical. 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 turns off its SDA output driver.
// The other master goes on to complete its transaction. NO INFORMATION IS LOST during the
// arbitration process."
//
// Four properties are packed into that paragraph, and this block exists to make each one testable.
//
// 1. THE TEST IS ASYMMETRIC. A master that sends 0 and sees 0 learns nothing -- it may be alone or
// it may be winning. Only "I sent 1 and the line is 0" is conclusive, because on a wired-AND bus
// nothing can pull a line low except another device. Sending 0 is never evidence of anything, and
// mutation C1 makes the comparison symmetric.
//
// 2. THE SAMPLE POINT IS SCL HIGH. That is where the data is valid (Chapter 4.2), so it is the only
// place a read-back means anything. Comparing during the low phase reads a line that is being
// driven to its next value.
//
// 3. THERE IS NO ARBITER. Nothing in this module decides the winner. Each master independently
// discovers whether it lost, and the wire has already resolved the question -- section 3.1.8:
// "control of the I2C-bus is decided solely on the address and data sent by competing masters,
// there is no central master, nor any order of priority on the bus."
//
// 4. NO INFORMATION IS LOST, and this is the property people state loosely. It does NOT mean the
// bus carries the bitwise AND of the two messages. It means the bus carries THE WINNER'S MESSAGE,
// UNCHANGED -- because the loser stops driving the moment it loses, so it contributes nothing
// after that point. Since the first differing bit is won by whoever sends 0, the winner is the
// master with the numerically SMALLER byte, and:
//
// observed byte == min(A, B) NOT A & B
//
// Those two are equal surprisingly often, which is what makes the mistake survivable. They differ
// whenever the loser had a 0 where the winner has a 1 in a later bit: A = 0x80, B = 0x7F gives
// min = 0x7F and AND = 0x00. Mutation C5 is the AND, and the testbench carries that exact pair.
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_sda_arbiter #(
parameter TICK_W = 16
)(
input wire clk,
input wire rst_n,
// The clock is supplied from outside: its generation is Chapter 13.2's subject, and mixing the
// two would make it impossible to tell an arbitration bug from a synchronization bug.
input wire scl_in,
input wire begin_byte, // pulse: both masters load and start driving
input wire [7:0] byte_a,
input wire [7:0] byte_b,
// ---- the wired-AND data line ----
output wire sda_line,
output wire a_drive_low,
output wire b_drive_low,
// ---- what each master discovered, independently ----
output reg a_lost,
output reg b_lost,
output reg a_active, // still driving
output reg b_active,
output reg [3:0] loss_bit, // 7..0, the bit index at which the first loss occurred
output reg loss_valid, // pulse when a loss is first detected
// ---- the byte the BUS actually carried ----
output reg byte_done,
output reg [7:0] observed_byte,
output reg [3:0] bits_sent,
// ---- totals ----
output reg [TICK_W-1:0] n_bits_compared,
output reg [TICK_W-1:0] n_losses
);
reg [7:0] sh_a, sh_b; // what each master still has to send, MSB first
reg [3:0] bit_idx; // 7 down to 0
reg running;
reg scl_q;
wire scl_rise = scl_in && !scl_q;
wire scl_fall = !scl_in && scl_q;
// Each master's intended bit for the current position.
wire a_bit = sh_a[7];
wire b_bit = sh_b[7];
// Open-drain: drive low for a 0, release for a 1 -- and only while still active.
assign a_drive_low = a_active && !a_bit;
assign b_drive_low = b_active && !b_bit;
assign sda_line = !a_drive_low && !b_drive_low;
reg [7:0] obs;
always @(posedge clk) begin
if (!rst_n) begin
sh_a <= 8'h00;
sh_b <= 8'h00;
bit_idx <= 4'd0;
running <= 1'b0;
scl_q <= 1'b1;
a_lost <= 1'b0;
b_lost <= 1'b0;
a_active <= 1'b0;
b_active <= 1'b0;
loss_bit <= 4'd0;
loss_valid <= 1'b0;
byte_done <= 1'b0;
observed_byte <= 8'h00;
bits_sent <= 4'd0;
obs <= 8'h00;
n_bits_compared <= {TICK_W{1'b0}};
n_losses <= {TICK_W{1'b0}};
end else begin
scl_q <= scl_in;
loss_valid <= 1'b0;
byte_done <= 1'b0;
if (begin_byte) begin
sh_a <= byte_a;
sh_b <= byte_b;
bit_idx <= 4'd7;
running <= 1'b1;
a_active <= 1'b1;
b_active <= 1'b1;
a_lost <= 1'b0;
b_lost <= 1'b0;
obs <= 8'h00;
bits_sent <= 4'd0;
end else if (running) begin
// ---- the read-back, WHILE SCL IS HIGH ----
if (scl_rise) begin
n_bits_compared <= n_bits_compared + 1'b1;
// Record what the BUS carried, which is what a third party would have seen.
obs <= {obs[6:0], sda_line};
// Master A: the asymmetric test. Sending 0 proves nothing; sending 1 and
// observing 0 proves another device is pulling the line down.
if (a_active && a_bit && !sda_line) begin
a_lost <= 1'b1;
a_active <= 1'b0; // turn the driver off IMMEDIATELY
loss_bit <= bit_idx;
if (!a_lost && !b_lost) begin
loss_valid <= 1'b1;
n_losses <= n_losses + 1'b1;
end
end
// Master B: identical logic, no coordination with A whatsoever.
if (b_active && b_bit && !sda_line) begin
b_lost <= 1'b1;
b_active <= 1'b0;
loss_bit <= bit_idx;
if (!a_lost && !b_lost) begin
loss_valid <= 1'b1;
n_losses <= n_losses + 1'b1;
end
end
end
// ---- advance on the falling edge, where SDA is allowed to change ----
if (scl_fall) begin
sh_a <= {sh_a[6:0], 1'b1}; // shift in 1 = released
sh_b <= {sh_b[6:0], 1'b1};
bits_sent <= bits_sent + 4'd1;
if (bit_idx == 4'd0) begin
running <= 1'b0;
byte_done <= 1'b1;
observed_byte <= obs;
end else begin
bit_idx <= bit_idx - 4'd1;
end
end
end
end
end
endmodule `timescale 1ns/1ps
// The clock is driven by the testbench, deliberately: Chapter 13.2 owns clock generation, and mixing
// the two would make an arbitration failure indistinguishable from a synchronization failure.
//
// Every expectation is computed here from the two byte values, independently of the DUT:
//
// winner = the master with the numerically SMALLER byte (0 wins the first differing bit)
// observed byte = min(A, B) NOT A & B
// loss bit = the index of the highest bit where they differ
//
// The pair 0x80 / 0x7F is in the suite specifically because min = 0x7F while AND = 0x00, so it
// separates "the winner's message, unchanged" from "the bitwise AND of both messages".
// (Verilog-2001 testbench -- same stimulus, same checks.)
module i2c_sda_arbiter_tb;
localparam TICK_W = 16;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg scl = 1'b1;
reg begin_byte = 1'b0;
reg [7:0] byte_a = 8'h00, byte_b = 8'h00;
wire sda_line, a_drive_low, b_drive_low;
wire a_lost, b_lost, a_active, b_active, loss_valid, byte_done;
wire [3:0] loss_bit, bits_sent;
wire [7:0] observed_byte;
wire [TICK_W-1:0] n_bits_compared, n_losses;
i2c_sda_arbiter #(.TICK_W(TICK_W)) dut (
.clk(clk), .rst_n(rst_n), .scl_in(scl),
.begin_byte(begin_byte), .byte_a(byte_a), .byte_b(byte_b),
.sda_line(sda_line), .a_drive_low(a_drive_low), .b_drive_low(b_drive_low),
.a_lost(a_lost), .b_lost(b_lost), .a_active(a_active), .b_active(b_active),
.loss_bit(loss_bit), .loss_valid(loss_valid),
.byte_done(byte_done), .observed_byte(observed_byte), .bits_sent(bits_sent),
.n_bits_compared(n_bits_compared), .n_losses(n_losses)
);
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 k = 0;
integer want_bit = 0;
integer l0 = 0;
integer c0 = 0;
task tick(input integer n); begin repeat (n) @(negedge clk); end endtask
// Independently computed expectations.
function [7:0] exp_observed(input [7:0] a, input [7:0] b);
exp_observed = (a < b) ? a : b; // the smaller message wins, unchanged
endfunction
function integer exp_loss_bit(input [7:0] a, input [7:0] b);
begin
exp_loss_bit = -1; // no difference -> no loss
for (k = 7; k >= 0; k--)
if (exp_loss_bit < 0 && a[k] !== b[k]) exp_loss_bit = k;
end
endfunction
// Drive one byte: eight SCL periods, with SDA changing on the low phase and read back on high.
task send(input [7:0] a, input [7:0] b);
begin
byte_a = a; byte_b = b;
scl = 1'b0; tick(4);
begin_byte = 1'b1; tick(1); begin_byte = 1'b0;
for (k = 0; k < 8; k = k + 1) begin
tick(6); // low phase: SDA settles
scl = 1'b1; tick(8); // high phase: the read-back happens here
scl = 1'b0; tick(2); // falling edge: advance
end
tick(6);
scl = 1'b1; tick(6);
end
endtask
// Run one case and check everything about it.
task check_case(input [7:0] a, input [7:0] b, input [8*44:1] label);
reg [7:0] want_obs;
begin
want_obs = exp_observed(a, b);
want_bit = exp_loss_bit(a, b);
send(a, b);
if (observed_byte !== want_obs) begin
$display("FAIL: %s A=%02h B=%02h -- the bus carried %02h, expected %02h (the WINNER'S byte unchanged; A&B would be %02h)",
label, a, b, observed_byte, want_obs, a & b); errors = errors + 1; end
if (want_bit < 0) begin
// Identical transmissions: section 3.1.8 says both masters complete without error.
if (a_lost !== 1'b0 || b_lost !== 1'b0) begin
$display("FAIL: %s identical bytes %02h produced a loss (a=%b b=%b) -- two masters sending the same thing must BOTH complete",
label, a, a_lost, b_lost); errors = errors + 1; end
end else begin
if (loss_bit != want_bit[3:0]) begin
$display("FAIL: %s A=%02h B=%02h -- loss reported at bit %0d, expected %0d",
label, a, b, loss_bit, want_bit); errors = errors + 1; end
// Exactly one master loses, and it is the one with the larger byte.
if (a < b) begin
if (a_lost !== 1'b0 || b_lost !== 1'b1) begin
$display("FAIL: %s A=%02h < B=%02h so B must lose, got a_lost=%b b_lost=%b",
label, a, b, a_lost, b_lost); errors = errors + 1; end
end else begin
if (a_lost !== 1'b1 || b_lost !== 1'b0) begin
$display("FAIL: %s A=%02h > B=%02h so A must lose, got a_lost=%b b_lost=%b",
label, a, b, a_lost, b_lost); errors = errors + 1; end
end
end
end
endtask
initial begin
tick(4); rst_n = 1'b1; tick(4);
// ---- 1. reset ------------------------------------------------------------------------
if (n_losses !== {TICK_W{1'b0}} || n_bits_compared !== {TICK_W{1'b0}}) begin
$display("FAIL: counters nonzero out of reset"); errors = errors + 1; end
if (a_lost !== 1'b0 || b_lost !== 1'b0) begin
$display("FAIL: a loss was reported out of reset"); errors = errors + 1; end
if (sda_line !== 1'b1) begin
$display("FAIL: SDA not released out of reset"); errors = errors + 1; end
// ---- 2. IDENTICAL transmissions: both masters complete, no loss ----------------------
// Section 3.1.8: "Two masters can actually complete an entire transaction without error, as
// long as the transmissions are identical." This is the case that proves the test is
// asymmetric -- a symmetric comparison would find a difference nowhere and yet a design
// that flagged "both drove the same bit" would fail here.
check_case(8'hA5, 8'hA5, "identical");
if (a_active !== 1'b1 || b_active !== 1'b1) begin
$display("FAIL: identical bytes left a=%b b=%b active -- both must still be driving",
a_active, b_active); errors = errors + 1; end
// ---- 3. loss on the very FIRST bit ---------------------------------------------------
check_case(8'h00, 8'h80, "first-bit loss");
// ---- 4. loss on the LAST bit ---------------------------------------------------------
check_case(8'hA4, 8'hA5, "last-bit loss");
// ---- 5. THE pair that separates min from AND -----------------------------------------
// A = 1000_0000, B = 0111_1111. B wins at bit 7 and then sends seven ones, so the bus
// carries 0x7F. The bitwise AND is 0x00, which is what a design that merged both masters'
// contributions for the whole byte would report -- and it is wrong by every bit but one.
check_case(8'h80, 8'h7F, "min-not-AND");
if (observed_byte !== 8'h7F) begin
$display("FAIL: the bus carried %02h for 0x80 vs 0x7F, expected 0x7F -- 'no information is lost' means the WINNER'S message survives, not that the two are ANDed",
observed_byte); errors = errors + 1; end
// ---- 6. the mirror of test 5 ---------------------------------------------------------
check_case(8'h7F, 8'h80, "min-not-AND mirrored");
// ---- 7. a mid-byte loss, with the loser holding 0s AFTER the loss point ---------------
// A = 1010_0000, B = 1001_1111. They agree on bits 7,6; at bit 5 A sends 1 and B sends 0,
// so B wins and the bus carries B = 0x9F. A's remaining zeros never reach the wire.
check_case(8'hA0, 8'h9F, "loser has zeros later");
// ---- 8. the loser stops driving IMMEDIATELY ------------------------------------------
// "The moment there is a difference ... the DATA 1 output is switched off." Checked at the
// level of the driver rather than the flag, because the driver is what the bus sees.
begin
byte_a = 8'hFF; byte_b = 8'h00;
scl = 1'b0; tick(4);
begin_byte = 1'b1; tick(1); begin_byte = 1'b0;
tick(6);
scl = 1'b1; tick(3);
// A sent 1, the line is 0 (B pulled it), so A must now be off.
if (a_active !== 1'b0) begin
$display("FAIL: master A was still active after losing on the first bit"); errors = errors + 1; end
if (a_drive_low !== 1'b0) begin
$display("FAIL: master A was still driving SDA low after losing"); errors = errors + 1; end
// And B is unaffected.
if (b_active !== 1'b1) begin
$display("FAIL: the WINNER stopped driving -- arbitration must not affect it"); errors = errors + 1; end
scl = 1'b0; tick(4);
// finish the byte
for (k = 1; k < 8; k = k + 1) begin
tick(6); scl = 1'b1; tick(8); scl = 1'b0; tick(2);
end
tick(6); scl = 1'b1; tick(6);
if (observed_byte !== 8'h00) begin
$display("FAIL: with A=FF and B=00 the bus carried %02h, expected 00", observed_byte);
errors = errors + 1; end
end
// ---- 9. exactly ONE loss is reported per contested byte -------------------------------
begin
l0 = n_losses;
check_case(8'hF0, 8'h0F, "one loss only");
if (n_losses != l0 + 1) begin
$display("FAIL: a contested byte produced %0d losses, expected exactly 1",
n_losses - l0); errors = errors + 1; end
end
// ---- 10. an uncontested byte produces no loss ------------------------------------------
// The negative case: both masters sending the same thing is test 2; here only the shape of
// the check matters -- a design that always found a loser would pass tests 3 to 9.
begin
l0 = n_losses;
check_case(8'h3C, 8'h3C, "uncontested");
if (n_losses != l0) begin
$display("FAIL: an uncontested byte reported %0d loss(es)", n_losses - l0);
errors = errors + 1; end
end
// ---- 11. eight bits are compared per byte ----------------------------------------------
// The read-back happens once per bit, on the rising edge. A design comparing on both edges
// would report sixteen, and one comparing only when a loss occurred would report far fewer.
begin
c0 = n_bits_compared;
check_case(8'h55, 8'h55, "bit count");
if (n_bits_compared != c0 + 8) begin
$display("FAIL: %0d bits compared in one byte, expected 8", n_bits_compared - c0);
errors = errors + 1; end
end
if (errors == 0)
$display("PASS: the test is asymmetric so identical transmissions both complete, the loser stops driving immediately, the bus carries the winner's byte unchanged rather than the bitwise AND, and no arbiter decides anything");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
#2000000;
$display("FAIL: watchdog expired");
$finish;
end
endmodule -- SDA ARBITRATION: THE WIRED-AND DECIDES, BIT BY BIT -- the VHDL form.
--
-- UM10204 section 3.1.8: "Arbitration proceeds bit by bit. During every bit, WHILE SCL IS HIGH, each
-- master checks to see if the SDA level matches what it has sent. ... 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 turns off its SDA output driver. ... NO INFORMATION IS LOST during the arbitration
-- process."
--
-- "No information is lost" does NOT mean the bus carries the bitwise AND of the two messages. It means
-- the bus carries THE WINNER'S MESSAGE, UNCHANGED: the loser stops driving the moment it loses and
-- contributes nothing afterwards. Since whoever sends 0 wins the first differing bit, the winner is
-- the master with the numerically SMALLER byte, and the observed byte is min(A,B) -- not A and B.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_sda_arbiter is
generic (
TICK_W : natural := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
scl_in : in std_logic;
begin_byte : in std_logic;
byte_a : in std_logic_vector(7 downto 0);
byte_b : in std_logic_vector(7 downto 0);
sda_line : out std_logic;
a_drive_low : out std_logic;
b_drive_low : out std_logic;
a_lost : out std_logic;
b_lost : out std_logic;
a_active : out std_logic;
b_active : out std_logic;
loss_bit : out unsigned(3 downto 0);
loss_valid : out std_logic;
byte_done : out std_logic;
observed_byte : out std_logic_vector(7 downto 0);
bits_sent : out unsigned(3 downto 0);
n_bits_compared : out unsigned(TICK_W-1 downto 0);
n_losses : out unsigned(TICK_W-1 downto 0)
);
end entity;
architecture rtl of i2c_sda_arbiter is
signal sh_a, sh_b : std_logic_vector(7 downto 0) := (others => '0');
signal bit_idx : unsigned(3 downto 0) := (others => '0');
signal running : std_logic := '0';
signal scl_q : std_logic := '1';
signal scl_rise_s, scl_fall_s : std_logic;
signal s_aa, s_ba : std_logic := '0'; -- active
signal s_al, s_bl : std_logic := '0'; -- lost
signal s_ad, s_bd : std_logic;
signal s_line : std_logic;
signal obs : std_logic_vector(7 downto 0) := (others => '0');
signal r_nbc, r_nl : unsigned(TICK_W-1 downto 0) := (others => '0');
begin
scl_rise_s <= '1' when (scl_in = '1' and scl_q = '0') else '0';
scl_fall_s <= '1' when (scl_in = '0' and scl_q = '1') else '0';
-- Open-drain: drive low for a 0, release for a 1, and only while still active.
s_ad <= '1' when (s_aa = '1' and sh_a(7) = '0') else '0';
s_bd <= '1' when (s_ba = '1' and sh_b(7) = '0') else '0';
s_line <= '1' when (s_ad = '0' and s_bd = '0') else '0';
sda_line <= s_line;
a_drive_low <= s_ad;
b_drive_low <= s_bd;
a_lost <= s_al;
b_lost <= s_bl;
a_active <= s_aa;
b_active <= s_ba;
n_bits_compared <= r_nbc;
n_losses <= r_nl;
process (clk) is
begin
if rising_edge(clk) then
if rst_n = '0' then
sh_a <= (others => '0');
sh_b <= (others => '0');
bit_idx <= (others => '0');
running <= '0';
scl_q <= '1';
s_al <= '0';
s_bl <= '0';
s_aa <= '0';
s_ba <= '0';
loss_bit <= (others => '0');
loss_valid <= '0';
byte_done <= '0';
observed_byte <= (others => '0');
bits_sent <= (others => '0');
obs <= (others => '0');
r_nbc <= (others => '0');
r_nl <= (others => '0');
else
scl_q <= scl_in;
loss_valid <= '0';
byte_done <= '0';
if begin_byte = '1' then
sh_a <= byte_a;
sh_b <= byte_b;
bit_idx <= to_unsigned(7, 4);
running <= '1';
s_aa <= '1';
s_ba <= '1';
s_al <= '0';
s_bl <= '0';
obs <= (others => '0');
bits_sent <= (others => '0');
elsif running = '1' then
-- the read-back, WHILE SCL IS HIGH
if scl_rise_s = '1' then
r_nbc <= r_nbc + 1;
obs <= obs(6 downto 0) & s_line;
-- Master A: the asymmetric test. Sending 0 proves nothing.
if s_aa = '1' and sh_a(7) = '1' and s_line = '0' then
s_al <= '1';
s_aa <= '0'; -- turn the driver off IMMEDIATELY
loss_bit <= bit_idx;
if s_al = '0' and s_bl = '0' then
loss_valid <= '1';
r_nl <= r_nl + 1;
end if;
end if;
-- Master B: identical logic, no coordination with A whatsoever.
if s_ba = '1' and sh_b(7) = '1' and s_line = '0' then
s_bl <= '1';
s_ba <= '0';
loss_bit <= bit_idx;
if s_al = '0' and s_bl = '0' then
loss_valid <= '1';
r_nl <= r_nl + 1;
end if;
end if;
end if;
-- advance on the falling edge, where SDA is allowed to change
if scl_fall_s = '1' then
sh_a <= sh_a(6 downto 0) & '1'; -- shift in 1 = released
sh_b <= sh_b(6 downto 0) & '1';
bits_sent <= bits_sent + 1;
if bit_idx = to_unsigned(0, 4) then
running <= '0';
byte_done <= '1';
observed_byte <= obs;
else
bit_idx <= bit_idx - 1;
end if;
end if;
end if;
end if;
end if;
end process;
end architecture; -- The VHDL testbench for the bit-by-bit arbiter. Expectations are computed here from the two byte
-- values, independently of the DUT:
--
-- winner = the master with the numerically SMALLER byte
-- observed byte = min(A, B) NOT A and B
-- loss bit = the highest bit index where they differ
--
-- The pair 0x80 / 0x7F is in the suite specifically because min = 0x7F while the AND is 0x00, so it
-- separates "the winner's message, unchanged" from "the bitwise AND of both messages".
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_sda_arbiter_tb is
end entity;
architecture tb of i2c_sda_arbiter_tb is
constant TICK_W : natural := 16;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal scl : std_logic := '1';
signal begin_byte : std_logic := '0';
signal byte_a, byte_b : std_logic_vector(7 downto 0) := (others => '0');
signal sda_line, a_drive_low, b_drive_low : std_logic;
signal a_lost, b_lost, a_active, b_active, loss_valid, byte_done : std_logic;
signal loss_bit, bits_sent : unsigned(3 downto 0);
signal observed_byte : std_logic_vector(7 downto 0);
signal n_bits_compared, n_losses : unsigned(TICK_W-1 downto 0);
signal done : boolean := false;
signal errors : integer := 0;
function exp_observed(a, b : std_logic_vector(7 downto 0)) return std_logic_vector is
begin
if unsigned(a) < unsigned(b) then return a; else return b; end if;
end function;
function exp_loss_bit(a, b : std_logic_vector(7 downto 0)) return integer is
variable r : integer := -1;
begin
for k in 7 downto 0 loop
if r < 0 and a(k) /= b(k) then r := k; end if;
end loop;
return r;
end function;
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 : entity work.i2c_sda_arbiter
generic map (TICK_W => TICK_W)
port map (clk => clk, rst_n => rst_n, scl_in => scl,
begin_byte => begin_byte, byte_a => byte_a, byte_b => byte_b,
sda_line => sda_line, a_drive_low => a_drive_low, b_drive_low => b_drive_low,
a_lost => a_lost, b_lost => b_lost, a_active => a_active, b_active => b_active,
loss_bit => loss_bit, loss_valid => loss_valid,
byte_done => byte_done, observed_byte => observed_byte, bits_sent => bits_sent,
n_bits_compared => n_bits_compared, n_losses => n_losses);
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;
procedure send(a, b : in std_logic_vector(7 downto 0)) is
begin
byte_a <= a; byte_b <= b;
scl <= '0'; tick(4);
begin_byte <= '1'; tick(1); begin_byte <= '0';
for k in 0 to 7 loop
tick(6);
scl <= '1'; tick(8);
scl <= '0'; tick(2);
end loop;
tick(6);
scl <= '1'; tick(6);
end procedure;
procedure check_case(a, b : in std_logic_vector(7 downto 0); tag : in string) is
variable want_bit : integer;
begin
want_bit := exp_loss_bit(a, b);
send(a, b);
chk(observed_byte = exp_observed(a, b),
tag & ": the bus carried the wrong byte (the WINNER'S byte unchanged is expected, not the AND)");
if want_bit < 0 then
chk(a_lost = '0' and b_lost = '0',
tag & ": identical bytes produced a loss -- two masters sending the same thing must BOTH complete");
else
chk(to_integer(loss_bit) = want_bit, tag & ": the loss was reported at the wrong bit");
if unsigned(a) < unsigned(b) then
chk(a_lost = '0' and b_lost = '1', tag & ": A is smaller so B must lose");
else
chk(a_lost = '1' and b_lost = '0', tag & ": A is larger so A must lose");
end if;
end if;
end procedure;
variable l0, c0 : integer;
begin
tick(4); rst_n <= '1'; tick(4);
-- 1. reset
chk(n_losses = 0 and n_bits_compared = 0, "counters nonzero out of reset");
chk(a_lost = '0' and b_lost = '0', "a loss was reported out of reset");
chk(sda_line = '1', "SDA not released out of reset");
-- 2. IDENTICAL transmissions: both masters complete, no loss
check_case(x"A5", x"A5", "identical");
chk(a_active = '1' and b_active = '1',
"identical bytes left a master inactive -- both must still be driving");
-- 3. loss on the very FIRST bit
check_case(x"00", x"80", "first-bit loss");
-- 4. loss on the LAST bit
check_case(x"A4", x"A5", "last-bit loss");
-- 5. THE pair that separates min from AND
check_case(x"80", x"7F", "min-not-AND");
chk(observed_byte = x"7F",
"0x80 vs 0x7F did not carry 0x7F -- 'no information is lost' means the WINNER'S message survives, not that the two are ANDed");
-- 6. the mirror of test 5
check_case(x"7F", x"80", "min-not-AND mirrored");
-- 7. a mid-byte loss with the loser holding zeros AFTER the loss point
check_case(x"A0", x"9F", "loser has zeros later");
-- 8. the loser stops driving IMMEDIATELY, checked at the driver
byte_a <= x"FF"; byte_b <= x"00";
scl <= '0'; tick(4);
begin_byte <= '1'; tick(1); begin_byte <= '0';
tick(6);
scl <= '1'; tick(3);
chk(a_active = '0', "master A was still active after losing on the first bit");
chk(a_drive_low = '0', "master A was still driving SDA low after losing");
chk(b_active = '1', "the WINNER stopped driving -- arbitration must not affect it");
scl <= '0'; tick(4);
for k in 1 to 7 loop
tick(6); scl <= '1'; tick(8); scl <= '0'; tick(2);
end loop;
tick(6); scl <= '1'; tick(6);
chk(observed_byte = x"00", "with A=FF and B=00 the bus carried the wrong byte");
-- 9. exactly ONE loss per contested byte
l0 := to_integer(n_losses);
check_case(x"F0", x"0F", "one loss only");
chk(to_integer(n_losses) = l0 + 1, "a contested byte did not produce exactly one loss");
-- 10. an uncontested byte produces no loss
l0 := to_integer(n_losses);
check_case(x"3C", x"3C", "uncontested");
chk(to_integer(n_losses) = l0, "an uncontested byte reported a loss");
-- 11. eight bits are compared per byte
c0 := to_integer(n_bits_compared);
check_case(x"55", x"55", "bit count");
chk(to_integer(n_bits_compared) = c0 + 8, "the wrong number of bits was compared in one byte");
if errors = 0 then
report "i2c_sda_arbiter self-check complete: the test is asymmetric so identical transmissions both complete, the loser stops driving immediately, the bus carries the winner's byte unchanged rather than the bitwise AND, and no arbiter decides anything" severity note;
else
report "FAILURES in i2c_sda_arbiter" severity error;
end if;
done <= true;
wait;
end process;
end architecture;7a. Five Decisions Worth Defending
The comparison is asymmetric, and written as a_bit && !sda_line rather than as an inequality. §2's table is the argument. §8 records that the symmetric form is provably equivalent in this design — and why the asymmetric form is kept anyway.
The read-back happens on the rising edge, while SCL is high. §3.1.8 says "while SCL is HIGH", and that is where the data is valid (Chapter 4.2). Comparing during the low phase reads a line that is being driven to its next value. Mutation P5 moves it to the falling edge and the very first test catches it.
Each master's check is independent, with no shared state. §3's diagram has no path between the comparators, and the code has no variable read by both. Mutation P6 disables master B's check entirely.
The observed byte is sampled from the LINE, one bit per high phase. Not computed from the two patterns — that is the distinction §4 is about, and mutation P4 is the computed version.
The loser's driver is gated on a_active, so turning the flag off turns the driver off in the same cycle. §3.1.8: "the moment there is a difference … the DATA 1 output is switched off." A separate "stop driving" step, reached a cycle later, would put one more of the loser's bits on the wire.
7b. Verified Execution
$ iverilog -g2012 -o d3 i2c_sda_arbiter.sv i2c_sda_arbiter_tb.sv && ./d3
PASS: the test is asymmetric so identical transmissions both complete, the loser stops
driving immediately, the bus carries the winner's byte unchanged rather than the bitwise
AND, and no arbiter decides anything
i2c_sda_arbiter_tb.sv:218: $finish called at 14550000 (1ps)
$ iverilog -g2005 -o v3 i2c_sda_arbiter.v i2c_sda_arbiter_tb.v && ./v3
PASS: the test is asymmetric so identical transmissions both complete, the loser stops
driving immediately, the bus carries the winner's byte unchanged rather than the bitwise
AND, and no arbiter decides anything
i2c_sda_arbiter_tb.v:220: $finish called at 14550000 (1ps)
$ nvc -a i2c_sda_arbiter.vhd i2c_sda_arbiter_tb.vhd
$ nvc -e i2c_sda_arbiter_tb && nvc -r i2c_sda_arbiter_tb --stop-time=1000us
** Note: 14550ns+1: i2c_sda_arbiter self-check complete: the test is asymmetric so identical
transmissions both complete, the loser stops driving immediately, the bus carries the
winner's byte unchanged rather than the bitwise AND, and no arbiter decides anythingAll three at 14550 ns — the shortest checker run in either module, because arbitration is eight comparisons.
7c. What the Testbench Proves
Every expectation is computed from the two byte values at the call site, independently of the design: the winner is the smaller byte, the observed byte is min(A,B), and the loss bit is the highest index at which they differ.
| # | stimulus | what it establishes |
|---|---|---|
| 1 | reset | no loss reported; SDA released |
| 2 | identical bytes A5/A5 | no loss, and both masters still active |
| 3 | 00 vs 80 | loss on the first bit |
| 4 | A4 vs A5 | loss on the last bit |
| 5 | 80 vs 7F | the bus carries 7F — min, not AND |
| 6 | 7F vs 80 | the same, with the masters swapped |
| 7 | A0 vs 9F | the loser's later zeros never reach the wire |
| 8 | FF vs 00 | the loser's driver is off immediately; the winner is unaffected |
| 9 | F0 vs 0F | exactly one loss per contested byte |
| 10 | 3C vs 3C | an uncontested byte reports no loss |
| 11 | 55 vs 55 | eight bits compared per byte |
Test 5 is the most important test in the chapter. It is the only stimulus in the suite for which min(A,B) and A & B differ substantially, and without it mutation P4 survives — a design merging both masters for the whole byte would pass tests 2, 3, 4, 9, 10 and 11 without complaint.
Test 2 is what makes the asymmetry testable. Identical bytes must produce no loss and leave both masters driving. A design whose comparison was "did the line match" in a form that fired on agreement would fail here and nowhere else.
Test 8 checks the driver rather than the flag, because the driver is what the bus sees. And it checks the winner too: b_active must still be set, because §3.1.8 says the contest does not affect the winning master. A design that stopped both masters would pass every loss check and break every transfer.
Test 11 is a count, and counts catch structural errors. Eight comparisons per byte: a design comparing on both edges reports sixteen, and one comparing only when a loss occurs reports far fewer. Mutation P5 is caught by test 2's value and would also be caught here.
8. Mutation Testing
Six defects injected into the SystemVerilog arbiter, and one removed as provably equivalent.
| # | injected defect | outcome |
|---|---|---|
| P1 | only the first bit of the byte is arbitrated | killed — test 7 |
| P2 | the loss test is inverted: sending 0 and seeing 1 | killed — test 3 |
| P3 | the loser keeps its SDA driver on | killed — test 5 |
| P4 | the observed byte is the bitwise AND of both patterns | killed — test 5 |
| P5 | the read-back happens on the falling edge | killed — test 2 |
| P6 | only master A is ever checked | killed — test 3 |
| — | removed: the comparison is made symmetric | equivalent by construction — see below |
Six injected, six killed. Two notes, and the second is the more interesting.
P3 and P4 fail with the same message, and that is worth noticing. Both produce the bus carried 00, expected 7f on test 5 — because a loser that keeps driving and a design that ANDs both patterns are, on the wire, the same thing. They are different defects with one symptom, which is a reminder that a failing assertion identifies a symptom and not a cause.
The symmetric comparison is a provably equivalent mutant, and the proof is short. Replacing a_bit && !sda_line with a_bit != sda_line survived every test. It is not a test gap:
a_drive_low = a_active && !a_bit -- open-drain: drive low for a zero
sda_line = !a_drive_low && !b_drive_low -- wired-AND
so, while a master is active:
a_bit = 0 => a_drive_low = 1 => sda_line = 0
therefore the only mismatch reachable while active is a_bit = 1, sda_line = 0
-- which IS the asymmetric condition. No stimulus can distinguish the two forms.By Chapter 11.4 §8's taxonomy this is equivalence by construction, not by configuration — no parameterisation exposes it. And Chapter 11.2 §8's precedent says the honest response to such a mutant is to delete the dead code.
Here the code is not dead, so it is kept — and the reason is worth recording. Two arguments:
It states the specification's own wording. §3.1.8 describes a master that "tries to send a HIGH, but detects that the SDA level is LOW". Code that reads that way is code a reviewer can check against the source.
And the equivalence depends on reading back the same combinational node the master drives — which no real master does. A real master reads a synchronised and possibly filtered pin (Chapter 11.8), so a_bit = 0 can briefly coexist with a stale high reading while the filter catches up. In that design the symmetric form reports a loss that never happened, and the asymmetric form does not.
An equivalence that holds only because of an idealisation in the model is not a reason to adopt the weaker form. The proof is real; its premise is an artefact of the testbench.
9. Verification Connection — One Property, and the One That Must Not Be Written
// THE property of the chapter, and note the antecedent: only "sent a one" qualifies. A property
// written over both polarities would be asserting something about a case in which the master is
// itself the cause of the level it observes.
property p_loss_iff_sent_one_saw_zero;
@(posedge clk) (scl_in && $rose(scl_in) && my_bit && !sda_in) |=> arb_lost;
endproperty
assert property (p_loss_iff_sent_one_saw_zero)
else $error("sent a one and observed a zero while SCL was high, but no loss was declared");
// The NEGATIVE property that matters most here: sending a zero is never evidence. Without it a
// monitor can be "correct" on contested traffic and declare a loss on every zero it sends --
// which is every transfer, since addresses contain zeros.
property p_sending_zero_is_never_a_loss;
@(posedge clk) (!my_bit) |-> !$rose(arb_lost);
endproperty
assert property (p_sending_zero_is_never_a_loss)
else $error("a loss was declared while this master was itself pulling SDA low");
// The winner is UNAFFECTED. Section 3.1.8 states it twice, and it is the property that catches a
// design which stops both masters on a contest.
property p_winner_unaffected;
@(posedge clk) (arb_lost_other && !arb_lost) |-> still_driving;
endproperty
assert property (p_winner_unaffected)
else $error("the winning master stopped driving -- arbitration must not affect it");
// And the property that must NOT be written:
//
// property p_exactly_one_loser;
// @(posedge clk) contested |-> (a_lost ^ b_lost);
// endproperty
//
// It is FALSE. Section 3.1.8: "Two masters can actually complete an entire transaction without
// error, as long as the transmissions are identical." Identical transmissions produce no loser at
// all, so this property fails on legal traffic -- and because the traffic is legal, the failure
// would be waived, which trains reviewers to waive the ones that matter.
property p_at_most_one_loser;
@(posedge clk) 1'b1 |-> !(a_lost && b_lost);
endproperty
assert property (p_at_most_one_loser)
else $error("both masters declared a loss -- impossible, since a zero always wins"); covergroup i2c_arb_cg with function sample(int loss_bit, bit contested, int byte_a, int byte_b,
int common_prefix_len, bit in_address_phase);
// WHERE the loss happened. A suite whose contests always separate on bit 7 has not tested
// arbitration -- it has tested one comparison. The bins that matter are the extremes: bit 7 is
// the first opportunity, bit 0 the last, and "never" is the identical-transmission case.
decided_at: coverpoint loss_bit {
bins first_bit = {7};
bins early = {[5:6]};
bins middle = {[2:4]};
bins last_bit = {0};
bins late = {1};
}
// THE bin a suite is most likely to leave empty, and section 5 is why it must not be: two
// masters sending identical bytes never separate, and a checker asserting that somebody loses
// is wrong. This is the coverage form of test 2.
resolution: coverpoint contested {
bins identical = {0}; // no loser exists -- section 3.1.8 permits this
bins contested = {1};
}
// How long the two messages agreed before diverging. "This process may take many bits", so a
// suite where the prefix is always zero or always eight has exercised neither the immediate
// decision nor the drawn-out one.
prefix: coverpoint common_prefix_len {
bins immediate = {0};
bins short = {[1:3]};
bins long = {[4:7]};
bins whole = {8}; // identical: arbitration ran the entire byte
}
decided_x_prefix: cross decided_at, prefix;
// ADDRESS phase versus DATA phase, because losing during addressing carries an obligation that
// losing later does not -- Chapter 13.4's obligation 5. A suite that only ever contests in the
// data phase leaves that obligation unexercised.
phase: coverpoint in_address_phase { bins address = {1}; bins data = {0}; }
resolution_x_phase: cross resolution, phase;
// And the pair that separates min from AND. Covering it explicitly is the only way a report can
// state that the distinction was tested rather than assumed.
min_not_and: coverpoint ((byte_a & byte_b) != ((byte_a < byte_b) ? byte_a : byte_b)) {
bins and_differs_from_min = {1};
}
endgroup10. FPGA and ASIC Implications
The arbiter is one comparator and one flag per master — a handful of flops. This is the cheapest mandatory feature in the specification, and that is worth saying because its absence is so consequential: Chapter 13.1 §11 is a production bus corrupted for want of about fifteen lines.
The required hardware is an SDA input path. A synchroniser, and the comparator. A master whose SDA is output-only cannot arbitrate at all — Table 2 lists arbitration as mandatory for a multi-master configuration, so such a master is non-compliant on such a bus, and its failure mode is silent data corruption rather than an error.
The read-back must be taken where the data is valid, and the filter is inside that window. SCL high is the sample point, and if the design filters SDA (Chapter 11.8) then the filtered value is what must be compared — so the filter's T_SP + 1 latency has to fit inside the high phase. At Fast-mode Plus with a 50 ns filter that is 60 ns of a 260 ns minimum high phase, which is comfortable; a filter sized without regard to it would not be.
Address allocation is the only priority lever there is. §3 established that the winner is decided by the message. So a device that must usually win a contest should be given a low slave address, and a master that must usually win should address low-numbered targets. There is no configuration bit and no arbitration weight — only the numbers in the address map. That is a system-architecture decision that has to be made before the addresses are assigned, and it cannot be retrofitted.
A losing master must not merely stop driving — it must not resume. Which is Chapter 13.4's subject, and the reason it needs a chapter: stopping is one line, and the four obligations that follow are not.
And on a bus with three or more masters, arbitration still resolves in one pass. Every master runs the same self-test, and every master sending a one at the deciding bit loses simultaneously. There is no round-robin and no second round: the survivors are exactly those sending the minimum byte, and if several are sending the same minimum they all continue — §5's case, with more participants.
11. Debugging — The Two Masters That Both Thought They Had Written the Register
Pitfall — assuming a contested transfer always produces a loser
// A dual-redundant controller: two identical boards, either of which can drive a shared I2C bus to
// a set of actuators. The design is deliberately symmetric -- both boards run the same firmware
// image, address the same targets, and write the same values.
//
// Both masters implement arbitration correctly. Both read SDA back, both detect loss, both retry.
// And the supervisor layer relied on arbitration to enforce single-writer semantics:
//
// // "Only one board can win the bus, so only one board performs the write. The loser retries
// // and finds the register already set, which is idempotent, so this is safe."
// write_actuator(ACTUATOR_ADDR, SAFE_STATE);
// if (arb_lost) {
// retry_when_free(); // the other board did it; we will confirm
// } else {
// mark_write_performed(); // WE did it -- used for the audit log and for the
// // "which board is active" indicator
// }
//
// The reasoning is explicit and it is wrong in one word: "only one board CAN win".Under normal operation the system behaved correctly and the active-board indicator was stable.
During a failover rehearsal -- both boards brought up simultaneously, deliberately -- both boards logged that they had performed the write. Both set mark_write_performed(). The active-board indicator showed two active boards, which the supervisor treated as a fault and escalated to a full shutdown.
The actuator itself was in the right state. Exactly one transfer appeared on the bus. The slave acknowledged once. Nothing was corrupted and nothing was lost -- the only thing wrong was that two boards each believed they were the one that had done it.
The first theory was that arb_lost was not being latched on one board, and a great deal of time went into that signal on both boards. It was correct on both: neither board had lost, because arb_lost never rose on either.
Which was dismissed as impossible for a while, because "two masters transferred and neither lost" reads like a contradiction.
It is not. Section 3.1.8: "Two masters can actually complete an entire transaction without error, AS LONG AS THE TRANSMISSIONS ARE IDENTICAL."
Both boards ran the same firmware image. Both addressed the same actuator. Both wrote the same SAFE_STATE value. There was no differing bit anywhere in the transfer -- not in the address, not in the R/W bit, not in the data byte -- so the asymmetric test never fired on either master. Both drove every bit, the wired-AND merged two identical commands into one, and the slave saw a single perfectly ordinary transfer.
Arbitration had nothing to resolve, so it resolved nothing, and both boards were correct in believing they had performed the write. They had -- jointly, and indistinguishably.
Arbitration separates DIFFERENT messages. It provides no mechanism whatsoever for separating identical ones, because on a wired-AND bus two identical commands are indistinguishable from one (Chapter 13.1 section 4).
The supervisor layer used "I did not lose arbitration" as a proxy for "I was the only writer". That inference is invalid, and it is invalid in exactly the case a redundant design is most likely to produce: two identical units doing the identical thing at the identical moment.
The deeper error is treating arbitration as a mutual-exclusion primitive. It is not one. It is a collision-RESOLUTION mechanism whose guarantee is that the bus carries one coherent message and that no data is corrupted. It guarantees nothing about how many devices believe they sent that message, and section 3.1.8 says so in as many words.
Note also what was NOT broken: the bus, the slave, the actuator, and both masters' arbitration logic. The failure was entirely in an inference drawn above them.
12. Common Misconceptions
"Arbitration needs an arbiter." There is none, and nowhere to put one. Each master runs one self-test and the wire has already decided.
"A master can be given higher priority." §3.1.8: "there is no central master, nor any order of priority on the bus." Control is decided by the data, so a lower-valued message is the only lever — which makes it an address-allocation decision.
"The test is: does the line match what I sent?" Only in one direction. Sending a zero makes the line zero, so a match proves nothing. Only sent one, saw zero is conclusive.
"No information is lost means the bus carries both messages ANDed." It carries the winner's message unchanged. For 0x80 against 0x7F, the bus carries 0x7F and the AND would be 0x00.
"A contested transfer always has a loser." Not if the transmissions are identical — then neither master ever sees a differing bit and both complete. §5.
"Arbitration is decided in the address byte." "This process may take many bits." It is decided at the first differing bit, wherever that is — possibly never.
"The winner has to retry or be notified." The winner is unaffected and notices nothing. §3.1.8 states it twice.
"Arbitration gives me mutual exclusion." It gives one coherent message on the wire. Two devices can both believe they sent it, which is §11 taking a redundant system down.
13. Reason It Through
Why is "I sent a zero and the line is zero" not evidence of winning?
Because you are the cause of the zero. On an open-drain bus a low line means somebody is pulling it down, and when you are pulling it down the observation is fully explained by your own action. Nothing about anyone else follows.
Masters send 0x80 and 0x7F. What appears on the bus, and what does the AND reading predict?
The bus carries 0x7F — B wins at bit 7 and then sends seven ones, all of which reach the wire. The AND reading predicts 0x00, which is wrong in seven of eight bits, and is a byte neither master sent.
Two masters send identical addresses and identical data. How many lose?
Neither. There is no differing bit, so the asymmetric test never fires, and both drive the entire transfer. The slave sees one transfer and both masters correctly believe they conducted it.
A supervisor uses "arb_lost did not rise" to conclude it was the only writer. Under what circumstances is that wrong, and why is it the likeliest case for a redundant design?
It is wrong whenever there was no contest to lose — which includes the case where another master sent the identical message. Two interchangeable units running identical firmware produce identical messages by construction, so that is precisely the configuration in which the inference fails.
A design's arbitration works on every test except one where the bus carries a byte neither master sent. What are the two likely causes, and why do they look the same?
Either the loser kept driving, or the observed byte was computed as the AND of both patterns instead of sampled from the line. They produce the same waveform, because a loser that never stops is the AND. A failing assertion identifies a symptom, not a cause.
Three masters contend and two are sending the same minimum byte. What happens?
The third loses at the first bit where it sends a one against their zero; the two sending the identical minimum never separate and both complete. Arbitration resolves in a single pass — there is no second round — and the survivors are exactly the set sending the minimum.
14. Understanding Check
15. Summary
Arbitration proceeds bit by bit, while SCL is high, and each master performs one test on itself.
The test is asymmetric. Sent one, saw zero: lost. Anything else: nothing learned. Sending a zero is never evidence, because the master is the cause of the zero.
There is no arbiter and no priority. The wire has already decided, and each master only discovers the result about itself. Control is settled by the message, so a low-valued address is the only priority lever that exists.
"No information is lost" means the winner's message survives unchanged — min(A,B), not A & B. For 0x80 against 0x7F those differ in seven of eight bits.
The winner is unaffected and notices nothing. It does not retry and does not need to be told.
Identical transmissions produce no loser and both masters complete. So a design must not assume a loser exists, and arbitration is not a mutual-exclusion primitive.
The loser's driver must go off in the same cycle, or one more of its bits reaches the wire.
And the cost is one comparator and one flag — the cheapest mandatory feature in the specification, whose absence corrupts data silently.
16. What Comes Next
The loser has discovered it lost and switched its driver off. Chapter 13.4 is about everything it must do next — and §3.1.8 places five obligations on it, of which stopping is only the second.
The one that is almost always missed is the fifth, and its reasoning is worth previewing because it is not obvious:
"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 losing master was transmitting an address. The winner's address differs from its own only from the deciding bit onward — and may be this device's own slave address. 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.
The chapter also settles what the loser may and may not do with the clock, and why the specification grants it permission to keep clocking rather than requiring it to stop.
Continue learning
Related tutorials
- Related topic
Reasoning About Multi-Master Arbitration
Arbitration worked on a waveform rather than from a definition: what a controller can know, why the winner never learns it won, and a measured comparison of three loss-detection rules — one missing 9.3 % of losses entirely, another driving for six more bits onto the winner’s transfer.
- Related topic
Wired-AND — Many Drivers, One Line, No Contention
Put several open-drain devices on one node and a logic function appears in the wiring: any participant asserting LOW wins, and HIGH requires unanimous release. Derive dominant LOW, see why two devices pulling together is agreement rather than conflict, and watch three of the protocol's mechanisms become predictable.
- Related topic
The I²C Stretching Mechanism — Holding SCL Low
Stretching needed no new mechanism: the specification already described it for multi-master synchronization. One sentence decides whether a master survives it — and getting it wrong collapses the high phase on the bit a stretch ended.
- 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.
