Skip to content
VLSI Mentor

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.

interpretationwhat a passive agent contains
build everything, set a flag, ask the driver not to drivea driver that is capable of driving and has been asked not to
do not build the driver or the sequencer at allno object that owns a bus line
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_agent.sv — the switch decides what exists
         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:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_agent.sv — one get, then set for the subtree
         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.

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_agent_config.sv — failing early, with a reason
      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

Each 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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_agent.sv — connect_phase, and one thing it does not do
         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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_agent_config.sv — one object, handed down
   // -----------------------------------------------------------------------------
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_agent.sv — the switch that decides what exists
   // -----------------------------------------------------------------------------
   // 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

   endclass
A block diagram comparing two agent builds. The active build shows a configuration object feeding a sequencer, a driver and a monitor, with the sequencer connected to the driver and the driver connected to the bus. The passive build shows the same configuration object feeding only a monitor, with no driver and no sequencer, and the monitor reading the bus. Both builds export the monitor's analysis port.agent_confighanded downSequencerACTIVE onlyDriver orresponderACTIVE only · byroleMonitoralways builtResolved busi2c_ifAnalysis portexported eitherwayitemspull lowread only12
Figure 1 — the agent in both builds, side by side. The active build contains a sequencer, one driver chosen by role, and a monitor; the passive build contains only the monitor, and therefore contains no object that owns a bus line. The monitor's analysis port is exported in both, unchanged, which is what lets a consumer connect to an agent without knowing which build it is.

A driver and a monitor watching different buses

Pitfall — every component looking up its own configuration
Buggy Code
// 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
Buggy Code
// 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