I²C · Module 17
SDA Open-Drain Control and Line Ownership in RTL
The transmitted bit is the inverse of the drive enable, and that inversion belongs in exactly one place. Makes SDA ownership an explicit signal rather than an implication of the state encoding, shows why an ownership conflict must be reported rather than resolved silently, and derives arbitration detection from three signals the block already has — including the intent bit without which every read looks like a lost arbitration.
SCL has one owner: this master, always. Chapter 17.3 could therefore treat it as a line it drives and reads, and nothing else.
SDA is a different problem. It changes hands once per byte, and four separate blocks inside the master want to drive it at different moments. That makes ownership the subject of this chapter, and it makes "who is driving SDA right now" a question the hardware has to answer explicitly rather than imply.
1. The Sign Error This Block Exists to Prevent
An I²C output drives LOW or RELEASES. There is no drive-high. So:
tx_bit = 1 -> drive_low = 0 release; the pull-up makes the line high
tx_bit = 0 -> drive_low = 1 pull it downThe transmitted bit is the inverse of the drive enable. Chapter 17.1 named this the single most common sign error in a first I²C master, and the reason it is so common is that the inversion is easy to write correctly once and then to write again, differently, somewhere else.
That is a small design decision with a disproportionate payoff: a sign error can now exist in only one line of the whole master, and one mutation (M1) tests it.
2. Ownership Is a Signal, Not an Implication
So in every byte, ownership of SDA changes hands exactly once — at the ninth pulse — and changes back at the start of the next byte. Four blocks in this master want to drive SDA:
| Owner | Drives SDA for | Chapter |
|---|---|---|
| the framer | START and STOP edges | 17.5 |
| the bit engine | the eight data bits | 17.6 |
| the ack driver | the master's ACK when reading | 17.7 |
| recovery | the STOP that ends a bus clear | 17.11 |
Now the argument for making this explicit. If ownership is implied by the transaction controller's state encoding, then every one of those four blocks must be gated by a decode of that encoding — and adding a state to the controller silently changes who owns the line. The coupling is invisible: nothing in the framer's source mentions the controller, yet the framer's behaviour depends on the controller's state list.
Making ownership an explicit request/grant makes a conflict detectable. And this block detects it rather than quietly resolving it:
The priority is stated rather than emergent — the framer outranks the bit engine, because a START must be able to pre-empt a data bit — which is test T4.
3. Arbitration, for Free
Written as a condition, that is:
arbitration lost = (we are transmitting)
AND (we sent a one)
AND (SDA reads low)
AND (SCL reads high)Every one of those four signals is already in this block. No dedicated arbitration detector is required — and Chapter 17.10 shows the identical shape on SCL is clock stretching.
3a. The asymmetry the specification implies and does not state
A master can only ever lose by trying to send a one.
Sending a zero and reading a zero is indistinguishable from winning, because the wired-AND hides the competitor. So a master transmitting all zeros cannot detect a rival at all — until the rival sends a one, which it must, since two addresses that differ have to differ somewhere.
That is not a limitation to be fixed. It is why bitwise arbitration terminates.
3b. The intent bit, without which every read is a lost arbitration
This is the subtlest requirement in the block, and it is the one a first implementation omits.
A receiving master has released SDA on purpose. Electrically, releasing is transmitting a one. So "sent a one, read a zero" is true of every single bit of every read — and a detector without a gate on transmit intent declares arbitration lost on the first zero any target ever sends back.
This is the same lesson as Chapter 17.1's monitor boundary, seen from inside the design: attribution requires an assumption the bus does not supply. Mutation M5 below removes tx_active and the suite catches it.
4. The Ownership Path
Note what does not appear: any path from the transaction controller's state into the four owner blocks. That absence is the point of §2.
5. The Block, in Three Languages
// -----------------------------------------------------------------------------
// i2c_sda_ctrl.sv
// SDA open-drain control and line ownership.
//
// THE SIGN ERROR THIS BLOCK EXISTS TO PREVENT. An I²C output drives LOW or RELEASES;
// there is no drive-high. So the bit a master transmits is the INVERSE of its drive
// enable, and the block that converts "the bit I want to send" into "pull the line
// down" is the single most common place to get a sign wrong. Making it one named
// block with one named conversion means the inversion appears exactly once.
//
// tx_bit = 1 -> drive_low = 0 (release; the pull-up makes it high)
// tx_bit = 0 -> drive_low = 1 (pull it down)
//
// OWNERSHIP IS A SIGNAL, NOT AN IMPLICATION. UM10204 §3.1.4: "the transmitter releases
// the SDA line during the acknowledge clock pulse so the receiver can pull the SDA
// line LOW". So in every byte, ownership of SDA changes hands exactly once -- at the
// ninth pulse -- and changes back at the start of the next byte. Four blocks in this
// master want to drive SDA at different times:
//
// the framer -- START and STOP edges (Chapter 17.5)
// the bit engine -- the eight data bits (Chapter 17.6)
// the ack driver -- the master's ACK when reading (Chapter 17.7)
// recovery -- the STOP that ends a bus clear(Chapter 17.11)
//
// If ownership is implied by the controller's state encoding, then every one of those
// blocks has to be gated by a decode of that encoding, and adding a state silently
// changes who owns the line. Making ownership explicit means a conflict is detectable,
// and this block detects it rather than resolving it quietly: two simultaneous owners
// is a design error, not a bus condition, and a design error that resolves itself is a
// design error that ships.
//
// ARBITRATION, FOR FREE. §3.1.8: "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." That is
//
// (released) AND (line reads low) AND (SCL is high)
//
// which is three signals this block already has. No dedicated detector is required,
// and Chapter 17.10 shows the identical shape on SCL is clock stretching.
//
// Note the asymmetry the specification implies and does not state: a master can only
// ever LOSE by trying to send a one. Sending a zero and reading a zero is
// indistinguishable from winning, so a master transmitting all zeros cannot detect a
// competitor at all -- until the competitor sends a one, which it must, since two
// addresses that differ have to differ somewhere.
// -----------------------------------------------------------------------------
module i2c_sda_ctrl #(
// Which owner index wins if two request at once. Resolving rather than jamming
// keeps the bus in a defined state while `owner_conflict` reports the bug.
parameter int N_OWNER = 4,
parameter int CNT_W = 16
) (
input logic clk,
input logic rst_n,
// One request and one bit value per owner. Bit i is owner i:
// 0 = framer, 1 = bit engine, 2 = ack driver, 3 = recovery.
input logic [N_OWNER-1:0] req,
input logic [N_OWNER-1:0] bit_val, // the BIT each owner wants to transmit
// The bus.
input logic sda_in, // SDA, read back
input logic scl_in, // SCL, read back -- arbitration is checked
// only while SCL is high (§3.1.8)
// ARBITRATION APPLIES ONLY WHILE TRANSMITTING, and this is the signal that says so.
//
// A receiving master has RELEASED SDA on purpose, which electrically is transmitting
// a one. So "sent a one, read a zero" is true of every single bit of every read, and
// a detector without this gate declares arbitration lost on the first zero any
// target ever sends back. The electrical condition is identical in the two cases and
// only the master's intent separates them, which is why the intent has to be an
// input rather than something the block infers.
input logic tx_active, // we are driving data, not receiving
output logic sda_drive_low,
// Who actually has the line, and what we believe we are transmitting.
output logic [N_OWNER-1:0] grant,
output logic owned, // somebody owns it
output logic tx_bit, // the bit we intend to transmit
output logic owner_conflict, // two owners requested at once: a BUG
output logic [CNT_W-1:0] conflicts,
// Arbitration, from the same three signals.
output logic arb_loss_now, // this cycle: we sent 1 and the line is 0
output logic arb_lost, // sticky until cleared
output logic [CNT_W-1:0] arb_losses,
input logic arb_clear // the controller has handled it
);
// ---- priority resolution ------------------------------------------------
// Lowest index wins. The framer outranks the bit engine because a START must be
// able to pre-empt a transfer -- §3.1.10 note 4 requires a device to accept a START
// anywhere, and a master issuing one must not be fighting its own datapath.
logic [N_OWNER-1:0] higher;
genvar g;
generate
for (g = 0; g < N_OWNER; g = g + 1) begin : gen_grant
if (g == 0) assign higher[g] = 1'b0;
else assign higher[g] = |req[g-1:0];
end
endgenerate
assign grant = req & ~higher;
assign owned = |req;
// The bit the granted owner wants. With one-hot grant this is an OR of the masked
// values; with nobody granted it is 1, because a released line reads high and a
// master that owns nothing is transmitting nothing.
wire [N_OWNER-1:0] masked = grant & bit_val;
assign tx_bit = owned ? (|masked) : 1'b1;
// THE INVERSION, in one place -- and gated by reset.
//
// Reset dominance on the PAD, not merely on the logic feeding it, is a hardware
// requirement rather than defensive coding. A master's reset is asynchronous to the
// bus, so a master reset in the middle of a transfer must let go of SDA in the same
// instant; if its output enable survives reset it holds the line low and creates
// exactly the stuck bus of §3.1.16, for which the nine-pulse remedy exists. The
// blocks that drive `req` are reset too, but relying on that makes the pad's
// behaviour depend on every one of them, and a single un-reset request bit
// anywhere upstream takes the bus down.
assign sda_drive_low = (rst_n && owned) ? ~tx_bit : 1'b0;
// ---- arbitration --------------------------------------------------------
// Sent a one, the line reads zero, SCL is high so everybody is sampling -- and we
// were transmitting rather than receiving.
assign arb_loss_now = tx_active && owned && tx_bit && !sda_in && scl_in;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
owner_conflict <= 1'b0;
conflicts <= {CNT_W{1'b0}};
arb_lost <= 1'b0;
arb_losses <= {CNT_W{1'b0}};
end else begin
// More than one request is a design error. Report it and count it; the
// priority resolution above has already kept the bus defined.
owner_conflict <= (req != (req & ~higher));
if ((req != (req & ~higher)) && !owner_conflict)
conflicts <= conflicts + 1'b1;
if (arb_clear) begin
arb_lost <= 1'b0;
end else if (arb_loss_now && !arb_lost) begin
arb_lost <= 1'b1;
arb_losses <= arb_losses + 1'b1;
end
end
end
endmodule // -----------------------------------------------------------------------------
// i2c_sda_ctrl.sv
// SDA open-drain control and line ownership.
//
// THE SIGN ERROR THIS BLOCK EXISTS TO PREVENT. An I²C output drives LOW or RELEASES;
// there is no drive-high. So the bit a master transmits is the INVERSE of its drive
// enable, and the block that converts "the bit I want to send" into "pull the line
// down" is the single most common place to get a sign wrong. Making it one named
// block with one named conversion means the inversion appears exactly once.
//
// tx_bit = 1 -> drive_low = 0 (release; the pull-up makes it high)
// tx_bit = 0 -> drive_low = 1 (pull it down)
//
// OWNERSHIP IS A SIGNAL, NOT AN IMPLICATION. UM10204 §3.1.4: "the transmitter releases
// the SDA line during the acknowledge clock pulse so the receiver can pull the SDA
// line LOW". So in every byte, ownership of SDA changes hands exactly once -- at the
// ninth pulse -- and changes back at the start of the next byte. Four blocks in this
// master want to drive SDA at different times:
//
// the framer -- START and STOP edges (Chapter 17.5)
// the bit engine -- the eight data bits (Chapter 17.6)
// the ack driver -- the master's ACK when reading (Chapter 17.7)
// recovery -- the STOP that ends a bus clear(Chapter 17.11)
//
// If ownership is implied by the controller's state encoding, then every one of those
// blocks has to be gated by a decode of that encoding, and adding a state silently
// changes who owns the line. Making ownership explicit means a conflict is detectable,
// and this block detects it rather than resolving it quietly: two simultaneous owners
// is a design error, not a bus condition, and a design error that resolves itself is a
// design error that ships.
//
// ARBITRATION, FOR FREE. §3.1.8: "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." That is
//
// (released) AND (line reads low) AND (SCL is high)
//
// which is three signals this block already has. No dedicated detector is required,
// and Chapter 17.10 shows the identical shape on SCL is clock stretching.
//
// Note the asymmetry the specification implies and does not state: a master can only
// ever LOSE by trying to send a one. Sending a zero and reading a zero is
// indistinguishable from winning, so a master transmitting all zeros cannot detect a
// competitor at all -- until the competitor sends a one, which it must, since two
// addresses that differ have to differ somewhere.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_sda_ctrl #(
// Which owner index wins if two request at once. Resolving rather than jamming
// keeps the bus in a defined state while `owner_conflict` reports the bug.
parameter N_OWNER = 4,
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
// One request and one bit value per owner. Bit i is owner i:
// 0 = framer, 1 = bit engine, 2 = ack driver, 3 = recovery.
input wire [N_OWNER-1:0] req,
input wire [N_OWNER-1:0] bit_val, // the BIT each owner wants to transmit
// The bus.
input wire sda_in, // SDA, read back
input wire scl_in, // SCL, read back -- arbitration is checked
// only while SCL is high (§3.1.8)
// ARBITRATION APPLIES ONLY WHILE TRANSMITTING, and this is the signal that says so.
//
// A receiving master has RELEASED SDA on purpose, which electrically is transmitting
// a one. So "sent a one, read a zero" is true of every single bit of every read, and
// a detector without this gate declares arbitration lost on the first zero any
// target ever sends back. The electrical condition is identical in the two cases and
// only the master's intent separates them, which is why the intent has to be an
// input rather than something the block infers.
input wire tx_active, // we are driving data, not receiving
output wire sda_drive_low,
// Who actually has the line, and what we believe we are transmitting.
output wire [N_OWNER-1:0] grant,
output wire owned, // somebody owns it
output wire tx_bit, // the bit we intend to transmit
output reg owner_conflict, // two owners requested at once: a BUG
output reg [CNT_W-1:0] conflicts,
// Arbitration, from the same three signals.
output wire arb_loss_now, // this cycle: we sent 1 and the line is 0
output reg arb_lost, // sticky until cleared
output reg [CNT_W-1:0] arb_losses,
input wire arb_clear // the controller has handled it
);
// ---- priority resolution ------------------------------------------------
// Lowest index wins. The framer outranks the bit engine because a START must be
// able to pre-empt a transfer -- §3.1.10 note 4 requires a device to accept a START
// anywhere, and a master issuing one must not be fighting its own datapath.
wire [N_OWNER-1:0] higher;
genvar g;
generate
for (g = 0; g < N_OWNER; g = g + 1) begin : gen_grant
if (g == 0) assign higher[g] = 1'b0;
else assign higher[g] = |req[g-1:0];
end
endgenerate
assign grant = req & ~higher;
assign owned = |req;
// The bit the granted owner wants. With one-hot grant this is an OR of the masked
// values; with nobody granted it is 1, because a released line reads high and a
// master that owns nothing is transmitting nothing.
wire [N_OWNER-1:0] masked = grant & bit_val;
assign tx_bit = owned ? (|masked) : 1'b1;
// THE INVERSION, in one place -- and gated by reset.
//
// Reset dominance on the PAD, not merely on the logic feeding it, is a hardware
// requirement rather than defensive coding. A master's reset is asynchronous to the
// bus, so a master reset in the middle of a transfer must let go of SDA in the same
// instant; if its output enable survives reset it holds the line low and creates
// exactly the stuck bus of §3.1.16, for which the nine-pulse remedy exists. The
// blocks that drive `req` are reset too, but relying on that makes the pad's
// behaviour depend on every one of them, and a single un-reset request bit
// anywhere upstream takes the bus down.
assign sda_drive_low = (rst_n && owned) ? ~tx_bit : 1'b0;
// ---- arbitration --------------------------------------------------------
// Sent a one, the line reads zero, SCL is high so everybody is sampling -- and we
// were transmitting rather than receiving.
assign arb_loss_now = tx_active && owned && tx_bit && !sda_in && scl_in;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
owner_conflict <= 1'b0;
conflicts <= {CNT_W{1'b0}};
arb_lost <= 1'b0;
arb_losses <= {CNT_W{1'b0}};
end else begin
// More than one request is a design error. Report it and count it; the
// priority resolution above has already kept the bus defined.
owner_conflict <= (req != (req & ~higher));
if ((req != (req & ~higher)) && !owner_conflict)
conflicts <= conflicts + 1'b1;
if (arb_clear) begin
arb_lost <= 1'b0;
end else if (arb_loss_now && !arb_lost) begin
arb_lost <= 1'b1;
arb_losses <= arb_losses + 1'b1;
end
end
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_sda_ctrl.vhd
-- SDA open-drain control and line ownership.
-- Behavioural twin of i2c_sda_ctrl.sv / .v.
--
-- THE SIGN ERROR THIS BLOCK EXISTS TO PREVENT. An I²C output drives LOW or RELEASES; there
-- is no drive-high. So the bit a master transmits is the INVERSE of its drive enable, and
-- the block that converts "the bit I want to send" into "pull the line down" is the single
-- most common place to get a sign wrong. Making it one named block with one named
-- conversion means the inversion appears exactly once.
--
-- tx_bit = '1' -> drive_low = '0' (release; the pull-up makes it high)
-- tx_bit = '0' -> drive_low = '1' (pull it down)
--
-- OWNERSHIP IS A SIGNAL, NOT AN IMPLICATION. §3.1.4: "the transmitter releases the SDA
-- line during the acknowledge clock pulse so the receiver can pull the SDA line LOW". So
-- in every byte, ownership of SDA changes hands exactly once -- at the ninth pulse -- and
-- changes back at the start of the next byte. Four blocks want to drive SDA at different
-- times: the framer, the bit engine, the acknowledge, and recovery. If ownership were
-- implied by the controller's state encoding, adding a state would silently change who
-- owns the line. Making it explicit means a conflict is DETECTABLE, and this block reports
-- one rather than resolving it quietly: two simultaneous owners is a design error, and a
-- design error that resolves itself is a design error that ships.
--
-- ARBITRATION, FOR FREE. §3.1.8: sent a one, the line reads zero, while SCL is high -- and
-- `tx_active`, because a master that released SDA in order to RECEIVE is not arbitrating.
-- The electrical condition is identical in the two cases and only intent separates them.
--
-- AND THE ASYMMETRY NOBODY STATES: a master can only ever LOSE by trying to send a ONE.
-- Sending a zero and reading a zero is indistinguishable from winning.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_sda_ctrl is
generic (
N_OWNER : integer := 4;
CNT_W : integer := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
req : in std_logic_vector(N_OWNER-1 downto 0);
bit_val : in std_logic_vector(N_OWNER-1 downto 0);
sda_in : in std_logic;
scl_in : in std_logic;
tx_active : in std_logic;
sda_drive_low : out std_logic;
grant : out std_logic_vector(N_OWNER-1 downto 0);
owned : out std_logic;
tx_bit : out std_logic;
owner_conflict : out std_logic;
conflicts : out unsigned(CNT_W-1 downto 0);
arb_loss_now : out std_logic;
arb_lost : out std_logic;
arb_losses : out unsigned(CNT_W-1 downto 0);
arb_clear : in std_logic
);
end entity i2c_sda_ctrl;
architecture rtl of i2c_sda_ctrl is
-- Lowest index wins. The framer outranks the bit engine because a START must be able to
-- pre-empt a transfer -- §3.1.10 note 4 requires a device to accept a START anywhere,
-- and a master issuing one must not be fighting its own datapath.
function resolve (r : std_logic_vector) return std_logic_vector is
variable g : std_logic_vector(r'range) := (others => '0');
variable seen : boolean := false;
begin
for i in 0 to r'length-1 loop
if r(i) = '1' and not seen then
g(i) := '1';
seen := true;
end if;
end loop;
return g;
end function;
function any_set (v : std_logic_vector) return std_logic is
variable a : std_logic := '0';
begin
for i in v'range loop
if v(i) = '1' then a := '1'; end if;
end loop;
return a;
end function;
signal grant_i : std_logic_vector(N_OWNER-1 downto 0);
signal owned_i : std_logic;
signal txbit_i : std_logic;
signal conf_now : std_logic;
signal n_conf : unsigned(CNT_W-1 downto 0) := (others => '0');
signal n_arb : unsigned(CNT_W-1 downto 0) := (others => '0');
signal lost_i : std_logic := '0';
signal conf_q : std_logic := '0';
signal loss_now : std_logic;
begin
grant_i <= resolve(req);
owned_i <= any_set(req);
-- The bit the granted owner wants. With nobody granted it is '1', because a released
-- line reads high and a master that owns nothing is transmitting nothing.
txbit_i <= any_set(grant_i and bit_val) when owned_i = '1' else '1';
grant <= grant_i;
owned <= owned_i;
tx_bit <= txbit_i;
-- THE INVERSION, in one place -- and gated by reset.
--
-- Reset dominance on the PAD, not merely on the logic feeding it, is a hardware
-- requirement rather than defensive coding. A master's reset is asynchronous to the bus,
-- so a master reset in the middle of a transfer must let go of SDA in the same instant;
-- if its output enable survives reset it holds the line low and creates exactly the
-- stuck bus of §3.1.16. The blocks driving `req` are reset too, but relying on that
-- makes the pad's behaviour depend on every one of them.
sda_drive_low <= (not txbit_i) when (rst_n = '1' and owned_i = '1') else '0';
-- Sent a one, the line reads zero, SCL is high so everybody is sampling -- and we were
-- transmitting rather than receiving.
loss_now <= '1' when (tx_active = '1' and owned_i = '1' and txbit_i = '1'
and sda_in = '0' and scl_in = '1') else '0';
arb_loss_now <= loss_now;
conf_now <= '1' when req /= grant_i else '0';
owner_conflict <= conf_q;
conflicts <= n_conf;
arb_lost <= lost_i;
arb_losses <= n_arb;
process (clk, rst_n)
begin
if rst_n = '0' then
conf_q <= '0';
n_conf <= (others => '0');
lost_i <= '0';
n_arb <= (others => '0');
elsif rising_edge(clk) then
-- More than one request is a design error. Report it and count it; the priority
-- resolution above has already kept the bus defined.
conf_q <= conf_now;
if conf_now = '1' and conf_q = '0' then
n_conf <= n_conf + 1;
end if;
if arb_clear = '1' then
lost_i <= '0';
elsif loss_now = '1' and lost_i = '0' then
lost_i <= '1';
n_arb <= n_arb + 1;
end if;
end if;
end process;
end architecture rtl;5a. The testbenches
Twelve tests, and note which of them assert inabilities rather than behaviours.
| # | Test | Property |
|---|---|---|
| T1 | the inversion | a one releases, a zero pulls low |
| T2 | owning nothing drives nothing | and tx_bit reads as one, because releasing is a one |
| T3 | a released line is not a high line | the DUT releases, the bench pulls low, the DUT sees low |
| T4 | ownership priority | the framer outranks the bit engine |
| T5 | a conflict is reported, not swallowed | two owners at once raises the flag and counts |
| T6 | arbitration loss | sent a one, line reads zero, SCL high |
| T7 | the asymmetry | a master can only lose by sending a one |
| T8 | arbitration is checked only while SCL is high | §3.1.8's "during every bit, while SCL is HIGH" |
| T9 | the driver is turned off "the moment there is a difference" | §3.1.8 obligation 2 |
| T10 | the latch is sticky until cleared | and counts once per loss |
| T11 | every owner drives through the same inversion | four owners, one sign |
| T12 | reset releases the line | a master out of reset holding SDA low is a dead bus |
T3 and T12 are the two worth pausing on.
T3 asserts a distinction, not a value. The DUT releases SDA; the bench, acting as another device, pulls it low; and the test asserts that the DUT's readback reports low while its own drive intent is release. That is drive-intent-versus-observed-bus made executable, and a design that conflates them passes every other test in the suite.
T12 asserts that reset dominates the pad. A master coming out of reset while holding SDA low takes the entire bus down and nothing else can lift it — the wired-AND consequence from Chapter 17.1 §4. That is why the drive expression is gated on rst_n rather than relying on the owners being reset, and mutation M2 removes exactly that gate.
`timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_sda_ctrl_tb.sv
// Independent oracle for i2c_sda_ctrl.
//
// The block is instantiated on a real wired-AND bus with a SECOND device, because
// three of its properties are about what the LINE does rather than about what the
// block outputs: the inversion, arbitration loss, and the fact that a released line
// only reads high if everybody released it.
//
// Device 0 is the DUT. Device 1 is the bench, which can pull either line down and so
// can produce arbitration loss the way a competing master does -- by transmitting a
// zero at the same instant.
// -----------------------------------------------------------------------------
module i2c_sda_ctrl_tb;
localparam integer NO = 4; // owners: 0 framer, 1 bit engine, 2 ack, 3 recovery
localparam integer OWN_FRAMER = 0, OWN_BIT = 1, OWN_ACK = 2, OWN_RECOV = 3;
logic clk = 1'b0;
logic rst_n = 1'b0;
logic [NO-1:0] req = {NO{1'b0}};
logic [NO-1:0] bit_val = {NO{1'b0}};
logic arb_clear = 1'b0;
logic tx_active = 1'b1;
// the bench, as the second device
logic other_sda_low = 1'b0;
logic other_scl_low = 1'b0;
logic dut_sda_low;
logic scl, sda;
logic [1:0] scl_in, sda_in, scl_rbl, sda_rbl;
logic [7:0] scl_holders, sda_holders;
logic [NO-1:0] grant;
logic owned, tx_bit, arb_loss_now;
logic owner_conflict, arb_lost;
logic [15:0] conflicts, arb_losses;
i2c_line_model #(.N_DEV(2)) bus (
.scl_drive_low({other_scl_low, 1'b0}),
.sda_drive_low({other_sda_low, dut_sda_low}),
.scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
.scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
.scl_holders(scl_holders), .sda_holders(sda_holders));
i2c_sda_ctrl #(.N_OWNER(NO), .CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.req(req), .bit_val(bit_val),
.sda_in(sda_in[0]), .scl_in(scl_in[0]), .tx_active(tx_active),
.sda_drive_low(dut_sda_low),
.grant(grant), .owned(owned), .tx_bit(tx_bit),
.owner_conflict(owner_conflict), .conflicts(conflicts),
.arb_loss_now(arb_loss_now), .arb_lost(arb_lost),
.arb_losses(arb_losses), .arb_clear(arb_clear));
always #5 clk = ~clk;
integer errors = 0;
integer k;
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk);
rst_n = 1'b0; req = {NO{1'b0}}; bit_val = {NO{1'b0}};
arb_clear = 1'b0; tx_active = 1'b1;
other_sda_low = 1'b0; other_scl_low = 1'b0;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
step;
end
endtask
// Let owner `o` transmit bit `b`, and settle combinationally.
task own (input integer o, input b);
begin
@(negedge clk);
req = {NO{1'b0}}; req[o] = 1'b1;
bit_val = {NO{1'b0}}; bit_val[o] = b;
#1;
end
endtask
task release_all; begin @(negedge clk); req = {NO{1'b0}}; #1; end endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d expected %0d", what, g, e);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_sda_ctrl: drive low, release high, and one inversion ===");
// ----------------------------------------------------------------
// T1. THE INVERSION. Transmitting a one RELEASES the line; transmitting a zero
// pulls it down. This is the sign that a first I²C master gets wrong, and
// it is worth asserting on the LINE rather than on the drive enable.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1);
$display("T1 transmitting a one releases the line; a zero pulls it down");
ck_bit("T1 a one means do not drive", dut_sda_low, 1'b0);
ck_bit("T1 and the line reads high", sda, 1'b1);
own(OWN_BIT, 1'b0);
ck_bit("T1 a zero means drive low", dut_sda_low, 1'b1);
ck_bit("T1 and the line reads low", sda, 1'b0);
ck_int("T1 exactly one device is holding SDA", sda_holders, 1);
// ----------------------------------------------------------------
// T2. Owning nothing drives nothing, and `tx_bit` reads as a one -- because a
// released line reads high and a master that owns nothing is transmitting
// nothing rather than transmitting a zero.
// ----------------------------------------------------------------
release_all;
$display("T2 owning nothing drives nothing");
ck_bit("T2 not owned", owned, 1'b0);
ck_bit("T2 nothing driven", dut_sda_low, 1'b0);
ck_bit("T2 the line is high", sda, 1'b1);
ck_bit("T2 tx_bit reads as a one", tx_bit, 1'b1);
ck_int("T2 no grant", grant, 0);
// ----------------------------------------------------------------
// T3. A RELEASED LINE IS NOT A HIGH LINE. The DUT releases, the bench pulls
// down, and the line is low -- which is the whole reason every block in
// this module reads the line back instead of trusting its own output.
// ----------------------------------------------------------------
release_all;
@(negedge clk); other_sda_low = 1'b1; #1;
$display("T3 a released line is not the same thing as a high line");
ck_bit("T3 the DUT is driving nothing", dut_sda_low, 1'b0);
ck_bit("T3 and the line is still low", sda, 1'b0);
ck_bit("T3 which the DUT can see", sda_in[0], 1'b0);
@(negedge clk); other_sda_low = 1'b0; #1;
ck_bit("T3 released by both, the pull-up wins", sda, 1'b1);
// ----------------------------------------------------------------
// T4. OWNERSHIP PRIORITY. The framer outranks the bit engine, because a START
// must be able to pre-empt a transfer in progress.
// ----------------------------------------------------------------
do_reset;
@(negedge clk);
req = 4'b0000; req[OWN_FRAMER] = 1'b1; req[OWN_BIT] = 1'b1;
bit_val = 4'b0000; bit_val[OWN_FRAMER] = 1'b0; bit_val[OWN_BIT] = 1'b1;
#1;
$display("T4 the framer outranks the bit engine");
ck_int("T4 the framer is granted", grant, 1 << OWN_FRAMER);
ck_bit("T4 and its bit is the one transmitted", tx_bit, 1'b0);
ck_bit("T4 so the line goes low", sda, 1'b0);
// ----------------------------------------------------------------
// T5. AND A CONFLICT IS REPORTED, NOT SWALLOWED. Two owners requesting at once
// is a design error. The priority resolution keeps the bus defined, and the
// flag says the bug happened -- because a design error that resolves itself
// quietly is a design error that ships.
// ----------------------------------------------------------------
step;
$display("T5 two simultaneous owners is reported as a bug, not resolved quietly");
ck_bit("T5 the conflict is flagged", owner_conflict, 1'b1);
ck_int("T5 and counted", conflicts, 1);
release_all; step;
ck_bit("T5 and the flag clears when the conflict does", owner_conflict, 1'b0);
// ----------------------------------------------------------------
// T6. ARBITRATION LOSS. §3.1.8: sent a one, the line reads zero, while SCL is
// high. The bench produces it the way a competing master does -- by
// transmitting a zero in the same bit slot.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1); // we send a one
@(negedge clk); other_sda_low = 1'b1; // the competitor sends a zero
#1;
$display("T6 sent a one, read a zero, while SCL is high: arbitration lost");
ck_bit("T6 SCL is high", scl, 1'b1);
ck_bit("T6 we intended a one", tx_bit, 1'b1);
ck_bit("T6 the line reads zero", sda_in[0], 1'b0);
ck_bit("T6 loss is detected this cycle", arb_loss_now, 1'b1);
step;
ck_bit("T6 and latched", arb_lost, 1'b1);
ck_int("T6 and counted once", arb_losses, 1);
// ----------------------------------------------------------------
// T7. THE ASYMMETRY. A master can only lose by sending a ONE. Sending a zero
// and reading a zero is indistinguishable from winning, so a master
// transmitting zeros cannot see a competitor at all.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b0); // we send a zero
@(negedge clk); other_sda_low = 1'b1; // so does the competitor
#1;
$display("T7 a master transmitting a zero cannot detect a competitor");
ck_bit("T7 the line is low, as we intended", sda_in[0], 1'b0);
ck_bit("T7 no loss is detected", arb_loss_now, 1'b0);
step;
ck_bit("T7 and none is latched", arb_lost, 1'b0);
ck_int("T7 no losses counted", arb_losses, 0);
// ----------------------------------------------------------------
// T7b. AND ONLY WHILE TRANSMITTING. A receiving master has released SDA on
// purpose, so "sent a one, read a zero" is true of every bit of every read.
// Without the tx_active gate the block would declare arbitration lost on
// the first zero any target ever sent back.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1); // released, i.e. transmitting a one
@(negedge clk); tx_active = 1'b0; // but we are RECEIVING
@(negedge clk); other_sda_low = 1'b1; // and the target answers with a zero
#1;
$display("T7b a receiving master reading a zero has not lost anything");
ck_bit("T7b the electrical condition is present", sda_in[0], 1'b0);
ck_bit("T7b but no loss is declared", arb_loss_now, 1'b0);
step;
ck_bit("T7b and none is latched", arb_lost, 1'b0);
// The same instant, with tx_active high, IS a loss.
@(negedge clk); tx_active = 1'b1; #1;
ck_bit("T7b the identical bus state while transmitting IS a loss",
arb_loss_now, 1'b1);
// ----------------------------------------------------------------
// T8. ARBITRATION IS CHECKED ONLY WHILE SCL IS HIGH. §3.1.8: "During every
// bit, while SCL is HIGH, each master checks to see if the SDA level
// matches what it has sent." While SCL is low SDA is allowed to be
// changing, so a mismatch there means nothing.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1);
@(negedge clk); other_sda_low = 1'b1; other_scl_low = 1'b1; // SCL held low
#1;
$display("T8 a mismatch while SCL is low is not arbitration loss");
ck_bit("T8 SCL is low", scl, 1'b0);
ck_bit("T8 the mismatch exists", sda_in[0], 1'b0);
ck_bit("T8 but no loss is declared", arb_loss_now, 1'b0);
step;
ck_bit("T8 and none is latched", arb_lost, 1'b0);
// Release SCL and the very same mismatch becomes a loss.
@(negedge clk); other_scl_low = 1'b0; #1;
ck_bit("T8 the same mismatch with SCL high IS a loss", arb_loss_now, 1'b1);
// ----------------------------------------------------------------
// T9. Obligation 2 of §3.1.8: the driver is turned off "the moment there is a
// difference". The controller does that by dropping its request, and the
// block must then be driving nothing at all.
// ----------------------------------------------------------------
step;
release_all;
$display("T9 the loser turns its driver off immediately");
ck_bit("T9 driving nothing", dut_sda_low, 1'b0);
ck_bit("T9 no longer owned", owned, 1'b0);
ck_bit("T9 but the loss is still latched", arb_lost, 1'b1);
// ----------------------------------------------------------------
// T10. The latch is sticky until the controller clears it, and counts once per
// loss rather than once per cycle of the mismatch.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1);
@(negedge clk); other_sda_low = 1'b1; #1;
for (k = 0; k < 10; k = k + 1) step; // the mismatch persists for ten cycles
$display("T10 a loss is counted once, not once per cycle");
ck_bit("T10 still latched", arb_lost, 1'b1);
ck_int("T10 counted exactly once", arb_losses, 1);
@(negedge clk); arb_clear = 1'b1;
step;
@(negedge clk); arb_clear = 1'b0; #1;
ck_bit("T10 cleared on request", arb_lost, 1'b0);
// ----------------------------------------------------------------
// T11. Every owner drives through the same inversion. Four owners, one sign.
// ----------------------------------------------------------------
do_reset;
$display("T11 all four owners drive through the same single inversion");
for (k = 0; k < NO; k = k + 1) begin
own(k, 1'b0);
ck_bit("T11 a zero from this owner pulls the line low", sda, 1'b0);
ck_int("T11 and that owner is granted", grant, 1 << k);
own(k, 1'b1);
ck_bit("T11 a one from this owner releases it", sda, 1'b1);
end
// ----------------------------------------------------------------
// T12. Reset releases the line. A master coming out of reset holding SDA low
// is Chapter 15.4's stuck bus, created by the master itself.
// ----------------------------------------------------------------
@(negedge clk); rst_n = 1'b0; req = {NO{1'b1}}; bit_val = {NO{1'b0}}; #1;
$display("T12 reset releases SDA even with every owner asking for a zero");
ck_bit("T12 nothing driven", dut_sda_low, 1'b0);
ck_bit("T12 the line is high", sda, 1'b1);
step;
ck_bit("T12 no conflict latched through reset", owner_conflict, 1'b0);
ck_bit("T12 no arbitration loss latched through reset", arb_lost, 1'b0);
if (errors == 0)
$display("=== i2c_sda_ctrl: ALL CHECKS PASSED ===");
else
$display("=== i2c_sda_ctrl: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_sda_ctrl_tb.sv
// Independent oracle for i2c_sda_ctrl.
//
// The block is instantiated on a real wired-AND bus with a SECOND device, because
// three of its properties are about what the LINE does rather than about what the
// block outputs: the inversion, arbitration loss, and the fact that a released line
// only reads high if everybody released it.
//
// Device 0 is the DUT. Device 1 is the bench, which can pull either line down and so
// can produce arbitration loss the way a competing master does -- by transmitting a
// zero at the same instant.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_sda_ctrl_tb;
localparam integer NO = 4; // owners: 0 framer, 1 bit engine, 2 ack, 3 recovery
localparam integer OWN_FRAMER = 0, OWN_BIT = 1, OWN_ACK = 2, OWN_RECOV = 3;
reg clk = 1'b0;
reg rst_n = 1'b0;
reg [NO-1:0] req = {NO{1'b0}};
reg [NO-1:0] bit_val = {NO{1'b0}};
reg arb_clear = 1'b0;
reg tx_active = 1'b1;
// the bench, as the second device
reg other_sda_low = 1'b0;
reg other_scl_low = 1'b0;
wire dut_sda_low;
wire scl, sda;
wire [1:0] scl_in, sda_in, scl_rbl, sda_rbl;
wire [7:0] scl_holders, sda_holders;
wire [NO-1:0] grant;
wire owned, tx_bit, arb_loss_now;
wire owner_conflict, arb_lost;
wire [15:0] conflicts, arb_losses;
i2c_line_model #(.N_DEV(2)) bus (
.scl_drive_low({other_scl_low, 1'b0}),
.sda_drive_low({other_sda_low, dut_sda_low}),
.scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
.scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
.scl_holders(scl_holders), .sda_holders(sda_holders));
i2c_sda_ctrl #(.N_OWNER(NO), .CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.req(req), .bit_val(bit_val),
.sda_in(sda_in[0]), .scl_in(scl_in[0]), .tx_active(tx_active),
.sda_drive_low(dut_sda_low),
.grant(grant), .owned(owned), .tx_bit(tx_bit),
.owner_conflict(owner_conflict), .conflicts(conflicts),
.arb_loss_now(arb_loss_now), .arb_lost(arb_lost),
.arb_losses(arb_losses), .arb_clear(arb_clear));
always #5 clk = ~clk;
integer errors = 0;
integer k;
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk);
rst_n = 1'b0; req = {NO{1'b0}}; bit_val = {NO{1'b0}};
arb_clear = 1'b0; tx_active = 1'b1;
other_sda_low = 1'b0; other_scl_low = 1'b0;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
step;
end
endtask
// Let owner `o` transmit bit `b`, and settle combinationally.
task own (input integer o, input b);
begin
@(negedge clk);
req = {NO{1'b0}}; req[o] = 1'b1;
bit_val = {NO{1'b0}}; bit_val[o] = b;
#1;
end
endtask
task release_all; begin @(negedge clk); req = {NO{1'b0}}; #1; end endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d expected %0d", what, g, e);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_sda_ctrl: drive low, release high, and one inversion ===");
// ----------------------------------------------------------------
// T1. THE INVERSION. Transmitting a one RELEASES the line; transmitting a zero
// pulls it down. This is the sign that a first I²C master gets wrong, and
// it is worth asserting on the LINE rather than on the drive enable.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1);
$display("T1 transmitting a one releases the line; a zero pulls it down");
ck_bit("T1 a one means do not drive", dut_sda_low, 1'b0);
ck_bit("T1 and the line reads high", sda, 1'b1);
own(OWN_BIT, 1'b0);
ck_bit("T1 a zero means drive low", dut_sda_low, 1'b1);
ck_bit("T1 and the line reads low", sda, 1'b0);
ck_int("T1 exactly one device is holding SDA", sda_holders, 1);
// ----------------------------------------------------------------
// T2. Owning nothing drives nothing, and `tx_bit` reads as a one -- because a
// released line reads high and a master that owns nothing is transmitting
// nothing rather than transmitting a zero.
// ----------------------------------------------------------------
release_all;
$display("T2 owning nothing drives nothing");
ck_bit("T2 not owned", owned, 1'b0);
ck_bit("T2 nothing driven", dut_sda_low, 1'b0);
ck_bit("T2 the line is high", sda, 1'b1);
ck_bit("T2 tx_bit reads as a one", tx_bit, 1'b1);
ck_int("T2 no grant", grant, 0);
// ----------------------------------------------------------------
// T3. A RELEASED LINE IS NOT A HIGH LINE. The DUT releases, the bench pulls
// down, and the line is low -- which is the whole reason every block in
// this module reads the line back instead of trusting its own output.
// ----------------------------------------------------------------
release_all;
@(negedge clk); other_sda_low = 1'b1; #1;
$display("T3 a released line is not the same thing as a high line");
ck_bit("T3 the DUT is driving nothing", dut_sda_low, 1'b0);
ck_bit("T3 and the line is still low", sda, 1'b0);
ck_bit("T3 which the DUT can see", sda_in[0], 1'b0);
@(negedge clk); other_sda_low = 1'b0; #1;
ck_bit("T3 released by both, the pull-up wins", sda, 1'b1);
// ----------------------------------------------------------------
// T4. OWNERSHIP PRIORITY. The framer outranks the bit engine, because a START
// must be able to pre-empt a transfer in progress.
// ----------------------------------------------------------------
do_reset;
@(negedge clk);
req = 4'b0000; req[OWN_FRAMER] = 1'b1; req[OWN_BIT] = 1'b1;
bit_val = 4'b0000; bit_val[OWN_FRAMER] = 1'b0; bit_val[OWN_BIT] = 1'b1;
#1;
$display("T4 the framer outranks the bit engine");
ck_int("T4 the framer is granted", grant, 1 << OWN_FRAMER);
ck_bit("T4 and its bit is the one transmitted", tx_bit, 1'b0);
ck_bit("T4 so the line goes low", sda, 1'b0);
// ----------------------------------------------------------------
// T5. AND A CONFLICT IS REPORTED, NOT SWALLOWED. Two owners requesting at once
// is a design error. The priority resolution keeps the bus defined, and the
// flag says the bug happened -- because a design error that resolves itself
// quietly is a design error that ships.
// ----------------------------------------------------------------
step;
$display("T5 two simultaneous owners is reported as a bug, not resolved quietly");
ck_bit("T5 the conflict is flagged", owner_conflict, 1'b1);
ck_int("T5 and counted", conflicts, 1);
release_all; step;
ck_bit("T5 and the flag clears when the conflict does", owner_conflict, 1'b0);
// ----------------------------------------------------------------
// T6. ARBITRATION LOSS. §3.1.8: sent a one, the line reads zero, while SCL is
// high. The bench produces it the way a competing master does -- by
// transmitting a zero in the same bit slot.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1); // we send a one
@(negedge clk); other_sda_low = 1'b1; // the competitor sends a zero
#1;
$display("T6 sent a one, read a zero, while SCL is high: arbitration lost");
ck_bit("T6 SCL is high", scl, 1'b1);
ck_bit("T6 we intended a one", tx_bit, 1'b1);
ck_bit("T6 the line reads zero", sda_in[0], 1'b0);
ck_bit("T6 loss is detected this cycle", arb_loss_now, 1'b1);
step;
ck_bit("T6 and latched", arb_lost, 1'b1);
ck_int("T6 and counted once", arb_losses, 1);
// ----------------------------------------------------------------
// T7. THE ASYMMETRY. A master can only lose by sending a ONE. Sending a zero
// and reading a zero is indistinguishable from winning, so a master
// transmitting zeros cannot see a competitor at all.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b0); // we send a zero
@(negedge clk); other_sda_low = 1'b1; // so does the competitor
#1;
$display("T7 a master transmitting a zero cannot detect a competitor");
ck_bit("T7 the line is low, as we intended", sda_in[0], 1'b0);
ck_bit("T7 no loss is detected", arb_loss_now, 1'b0);
step;
ck_bit("T7 and none is latched", arb_lost, 1'b0);
ck_int("T7 no losses counted", arb_losses, 0);
// ----------------------------------------------------------------
// T7b. AND ONLY WHILE TRANSMITTING. A receiving master has released SDA on
// purpose, so "sent a one, read a zero" is true of every bit of every read.
// Without the tx_active gate the block would declare arbitration lost on
// the first zero any target ever sent back.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1); // released, i.e. transmitting a one
@(negedge clk); tx_active = 1'b0; // but we are RECEIVING
@(negedge clk); other_sda_low = 1'b1; // and the target answers with a zero
#1;
$display("T7b a receiving master reading a zero has not lost anything");
ck_bit("T7b the electrical condition is present", sda_in[0], 1'b0);
ck_bit("T7b but no loss is declared", arb_loss_now, 1'b0);
step;
ck_bit("T7b and none is latched", arb_lost, 1'b0);
// The same instant, with tx_active high, IS a loss.
@(negedge clk); tx_active = 1'b1; #1;
ck_bit("T7b the identical bus state while transmitting IS a loss",
arb_loss_now, 1'b1);
// ----------------------------------------------------------------
// T8. ARBITRATION IS CHECKED ONLY WHILE SCL IS HIGH. §3.1.8: "During every
// bit, while SCL is HIGH, each master checks to see if the SDA level
// matches what it has sent." While SCL is low SDA is allowed to be
// changing, so a mismatch there means nothing.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1);
@(negedge clk); other_sda_low = 1'b1; other_scl_low = 1'b1; // SCL held low
#1;
$display("T8 a mismatch while SCL is low is not arbitration loss");
ck_bit("T8 SCL is low", scl, 1'b0);
ck_bit("T8 the mismatch exists", sda_in[0], 1'b0);
ck_bit("T8 but no loss is declared", arb_loss_now, 1'b0);
step;
ck_bit("T8 and none is latched", arb_lost, 1'b0);
// Release SCL and the very same mismatch becomes a loss.
@(negedge clk); other_scl_low = 1'b0; #1;
ck_bit("T8 the same mismatch with SCL high IS a loss", arb_loss_now, 1'b1);
// ----------------------------------------------------------------
// T9. Obligation 2 of §3.1.8: the driver is turned off "the moment there is a
// difference". The controller does that by dropping its request, and the
// block must then be driving nothing at all.
// ----------------------------------------------------------------
step;
release_all;
$display("T9 the loser turns its driver off immediately");
ck_bit("T9 driving nothing", dut_sda_low, 1'b0);
ck_bit("T9 no longer owned", owned, 1'b0);
ck_bit("T9 but the loss is still latched", arb_lost, 1'b1);
// ----------------------------------------------------------------
// T10. The latch is sticky until the controller clears it, and counts once per
// loss rather than once per cycle of the mismatch.
// ----------------------------------------------------------------
do_reset;
own(OWN_BIT, 1'b1);
@(negedge clk); other_sda_low = 1'b1; #1;
for (k = 0; k < 10; k = k + 1) step; // the mismatch persists for ten cycles
$display("T10 a loss is counted once, not once per cycle");
ck_bit("T10 still latched", arb_lost, 1'b1);
ck_int("T10 counted exactly once", arb_losses, 1);
@(negedge clk); arb_clear = 1'b1;
step;
@(negedge clk); arb_clear = 1'b0; #1;
ck_bit("T10 cleared on request", arb_lost, 1'b0);
// ----------------------------------------------------------------
// T11. Every owner drives through the same inversion. Four owners, one sign.
// ----------------------------------------------------------------
do_reset;
$display("T11 all four owners drive through the same single inversion");
for (k = 0; k < NO; k = k + 1) begin
own(k, 1'b0);
ck_bit("T11 a zero from this owner pulls the line low", sda, 1'b0);
ck_int("T11 and that owner is granted", grant, 1 << k);
own(k, 1'b1);
ck_bit("T11 a one from this owner releases it", sda, 1'b1);
end
// ----------------------------------------------------------------
// T12. Reset releases the line. A master coming out of reset holding SDA low
// is Chapter 15.4's stuck bus, created by the master itself.
// ----------------------------------------------------------------
@(negedge clk); rst_n = 1'b0; req = {NO{1'b1}}; bit_val = {NO{1'b0}}; #1;
$display("T12 reset releases SDA even with every owner asking for a zero");
ck_bit("T12 nothing driven", dut_sda_low, 1'b0);
ck_bit("T12 the line is high", sda, 1'b1);
step;
ck_bit("T12 no conflict latched through reset", owner_conflict, 1'b0);
ck_bit("T12 no arbitration loss latched through reset", arb_lost, 1'b0);
if (errors == 0)
$display("=== i2c_sda_ctrl: ALL CHECKS PASSED ===");
else
$display("=== i2c_sda_ctrl: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_sda_ctrl_tb.vhd
-- Independent oracle for i2c_sda_ctrl. Behavioural twin of the SV and Verilog benches.
--
-- The block is instantiated on a real wired-AND bus with a SECOND device, because three of
-- its properties are about what the LINE does rather than what the block outputs: the
-- inversion, arbitration loss, and the fact that a released line only reads high if
-- everybody released it.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_sda_ctrl_tb is
end entity i2c_sda_ctrl_tb;
architecture sim of i2c_sda_ctrl_tb is
constant TCLK : time := 10 ns;
constant NO : integer := 4;
constant OWN_FRAMER : integer := 0;
constant OWN_BIT : integer := 1;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal req : std_logic_vector(NO-1 downto 0) := (others => '0');
signal bit_val : std_logic_vector(NO-1 downto 0) := (others => '0');
signal arb_clear : std_logic := '0';
signal tx_active : std_logic := '1';
signal other_sda_low : std_logic := '0';
signal other_scl_low : std_logic := '0';
signal dut_sda_low : std_logic;
signal scl_drv, sda_drv : std_logic_vector(1 downto 0);
signal scl, sda : std_logic;
signal scl_in, sda_in, scl_rbl, sda_rbl : std_logic_vector(1 downto 0);
signal scl_h, sda_h : unsigned(7 downto 0);
signal grant : std_logic_vector(NO-1 downto 0);
signal owned, tx_bit, arb_loss_now, owner_conflict, arb_lost : std_logic;
signal conflicts, arb_losses : unsigned(15 downto 0);
signal halt : boolean := false;
begin
scl_drv <= other_scl_low & '0';
sda_drv <= other_sda_low & dut_sda_low;
bus_m : entity work.i2c_line_model
generic map (N_DEV => 2)
port map (scl_drive_low => scl_drv, sda_drive_low => sda_drv,
scl => scl, sda => sda, scl_in => scl_in, sda_in => sda_in,
scl_released_but_low => scl_rbl, sda_released_but_low => sda_rbl,
scl_holders => scl_h, sda_holders => sda_h);
dut : entity work.i2c_sda_ctrl
generic map (N_OWNER => NO, CNT_W => 16)
port map (clk => clk, rst_n => rst_n, req => req, bit_val => bit_val,
sda_in => sda_in(0), scl_in => scl_in(0), tx_active => tx_active,
sda_drive_low => dut_sda_low,
grant => grant, owned => owned, tx_bit => tx_bit,
owner_conflict => owner_conflict, conflicts => conflicts,
arb_loss_now => arb_loss_now, arb_lost => arb_lost,
arb_losses => arb_losses, arb_clear => arb_clear);
clkgen : process
begin
while not halt loop
clk <= '0'; wait for TCLK/2;
clk <= '1'; wait for TCLK/2;
end loop;
wait;
end process;
stim : process
variable err : integer := 0;
variable onehot : std_logic_vector(NO-1 downto 0);
procedure ck_int (what : string; g : integer; e : integer) is
begin
if g /= e then
report " FAIL " & what & ": got " & integer'image(g)
& " expected " & integer'image(e) severity note;
err := err + 1;
end if;
end procedure;
procedure ck_bit (what : string; g : std_logic; e : std_logic) is
begin
if g /= e then
report " FAIL " & what & ": got " & std_logic'image(g)
& " expected " & std_logic'image(e) severity note;
err := err + 1;
end if;
end procedure;
procedure step is
begin
wait until rising_edge(clk); wait until falling_edge(clk);
end procedure;
procedure do_reset is
begin
wait until falling_edge(clk);
rst_n <= '0'; req <= (others => '0'); bit_val <= (others => '0');
arb_clear <= '0'; tx_active <= '1';
other_sda_low <= '0'; other_scl_low <= '0';
for i in 0 to 2 loop wait until rising_edge(clk); end loop;
wait until falling_edge(clk); rst_n <= '1';
step;
end procedure;
-- Let owner `o` transmit bit `b`, and settle combinationally.
procedure own (o : integer; b : std_logic) is
variable r, v : std_logic_vector(NO-1 downto 0);
begin
wait until falling_edge(clk);
r := (others => '0'); r(o) := '1';
v := (others => '0'); v(o) := b;
req <= r; bit_val <= v;
wait for 1 ns;
end procedure;
procedure release_all is
begin
wait until falling_edge(clk);
req <= (others => '0');
wait for 1 ns;
end procedure;
begin
report "=== i2c_sda_ctrl: drive low, release high, and one inversion ==="
severity note;
-- T1. THE INVERSION. Transmitting a one RELEASES the line; a zero pulls it down.
-- Asserted on the LINE rather than on the drive enable.
do_reset;
own(OWN_BIT, '1');
report "T1 transmitting a one releases the line; a zero pulls it down"
severity note;
ck_bit("T1 a one means do not drive", dut_sda_low, '0');
ck_bit("T1 and the line reads high", sda, '1');
own(OWN_BIT, '0');
ck_bit("T1 a zero means drive low", dut_sda_low, '1');
ck_bit("T1 and the line reads low", sda, '0');
ck_int("T1 exactly one device is holding SDA", to_integer(sda_h), 1);
-- T2. Owning nothing drives nothing, and `tx_bit` reads as a one -- because a
-- released line reads high and a master that owns nothing is transmitting
-- nothing rather than transmitting a zero.
release_all;
report "T2 owning nothing drives nothing" severity note;
ck_bit("T2 not owned", owned, '0');
ck_bit("T2 nothing driven", dut_sda_low, '0');
ck_bit("T2 the line is high", sda, '1');
ck_bit("T2 tx_bit reads as a one", tx_bit, '1');
ck_int("T2 no grant", to_integer(unsigned(grant)), 0);
-- T3. A RELEASED LINE IS NOT A HIGH LINE, which is the whole reason every block in
-- this module reads the line back instead of trusting its own output.
release_all;
wait until falling_edge(clk); other_sda_low <= '1'; wait for 1 ns;
report "T3 a released line is not the same thing as a high line" severity note;
ck_bit("T3 the DUT is driving nothing", dut_sda_low, '0');
ck_bit("T3 and the line is still low", sda, '0');
ck_bit("T3 which the DUT can see", sda_in(0), '0');
wait until falling_edge(clk); other_sda_low <= '0'; wait for 1 ns;
ck_bit("T3 released by both, the pull-up wins", sda, '1');
-- T4. OWNERSHIP PRIORITY. The framer outranks the bit engine, because a START must
-- be able to pre-empt a transfer in progress.
do_reset;
wait until falling_edge(clk);
req <= (OWN_FRAMER => '1', OWN_BIT => '1', others => '0');
bit_val <= (OWN_FRAMER => '0', OWN_BIT => '1', others => '0');
wait for 1 ns;
report "T4 the framer outranks the bit engine" severity note;
ck_int("T4 the framer is granted", to_integer(unsigned(grant)), 2**OWN_FRAMER);
ck_bit("T4 and its bit is the one transmitted", tx_bit, '0');
ck_bit("T4 so the line goes low", sda, '0');
-- T5. AND A CONFLICT IS REPORTED, NOT SWALLOWED. A design error that resolves
-- itself quietly is a design error that ships.
step;
report "T5 two simultaneous owners is reported as a bug, not resolved quietly"
severity note;
ck_bit("T5 the conflict is flagged", owner_conflict, '1');
ck_int("T5 and counted", to_integer(conflicts), 1);
release_all; step;
ck_bit("T5 and the flag clears when the conflict does", owner_conflict, '0');
-- T6. ARBITRATION LOSS. §3.1.8: sent a one, the line reads zero, SCL is high. The
-- bench produces it the way a competing master does.
do_reset;
own(OWN_BIT, '1');
wait until falling_edge(clk); other_sda_low <= '1';
wait for 1 ns;
report "T6 sent a one, read a zero, while SCL is high: arbitration lost"
severity note;
ck_bit("T6 SCL is high", scl, '1');
ck_bit("T6 we intended a one", tx_bit, '1');
ck_bit("T6 the line reads zero", sda_in(0), '0');
ck_bit("T6 loss is detected this cycle", arb_loss_now, '1');
step;
ck_bit("T6 and latched", arb_lost, '1');
ck_int("T6 and counted once", to_integer(arb_losses), 1);
-- T7. THE ASYMMETRY. A master can only lose by sending a ONE.
do_reset;
own(OWN_BIT, '0');
wait until falling_edge(clk); other_sda_low <= '1';
wait for 1 ns;
report "T7 a master transmitting a zero cannot detect a competitor" severity note;
ck_bit("T7 the line is low, as we intended", sda_in(0), '0');
ck_bit("T7 no loss is detected", arb_loss_now, '0');
step;
ck_bit("T7 and none is latched", arb_lost, '0');
ck_int("T7 no losses counted", to_integer(arb_losses), 0);
-- T7b. AND ONLY WHILE TRANSMITTING. A receiving master has released SDA on purpose,
-- so "sent a one, read a zero" is true of every bit of every read. Without the
-- gate the block would declare arbitration lost on the first zero any target
-- ever sent back.
do_reset;
own(OWN_BIT, '1');
wait until falling_edge(clk); tx_active <= '0';
wait until falling_edge(clk); other_sda_low <= '1';
wait for 1 ns;
report "T7b a receiving master reading a zero has not lost anything" severity note;
ck_bit("T7b the electrical condition is present", sda_in(0), '0');
ck_bit("T7b but no loss is declared", arb_loss_now, '0');
step;
ck_bit("T7b and none is latched", arb_lost, '0');
wait until falling_edge(clk); tx_active <= '1'; wait for 1 ns;
ck_bit("T7b the identical bus state while transmitting IS a loss",
arb_loss_now, '1');
-- T8. ARBITRATION IS CHECKED ONLY WHILE SCL IS HIGH. §3.1.8: "During every bit,
-- while SCL is HIGH". While SCL is low SDA is allowed to be changing.
do_reset;
own(OWN_BIT, '1');
wait until falling_edge(clk); other_sda_low <= '1'; other_scl_low <= '1';
wait for 1 ns;
report "T8 a mismatch while SCL is low is not arbitration loss" severity note;
ck_bit("T8 SCL is low", scl, '0');
ck_bit("T8 the mismatch exists", sda_in(0), '0');
ck_bit("T8 but no loss is declared", arb_loss_now, '0');
step;
ck_bit("T8 and none is latched", arb_lost, '0');
wait until falling_edge(clk); other_scl_low <= '0'; wait for 1 ns;
ck_bit("T8 the same mismatch with SCL high IS a loss", arb_loss_now, '1');
-- T9. Obligation 2 of §3.1.8: the driver is turned off "the moment there is a
-- difference". The controller does that by dropping its request.
step;
release_all;
report "T9 the loser turns its driver off immediately" severity note;
ck_bit("T9 driving nothing", dut_sda_low, '0');
ck_bit("T9 no longer owned", owned, '0');
ck_bit("T9 but the loss is still latched", arb_lost, '1');
-- T10. A loss is counted once per contest rather than once per cycle, and is sticky
-- until the controller clears it.
do_reset;
own(OWN_BIT, '1');
wait until falling_edge(clk); other_sda_low <= '1'; wait for 1 ns;
for i in 0 to 9 loop step; end loop;
report "T10 a loss is counted once, not once per cycle" severity note;
ck_bit("T10 still latched", arb_lost, '1');
ck_int("T10 counted exactly once", to_integer(arb_losses), 1);
wait until falling_edge(clk); arb_clear <= '1';
step;
wait until falling_edge(clk); arb_clear <= '0'; wait for 1 ns;
ck_bit("T10 cleared on request", arb_lost, '0');
-- T11. Every owner drives through the same inversion. Four owners, one sign.
do_reset;
report "T11 all four owners drive through the same single inversion" severity note;
for k in 0 to NO-1 loop
own(k, '0');
ck_bit("T11 a zero from this owner pulls the line low", sda, '0');
ck_int("T11 and that owner is granted", to_integer(unsigned(grant)), 2**k);
own(k, '1');
ck_bit("T11 a one from this owner releases it", sda, '1');
end loop;
-- T12. Reset releases the line. A master coming out of reset holding SDA low is
-- Chapter 15.4's stuck bus, created by the master itself.
wait until falling_edge(clk);
rst_n <= '0'; req <= (others => '1'); bit_val <= (others => '0');
wait for 1 ns;
report "T12 reset releases SDA even with every owner asking for a zero"
severity note;
ck_bit("T12 nothing driven", dut_sda_low, '0');
ck_bit("T12 the line is high", sda, '1');
step;
ck_bit("T12 no conflict latched through reset", owner_conflict, '0');
ck_bit("T12 no arbitration loss latched through reset", arb_lost, '0');
if err = 0 then
report "=== i2c_sda_ctrl: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_sda_ctrl: " & integer'image(err)
& " CHECK(S) FAILED ===" severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;5b. Execution
| Design | SystemVerilog | Verilog-2001 | VHDL | Finish |
|---|---|---|---|---|
i2c_sda_ctrl | PASS 12/12 | PASS 12/12 | PASS 12/12 | 920 ns, all three |
6. Mutation Testing — and One Mutant That Did Not Elaborate
Eight defects. Seven were straightforward; the eighth is a lesson in reading mutation scores rather than trusting them.
| # | Injected defect | Expected detection | Result |
|---|---|---|---|
| M1 | the sign error — drive HIGH for a one instead of releasing | T1, T11 | KILLED (17 failures) |
| M2 | reset no longer dominates the pad | T12 | KILLED (3) |
| M3 | arbitration checked regardless of SCL phase | T8 | KILLED (3) |
| M4 | lose arbitration on a transmitted zero too | T7 | KILLED (4) |
| M5 | drop tx_active — every received zero is a lost arbitration | T6, T7 | KILLED (3) |
| M6 | swallow an ownership conflict instead of reporting it | T5 | KILLED (2) |
| M7′ | drop the priority — all owners drive at once | T4 | KILLED (4) |
| M8 | an unowned line reports a transmitted zero | T2 | KILLED (2) |
valid mutants: 8 killed: 8 survived: 0
baseline PASS before injection; PASS after restore.The mutant that was not a mutant
The priority-reversal defect was first injected by rewriting the generate loop's mask as |req[N_OWNER-1:g+1]. It was scored as killed with zero failure lines — and zero failure lines is the tell:
i2c_sda_ctrl.sv:100: error: part select req[3:4] is out of order.It never elaborated. A build failure counts as "the suite did not pass", which is indistinguishable from a kill if you only read the verdict, and it is no evidence at all that the testbench can detect a reversed priority.
Re-injected as a defect that compiles — dropping the priority mask entirely, so every requesting owner drives simultaneously — it dies properly with four failure lines against T4.
7. Verification Connection — The Monitor That Cannot Attribute
// Section 3b said the electrical condition for "arbitration lost" and for "an
// ordinary received zero" are IDENTICAL, and that only the master's intent
// separates them. That has a direct consequence for a verification component,
// and it is the reason a protocol monitor is harder than it looks:
//
// A PASSIVE MONITOR CANNOT DETECT ARBITRATION LOSS FROM THE BUS ALONE.
//
// It sees SDA low while SCL is high. To call that a lost arbitration it must know
// that some master intended a one -- which is not on the wire. So a monitor has
// exactly three honest options:
//
// 1. Track the transaction itself. If the monitor has been following the frame
// since the START, it knows which bit of which byte is in flight and what the
// addressed direction was, so it can infer intent from the protocol state.
// This is the correct answer, and it is why a monitor is a state machine
// rather than a sampler.
//
// 2. Report the OBSERVATION and let the scoreboard attribute it. The monitor
// emits "SDA read low in bit slot 3 of the address byte"; the scoreboard,
// which knows what the DUT was asked to do, decides whether that is a loss.
//
// 3. Take intent as a configured input from a driver that is generating the
// competing traffic -- valid in a multi-master bench where the environment
// owns both sides.
//
// What is NOT an option is peeking at the DUT's tx_active. That passes a DUT
// whose intent register is right and whose pad is broken, which is Chapter 17.1's
// observability boundary restated.
//
// COVERAGE. The asymmetry of §3a is a coverage obligation, not a curiosity:
//
// cover: a master LOST while sending a one -- reachable
// cover: a master lost while sending a zero -- UNREACHABLE by design
//
// The second bin must be declared illegal or excluded with a comment, because an
// unreachable bin left in a coverage model is indistinguishable from a coverage
// hole at sign-off, and someone will eventually write stimulus trying to hit it.8. FPGA and ASIC Implications
On an FPGA, this block's output is the output enable of a tri-state buffer with the data input tied low. sda_drive_low drives that enable directly — and the readback must come from the buffer's input pin, not from a copy of the enable, which is Chapter 17.1's mutation M4 realised in hardware. The synthesis tool will happily optimise a readback that is fed from the enable into a wire, and the design then simulates correctly and cannot arbitrate.
The priority resolver is a small combinational cone — four requests, four grants — and is not a timing concern at any I²C frequency. What is worth noting is that sda_drive_low is combinational from req and bit_val, so the path from an owner's register to the pad is one level of logic plus the pad delay. That is deliberate: registering it would add a cycle between the drive point and the line, and Chapter 17.3 placed the drive point assuming the bit reaches the line in the cycle after the strobe.
On an ASIC, the same block feeds a pad cell configured as open-drain, and the rst_n gate matters more than on an FPGA: during power-up ramp, before the reset deasserts, the pad must be released. A pad whose enable is undefined during reset can hold the bus down at power-on and prevent every other device from communicating — including the devices that would otherwise have worked. That is the failure T12 exists for, and it is a board-level symptom with an on-die cause.
9. Debugging — The Master That Lost Arbitration on Every Read
A newly integrated master performs writes flawlessly. Every read transaction aborts, and the error register reports arbitration lost. The bus has exactly one master on it -- there is nothing to arbitrate against. A scope shows clean, correct I2C traffic: address, acknowledge, and the target beginning to return data before the master gives up mid-byte.
The arbitration comparator has no transmit-intent term. A receiving master has released SDA deliberately, and releasing is electrically the same as sending a one -- so 'sent a one, read a zero' is true of every zero bit the target ever returns. The detector was correct for the transmit case and had simply never been qualified with the master's own direction. The single-master board is not a mitigating detail but the reason it was caught at all: with a real competitor present the same symptom would have been read as genuine contention and the defect could have survived indefinitely.
Add the intent term: arbitration is checked only while this master is actively transmitting, which is mutation M5 in reverse. Note that no amount of bus observation could have supplied that term -- the electrical condition is identical in the two cases and the difference exists only inside the master, which is why it has to be an explicit input rather than something derived from sda_in and scl_in. For the regression: a read test whose target returns a byte containing at least one zero. A target model that returned 0xFF, or a test that only ever wrote, cannot expose this -- and one of the four devices on this board did return 0xFF, which is how the fault came to look device-specific.Three generalisations.
A single-master board made the bug obvious; a multi-master board would have hidden it. "Arbitration lost" on a bus with nothing to arbitrate against is self-evidently wrong. The same report on a busy bus is plausible, and plausible reports do not get investigated. The easier environment was the better test.
The missing term could not be found by looking at the bus. Every signal the comparator used was correct and correctly connected. The defect was an absent qualification whose justification lives entirely inside the master's intent — the same reason §7's monitor cannot attribute a loss either.
One target returning 0xFF made the fault look device-specific. A byte with no zeros in it cannot trigger the bug, so the fault appeared on three devices and not the fourth, which is exactly the shape that sends an investigation toward the devices instead of toward the master.
10. Common Misconceptions
"A master sets SDA high to send a one." It releases SDA, and the pull-up raises the line. The transmitted bit is the inverse of the drive enable. §1.
"The inversion is too trivial to isolate." Isolating it is what makes a sign error a one-line possibility instead of a four-place one, and it is what lets a single mutation test the whole property. §1.
"SDA ownership follows from the controller's state." Then every owner is coupled to the controller's state list, and adding a state silently changes who drives the line — with nothing in the owners' source to show it. §2.
"An ownership conflict should be resolved silently." Resolving keeps the bus defined and is correct; not reporting it means a design error that fixes itself, which is a design error that ships. Do both. §2.
"Arbitration needs a dedicated detector." It needs four signals the block already has, and the identical shape on SCL is clock stretching. §3.
"A master can lose arbitration while sending a zero." It cannot detect that, because sending a zero and reading a zero is indistinguishable from winning. The asymmetry is why bitwise arbitration terminates. §3a.
"Arbitration can be detected from the bus alone." The electrical condition is identical to an ordinary received zero. Only the master's intent separates them, and intent is not on the wire. §3b.
"A receiving master is not driving, so arbitration logic is idle." A receiving master has released SDA, which is electrically a transmitted one — so an ungated detector fires on the first zero the target returns. §3b and §9.
"Reset resets the owners, so the pad is safe." The drive expression must be gated on reset itself. A master coming out of reset holding SDA low takes the bus down and nothing else can lift it. §5a, T12.
"A mutation that fails to build has been killed." It produced a non-passing run and no evidence. A kill with zero failure lines is not a kill. §6.
11. Reason It Through
Why does isolating the inversion in one block matter more than it appears to?
Because a sign error can then exist in exactly one line of the entire master, which makes it findable by one test and one mutation. Spread across four owners, the same error has four independent chances to appear and no single test covers it. §1.
What specifically goes wrong if SDA ownership is decoded from the transaction controller's state?
Every owner becomes coupled to the controller's state list without referencing it. Adding a state to the controller changes who owns SDA, the change is invisible in the owners' source, and no conflict is detectable because there is no explicit request to conflict. §2.
Why report an ownership conflict if the block already resolves it?
Resolving keeps the bus in a defined state; reporting is what stops the bug from shipping. A conflict is a design error rather than a bus condition, and an error that silently corrects itself is never fixed. §2.
A master is receiving. Electrically it is transmitting ones. What does the arbitration detector have to be told, and why can it not work it out?
That the master is not transmitting. It cannot work it out because the bus condition — released, reads low, SCL high — is identical whether a competitor is winning or a target is simply sending a zero. The distinguishing fact exists only inside the master. §3b.
Why must a coverage model declare "lost while sending a zero" unreachable rather than leaving it as an unfilled bin?
Because an unreachable bin is indistinguishable from a coverage hole at sign-off, and someone will write stimulus to chase it. The asymmetry is a property of the protocol, so the bin is illegal by design and should say so. §7.
Two mutation runs in this module produced misleading scores in opposite directions. What were they?
An unreachable mutant that survived, in 17.2 — a case item shadowed by an earlier one — and a non-elaborating mutant scored as killed, here. Both look like ordinary results from the verdict alone, which is why the evidence has to be read. §6.
Why is sda_drive_low combinational rather than registered?
Because Chapter 17.3 placed the drive point on the assumption that a bit reaches the line in the cycle after the strobe. Registering here inserts an extra cycle and silently consumes one cycle of tSU;DAT. §8.
12. Understanding Check
13. Summary
The transmitted bit is the inverse of the drive enable, because an I²C output drives low or releases and there is no drive-high. Isolating that inversion in one named block makes a sign error a one-line possibility that one test and one mutation can cover.
SDA changes hands once per byte, at the acknowledge pulse, and four blocks inside the master want to drive it — so ownership is a first-class signal rather than an implication of the controller's state encoding.
Deriving ownership from the state encoding couples every owner to the controller invisibly. Adding a state then changes who drives the line, with nothing in the owners' source to reveal it.
An ownership conflict is resolved and reported. Resolving keeps the bus defined; reporting stops a design error that corrects itself from shipping.
Arbitration detection needs no dedicated block — four signals already present, and the identical shape on SCL is clock stretching.
A master can only lose by sending a one. Sending a zero and reading a zero is indistinguishable from winning, and that asymmetry is why bitwise arbitration terminates.
The detector must be gated on transmit intent, because a receiving master has released SDA and releasing is electrically a transmitted one. Without the gate, every zero a target returns reads as a lost arbitration — and no bus observation can supply the missing term, since the electrical condition is identical in both cases.
Reset must dominate the pad. A master leaving reset while holding SDA low takes the bus down and nothing else can lift it.
Eight valid mutants, eight killed — and the ninth attempt was not a mutant at all: it failed to elaborate and was scored as a kill with zero failure lines.
So a mutation score has a failure mode at each end. Chapter 17.2 found an unreachable mutant that survived; this chapter found a non-elaborating one recorded as killed. Read the evidence, not the verdict.
14. What Comes Next
Both lines now have blocks. SCL has legal phases and announces the two instants; SDA has one inversion, explicit ownership and arbitration detection.
Chapter 17.5 builds the first block that uses them — and it is the one Chapter 17.1 §3 proved cannot live anywhere else. The framing sequencer moves SDA while SCL is high, which is precisely the thing the bit engine exists to prevent, and it must hold the resulting edges for tHD;STA and tSU;STO before the transaction may proceed.
It is also the block that owns the bus-free interval: tBUF between a STOP and the next START, which is the one timing parameter that constrains what a master may do when it is doing nothing.
Continue learning
Related tutorials
- Related topic
Decomposing an I²C Master — From Requirements to Architecture
An I²C master is not one state machine, and the reason is structural rather than stylistic: the protocol imposes four independent time bases that change on four unrelated events. Derives the block structure from the normative obligations, establishes the wired-AND bus model every later chapter is written against, and shows why the framing generator cannot live inside the bit engine.
- Related topic
The Bit Engine — Driving and Sampling One Bit
One bit, and deliberately no more. Shows how making the drive point the only place SDA is written turns the data-valid rule from a property to be checked into one that cannot be violated, why transmit and receive are the same logic differing by one bit, and why an engine needs two different kinds of release because two normative rules are in tension.
- Related topic
Bus Feedback — Clock Stretching and Arbitration From One Comparison
Clock stretching and arbitration loss are not two features. They are one comparison — a line this master released that reads back low — applied to two wires, differing only in a timing qualifier and an intent gate. Builds both from a single comparator and shows why a master can only ever lose by trying to send a one.
- Related topic
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.
