I²C · Module 19
Modeling Open Drain — 0/Z, Output Enable and What Synthesis Infers
An I²C output has two states, LOW and RELEASED, and neither is 'drive a 1'. Compares the resolved-0/Z pin, the output-enable pair and the two-valued drive intent, builds the boundary stage that joins them, and separates what synthesis is expected to infer from what this chapter can actually prove.
Modules 17 and 18 built a controller and a target, verified them against each other, and ended with two synthesizable blocks whose bus interface is four signals: scl_drive_low, sda_drive_low out, and the two lines read back in. Every test passed. Thirty tri-HDL combinations, zero surviving mutants.
None of that is evidence that either block works on a board.
This module is about the gap, and this chapter is about its first and widest part: the four signals above are not pins. Something has to turn them into pins, and the choice of how is made in RTL, early, in a line of code that looks too simple to matter.
1. An I²C Output Has Two States, and "1" Is Not One of Them
This is not a detail of the electrical specification that RTL can abstract away. It changes the arity of the output. A push-pull output carries one bit of information — the value. An open-drain output carries one bit too, but it is a different bit: do I own this line right now?
The two are easy to confuse because both are one bit wide, and a bug that confuses them compiles, simulates against a careless bus model, and destroys hardware.
| push-pull output | open-drain output | |
|---|---|---|
| states | drives 0, drives 1 | drives 0, releases |
| the bit means | the value on the wire | whether I am holding the wire |
| two devices asserting | short circuit | legal, and load-bearing |
| who makes it HIGH | the driver | the pull-up, only |
| what a release means | — | "not my business any more" |
The third row is the reason I²C is built this way. Simultaneous LOWs from two devices are not an error condition to be avoided — they are the mechanism behind acknowledge, clock stretching and arbitration. Every one of those features in Modules 17 and 18 depends on two devices being allowed to pull the same wire down at the same time.
2. Three Ways RTL Says It, and They Are Not Interchangeable
There are three common ways to express an open-drain output in HDL. All three can be made to work. They differ in where they work, and a design that picks the wrong one for the wrong layer is the single most common way a logically correct I²C block fails in an FPGA.
Style A — the resolved pin
// A single net carrying 0 or Z, resolved by the simulator against a pull-up.
//
// NOT COMPILED AS PART OF THIS CHAPTER'S VERIFIED SET -- it is shown to be compared
// against, and Section 8 explains where it is and is not appropriate.
assign sda = drive_low ? 1'b0 : 1'bz; // and somewhere: pullup(sda);This is the most literal transcription of the schematic, and inside a protocol engine it is the wrong tool for three separate reasons.
The first is that it makes sda a four-valued signal in the middle of your design. Any logic that reads it now has to reason about Z and X, and the casez/=== habits that follow are how X-optimism and X-pessimism bugs get in.
The second is that it couples your core to a resolution mechanism. The Z only becomes a 1 because a pullup primitive or a testbench model said so. That model lives outside your module, which means your module's behaviour is no longer determined by your module.
The third is the one this module exists for: Z inside the fabric and Z at a pin are different things. Chapter 19.2 takes that apart properly.
Style B — the output-enable pair
// Two signals: the value to drive, and whether to drive it. This is the interface
// every FPGA I/O buffer presents, under every vendor's name for it.
//
// For open drain the first one is a constant:
//
// pad_o = 0 always -- the only value a conforming output drives
// pad_oe = 1 I am holding the line LOW
// pad_oe = 0 I have released itThis is what the hardware is. It belongs at the boundary, and Chapter 19.2 is where it gets built.
Style C — the two-valued drive intent
This is what Modules 17 and 18 already emit, and it is the right language for a protocol engine:
drive_low = 1 → I want this line LOW
drive_low = 0 → I have no claim on this lineOne bit, two values, no Z, no resolution, no vendor. The protocol engine says what it wants; a single stage at the edge of the design turns that want into pins.
3. The Pad Law
The stage that joins Style C to Style B is four lines long, and its core is one equation worth stating on its own:
a pad pulls its net LOW ⟺ the pad is ENABLED ∧ the pad's data is 0
a pad asks for a SHORT ⟺ the pad is ENABLED ∧ the pad's data is 1Both halves matter. The first is the behaviour you want. The second is the behaviour that must be unreachable, and naming it makes it testable — an untested illegal case is an assumption, not a property.
4. The Output Stage
// -----------------------------------------------------------------------------
// i2c_od_out.sv
// The output stage of one conforming open-drain pin.
//
// This is the whole of what "open drain" means, expressed as logic. The protocol
// engine produces ONE bit of intent -- `drive_low` -- and this stage turns it into
// the two signals a bidirectional pad actually consumes: the data to drive, and
// whether to drive at all.
//
// THE DATA IS A CONSTANT. `pad_o` is tied to 0 and there is no parameter, no mode
// and no condition that changes it. That is not a simplification: a conforming I²C
// output has exactly two states, LOW and RELEASED, so the only value it ever drives
// is 0. An output that can also drive 1 is a push-pull output, and two of those on
// one wire is a short circuit, not a bus.
//
// WHY `pulls_low` IS WRITTEN AS A FUNCTION OF THE PAD SIGNALS rather than copied
// from `drive_low`: it is the law a real pad obeys -- a pad pulls its net down when
// it is ENABLED and its data is 0. Writing the mechanism means a wrong `pad_o`
// anywhere in the chain changes the contribution instead of being hidden by a
// shortcut, and a mutation that drives 1 is caught here rather than at a pin nobody
// is looking at.
//
// WHAT IS DELIBERATELY ABSENT: a `drives_high` monitor. An earlier draft had one,
// computing `pad_oe & pad_o` so a synthesised design could report the illegal
// combination the way Chapter 18.11 reports `n_sda_conflict`. It was removed because
// `pad_o` is tied to 0 here, which makes `pad_oe & pad_o` PROVABLY CONSTANT 0 -- a
// monitor that cannot fire cannot be tested, and one that cannot be tested
// guarantees nothing. Tying it off was undetectable, which is exactly the
// diagnosis. The property is real, so it moves to Chapter 19.2, where `pad_o` is an
// input the caller supplies and a wrong value is genuinely possible.
// -----------------------------------------------------------------------------
module i2c_od_out (
// ---- the protocol engine's side -----------------------------------------
// One bit of intent, exactly as Modules 17 and 18 emit it:
// 1 = pull the line LOW 0 = RELEASE the line
// Note what is NOT here: any way to say "drive HIGH".
input logic drive_low,
// ---- the pad's side ------------------------------------------------------
// Data to the pad. Constant 0 -- see the header.
output logic pad_o,
// Output enable. 1 = drive `pad_o` onto the pin, 0 = release to high impedance.
output logic pad_oe,
// ---- what the bus sees ---------------------------------------------------
// This pin's contribution to a wired-AND net: does it hold the line down?
output logic pulls_low
);
// Open drain: the driven value is always 0.
assign pad_o = 1'b0;
// Intent becomes ownership. This is the only place the mapping is made, which is
// why an inverted enable is a one-line bug with a bus-wide consequence.
assign pad_oe = drive_low;
// The pad law: enabled AND driving a zero.
assign pulls_low = pad_oe & ~pad_o;
endmoduleTwo things in that file are deliberate and neither is obvious.
pulls_low is computed from the pad signals, not copied from drive_low. Writing assign pulls_low = drive_low; would be shorter and — in a correct design — identical. It is avoided because it hides the mechanism: with the law written out, a wrong pad_o anywhere in the chain changes the contribution and a test notices. Section 7 shows a mutation that proves this is not a hypothetical benefit.
There is no drives_high monitor, and an earlier draft of this chapter had one. That story is worth more than the code.
Same design in Verilog-2001 and VHDL:
// -----------------------------------------------------------------------------
// i2c_od_out.v
// The output stage of one conforming open-drain pin.
//
// This is the whole of what "open drain" means, expressed as logic. The protocol
// engine produces ONE bit of intent -- `drive_low` -- and this stage turns it into
// the two signals a bidirectional pad actually consumes: the data to drive, and
// whether to drive at all.
//
// THE DATA IS A CONSTANT. `pad_o` is tied to 0 and there is no parameter, no mode
// and no condition that changes it. That is not a simplification: a conforming I²C
// output has exactly two states, LOW and RELEASED, so the only value it ever drives
// is 0. An output that can also drive 1 is a push-pull output, and two of those on
// one wire is a short circuit, not a bus.
//
// WHY `pulls_low` IS WRITTEN AS A FUNCTION OF THE PAD SIGNALS rather than copied
// from `drive_low`: it is the law a real pad obeys -- a pad pulls its net down when
// it is ENABLED and its data is 0. Writing the mechanism means a wrong `pad_o`
// anywhere in the chain changes the contribution instead of being hidden by a
// shortcut, and a mutation that drives 1 is caught here rather than at a pin nobody
// is looking at.
//
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
//
// WHAT IS DELIBERATELY ABSENT: a `drives_high` monitor. An earlier draft had one,
// computing `pad_oe & pad_o` so a synthesised design could report the illegal
// combination the way Chapter 18.11 reports `n_sda_conflict`. It was removed because
// `pad_o` is tied to 0 here, which makes `pad_oe & pad_o` PROVABLY CONSTANT 0 -- a
// monitor that cannot fire cannot be tested, and one that cannot be tested
// guarantees nothing. Tying it off was undetectable, which is exactly the
// diagnosis. The property is real, so it moves to Chapter 19.2, where `pad_o` is an
// input the caller supplies and a wrong value is genuinely possible.
// -----------------------------------------------------------------------------
module i2c_od_out (
// ---- the protocol engine's side -----------------------------------------
// One bit of intent, exactly as Modules 17 and 18 emit it:
// 1 = pull the line LOW 0 = RELEASE the line
// Note what is NOT here: any way to say "drive HIGH".
input wire drive_low,
// ---- the pad's side ------------------------------------------------------
// Data to the pad. Constant 0 -- see the header.
output wire pad_o,
// Output enable. 1 = drive `pad_o` onto the pin, 0 = release to high impedance.
output wire pad_oe,
// ---- what the bus sees ---------------------------------------------------
// This pin's contribution to a wired-AND net: does it hold the line down?
output wire pulls_low
);
// Open drain: the driven value is always 0.
assign pad_o = 1'b0;
// Intent becomes ownership. This is the only place the mapping is made, which is
// why an inverted enable is a one-line bug with a bus-wide consequence.
assign pad_oe = drive_low;
// The pad law: enabled AND driving a zero.
assign pulls_low = pad_oe & ~pad_o;
endmodule -- -----------------------------------------------------------------------------
-- i2c_od_out.vhd
-- The output stage of one conforming open-drain pin.
-- Behavioural twin of the SystemVerilog and Verilog designs.
--
-- The protocol engine produces ONE bit of intent -- `drive_low` -- and this stage
-- turns it into the two signals a bidirectional pad consumes: the data to drive,
-- and whether to drive at all. `pad_o` is tied to '0' and nothing changes it: a
-- conforming I²C output has exactly two states, LOW and RELEASED.
--
-- NOTE ON VHDL SPECIFICALLY: `std_logic` can represent 'Z' and resolve multiple
-- drivers natively, so it is tempting to model the pin itself here. That is
-- deliberately NOT done. This stage stays two-valued so the contract is identical
-- in all three languages, and so the core never depends on a resolution mechanism
-- that exists in the simulator but not in the fabric. Chapter 19.2 is where the
-- pin-level representation belongs.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
entity i2c_od_out is
port (
-- One bit of intent, exactly as Modules 17 and 18 emit it:
-- '1' = pull the line LOW '0' = RELEASE the line
-- Note what is NOT here: any way to say "drive HIGH".
drive_low : in std_logic;
-- Data to the pad. Constant '0' -- see the header.
pad_o : out std_logic;
-- Output enable. '1' = drive `pad_o` onto the pin, '0' = release to Hi-Z.
pad_oe : out std_logic;
-- This pin's contribution to a wired-AND net: does it hold the line down?
pulls_low : out std_logic
);
end entity i2c_od_out;
architecture rtl of i2c_od_out is
-- Internal copies so the contribution is computed from the pad signals rather
-- than from `drive_low` directly -- the same reason given in the SV header.
signal pad_o_i : std_logic;
signal pad_oe_i : std_logic;
begin
-- Open drain: the driven value is always '0'.
pad_o_i <= '0';
-- Intent becomes ownership.
pad_oe_i <= drive_low;
pad_o <= pad_o_i;
pad_oe <= pad_oe_i;
-- The pad law: enabled AND driving a zero.
pulls_low <= pad_oe_i and (not pad_o_i);
end architecture rtl;The VHDL file carries a note the other two do not need. std_logic resolves multiple drivers natively and has a real 'Z', so VHDL could model the pin here — and deliberately does not, so that the contract is identical in all three languages and the core never leans on a resolution mechanism that exists in the simulator and not in the fabric.
5. Verifying It on a Real Bus
A stage in isolation cannot demonstrate what open drain is for. The bench therefore puts two stages on Module 16's i2c_line_model — the same wired-AND bus model Modules 17 and 18 are verified against — and wires the bus from pulls_low, not from drive_low, so it cannot shortcut past the logic under test.
// -----------------------------------------------------------------------------
// i2c_od_out_tb.sv
// Independent oracle for i2c_od_out, on a real wired-AND bus.
//
// The stage alone cannot show what open drain is FOR, so the bench instantiates two
// of them on Module 16's `i2c_line_model` -- the same bus model Modules 17 and 18
// are verified against. What is proven here is the chain:
//
// drive_low -> pad_oe -> pulls_low -> wired-AND -> resolved line -> read back
//
// Every wait is a fixed number of settle delays; there is no DUT-dependent wait,
// so the bench cannot hang.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_od_out_tb;
localparam int N = 2; // two devices, the smallest bus that can conflict
logic [N-1:0] drive_low;
logic [N-1:0] pad_o, pad_oe, pulls_low;
// The bus. `sda` is the resolved level; `sda_in[i]` is what device i reads back.
wire sda, scl;
wire [N-1:0] sda_in, scl_in, sda_rbl, scl_rbl;
wire [7:0] sda_holders, scl_holders;
integer errors = 0;
integer i, combo;
logic exp_line;
// ---- two output stages, one per device -------------------------------------
genvar g;
generate
for (g = 0; g < N; g = g + 1) begin : g_dev
i2c_od_out u_od (
.drive_low(drive_low[g]), .pad_o(pad_o[g]), .pad_oe(pad_oe[g]),
.pulls_low(pulls_low[g]));
end
endgenerate
// ---- the bus, driven by the CONTRIBUTIONS the stages computed ---------------
// Note it is `pulls_low` that is wired here, not `drive_low`. The bench must not
// shortcut past the stage it is testing -- Chapter 17.1's observability rule.
i2c_line_model #(.N_DEV(N)) bus (
.scl_drive_low({N{1'b0}}), .sda_drive_low(pulls_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));
// ---- the pad law, modelled independently of the DUT -------------------------
// `i2c_od_out` computes its contribution from (pad_oe, pad_o). The bench must not
// reuse the DUT's expression to check the DUT's expression, so the law is written
// out once here and validated exhaustively in T0 before it is trusted anywhere
// else. A pad pulls its net down when it is enabled and its data is 0; it ASKS
// FOR A SHORT CIRCUIT when it is enabled and its data is 1.
function automatic law_pulls_low (input oe, input o); law_pulls_low = oe & ~o; endfunction
function automatic law_illegal (input oe, input o); law_illegal = oe & o; endfunction
task settle; begin #5; end endtask
task ck (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_idx (input [200*8:1] what, input integer idx,
input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s[%0d]: got %0d expected %0d", what, idx, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_od_out: LOW or RELEASE, and nothing else ===");
// ----------------------------------------------------------------
// T0. THE LAW BEFORE THE DESIGN. Every later test compares the DUT against
// `law_pulls_low`, so the law is checked over all four (oe, o) pairs
// first. Three of them a conforming open-drain pin reaches; the fourth --
// enabled with a 1 -- is the short circuit, and it is listed here so the
// illegal case is a tested value rather than an untested assumption.
// ----------------------------------------------------------------
ck("T0 released, data 0: no pull", law_pulls_low(1'b0, 1'b0), 0);
ck("T0 released, data 1: no pull", law_pulls_low(1'b0, 1'b1), 0);
ck("T0 enabled with a 0: pulls low", law_pulls_low(1'b1, 1'b0), 1);
ck("T0 enabled with a 1: does NOT pull",law_pulls_low(1'b1, 1'b1), 0);
ck("T0 and enabled-with-a-1 is the illegal case",
law_illegal(1'b1, 1'b1), 1);
ck("T0 no other pair is illegal",
law_illegal(1'b0,1'b0) | law_illegal(1'b0,1'b1) | law_illegal(1'b1,1'b0), 0);
$display("T0 the pad law holds for all four (oe, data) pairs");
// ----------------------------------------------------------------
// T1. DRIVE LOW. Intent becomes ownership: the enable asserts, the data stays
// 0, and the pin contributes a pull-down.
// ----------------------------------------------------------------
drive_low = 2'b01; settle;
$display("T1 drive_low=1 enables the pad with a zero on it");
ck("T1 pad_oe asserted", pad_oe[0], 1);
ck("T1 pad_o is still 0", pad_o[0], 0);
ck("T1 the pin pulls low", pulls_low[0], 1);
ck("T1 and is not enabled with a one",
law_illegal(pad_oe[0], pad_o[0]), 0);
// ----------------------------------------------------------------
// T2. RELEASE. The enable deasserts. Releasing is NOT driving a 1: the pin
// stops contributing, and what the line becomes is somebody else's
// business -- the pull-up's, or another device's.
// ----------------------------------------------------------------
drive_low = 2'b00; settle;
$display("T2 drive_low=0 releases: no enable, no contribution, no drive-high");
ck("T2 pad_oe deasserted", pad_oe[0], 0);
ck("T2 no pull-down", pulls_low[0], 0);
ck("T2 still not the illegal case",
law_illegal(pad_oe[0], pad_o[0]), 0);
// ----------------------------------------------------------------
// T3. BOTH RELEASED -> the pull-up decides. This is the only mechanism that
// ever produces a HIGH on this bus.
// ----------------------------------------------------------------
drive_low = 2'b00; settle;
$display("T3 every device released: the pull-up, and only the pull-up, makes it HIGH");
ck("T3 the line is high", sda, 1);
ck("T3 nobody holds it", sda_holders, 0);
// ----------------------------------------------------------------
// T4. ONE DEVICE PULLS -> EXTERNAL LOW DOMINATES A RELEASE. Device 1 has
// released, and it reads LOW anyway. A device that trusted its own intent
// instead of the pin would believe the line was high.
// ----------------------------------------------------------------
drive_low = 2'b01; settle;
$display("T4 one device pulling beats every release, and the releaser sees it");
ck("T4 the line is low", sda, 0);
ck("T4 exactly one holder", sda_holders, 1);
ck("T4 the puller reads low", sda_in[0], 0);
ck("T4 the RELEASER also reads low",sda_in[1], 0);
ck("T4 and device 1 is not driving",pulls_low[1], 0);
// ----------------------------------------------------------------
// T5. TWO DEVICES PULL -> still LOW, and no conflict. This is the difference
// between a wired-AND bus and a push-pull net: simultaneous LOWs are legal
// and are how ACK and arbitration work.
// ----------------------------------------------------------------
drive_low = 2'b11; settle;
$display("T5 two devices pulling together is legal, not a conflict");
ck("T5 the line is low", sda, 0);
ck("T5 two holders", sda_holders, 2);
ck("T5 neither is enabled with a one",
law_illegal(pad_oe[0],pad_o[0]) | law_illegal(pad_oe[1],pad_o[1]), 0);
// ----------------------------------------------------------------
// T6. A RELEASE WHILE ANOTHER DEVICE HOLDS DOES NOT RAISE THE LINE. Device 0
// lets go; device 1 is still pulling; the line stays down. If releasing
// drove a 1 this is where the short circuit would appear.
// ----------------------------------------------------------------
drive_low = 2'b10; settle;
$display("T6 releasing is not driving high: the line stays down");
ck("T6 the line is still low", sda, 0);
ck("T6 one holder left", sda_holders, 1);
ck("T6 the releaser reads low",sda_in[0], 0);
// ----------------------------------------------------------------
// T7. EXHAUSTIVE OVER EVERY DRIVE COMBINATION. Four cases at N=2, and the
// resolved level must equal the AND of the releases every time. An
// exhaustive sweep is affordable here and leaves no untested combination,
// which a handful of hand-picked vectors cannot claim.
// ----------------------------------------------------------------
for (combo = 0; combo < (1 << N); combo = combo + 1) begin
drive_low = combo[N-1:0];
settle;
exp_line = (combo == 0); // HIGH only when nobody pulls
ck_idx("T7 resolved level is the wired-AND", combo, sda, exp_line);
ck_idx("T7 holder count matches", combo, sda_holders,
(combo[0] ? 1 : 0) + (combo[1] ? 1 : 0));
for (i = 0; i < N; i = i + 1) begin
ck_idx("T7 contribution equals intent", combo,
pulls_low[i], combo[i]);
// Every device reads the RESOLVED line, not its own intent.
ck_idx("T7 readback is the bus, not the intent", combo,
sda_in[i], exp_line);
end
end
$display("T7 all %0d drive combinations resolve as a wired-AND", 1 << N);
// ----------------------------------------------------------------
// T8. THE INTENT AND THE PIN DISAGREE, AND THAT IS NORMAL. Device 0 has
// released (intent = release) while the line is LOW. Any logic that
// substituted its own drive intent for the pin would be wrong here, and
// this is exactly the substitution Chapter 19.7 catalogues as a mismatch
// that passes in simulation on an ideal bus.
// ----------------------------------------------------------------
drive_low = 2'b10; settle;
$display("T8 intent and pin legitimately disagree -- read the pin");
ck("T8 device 0 intends to release", pulls_low[0], 0);
ck("T8 but the pin reads low", sda_in[0], 0);
// A device substituting intent for observation would conclude "I released, so
// the line must be HIGH". Assert that guess and the truth actually differ, so
// the test fails if the bus ever stops being able to contradict a release.
ck("T8 the intent-based guess is HIGH and the pin disagrees",
((~pulls_low[0]) !== sda_in[0]), 1);
// ----------------------------------------------------------------
// T9. THE STAGE NEVER REACHES THE ILLEGAL COMBINATION. Swept over every drive
// value, `law_illegal(pad_oe, pad_o)` must stay 0 for every device -- which
// is the "never actively drives HIGH" requirement, checked against the
// independently written law rather than against a monitor inside the DUT.
// A monitor inside a module whose `pad_o` is a tied constant cannot fire,
// so it could be tied off with no test noticing; see the design header.
// ----------------------------------------------------------------
for (combo = 0; combo < (1 << N); combo = combo + 1) begin
drive_low = combo[N-1:0]; settle;
for (i = 0; i < N; i = i + 1) begin
ck_idx("T9 never enabled with a one", combo,
law_illegal(pad_oe[i], pad_o[i]), 0);
ck_idx("T9 the pad data stayed 0", combo, pad_o[i], 0);
ck_idx("T9 contribution matches the law", combo,
pulls_low[i], law_pulls_low(pad_oe[i], pad_o[i]));
end
end
$display("T9 no drive value makes the stage ask the pad for a one");
if (errors == 0) $display("=== i2c_od_out: ALL CHECKS PASSED ===");
else $display("=== i2c_od_out: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmoduleThree of those tests carry most of the value.
T0 checks the law before anything uses it. The bench compares the DUT against law_pulls_low, so that function is itself validated over all four (oe, data) pairs first — including the illegal one. A bench whose oracle is unchecked is a bench that agrees with itself.
T4 and T8 are the same situation read two ways. Device 1 has released the line and reads LOW anyway, because device 0 is holding it. T4 states that as the wired-AND rule. T8 states it as the trap: a device that substituted its own intent for the pin would conclude the line was HIGH, and be wrong. That substitution is a real and common bug, and it is the first entry in Chapter 19.7's catalogue.
T7 is exhaustive, not representative. With two devices there are four drive combinations, so there is no reason to sample. Every combination is checked for the resolved level, the holder count, each contribution and — the part that matters — each device's readback, which must equal the resolved line rather than that device's own intent.
// -----------------------------------------------------------------------------
// i2c_od_out_tb.v
// Independent oracle for i2c_od_out, on a real wired-AND bus.
//
// The stage alone cannot show what open drain is FOR, so the bench instantiates two
// of them on Module 16's `i2c_line_model` -- the same bus model Modules 17 and 18
// are verified against. What is proven here is the chain:
//
// drive_low -> pad_oe -> pulls_low -> wired-AND -> resolved line -> read back
//
// Every wait is a fixed number of settle delays; there is no DUT-dependent wait,
// so the bench cannot hang.
//
// (Verilog-2001 -- the same tests as the SystemVerilog bench.)
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_od_out_tb;
localparam N = 2; // two devices, the smallest bus that can conflict
reg [N-1:0] drive_low;
wire [N-1:0] pad_o, pad_oe, pulls_low;
// The bus. `sda` is the resolved level; `sda_in[i]` is what device i reads back.
wire sda, scl;
wire [N-1:0] sda_in, scl_in, sda_rbl, scl_rbl;
wire [7:0] sda_holders, scl_holders;
integer errors = 0;
integer i, combo;
reg [N-1:0] cv;
reg exp_line;
// ---- two output stages, one per device -------------------------------------
genvar g;
generate
for (g = 0; g < N; g = g + 1) begin : g_dev
i2c_od_out u_od (
.drive_low(drive_low[g]), .pad_o(pad_o[g]), .pad_oe(pad_oe[g]),
.pulls_low(pulls_low[g]));
end
endgenerate
// ---- the bus, driven by the CONTRIBUTIONS the stages computed ---------------
// Note it is `pulls_low` that is wired here, not `drive_low`. The bench must not
// shortcut past the stage it is testing -- Chapter 17.1's observability rule.
i2c_line_model #(.N_DEV(N)) bus (
.scl_drive_low({N{1'b0}}), .sda_drive_low(pulls_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));
// ---- the pad law, modelled independently of the DUT -------------------------
// `i2c_od_out` computes its contribution from (pad_oe, pad_o). The bench must not
// reuse the DUT's expression to check the DUT's expression, so the law is written
// out once here and validated exhaustively in T0 before it is trusted anywhere
// else. A pad pulls its net down when it is enabled and its data is 0; it ASKS
// FOR A SHORT CIRCUIT when it is enabled and its data is 1.
function law_pulls_low; input oe; input o; begin law_pulls_low = oe & ~o; end endfunction
function law_illegal; input oe; input o; begin law_illegal = oe & o; end endfunction
task settle; begin #5; end endtask
task ck (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_idx (input [200*8:1] what, input integer idx,
input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s[%0d]: got %0d expected %0d", what, idx, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_od_out: LOW or RELEASE, and nothing else ===");
// ----------------------------------------------------------------
// T0. THE LAW BEFORE THE DESIGN. Every later test compares the DUT against
// `law_pulls_low`, so the law is checked over all four (oe, o) pairs
// first. Three of them a conforming open-drain pin reaches; the fourth --
// enabled with a 1 -- is the short circuit, and it is listed here so the
// illegal case is a tested value rather than an untested assumption.
// ----------------------------------------------------------------
ck("T0 released, data 0: no pull", law_pulls_low(1'b0, 1'b0), 0);
ck("T0 released, data 1: no pull", law_pulls_low(1'b0, 1'b1), 0);
ck("T0 enabled with a 0: pulls low", law_pulls_low(1'b1, 1'b0), 1);
ck("T0 enabled with a 1: does NOT pull",law_pulls_low(1'b1, 1'b1), 0);
ck("T0 and enabled-with-a-1 is the illegal case",
law_illegal(1'b1, 1'b1), 1);
ck("T0 no other pair is illegal",
law_illegal(1'b0,1'b0) | law_illegal(1'b0,1'b1) | law_illegal(1'b1,1'b0), 0);
$display("T0 the pad law holds for all four (oe, data) pairs");
// ----------------------------------------------------------------
// T1. DRIVE LOW. Intent becomes ownership: the enable asserts, the data stays
// 0, and the pin contributes a pull-down.
// ----------------------------------------------------------------
drive_low = 2'b01; settle;
$display("T1 drive_low=1 enables the pad with a zero on it");
ck("T1 pad_oe asserted", pad_oe[0], 1);
ck("T1 pad_o is still 0", pad_o[0], 0);
ck("T1 the pin pulls low", pulls_low[0], 1);
ck("T1 and is not enabled with a one",
law_illegal(pad_oe[0], pad_o[0]), 0);
// ----------------------------------------------------------------
// T2. RELEASE. The enable deasserts. Releasing is NOT driving a 1: the pin
// stops contributing, and what the line becomes is somebody else's
// business -- the pull-up's, or another device's.
// ----------------------------------------------------------------
drive_low = 2'b00; settle;
$display("T2 drive_low=0 releases: no enable, no contribution, no drive-high");
ck("T2 pad_oe deasserted", pad_oe[0], 0);
ck("T2 no pull-down", pulls_low[0], 0);
ck("T2 still not the illegal case",
law_illegal(pad_oe[0], pad_o[0]), 0);
// ----------------------------------------------------------------
// T3. BOTH RELEASED -> the pull-up decides. This is the only mechanism that
// ever produces a HIGH on this bus.
// ----------------------------------------------------------------
drive_low = 2'b00; settle;
$display("T3 every device released: the pull-up, and only the pull-up, makes it HIGH");
ck("T3 the line is high", sda, 1);
ck("T3 nobody holds it", sda_holders, 0);
// ----------------------------------------------------------------
// T4. ONE DEVICE PULLS -> EXTERNAL LOW DOMINATES A RELEASE. Device 1 has
// released, and it reads LOW anyway. A device that trusted its own intent
// instead of the pin would believe the line was high.
// ----------------------------------------------------------------
drive_low = 2'b01; settle;
$display("T4 one device pulling beats every release, and the releaser sees it");
ck("T4 the line is low", sda, 0);
ck("T4 exactly one holder", sda_holders, 1);
ck("T4 the puller reads low", sda_in[0], 0);
ck("T4 the RELEASER also reads low",sda_in[1], 0);
ck("T4 and device 1 is not driving",pulls_low[1], 0);
// ----------------------------------------------------------------
// T5. TWO DEVICES PULL -> still LOW, and no conflict. This is the difference
// between a wired-AND bus and a push-pull net: simultaneous LOWs are legal
// and are how ACK and arbitration work.
// ----------------------------------------------------------------
drive_low = 2'b11; settle;
$display("T5 two devices pulling together is legal, not a conflict");
ck("T5 the line is low", sda, 0);
ck("T5 two holders", sda_holders, 2);
ck("T5 neither is enabled with a one",
law_illegal(pad_oe[0],pad_o[0]) | law_illegal(pad_oe[1],pad_o[1]), 0);
// ----------------------------------------------------------------
// T6. A RELEASE WHILE ANOTHER DEVICE HOLDS DOES NOT RAISE THE LINE. Device 0
// lets go; device 1 is still pulling; the line stays down. If releasing
// drove a 1 this is where the short circuit would appear.
// ----------------------------------------------------------------
drive_low = 2'b10; settle;
$display("T6 releasing is not driving high: the line stays down");
ck("T6 the line is still low", sda, 0);
ck("T6 one holder left", sda_holders, 1);
ck("T6 the releaser reads low",sda_in[0], 0);
// ----------------------------------------------------------------
// T7. EXHAUSTIVE OVER EVERY DRIVE COMBINATION. Four cases at N=2, and the
// resolved level must equal the AND of the releases every time. An
// exhaustive sweep is affordable here and leaves no untested combination,
// which a handful of hand-picked vectors cannot claim.
// ----------------------------------------------------------------
for (combo = 0; combo < (1 << N); combo = combo + 1) begin
cv = combo[N-1:0]; drive_low = cv;
settle;
exp_line = (combo == 0); // HIGH only when nobody pulls
ck_idx("T7 resolved level is the wired-AND", combo, sda, exp_line);
ck_idx("T7 holder count matches", combo, sda_holders,
(combo[0] ? 1 : 0) + (combo[1] ? 1 : 0));
for (i = 0; i < N; i = i + 1) begin
ck_idx("T7 contribution equals intent", combo,
pulls_low[i], cv[i]);
// Every device reads the RESOLVED line, not its own intent.
ck_idx("T7 readback is the bus, not the intent", combo,
sda_in[i], exp_line);
end
end
$display("T7 all %0d drive combinations resolve as a wired-AND", 1 << N);
// ----------------------------------------------------------------
// T8. THE INTENT AND THE PIN DISAGREE, AND THAT IS NORMAL. Device 0 has
// released (intent = release) while the line is LOW. Any logic that
// substituted its own drive intent for the pin would be wrong here, and
// this is exactly the substitution Chapter 19.7 catalogues as a mismatch
// that passes in simulation on an ideal bus.
// ----------------------------------------------------------------
drive_low = 2'b10; settle;
$display("T8 intent and pin legitimately disagree -- read the pin");
ck("T8 device 0 intends to release", pulls_low[0], 0);
ck("T8 but the pin reads low", sda_in[0], 0);
// A device substituting intent for observation would conclude "I released, so
// the line must be HIGH". Assert that guess and the truth actually differ, so
// the test fails if the bus ever stops being able to contradict a release.
ck("T8 the intent-based guess is HIGH and the pin disagrees",
((~pulls_low[0]) !== sda_in[0]), 1);
// ----------------------------------------------------------------
// T9. THE STAGE NEVER REACHES THE ILLEGAL COMBINATION. Swept over every drive
// value, `law_illegal(pad_oe, pad_o)` must stay 0 for every device -- which
// is the "never actively drives HIGH" requirement, checked against the
// independently written law rather than against a monitor inside the DUT.
// A monitor inside a module whose `pad_o` is a tied constant cannot fire,
// so it could be tied off with no test noticing; see the design header.
// ----------------------------------------------------------------
for (combo = 0; combo < (1 << N); combo = combo + 1) begin
cv = combo[N-1:0]; drive_low = cv; settle;
for (i = 0; i < N; i = i + 1) begin
ck_idx("T9 never enabled with a one", combo,
law_illegal(pad_oe[i], pad_o[i]), 0);
ck_idx("T9 the pad data stayed 0", combo, pad_o[i], 0);
ck_idx("T9 contribution matches the law", combo,
pulls_low[i], law_pulls_low(pad_oe[i], pad_o[i]));
end
end
$display("T9 no drive value makes the stage ask the pad for a one");
if (errors == 0) $display("=== i2c_od_out: ALL CHECKS PASSED ===");
else $display("=== i2c_od_out: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- -----------------------------------------------------------------------------
-- i2c_od_out_tb.vhd
-- Independent oracle for i2c_od_out, on a real wired-AND bus.
-- Behavioural twin of the SystemVerilog and Verilog benches.
--
-- Two output stages hang off Module 16's `i2c_line_model` -- the same bus model
-- Modules 17 and 18 are verified against. What is proven is the chain:
--
-- drive_low -> pad_oe -> pulls_low -> wired-AND -> resolved line -> read back
--
-- Every wait is a fixed settle delay; there is no DUT-dependent wait.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_od_out_tb is
end entity i2c_od_out_tb;
architecture sim of i2c_od_out_tb is
constant N : integer := 2; -- two devices, the smallest bus that can conflict
signal drive_low : std_logic_vector(N-1 downto 0) := (others => '0');
signal pad_o : std_logic_vector(N-1 downto 0);
signal pad_oe : std_logic_vector(N-1 downto 0);
signal pulls_low : std_logic_vector(N-1 downto 0);
signal scl, sda : std_logic;
signal scl_in, sda_in : std_logic_vector(N-1 downto 0);
signal scl_rbl, sda_rbl : std_logic_vector(N-1 downto 0);
signal scl_holders, sda_holders : unsigned(7 downto 0);
signal scl_none : std_logic_vector(N-1 downto 0) := (others => '0');
begin
-- ---- two output stages, one per device -----------------------------------
g_dev : for g in 0 to N-1 generate
u_od : entity work.i2c_od_out
port map (drive_low => drive_low(g), pad_o => pad_o(g),
pad_oe => pad_oe(g), pulls_low => pulls_low(g));
end generate;
-- ---- the bus, driven by the CONTRIBUTIONS the stages computed -------------
-- Note it is `pulls_low` that is wired here, not `drive_low`: the bench must not
-- shortcut past the stage it is testing.
bus_model : entity work.i2c_line_model
generic map (N_DEV => N)
port map (scl_drive_low => scl_none, sda_drive_low => pulls_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);
stim : process
variable err : integer := 0;
variable exp_line : integer;
-- ---- the pad law, modelled independently of the DUT --------------------
-- The DUT computes its contribution from (pad_oe, pad_o). The bench must not
-- reuse the DUT's expression to check the DUT's expression, so the law is
-- written out once here and validated exhaustively in T0 before it is trusted.
function law_pulls_low (oe : std_logic; o : std_logic) return integer is
begin
if oe = '1' and o = '0' then return 1; else return 0; end if;
end function;
function law_illegal (oe : std_logic; o : std_logic) return integer is
begin
if oe = '1' and o = '1' then return 1; else return 0; end if;
end function;
function b2i (b : std_logic) return integer is
begin
if b = '1' then return 1; else return 0; end if;
end function;
procedure settle is
begin
wait for 5 ns;
end procedure;
procedure ck (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_idx (what : string; idx : integer; g : integer; e : integer) is
begin
if g /= e then
report " FAIL " & what & "[" & integer'image(idx) & "]: got "
& integer'image(g) & " expected " & integer'image(e) severity note;
err := err + 1;
end if;
end procedure;
begin
report "=== i2c_od_out: LOW or RELEASE, and nothing else ===" severity note;
-- T0. The law before the design. Every later test compares the DUT against
-- `law_pulls_low`, so the law is checked over all four (oe, o) pairs
-- first. Three of them a conforming open-drain pin reaches; the fourth --
-- enabled with a '1' -- is the short circuit, listed here so the illegal
-- case is a tested value rather than an untested assumption.
ck("T0 released, data 0: no pull", law_pulls_low('0','0'), 0);
ck("T0 released, data 1: no pull", law_pulls_low('0','1'), 0);
ck("T0 enabled with a 0: pulls low", law_pulls_low('1','0'), 1);
ck("T0 enabled with a 1: does NOT pull", law_pulls_low('1','1'), 0);
ck("T0 and enabled-with-a-1 is the illegal case", law_illegal('1','1'), 1);
ck("T0 no other pair is illegal",
law_illegal('0','0') + law_illegal('0','1') + law_illegal('1','0'), 0);
report "T0 the pad law holds for all four (oe, data) pairs" severity note;
-- T1. Drive low. Intent becomes ownership: the enable asserts, the data stays
-- '0', and the pin contributes a pull-down.
drive_low <= "01"; settle;
report "T1 drive_low=1 enables the pad with a zero on it" severity note;
ck("T1 pad_oe asserted", b2i(pad_oe(0)), 1);
ck("T1 pad_o is still 0", b2i(pad_o(0)), 0);
ck("T1 the pin pulls low", b2i(pulls_low(0)), 1);
ck("T1 and is not enabled with a one", law_illegal(pad_oe(0), pad_o(0)), 0);
-- T2. Release. The enable deasserts. Releasing is NOT driving a '1': the pin
-- stops contributing, and what the line becomes is somebody else's
-- business -- the pull-up's, or another device's.
drive_low <= "00"; settle;
report "T2 drive_low=0 releases: no enable, no contribution, no drive-high"
severity note;
ck("T2 pad_oe deasserted", b2i(pad_oe(0)), 0);
ck("T2 no pull-down", b2i(pulls_low(0)), 0);
ck("T2 still not the illegal case", law_illegal(pad_oe(0), pad_o(0)), 0);
-- T3. Both released -> the pull-up decides. This is the only mechanism that
-- ever produces a HIGH on this bus.
drive_low <= "00"; settle;
report "T3 every device released: the pull-up, and only the pull-up, makes it HIGH"
severity note;
ck("T3 the line is high", b2i(sda), 1);
ck("T3 nobody holds it", to_integer(sda_holders), 0);
-- T4. One device pulls -> external low dominates a release. Device 1 has
-- released, and it reads LOW anyway. A device that trusted its own intent
-- instead of the pin would believe the line was high.
drive_low <= "01"; settle;
report "T4 one device pulling beats every release, and the releaser sees it"
severity note;
ck("T4 the line is low", b2i(sda), 0);
ck("T4 exactly one holder", to_integer(sda_holders), 1);
ck("T4 the puller reads low", b2i(sda_in(0)), 0);
ck("T4 the RELEASER also reads low", b2i(sda_in(1)), 0);
ck("T4 and device 1 is not driving", b2i(pulls_low(1)), 0);
-- T5. Two devices pull -> still LOW, and no conflict. This is the difference
-- between a wired-AND bus and a push-pull net: simultaneous LOWs are legal
-- and are how ACK and arbitration work.
drive_low <= "11"; settle;
report "T5 two devices pulling together is legal, not a conflict" severity note;
ck("T5 the line is low", b2i(sda), 0);
ck("T5 two holders", to_integer(sda_holders), 2);
ck("T5 neither is enabled with a one",
law_illegal(pad_oe(0),pad_o(0)) + law_illegal(pad_oe(1),pad_o(1)), 0);
-- T6. A release while another device holds does not raise the line. Device 0
-- lets go; device 1 is still pulling; the line stays down. If releasing
-- drove a '1' this is where the short circuit would appear.
drive_low <= "10"; settle;
report "T6 releasing is not driving high: the line stays down" severity note;
ck("T6 the line is still low", b2i(sda), 0);
ck("T6 one holder left", to_integer(sda_holders), 1);
ck("T6 the releaser reads low", b2i(sda_in(0)), 0);
-- T7. Exhaustive over every drive combination. Four cases at N=2, and the
-- resolved level must equal the AND of the releases every time.
for combo in 0 to (2**N)-1 loop
drive_low <= std_logic_vector(to_unsigned(combo, N));
settle;
if combo = 0 then exp_line := 1; else exp_line := 0; end if;
ck_idx("T7 resolved level is the wired-AND", combo, b2i(sda), exp_line);
ck_idx("T7 holder count matches", combo, to_integer(sda_holders),
(combo mod 2) + ((combo / 2) mod 2));
for i in 0 to N-1 loop
ck_idx("T7 contribution equals intent", combo,
b2i(pulls_low(i)), (combo / (2**i)) mod 2);
-- Every device reads the RESOLVED line, not its own intent.
ck_idx("T7 readback is the bus, not the intent", combo,
b2i(sda_in(i)), exp_line);
end loop;
end loop;
report "T7 all drive combinations resolve as a wired-AND" severity note;
-- T8. The intent and the pin legitimately disagree, and that is normal.
-- Device 0 has released while the line is LOW. Any logic that substituted
-- its own drive intent for the pin would be wrong here, and that is the
-- substitution Chapter 19.7 catalogues as a mismatch which passes in
-- simulation on an ideal bus.
drive_low <= "10"; settle;
report "T8 intent and pin legitimately disagree -- read the pin" severity note;
ck("T8 device 0 intends to release", b2i(pulls_low(0)), 0);
ck("T8 but the pin reads low", b2i(sda_in(0)), 0);
-- A device substituting intent for observation would conclude "I released, so
-- the line must be HIGH". Assert that guess and the truth actually differ.
ck("T8 the intent-based guess is HIGH and the pin disagrees",
b2i(not pulls_low(0)) - b2i(sda_in(0)), 1);
-- T9. The stage never reaches the illegal combination. Swept over every drive
-- value, `law_illegal(pad_oe, pad_o)` must stay 0 for every device --
-- the "never actively drives HIGH" requirement, checked against the
-- independently written law rather than a monitor inside the DUT. A monitor
-- inside a module whose `pad_o` is a tied constant cannot fire, so it could
-- be tied off with no test noticing; see the design header.
for combo in 0 to (2**N)-1 loop
drive_low <= std_logic_vector(to_unsigned(combo, N));
settle;
for i in 0 to N-1 loop
ck_idx("T9 never enabled with a one", combo,
law_illegal(pad_oe(i), pad_o(i)), 0);
ck_idx("T9 the pad data stayed 0", combo, b2i(pad_o(i)), 0);
ck_idx("T9 contribution matches the law", combo,
b2i(pulls_low(i)), law_pulls_low(pad_oe(i), pad_o(i)));
end loop;
end loop;
report "T9 no drive value makes the stage ask the pad for a one" severity note;
if err = 0 then
report "=== i2c_od_out: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_od_out: " & integer'image(err) & " CHECK(S) FAILED ==="
severity note;
end if;
wait;
end process;
end architecture sim;6. The Timing Picture
Two open-drain devices resolving one line
8 cycles7. What the Mutations Found
Nine mutations were injected into the design and the bench. Eight were killed; the ninth is equivalent and is proven so rather than argued away.
| # | mutation | verdict | what the killing check was |
|---|---|---|---|
| A01 | pad_oe = ~drive_low | KILLED (27) | the enable inverts — the device holds the line whenever it means to release |
| A02 | pad_o = 1'b1 | KILLED (43) | the pad law turns a driven 1 into "not pulling", so the device stops holding the line |
| A03 | pad_oe = 1'b1 | KILLED (19) | never releases |
| A04 | pad_oe = 1'b0 | KILLED (29) | never drives |
| A05 | pulls_low = pad_oe | EQUIVALENT | see below |
| A06 | pulls_low = ~pad_o | KILLED (22) | contribution ignores ownership |
| A07 | pulls_low = ~(pad_oe & ~pad_o) | KILLED (33) | contribution inverted |
| A08 | bench: law_pulls_low = oe | KILLED (1) | T0 catches a weakened oracle |
| A09 | bench: law_illegal = 1'b0 | KILLED (1) | T0 catches a disabled illegal-case check |
A01, A02, A04 and A07 were injected separately into all three languages and killed with identical failure counts in each — 27, 43, 29 and 33 — which is the strongest parity evidence available short of formal equivalence.
A08 and A09 are mutations of the testbench, not the design, and they exist because of a failure mode that a design-only mutation set cannot see: an oracle can be quietly weakened. Tying law_illegal to zero would disable the never-drives-high check everywhere it is used. T0 fails immediately, which is what makes the rest of the bench's use of that law trustworthy.
8. What Synthesis Does With This — and What This Chapter Cannot Claim
With that boundary stated, the expectation is:
Style C plus a boundary stage is the portable answer. The core carries drive_low. One stage at the top level turns it into whatever the target needs. Only that stage changes when the target changes, and it is small enough to read.
A Z assigned to a top-level inout port is the case tools handle well. It is the idiom vendors document, and it is the one their inference is written for.
A Z assigned to an internal net is the case that bites. FPGA fabric has no internal tri-state routing. A tool meeting an internal tri-state net must either convert it to multiplexers — changing the timing and the resource count — or refuse. Which of those happens depends on the tool and the device family, and that dependence is the reason not to write it: identical RTL with different behaviour across tools is not portable RTL.
"What synthesis infers" is not a property of your HDL alone. It is a property of your HDL, the tool, the device family and the constraints. A design whose correctness rests on an inference is a design whose correctness rests on all four.
Chapter 19.2 builds the boundary stage, names the primitives each vendor uses, and keeps the vendor-specific parts clearly labelled as such.
9. Focused Verification Insight
Module 20 owns I²C verification architecture — agents, monitors, scoreboards, the test plan. Two things belong here, because they are about this layer and would be awkward to retrofit later.
A monitor must observe the pin, never the intent. T8 is the executable form of this. A monitor built on a DUT's drive_low output would report a clean bus at the exact moment the bus is contended, because drive intent is the one thing that cannot tell you what the line became. Module 20's monitor takes the resolved level; the reason is here.
Coverage that is worth collecting at this layer is about ownership, not values: each line driven LOW and released; both lines, not just SDA; more than one device holding simultaneously; a device reading LOW while it is itself released. That last bin is the one that distinguishes a bench that has exercised the wired-AND from one that has only ever had a single active device.
An assertion worth writing — as a concept here; Chapter 19.2 has the version that can actually be violated:
// Shown as a property, not run: Icarus Verilog does not support concurrent
// assertions, so nothing in this chapter's verified set uses them. The equivalent
// checks are performed procedurally by the bench above -- T9 is this property,
// sampled every settle instead of every clock.
// The pad is never enabled with a one on it.
property p_never_drives_high;
@(posedge clk) not (pad_oe && pad_o);
endproperty
// A release removes this device's contribution -- it does not raise the line.
property p_release_is_not_a_drive;
@(posedge clk) (!drive_low) |-> (!pulls_low);
endproperty10. Misconceptions
11. Debugging
SDA never falls at the pin, and the RTL simulation is perfect
Pitfall — the output enable inverted at the one place the mapping is made
// The protocol engine is Module 18's verified target. Its bus interface is two
// drive-intent outputs, and the board wrapper converts them to pad signals:
//
// assign sda_pad_oe = ~sda_drive_low; // <-- the whole bug
// assign sda_pad_o = 1'b0;
//
// The inversion was not a typo. It was written by someone who reasoned about the
// signal NAME rather than the signal: an active-low bus line is "low = asserted",
// and pad enables on the previous project in this codebase were active-low. Both
// halves of that sentence are true. Neither applies to an output enable, which is
// asserted HIGH to mean "drive".
//
// Every RTL simulation still passes, because every RTL testbench in Modules 17
// and 18 drives the bus model from drive_low directly and never instantiates the
// wrapper. The inverted line is not in the simulated path.On the board: SDA sits LOW permanently from the moment the FPGA leaves reset. SCL looks correct and clocks normally. No transfer ever completes; the controller reports arbitration lost on its first attempt, every attempt.
An external analyser shows SDA held down continuously with no framing at all. The ILA shows the target's internal state machine in IDLE, selected deasserted, sda_drive_low deasserted -- that is, the logic believes it is not touching SDA.
The two views disagree, and that disagreement is the whole diagnosis: the logic's belief about what it is driving and the pin's actual state are separated by exactly one line of code, and that line is outside everything that was verified.
sda_pad_oe = ~sda_drive_low means the pad is ENABLED whenever the engine wants to RELEASE. Since an idle target releases SDA continuously, the pad is enabled continuously with a 0 on it, so the device holds the bus down forever -- including before the first transfer, which is why nothing ever starts.
Note the failure is not intermittent and not timing-dependent. It is total, from power-on, which is actually the good case: an inverted enable that only asserted occasionally would present as rare unexplained arbitration losses.
The fix is to delete the inversion. The more useful fix is structural: there should be exactly one place in the design where drive intent becomes an output enable, it should be the module in Section 4, and it should be instantiated rather than open-coded in a wrapper -- because an open-coded mapping is a mapping nobody tests.
The evidence that would have caught it in simulation is the bench in Section 5: it drives the bus model from pulls_low, through the stage, rather than from drive_low around it. Mutation A01 is this exact bug, and it fails 27 checks.
12. Reason It Through
13. Questions
14. What This Chapter Settled
The protocol engine emits one bit of intent per line. One small stage turns that intent into the two signals a pad consumes, and the value it drives is always zero. The contribution to the bus follows the pad law, enabled ∧ data = 0, and the resolved level is produced by everyone's contributions together with the pull-up — never by any device driving a one.
Verified, in all three languages, against the same wired-AND bus model the controller and the target were verified against. Nine mutations, eight killed, one proven equivalent, and one output deleted because it turned out to be a monitor that could not fire.
What is still missing is the pad itself. pad_o and pad_oe currently go nowhere: there is no I/O buffer, no package pin, and no account of why an internal tri-state net is a different proposition from a top-level one. That is Chapter 19.2, and it is also where the drives_high property finds a home where it can actually be violated.
Continue learning
Related tutorials
- Related topic
Tri-State Buffers and FPGA I/O Primitives
What sits between an RTL signal and a package pin: the data path, the enable, the always-live input buffer and the pad. Builds the vendor-neutral wrapper an I²C core talks to, shows why an internal tri-state net is a different proposition from a top-level one, and why the one signal every vendor spells differently is the one most likely to be inverted.
- Related topic
Open-Drain Outputs — Drive Low, Release High
The architectural move the whole bus rests on, and it is a subtraction: delete every device's ability to drive HIGH. What remains is one switch to ground, so the two states are pull LOW and release — and two devices can never impose opposite levels because only one level can be imposed at all.
- Related topic
FPGA as Master and as Target — Integration Patterns
Assembles everything: Module 18's verified target behind Module 19's pad, synchronizer and filter, proven end to end on a wired-AND bus in three languages. Shows the controller-side shape on Module 17's real interface, where a soft CPU attaches, and closes with two mutations that cannot be killed in simulation — the module's thesis stated as evidence.
- Related topic
Hardware Bring-Up and On-Chip Debug
What to do on the morning the board arrives, in what order, with what instrument. Builds a synthesizable bus-health block that answers 'is the bus even there' at a glance, works through a probe set that spans layers rather than concentrating on the FSM, and shows why an analyzer and an ILA disagreeing is the most localising evidence available.
