Skip to content
VLSI Mentor

I²C · Module 19

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.

Chapter 19.1 ended with two signals going nowhere. pad_o is a constant zero, pad_oe says whether this device is holding the line, and nothing in the design so far connects either of them to anything physical.

This chapter connects them. It also settles the question 19.1 raised and deferred: why writing 1'bz inside your design and writing it at a port are two different acts with two different outcomes.

1. Four Signals, Whatever the Vendor Calls Them

A bidirectional pin is not one signal. It is a small circuit with a fixed shape, and every FPGA family implements the same shape:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   fabric                     I/O block                    package pin
   ──────                     ─────────                    ───────────

   data out  ────────────▶ ┌───────────┐
                           │  driver   │──────────┐
   enable    ────────────▶ └───────────┘          │
                                                  ├──────────▶  ◉  pad
   data in   ◀──────────── ┌───────────┐          │
                           │  receiver │◀─────────┘
                           └───────────┘

Three of those four are worth pausing on.

The driver has an enable, and the enable is the whole point. Without it a pin can only be an output. With it, the pin can stop driving — which is what makes the pin bidirectional and what makes open drain possible.

The receiver is not switched off while the driver is active. It sits on the pad, permanently, watching whatever the pad's voltage actually is. This matters more than it looks, and Section 5 is about it.

The pad is shared. Everything past the pad is outside your chip: the trace, the other devices, the pull-up. Your design's authority ends at the pad, and this is exactly where Modules 17 and 18 stopped.

2. The Wrapper

The core should not know which vendor it is compiled for. So one module stands between them, and it is the only file that changes when the target changes.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_io_wrapper.sv — the vendor-neutral boundary
   // -----------------------------------------------------------------------------
   // i2c_io_wrapper.sv
   // The vendor-neutral boundary between a protocol engine and a bidirectional pin.
   //
   // Chapter 19.1 produced `pad_o` and `pad_oe` and left them going nowhere. This is
   // where they go. The wrapper owns three things and nothing else:
   //
   //   1. the OUTPUT path      -- data and enable, driven onto the pin
   //   2. the INPUT path       -- the resolved pin level, read back unconditionally
   //   3. the LEGALITY CHECK   -- the combination a conforming open-drain pin must
   //                              never present, which is now checkable because
   //                              `pad_o` arrives from OUTSIDE this module
   //
   // WHY `pad_o` IS AN INPUT AND NOT A CONSTANT. In 19.1 the data was tied to 0 inside
   // the output stage, which made `pad_oe & pad_o` provably constant zero -- a monitor
   // that cannot fire, so tying it off was undetectable and it was deleted. Here the
   // caller supplies `pad_o`, so a wrong value is reachable, observable and tested. The
   // property did not change; the place where it can be violated did.
   //
   // WHY THE INPUT PATH IS UNCONDITIONAL. `pin_level` is not gated by `pad_oe`. A
   // conforming device reads the line at all times, including while it is driving --
   // that is how Module 17 detects arbitration loss and how Module 18 sees a clock
   // stretch from a third device. An input buffer gated by the output enable is a real
   // and popular bug, and mutation B05 is exactly it.
   //
   // WHAT THIS MODULE IS NOT. It is not a vendor primitive and it does not instantiate
   // one. It is the portable shape every vendor's bidirectional buffer presents, so the
   // core above it never changes when the target does. The chapter body maps this
   // interface onto AMD/Xilinx, Intel/Altera and Lattice equivalents and labels each
   // mapping as vendor-specific, unexecuted reference material.
   // -----------------------------------------------------------------------------

   module i2c_io_wrapper (
      // ---- from the open-drain output stage (Chapter 19.1) ---------------------
      // Data to drive. For a conforming I²C pin this is always 0; it is an INPUT here
      // so that a wrong value is expressible and therefore testable.
      input  logic pad_o,
      // Output enable. 1 = drive `pad_o` onto the pin, 0 = release to high impedance.
      input  logic pad_oe,

      // ---- the pin, as a two-signal model -------------------------------------
      // What this device contributes to the shared net: it holds the line down when it
      // is enabled with a zero. This is the same pad law Chapter 19.1 states, applied
      // once, here, at the boundary.
      output logic pin_pulls_low,
      // The resolved level of the shared net, from the bus. Read UNCONDITIONALLY.
      input  logic pin_resolved,

      // ---- to the protocol engine ---------------------------------------------
      // What the line IS, as far as this device can know. Chapter 19.4 is what must
      // happen to this signal before any logic is allowed to act on it.
      output logic pin_level,

      // ---- the legality monitor, now able to fire ------------------------------
      // Asserted while the caller asks the pad to drive a 1. MUST STAY 0 for every
      // conforming driver. Unlike 19.1's deleted version this depends on an input, so
      // a test can make it assert -- and T6 does.
      output logic drives_high
   );

      // The pad law: a pad pulls its net down when it is enabled and its data is 0.
      assign pin_pulls_low = pad_oe & ~pad_o;

      // The illegal combination: enabled and driving a one.
      assign drives_high   = pad_oe &  pad_o;

      // The input path. No gating, no enable term, no condition.
      assign pin_level     = pin_resolved;

   endmodule

Two decisions in that file are the chapter, and both were forced by evidence rather than taste.

pad_o is an input, and that is what makes the legality check real

Chapter 19.1 shipped a drives_high monitor, then deleted it. The reason was not that the property was wrong — it is the most important property an open-drain driver has. The reason was that inside 19.1 the property was unfalsifiable: pad_o was tied to zero, so pad_oe & pad_o was provably constant, and a mutation tying the monitor to zero passed every test.

Here pad_o arrives from outside. A wrong value is expressible, so the monitor can fire, so a test can prove it fires — and one does.

The input path has no enable term

pin_level is pin_resolved. No condition, no gating, no pad_oe anywhere near it.

The temptation to gate it is real, and it comes from a plausible-sounding argument: while I am driving the pin, reading it back tells me only what I am driving, so I may as well not bother. Every clause of that is wrong for I²C.

A device on an I²C bus must read the line while it is driving, because:

  • a controller detects arbitration loss by driving a 1 (that is, releasing) and finding the line LOW — Module 17's entire mechanism;
  • a target discovers a clock stretch by a third device the same way;
  • a controller confirms a target's ACK by releasing SDA and looking at what the target did with it.

Gating the input buffer with the output enable breaks all three, and it breaks them silently — the design still works perfectly whenever it happens to be the only active device, which includes most of a simple bench. Mutation B05 is exactly this bug, and Section 4 shows which test catches it.

3. Verifying a Boundary From Both Sides

The wrapper has a protocol side and a bus side, so the bench drives both at once: 19.1's output stage supplies the pad signals, Module 16's i2c_line_model resolves the net, and a second device provides the traffic this device has to observe while it is itself driving.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_io_wrapper_tb.sv — the self-checking testbench
   // -----------------------------------------------------------------------------
   // i2c_io_wrapper_tb.sv
   // Independent oracle for i2c_io_wrapper, on the shared wired-AND bus.
   //
   // The wrapper is the boundary, so the bench exercises it from both sides at once:
   // Chapter 19.1's output stage feeds it, Module 16's `i2c_line_model` resolves the
   // net, and a second device provides the traffic this device must observe while
   // driving. What is proven is the full round trip:
   //
   //   drive_low -> pad_o/pad_oe -> pin_pulls_low -> wired-AND -> pin_level
   //
   // T6 is the test 19.1 could not write: it drives the illegal combination directly
   // into the wrapper's `pad_o` input and proves the legality monitor fires.
   //
   // Every wait is a fixed settle delay; there is no DUT-dependent wait.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_io_wrapper_tb;

      localparam int N = 2;          // device 0 is the DUT's pin, device 1 is traffic

      // device 0: the full path -- output stage feeding the wrapper
      logic        drive_low;
      logic        od_pad_o, od_pad_oe, od_pulls_low;
      logic        pin_pulls_low, pin_level, drives_high;

      // the wrapper's data input, so the bench can force the illegal case in T6
      logic        force_bad_data;
      wire         wrap_pad_o = od_pad_o | force_bad_data;

      // device 1: an ordinary other device, driven directly by the bench
      logic        other_low;

      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 combo;

      // ---- device 0's output stage (Chapter 19.1, reused unchanged) ---------------
      i2c_od_out u_od (
         .drive_low(drive_low), .pad_o(od_pad_o), .pad_oe(od_pad_oe),
         .pulls_low(od_pulls_low));

      // ---- the boundary under test -----------------------------------------------
      i2c_io_wrapper dut (
         .pad_o(wrap_pad_o), .pad_oe(od_pad_oe),
         .pin_pulls_low(pin_pulls_low), .pin_resolved(sda),
         .pin_level(pin_level), .drives_high(drives_high));

      // ---- the bus: device 0 through the wrapper, device 1 direct ----------------
      i2c_line_model #(.N_DEV(N)) bus (
         .scl_drive_low({N{1'b0}}),
         .sda_drive_low({other_low, pin_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));

      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_io_wrapper: the boundary, from both sides ===");
         force_bad_data = 1'b0;

         // ----------------------------------------------------------------
         // T1. THE OUTPUT PATH. Intent reaches the net through the wrapper, and the
         //     wrapper's contribution agrees with the output stage's -- they are the
         //     same law applied at two layers, so a disagreement means one is wrong.
         // ----------------------------------------------------------------
         drive_low = 1'b1; other_low = 1'b0; settle;
         $display("T1  a drive reaches the net through the boundary");
         ck("T1 the wrapper pulls low",         pin_pulls_low, 1);
         ck("T1 agreeing with the stage",       od_pulls_low,  1);
         ck("T1 the line went low",             sda, 0);
         ck("T1 and nothing drove high",        drives_high, 0);

         // ----------------------------------------------------------------
         // T2. RELEASE. The contribution disappears and the pull-up takes the line.
         // ----------------------------------------------------------------
         drive_low = 1'b0; other_low = 1'b0; settle;
         $display("T2  a release removes the contribution and the pull-up wins");
         ck("T2 no contribution", pin_pulls_low, 0);
         ck("T2 the line is high", sda, 1);
         ck("T2 the engine observes high", pin_level, 1);

         // ----------------------------------------------------------------
         // T3. THE INPUT PATH IS UNCONDITIONAL -- READ WHILE RELEASED. Another device
         //     pulls; this device is not driving; it must see the LOW.
         // ----------------------------------------------------------------
         drive_low = 1'b0; other_low = 1'b1; settle;
         $display("T3  released and reading: somebody else's low is visible");
         ck("T3 we are not driving",   pin_pulls_low, 0);
         ck("T3 the line is low",      sda, 0);
         ck("T3 and we observe it",    pin_level, 0);

         // ----------------------------------------------------------------
         // T4. THE INPUT PATH IS UNCONDITIONAL -- READ WHILE DRIVING. This is the test
         //     that matters. This device is driving LOW; so is the other one; the
         //     readback must still be live. A design that gated the input buffer with
         //     the output enable passes T1..T3 and fails here, and on hardware it
         //     presents as a controller that never notices arbitration loss.
         // ----------------------------------------------------------------
         drive_low = 1'b1; other_low = 1'b1; settle;
         $display("T4  driving and reading at the same time: both devices hold it");
         ck("T4 we are driving",           pin_pulls_low, 1);
         ck("T4 two holders",              sda_holders, 2);
         ck("T4 the readback is still live", pin_level, 0);

         // ----------------------------------------------------------------
         // T5. WE RELEASE, THE OTHER KEEPS HOLDING. Our intent says released; the pin
         //     says LOW; the readback must follow the pin. Chapter 19.1's T8, now
         //     through the real boundary.
         // ----------------------------------------------------------------
         drive_low = 1'b0; other_low = 1'b1; settle;
         $display("T5  our intent and the pin disagree, and the readback follows the pin");
         ck("T5 our contribution is gone", pin_pulls_low, 0);
         ck("T5 the pin is low",           pin_level, 0);
         ck("T5 one holder, and it is not us", sda_holders, 1);

         // ----------------------------------------------------------------
         // T6. THE ILLEGAL COMBINATION, DRIVEN ON PURPOSE. `pad_o` is an input here, so
         //     the bench can present enabled-with-a-one -- the case Chapter 19.1 could
         //     not express. Two things must happen: the monitor must fire, and the
         //     contribution must NOT claim a pull-down, because a pad driving a 1 is
         //     not holding the line low. This is the false-positive test that makes the
         //     monitor trustworthy: a check never shown to fire cannot be relied on.
         // ----------------------------------------------------------------
         drive_low = 1'b1; other_low = 1'b0; force_bad_data = 1'b1; settle;
         $display("T6  enabled with a one: the monitor fires, as a monitor must be able to");
         ck("T6 the monitor asserts",        drives_high, 1);
         ck("T6 the data really is a one",   wrap_pad_o, 1);
         ck("T6 and it is NOT pulling low",  pin_pulls_low, 0);
         force_bad_data = 1'b0; settle;
         ck("T6 removing it clears the monitor", drives_high, 0);
         ck("T6 and the pull-down returns",      pin_pulls_low, 1);

         // ----------------------------------------------------------------
         // T7. EXHAUSTIVE OVER THE BOUNDARY'S INPUT SPACE. Three inputs --
         //     (pad_oe via drive_low, pad_o via force_bad_data, the other device) --
         //     so eight combinations, checked against the law rather than sampled.
         //     `pin_level` must equal the resolved line in every one of them.
         // ----------------------------------------------------------------
         for (combo = 0; combo < 8; combo = combo + 1) begin
            drive_low      = combo[0];
            force_bad_data = combo[1];
            other_low      = combo[2];
            settle;
            // NOTE the expectations are written as conditionals, not as `~expr`. A
            // one-bit expression passed to an `integer` argument is evaluated in
            // 32-bit context, so `~` inverts all thirty-two bits and the expected
            // value arrives as -1 or -2. The first draft of this bench did exactly
            // that and reported eight failures against a correct design.
            ck_idx("T7 pull-down is enabled-and-zero", combo,
                   pin_pulls_low, ((combo[0] == 1 && combo[1] == 0) ? 1 : 0));
            ck_idx("T7 monitor is enabled-and-one",    combo,
                   drives_high,   ((combo[0] == 1 && combo[1] == 1) ? 1 : 0));
            ck_idx("T7 the two are mutually exclusive", combo,
                   ((pin_pulls_low === 1'b1 && drives_high === 1'b1) ? 1 : 0), 0);
            // The readback tracks the net, whatever this device is doing to it.
            ck_idx("T7 readback equals the resolved line", combo, pin_level, sda);
            ck_idx("T7 the line is high iff nobody pulls", combo, sda,
                   ((((combo[0] == 1 && combo[1] == 0) || combo[2] == 1)) ? 0 : 1));
         end
         $display("T7  all eight boundary input combinations obey the pad law");

         if (errors == 0) $display("=== i2c_io_wrapper: ALL CHECKS PASSED ===");
         else             $display("=== i2c_io_wrapper: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule

Three tests carry the weight.

T4 — driving and reading at the same time. Both devices hold the line; this device is one of them; pin_level must still be live. T1 through T3 all pass with a gated input buffer. T4 does not.

T6 — the illegal combination, on purpose. The bench forces pad_o high while the pad is enabled, and requires two things: the monitor asserts, and pin_pulls_low goes false — because a pad driving a 1 is not holding the line down. Then it removes the fault and requires both to return. That last half is what makes it a real false-positive test rather than a one-way assertion.

T7 — exhaustive over the boundary's inputs. Three inputs, eight combinations, no sampling. Every one is checked against the pad law, and pin_level is checked to equal the resolved line in all eight — including the two where this device is the one making the line low.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_io_wrapper.v — the same design in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_io_wrapper.v
   // The vendor-neutral boundary between a protocol engine and a bidirectional pin.
   //
   // Chapter 19.1 produced `pad_o` and `pad_oe` and left them going nowhere. This is
   // where they go. The wrapper owns three things and nothing else:
   //
   //   1. the OUTPUT path      -- data and enable, driven onto the pin
   //   2. the INPUT path       -- the resolved pin level, read back unconditionally
   //   3. the LEGALITY CHECK   -- the combination a conforming open-drain pin must
   //                              never present, which is now checkable because
   //                              `pad_o` arrives from OUTSIDE this module
   //
   // WHY `pad_o` IS AN INPUT AND NOT A CONSTANT. In 19.1 the data was tied to 0 inside
   // the output stage, which made `pad_oe & pad_o` provably constant zero -- a monitor
   // that cannot fire, so tying it off was undetectable and it was deleted. Here the
   // caller supplies `pad_o`, so a wrong value is reachable, observable and tested. The
   // property did not change; the place where it can be violated did.
   //
   // WHY THE INPUT PATH IS UNCONDITIONAL. `pin_level` is not gated by `pad_oe`. A
   // conforming device reads the line at all times, including while it is driving --
   // that is how Module 17 detects arbitration loss and how Module 18 sees a clock
   // stretch from a third device. An input buffer gated by the output enable is a real
   // and popular bug, and mutation B05 is exactly it.
   //
   // WHAT THIS MODULE IS NOT. It is not a vendor primitive and it does not instantiate
   // one. It is the portable shape every vendor's bidirectional buffer presents, so the
   // core above it never changes when the target does. The chapter body maps this
   // interface onto AMD/Xilinx, Intel/Altera and Lattice equivalents and labels each
   // mapping as vendor-specific, unexecuted reference material.
   // -----------------------------------------------------------------------------

   module i2c_io_wrapper (
      // ---- from the open-drain output stage (Chapter 19.1) ---------------------
      // Data to drive. For a conforming I²C pin this is always 0; it is an INPUT here
      // so that a wrong value is expressible and therefore testable.
      input  wire  pad_o,
      // Output enable. 1 = drive `pad_o` onto the pin, 0 = release to high impedance.
      input  wire  pad_oe,

      // ---- the pin, as a two-signal model -------------------------------------
      // What this device contributes to the shared net: it holds the line down when it
      // is enabled with a zero. This is the same pad law Chapter 19.1 states, applied
      // once, here, at the boundary.
      output wire  pin_pulls_low,
      // The resolved level of the shared net, from the bus. Read UNCONDITIONALLY.
      input  wire  pin_resolved,

      // ---- to the protocol engine ---------------------------------------------
      // What the line IS, as far as this device can know. Chapter 19.4 is what must
      // happen to this signal before any logic is allowed to act on it.
      output wire  pin_level,

      // ---- the legality monitor, now able to fire ------------------------------
      // Asserted while the caller asks the pad to drive a 1. MUST STAY 0 for every
      // conforming driver. Unlike 19.1's deleted version this depends on an input, so
      // a test can make it assert -- and T6 does.
      output wire  drives_high
   );

      // The pad law: a pad pulls its net down when it is enabled and its data is 0.
      assign pin_pulls_low = pad_oe & ~pad_o;

      // The illegal combination: enabled and driving a one.
      assign drives_high   = pad_oe &  pad_o;

      // The input path. No gating, no enable term, no condition.
      assign pin_level     = pin_resolved;

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_io_wrapper_tb.v — the same tests in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_io_wrapper_tb.v
   // Independent oracle for i2c_io_wrapper, on the shared wired-AND bus.
   //
   // The wrapper is the boundary, so the bench exercises it from both sides at once:
   // Chapter 19.1's output stage feeds it, Module 16's `i2c_line_model` resolves the
   // net, and a second device provides the traffic this device must observe while
   // driving. What is proven is the full round trip:
   //
   //   drive_low -> pad_o/pad_oe -> pin_pulls_low -> wired-AND -> pin_level
   //
   // T6 is the test 19.1 could not write: it drives the illegal combination directly
   // into the wrapper's `pad_o` input and proves the legality monitor fires.
   //
   // Every wait is a fixed settle delay; there is no DUT-dependent wait.
   //
   // (Verilog-2001 -- the same tests as the SystemVerilog bench.)
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_io_wrapper_tb;

      localparam N = 2;          // device 0 is the DUT's pin, device 1 is traffic

      // device 0: the full path -- output stage feeding the wrapper
      reg          drive_low;
      wire         od_pad_o, od_pad_oe, od_pulls_low;
      wire         pin_pulls_low, pin_level, drives_high;

      // the wrapper's data input, so the bench can force the illegal case in T6
      reg          force_bad_data;
      wire         wrap_pad_o = od_pad_o | force_bad_data;

      // device 1: an ordinary other device, driven directly by the bench
      reg          other_low;

      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 combo;

      // ---- device 0's output stage (Chapter 19.1, reused unchanged) ---------------
      i2c_od_out u_od (
         .drive_low(drive_low), .pad_o(od_pad_o), .pad_oe(od_pad_oe),
         .pulls_low(od_pulls_low));

      // ---- the boundary under test -----------------------------------------------
      i2c_io_wrapper dut (
         .pad_o(wrap_pad_o), .pad_oe(od_pad_oe),
         .pin_pulls_low(pin_pulls_low), .pin_resolved(sda),
         .pin_level(pin_level), .drives_high(drives_high));

      // ---- the bus: device 0 through the wrapper, device 1 direct ----------------
      i2c_line_model #(.N_DEV(N)) bus (
         .scl_drive_low({N{1'b0}}),
         .sda_drive_low({other_low, pin_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));

      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_io_wrapper: the boundary, from both sides ===");
         force_bad_data = 1'b0;

         // ----------------------------------------------------------------
         // T1. THE OUTPUT PATH. Intent reaches the net through the wrapper, and the
         //     wrapper's contribution agrees with the output stage's -- they are the
         //     same law applied at two layers, so a disagreement means one is wrong.
         // ----------------------------------------------------------------
         drive_low = 1'b1; other_low = 1'b0; settle;
         $display("T1  a drive reaches the net through the boundary");
         ck("T1 the wrapper pulls low",         pin_pulls_low, 1);
         ck("T1 agreeing with the stage",       od_pulls_low,  1);
         ck("T1 the line went low",             sda, 0);
         ck("T1 and nothing drove high",        drives_high, 0);

         // ----------------------------------------------------------------
         // T2. RELEASE. The contribution disappears and the pull-up takes the line.
         // ----------------------------------------------------------------
         drive_low = 1'b0; other_low = 1'b0; settle;
         $display("T2  a release removes the contribution and the pull-up wins");
         ck("T2 no contribution", pin_pulls_low, 0);
         ck("T2 the line is high", sda, 1);
         ck("T2 the engine observes high", pin_level, 1);

         // ----------------------------------------------------------------
         // T3. THE INPUT PATH IS UNCONDITIONAL -- READ WHILE RELEASED. Another device
         //     pulls; this device is not driving; it must see the LOW.
         // ----------------------------------------------------------------
         drive_low = 1'b0; other_low = 1'b1; settle;
         $display("T3  released and reading: somebody else's low is visible");
         ck("T3 we are not driving",   pin_pulls_low, 0);
         ck("T3 the line is low",      sda, 0);
         ck("T3 and we observe it",    pin_level, 0);

         // ----------------------------------------------------------------
         // T4. THE INPUT PATH IS UNCONDITIONAL -- READ WHILE DRIVING. This is the test
         //     that matters. This device is driving LOW; so is the other one; the
         //     readback must still be live. A design that gated the input buffer with
         //     the output enable passes T1..T3 and fails here, and on hardware it
         //     presents as a controller that never notices arbitration loss.
         // ----------------------------------------------------------------
         drive_low = 1'b1; other_low = 1'b1; settle;
         $display("T4  driving and reading at the same time: both devices hold it");
         ck("T4 we are driving",           pin_pulls_low, 1);
         ck("T4 two holders",              sda_holders, 2);
         ck("T4 the readback is still live", pin_level, 0);

         // ----------------------------------------------------------------
         // T5. WE RELEASE, THE OTHER KEEPS HOLDING. Our intent says released; the pin
         //     says LOW; the readback must follow the pin. Chapter 19.1's T8, now
         //     through the real boundary.
         // ----------------------------------------------------------------
         drive_low = 1'b0; other_low = 1'b1; settle;
         $display("T5  our intent and the pin disagree, and the readback follows the pin");
         ck("T5 our contribution is gone", pin_pulls_low, 0);
         ck("T5 the pin is low",           pin_level, 0);
         ck("T5 one holder, and it is not us", sda_holders, 1);

         // ----------------------------------------------------------------
         // T6. THE ILLEGAL COMBINATION, DRIVEN ON PURPOSE. `pad_o` is an input here, so
         //     the bench can present enabled-with-a-one -- the case Chapter 19.1 could
         //     not express. Two things must happen: the monitor must fire, and the
         //     contribution must NOT claim a pull-down, because a pad driving a 1 is
         //     not holding the line low. This is the false-positive test that makes the
         //     monitor trustworthy: a check never shown to fire cannot be relied on.
         // ----------------------------------------------------------------
         drive_low = 1'b1; other_low = 1'b0; force_bad_data = 1'b1; settle;
         $display("T6  enabled with a one: the monitor fires, as a monitor must be able to");
         ck("T6 the monitor asserts",        drives_high, 1);
         ck("T6 the data really is a one",   wrap_pad_o, 1);
         ck("T6 and it is NOT pulling low",  pin_pulls_low, 0);
         force_bad_data = 1'b0; settle;
         ck("T6 removing it clears the monitor", drives_high, 0);
         ck("T6 and the pull-down returns",      pin_pulls_low, 1);

         // ----------------------------------------------------------------
         // T7. EXHAUSTIVE OVER THE BOUNDARY'S INPUT SPACE. Three inputs --
         //     (pad_oe via drive_low, pad_o via force_bad_data, the other device) --
         //     so eight combinations, checked against the law rather than sampled.
         //     `pin_level` must equal the resolved line in every one of them.
         // ----------------------------------------------------------------
         for (combo = 0; combo < 8; combo = combo + 1) begin
            drive_low      = combo[0];
            force_bad_data = combo[1];
            other_low      = combo[2];
            settle;
            // NOTE the expectations are written as conditionals, not as `~expr`. A
            // one-bit expression passed to an `integer` argument is evaluated in
            // 32-bit context, so `~` inverts all thirty-two bits and the expected
            // value arrives as -1 or -2. The first draft of this bench did exactly
            // that and reported eight failures against a correct design.
            ck_idx("T7 pull-down is enabled-and-zero", combo,
                   pin_pulls_low, ((combo[0] == 1 && combo[1] == 0) ? 1 : 0));
            ck_idx("T7 monitor is enabled-and-one",    combo,
                   drives_high,   ((combo[0] == 1 && combo[1] == 1) ? 1 : 0));
            ck_idx("T7 the two are mutually exclusive", combo,
                   ((pin_pulls_low === 1'b1 && drives_high === 1'b1) ? 1 : 0), 0);
            // The readback tracks the net, whatever this device is doing to it.
            ck_idx("T7 readback equals the resolved line", combo, pin_level, sda);
            ck_idx("T7 the line is high iff nobody pulls", combo, sda,
                   ((((combo[0] == 1 && combo[1] == 0) || combo[2] == 1)) ? 0 : 1));
         end
         $display("T7  all eight boundary input combinations obey the pad law");

         if (errors == 0) $display("=== i2c_io_wrapper: ALL CHECKS PASSED ===");
         else             $display("=== i2c_io_wrapper: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_io_wrapper.vhd — the same design in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_io_wrapper.vhd
   -- The vendor-neutral boundary between a protocol engine and a bidirectional pin.
   -- Behavioural twin of the SystemVerilog and Verilog designs.
   --
   -- Chapter 19.1 produced `pad_o` and `pad_oe` and left them going nowhere. This is
   -- where they go. The wrapper owns the output path, the input path, and the legality
   -- check that 19.1 could not make testable -- because here `pad_o` arrives from
   -- outside the module, so a wrong value is reachable.
   --
   -- THE INPUT PATH IS UNCONDITIONAL. `pin_level` is not gated by `pad_oe`. A
   -- conforming device reads the line at all times, including while driving: that is
   -- how Module 17 detects arbitration loss and how Module 18 sees a third device's
   -- clock stretch. An input buffer gated by the output enable is a real and popular
   -- bug, and mutation B05 is exactly it.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;

   entity i2c_io_wrapper is
      port (
         -- ---- from the open-drain output stage (Chapter 19.1) ------------------
         -- Data to drive. For a conforming I²C pin this is always '0'; it is an INPUT
         -- so that a wrong value is expressible and therefore testable.
         pad_o         : in  std_logic;
         -- Output enable. '1' = drive `pad_o` onto the pin, '0' = release to Hi-Z.
         pad_oe        : in  std_logic;

         -- ---- the pin, as a two-signal model ----------------------------------
         -- What this device contributes to the shared net.
         pin_pulls_low : out std_logic;
         -- The resolved level of the shared net, from the bus. Read UNCONDITIONALLY.
         pin_resolved  : in  std_logic;

         -- ---- to the protocol engine ------------------------------------------
         -- What the line IS, as far as this device can know. Chapter 19.4 is what must
         -- happen to this signal before any logic is allowed to act on it.
         pin_level     : out std_logic;

         -- ---- the legality monitor, now able to fire ---------------------------
         -- Asserted while the caller asks the pad to drive a '1'. MUST STAY '0' for
         -- every conforming driver, and T6 proves it can assert.
         drives_high   : out std_logic
      );
   end entity i2c_io_wrapper;

   architecture rtl of i2c_io_wrapper is
   begin

      -- The pad law: a pad pulls its net down when it is enabled and its data is '0'.
      pin_pulls_low <= pad_oe and (not pad_o);

      -- The illegal combination: enabled and driving a one.
      drives_high   <= pad_oe and pad_o;

      -- The input path. No gating, no enable term, no condition.
      pin_level     <= pin_resolved;

   end architecture rtl;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_io_wrapper_tb.vhd — the same tests in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_io_wrapper_tb.vhd
   -- Independent oracle for i2c_io_wrapper, on the shared wired-AND bus.
   -- Behavioural twin of the SystemVerilog and Verilog benches.
   --
   -- The wrapper is the boundary, so the bench exercises it from both sides at once:
   -- Chapter 19.1's output stage feeds it, Module 16's `i2c_line_model` resolves the
   -- net, and a second device provides the traffic this device must observe while
   -- driving. T6 is the test 19.1 could not write -- it drives the illegal combination
   -- into `pad_o` and proves the legality monitor fires.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_io_wrapper_tb is
   end entity i2c_io_wrapper_tb;

   architecture sim of i2c_io_wrapper_tb is

      constant N : integer := 2;   -- device 0 is the DUT's pin, device 1 is traffic

      signal drive_low      : std_logic := '0';
      signal od_pad_o       : std_logic;
      signal od_pad_oe      : std_logic;
      signal od_pulls_low   : std_logic;
      signal pin_pulls_low  : std_logic;
      signal pin_level      : std_logic;
      signal drives_high    : std_logic;

      -- the wrapper's data input, so the bench can force the illegal case in T6
      signal force_bad_data : std_logic := '0';
      signal wrap_pad_o     : std_logic;

      signal other_low : std_logic := '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');
      signal sda_drv  : std_logic_vector(N-1 downto 0);

   begin

      wrap_pad_o <= od_pad_o or force_bad_data;
      sda_drv    <= other_low & pin_pulls_low;

      -- ---- device 0's output stage (Chapter 19.1, reused unchanged) -------------
      u_od : entity work.i2c_od_out
         port map (drive_low => drive_low, pad_o => od_pad_o,
                   pad_oe => od_pad_oe, pulls_low => od_pulls_low);

      -- ---- the boundary under test ---------------------------------------------
      dut : entity work.i2c_io_wrapper
         port map (pad_o => wrap_pad_o, pad_oe => od_pad_oe,
                   pin_pulls_low => pin_pulls_low, pin_resolved => sda,
                   pin_level => pin_level, drives_high => drives_high);

      -- ---- the bus: device 0 through the wrapper, device 1 direct ---------------
      bus_model : entity work.i2c_line_model
         generic map (N_DEV => N)
         port map (scl_drive_low => scl_none, sda_drive_low => sda_drv,
                   scl => scl, sda => sda, scl_in => scl_in, sda_in => sda_in,
                   scl_released_but_low => scl_rbl, sda_released_but_low => sda_rbl,
                   scl_holders => scl_holders, sda_holders => sda_holders);

      stim : process
         variable err : integer := 0;

         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;

         variable b_oe, b_o, b_other : integer;
         variable exp_pull, exp_high, exp_line : integer;

      begin
         report "=== i2c_io_wrapper: the boundary, from both sides ===" severity note;

         -- T1. The output path. Intent reaches the net through the wrapper, and the
         --     wrapper's contribution agrees with the output stage's -- the same law
         --     applied at two layers, so a disagreement means one of them is wrong.
         drive_low <= '1'; other_low <= '0'; settle;
         report "T1  a drive reaches the net through the boundary" severity note;
         ck("T1 the wrapper pulls low",   b2i(pin_pulls_low), 1);
         ck("T1 agreeing with the stage", b2i(od_pulls_low),  1);
         ck("T1 the line went low",       b2i(sda), 0);
         ck("T1 and nothing drove high",  b2i(drives_high), 0);

         -- T2. Release. The contribution disappears and the pull-up takes the line.
         drive_low <= '0'; other_low <= '0'; settle;
         report "T2  a release removes the contribution and the pull-up wins" severity note;
         ck("T2 no contribution",         b2i(pin_pulls_low), 0);
         ck("T2 the line is high",        b2i(sda), 1);
         ck("T2 the engine observes high", b2i(pin_level), 1);

         -- T3. The input path is unconditional -- read while released.
         drive_low <= '0'; other_low <= '1'; settle;
         report "T3  released and reading: somebody else's low is visible" severity note;
         ck("T3 we are not driving", b2i(pin_pulls_low), 0);
         ck("T3 the line is low",    b2i(sda), 0);
         ck("T3 and we observe it",  b2i(pin_level), 0);

         -- T4. The input path is unconditional -- read WHILE DRIVING. This is the test
         --     that matters. A design that gated the input buffer with the output
         --     enable passes T1..T3 and fails here, and on hardware it presents as a
         --     controller that never notices arbitration loss.
         drive_low <= '1'; other_low <= '1'; settle;
         report "T4  driving and reading at the same time: both devices hold it"
                severity note;
         ck("T4 we are driving",             b2i(pin_pulls_low), 1);
         ck("T4 two holders",                to_integer(sda_holders), 2);
         ck("T4 the readback is still live", b2i(pin_level), 0);

         -- T5. We release, the other keeps holding. Our intent says released; the pin
         --     says LOW; the readback must follow the pin.
         drive_low <= '0'; other_low <= '1'; settle;
         report "T5  our intent and the pin disagree, and the readback follows the pin"
                severity note;
         ck("T5 our contribution is gone",     b2i(pin_pulls_low), 0);
         ck("T5 the pin is low",               b2i(pin_level), 0);
         ck("T5 one holder, and it is not us", to_integer(sda_holders), 1);

         -- T6. The illegal combination, driven on purpose. `pad_o` is an input here, so
         --     the bench can present enabled-with-a-one -- the case Chapter 19.1 could
         --     not express. The monitor must fire, and the contribution must NOT claim
         --     a pull-down. This is the false-positive test that makes the monitor
         --     trustworthy: a check never shown to fire cannot be relied on.
         drive_low <= '1'; other_low <= '0'; force_bad_data <= '1'; settle;
         report "T6  enabled with a one: the monitor fires, as a monitor must be able to"
                severity note;
         ck("T6 the monitor asserts",       b2i(drives_high), 1);
         ck("T6 the data really is a one",  b2i(wrap_pad_o), 1);
         ck("T6 and it is NOT pulling low", b2i(pin_pulls_low), 0);
         force_bad_data <= '0'; settle;
         ck("T6 removing it clears the monitor", b2i(drives_high), 0);
         ck("T6 and the pull-down returns",      b2i(pin_pulls_low), 1);

         -- T7. Exhaustive over the boundary's input space. Three inputs -- pad_oe via
         --     drive_low, pad_o via force_bad_data, and the other device -- so eight
         --     combinations, checked against the law rather than sampled.
         for combo in 0 to 7 loop
            b_oe    := combo mod 2;
            b_o     := (combo / 2) mod 2;
            b_other := (combo / 4) mod 2;
            if b_oe = 1 then drive_low <= '1'; else drive_low <= '0'; end if;
            if b_o  = 1 then force_bad_data <= '1'; else force_bad_data <= '0'; end if;
            if b_other = 1 then other_low <= '1'; else other_low <= '0'; end if;
            settle;
            if b_oe = 1 and b_o = 0 then exp_pull := 1; else exp_pull := 0; end if;
            if b_oe = 1 and b_o = 1 then exp_high := 1; else exp_high := 0; end if;
            if exp_pull = 1 or b_other = 1 then exp_line := 0; else exp_line := 1; end if;
            ck_idx("T7 pull-down is enabled-and-zero", combo, b2i(pin_pulls_low), exp_pull);
            ck_idx("T7 monitor is enabled-and-one",    combo, b2i(drives_high),   exp_high);
            ck_idx("T7 the two are mutually exclusive", combo,
                   b2i(pin_pulls_low) * b2i(drives_high), 0);
            -- The readback tracks the net, whatever this device is doing to it.
            ck_idx("T7 readback equals the resolved line", combo,
                   b2i(pin_level), b2i(sda));
            ck_idx("T7 the line is high iff nobody pulls", combo, b2i(sda), exp_line);
         end loop;
         report "T7  all eight boundary input combinations obey the pad law" severity note;

         if err = 0 then
            report "=== i2c_io_wrapper: ALL CHECKS PASSED ===" severity note;
         else
            report "=== i2c_io_wrapper: " & integer'image(err) & " CHECK(S) FAILED ==="
                   severity note;
         end if;
         wait;
      end process;

   end architecture sim;

4. What the Mutations Found

#mutationverdictcaught by
B01pin_pulls_low = pad_oeKILLED (6)T6/T7 — a pad driving a 1 must not claim a pull-down
B02pin_pulls_low = ~pad_oKILLED (9)contribution without ownership
B03drives_high = 1'b0KILLED (3)T6 — the test 19.1 could not write
B04drives_high = pad_oKILLED (2)fires while released
B05input buffer gated by pad_oeKILLED (4)T4 — driving and reading at once
B06pin_level = ~pin_pulls_lowKILLED (5)T5 — intent substituted for observation
B07pin_level = ~pin_resolvedKILLED (12)polarity
B08pin_level = 1'b1KILLED (8)the "I released it so it must be high" assumption

B03 and B05 are the two that justify the chapter. B03 is a property that became testable by moving it. B05 is a bug that only a bench with two active devices can see, which is why the bench has one.

5. Internal Tri-State Is Not I/O Tri-State

With that boundary stated, the distinction is:

At a top-level inout port, Z is the supported idiom. assign pad = oe ? data : 1'bz; on a port that leaves the chip is what the I/O block exists for, it is what vendors document, and it is what their inference is written to recognise.

Inside the fabric, there is nothing for Z to become. FPGA interconnect is built from multiplexers and routing switches; there is no tri-state routing resource between two logic blocks. A tool meeting an internal tri-state net has to either rewrite it as multiplexers — changing timing, area and sometimes behaviour on X — or refuse to build it.

Which of those two happens depends on the tool, the version, the device family and your synthesis settings. That dependence is the reason to avoid it: identical RTL that behaves differently across tools is not portable RTL, even when every individual tool is behaving reasonably.

top-level inout portinternal net
what Z meansrelease the padnothing physical exists
what the tool doesmaps to an I/O bufferrewrites, or errors
is it portableyes, this is the idiomno — outcome varies by tool
does simulation warn younono

The last row is why this is a Module 19 problem and not a Module 18 problem. Four-valued RTL simulation resolves internal tri-state nets perfectly happily. The simulator has a Z; the fabric does not.

6. Mapping the Wrapper Onto a Vendor Primitive

The wrapper's four signals correspond one-to-one with every vendor's bidirectional buffer:

wrapper signalthe primitive's role
pad_odata driven onto the pin — for I²C, always 0
pad_oewhether to drive at all
pin_leveldata received from the pin, always live
pin_pulls_low / pin_resolvedthe pad itself, as a two-signal model of one shared net
A left-to-right block diagram in two rows. The top row shows the portable path: a protocol core emits drive_low to an open-drain stage, which emits pad data and output enable to the vendor-neutral I/O wrapper, which connects to a vendor primitive, which connects to a package pin. The bottom row shows the return path: the package pin's resolved level enters the vendor primitive and returns through the wrapper to the protocol core. The vendor primitive and the package pin are marked as the only vendor-specific elements.Protocol coreModules 17 / 18Open-drain stage19.1 — verifiedI/O wrapper19.2 — verifiedVendor primitiveNOT portablePackage pinone conductorObserved levelalways livedrive_lowo, oe12
Figure 1 — where the vendor boundary falls. Everything left of the dashed column is portable and verified in this chapter and the last one; only the rightmost module names a vendor. The two-signal pin model collapses into one physical inout port exactly once, at the top level, which is why a change of FPGA family touches one file.

So instantiating a real primitive replaces the last row only. pin_pulls_low and pin_resolved — a model of a shared net — become one physical inout port, and the other two signals connect straight through.

That is not a cop-out; it is the engineering procedure. The compiler cannot catch an inverted enable, simulation of your RTL cannot catch it because the primitive is not in the RTL, and it is a one-bit mistake with a total failure mode. The only things that catch it are reading the library guide and measuring the pin — which is why Chapter 19.8 puts "confirm the idle bus voltage" near the top of the bring-up order.

Azvya Education Pvt. Ltd.VLSI Mentor
io_mapping_sketch.sv — VENDOR-SPECIFIC SHAPE, ILLUSTRATIVE, NOT COMPILED
   // What the top level looks like once a real primitive replaces the bus model. The
   // primitive's name and the SENSE of its control input come from your vendor's
   // library guide -- see the callout above; they are deliberately left as <NAME> and
   // <CONTROL> here rather than guessed at.
   //
   // NOT PART OF THIS CHAPTER'S VERIFIED SET: no vendor library was available, so this
   // was never compiled or synthesised. It is a wiring sketch, not a tested artifact.

   module i2c_top (
      input  wire clk, rst_n,
      inout  wire sda_pin,          // leaves the chip
      inout  wire scl_pin
   );
      wire sda_drive_low, scl_drive_low;   // from the core (Modules 17 / 18)
      wire sda_pad_o, sda_pad_oe;          // from i2c_od_out (Chapter 19.1)
      wire sda_level;                      // to the core, via Chapter 19.4

      i2c_od_out u_sda_od (
         .drive_low(sda_drive_low), .pad_o(sda_pad_o),
         .pad_oe(sda_pad_oe), .pulls_low(/* unused at the top level */));

      // <NAME> u_sda_pad (
      //    .I  (sda_pad_o),           // data out -- constant 0 for I2C
      //    .<CONTROL>(...),           // derived from sda_pad_oe -- CHECK THE SENSE
      //    .O  (sda_level),           // data in -- always live
      //    .IO (sda_pin)              // the package pin
      // );

      // ... and the same three lines again for SCL, which is not optional: a target
      // that can stretch drives SCL too, and Chapter 19.6 has the constraint
      // consequences of forgetting it.
   endmodule

7. ASIC Contrast, Briefly

An ASIC pad cell presents the same four signals and a longer list of choices the FPGA made for you: drive strength, slew, whether an ESD structure and a Schmitt input are present, and whether the cell is open-drain in silicon rather than open-drain by convention. An FPGA I/O block is a configurable pad cell with the menu fixed at bitstream time.

One difference matters for I²C specifically. A genuine open-drain pad cell has no pull-up transistor at all, so the illegal combination this chapter monitors is not merely forbidden — it is unbuildable. On an FPGA the pin is a general-purpose push-pull buffer being used as open drain, so "never drive high" is a property of your RTL, enforced by a design convention and a monitor, not by the silicon. That is precisely why drives_high is worth exporting from the wrapper.

8. Focused Verification Insight

Module 20 owns the verification architecture. Two observations belong here.

The pin boundary is where a monitor should attach, and the wrapper is what makes that possible. pin_resolved is the resolved line; pin_pulls_low is this device's contribution. A monitor on the first sees what the bus did; a monitor on the second sees what this device asked for. Module 20's monitor wants the first, and a scoreboard that needs to attribute a LOW to a device wants both.

Coverage at this layer is about the boundary's input space, not about protocol. The eight T7 combinations are the coverage model: each (pad_oe, pad_o, other-device) triple, with the two illegal-combination cases marked as bins that must stay at zero in any conforming run. An illegal_bins construct expresses that directly — though note that illegal_bins is a SystemVerilog keyword that Icarus does not support, so nothing in this chapter's verified set uses it.

Azvya Education Pvt. Ltd.VLSI Mentor
wrapper_properties.sv — assertion CONCEPT, not executed
   // Shown as properties, not run: Icarus Verilog does not support concurrent
   // assertions, so this chapter's verified set checks the same facts procedurally --
   // T7 is p_never_drives_high sampled every settle instead of every clock.

   // The conforming-driver obligation.
   property p_never_drives_high;
      @(posedge clk) not (pad_oe && pad_o);
   endproperty

   // The input path is never gated. Stated as: the observed level always equals the
   // resolved level, whatever this device is doing to the pin.
   property p_input_always_live;
      @(posedge clk) pin_level == pin_resolved;
   endproperty

   // A pull-down claim and a drive-high claim are mutually exclusive by construction.
   property p_exclusive;
      @(posedge clk) not (pin_pulls_low && drives_high);
   endproperty

9. Misconceptions

10. Debugging

The controller never notices it lost the bus

Pitfall — an input buffer gated by the output enable
Buggy Code
// A board wrapper for Module 17's verified controller. The author reasoned that
// while the FPGA is driving a line, reading it back can only return what the FPGA
// itself is driving, so the read is wasted logic:
//
//     assign sda_level = sda_pad_oe ? 1'b1 : sda_pin;   // <-- the bug
//     assign scl_level = scl_pad_oe ? 1'b1 : scl_pin;
//
// The reasoning has a hidden premise: that this device is the only one that can
// change the line. On a shared open-drain bus that premise is false by design --
// and the features that depend on it being false are exactly the ones I2C uses
// for arbitration, clock stretching and acknowledge.
//
// Note the substituted value: 1'b1. Whoever wrote it chose the value a RELEASED
// line settles to, which is why the bug is invisible while nothing else is
// talking -- the guess is right whenever it does not matter.
Symptom

Single-controller bench: flawless. Two-controller board: the second controller occasionally corrupts a transfer, and the first one reports success for a transfer that never happened on the wire.

An external analyser shows classic arbitration: both controllers start, both send an address, they diverge at a data bit, one of them loses. The wire is entirely correct I2C.

The ILA shows the losing controller's arbitration-loss detector never firing. Its internal SDA observation reads HIGH for every bit in which it released the line -- including the bits in which the other controller was holding SDA down. It did not mis-handle arbitration loss; it never saw it.

The two views disagree, and the disagreement is the whole diagnosis: the wire says contention, the logic says clean. One assignment separates them.

Root Cause

sda_level = sda_pad_oe ? 1'b1 : sda_pin substitutes an assumption for an observation whenever this device is driving.

Arbitration detection works by comparing what you drove with what the line became: you release SDA (drive a logical 1), and if the line reads LOW, somebody else is holding it and you have lost. This wrapper returns 1'b1 for exactly that case, so the comparison always agrees and the loss is never detected.

The same line breaks clock-stretch detection on SCL: the controller releases SCL, a target holds it down, and the controller reads back its own assumption that SCL went high -- so it clocks the next bit into a target that is not ready.

Two things about the failure shape are worth noting. It is intermittent, because it needs a second active device. And it produces a FALSE SUCCESS rather than an error, which is strictly worse: the controller reports a completed transfer that the bus never carried, so the corruption surfaces later, somewhere else, as bad data from a device that was never written.

The fix is to delete the condition -- the input path has no enable term. The structural fix is to instantiate the wrapper in Section 2 rather than open-coding the mapping, because an open-coded boundary is a boundary nobody tests.

Mutation B05 is this exact bug. It is killed by four checks, all of them in T4 -- the only test in which two devices drive the line at the same time. A bench with one active device cannot find it, which is why this one has two.

11. Reason It Through

12. Questions

13. What This Chapter Settled

A bidirectional pin is four signals, and the wrapper in Section 2 is the only module that needs to know it. The output path applies the pad law once, at the boundary. The input path is unconditional, because an I²C device reads the line while it drives it — and the three features that depend on that are the ones every controller and target in Modules 17 and 18 rely on.

The legality monitor Chapter 19.1 deleted now lives somewhere it can be violated, and a test proves it fires and then stops. Sixteen mutations across three languages, all killed, with identical failure counts for the four injected everywhere.

What is still missing is the reason a released line goes high at all. Nothing in this chapter or the last one produces a HIGH: the wrapper releases, the bus model asserts that a released net reads 1, and no resistor has been sized, costed or justified. Chapter 19.3 is that resistor — why the bus needs an external one, why the pull-up inside the FPGA pin is not it, and what the arithmetic is.

Continue learning

Related tutorials