I²C · Module 21
Assembling the I²C Agent
Why passive must mean not built rather than told to be quiet, why configuration is handed down rather than looked up, and why a participant plays one role. Includes a driver and a monitor bound to different buses with no error reported anywhere.
Four components exist. This chapter makes them one unit that another project can instantiate, and the whole of that unit's quality rests on two decisions: what the active/passive switch actually switches, and how configuration reaches the components inside.
1. Passive Means Not Built, Not Told To Be Quiet
The switch has two possible meanings and only one of them is worth having.
| interpretation | what a passive agent contains |
|---|---|
| build everything, set a flag, ask the driver not to drive | a driver that is capable of driving and has been asked not to |
| do not build the driver or the sequencer at all | no object that owns a bus line |
monitor = i2c_monitor::type_id::create("monitor", this);
if (cfg.is_active == UVM_ACTIVE) begin
sequencer = uvm_sequencer #(i2c_seq_item)::type_id::create("sequencer", this);
// A participant is EITHER a controller or a target. Building both would put two
// drivers on one pull index, which is a silent fault.
if (cfg.role == I2C_CONTROLLER)
driver = i2c_master_driver::type_id::create("driver", this);
else
responder = i2c_responder_driver::type_id::create("responder", this);Chapter 20.4 argued that passivity should be structural. This is what structural means at the topology level: in a passive build there is nothing in the hierarchy that could drive, so "it does not drive" is not a property anyone has to maintain.
The weaker interpretation fails in the ordinary way — a flag defaulted wrongly, inverted in one branch, or read before it was set — and each failure produces a second driver on a bus that cannot report one.
The monitor is built either way, and that is the whole payoff of it being passive. The same agent watches a bus it drives and a bus somebody else drives, with no mode flag anywhere, because it could not have influenced either.
2. A Participant Is One Role, Never Both
Notice that the active branch builds a controller driver or a responder, on cfg.role.
Building both would put two drivers on one pull index, and Chapter 21.1 §4 established that the resolved line cannot reveal that: low is low, and one driver's release silently undoes the other's pull. The failure is scheduling-dependent and looks like arbitration.
One class serving both roles, selected by configuration, is also what makes a second master a configuration change rather than a new agent class. A project testing arbitration instantiates two agents with different drv_index and different roles, and edits nothing.
3. Configuration Is Handed Down, Not Looked Up
Every component in this VIP gets its configuration from a single object, and the agent passes it to its children explicitly:
if (!uvm_config_db #(i2c_agent_config)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no i2c_agent_config for the agent")
cfg.check_valid(get_full_name());
// Pass the config DOWN rather than letting children search for it. A child that
// does its own lookup can find a different object from its parent's, and the two
// then disagree about the interface or the timing with nothing to reveal it.
uvm_config_db #(i2c_agent_config)::set(this, "*", "cfg", cfg);The alternative — every component performing its own get from the top — looks equivalent and is not.
check_valid is called here rather than in each component for the same reason: one place, one failure, at a point where the topology is final and nothing has run.
function void check_valid(string ctx);
if (vif == null)
`uvm_fatal("CFG", {ctx, ": i2c_agent_config.vif is null"})
if (half_period < 4)
`uvm_fatal("CFG", {ctx, ": half_period below 4 leaves no room to move SDA while SCL is low"})
if (stretch_timeout <= half_period)
`uvm_fatal("CFG", {ctx, ": stretch_timeout must exceed one half period or every transfer times out"})
endfunctionEach of those three is a real misconfiguration with a non-obvious symptom. A null interface appears as a null dereference thousands of lines away. A half period below four leaves no room to move SDA while SCL is low, so the driver violates the data-valid rule it exists to obey. A stretch timeout shorter than a half period makes every transfer time out, which looks like a target that never responds.
4. The Responder Is Deliberately Unconnected
if (cfg.is_active == UVM_ACTIVE && cfg.role == I2C_CONTROLLER)
driver.seq_item_port.connect(sequencer.seq_item_export);The responder takes no items. Chapter 21.4 §1 covers why: it is a state machine over edges, so there is nothing for a sequencer to arbitrate, and a responder wired to one blocks in get_next_item forever — presenting as a target that never acknowledges, on a bus that looks healthy.
The comment in the source says so explicitly, because an unconnected port in a connect_phase is otherwise indistinguishable from an omission.
5. The Monitor's Port Is Exported, Not Consumed
The agent provides an accessor for the monitor's analysis port and subscribes to nothing itself.
An agent that consumed its own monitor would decide what listens to it. The point of an analysis port is that the producer does not know or care who is listening — that is what lets the same agent feed a scoreboard in one environment, a coverage collector in another, both in a third, and neither in a fourth without any of them being a special case.
6. The Agent and Its Configuration
// -----------------------------------------------------------------------------
// i2c_agent_config.sv
// Everything an agent needs to know, in one object handed down rather than looked up.
//
// NOT EXECUTED -- see i2c_if.sv.
//
// WHY A CONFIG OBJECT AND NOT A SET OF config_db ENTRIES. Ten separate `uvm_config_db`
// entries are ten independent chances to mistype a field name, and a mistyped name does
// not fail -- `get` returns 0 and the field keeps its default. The classic version of
// this bug is a timeout that silently stays at its initial value, so a test that should
// report a stuck bus instead runs for the whole simulation. One object means one `get`
// whose failure is checked once, loudly.
//
// The virtual interface lives here too, for the same reason: it is the field most often
// absent, and a null virtual interface presents as a null dereference thousands of lines
// from its cause.
// -----------------------------------------------------------------------------
// Which side of the bus this agent plays. A participant is one or the other, never
// both: building a controller driver and a responder driver in one agent would put two
// drivers on one pull index, and the resolved line cannot reveal that.
typedef enum { I2C_CONTROLLER, I2C_TARGET } i2c_role_e;
class i2c_agent_config extends uvm_object;
`uvm_object_utils(i2c_agent_config)
// ---- connection ---------------------------------------------------------
virtual i2c_if vif;
// Which pull-bit index this agent owns. Two agents sharing an index would mask each
// other's releases, and the resolved line cannot reveal it -- low is low.
int unsigned drv_index = 0;
// ---- active or passive --------------------------------------------------
// The single switch Chapter 20.4 is about. Passive means the agent builds no
// sequencer and no driver at all, rather than building them and asking them not to
// drive: a component that exists and is asked to be quiet is a component that can be
// asked incorrectly.
uvm_active_passive_enum is_active = UVM_ACTIVE;
// Controller or target. Read by the agent's build_phase to decide WHICH driver to
// create, which is why it is configuration and not a parameter: the same agent class
// serves both sides, and a project adding a second master changes a field.
i2c_role_e role = I2C_CONTROLLER;
// ---- timing -------------------------------------------------------------
// Clocks per half bit period. The monitor does NOT use this, and must not: it is
// edge-driven precisely so that clock stretching needs no configuration.
int unsigned half_period = 16;
// Clocks to wait for a stretched SCL before declaring the bus stuck. This is a
// POLICY, not a protocol fact -- the specification places no bound on a stretch, so
// there is nothing to derive. It says how patient this environment intends to be.
int unsigned stretch_timeout = 4000;
// ---- responder policy ---------------------------------------------------
// Thin on purpose. A responder that modelled the target's register map would agree
// with the target about every register decision including the wrong ones, which is
// Chapter 20.4's common-mode failure arriving through the stimulus.
bit [6:0] resp_addr = 7'h50;
bit resp_ack = 1'b1; // answer our address, or refuse it
int unsigned resp_nack_after = 0; // acknowledge N data bytes, then refuse (0 = all)
int unsigned resp_stretch_clks = 0; // hold SCL after an acknowledge (0 = never)
bit [7:0] resp_read_base = 8'h00; // first byte of a read, incrementing
function new(string name = "i2c_agent_config");
super.new(name);
endfunction
// Fail loudly and early rather than at first use. `end_of_elaboration_phase` is the
// right place for a caller to invoke this: the topology is final and nothing has run.
function void check_valid(string ctx);
if (vif == null)
`uvm_fatal("CFG", {ctx, ": i2c_agent_config.vif is null"})
if (half_period < 4)
`uvm_fatal("CFG", {ctx, ": half_period below 4 leaves no room to move SDA while SCL is low"})
if (stretch_timeout <= half_period)
`uvm_fatal("CFG", {ctx, ": stretch_timeout must exceed one half period or every transfer times out"})
endfunction
endclass // -----------------------------------------------------------------------------
// i2c_agent.sv
// Sequencer, driver and monitor, with one switch that decides which of them exist.
//
// NOT EXECUTED -- see i2c_if.sv.
//
// THE ACTIVE/PASSIVE SWITCH IS A BUILD DECISION, NOT A RUNTIME ONE. A passive agent
// builds no sequencer and no driver at all. The alternative -- build them and ask the
// driver not to drive -- means a component exists that is capable of driving and is
// being asked not to, and it can be asked incorrectly. Chapter 20.4's argument is that
// passivity should be structural, and here it is: in a passive agent there is no object
// in the hierarchy that owns a pull bit.
//
// THE MONITOR IS BUILT EITHER WAY. That is the whole benefit of it being passive: the
// same agent watches a bus it drives and a bus somebody else drives, with no mode flag,
// because it cannot have influenced either one.
//
// ONE AGENT PER PARTICIPANT, not one per bus. Two masters on a bus is two active agents
// with different `drv_index` values, which is also why the index is configuration rather
// than a constant: two agents sharing it would mask each other's releases, and the
// resolved line cannot reveal that -- low is low.
// -----------------------------------------------------------------------------
class i2c_agent extends uvm_agent;
`uvm_component_utils(i2c_agent)
i2c_agent_config cfg;
// Present in every build.
i2c_monitor monitor;
// Present only when active.
uvm_sequencer #(i2c_seq_item) sequencer;
i2c_master_driver driver;
i2c_responder_driver responder;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(i2c_agent_config)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no i2c_agent_config for the agent")
cfg.check_valid(get_full_name());
// Pass the config DOWN rather than letting children search for it. A child that
// does its own lookup can find a different object from its parent's, and the two
// then disagree about the interface or the timing with nothing to reveal it.
uvm_config_db #(i2c_agent_config)::set(this, "*", "cfg", cfg);
monitor = i2c_monitor::type_id::create("monitor", this);
if (cfg.is_active == UVM_ACTIVE) begin
sequencer = uvm_sequencer #(i2c_seq_item)::type_id::create("sequencer", this);
// A participant is EITHER a controller or a target. Building both would put two
// drivers on one pull index, which is a silent fault.
if (cfg.role == I2C_CONTROLLER)
driver = i2c_master_driver::type_id::create("driver", this);
else
responder = i2c_responder_driver::type_id::create("responder", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (cfg.is_active == UVM_ACTIVE && cfg.role == I2C_CONTROLLER)
driver.seq_item_port.connect(sequencer.seq_item_export);
// The responder takes no items: it is a state machine over edges, not a loop over
// a queue, so it is deliberately NOT connected to the sequencer. A responder wired
// to a sequencer would block waiting for stimulus that never comes.
endfunction
// The monitor's port is exported rather than consumed here. An agent that subscribed
// to its own monitor would decide what listens to it, and the point of an analysis
// port is that the agent does not know or care.
function uvm_analysis_port #(i2c_txn) txn_port();
return monitor.ap;
endfunction
endclassA driver and a monitor watching different buses
Pitfall — every component looking up its own configuration
// Each component fetches its own config from the top of the database. It looks
// tidier than threading an object through the hierarchy.
//
// // in the driver
// function void build_phase(uvm_phase phase);
// super.build_phase(phase);
// if (!uvm_config_db #(virtual i2c_if)::get(this, "", "vif", vif))
// uvm_fatal("NOVIF", "no interface");
// endfunction
//
// // in the monitor -- same lookup, independently
// if (!uvm_config_db #(virtual i2c_if)::get(this, "", "vif", vif))
// uvm_fatal("NOVIF", "no interface");
//
// The top level sets one entry for the DUT bus. A second test later adds a
// wildcard entry for a second bus:
//
// uvm_config_db #(virtual i2c_if)::set(null, "*", "vif", bus_b);
//
// Resolution is by path specificity and set order. The driver and the monitor sit
// at different depths, so they do not necessarily resolve the same way -- and now
// the driver drives bus A while the monitor watches bus B.
//
// The symptom: the monitor reports NOTHING while the driver reports a completed
// transfer. The scoreboard sees no transactions and reports that it checked zero.
// Everything looks like a connection problem in the monitor, which is fine.Pitfall — passive implemented as a flag the driver checks
// A passive agent that builds everything and asks the driver to stay quiet:
//
// function void build_phase(uvm_phase phase);
// super.build_phase(phase);
// sequencer = uvm_sequencer#(i2c_seq_item)::type_id::create("sequencer", this);
// driver = i2c_master_driver::type_id::create("driver", this);
// monitor = i2c_monitor::type_id::create("monitor", this);
// driver.passive = (cfg.is_active == UVM_PASSIVE); // <-- the promise
// endfunction
//
// // in the driver
// task drive_bit(bit v);
// if (!passive) vif.drv_cb.sda_drive_low[drv_idx] <= ~v;
// ...
// endtask
//
// It works. Then someone adds a release in the reset path, outside the guard:
//
// task reset_lines();
// vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0; // no 'if (!passive)'
// vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
// endtask
//
// A release looks harmless. It is not: this agent is passive because a DIFFERENT
// agent owns index drv_idx, and the release clears that agent's pull. The active
// master loses the bus mid-byte, intermittently, with no component reporting
// anything wrong.7. What 21.7 Settled
Passive means not built. A flag the driver checks protects only the statements it encloses, and a release written outside the guard clears another agent's pull — a fault the resolved bus cannot report.
A participant has one role. Building both drivers puts two owners on one drive index, which is undetectable from the line and looks like arbitration.
Configuration is fetched once and handed down. Independent lookups can bind two components in one agent to different objects, with no error anywhere and a symptom that points at the wrong component.
Validation happens where the topology is final, and each check corresponds to a misconfiguration whose natural symptom is misleading.
The monitor's port is exported, not consumed, which is what makes the agent reusable by environments that want different subscribers or none.
Next, several agents plus the components that decide whether any of it was correct. Chapter 21.8 — The Environment.
Continue learning
Related tutorials
- Related topic
The Environment — Agents, Scoreboard, Coverage and Configuration
The one connection that decides whether an environment can detect anything, why the contract is copied from the datasheet rather than read from the DUT, and why a checker needs one negative test per comparison path — a mutation that disabled half a scoreboard survived a suite that already had one.
- Related topic
UART UVM Environment Architecture
How agent, scoreboard, reference model, coverage and configuration fit together for a serial asynchronous interface, what each UVM class replaces from the directed environment, and what the methodology does not change.
- 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.
