I²C · Module 20
Active and Passive Components — Agents, Monitors and Responders
The boundary that decides reuse on a wired-AND bus: which components may pull a line low. Argues that passivity has to be a property of a port list rather than a promise, and works through the common-mode failure that makes a monitor sharing the design's framing logic incapable of detecting a framing bug.
On a bus where the only action available is pulling a line low, the question "which components are allowed to do that" is the whole of the architecture. Everything else — reuse, trust, what a passing result means — follows from the answer.
There are exactly two kinds of component, and the boundary is not a matter of layering or of who talks to whom. It is physical.
1. The Distinction
Active. Pulls SCL or SDA low, at times of its own choosing. It changes what happens.
Passive. Reads the resolved lines and nothing else. It cannot change what happens, and that is not a restriction to be worked around — it is the entire source of its value.
This module's environment has four components on the bus. Three are active by design and one is passive by construction.
| component | role | drives | chapter |
|---|---|---|---|
| controller driver | generates transfers, owns SCL | SCL, SDA | 20.5 |
| responder model | behaves like a target device | SCL (stretching), SDA | 20.6 |
| fault injector | creates conditions that should not occur | SCL, SDA | 20.9 |
| monitor | reports what happened | nothing | 20.7 |
The fault injector is worth noticing in that list. It is active, deliberately, and being a third participant on a shared bus rather than a hook inside the design is what makes it able to produce conditions the design has to survive. Injection is not observation and does not belong in a monitor — the two are opposite in exactly the sense this chapter is about.
2. Why the Distinction Decides Reuse
A passive component can be instantiated anywhere without changing the result. That is a strong statement and it has three consequences that are each worth more than they sound.
It can watch a bus it was not written for. The monitor in this module is verified against synthetic traces in mon_selftest, where a bench drives the lines directly and there is no design at all. The same module, unmodified, then watches the real Module 18 target in env_vs_dut against three HDL implementations. Same file, four situations, because it has no opinion about who else is present.
Two of them can coexist. Two monitors on one bus is harmless. Two drivers is a design decision about arbitration.
It can be left in. A monitor is safe in a regression, in a debug build, and in a system-level simulation where the real controller is somebody else's code. An active component in the same place is a second master.
3. Passivity Must Be Structural
A component is not passive because its author intended it to be. It is passive if it has no output that reaches the bus, and that is a property of a port list — checkable by reading fourteen lines, and impossible to violate by accident later.
module i2c_mon #(
parameter int MAX_BYTES = 8
) (
input logic clk,
input logic rst_n,
// ---- the only inputs: the RESOLVED bus -----------------------------------
input logic scl,
input logic sda,
// ---- everything else is an observation -----------------------------------
output logic saw_start,
output logic saw_restart,
// ... and no scl_drive_low, no sda_drive_low, no output enable, anywhere
);There is no drive_low output and no output enable. Adding one later would be a port-list change visible in a diff, which is the point: the property survives maintenance by people who have not read this chapter.
Compare the two enforcement styles honestly, because the weaker one is extremely common:
| approach | fails how |
|---|---|
a passive parameter guarding drive statements | works, until the guard is inverted, defaulted wrongly, or one drive statement is written outside it |
| "the monitor does not drive" in a comment | is true when written |
| no drive port exists | cannot fail without a port-list change |
The bench asserts it anyway. With every active component released, both lines must read high — if the monitor were pulling anything, they would not:
// T9. THE MONITOR NEVER DRIVES. Its port list has no output that reaches the
// bus, so this is structural rather than dynamic -- but the bench asserts
// it anyway, because the property is the reason the component is trusted.
do_reset;
@(negedge clk); scl_low = 1'b0; sda_low = 1'b0;
for (n = 0; n < 20; n = n + 1) step;
ck("T9 SCL reads high", scl, 1);
ck("T9 SDA reads high", sda, 1);Asserting a structural property looks redundant and is not. It documents why the component is trusted in a place where a future reader is looking, and it converts a silent architectural regression into a failing test.
4. The Failure That Makes a Monitor Worthless
Passivity is necessary and it is not sufficient. A monitor can drive nothing, be instantiated correctly, and still be incapable of detecting the defect it exists to detect — if it derives its answer from the same code as the thing it is watching.
The same argument applies to the responder, one step out. A responder model written by copying the target's register logic will agree with the target about every register decision, including the wrong ones. This is why 20.6's responder has a deliberately thin policy — acknowledge or not, stretch or not, a read base — rather than a second register file: a thin model has less opportunity to share a mistake.
5. What Active Components Owe
An active component is a participant, which means every protocol rule the environment checks of others applies to it as well. This is stated often and obeyed rarely, because the driver is the component with a reason to cheat: it knows what it is trying to do and the shortcut usually works.
The rule that matters most is the data-valid rule. SDA may change only while SCL is low, except for framing. A driver that changes SDA while SCL is high has generated a START or a STOP, whatever it intended, and a target that reacts accordingly is correct.
Two more obligations, both of which this module got wrong at first and fixed:
Every wait must be bounded. An active component that waits for the bus to do something must have a timeout, because the bus may never do it. The driver's stretch wait has STRETCH_TIMEOUT; 20.9's T5 exists to prove the timeout works by holding SCL low forever, and T5b exists to prove the driver waits when the hold is short. Without the second test the first one is evidence that the driver gives up, which is not the property wanted.
Releasing is not the same as being high. After releasing a line, an active component must read the resolved value before acting on it. Somebody else may still be holding it — that is what a stretch is — and a driver that assumes its own release took effect clocks data into a target that has stopped listening.
6. One Module, Two Situations
The clearest demonstration that passivity buys reuse is that the monitor in this environment is run in two structurally different situations without modification.
The monitor that acknowledged on the design's behalf
Pitfall — a monitor that drives, added for a reason that sounded good
// A monitor for an I2C target. During bring-up the target's acknowledge logic
// was not ready, so the monitor was given a small helper so that transfers
// would complete and the rest of the environment could be developed:
//
// module i2c_mon (
// input logic scl, sda,
// output logic sda_drive_low, // <-- "temporary"
// output logic byte_valid, ...
// );
// // acknowledge on the target's behalf so transfers complete
// always @(posedge clk)
// sda_drive_low <= (bitcnt == 4'd8) && addr_matched;
//
// Eight months later the target's acknowledge logic is finished and works. The
// helper is still there. Nobody remembers it, because nothing fails.
//
// Then a customer reports that the part NACKs its own address intermittently.
// In simulation it never does -- because on every transfer TWO devices pull SDA
// low in the acknowledge slot, and the wired-AND of a correct acknowledge and
// the monitor's acknowledge is indistinguishable from a correct acknowledge
// alone. The environment cannot see a missing ACK. It has been supplying one.Pitfall — the monitor that shared the design's framing detector
// Sensible-looking reuse. The target already has a verified framing detector,
// so the monitor instantiates the same module:
//
// i2c_framing fr_dut (.scl(scl), .sda(sda), .start(dut_start), ...); // in the DUT
// i2c_framing fr_mon (.scl(scl), .sda(sda), .start(mon_start), ...); // in the monitor
//
// // the scoreboard's framing check
// if (dut_start !== mon_start) error("framing mismatch");
//
// This check can never fire. Two instances of one module, given identical
// inputs, produce identical outputs -- always, including when the module is
// wrong. The framing detector has a bug: it treats an SDA fall as a START
// without requiring SCL to be high. Both instances have it. They agree.
//
// The environment reports a clean framing check on a target that starts
// transfers on noise.7. What 20.4 Settled
The boundary is physical: can this component pull a line low. Not layering, not who talks to whom. On a wired-AND bus that single capability is the whole of the difference.
Passivity must be structural. No drive port, so violating it requires a port-list change. A parameter or a comment is a promise, and promises do not survive bring-up.
Passivity buys reuse, and reuse is really about what a result means. The monitor here runs against synthetic traces with no design present and against three real target implementations, unmodified, because it cannot have influenced any of them.
Passive is not the same as independent. A monitor that shares framing code with the design agrees with it by construction, including in error. Independent reimplementation from the specification is the one correct duplication in this curriculum.
Active components owe the protocol everything they check of others. A driver that violates the data-valid rule produces failure reports against correct targets, and the usual outcome is a workaround in the design — a testbench bug that ships as hardware.
The next chapter builds the first active component, and it is the one with the most to obey: the driver owns SCL, decides when every bit moves, and has to follow every rule it is there to test. Chapter 20.5 — The Master Agent.
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
Master-Receiver and Slave-Transmitter Roles
The master still owns SCL while the slave owns SDA — and the slave-transmitter has no way to refuse anything, because the ninth bit is not its to drive. This chapter derives that asymmetry and builds the slave side of a read.
- Related topic
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.
- Related topic
Building a Reusable I²C VIP
The only honest test of reuse is whether another project can adopt the VIP without editing it. Publishes a UVM structural linter with fifteen checks, every one proven able to fail plus two proven to stay quiet, and reports the three defects it found in this module's own code.
