Skip to content
VLSI Mentor

I²C · Module 21

The Monitor — Sampling the Bus Without Disturbing It

Three checkable properties: it drives nothing because its modport has no outputs, its framing is its own because a shared oracle agrees in error, and it is edge-driven so clock stretching needs no handling. Includes the bring-up shortcut that ends with an environment supplying the acknowledge it is meant to be checking.

The monitor is the component every other component's credibility rests on. If it is wrong, the scoreboard compares two wrong things, the coverage report describes traffic that did not happen, and every green test in the regression is a statement about the monitor.

It is also the component with the most ways to be quietly useless, and none of them look like bugs.

1. Three Properties, Each One Checkable

A reviewer should be able to verify each of these by reading, which is the point of stating them as properties rather than as intentions.

It drives nothing. Not because it chooses not to — because its connection has no output in it.

Its framing is its own. Not the DUT's, and not the driver's.

It is edge-driven. There is no bit period, bus rate or divider anywhere in the file.

The rest of the chapter is why each one matters, and what goes wrong when it is relaxed.

2. Passivity Belongs in the Connection

Chapter 20.4 argued that a monitor must be unable to drive. In a class-based environment there are three ways to arrange that, and only one of them survives maintenance.

approachhow it fails
a passive bit guarding every drive statementworks until the bit is defaulted wrongly, inverted, or one drive is written outside the guard
a comment saying the monitor does not driveis true when written
a modport whose clocking block has no outputscannot fail without changing the modport, which appears in a diff
Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_if.sv — the monitor's entire view of the bus
      clocking mon_cb @(posedge clk);
         input scl, sda, scl_holders, sda_holders;
      endclocking

      modport mon_mp (clocking mon_cb);

There is no output. A monitor connected this way has nothing to assign, so its passivity is a consequence of what it can reach rather than a rule it follows.

3. Independence Costs a Reimplementation

Module 18's target contains a framing detector. It is correct, it is verified, and reusing it inside the monitor is the normal engineering choice.

It would also destroy the monitor. Two instances of the same logic given identical inputs agree always — including when the logic is wrong. A monitor whose framing comes from the design's framing detector cannot detect a framing bug, which is a substantial fraction of what it exists for.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_monitor.sv — framing written from UM10204, not from the target
            start_now = sda_fall && scl_now && scl_d;
            stop_now  = sda_rise && scl_now && scl_d;

Note scl_now && scl_d rather than scl_now alone. An SDA transition in the same cycle as an SCL edge is a data bit changing at a bit boundary, not framing; requiring SCL to have been high in the previous cycle as well restricts framing to transitions in the middle of a high period. Removing that term is a mutation the VHDL twin's bench kills in twenty-one checks, which measures how much of the reconstruction rests on it.

4. Edge-Driven, So Stretching Needs No Handling

There is no half_period anywhere in the monitor, and the agent's configuration object carries one. That omission is deliberate.

Clock stretching exists to make the bus rate unpredictable. A monitor that counted cycles between bits would need to know the rate, and would break the moment a target held SCL low — the very feature the environment most needs to be able to watch. Worse, its reports would stay plausible while being wrong, which sends debugging to the DUT.

An edge-driven monitor sees a stretched transfer as a transfer whose edges are further apart, which is what it is. It needs no configuration, and therefore cannot be misconfigured.

Sampling at the rising edge is not a convenience either — it follows from the protocol's own guarantee. SDA may not change while SCL is high, so the rising edge is the instant the value is defined. Any other sampling point reads a line that is permitted to be in transition.

5. The Byte and Its Acknowledge Are One Fact

byte_ap publishes at the ninth rising edge, not the eighth, and carries the byte with its acknowledge together.

Eight bits are not yet a protocol fact: whether they were received is undetermined until the ninth slot. Reporting at the eighth would force every consumer to remember the byte and wait for a separate acknowledge event — and every consumer would have to get that pairing right independently.

6. The Monitor

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_monitor.sv — two inputs, no outputs that reach the bus
   // -----------------------------------------------------------------------------
   // i2c_monitor.sv
   // The passive component, and the one with the most ways to be quietly useless.
   //
   // NOT EXECUTED -- see i2c_if.sv. The reconstruction algorithm is a transcription of
   // Chapter 20.7's `i2c_mon`, which was simulated against nine hand-built traces -- two of
   // them deliberately illegal -- in SystemVerilog and VHDL, and survived eighteen
   // mutations.
   //
   // THREE PROPERTIES, and each one is a decision a reviewer should be able to check:
   //
   //   1. IT DRIVES NOTHING. It connects through `mon_mp`, whose clocking block has no
   //      outputs, so there is nothing in its view of the interface to drive. Passivity is
   //      a property of the connection rather than of the author's intentions, which is
   //      what makes it survive a bring-up shortcut six months from now.
   //
   //   2. ITS FRAMING IS ITS OWN. Not the DUT's framing detector, and not the driver's.
   //      Two instances of one implementation given identical inputs agree ALWAYS,
   //      including when the implementation is wrong -- so a monitor that borrows the
   //      design's framing cannot detect a framing bug, which is a large part of what it
   //      exists for. The rules below are written from UM10204. This is the one place in
   //      this curriculum where duplicating verified logic is correct.
   //
   //   3. IT IS EDGE-DRIVEN. There is no bit period, no bus rate and no divider anywhere
   //      in this file, which is why clock stretching needs no handling: a stretched
   //      transfer is a transfer whose edges are further apart. A monitor that counted
   //      cycles would break on the one feature it most needs to watch, and its reports
   //      would stay plausible while being wrong -- which sends debugging to the DUT.
   //
   // IT REPORTS, IT DOES NOT JUDGE. A NACK is published as observed. It is frequently the
   // correct behaviour -- a foreign address, a read-only register, a controller ending a
   // read -- and deciding needs a contract the monitor does not have.
   // -----------------------------------------------------------------------------

   class i2c_monitor extends uvm_monitor;

      `uvm_component_utils(i2c_monitor)

      virtual i2c_if   vif;
      i2c_agent_config cfg;

      // Published to anything that cares, and to nothing in particular. An analysis port
      // is a broadcast: the monitor does not know whether a scoreboard, a coverage
      // collector, both or neither is listening, which is what lets it be reused.
      uvm_analysis_port #(i2c_txn) ap;

      // Byte-level, for consumers that work below the transaction level.
      uvm_analysis_port #(i2c_txn) byte_ap;

      int unsigned n_txns, n_bytes, n_nacks;

      function new(string name, uvm_component parent);
         super.new(name, parent);
      endfunction

      function void build_phase(uvm_phase phase);
         super.build_phase(phase);
         ap      = new("ap", this);
         byte_ap = new("byte_ap", this);
         if (!uvm_config_db #(i2c_agent_config)::get(this, "", "cfg", cfg))
            `uvm_fatal("NOCFG", "no i2c_agent_config for the monitor")
         vif = cfg.vif;
         if (vif == null)
            `uvm_fatal("NOVIF", "i2c_agent_config.vif is null in the monitor")
      endfunction

      task run_phase(uvm_phase phase);
         super.run_phase(phase);
         collect();
      endtask

      // ---- reconstruction -----------------------------------------------------
      task collect();
         bit scl_d = 1'b1, sda_d = 1'b1;
         bit scl_now, sda_now;
         bit scl_rise, sda_fall, sda_rise;
         bit start_now, stop_now;

         bit          active = 1'b0;
         bit          addr_seen = 1'b0;
         bit          in_byte = 1'b0;
         bit [7:0]    shreg = 8'h00;
         int unsigned bitcnt = 0;
         i2c_txn      acc;

         acc = i2c_txn::type_id::create("acc");
         forever begin
            @(vif.mon_cb);
            scl_now = vif.mon_cb.scl;
            sda_now = vif.mon_cb.sda;

            scl_rise = scl_now && !scl_d;
            sda_fall = !sda_now && sda_d;
            sda_rise = sda_now && !sda_d;

            // START and STOP are SDA transitions WHILE SCL IS HIGH. Both the current and
            // the previous SCL are required high: an SDA transition in the same cycle as
            // an SCL edge is a data bit changing at a bit boundary, not framing. Removing
            // the previous-cycle term is a mutation that the VHDL twin's bench kills in
            // twenty-one checks, which measures how much of the reconstruction rests on it.
            start_now = sda_fall && scl_now && scl_d;
            stop_now  = sda_rise && scl_now && scl_d;

            if (start_now) begin
               // A repeated START both ENDS one transfer and BEGINS another, in the same
               // cycle. A consumer that assumed a STOP always precedes a START would lose
               // the second phase entirely.
               if (active) begin
                  acc.ended_by_restart = 1'b1;
                  acc.ended_by_stop    = 1'b0;
                  acc.truncated        = in_byte;
                  publish(acc);
               end
               acc = i2c_txn::type_id::create("acc");
               acc.began_with_restart = active;
               active    = 1'b1;
               addr_seen = 1'b0;
               in_byte   = 1'b0;
               bitcnt    = 0;
               shreg     = 8'h00;
            end
            else if (stop_now) begin
               if (active) begin
                  acc.ended_by_restart = 1'b0;
                  acc.ended_by_stop    = 1'b1;
                  // Framing arrived mid-byte: those bits never completed, no acknowledge
                  // slot occurred, and no device received them. They are not data.
                  acc.truncated        = in_byte;
                  publish(acc);
               end
               active  = 1'b0;
               in_byte = 1'b0;
            end
            else if (scl_rise && active) begin
               if (bitcnt < 8) begin
                  shreg   = {shreg[6:0], sda_now};
                  bitcnt++;
                  in_byte = 1'b1;
               end else begin
                  // The NINTH rising edge, and the byte is published WITH its acknowledge.
                  // Eight bits are not yet a protocol fact: whether they were received is
                  // undetermined until this slot. Splitting them would make every consumer
                  // responsible for pairing a byte with a later acknowledge event, and
                  // every consumer would have to get that pairing right on its own.
                  n_bytes++;
                  if (sda_now) n_nacks++;
                  if (!addr_seen) begin
                     acc.addr       = shreg[7:1];
                     acc.read       = shreg[0];
                     acc.addr_acked = ~sda_now;
                     addr_seen      = 1'b1;
                  end else begin
                     acc.data = new[acc.data.size() + 1](acc.data);
                     acc.acks = new[acc.acks.size() + 1](acc.acks);
                     acc.data[acc.data.size() - 1] = shreg;
                     acc.acks[acc.acks.size() - 1] = ~sda_now;
                  end
                  publish_byte(shreg, ~sda_now, !addr_seen);
                  bitcnt  = 0;
                  shreg   = 8'h00;
                  in_byte = 1'b0;
               end
            end

            scl_d = scl_now;
            sda_d = sda_now;
         end
      endtask

      function void publish(i2c_txn t);
         n_txns++;
         ap.write(t);
      endfunction

      function void publish_byte(bit [7:0] d, bit acked, bit is_addr);
         i2c_txn b;
         b = i2c_txn::type_id::create("byte");
         b.data = new[1];
         b.acks = new[1];
         b.data[0] = d;
         b.acks[0] = acked;
         byte_ap.write(b);
      endfunction

   endclass

Two analysis ports, and the reason there are two is worth stating. ap publishes assembled transactions for the scoreboard; byte_ap publishes bytes for consumers working below the transaction level. A single port carrying both would force every subscriber to discriminate, and a subscriber that got the discrimination wrong would silently process the wrong stream.

The framing check that had never failed, and could not be made to

Pitfall — a monitor that instantiated the design's framing detector
Buggy Code
// Sensible-looking reuse. The target already has a verified framing module, so
// the monitor instantiates the same one:
//
//    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 environment's framing check
//    if (dut_start !== mon_start) uvm_error("FRAMING", "mismatch");
//
// The framing module has a bug: it treats ANY SDA fall as a START.
//
//    assign start = sda_fall;                   // missing: & scl & scl_d
//
// So a data bit falling at the wrong moment starts a transfer. The target does
// this. THE MONITOR DOES EXACTLY THE SAME THING, because it is the same module,
// and agrees with the target on every trace.
//
// The check has never fired and CANNOT be made to fire -- break the framing
// module further and both instances break together. The environment reports a
// clean framing check on a target that starts transfers on noise.
Pitfall — a monitor that counted cycles, and a target that stretched
Buggy Code
// A monitor written against a known 100 kHz bus:
//
//    localparam CYCLES_PER_BIT = 100;
//    task collect();
//       forever begin
//          @(vif.mon_cb);
//          if (active) begin
//             tick++;
//             if (tick == CYCLES_PER_BIT/2) begin     // sample mid-bit
//                shreg = {shreg[6:0], vif.mon_cb.sda};
//                bitcnt++;  tick = 0;
//             end
//          end
//       end
//    endtask
//
// Works perfectly. Then a target stretches the clock, and the monitor's byte
// boundaries drift away from the real ones -- so bytes are reported containing
// bits from two adjacent bus bits, and eventually the byte COUNT diverges too.
//
// The reports are garbage, and they are garbage about a transfer that was
// entirely correct. Debugging starts at the target.

7. What 21.5 Settled

Passivity belongs in the modport. A view with no outputs cannot be driven, which survives the bring-up shortcut that a parameter and a comment do not — and that shortcut ends with an environment supplying the acknowledge it is supposed to be checking.

Independence requires reimplementation. Framing written from the specification rather than borrowed from the design, because two instances of one implementation agree always, including in error. The symptom of getting it wrong is a check that cannot be made to fail.

Edge-driven, with no rate anywhere. Clock stretching then needs no handling, the component cannot be misconfigured, and the sampling instant follows from the protocol's stability guarantee rather than from a divider.

A byte and its acknowledge are one fact, published together at the ninth edge, because eight bits are not yet a protocol fact.

An unconnected analysis port is silently dead, which is why the port is checked structurally and why the monitor exports counters that distinguish "never looked" from "looked, and nobody listened".

Next: the reconstruction state machine itself, and the question this chapter deferred — what a monitor should emit when the traffic on the bus is illegal. Chapter 21.6 — Transaction Reconstruction in the Monitor.

Continue learning