Skip to content
VLSI Mentor

I²C · Module 21

The i2c_if Interface — Bidirectional Signals in a Testbench

Models an open-drain bus as a SystemVerilog interface with no inout and no Z anywhere, puts the wired-AND resolution in exactly one place, and locates passivity in a modport rather than a parameter. Opens with a measured account of what this environment can execute of the language UVM is built on — one feature of ten works, one silently returns wrong answers.

Module 20 built a verification architecture and argued for every decision in it. This module builds the same architecture in the form a production environment actually ships: classes, phases, a factory, a configuration database, an agent that can be active or passive.

Before any of that, a statement about evidence, because it governs how every chapter in this module should be read.

1. What This Environment Can Execute, Measured

Modules 16 to 20 published code that was simulated, mutated and verified. This module cannot do that, and the reason is not a preference.

That leaves a real question: what is a chapter of unexecuted code worth? Three things are available, and this module does all three rather than asking for trust.

The architecture is not new. Every decision here was argued and simulated in Module 20, against Module 18's real target, in three HDLs, to zero valid non-equivalent survivors. This module changes the packaging, not the behaviour.

The algorithms are traceable, mechanically. Chapter 21.3 publishes a tool that reduces each driver task — in the UVM class and in Module 20's verified module — to its ordered sequence of bus operations, and compares them. It found two real transcription defects in the code published here.

The structure is checked, mechanically. Chapter 21.11 publishes a UVM structural linter with fifteen checks, every one of them proven capable of failing against a deliberately broken specimen. A monitor that drives, a driver that never completes an item, a port nothing consumes, a phase that skips its super — all mechanical, all found without a simulator.

None of that is a substitute for running the code. It is what can honestly be produced without a simulator, and saying which is which is the point.

2. Why There Is No inout Anywhere

The instinct when modelling a bidirectional bus in a testbench is a tri-state net:

Azvya Education Pvt. Ltd.VLSI Mentor
The natural approach, and why it describes hardware that cannot be built
   interface i2c_if(input logic clk);
      // DO NOT DO THIS
      wire sda;
      logic sda_out, sda_oe;
      assign sda = sda_oe ? sda_out : 1'bz;    // drives HIGH when sda_out is 1
   endinterface

That works in simulation and it is wrong, for the reason Chapter 19.1 spends a chapter on: an open-drain device cannot drive high. sda_out = 1 with sda_oe = 1 is a strong high, and a real device putting a strong high on a line another device is pulling low produces contention rather than a 1. A testbench built on this model will pass tests that hardware fails.

So the interface has no drivable sda at all. A participant owns one bit per line:

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_if.sv — the only action available is pulling low
      logic [N_DRV-1:0] scl_drive_low;
      logic [N_DRV-1:0] sda_drive_low;

      wire scl = ~(|scl_drive_low | dut_scl_drive_low);
      wire sda = ~(|sda_drive_low | dut_sda_drive_low);

1 is now the absence of an action rather than an action, which is what open drain means. And the resolution happens in exactly one place, so no component can disagree with it — a component that computed its own idea of the bus level would be a second, unreviewed model of the physics.

3. Passivity Belongs in the Modport

Chapter 20.4 argued that a monitor must be unable to drive, and that enforcing it with a parameter or a comment is a promise rather than a mechanism. An interface gives a stronger option.

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_if.sv — a view with no outputs in it
      clocking drv_cb @(posedge clk);
         output scl_drive_low, sda_drive_low;
         input  scl, sda, scl_holders, sda_holders;
      endclocking

      clocking mon_cb @(posedge clk);
         input scl, sda, scl_holders, sda_holders;
      endclocking

      modport drv_mp (clocking drv_cb);
      modport mon_mp (clocking mon_cb);

A monitor connected through mon_mp has no output in its view of the interface. Its passivity is not a rule it follows; it is a consequence of what it can reach. Adding a drive would require changing the modport, which appears in a diff and has to be justified.

4. The One Fact the Resolved Line Cannot Give You

The interface also exports a count of how many participants are pulling each line, and that is not a convenience.

The acknowledge slot must have exactly one driver. Two devices acknowledging at once is a genuine fault — two targets at the same address, or a monitor that "helpfully" acknowledges during bring-up. It is invisible from the resolved line: low is low, and the wired-AND of one puller and two is bit-identical. No amount of monitoring sda can detect it.

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_if.sv — counting intents, because the line cannot
      wire [31:0] sda_holders = $countones(sda_drive_low) + (dut_sda_drive_low ? 1 : 0);

One cycle of overlap is legal at an acknowledge handover. A sustained overlap is a second driver. That question can only be asked here.

5. The Interface

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_if.sv — an open-drain bus with no inout in it
   // -----------------------------------------------------------------------------
   // i2c_if.sv
   // An open-drain bus as a SystemVerilog interface.
   //
   // NOT EXECUTED. No UVM library and no UVM-capable simulator exists in this
   // environment -- `probe/capability.sh` measures it, and a virtual interface inside a
   // class is itself a compile error on the only Verilog simulator available here. This
   // file is published as reviewed, statically checked source, not as a simulated result.
   //
   // THE ONE DECISION THAT MATTERS. There is no `sda` output. A participant owns a
   // `*_drive_low` bit and nothing else, exactly as in Module 16's bus model and Module
   // 19's pad: the only action available on this bus is PULLING LOW, and `1` is the
   // absence of an action rather than an action. An interface that exposed a drivable
   // `sda` would let a testbench component drive the line high, which no open-drain
   // device can do -- and Chapter 20.4's argument is that this has to be impossible
   // rather than merely discouraged.
   //
   // Consequently the resolved value is computed here, in one place, as a wired-AND of
   // every participant's pull. A component reads `scl`/`sda` and writes only its own
   // `*_drive_low` bit, and the two are genuinely different signals: a component that
   // releases a line and then assumes it is high is the defect Chapter 20.5's driver
   // exists to avoid.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   interface i2c_if #(
      // Participants that can pull the lines. The DUT is not one of them -- it connects
      // to the resolved nets through its own pins.
      parameter int N_DRV = 4
   ) (
      input logic clk
   );

      // ---- what each testbench participant requests ----------------------------
      // Index 0 is by convention the master driver, 1 the responder, 2 the error
      // injector, 3 spare. Nothing enforces that; the resolution below does not care.
      logic [N_DRV-1:0] scl_drive_low;
      logic [N_DRV-1:0] sda_drive_low;

      // ---- what the DUT requests ----------------------------------------------
      logic dut_scl_drive_low;
      logic dut_sda_drive_low;

      // ---- what the bus therefore IS ------------------------------------------
      // Wired-AND: low if ANY participant pulls, high only if every one released. This
      // is the only place in the VIP that decides what the bus is, which is why a
      // component cannot accidentally disagree with it.
      wire scl = ~(|scl_drive_low | dut_scl_drive_low);
      wire sda = ~(|sda_drive_low | dut_sda_drive_low);

      // ---- evidence a monitor cannot get from the line -------------------------
      // How MANY participants are pulling. The resolved line cannot answer this -- low
      // is low, and the wired-AND of one puller and two is bit-identical -- so a second
      // unintended driver in an acknowledge slot is undetectable without a count.
      // Chapter 20.9 argues this is the one protocol rule that has no bus-level check.
      wire [31:0] scl_holders = $countones(scl_drive_low) + (dut_scl_drive_low ? 1 : 0);
      wire [31:0] sda_holders = $countones(sda_drive_low) + (dut_sda_drive_low ? 1 : 0);

      // ---- clocking blocks -----------------------------------------------------
      // An ACTIVE participant drives its own pull bits and samples the resolved lines.
      // A PASSIVE one samples and has no outputs at all: the modport is where passivity
      // stops being a promise. A monitor connected through `mon_mp` cannot drive,
      // because there is nothing in its view to drive.
      clocking drv_cb @(posedge clk);
         output scl_drive_low, sda_drive_low;
         input  scl, sda, scl_holders, sda_holders;
      endclocking

      clocking mon_cb @(posedge clk);
         input scl, sda, scl_holders, sda_holders;
      endclocking

      modport drv_mp (clocking drv_cb);
      modport mon_mp (clocking mon_cb);

      // ---- reset the interface's own drivers -----------------------------------
      // Release everything. A participant that has not been started must not be holding
      // a line, or every test after it inherits a wedged bus.
      task automatic release_all();
         scl_drive_low = '0;
         sda_drive_low = '0;
      endtask

      initial begin
         scl_drive_low     = '0;
         sda_drive_low     = '0;
         dut_scl_drive_low = 1'b0;
         dut_sda_drive_low = 1'b0;
      end

   endinterface
A block diagram. On the left, four testbench participants — master driver, responder driver, error injector and a spare — each send a pull-low request into a central wired-AND resolver. The DUT also sends a pull-low request into the resolver. The resolver outputs the resolved SCL and SDA, which feed back to all participants and to the DUT's pins, and feed the monitor through a one-way arrow.Master driverindex 0Responderindex 1Error injectorindex 2DUT pinsModule 18Wired-ANDone place onlyscl · sdawhat the bus ISholdershow MANY pullMonitormon_mp · nooutputsdrive_lowread only12
Figure 1 — four participants and a DUT sharing two wires, with the resolution in exactly one place. Every arrow into the resolver is a pull-low request; the resolved lines fan back out to everyone, including the participant that requested the pull. The monitor's connection is one-way by construction — its modport contains no output — which is what makes its presence free.

The testbench that passed every test and described impossible hardware

Pitfall — a tri-state interface for an open-drain bus
Buggy Code
// An I2C interface written the way a bidirectional bus is normally modelled.
//
//    interface i2c_if(input logic clk);
//       wire  sda,     scl;
//       logic sda_out, sda_oe;
//       logic scl_out, scl_oe;
//       assign sda = sda_oe ? sda_out : 1'bz;
//       assign scl = scl_oe ? scl_out : 1'bz;
//    endinterface
//
// The driver sets sda_out = 1, sda_oe = 1 to send a one. In simulation the net
// resolves to 1 and every test passes.
//
// TWO THINGS ARE NOW WRONG AND NEITHER FAILS IN SIMULATION:
//
// 1. The testbench drives a STRONG HIGH. A real open-drain device cannot. So
//    the environment has never tested the case where the DUT pulls low while
//    the testbench "sends a one" -- on hardware that is contention; here the
//    simulator resolves 1 vs 0 to X, or to 1 if the strengths differ, and the
//    test either passes or fails for a reason unrelated to the protocol.
//
// 2. Arbitration cannot be modelled AT ALL. Arbitration IS the wired-AND: a
//    master that sends a one and reads back a zero has lost. With tri-state
//    resolution the read-back is X, not 0, so the losing master never notices.
Pitfall — two agents sharing one pull index
Buggy Code
// Two masters on one bus, to test arbitration. Both agents are configured from
// the same config object, copied:
//
//    acfg = i2c_agent_config::type_id::create("cfg");
//    acfg.vif = bus;
//    uvm_config_db #(i2c_agent_config)::set(this, "agent_a", "cfg", acfg);
//    uvm_config_db #(i2c_agent_config)::set(this, "agent_b", "cfg", acfg);  // SAME object
//
// drv_index defaults to 0 in both. So both drivers write sda_drive_low[0].
//
// Agent A pulls low:        sda_drive_low[0] = 1
// Agent B then releases:    sda_drive_low[0] = 0   <-- undoes A's pull
//
// The bus goes high while A believes it is holding the line low. A's next
// read-back shows 1, so A concludes it lost arbitration -- and B, which also
// reads 1, concludes it won. Neither is true. The arbitration test reports a
// winner and has tested nothing, and the symptom moves with scheduling order.

6. What 21.1 Settled

The evidence situation is measured, not asserted. One of ten language features UVM needs works on the available simulator, one silently returns wrong answers, and eight are rejected — including the virtual interface every driver needs to reach a DUT. The code here is NOT EXECUTED and the probe is reproducible.

An open-drain interface has no inout and no Z. A participant owns a pull-low bit; 1 is the absence of an action. The presence of 1'bz in such an interface is itself the defect, and it makes arbitration inexpressible.

The resolution lives in exactly one place, so no component can hold a private view of what the bus is.

Passivity belongs in the modport. A view with no outputs cannot be driven, which is stronger than a parameter and much stronger than a comment.

One protocol rule can only be checked by counting intents. How many devices are pulling a line is not recoverable from the line, which is why the interface exports it.

Next: what a transaction object should contain, and why an intent and an observation must be two classes that are not related by inheritance. Chapter 21.2 — The Sequence Item.

Continue learning