Skip to content
VLSI Mentor

I²C · Module 20

Monitor Architecture and Transaction Reconstruction

Rebuilding a transaction from raw edges without assuming the design is correct and without borrowing a line of its framing logic. Covers why an edge-driven monitor handles clock stretching for free, why the byte and its acknowledge are one fact, and the two illegal traces that make the framing rules testable at all.

The monitor is the hardest component in the environment to get right, and the reason is not that reconstruction is difficult. It is that every shortcut available to it is a shortcut that makes the environment agree with the design.

It has to answer, from two wires: where did a transfer start, which address was it, which direction, how many bytes, was each one acknowledged, and how did it end. All of that is derivable from SCL and SDA. It is also derivable from the design's own internal state, and from the driver's idea of what it sent, and both of those are much easier.

1. What It Must Know, and What It Must Not

the monitor knowsthe monitor must not know
the resolved level of SCL and SDAwhat the driver intended to send
the specification's framing rulesthe target's register map
how to assemble bits into bytesany expected value
the direction bit's positionwhether what it saw was correct

Two inputs, and one of them is a clock for its own registers. That is the entire interface to the outside world, and it is enforced by the port list rather than by discipline — Chapter 20.4 argues that case.

The right-hand column has a consequence that is easy to state and easy to violate: the monitor reports a NACK, it does not report an error. A NACK is frequently the correct behaviour — a foreign address, a read-only register, the controller terminating a read. A monitor that flags it has made a judgement using knowledge it does not have, and the environment now reports failures on correct traffic.

2. Independence Costs a Reimplementation

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

It would also destroy the monitor. Two instances of the same logic given the same 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.

So the monitor implements framing again, from the specification:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_mon.sv — framing, written from UM10204 rather than from the target
      wire  scl_rise = scl & ~scl_d;
      wire  scl_fall = ~scl & scl_d;
      wire  sda_fall = ~sda & sda_d;
      wire  sda_rise = sda & ~sda_d;

      // Framing is an SDA edge while SCL is HIGH -- the same PROTOCOL DEFINITION as
      // Chapter 18.3, reached by a different implementation.
      wire start_now = sda_fall & scl & scl_d;
      wire stop_now  = sda_rise & scl & scl_d;

Note scl & scl_d rather than scl. An SDA transition in the same cycle as an SCL edge is not framing — it is a data bit changing at a boundary — and requiring SCL to have been high in the previous cycle as well is what makes the distinction. Mutation M10 removes the SCL condition entirely and the VHDL twin's bench kills it in twenty-one checks, which is a measure of how much of the reconstruction depends on framing being right.

3. Edge-Driven, Which Makes Stretching Free

The monitor has no notion of a bit period, a baud rate, or a clock divider. Everything it does is triggered by an edge on SCL or SDA.

That is not a stylistic choice; it is what makes clock stretching a non-issue. A monitor that counted cycles between bits would need to know the bus rate, and would break the moment a target held SCL low — the very feature the environment most needs to be able to watch. An edge-driven monitor sees a stretched transfer as a transfer whose edges are further apart, which is exactly what it is.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_mon.sv — one byte, assembled at SCL rising edges
            end else if (scl_rise && txn_active) begin
               if (bitcnt < 4'd8) begin
                  shreg   <= {shreg[6:0], sda};
                  bitcnt  <= bitcnt + 4'd1;
                  in_byte <= 1'b1;
               end else begin
                  // The ninth bit: the acknowledge. LOW means acknowledged, and it is
                  // whoever is RECEIVING that drives it -- the monitor does not care
                  // which device, only what the bus said.
                  byte_valid   <= 1'b1;
                  byte_data    <= shreg;
                  byte_acked   <= ~sda;
                  byte_is_addr <= ~addr_seen;
                  n_bytes      <= n_bytes + 16'd1;
                  if (sda) n_nacks <= n_nacks + 16'd1;

                  if (!addr_seen) begin

Sampling SDA at SCL's rising edge is not a convenience. It is the protocol's own guarantee: SDA may not change while SCL is high, so the rising edge is the instant at which the value is defined. A monitor that sampled anywhere else would be reading a line that is permitted to be in transition.

4. The Byte and Its Acknowledge Are One Fact

byte_valid fires at the ninth rising edge, not the eighth, and carries byte_data and byte_acked together.

The reason is that a byte without its acknowledge is not yet a protocol fact. Eight bits have appeared on the wire; whether they were received is undetermined until the ninth slot. A monitor that reported at the eighth edge 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.

5. The Cases That Decide Whether It Works

Four reconstruction cases separate a monitor that works from one that works on cooperative traffic.

A repeated START ends one transaction and begins another. Not "ends" and not "begins" — both, in the same cycle. The monitor reports txn_done with txn_ended_by_restart set while simultaneously opening the next phase, and a consumer that assumed a STOP always precedes a START would lose the second phase entirely.

A truncated byte transfers nothing. If framing arrives mid-byte, the bits accumulated so far are not data. They never completed, no acknowledge slot occurred, and no device received them. The monitor discards them, and n_bytes does not increase.

An illegal SDA change while SCL is high is framing. Not an error to be flagged, and not a data bit to be shrugged at — by the specification it is a START or STOP, and every conforming device will treat it as one. The monitor treats it the same way, which means a partial byte is dropped and a new phase opens. This is the behaviour the fault injector in 20.9 relies on to reframe a real target mid-transfer.

The data count is per phase, not per transaction. After a repeated START the count restarts, because the phase is what the direction bit applies to. A write-pointer-then-read sequence is one bus occupancy and two phases with different directions, and a count that spanned both would be meaningless.

6. The Monitor

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_mon.sv — the passive monitor: two inputs, no drive port, independent framing
   // -----------------------------------------------------------------------------
   // i2c_mon.sv
   // A PASSIVE I²C monitor: resolved bus in, reconstructed transaction out.
   //
   // WHAT MAKES THIS A MONITOR AND NOT A SECOND DUT:
   //
   //   1. IT DRIVES NOTHING. There is no output that reaches the bus, no `drive_low`,
   //      no output enable. It has two inputs from the wire and everything else is an
   //      observation. A component that can affect what it measures is not a monitor.
   //
   //   2. IT READS THE RESOLVED BUS, not any participant's intent. Chapter 19.1 showed
   //      that on an open-drain bus a device's own drive intent and the line's actual
   //      level legitimately disagree -- a releasing device reads LOW while somebody
   //      else holds the line. A monitor built on intent reports a clean bus at the
   //      exact moment the bus is contended.
   //
   //   3. IT SHARES NO LOGIC WITH THE DUT. Module 18.3's framing detector and this
   //      block both implement "SDA edge while SCL is high", and they are SEPARATE
   //      IMPLEMENTATIONS on purpose. Reusing 18.3 here would make one defect appear
   //      identically in the design and in the checker, and the comparison would pass.
   //      Sharing a protocol DEFINITION is correct; sharing an implementation is
   //      common-mode failure. This module therefore samples levels rather than
   //      consuming one-cycle edge pulses, and counts bits with its own counter.
   //
   // WHAT IT RECONSTRUCTS, in the order the pipeline builds it:
   //
   //      resolved SDA/SCL
   //   -> START / repeated START / STOP           (SDA edge while SCL high)
   //   -> bit sampling                            (SDA at the SCL rising edge)
   //   -> byte + ninth-bit acknowledge            (8 + 1)
   //   -> address byte -> 7-bit address, direction
   //   -> data bytes, in order
   //   -> transaction boundary                    (START .. STOP or repeated START)
   //
   // WHAT IT DELIBERATELY DOES NOT DO: decide whether the traffic was CORRECT. It
   // reports what happened. Chapter 20.8's scoreboard decides whether that was right,
   // using an independently produced expectation. A monitor that also judged would
   // collapse observation and prediction into one component, and a single wrong
   // assumption would then be invisible.
   //
   // BOUNDED BY CONSTRUCTION: every state transition is driven by a sampled level
   // change, and the byte counter saturates. There is no wait, so there is nothing to
   // hang -- a dead bus simply produces no events, which is itself an observation.
   // -----------------------------------------------------------------------------

   module i2c_mon #(
      // Bytes of one transaction the monitor will retain. A real environment streams
      // these to an analysis consumer; retaining a window keeps this module testable
      // without a queue implementation, which Module 21 owns.
      parameter int MAX_BYTES = 8
   ) (
      input  logic clk,
      input  logic rst_n,

      // ---- the only inputs: the RESOLVED bus -----------------------------------
      // Not a participant's drive intent. Not a DUT internal signal.
      input  logic scl,
      input  logic sda,

      // ---- framing events, as one-cycle pulses ---------------------------------
      output logic saw_start,       // a START of either kind
      output logic saw_restart,     // ... and a transfer was already open
      output logic saw_stop,

      // ---- byte-level reconstruction -------------------------------------------
      // One cycle, at the NINTH SCL RISING EDGE -- the instant the acknowledge bit is
      // guaranteed stable. The byte and its acknowledge are reported together because
      // a byte without its acknowledge is not yet a protocol fact.
      output logic byte_valid,
      output logic [7:0] byte_data,
      output logic       byte_acked,      // 1 = the ninth bit was LOW (acknowledged)
      output logic       byte_is_addr,    // this byte was the address byte

      // ---- transaction-level reconstruction ------------------------------------
      output logic       txn_active,
      output logic [6:0] txn_addr,
      output logic       txn_read,        // direction latched from the address byte
      output logic       txn_addr_acked,
      output logic [3:0] txn_n_data,      // data bytes seen in this phase, saturating
      // One cycle, when a transaction ends at a STOP or a repeated START.
      output logic       txn_done,
      output logic       txn_ended_by_restart,

      // ---- counters, for a bench to assert against ------------------------------
      output logic [15:0] n_txns,
      output logic [15:0] n_bytes,
      output logic [15:0] n_nacks
   );

      // ---- the monitor's own view of the two lines -----------------------------
      // Its OWN registers, not Module 18's. One delay stage is all a monitor needs to
      // see an edge in an already-synchronous simulation; a real environment sampling
      // asynchronous pins would use Chapter 19.4's synchroniser first, and Section 6
      // of the chapter says what that costs a monitor's timestamps.
      logic scl_d, sda_d;
      wire  scl_rise = scl & ~scl_d;
      wire  scl_fall = ~scl & scl_d;
      wire  sda_fall = ~sda & sda_d;
      wire  sda_rise = sda & ~sda_d;

      // Framing is an SDA edge while SCL is HIGH -- the same PROTOCOL DEFINITION as
      // Chapter 18.3, reached by a different implementation.
      wire start_now = sda_fall & scl & scl_d;
      wire stop_now  = sda_rise & scl & scl_d;

      logic [3:0] bitcnt;
      logic [7:0] shreg;
      logic       in_byte;         // a byte is being assembled
      logic       addr_seen;       // the address byte of this phase has completed

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            scl_d <= 1'b1; sda_d <= 1'b1;
            saw_start <= 1'b0; saw_restart <= 1'b0; saw_stop <= 1'b0;
            byte_valid <= 1'b0; byte_data <= 8'h00; byte_acked <= 1'b0;
            byte_is_addr <= 1'b0;
            txn_active <= 1'b0; txn_addr <= 7'h00; txn_read <= 1'b0;
            txn_addr_acked <= 1'b0; txn_n_data <= 4'd0;
            txn_done <= 1'b0; txn_ended_by_restart <= 1'b0;
            bitcnt <= 4'd0; shreg <= 8'h00; in_byte <= 1'b0; addr_seen <= 1'b0;
            n_txns <= 16'd0; n_bytes <= 16'd0; n_nacks <= 16'd0;
         end else begin
            scl_d <= scl; sda_d <= sda;
            saw_start <= 1'b0; saw_restart <= 1'b0; saw_stop <= 1'b0;
            byte_valid <= 1'b0; txn_done <= 1'b0; txn_ended_by_restart <= 1'b0;

            // ---------------- framing ----------------------------------------
            if (start_now) begin
               saw_start <= 1'b1;
               if (txn_active) begin
                  // A repeated START ends the previous phase and opens a new one. The
                  // PREVIOUS transaction is reported now, which is why `txn_done` and
                  // `saw_restart` can be asserted in the same cycle.
                  saw_restart          <= 1'b1;
                  txn_done             <= 1'b1;
                  txn_ended_by_restart <= 1'b1;
                  n_txns               <= n_txns + 16'd1;
               end
               txn_active <= 1'b1;
               addr_seen  <= 1'b0;
               txn_n_data <= 4'd0;
               in_byte    <= 1'b0;
               bitcnt     <= 4'd0;
               shreg      <= 8'h00;
            end else if (stop_now) begin
               saw_stop <= 1'b1;
               if (txn_active) begin
                  txn_done <= 1'b1;
                  n_txns   <= n_txns + 16'd1;
               end
               txn_active <= 1'b0;
               addr_seen  <= 1'b0;
               in_byte    <= 1'b0;
               bitcnt     <= 4'd0;

            // ---------------- bit sampling ------------------------------------
            // SDA is sampled at the SCL RISING edge, because that is the only instant
            // the specification guarantees it is stable. Sampling on the falling edge
            // reads a value the transmitter is entitled to be changing -- mutation
            // M03 does exactly that.
            end else if (scl_rise && txn_active) begin
               if (bitcnt < 4'd8) begin
                  shreg   <= {shreg[6:0], sda};
                  bitcnt  <= bitcnt + 4'd1;
                  in_byte <= 1'b1;
               end else begin
                  // The ninth bit: the acknowledge. LOW means acknowledged, and it is
                  // whoever is RECEIVING that drives it -- the monitor does not care
                  // which device, only what the bus said.
                  byte_valid   <= 1'b1;
                  byte_data    <= shreg;
                  byte_acked   <= ~sda;
                  byte_is_addr <= ~addr_seen;
                  n_bytes      <= n_bytes + 16'd1;
                  if (sda) n_nacks <= n_nacks + 16'd1;

                  if (!addr_seen) begin
                     // First byte of the phase is the address: seven bits then R/W.
                     txn_addr       <= shreg[7:1];
                     txn_read       <= shreg[0];
                     txn_addr_acked <= ~sda;
                     addr_seen      <= 1'b1;
                  end else if (txn_n_data != 4'd15) begin
                     txn_n_data <= txn_n_data + 4'd1;
                  end
                  bitcnt  <= 4'd0;
                  shreg   <= 8'h00;
                  in_byte <= 1'b0;
               end
            end
         end
      end

   endmodule

One implementation note, because it looks like a shortcut and is not. The monitor keeps its own one-stage delay of SCL and SDA — scl_d and sda_d — rather than using anything from the design. One stage is all an edge detector needs in an already-synchronous simulation. A real environment sampling a physical bus would need the synchronisation of 19.4, and it is worth being explicit that the monitor here does not model metastability, because nothing in RTL simulation can.

7. The Bench: Traces Known by Construction

The monitor is verified against traces a bench drives directly onto the lines, with no design present. That is the only arrangement in which the expected reconstruction is known independently of anything being tested — the bench built the trace, so it knows what the answer is.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_mon_tb.sv — nine traces, two of them illegal
   // -----------------------------------------------------------------------------
   // i2c_mon_tb.sv
   // The monitor is verification code, so it gets verified.
   //
   // THE BENCH DRIVES THE BUS DIRECTLY, not through any DUT. That is deliberate: the
   // monitor's contract is "given this waveform, report this transaction", and the only
   // way to test it is to produce waveforms whose correct interpretation is known by
   // construction. Attaching a DUT here would mean a monitor failure and a DUT failure
   // were indistinguishable -- Chapter 20.9 attaches the real DUT, once the monitor is
   // trustworthy.
   //
   // THE TRACES INCLUDE MALFORMED ONES. A monitor exercised only on legal traffic has
   // been shown to decode, not to discriminate. T7 and T8 feed it a truncated byte and
   // an SDA change while SCL is high -- the two things the protocol forbids -- and
   // require the reconstruction to differ from the legal case.
   //
   // Every wait is a fixed number of clocks; nothing waits on the monitor.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_mon_tb;

      localparam int HALF = 6;         // clocks per SCL half-phase

      logic clk = 1'b0, rst_n = 1'b0;

      // The bench owns the bus outright here: two drive-intent signals resolved by a
      // wired-AND, which is what Chapter 19.1's pad law reduces to for one participant.
      // LOW or RELEASE only -- the bench never drives HIGH.
      logic scl_low = 1'b0, sda_low = 1'b0;
      wire  scl = ~scl_low;
      wire  sda = ~sda_low;

      logic saw_start, saw_restart, saw_stop;
      logic byte_valid, byte_acked, byte_is_addr;
      logic [7:0] byte_data;
      logic txn_active, txn_read, txn_addr_acked, txn_done, txn_ended_by_restart;
      logic [6:0] txn_addr;
      logic [3:0] txn_n_data;
      logic [15:0] n_txns, n_bytes, n_nacks;

      integer errors = 0;
      integer n, k;

      i2c_mon #(.MAX_BYTES(8)) dut (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .saw_start(saw_start), .saw_restart(saw_restart), .saw_stop(saw_stop),
         .byte_valid(byte_valid), .byte_data(byte_data), .byte_acked(byte_acked),
         .byte_is_addr(byte_is_addr),
         .txn_active(txn_active), .txn_addr(txn_addr), .txn_read(txn_read),
         .txn_addr_acked(txn_addr_acked), .txn_n_data(txn_n_data),
         .txn_done(txn_done), .txn_ended_by_restart(txn_ended_by_restart),
         .n_txns(n_txns), .n_bytes(n_bytes), .n_nacks(n_nacks));

      // ---- an observer, so one-cycle reports can be checked after the fact --------
      // The monitor emits `byte_valid` for exactly one cycle. A bench that only looked
      // at it between transfers would never see it, so the reports are captured here --
      // which is itself the pattern Chapter 20.4 calls an analysis consumer.
      integer nb = 0;
      logic [7:0] cap_data  [0:15];
      logic       cap_ack   [0:15];
      logic       cap_isaddr[0:15];
      integer ntd = 0, nrs = 0;
      always @(posedge clk) begin
         if (rst_n) begin
            if (byte_valid && nb < 16) begin
               cap_data[nb]   <= byte_data;
               cap_ack[nb]    <= byte_acked;
               cap_isaddr[nb] <= byte_is_addr;
               nb <= nb + 1;
            end
            if (txn_done)   ntd <= ntd + 1;
            if (saw_restart) nrs <= nrs + 1;
         end
      end

      always #5 clk = ~clk;

      task step;  begin @(posedge clk); @(negedge clk); end endtask
      task phase; begin for (n = 0; n < HALF; n = n + 1) step; end endtask

      task do_reset;
         begin
            @(negedge clk); rst_n = 1'b0; scl_low = 1'b0; sda_low = 1'b0;
            step; step;
            @(negedge clk); rst_n = 1'b1;
            phase;
            nb = 0; ntd = 0; nrs = 0;
         end
      endtask

      // ---- a controller, driving pins by hand ----------------------------------
      // SDA changes only while SCL is LOW for data, and only while SCL is HIGH for
      // framing. That distinction IS the protocol, and a bench that blurred it could
      // not test a monitor that depends on it.
      task t_start;
         begin
            @(negedge clk); sda_low = 1'b0; scl_low = 1'b0; phase;   // both released
            @(negedge clk); sda_low = 1'b1; phase;                    // SDA falls, SCL high
            @(negedge clk); scl_low = 1'b1; phase;
         end
      endtask

      task t_restart;
         begin
            @(negedge clk); scl_low = 1'b1; sda_low = 1'b0; phase;
            @(negedge clk); scl_low = 1'b0; phase;                    // SCL high, SDA high
            @(negedge clk); sda_low = 1'b1; phase;                    // SDA falls
            @(negedge clk); scl_low = 1'b1; phase;
         end
      endtask

      task t_stop;
         begin
            @(negedge clk); scl_low = 1'b1; sda_low = 1'b1; phase;
            @(negedge clk); scl_low = 1'b0; phase;                    // SCL high, SDA low
            @(negedge clk); sda_low = 1'b0; phase;                    // SDA rises
         end
      endtask

      task t_bit (input b);
         begin
            @(negedge clk); scl_low = 1'b1; phase;
            @(negedge clk); sda_low = ~b;  phase;
            @(negedge clk); scl_low = 1'b0; phase;
            @(negedge clk); scl_low = 1'b1; phase;
         end
      endtask

      // Eight bits MSB first, then a ninth slot in which the bench drives the
      // acknowledge itself -- because here the bench is playing both participants.
      task t_byte (input [7:0] d, input ack);
         begin
            for (k = 7; k >= 0; k = k - 1) t_bit(d[k]);
            t_bit(~ack);                  // ack=1 -> drive LOW -> the ninth bit is 0
         end
      endtask

      task ck (input [200*8:1] what, input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0d expected %0d", what, g, e);
               errors = errors + 1;
            end
         end
      endtask

      task ck_idx (input [200*8:1] what, input integer idx,
                   input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s[%0d]: got %0d expected %0d", what, idx, g, e);
               errors = errors + 1;
            end
         end
      endtask

      initial begin
         $display("=== i2c_mon: a monitor is verification code, so it gets verified ===");

         // ----------------------------------------------------------------
         // T1. RESET REPORTS NOTHING. A monitor that came out of reset claiming a
         //     transaction was open would inject a phantom item into every downstream
         //     consumer, and the scoreboard would compare against traffic that never
         //     existed.
         // ----------------------------------------------------------------
         do_reset;
         $display("T1  out of reset the monitor reports no transaction and no events");
         ck("T1 no transaction open", txn_active, 0);
         ck("T1 no transactions counted", n_txns, 0);
         ck("T1 no bytes counted",        n_bytes, 0);
         ck("T1 no framing event",  saw_start | saw_stop | saw_restart, 0);

         // ----------------------------------------------------------------
         // T2. A COMPLETE WRITE TRANSACTION IS RECONSTRUCTED. Address 0x50 write, two
         //     data bytes, all acknowledged, STOP. Everything the monitor claims is
         //     checked: the address, the direction, the byte VALUES, the byte ORDER,
         //     which byte was the address, and the acknowledge of each.
         // ----------------------------------------------------------------
         t_start;
         ck("T2 a START opens a transaction", txn_active, 1);
         t_byte(8'hA0, 1'b1);            // 0x50 << 1 | write
         t_byte(8'h11, 1'b1);
         t_byte(8'h22, 1'b1);
         t_stop;
         $display("T2  a write transaction is reconstructed from the wire alone");
         ck("T2 three bytes seen",        nb, 3);
         ck("T2 the address is 0x50",     txn_addr, 7'h50);
         ck("T2 the direction is write",  txn_read, 0);
         ck("T2 the address was acked",   txn_addr_acked, 1);
         ck("T2 first byte is the address", cap_isaddr[0], 1);
         ck("T2 second byte is not",      cap_isaddr[1], 0);
         ck("T2 byte 0 value", cap_data[0], 8'hA0);
         ck("T2 byte 1 value", cap_data[1], 8'h11);
         ck("T2 byte 2 value", cap_data[2], 8'h22);
         ck("T2 every byte acked", cap_ack[0] & cap_ack[1] & cap_ack[2], 1);
         ck("T2 one transaction completed", ntd, 1);
         ck("T2 no NACK was seen", n_nacks, 0);
         ck("T2 and it is closed now", txn_active, 0);

         // ----------------------------------------------------------------
         // T3. DIRECTION IS DECODED FROM THE ADDRESS BYTE'S LOW BIT. A monitor that
         //     ignored it would report every read as a write and the scoreboard would
         //     then predict the wrong side of the transfer entirely.
         // ----------------------------------------------------------------
         do_reset;
         t_start;
         t_byte(8'hA1, 1'b1);            // 0x50 << 1 | READ
         t_byte(8'h5A, 1'b1);            // the target's data, on the wire
         t_stop;
         $display("T3  the direction bit is decoded, so a read is not reported as a write");
         ck("T3 the address is still 0x50", txn_addr, 7'h50);
         ck("T3 the direction is READ",     txn_read, 1);
         ck("T3 the data byte was captured", cap_data[1], 8'h5A);

         // ----------------------------------------------------------------
         // T4. A NACK IS OBSERVED, AND IT IS A BUS FACT, NOT AN OPINION. The monitor
         //     reports the ninth bit it saw. Whether a NACK was CORRECT is Chapter
         //     20.8's question -- a monitor that decided would be judging, and a
         //     judging monitor hides its own wrong assumptions.
         // ----------------------------------------------------------------
         do_reset;
         t_start;
         t_byte(8'hA0, 1'b1);            // address, acked
         t_byte(8'h33, 1'b0);            // data, NACKed
         t_stop;
         $display("T4  a NACK on the wire is reported as observed, not judged");
         ck("T4 the address was acked",   cap_ack[0], 1);
         ck("T4 the data byte was NACKed", cap_ack[1], 0);
         ck("T4 one NACK counted",         n_nacks, 1);
         ck("T4 the byte value survived the NACK", cap_data[1], 8'h33);

         // ----------------------------------------------------------------
         // T5. A REPEATED START ENDS ONE TRANSACTION AND OPENS ANOTHER. This is the
         //     boundary a monitor most often gets wrong: the phase before the repeated
         //     START is a complete transaction and must be reported, and the address
         //     that follows belongs to a NEW one. A monitor that treated a repeated
         //     START as ordinary framing would merge a write and a read into one item,
         //     and no scoreboard could untangle them afterwards.
         // ----------------------------------------------------------------
         do_reset;
         t_start;
         t_byte(8'hA0, 1'b1);            // phase 1: write 0x50
         t_byte(8'h07, 1'b1);            // a pointer byte
         t_restart;
         t_byte(8'hA1, 1'b1);            // phase 2: read 0x50
         t_byte(8'h99, 1'b1);
         t_stop;
         $display("T5  a repeated START closes one transaction and opens the next");
         ck("T5 one repeated START seen", nrs, 1);
         ck("T5 TWO transactions completed", ntd, 2);
         ck("T5 the second phase is a read", txn_read, 1);
         ck("T5 four bytes in total",        nb, 4);
         ck("T5 the second address byte is flagged as an address", cap_isaddr[2], 1);
         ck("T5 and the byte after it is not",                    cap_isaddr[3], 0);

         // ----------------------------------------------------------------
         // T6. THE SECOND PHASE'S DATA COUNT RESTARTS. `txn_n_data` is per phase, not
         //     per bus session. A monitor that accumulated across a repeated START
         //     would report a two-byte read as a four-byte one.
         // ----------------------------------------------------------------
         ck("T6 the read phase carried one data byte", txn_n_data, 1);

         // ----------------------------------------------------------------
         // T7. A TRUNCATED BYTE IS NOT REPORTED AS A BYTE.
         //
         //     The negative test. Five bits are clocked and then a STOP arrives. A
         //     monitor that reported a byte here would hand the scoreboard a value
         //     assembled from bits the transmitter never finished sending -- and the
         //     scoreboard would compare it, fail, and blame the DUT.
         //
         //     A monitor exercised only on legal traffic cannot distinguish "decodes
         //     correctly" from "decodes anything it is given".
         // ----------------------------------------------------------------
         do_reset;
         t_start;
         t_byte(8'hA0, 1'b1);            // a good address byte first
         for (k = 0; k < 5; k = k + 1) t_bit(1'b1);   // five bits, then nothing
         t_stop;
         $display("T7  a truncated byte is discarded, not reported as data");
         ck("T7 only the address byte was reported", nb, 1);
         ck("T7 and it was the address",             cap_isaddr[0], 1);
         ck("T7 the transaction still ended",        ntd, 1);
         ck("T7 no phantom data byte",               txn_n_data, 0);

         // ----------------------------------------------------------------
         // T8. AN SDA CHANGE WHILE SCL IS HIGH IS FRAMING, NOT DATA.
         //
         //     The second negative test, and the sharper one. Mid-byte, SDA is changed
         //     while SCL is HIGH -- which the specification forbids for data and
         //     defines as framing. The monitor must treat it as a START and abandon the
         //     partial byte, because that is what every conforming device on the bus
         //     will do. A monitor that quietly sampled it as a data bit would
         //     reconstruct a byte no device ever received.
         // ----------------------------------------------------------------
         do_reset;
         t_start;
         t_byte(8'hA0, 1'b1);
         t_bit(1'b1); t_bit(1'b1);       // two bits of a data byte
         // now the illegal change: SDA falls while SCL is HIGH
         @(negedge clk); scl_low = 1'b1; phase;
         @(negedge clk); sda_low = 1'b0; phase;      // SDA released
         @(negedge clk); scl_low = 1'b0; phase;      // SCL HIGH
         @(negedge clk); sda_low = 1'b1; phase;      // SDA falls: a START
         @(negedge clk); scl_low = 1'b1; phase;
         $display("T8  an SDA edge while SCL is high is framing: the partial byte is dropped");
         ck("T8 it was reported as a START", nrs, 1);
         ck("T8 the first phase was closed", ntd, 1);
         ck("T8 only the address byte was ever reported", nb, 1);
         ck("T8 a transaction is open again", txn_active, 1);
         t_stop;

         // ----------------------------------------------------------------
         // 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.
         //     With the bench released, both lines must read HIGH: if the monitor were
         //     driving anything, they would not.
         // ----------------------------------------------------------------
         do_reset;
         @(negedge clk); scl_low = 1'b0; sda_low = 1'b0;
         for (n = 0; n < 20; n = n + 1) step;
         $display("T9  with the bench released, the monitor is holding nothing");
         ck("T9 SCL reads high", scl, 1);
         ck("T9 SDA reads high", sda, 1);

         if (errors == 0) $display("=== i2c_mon: ALL CHECKS PASSED ===");
         else             $display("=== i2c_mon: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule

Why byte_valid fires at the ninth edge and not the eighth

10 cycles
A ten-cycle waveform. SCL alternates low and high. SDA carries eight data bits and then an acknowledge bit that is low. A byte_valid signal is low throughout and rises only at the ninth SCL rising edge. Markers label the first sampling edge and the ninth edge where the byte and its acknowledge are reported together.bits assembled, four edges shownbits assembled, four edges shownthe acknowledge slottheacknowle…first sample: SDA is stable here by rulefirst sample: SDA is stablehere by ruleninth edge: byte AND acknowledgeninth edge: byte ANDacknowledgeSCLSDAbyte_validt0t1t2t3t4t5t6t7t8t9
The figure is compressed: a real byte takes eight rising edges and this shows four, plus the acknowledge. The point is the position of byte_valid. Reporting at the eighth edge would force every consumer to pair the byte with a later acknowledge event, and every consumer would have to get that pairing right on its own.
Figure 1 — one byte reconstructed from edges. SDA is sampled at each SCL rising edge because that is the instant the protocol guarantees it is stable. The ninth rising edge carries the acknowledge, and byte_valid fires there rather than at the eighth: eight bits are not yet a protocol fact, because whether they were received is undetermined until the ninth slot.

8. The VHDL Twin, and What Three Languages Actually Prove

The monitor exists in SystemVerilog and in VHDL, with the same nine traces and the same expected values.

Both files are published so that the claim is reproducible rather than merely described. The VHDL bench drives the same nine traces and asserts the same expected values; what differs is entirely language mechanics.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_mon.vhd — the VHDL twin, same rules written again
   -- -----------------------------------------------------------------------------
   -- i2c_mon.vhd
   -- The passive monitor, in VHDL.
   --
   -- WHY THIS FILE EXISTS AT ALL, since Chapter 20.4 argues the architecture is
   -- language-neutral: it is a SIMULATOR constraint, not an architectural one. Icarus
   -- compiles a SystemVerilog environment against the SystemVerilog and the
   -- Verilog-2001 DUT, so one environment already covers two implementations. The VHDL
   -- DUT needs an analyser that reads VHDL, and NVC will not read the SystemVerilog
   -- environment. So the environment is expressed twice, not three times, and the
   -- duplication tracks the toolchain rather than the design.
   --
   -- WHAT MUST BE IDENTICAL is the verification INTENT: the same reconstruction rules,
   -- the same reported fields, the same expected values, the same verdict. Chapter 20.4
   -- states the contract; these two files are two encodings of it. If they disagreed
   -- about what the bus meant, one of them would be wrong -- which is why the two
   -- benches assert the same numbers and are compared in Chapter 20.7's parity table.
   --
   -- EVERYTHING ELSE IS AS THE SYSTEMVERILOG VERSION: passive, resolved-bus only, its
   -- own framing implementation rather than Module 18.3's, and sampling SDA at the SCL
   -- rising edge because that is the only instant the specification guarantees it stable.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_mon is
      generic (
         MAX_BYTES : positive := 8
      );
      port (
         clk   : in std_logic;
         rst_n : in std_logic;

         -- the only inputs: the RESOLVED bus
         scl : in std_logic;
         sda : in std_logic;

         saw_start   : out std_logic;
         saw_restart : out std_logic;
         saw_stop    : out std_logic;

         byte_valid   : out std_logic;
         byte_data    : out std_logic_vector(7 downto 0);
         byte_acked   : out std_logic;
         byte_is_addr : out std_logic;

         txn_active           : out std_logic;
         txn_addr             : out std_logic_vector(6 downto 0);
         txn_read             : out std_logic;
         txn_addr_acked       : out std_logic;
         txn_n_data           : out unsigned(3 downto 0);
         txn_done             : out std_logic;
         txn_ended_by_restart : out std_logic;

         n_txns  : out unsigned(15 downto 0);
         n_bytes : out unsigned(15 downto 0);
         n_nacks : out unsigned(15 downto 0)
      );
   end entity i2c_mon;

   architecture obs of i2c_mon is
      signal scl_d, sda_d : std_logic := '1';
      signal bitcnt       : unsigned(3 downto 0) := (others => '0');
      signal shreg        : std_logic_vector(7 downto 0) := (others => '0');
      signal addr_seen    : std_logic := '0';
      signal active_i     : std_logic := '0';
   begin

      txn_active <= active_i;

      process (clk, rst_n)
         variable start_now, stop_now, scl_rise : boolean;
      begin
         if rst_n = '0' then
            scl_d <= '1'; sda_d <= '1';
            saw_start <= '0'; saw_restart <= '0'; saw_stop <= '0';
            byte_valid <= '0'; byte_data <= (others => '0');
            byte_acked <= '0'; byte_is_addr <= '0';
            active_i <= '0'; txn_addr <= (others => '0'); txn_read <= '0';
            txn_addr_acked <= '0'; txn_n_data <= (others => '0');
            txn_done <= '0'; txn_ended_by_restart <= '0';
            bitcnt <= (others => '0'); shreg <= (others => '0'); addr_seen <= '0';
            n_txns <= (others => '0'); n_bytes <= (others => '0');
            n_nacks <= (others => '0');
         elsif rising_edge(clk) then
            -- framing is an SDA edge while SCL is HIGH: the same PROTOCOL DEFINITION as
            -- Chapter 18.3, a different implementation
            start_now := (sda = '0' and sda_d = '1') and scl = '1' and scl_d = '1';
            stop_now  := (sda = '1' and sda_d = '0') and scl = '1' and scl_d = '1';
            scl_rise  := (scl = '1' and scl_d = '0');

            scl_d <= scl; sda_d <= sda;
            saw_start <= '0'; saw_restart <= '0'; saw_stop <= '0';
            byte_valid <= '0'; txn_done <= '0'; txn_ended_by_restart <= '0';

            if start_now then
               saw_start <= '1';
               if active_i = '1' then
                  saw_restart          <= '1';
                  txn_done             <= '1';
                  txn_ended_by_restart <= '1';
                  n_txns               <= n_txns + 1;
               end if;
               active_i   <= '1';
               addr_seen  <= '0';
               txn_n_data <= (others => '0');
               bitcnt     <= (others => '0');
               shreg      <= (others => '0');
            elsif stop_now then
               saw_stop <= '1';
               if active_i = '1' then
                  txn_done <= '1';
                  n_txns   <= n_txns + 1;
               end if;
               active_i  <= '0';
               addr_seen <= '0';
               bitcnt    <= (others => '0');
            elsif scl_rise and active_i = '1' then
               if bitcnt < 8 then
                  shreg  <= shreg(6 downto 0) & sda;
                  bitcnt <= bitcnt + 1;
               else
                  byte_valid <= '1';
                  byte_data  <= shreg;
                  if sda = '0' then byte_acked <= '1'; else byte_acked <= '0'; end if;
                  if addr_seen = '0' then byte_is_addr <= '1';
                  else                    byte_is_addr <= '0'; end if;
                  n_bytes <= n_bytes + 1;
                  if sda = '1' then n_nacks <= n_nacks + 1; end if;

                  if addr_seen = '0' then
                     txn_addr <= shreg(7 downto 1);
                     txn_read <= shreg(0);
                     if sda = '0' then txn_addr_acked <= '1';
                     else              txn_addr_acked <= '0'; end if;
                     addr_seen <= '1';
                  elsif txn_n_data /= 15 then
                     txn_n_data <= txn_n_data + 1;
                  end if;
                  bitcnt <= (others => '0');
                  shreg  <= (others => '0');
               end if;
            end if;
         end if;
      end process;

   end architecture obs;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_mon_tb.vhd — the same nine traces, in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_mon_tb.vhd
   -- The monitor's self-test, in VHDL: the same nine traces, the same expected values.
   --
   -- THE SAME VERIFICATION INTENT AS THE SYSTEMVERILOG BENCH, deliberately. Same trace
   -- set (legal write, read, NACK, repeated START, truncated byte, illegal SDA change),
   -- same expected byte values, same expected counts. Chapter 20.7's parity table
   -- compares the two, because two encodings of one contract that disagreed would mean
   -- one of them is wrong -- and a monitor is the component whose correctness everything
   -- downstream rests on.
   --
   -- The bench owns the bus outright and drives LOW or RELEASE only; it never drives HIGH.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_mon_tb is
   end entity i2c_mon_tb;

   architecture sim of i2c_mon_tb is

      constant HALFP : integer := 6;

      signal clk   : std_logic := '0';
      signal rst_n : std_logic := '0';

      signal scl_low, sda_low : std_logic := '0';
      signal scl, sda : std_logic;

      signal saw_start, saw_restart, saw_stop : std_logic;
      signal byte_valid, byte_acked, byte_is_addr : std_logic;
      signal byte_data : std_logic_vector(7 downto 0);
      signal txn_active, txn_read, txn_addr_acked, txn_done, txn_rs : std_logic;
      signal txn_addr : std_logic_vector(6 downto 0);
      signal txn_n_data : unsigned(3 downto 0);
      signal n_txns, n_bytes, n_nacks : unsigned(15 downto 0);

      -- capture: the monitor reports for one cycle, so the reports are collected here
      type byte_arr is array (0 to 15) of std_logic_vector(7 downto 0);
      type bit_arr  is array (0 to 15) of std_logic;
      signal cap_data   : byte_arr;
      signal cap_ack    : bit_arr;
      signal cap_isaddr : bit_arr;
      signal nb, ntd, nrs : integer := 0;
      signal clr_obs : boolean := false;

      signal halt : boolean := false;

   begin

      -- LOW or RELEASE only: the wired-AND of one participant
      scl <= not scl_low;
      sda <= not sda_low;

      clkgen : process
      begin
         while not halt loop
            clk <= '0'; wait for 5 ns;
            clk <= '1'; wait for 5 ns;
         end loop;
         wait;
      end process;

      dut : entity work.i2c_mon
         generic map (MAX_BYTES => 8)
         port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
                   saw_start => saw_start, saw_restart => saw_restart,
                   saw_stop => saw_stop,
                   byte_valid => byte_valid, byte_data => byte_data,
                   byte_acked => byte_acked, byte_is_addr => byte_is_addr,
                   txn_active => txn_active, txn_addr => txn_addr,
                   txn_read => txn_read, txn_addr_acked => txn_addr_acked,
                   txn_n_data => txn_n_data, txn_done => txn_done,
                   txn_ended_by_restart => txn_rs,
                   n_txns => n_txns, n_bytes => n_bytes, n_nacks => n_nacks);

      -- One driver per signal: the clear arrives as a REQUEST, not as a second driver.
      -- `std_logic`/integer signals driven from two processes resolve silently or error,
      -- and Module 19 lost a run to exactly that.
      obs : process (clk, clr_obs)
      begin
         if clr_obs then
            nb <= 0; ntd <= 0; nrs <= 0;
         elsif rising_edge(clk) then
            if rst_n = '1' then
               if byte_valid = '1' and nb < 16 then
                  cap_data(nb)   <= byte_data;
                  cap_ack(nb)    <= byte_acked;
                  cap_isaddr(nb) <= byte_is_addr;
                  nb <= nb + 1;
               end if;
               if txn_done    = '1' then ntd <= ntd + 1; end if;
               if saw_restart = '1' then nrs <= nrs + 1; end if;
            end if;
         end if;
      end process;

      stim : process
         variable err : integer := 0;

         function b2i (b : std_logic) return integer is
         begin
            if b = '1' then return 1; else return 0; end if;
         end function;

         procedure step is
         begin
            wait until rising_edge(clk);
            wait until falling_edge(clk);
         end procedure;

         procedure phase is
         begin
            for i in 1 to HALFP loop step; end loop;
         end procedure;

         procedure clear_obs is
         begin
            clr_obs <= true;  wait for 1 ns;
            clr_obs <= false; wait for 1 ns;
         end procedure;

         procedure do_reset is
         begin
            wait until falling_edge(clk);
            rst_n <= '0'; scl_low <= '0'; sda_low <= '0';
            step; step;
            wait until falling_edge(clk);
            rst_n <= '1';
            phase;
            clear_obs;
         end procedure;

         procedure t_start is
         begin
            wait until falling_edge(clk); sda_low <= '0'; scl_low <= '0'; phase;
            wait until falling_edge(clk); sda_low <= '1'; phase;
            wait until falling_edge(clk); scl_low <= '1'; phase;
         end procedure;

         procedure t_restart is
         begin
            wait until falling_edge(clk); scl_low <= '1'; sda_low <= '0'; phase;
            wait until falling_edge(clk); scl_low <= '0'; phase;
            wait until falling_edge(clk); sda_low <= '1'; phase;
            wait until falling_edge(clk); scl_low <= '1'; phase;
         end procedure;

         procedure t_stop is
         begin
            wait until falling_edge(clk); scl_low <= '1'; sda_low <= '1'; phase;
            wait until falling_edge(clk); scl_low <= '0'; phase;
            wait until falling_edge(clk); sda_low <= '0'; phase;
         end procedure;

         procedure t_bit (b : std_logic) is
         begin
            wait until falling_edge(clk); scl_low <= '1'; phase;
            wait until falling_edge(clk); sda_low <= not b; phase;
            wait until falling_edge(clk); scl_low <= '0'; phase;
            wait until falling_edge(clk); scl_low <= '1'; phase;
         end procedure;

         procedure t_byte (d : std_logic_vector(7 downto 0); ack : std_logic) is
         begin
            for k in 7 downto 0 loop t_bit(d(k)); end loop;
            t_bit(not ack);
         end procedure;

         procedure ck (what : string; g : integer; e : integer) is
         begin
            if g /= e then
               report "  FAIL " & what & ": got " & integer'image(g)
                      & " expected " & integer'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

      begin
         report "=== i2c_mon: a monitor is verification code, so it gets verified ==="
                severity note;

         -- T1. Reset reports nothing. A monitor claiming an open transaction out of
         --     reset would inject a phantom item into every downstream consumer.
         do_reset;
         report "T1  out of reset the monitor reports no transaction and no events"
                severity note;
         ck("T1 no transaction open",     b2i(txn_active), 0);
         ck("T1 no transactions counted", to_integer(n_txns), 0);
         ck("T1 no bytes counted",        to_integer(n_bytes), 0);
         ck("T1 no framing event",
            b2i(saw_start) + b2i(saw_stop) + b2i(saw_restart), 0);

         -- T2. A complete write transaction is reconstructed: address, direction, byte
         --     values, byte ORDER, which byte was the address, and each acknowledge.
         t_start;
         ck("T2 a START opens a transaction", b2i(txn_active), 1);
         t_byte(x"A0", '1');
         t_byte(x"11", '1');
         t_byte(x"22", '1');
         t_stop;
         report "T2  a write transaction is reconstructed from the wire alone" severity note;
         ck("T2 three bytes seen",          nb, 3);
         ck("T2 the address is 0x50",       to_integer(unsigned(txn_addr)), 16#50#);
         ck("T2 the direction is write",    b2i(txn_read), 0);
         ck("T2 the address was acked",     b2i(txn_addr_acked), 1);
         ck("T2 first byte is the address", b2i(cap_isaddr(0)), 1);
         ck("T2 second byte is not",        b2i(cap_isaddr(1)), 0);
         ck("T2 byte 0 value", to_integer(unsigned(cap_data(0))), 16#A0#);
         ck("T2 byte 1 value", to_integer(unsigned(cap_data(1))), 16#11#);
         ck("T2 byte 2 value", to_integer(unsigned(cap_data(2))), 16#22#);
         ck("T2 every byte acked",
            b2i(cap_ack(0)) + b2i(cap_ack(1)) + b2i(cap_ack(2)), 3);
         ck("T2 one transaction completed", ntd, 1);
         ck("T2 no NACK was seen",          to_integer(n_nacks), 0);
         ck("T2 and it is closed now",      b2i(txn_active), 0);

         -- T3. Direction is decoded from the address byte's low bit.
         do_reset;
         t_start;
         t_byte(x"A1", '1');
         t_byte(x"5A", '1');
         t_stop;
         report "T3  the direction bit is decoded, so a read is not reported as a write"
                severity note;
         ck("T3 the address is still 0x50", to_integer(unsigned(txn_addr)), 16#50#);
         ck("T3 the direction is READ",     b2i(txn_read), 1);
         ck("T3 the data byte was captured", to_integer(unsigned(cap_data(1))), 16#5A#);

         -- T4. A NACK is observed as a bus fact, not judged.
         do_reset;
         t_start;
         t_byte(x"A0", '1');
         t_byte(x"33", '0');
         t_stop;
         report "T4  a NACK on the wire is reported as observed, not judged" severity note;
         ck("T4 the address was acked",    b2i(cap_ack(0)), 1);
         ck("T4 the data byte was NACKed", b2i(cap_ack(1)), 0);
         ck("T4 one NACK counted",         to_integer(n_nacks), 1);
         ck("T4 the byte value survived the NACK",
            to_integer(unsigned(cap_data(1))), 16#33#);

         -- T5. A repeated START closes one transaction and opens the next.
         do_reset;
         t_start;
         t_byte(x"A0", '1');
         t_byte(x"07", '1');
         t_restart;
         t_byte(x"A1", '1');
         t_byte(x"99", '1');
         t_stop;
         report "T5  a repeated START closes one transaction and opens the next"
                severity note;
         ck("T5 one repeated START seen",   nrs, 1);
         ck("T5 TWO transactions completed", ntd, 2);
         ck("T5 the second phase is a read", b2i(txn_read), 1);
         ck("T5 four bytes in total",        nb, 4);
         ck("T5 the second address byte is flagged as an address",
            b2i(cap_isaddr(2)), 1);
         ck("T5 and the byte after it is not", b2i(cap_isaddr(3)), 0);

         -- T6. The second phase's data count restarts.
         ck("T6 the read phase carried one data byte", to_integer(txn_n_data), 1);

         -- T7. A truncated byte is not reported as a byte. The negative test: five bits
         --     then a STOP. A monitor that reported a byte here would hand the scoreboard
         --     a value assembled from bits never finished.
         do_reset;
         t_start;
         t_byte(x"A0", '1');
         for k in 1 to 5 loop t_bit('1'); end loop;
         t_stop;
         report "T7  a truncated byte is discarded, not reported as data" severity note;
         ck("T7 only the address byte was reported", nb, 1);
         ck("T7 and it was the address",             b2i(cap_isaddr(0)), 1);
         ck("T7 the transaction still ended",        ntd, 1);
         ck("T7 no phantom data byte",               to_integer(txn_n_data), 0);

         -- T8. An SDA change while SCL is HIGH is framing, not data: the partial byte is
         --     abandoned, because that is what every conforming device will do.
         do_reset;
         t_start;
         t_byte(x"A0", '1');
         t_bit('1'); t_bit('1');
         wait until falling_edge(clk); scl_low <= '1'; phase;
         wait until falling_edge(clk); sda_low <= '0'; phase;
         wait until falling_edge(clk); scl_low <= '0'; phase;
         wait until falling_edge(clk); sda_low <= '1'; phase;
         wait until falling_edge(clk); scl_low <= '1'; phase;
         report "T8  an SDA edge while SCL is high is framing: the partial byte is dropped"
                severity note;
         ck("T8 it was reported as a START",  nrs, 1);
         ck("T8 the first phase was closed",  ntd, 1);
         ck("T8 only the address byte was ever reported", nb, 1);
         ck("T8 a transaction is open again", b2i(txn_active), 1);
         t_stop;

         -- T9. The monitor never drives: with the bench released, both lines read HIGH.
         do_reset;
         wait until falling_edge(clk); scl_low <= '0'; sda_low <= '0';
         for i in 1 to 20 loop step; end loop;
         report "T9  with the bench released, the monitor is holding nothing" severity note;
         ck("T9 SCL reads high", b2i(scl), 1);
         ck("T9 SDA reads high", b2i(sda), 1);

         if err = 0 then
            report "=== i2c_mon: ALL CHECKS PASSED ===" severity note;
         else
            report "=== i2c_mon: " & integer'image(err) & " CHECK(S) FAILED ==="
                   severity note;
         end if;
         halt <= true;
         wait;
      end process;

   end architecture sim;

The framing check that could never fail

Pitfall — a monitor that instantiated the design's framing detector
Buggy Code
// The monitor reuses the target's verified framing module. This looks like good
// practice: one implementation, already tested.
//
//    i2c_framing fr (.scl(scl), .sda(sda),
//                    .start(mon_start), .stop(mon_stop), .restart(mon_restart));
//
// The framing module has a bug. It treats any SDA fall as a START:
//
//    assign start = sda_fall;              // missing: & scl
//
// 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 environment's framing check compares mon_start against the DUT's internal
// start signal. They are identical, always, by construction. The check has never
// fired and cannot be made to fire -- break the framing module further and both
// break together.
Pitfall — a monitor that counted cycles, and a target that stretched
Buggy Code
// A monitor written against a 100 kHz bus with a known clock period:
//
//    localparam CYCLES_PER_BIT = 100;
//    always @(posedge clk) begin
//       if (txn_active) begin
//          tick <= tick + 1;
//          if (tick == CYCLES_PER_BIT/2) begin      // sample mid-bit
//             shreg <= {shreg[6:0], sda};
//             bitcnt <= bitcnt + 1;
//             tick <= 0;
//          end
//       end
//    end
//
// Works perfectly. Then a target stretches the clock, and the monitor's byte
// boundaries drift away from the real ones -- so bytes are reported with bits
// from two different bus bits, and the byte COUNT eventually diverges too.
//
// The reports are garbage, and they are garbage about a transfer that was
// entirely correct. Debugging starts at the target.

9. What 20.7 Settled

A monitor's only inputs are the resolved lines, and its only outputs are observations. No expected values, no verdicts, no drive port. That is what lets the same file be the thing under test in one scenario and trusted infrastructure in another.

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

Edge-driven, not cycle-counting. No bus rate appears anywhere in the file, which makes clock stretching a non-event and makes the component impossible to misconfigure. Sampling at SCL's rising edge follows from the protocol's own stability guarantee.

A byte and its acknowledge are one fact, reported together at the ninth edge. Splitting them makes every consumer responsible for pairing them, and one-cycle phase errors against this interface appeared twice in this module — in the assembler and in the scoreboard — with identical symptoms: every value legal, every value one event out.

The negative traces are the ones that matter. A truncated byte and an illegal SDA change are conditions a correct design never produces, so a monitor verified only against a correct design has never had its framing tested in the direction that counts.

The monitor says what happened. Nothing so far says what should have happened — and 20.3 showed that the stimulus cannot answer it, because intent and observation agree perfectly on transfers the target refused entirely. The next chapter builds the component that knows, from the contract and never from the bus. Chapter 20.8 — Reference Model and Scoreboard Strategy.

Continue learning