Skip to content
VLSI Mentor

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 outputopen-drain output
statesdrives 0, drives 1drives 0, releases
the bit meansthe value on the wirewhether I am holding the wire
two devices assertingshort circuitlegal, and load-bearing
who makes it HIGHthe driverthe 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

Azvya Education Pvt. Ltd.VLSI Mentor
style_a_resolved_pin.sv — the pin as a resolved net (ILLUSTRATIVE, not the core's language)
   // 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

Azvya Education Pvt. Ltd.VLSI Mentor
style_b_oe_pair.sv — what a bidirectional pad actually consumes (ILLUSTRATIVE)
   // 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 it

This 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
drive_low = 1    →  I want this line LOW
drive_low = 0    →  I have no claim on this line

One 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 1

Both 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.

A five-stage left-to-right block diagram. A protocol engine emits drive_low into an open-drain output stage. The stage emits a constant zero data value and an output enable into a pad. The pad contributes a pull-down to a wired-AND bus node, which also receives a pull-up above it and other devices below it. The bus node produces the resolved level on the right.Protocol engine17 / 18 — drive_lowOpen-drainstage19.1 — this chapterPad19.2 — o = 0, oeBus nodewired-ANDPull-up19.3 — makes HIGHOther devicesmay hold it LOWResolved levelwhat everyone readsdrive_lowo, oepulls_low12
Figure 1 — the layers this chapter separates, and the one equation joining the middle two. Drive intent is what the protocol wants; the enable is what the pad is told; the contribution is what the net receives; the resolved level is what the wire became. The return path is left out of the drawing on purpose and is the subject of Figure 2: every device reads the resolved level, never its own intent.

4. The Output Stage

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_od_out.sv — intent becomes ownership
   // -----------------------------------------------------------------------------
   // 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;

   endmodule

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_od_out.v — the same design in Verilog-2001
   // -----------------------------------------------------------------------------
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_od_out.vhd — the same design in VHDL
   -- -----------------------------------------------------------------------------
   -- 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.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_od_out_tb.sv — the self-checking testbench
   // -----------------------------------------------------------------------------
   // 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

   endmodule

Three 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.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_od_out_tb.v — the same tests in Verilog-2001
   // -----------------------------------------------------------------------------
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_od_out_tb.vhd — the same tests in VHDL
   -- -----------------------------------------------------------------------------
   -- 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 cycles
A seven-cycle waveform. Device 0's drive_low is high in cycles 1 to 3 and low elsewhere. Device 0's pad output enable follows it exactly. Device 1's drive_low is high in cycles 5 and 6. The resolved SDA line is low whenever either drive_low is high, and high in cycles 0, 4 and 7.device 0 owns the linedevice 0 owns thelinedevice 1 owns itdevice 1owns itdev0 pullsdev0 pullsboth released → pull-upboth released → pull-updev1 pulls; dev0 reads LOWdev1 pulls; dev0 reads LOWclkdev0.drive_lowdev0.pad_oedev0.pulls_lowdev1.drive_lowSDA (resolved)dev0.reads10001001t0t1t2t3t4t5t6t7
Cycle 5: device 0's intent says 'released' and its readback says LOW. Both are correct, and any logic that cannot hold both at once has a bug.
Figure 2 — SIMULATED. Two devices on one line. Device 0's intent, its enable and the resolved level, with device 1 pulling in cycles 6 and 7. The line is LOW whenever either device pulls, and HIGH only in the cycles where both have released. Cycle 7 is the one to read twice: device 0 has released, and the line is still LOW.

7. 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.

#mutationverdictwhat the killing check was
A01pad_oe = ~drive_lowKILLED (27)the enable inverts — the device holds the line whenever it means to release
A02pad_o = 1'b1KILLED (43)the pad law turns a driven 1 into "not pulling", so the device stops holding the line
A03pad_oe = 1'b1KILLED (19)never releases
A04pad_oe = 1'b0KILLED (29)never drives
A05pulls_low = pad_oeEQUIVALENTsee below
A06pulls_low = ~pad_oKILLED (22)contribution ignores ownership
A07pulls_low = ~(pad_oe & ~pad_o)KILLED (33)contribution inverted
A08bench: law_pulls_low = oeKILLED (1)T0 catches a weakened oracle
A09bench: law_illegal = 1'b0KILLED (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:

Azvya Education Pvt. Ltd.VLSI Mentor
od_properties.sv — assertion CONCEPT, not executed in this chapter
   // 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);
   endproperty

10. 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
Buggy Code
// 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.
Symptom

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.

Root Cause

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