Skip to content
VLSI Mentor

I²C · Module 21

Transaction Reconstruction in the Monitor

The edge-to-transaction state machine, why a repeated START both ends and begins a transfer in one cycle, and what to publish when the traffic is illegal. Includes two failure signatures worth memorising — values belonging to the next transfer, and a count exactly half of what it should be.

Chapter 21.5 established what the monitor is. This chapter is about the decision inside it: given a stream of edges, what objects come out, and — the question that actually separates a working monitor from one that works only on cooperative traffic — what comes out when the traffic is wrong.

1. Accumulate, Then Publish

A transaction cannot be published as it is discovered, because it is not complete until the framing event that ends it. So the monitor keeps a single accumulator object, fills it in as bytes arrive, and publishes it at a framing event.

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_monitor.sv — the accumulator, and when it is released
         i2c_txn acc;
         acc = i2c_txn::type_id::create("acc");
         forever begin
            ...
            if (start_now) begin
               if (active) begin              // a PREVIOUS transfer is open: end it
                  acc.ended_by_restart = 1'b1;
                  acc.truncated        = in_byte;
                  publish(acc);
               end
               acc = i2c_txn::type_id::create("acc");   // a NEW object, not a reset one
               acc.began_with_restart = active;
               ...

Two details there are load-bearing.

A new object, not a cleared one. The accumulator is published by handle. Clearing and reusing it would mutate the object a subscriber is still holding — and the scoreboard would compare the next transfer's fields while believing it had this one. The bug appears as a scoreboard reporting mismatches whose values belong to a transfer that had not happened yet, and it is a class of fault that does not exist in the struct-based version of 20.3 because a struct is copied on assignment.

acc.began_with_restart = active. Whether this transfer opened with a repeated START is precisely the question "was a transfer already open", which the monitor knows and nobody else does.

2. A Repeated START Is Two Events in One Cycle

This is the reconstruction's central awkwardness. A repeated START ends one transfer and begins another, simultaneously, and a monitor that treats framing as a set of mutually exclusive cases will lose one of them.

A state diagram for transaction reconstruction. An idle state moves on a START edge to an accumulating state. Within accumulating, a rising SCL edge either shifts a bit or, at the ninth edge, completes a byte with its acknowledge and returns. A STOP edge publishes the transaction and returns to idle. A START edge while accumulating both publishes the open transaction and re-enters accumulating. A truncation path leads from mid-byte framing to publish.Idleno transfer openAccumulatingbytes into accShift a bitat SCL riseByte completeninth edge, withackPublishto the analysisportTruncatedframing mid-byteSTARTSCL riseninthSTOPrestart12
Figure 1 — the reconstruction state machine. Note that the START edge is the only transition that both publishes and re-initialises: it closes the open transfer and opens the next one in the same cycle. The ninth-edge path is where a byte and its acknowledge become one fact together, and the dashed return is the abort case — framing arriving mid-byte, which publishes a transfer marked truncated rather than discarding the evidence.

A consumer that assumed a STOP always precedes a START would lose the second phase entirely — and the write-pointer-then-read access that every register device uses is exactly two phases joined by a repeated START. So the most common real transaction shape is the one a naive reconstruction drops.

3. What to Emit When the Traffic Is Illegal

Three illegal or unusual situations, and the decision in each case is a design decision rather than an obvious consequence.

A transfer that ends mid-byte

Framing arrives with some bits accumulated. Those bits never completed, no acknowledge slot occurred, and no device received them — so they are not data.

The choice is between discarding the transfer silently and publishing it marked truncated. This monitor publishes it.

An SDA change while SCL is high, mid-byte

Not an error to be flagged, and not a data bit to be shrugged at. By the specification it is a START or a STOP, and every conforming device will treat it as one. So the monitor treats it the same way: the partial byte is dropped and a new phase opens.

This is the behaviour Chapter 20.9's error injector relies on to reframe a real target mid-transfer, and it is why the injected fault is indistinguishable from legal framing — because it is not a corrupted START, it is a START.

A byte with no acknowledge slot

Eight bits and then framing. The byte is complete as bits and incomplete as a protocol event: nobody was given the chance to accept it. Treated the same as any truncation — the transfer is published, the byte is not in the payload.

4. Per Phase, Not Per Transfer

After a repeated START the data count restarts, because the direction bit applies to the phase rather than to the bus occupancy.

A write-pointer-then-read sequence is one occupancy and two phases with different directions. A count spanning both would be meaningless: three bytes total, of which one was written and two were read, is not a fact about anything. Chapter 20.7's T6 exists specifically to check that the count restarts, because a monitor that accumulated across phases would produce plausible totals that no consumer could interpret.

5. The Reconstruction

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_monitor.sv — the edge-to-transaction state machine
   // -----------------------------------------------------------------------------
   // 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

Why the byte is published at the ninth edge and not the eighth

10 cycles
A ten-cycle waveform. SCL alternates low and high. SDA carries eight data bits followed by an acknowledge bit that is low. A byte published signal stays low throughout and rises only at the ninth SCL rising edge. Markers label the first sampling edge and the ninth edge where byte and acknowledge are reported together.bits assembledbits assembledthe acknowledge slotthe acknowledge slotfirst sample: SDA stable by rulefirst sample: SDA stable byruleninth edge: byte AND ackninth edge: byte AND ackSCLSDAbyte outt0t1t2t3t4t5t6t7t8t9
Compressed: a real byte takes eight rising edges and this shows four, plus the acknowledge. The point is where the publication happens. Reporting at the eighth edge would make every consumer responsible for pairing a byte with a later acknowledge event, and each would have to get it right independently.
Figure 2 — one byte reconstructed from edges. SDA is sampled at each rising edge because that is the instant the protocol guarantees it is stable. The ninth rising edge carries the acknowledge, and that is where the byte is published — eight bits are not yet a protocol fact, because whether they were received is undetermined until this slot.

The scoreboard that compared a transfer that had not happened yet

Pitfall — an accumulator cleared and reused instead of replaced
Buggy Code
// Reconstruction with one accumulator object, cleared between transfers. It looks
// like an obvious efficiency: one allocation instead of thousands.
//
//    i2c_txn acc;
//    function void new_phase();
//       acc.addr = 0;  acc.read = 0;  acc.addr_acked = 0;
//       acc.data.delete();  acc.acks.delete();
//       acc.began_with_restart = 0;  acc.truncated = 0;
//    endfunction
//
//    if (stop_now) begin
//       acc.ended_by_stop = 1'b1;
//       ap.write(acc);            // publishes the HANDLE
//       new_phase();              // ... and immediately clears the object
//    end
//
// The scoreboard holds that handle. Analysis ports are not queues -- write() is a
// function call, but a subscriber that stores the handle for later, or that is
// reached through a TLM FIFO, sees the object as it is WHEN IT LOOKS.
//
// So the scoreboard compares fields that have been cleared, or worse, fields the
// NEXT transfer has already filled in. It reports mismatches whose values belong
// to a transfer that had not happened when the error was raised.
Pitfall — a repeated START treated as mutually exclusive with a START
Buggy Code
// Framing handled as a case over three exclusive events:
//
//    if      (start_now)   begin acc = new(); active = 1; end
//    else if (restart_now) begin acc = new(); active = 1; end
//    else if (stop_now)    begin publish(acc); active = 0; end
//
// where restart_now is start_now && active.
//
// The open transfer is NEVER PUBLISHED. A repeated START creates a new
// accumulator and the previous one is dropped, so a write-pointer-then-read
// sequence -- the access every register device uses -- reports only the READ.
//
// The scoreboard therefore never sees the write that set the pointer, so its model
// of the pointer is wrong from the first transfer onward, and it reports a DATA
// mismatch on every subsequent read. Every value it prints is plausible and the
// error count is large, which makes it look like a DUT fault in the register file.

6. What 21.6 Settled

Accumulate and publish at framing, with a fresh object each time. Publishing a handle transfers a reference, so reusing the accumulator lets a subscriber's view of a past transfer become the present one — a failure mode that appears only once transactions are class handles.

A repeated START is an ending and a beginning in the same cycle. Treating framing as exclusive cases drops the transfer it terminates, which is the most common real access shape, and makes every downstream comparison wrong.

Illegal traffic gets published, described rather than judged. Truncation is flagged, the partial byte is excluded from the payload, and an SDA change while SCL is high is treated as what the specification says it is — framing — because every conforming device will treat it that way too.

The data count is per phase. A count spanning a direction change is not a fact about anything.

Two failure signatures worth memorising. Reported values belonging to the next transfer means handle aliasing; a transaction count that is exactly half the expected number means a missed publication at a repeated START.

Next, the three components become one reusable unit with a single switch that decides which of them exist. Chapter 21.7 — Assembling the I²C Agent.

Continue learning