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:
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
endinterfaceThat 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:
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.
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.
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
// -----------------------------------------------------------------------------
// 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
endinterfaceThe testbench that passed every test and described impossible hardware
Pitfall — a tri-state interface for an open-drain bus
// 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
// 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
Related tutorials
- Related topic
Open-Drain Outputs — Drive Low, Release High
The architectural move the whole bus rests on, and it is a subtraction: delete every device's ability to drive HIGH. What remains is one switch to ground, so the two states are pull LOW and release — and two devices can never impose opposite levels because only one level can be imposed at all.
- Related topic
Repeated START — Holding the Bus Between Phases
A repeated START is not a new waveform. It is the START edge again, and what makes it a different event is that the bus was already busy. That single fact is why a classifier needs state and why a monitor that joins late cannot classify what it sees.
- Related topic
START/STOP Timing and Malformed Framing
Three framing margins, each with two anchor events, all of them minimums: the hold after a START, the setup before a repeated START, and the setup before a STOP. Build a sequencer that generates all three and refuses an illegal configuration, then catalogue the malformed framing the margins exist to prevent.
- Related topic
The Address Byte — Seven Address Bits and the R/W Bit
The first byte after a START is not an address followed by a direction bit. It is one eight-bit field that the bus, the slave and the datasheet all treat as a unit — and treating it as two things is the single most common source of I²C address confusion.
