The I2C Monitor, Checker and Agent

Build a monitor that reports at the right moment, separate the rules a bus analyser can check from those that need the driver view, read a measured fault matrix, and describe a UVM agent that never drives a 1.

Lesson
P8.10
Time
40 min
Runs on
Verilator
Check
4 questions

A bought protocol checker flagged "ACK driven by wrong device" on a bus where both devices were correct. It inferred the ACK's owner from byte counts, and its counter did not reset at a repeated START. Two weeks went into the design before anyone found the checker was wrong.

RunIntent rules that fired (up to 200 reports per rule)Totals
none (clean)noneintent broken 0, ck.broken 0
target stretches 3 µs (legal)noneintent broken 0, ck.broken 0
stop_between (legal on one controller)noneintent broken 0, ck.broken 0
ack_not_releasedack_slot_onlyintent broken 24, ck.broken 0
ack_wrong_clockack_slot_only, ack_ownerintent broken 122, ck.broken 0
push_pull_targetno_drive_when_idle, sda_stable, push_pull_contentionintent broken 2785, ck.broken 9
ctrl_ignores_sda (ctrl 0x51 against 0x50)loser_releasesintent broken 141, ck.broken 0
data_change_scl_highno_drive_when_idle, sda_stableintent broken 244, ck.broken 21
ignores_srreset_on_start, ack_slot_onlyintent broken 25, ck.broken 0
10-bit read (legal)noneintent broken 0, ck.broken 0
10-bit header, then Sr at onceten_bit_headerintent broken 2, ck.broken 0

Measured on Verilator: i2c_sva.sv fed by p8_intent.sv, one run per row. The library caps printed reports per rule while broken keeps counting every violation. Green means the right verdict: silent on legal runs, firing on faults. The wire checker (ck.broken) misses several ownership faults that the intent checker catches.

The Monitor

On I2C the clock is on the wire, so sampling data is easy: SDA at each SCL rise. START and STOP are harder, because they are SDA edges with no SCL edge; a device without interface hardware must sample SDA at least twice per clock to see them (§3.1.4).

Inside an I2C MonitorArrows show data moving from the pins to the printed line. Two detectors watch SDA; one sampler watches SCL.
Inside an I2C monitorArrows show data moving from the pins to the printed line. Two detectors watch SDA; one sampler watches SCL. component SCL, SDA the wires only component S/P detector SDA edge, SCL high component Bit sampler SDA at SCL rise component Byte + ACK nine samples per byte component Transaction address, R/W, bytes,Sr component Report at P one grammar line S, Sr, P
  1. Conditions

    S and P are SDA edges under a high SCL (§3.1.4), so this detector must look at SDA edges, not SCL edges.

  2. Bits and bytes

    Data is sampled at the SCL rise; every ninth sample is the ACK. A START or Sr resets the count (§3.1.10 note 4).

  3. Report

    The transaction closes at P. The twin that reports at S prints an address it has not yet seen.

Read the diagram as text
  • SCL, SDA: the wires only
  • S/P detector: SDA edge, SCL high
  • Bit sampler: SDA at SCL rise
  • Byte + ACK: nine samples per byte
  • Transaction: address, R/W, bytes, Sr
  • Report at P: one grammar line
  • SCL, SDA → S/P detector
  • SCL, SDA → Bit sampler
  • Bit sampler → Byte + ACK
  • Byte + ACK → Transaction
  • S/P detector → Transaction: S, Sr, P
  • Transaction → Report at P

The site's grammar, used by the decoder and the problems: I2C S 50 W A 3c A Sr 50 R A 3c NA P.

Measured on Verilator: the lesson monitor p8_mon on three legal register reads, then the twin that reports at START. Its lines carry whatever byte it last shifted in, before the address arrives.
I2C S 50 W A 3c A Sr 50 R A 3c NA P
I2C S 50 W A 3c A Sr 50 R A 3c NA P
I2C S 50 W A 3c A P
I2C S 50 R A 3c NA P

report_at_start = 1:
I2C S 00
I2C S 3c
I2C S 3c
I2C S 3c
I2C S 3c
I2C S 3c

The same monitor decodes 10-bit headers: I2C S 10:2aa W A 3c A Sr 10:2aa R A 3c NA P, where the library decoder prints I2C S 7a W A aa A 3c A Sr 7a R A 3c NA P.

Two Kinds of Rule

Seen on SDA and SCL aloneNeeds the driver view (ctrl_low, tgt_low)
START and STOP shapes; Srwhich device pulled the ACK low
SDA steady while SCL is highan ACK that overstays into the next byte
nine clocks per byte, ACK or NACKa target that pulls in the wrong clock
the Table 11 edge times (P8.7)a controller that keeps driving after it lost
tBUF between transactionsa push-pull output fighting an open-drain one

The right column is what the library checker's header says it cannot infer from SDA alone. On silicon those lanes do not exist, so a bus analyser reports only the symptom; in simulation, bind to them.

The intent assertions in i2c_sva.sv need more than pins: whether a clock is the ninth, who should be transmitting, whether the target was meant to accept. The lesson harness p8_intent.sv works those out from its own decoder of the wire, never from the device under test. The one exception is the target's byte counter for reset_on_start, a designer's white-box check.

The Fault Matrix

The matrix at the top runs every twin from P8.2 to P8.6 against those rules. Three legal runs must stay silent: a stretched read, a STOP between pointer and read on a single-controller bus, and a 10-bit read. A checker that flags any of them is wrong.

Each fault lands on the rule written for it. The overstaying ACK trips ack_slot_only, the early ACK ack_slot_only and the missing ninth-clock ACK ack_owner, the loser that keeps driving loser_releases, and the target that ignores Sr reset_on_start. First report for the overstaying ACK: I2C INTENT ack_slot_only at 27225.

The UVM Agent

A UVM Agent for a Wired-AND BusArrows show calls and transactions. The driver writes only the low lanes; the pull-up makes every 1.
A UVM agent for a wired-AND busArrows show calls and transactions. The driver writes only the low lanes; the pull-up makes every 1. component Sequencer i2c_txn items component Driver drives scl_low,sda_low component i2c_if tri1 scl, sda component Monitor samples the pins component Scoreboard analysis port itemslows onlywrite(t)
  1. Never drive a 1

    The interface resolves tri1 scl, sda from every agent's scl_low and sda_low. A driver that assigned sda = 1 would be a push-pull output.

  2. One agent, two roles

    The same agent runs as controller or target by configuration (is_controller), because on I2C the roles are decided per transaction (Table 2).

  3. Publish at Sr or P

    The monitor closes a transaction at Sr or STOP and writes it to the analysis port.

Read the diagram as text
  • Sequencer: i2c_txn items
  • Driver: drives scl_low, sda_low
  • i2c_if: tri1 scl, sda
  • Monitor: samples the pins
  • Scoreboard: analysis port
  • Sequencer → Driver: items
  • Driver → i2c_if: lows only
  • i2c_if → Monitor
  • Monitor → Scoreboard: write(t)

The agent files come from the library (i2c-uvm.mjs): interface, transaction, driver, monitor and agent. Their scope is the 7-bit register-target subset.

Measured on the server's UVM engine (uvm-core), UVM_ERROR count 0: the controller agent writes pointer 0x3c and 0x96, reads 0x3c back with Sr, then addresses 0x51. The target agent answers; the controller agent's monitor publishes each transaction.
I2C_TXN addr=50 rw=W data=[3c,96] acks=[1,1,1] sr=0
I2C_TXN addr=50 rw=W data=[3c] acks=[1,1] sr=0
I2C_TXN addr=50 rw=R data=[96] acks=[1,0] sr=1
I2C_TXN addr=51 rw=W data=[] acks=[0] sr=0

The sr=1 on the read is the transaction that began with a repeated START; the pointer write before it published at that Sr. The absent address shows acks=[0].

Try It

p8_intent decodes the wire and drives i2c_sva, which prints I2C INTENT <rule> for each miss; p8_mon prints one line per transaction at the STOP. The bench's own decoder is off.

Add target.ack_not_released = 1; or target.ignores_sr = 1; before #2000; and read the rules that fire. Set mon2.report_at_start = 1; to see the twin monitor.

The two lesson files
p8_intent.sv: an independent decoder that supplies the intent inputs.
`timescale 1ns/1ns
// p8_intent.sv: decodes the wire on its own and supplies i2c_sva's intent inputs every 50 ns.
//   condition  1 for an S or Sr, 2 for a P: an SDA edge under a high SCL between bytes. The same edge
//              inside a byte is a data edge that moved at the wrong time, so it stays 0.
//   ack_slot   the ninth SCL high of a byte;   target_tx  data bytes of a read the target accepted
//   ack_expected   the plan: the target accepts ADDRESS and the general call (or, with TEN, the 10-bit address
//              A10 in its two bytes); on reads the controller's own ACK is trusted
//   lost       the controller let SDA go (sent 1), read 0, and nobody else it knows of pulled: another controller
module p8_intent #(parameter [6:0] ADDRESS = 7'h50, parameter TEN = 0, parameter [9:0] A10 = 10'h2aa)
  (input wire scl, sda, cdl, tdl, c2dl, high_drive, input wire [3:0] target_count);
  reg tick = 0, rst_n = 0;
  always #25 tick = ~tick;
  initial #100 rst_n = 1;
  reg [1:0] condition = 0;
  reg start_seen = 0, byte_done = 0, active = 0, header = 1, reading = 0, selected = 0, lost = 0, low_next = 0, addressed10 = 0;
  reg [7:0] shift = 0, byte_value = 0;
  int count = 0;
  time set_at = 0, last_tick = 0;
  wire ack_slot = active && count == 9;
  wire target_tx = active && reading && !header && selected;
  wire hdr10 = shift[7:3] == 5'b11110 && shift[2:1] == A10[9:8];
  wire accepts = !TEN ? (shift[7:1] == ADDRESS || shift == 8'h00) : low_next ? shift == A10[7:0] : hdr10 && (!shift[0] || addressed10);
  wire ack_expected = target_tx ? cdl : (header || low_next) ? accepts : selected;
  wire free_bus = !active;
  always @(negedge sda) if (scl === 1'b1 && count <= 1) begin
    condition = 1; start_seen = 1; set_at = $time; active = 1; header = 1; count = 0; lost = 0; low_next = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active && count <= 1) begin
    condition = 2; set_at = $time; active = 0; count = 0; selected = 0; addressed10 = 0;
  end
  always @(posedge scl) if (active) begin
    count++;
    if (count <= 8) begin
      shift = {shift[6:0], sda};
      if (!target_tx && cdl === 1'b0 && sda === 1'b0 && tdl === 1'b0) lost = 1;
    end else begin byte_done = 1; byte_value = shift; set_at = $time; end
  end
  always @(negedge scl) if (active && count == 9) begin
    if (header) begin reading = shift[0]; selected = accepts; header = 0; low_next = TEN && hdr10 && !shift[0]; end
    else if (low_next) begin selected = accepts; addressed10 = accepts; low_next = 0; end
    count = 0;
  end
  // A flag lives until one sampling edge has seen it.
  always @(posedge tick) last_tick = $time;
  always @(negedge tick) if (set_at < last_tick) begin condition = 0; start_seen = 0; byte_done = 0; end
  i2c_sva ck(tick, rst_n, 1'b1, scl, sda, cdl, tdl, c2dl, high_drive, condition, ack_slot, target_tx, ack_expected,
             lost, free_bus, start_seen, target_count, byte_done, byte_value);
endmodule
p8_mon.sv: the transaction monitor with 10-bit decoding and the report-at-START twin.
`timescale 1ns/1ns
// p8_mon.sv: one line per transaction, printed at the STOP, in the library decoder's grammar.
// ten_bit = 1 decodes a 1111 0XX header and its low byte as 10:<address>. report_at_start = 1 is the twin
// that prints at the START, before a single address bit has been sampled.
module p8_mon(input wire scl, sda);
  bit report_at_start = 0, ten_bit = 1;
  string text = "";
  bit active = 0, header = 1, low_next = 0, top_ack = 0;
  int count = 0;
  reg [7:0] value = 0; reg [9:0] addr10 = 0;
  function automatic string ack(input bit level); if (level) return "NA"; return "A"; endfunction
  function automatic string dir(input bit rd); if (rd) return "R"; return "W"; endfunction
  always @(negedge sda) if (scl === 1'b1) begin
    low_next = 0;
    if (report_at_start) $display("I2C S %02h", value[7:1]);
    else if (active) text = {text, " Sr"};
    else text = "I2C S";
    active = 1; header = 1; count = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active) begin
    if (!report_at_start) $display("%s P", text);
    active = 0; count = 0;
  end
  always @(posedge scl) if (active) begin
    if (count < 8) begin value = {value[6:0], sda}; count++; end
    else begin
      if (header && ten_bit && value[7:3] == 5'b11110 && !value[0]) begin addr10[9:8] = value[2:1]; low_next = 1; top_ack = !sda; end
      else if (header && ten_bit && value[7:3] == 5'b11110) text = $sformatf("%s 10:%03h R %s", text, addr10, ack(sda));
      else if (low_next) begin addr10[7:0] = value; low_next = 0; text = $sformatf("%s 10:%03h W %s", text, addr10, ack(sda || !top_ack)); end
      else if (header) text = $sformatf("%s %02h %s %s", text, value[7:1], dir(value[0]), ack(sda));
      else text = $sformatf("%s %02h %s", text, value, ack(sda));
      header = 0; count = 0;
    end
  end
endmodule
RUNVerilator · simulator service
Edit the code, then run it
Expected: I2C S 50 W A 3c A Sr 50 R A 3c NA P I2C S 50 W A 3c A Sr 50 R A 3c NA P I2C S 50 W A 3c A P I2C S 50 R A 3c NA P intent broken 0, ck.broken 0
Read included file: i2c_ctrl.sv
`timescale 1ns/1ns
module i2c_ctrl(inout wire scl,sda,output reg scl_low=0,sda_low=0);
 parameter time TLOW=1300,THIGH=1200,HOLD=100,TBUF=1300,TIMEOUT=20000,THD_STA=600,TSU_STA=600,TSU_STO=600;
 bit ctrl_ignores_scl=0,ctrl_ignores_sda=0,eight_bit_addr=0,stop_between=0,data_change_scl_high=0,clear_eight_clocks=0,no_stop_after_clear=0,hold_zero_early=0,short_tbuf=0,fast_scl=0;
 bit loser_keeps_clocking_past_byte=0,loser_no_target_mode=0,drops_on_identical=0,arb_on_scl_low=0;
 bit lost=0,timed_out=0,active=0,target_mode=0;int clear_pulses=0;time stopped=0;
 assign scl=scl_low?1'b0:1'bz;assign sda=sda_low?1'b0:1'bz;
 task automatic wait_clock();time began;began=$time;
  if(!ctrl_ignores_scl)while(scl!==1'b1&&!timed_out)begin #10;if($time-began>=TIMEOUT)begin timed_out=1;scl_low=0;sda_low=0;end end
 endtask
 task automatic high_time();time began;began=$time-1;while(scl===1'b1&&$time-began<(fast_scl?100:THIGH))#1;endtask
 task automatic start();
  if(!active)begin lost=0;timed_out=0;target_mode=0;if($time-stopped<(short_tbuf?100:TBUF))#((short_tbuf?100:TBUF)-($time-stopped));end
  else begin scl_low=1;#(TLOW);end
  sda_low=0;#(HOLD);scl_low=0;wait_clock();#(TSU_STA);
  if(!timed_out)begin sda_low=1;#(THD_STA);scl_low=1;active=1;end
 endtask
 task automatic stop();
  if(!lost&&!timed_out)begin scl_low=1;#(HOLD);sda_low=1;#(TLOW);scl_low=0;wait_clock();#(TSU_STO);sda_low=0;stopped=$time;#1;end
  active=0;scl_low=0;sda_low=0;
 endtask
 task automatic bit_cycle(input bit value,input bit arbitrate,output bit sampled);
  sampled=1;
  if(!timed_out&&(!lost||loser_keeps_clocking_past_byte))begin
   scl_low=1;#(hold_zero_early?1:HOLD);sda_low=!value;
   if(arb_on_scl_low&&arbitrate&&value&&sda===0)lost=1;
   #(fast_scl?100:TLOW-(hold_zero_early?1:HOLD));scl_low=0;wait_clock();#1;sampled=sda;
   if(arbitrate&&!ctrl_ignores_sda&&((value&&!sampled)||drops_on_identical))begin
    lost=1;sda_low=0;target_mode=!loser_no_target_mode;
   end
   if(data_change_scl_high)begin #100;sda_low=!sda_low;end
   high_time();scl_low=1;
   if(lost&&!loser_keeps_clocking_past_byte)begin scl_low=0;sda_low=0;end
  end
 endtask
 task automatic write_byte(input reg [7:0] value,output bit ack);bit sampled;
  for(int b=7;b>=0;b--)bit_cycle(value[b],1,sampled);
  bit_cycle(1,0,sampled);ack=!sampled;sda_low=0;
 endtask
 task automatic read_byte(input bit ack,output reg [7:0] value);bit sampled;
  for(int b=7;b>=0;b--)begin bit_cycle(1,0,sampled);value[b]=sampled;end
  bit_cycle(!ack,0,sampled);sda_low=0;
 endtask
 task automatic address7(input reg [6:0] addr,input bit rd,output bit ack);
  write_byte(eight_bit_addr?{1'b0,addr}:{addr,rd},ack);
 endtask
 task automatic combined_read(input reg [6:0] addr,input reg [7:0] pointer,output reg [7:0] value);
  bit ack;start();address7(addr,0,ack);write_byte(pointer,ack);if(stop_between)stop();start();address7(addr,1,ack);read_byte(0,value);stop();
 endtask
 task automatic bus_clear(output bit cleared);
  bit sampled;clear_pulses=0;lost=0;timed_out=0;active=1;sda_low=0;scl_low=0;wait_clock();
  while(sda===0&&clear_pulses<(clear_eight_clocks?8:9)&&!timed_out)begin bit_cycle(1,0,sampled);#(HOLD);if(!timed_out)clear_pulses++;end
  cleared=sda===1&&!timed_out;
  if(!no_stop_after_clear)stop();else begin active=0;scl_low=0;sda_low=0;end
 endtask
endmodule
Read included file: i2c_target.sv
`timescale 1ns/1ns
module i2c_target #(parameter [9:0] ADDRESS=10'h050,parameter TEN_BIT=0)
 (inout wire scl,sda,output reg scl_low=0,sda_low=0,output wire high_drive);
 bit ack_not_released=0,ack_wrong_clock=0,stretch_never_released=0,wrong_addr_ack=0,no_stop_detect=0,autoinc_wrap_bug=0,push_pull_target=0,ignores_sr=0;
 // Legal receiver responses, not faults. rx_limit counts post-address bytes,
 // including the pointer byte; -1 is unlimited. A busy address is declined once.
 bit busy_nack=0;int rx_limit=-1,rx_count=0;
 // Optional general-call software reset: UM10204 §§3.1.13–14. This model
 // restores mem[i]=i and ptr=0; ADDRESS is fixed, not a programmable register.
 // Fault switches and bench configuration are not device reset state.
 time stretch_ns=0;bit general_call=1,software_reset=1,general_selected=0,reset_pending=0;int resets=0;
 reg [7:0] mem[0:255];reg [7:0] ptr=0,shift=0,tx=0;
 int state=0,count=0,bytes=0;bit active=0,selected=0,reading=0,need_ptr=1,ack_value=0,ack_active=0,low_addr_next=0,addressed10=0,reading_ack=0;
 // state: 0 receive, 1 receiver ACK, 2 transmit, 3 controller ACK, 4 ignore.
 assign high_drive=push_pull_target&&!sda_low;
 assign sda=sda_low?1'b0:high_drive?1'b1:1'bz;assign scl=scl_low?1'b0:1'bz;
 initial for(int i=0;i<256;i++)mem[i]=i[7:0];
 function automatic [7:0] increment(input reg [7:0] p);increment=autoinc_wrap_bug?((p+1)&8'h0f):p+1'b1;endfunction
 always @(negedge sda)if(scl===1)begin
  if(!(ignores_sr&&active))begin active=1;selected=0;state=0;count=0;bytes=0;rx_count=0;shift=0;reading=0;ack_active=0;sda_low=0;low_addr_next=0;general_selected=0;reset_pending=0;end
 end
 always @(posedge sda)if(scl===1&&active&&!no_stop_detect)begin active=0;selected=0;state=0;count=0;sda_low=0;addressed10=0;end
 always @(posedge scl)if(active)begin
  if(state==0)begin
   shift={shift[6:0],sda};count++;
   if(count==8)begin
    ack_value=0;
    if(bytes==0)begin
      reading=shift[0];
      general_selected=general_call&&shift==0;
      if(general_selected)begin selected=1;low_addr_next=0;end
      else if(TEN_BIT)begin selected=(shift[7:3]==5'b11110&&shift[2:1]==ADDRESS[9:8]&&(!reading||addressed10));low_addr_next=!reading;end
      else selected=(shift[7:1]==ADDRESS[6:0]||(wrong_addr_ack&&shift[7:1]==ADDRESS[6:0]+1'b1));
      if(selected&&busy_nack)begin selected=0;busy_nack=0;end
      ack_value=selected;need_ptr=1;
    end else if(low_addr_next)begin selected=selected&&shift==ADDRESS[7:0];ack_value=selected;addressed10=selected;low_addr_next=0;end
    else if(selected)begin
      ack_value=rx_limit<0||rx_count<rx_limit;
      if(general_selected)begin
       // Unsupported general-call commands are NACKed, never register writes.
       ack_value=ack_value&&bytes==1&&software_reset&&shift==8'h06;
       reset_pending=ack_value;
      end else if(ack_value)begin rx_count++;if(need_ptr)begin ptr=shift;need_ptr=0;end else begin mem[ptr]=shift;ptr=increment(ptr);end end
      if(!ack_value)selected=0;
    end
    bytes++;state=1;ack_active=0;count=0;
   end
  end else if(state==2)begin count++;if(count==8)begin state=3;count=0;reading_ack=0;end end
  else if(state==3&&reading_ack)begin if(sda===0)begin ptr=increment(ptr);tx=mem[ptr];state=2;count=0;end else state=4;end
 end
 always @(negedge scl)if(active)begin
  // Deliberate early-ACK fault: after seven samples, pull for clock 8.
  // The address prefix is already known; its R/W bit has not arrived yet.
  if(state==0&&count==7&&ack_wrong_clock)begin
   if(bytes==0)sda_low=!busy_nack&&((general_call&&shift[6:0]==0)||(TEN_BIT?(shift[6:2]==5'b11110&&shift[1:0]==ADDRESS[9:8]):(shift[6:0]==ADDRESS[6:0]||(wrong_addr_ack&&shift[6:0]==ADDRESS[6:0]+1'b1))));
   else sda_low=selected&&(rx_limit<0||rx_count<rx_limit);
  end else if(state==1)begin
   if(!ack_active)begin
    sda_low=ack_value&&!ack_wrong_clock;ack_active=1;
   end else begin
    sda_low=ack_not_released&&ack_value;ack_active=0;
    if(reset_pending)begin
     // Finish the ninth ACK clock before resetting and releasing the bus.
     for(int i=0;i<256;i++)mem[i]=i[7:0];
     ptr=0;shift=0;tx=0;count=0;bytes=0;rx_count=0;need_ptr=1;
     selected=0;reading=0;reading_ack=0;low_addr_next=0;addressed10=0;
     general_selected=0;reset_pending=0;resets++;sda_low=0;scl_low=0;state=4;
    end else if(!selected)state=4;
    else if(reading)begin state=2;tx=mem[ptr];count=0;sda_low=!tx[7];end
    else state=0;
    // Byte-level stretch after the ACK, as in UM10204 §3.1.9. Prepare
    // outgoing data before releasing SCL; overlap the controller's LOW.
    if(selected&&ack_value&&(stretch_ns>0||stretch_never_released))begin scl_low=1;if(!stretch_never_released)begin #(stretch_ns);scl_low=0;end end
   end
  end else if(state==2)sda_low=!tx[7-count];
  else if(state==3)begin sda_low=0;reading_ack=1;end
  else if(state==4)sda_low=0;
 end
endmodule
Read included file: i2c_checker.sv
`timescale 1ns/1ns
// Logical Fm timing only. Instantaneous digital pins cannot measure RC rise/fall or
// establish which high-SCL SDA edge was intended as data; driver intent is separate.
module i2c_checker #(parameter time TLOW=1300,THIGH=600,TBUF=1300,THD_STA=600,TSU_STA=600,TSU_DAT=100,THD_DAT=0,TSU_STO=600)
 (input wire scl,sda,input wire [2:0] sda_lows,scl_lows,input wire high_drive);
 int broken=0,starts=0,stops=0,pulses=0;bit active=0,seen_rise=0,seen_fall=0,start_pending=0;
 time rise=0,fall=0,changed=0,stop_time=0,start_time=0;
 task automatic bad(input string rule);broken++;$display("I2C CHECK %s at %0t",rule,$time);endtask
 always @(posedge scl)begin
  if(seen_fall&&$time-fall<TLOW)bad("tlow");
  if(active&&$time-changed<TSU_DAT)bad("tsu_dat");rise=$time;seen_rise=1;if(active)pulses++;
 end
 always @(negedge scl)begin
  if(active&&seen_rise&&$time-rise<THIGH)bad("thigh");
  if(start_pending&&$time-start_time<THD_STA)bad("thd_sta");start_pending=0;fall=$time;seen_fall=1;
 end
 always @(sda)begin
  if(scl===1)begin
   if(sda===0)begin
    if(!active&&stops>0&&$time-stop_time<TBUF)bad("tbuf");
    if(active&&$time-rise<TSU_STA)bad("tsu_sta");starts++;active=1;start_pending=1;start_time=$time;
   end else if(sda===1&&active)begin if($time-rise<TSU_STO)bad("tsu_sto");stops++;active=0;stop_time=$time;end
  end else if(seen_fall&&$time-fall<THD_DAT)bad("thd_dat");
  changed=$time;
 end
 always @(high_drive or sda_lows)begin #1;if(high_drive&&|sda_lows)bad("push_pull_contention");end
 // A separate intent-aware bench checks ACK ownership/ninth slot, lost-controller
 // withdrawal, timeouts and accidental START/STOP. Those are not inferable from SDA alone.
endmodule
Read included file: i2c_mon.sv
`timescale 1ns/1ns
module i2c_mon(input wire scl,sda);
 bit active=0,header=1,trace=0;int count=0,frames=0,starts=0,stops=0;reg [7:0] value=0;
 // Assemble the whole S ... Sr ... P transaction before one atomic report.
 // Incomplete/stuck transactions produce no line; counters remain live.
 string text="";
 always @(negedge sda)if(scl===1)begin if(active)text={text," Sr"};else text="I2C S";active=1;header=1;count=0;starts++;end
 always @(posedge sda)if(scl===1&&active)begin if(trace)$display("%s P",text);text="";active=0;count=0;frames++;stops++;end
 always @(posedge scl)if(active)begin
  if(count<8)begin value={value[6:0],sda};count++;end
  else begin
   if(header)text=$sformatf("%s %02h %s",text,value[7:1],value[0]?"R":"W");
   else text=$sformatf("%s %02h",text,value);
   text={text,sda?" NA":" A"};header=0;count=0;
  end
 end
endmodule
// Trusted recovery observer. Driver intent distinguishes the STOP setup edge
// from recovery pulses, including when a stuck target prevents an actual STOP.
module i2c_clear_mon(input wire scl,sda,ctrl_sda_low,enabled,done,timed_out);
 int pulses=0;bit stopped=0;
 always @(posedge enabled)begin pulses=0;stopped=0;end
 always @(posedge scl)if(enabled&&!ctrl_sda_low)pulses++;
 always @(posedge sda)if(enabled&&scl===1)stopped=1;
 always @(posedge done)$display("I2C CLEAR PULSES %0d STOP %0d CLEARED %0d TIMEOUT %0d",pulses,stopped,(sda===1&&!timed_out),timed_out);
endmodule
Read included file: i2c_sva.sv
`timescale 1ns/1ns
module i2c_sva #(parameter integer REPORT_LIMIT=200)(input wire tick,rst_n,enabled,scl,sda,cdl,tdl,c2dl,high_drive,
 input wire [1:0] condition, // 0 data, 1 completed S/Sr, 2 completed P
 input wire ack_slot,target_tx,ack_expected,lost,free_bus,start_seen,
 input wire [3:0] target_count,input wire byte_done,input wire [7:0] byte_value);
 int broken=0;bit previous_scl=0,previous_cdl=0,previous_tdl=0,primed=0;
 bit header=1,need_low_address=0,selected10=0;
 // Limit printed reports per rule per reset epoch; checking and broken keep
 // counting every violation. REPORT_LIMIT=0 is silent, <0 is unlimited.
 int reports[0:9];
 initial for(int r=0;r<10;r++)reports[r]=0;
 task automatic bad(input int rule_id,input string rule);
  broken++;
  if(REPORT_LIMIT<0||reports[rule_id]<REPORT_LIMIT)begin
   $display("I2C INTENT %s at %0t",rule,$time);reports[rule_id]++;
  end
 endtask
 always @(posedge tick)begin
  if(!rst_n)begin for(int r=0;r<10;r++)reports[r]=0;broken=0;primed=0;header=1;need_low_address=0;selected10=0;end
  else if(enabled)begin
   if(primed&&previous_scl&&scl)begin
    assert((previous_tdl==tdl)&&(condition!=0||previous_cdl==cdl))else bad(0,"sda_stable");
   end
   if(condition==1)begin assert(scl&&!sda)else bad(1,"start_shape");end
   if(condition==2)begin assert(scl&&sda)else bad(2,"stop_shape");end
   if(scl&&ack_slot)begin
    assert((target_tx?!tdl:!cdl)&&((target_tx?cdl:tdl)==ack_expected))else bad(3,"ack_owner");
   end
   if(scl&&!ack_slot&&!target_tx&&condition==0)begin
    assert(!tdl)else bad(4,"ack_slot_only");
   end
   if(lost&&!free_bus)begin assert(!cdl)else bad(5,"loser_releases");end
   if(free_bus)begin assert(!cdl&&!tdl&&!high_drive)else bad(6,"no_drive_when_idle");end
   assert(!(high_drive&&(cdl||tdl||c2dl)))else bad(7,"push_pull_contention");
   if(start_seen)begin
    assert(target_count==0)else bad(8,"reset_on_start");
    assert(!need_low_address)else bad(9,"ten_bit_header");
    header=1;need_low_address=0;
   end
   if(byte_done)begin
    if(header)begin
     if(byte_value[7:3]==5'b11110)begin
      assert(!byte_value[0]||selected10)else bad(9,"ten_bit_header");
      need_low_address=!byte_value[0];
     end
     header=0;
    end else if(need_low_address)begin need_low_address=0;selected10=1;end
   end
   if(condition==2)begin header=1;need_low_address=0;selected10=0;end
   previous_scl=scl;previous_cdl=cdl;previous_tdl=tdl;primed=1;
  end
 end
endmodule
Read included file: p8_intent.sv
`timescale 1ns/1ns
// p8_intent.sv: decodes the wire on its own and supplies i2c_sva's intent inputs every 50 ns.
//   condition  1 for an S or Sr, 2 for a P: an SDA edge under a high SCL between bytes. The same edge
//              inside a byte is a data edge that moved at the wrong time, so it stays 0.
//   ack_slot   the ninth SCL high of a byte;   target_tx  data bytes of a read the target accepted
//   ack_expected   the plan: the target accepts ADDRESS and the general call (or, with TEN, the 10-bit address
//              A10 in its two bytes); on reads the controller's own ACK is trusted
//   lost       the controller let SDA go (sent 1), read 0, and nobody else it knows of pulled: another controller
module p8_intent #(parameter [6:0] ADDRESS = 7'h50, parameter TEN = 0, parameter [9:0] A10 = 10'h2aa)
  (input wire scl, sda, cdl, tdl, c2dl, high_drive, input wire [3:0] target_count);
  reg tick = 0, rst_n = 0;
  always #25 tick = ~tick;
  initial #100 rst_n = 1;
  reg [1:0] condition = 0;
  reg start_seen = 0, byte_done = 0, active = 0, header = 1, reading = 0, selected = 0, lost = 0, low_next = 0, addressed10 = 0;
  reg [7:0] shift = 0, byte_value = 0;
  int count = 0;
  time set_at = 0, last_tick = 0;
  wire ack_slot = active && count == 9;
  wire target_tx = active && reading && !header && selected;
  wire hdr10 = shift[7:3] == 5'b11110 && shift[2:1] == A10[9:8];
  wire accepts = !TEN ? (shift[7:1] == ADDRESS || shift == 8'h00) : low_next ? shift == A10[7:0] : hdr10 && (!shift[0] || addressed10);
  wire ack_expected = target_tx ? cdl : (header || low_next) ? accepts : selected;
  wire free_bus = !active;
  always @(negedge sda) if (scl === 1'b1 && count <= 1) begin
    condition = 1; start_seen = 1; set_at = $time; active = 1; header = 1; count = 0; lost = 0; low_next = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active && count <= 1) begin
    condition = 2; set_at = $time; active = 0; count = 0; selected = 0; addressed10 = 0;
  end
  always @(posedge scl) if (active) begin
    count++;
    if (count <= 8) begin
      shift = {shift[6:0], sda};
      if (!target_tx && cdl === 1'b0 && sda === 1'b0 && tdl === 1'b0) lost = 1;
    end else begin byte_done = 1; byte_value = shift; set_at = $time; end
  end
  always @(negedge scl) if (active && count == 9) begin
    if (header) begin reading = shift[0]; selected = accepts; header = 0; low_next = TEN && hdr10 && !shift[0]; end
    else if (low_next) begin selected = accepts; addressed10 = accepts; low_next = 0; end
    count = 0;
  end
  // A flag lives until one sampling edge has seen it.
  always @(posedge tick) last_tick = $time;
  always @(negedge tick) if (set_at < last_tick) begin condition = 0; start_seen = 0; byte_done = 0; end
  i2c_sva ck(tick, rst_n, 1'b1, scl, sda, cdl, tdl, c2dl, high_drive, condition, ack_slot, target_tx, ack_expected,
             lost, free_bus, start_seen, target_count, byte_done, byte_value);
endmodule
Read included file: p8_mon.sv
`timescale 1ns/1ns
// p8_mon.sv: one line per transaction, printed at the STOP, in the library decoder's grammar.
// ten_bit = 1 decodes a 1111 0XX header and its low byte as 10:<address>. report_at_start = 1 is the twin
// that prints at the START, before a single address bit has been sampled.
module p8_mon(input wire scl, sda);
  bit report_at_start = 0, ten_bit = 1;
  string text = "";
  bit active = 0, header = 1, low_next = 0, top_ack = 0;
  int count = 0;
  reg [7:0] value = 0; reg [9:0] addr10 = 0;
  function automatic string ack(input bit level); if (level) return "NA"; return "A"; endfunction
  function automatic string dir(input bit rd); if (rd) return "R"; return "W"; endfunction
  always @(negedge sda) if (scl === 1'b1) begin
    low_next = 0;
    if (report_at_start) $display("I2C S %02h", value[7:1]);
    else if (active) text = {text, " Sr"};
    else text = "I2C S";
    active = 1; header = 1; count = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active) begin
    if (!report_at_start) $display("%s P", text);
    active = 0; count = 0;
  end
  always @(posedge scl) if (active) begin
    if (count < 8) begin value = {value[6:0], sda}; count++; end
    else begin
      if (header && ten_bit && value[7:3] == 5'b11110 && !value[0]) begin addr10[9:8] = value[2:1]; low_next = 1; top_ack = !sda; end
      else if (header && ten_bit && value[7:3] == 5'b11110) text = $sformatf("%s 10:%03h R %s", text, addr10, ack(sda));
      else if (low_next) begin addr10[7:0] = value; low_next = 0; text = $sformatf("%s 10:%03h W %s", text, addr10, ack(sda || !top_ack)); end
      else if (header) text = $sformatf("%s %02h %s %s", text, value[7:1], dir(value[0]), ack(sda));
      else text = $sformatf("%s %02h %s", text, value, ack(sda));
      header = 0; count = 0;
    end
  end
endmodule

Check Yourself

Predict the outputVerilator, the intent harness bound to the bench, target.stretch_ns = 3000, the correct controller, one register read. The program prints intent.ck.broken. What does it print?
`timescale 1ns/1ns
module tb;
  tri1 scl, sda;                          // the two lines; tri1 is the pull-up
  wire csl, cdl, tsl, tdl, c2sl, c2dl;    // driver view: 1 = that device pulls SCL or SDA low
  wire high_drive;                        // 1 only while a push-pull target drives SDA high
  bit ack, ack2, cleared; reg [7:0] value;
  i2c_ctrl    ctrl  (scl, sda, csl, cdl);              // controller: task calls drive the pins
  i2c_target  target(scl, sda, tsl, tdl, high_drive);  // register target, mem[i] = i
  assign c2sl = 0; assign c2dl = 0;       // no second controller
  i2c_checker #(.TLOW(1300), .THIGH(600), .TBUF(1300), .THD_STA(600), .TSU_STA(600), .TSU_DAT(100), .THD_DAT(0), .TSU_STO(600))
              ck(scl, sda, {c2dl, tdl, cdl}, {c2sl, tsl, csl}, high_drive);   // prints I2C CHECK <rule>
  i2c_mon     mon(scl, sda);                                // prints I2C S 50 W A ... P
  p8_intent intent(scl, sda, cdl, tdl, c2dl, high_drive, target.bytes[3:0]);   // i2c_sva with its intent inputs
  initial begin
    #2000;
    target.stretch_ns = 3000;
    ctrl.combined_read(7'h50, 8'h3c, value);
    $display("%0d", intent.ck.broken);
    $finish;
  end
  initial begin #3000000; $fatal(1, "I2C watchdog"); end
endmodule
Read included file: i2c_ctrl.sv
`timescale 1ns/1ns
module i2c_ctrl(inout wire scl,sda,output reg scl_low=0,sda_low=0);
 parameter time TLOW=1300,THIGH=1200,HOLD=100,TBUF=1300,TIMEOUT=20000,THD_STA=600,TSU_STA=600,TSU_STO=600;
 bit ctrl_ignores_scl=0,ctrl_ignores_sda=0,eight_bit_addr=0,stop_between=0,data_change_scl_high=0,clear_eight_clocks=0,no_stop_after_clear=0,hold_zero_early=0,short_tbuf=0,fast_scl=0;
 bit loser_keeps_clocking_past_byte=0,loser_no_target_mode=0,drops_on_identical=0,arb_on_scl_low=0;
 bit lost=0,timed_out=0,active=0,target_mode=0;int clear_pulses=0;time stopped=0;
 assign scl=scl_low?1'b0:1'bz;assign sda=sda_low?1'b0:1'bz;
 task automatic wait_clock();time began;began=$time;
  if(!ctrl_ignores_scl)while(scl!==1'b1&&!timed_out)begin #10;if($time-began>=TIMEOUT)begin timed_out=1;scl_low=0;sda_low=0;end end
 endtask
 task automatic high_time();time began;began=$time-1;while(scl===1'b1&&$time-began<(fast_scl?100:THIGH))#1;endtask
 task automatic start();
  if(!active)begin lost=0;timed_out=0;target_mode=0;if($time-stopped<(short_tbuf?100:TBUF))#((short_tbuf?100:TBUF)-($time-stopped));end
  else begin scl_low=1;#(TLOW);end
  sda_low=0;#(HOLD);scl_low=0;wait_clock();#(TSU_STA);
  if(!timed_out)begin sda_low=1;#(THD_STA);scl_low=1;active=1;end
 endtask
 task automatic stop();
  if(!lost&&!timed_out)begin scl_low=1;#(HOLD);sda_low=1;#(TLOW);scl_low=0;wait_clock();#(TSU_STO);sda_low=0;stopped=$time;#1;end
  active=0;scl_low=0;sda_low=0;
 endtask
 task automatic bit_cycle(input bit value,input bit arbitrate,output bit sampled);
  sampled=1;
  if(!timed_out&&(!lost||loser_keeps_clocking_past_byte))begin
   scl_low=1;#(hold_zero_early?1:HOLD);sda_low=!value;
   if(arb_on_scl_low&&arbitrate&&value&&sda===0)lost=1;
   #(fast_scl?100:TLOW-(hold_zero_early?1:HOLD));scl_low=0;wait_clock();#1;sampled=sda;
   if(arbitrate&&!ctrl_ignores_sda&&((value&&!sampled)||drops_on_identical))begin
    lost=1;sda_low=0;target_mode=!loser_no_target_mode;
   end
   if(data_change_scl_high)begin #100;sda_low=!sda_low;end
   high_time();scl_low=1;
   if(lost&&!loser_keeps_clocking_past_byte)begin scl_low=0;sda_low=0;end
  end
 endtask
 task automatic write_byte(input reg [7:0] value,output bit ack);bit sampled;
  for(int b=7;b>=0;b--)bit_cycle(value[b],1,sampled);
  bit_cycle(1,0,sampled);ack=!sampled;sda_low=0;
 endtask
 task automatic read_byte(input bit ack,output reg [7:0] value);bit sampled;
  for(int b=7;b>=0;b--)begin bit_cycle(1,0,sampled);value[b]=sampled;end
  bit_cycle(!ack,0,sampled);sda_low=0;
 endtask
 task automatic address7(input reg [6:0] addr,input bit rd,output bit ack);
  write_byte(eight_bit_addr?{1'b0,addr}:{addr,rd},ack);
 endtask
 task automatic combined_read(input reg [6:0] addr,input reg [7:0] pointer,output reg [7:0] value);
  bit ack;start();address7(addr,0,ack);write_byte(pointer,ack);if(stop_between)stop();start();address7(addr,1,ack);read_byte(0,value);stop();
 endtask
 task automatic bus_clear(output bit cleared);
  bit sampled;clear_pulses=0;lost=0;timed_out=0;active=1;sda_low=0;scl_low=0;wait_clock();
  while(sda===0&&clear_pulses<(clear_eight_clocks?8:9)&&!timed_out)begin bit_cycle(1,0,sampled);#(HOLD);if(!timed_out)clear_pulses++;end
  cleared=sda===1&&!timed_out;
  if(!no_stop_after_clear)stop();else begin active=0;scl_low=0;sda_low=0;end
 endtask
endmodule
Read included file: i2c_target.sv
`timescale 1ns/1ns
module i2c_target #(parameter [9:0] ADDRESS=10'h050,parameter TEN_BIT=0)
 (inout wire scl,sda,output reg scl_low=0,sda_low=0,output wire high_drive);
 bit ack_not_released=0,ack_wrong_clock=0,stretch_never_released=0,wrong_addr_ack=0,no_stop_detect=0,autoinc_wrap_bug=0,push_pull_target=0,ignores_sr=0;
 // Legal receiver responses, not faults. rx_limit counts post-address bytes,
 // including the pointer byte; -1 is unlimited. A busy address is declined once.
 bit busy_nack=0;int rx_limit=-1,rx_count=0;
 // Optional general-call software reset: UM10204 §§3.1.13–14. This model
 // restores mem[i]=i and ptr=0; ADDRESS is fixed, not a programmable register.
 // Fault switches and bench configuration are not device reset state.
 time stretch_ns=0;bit general_call=1,software_reset=1,general_selected=0,reset_pending=0;int resets=0;
 reg [7:0] mem[0:255];reg [7:0] ptr=0,shift=0,tx=0;
 int state=0,count=0,bytes=0;bit active=0,selected=0,reading=0,need_ptr=1,ack_value=0,ack_active=0,low_addr_next=0,addressed10=0,reading_ack=0;
 // state: 0 receive, 1 receiver ACK, 2 transmit, 3 controller ACK, 4 ignore.
 assign high_drive=push_pull_target&&!sda_low;
 assign sda=sda_low?1'b0:high_drive?1'b1:1'bz;assign scl=scl_low?1'b0:1'bz;
 initial for(int i=0;i<256;i++)mem[i]=i[7:0];
 function automatic [7:0] increment(input reg [7:0] p);increment=autoinc_wrap_bug?((p+1)&8'h0f):p+1'b1;endfunction
 always @(negedge sda)if(scl===1)begin
  if(!(ignores_sr&&active))begin active=1;selected=0;state=0;count=0;bytes=0;rx_count=0;shift=0;reading=0;ack_active=0;sda_low=0;low_addr_next=0;general_selected=0;reset_pending=0;end
 end
 always @(posedge sda)if(scl===1&&active&&!no_stop_detect)begin active=0;selected=0;state=0;count=0;sda_low=0;addressed10=0;end
 always @(posedge scl)if(active)begin
  if(state==0)begin
   shift={shift[6:0],sda};count++;
   if(count==8)begin
    ack_value=0;
    if(bytes==0)begin
      reading=shift[0];
      general_selected=general_call&&shift==0;
      if(general_selected)begin selected=1;low_addr_next=0;end
      else if(TEN_BIT)begin selected=(shift[7:3]==5'b11110&&shift[2:1]==ADDRESS[9:8]&&(!reading||addressed10));low_addr_next=!reading;end
      else selected=(shift[7:1]==ADDRESS[6:0]||(wrong_addr_ack&&shift[7:1]==ADDRESS[6:0]+1'b1));
      if(selected&&busy_nack)begin selected=0;busy_nack=0;end
      ack_value=selected;need_ptr=1;
    end else if(low_addr_next)begin selected=selected&&shift==ADDRESS[7:0];ack_value=selected;addressed10=selected;low_addr_next=0;end
    else if(selected)begin
      ack_value=rx_limit<0||rx_count<rx_limit;
      if(general_selected)begin
       // Unsupported general-call commands are NACKed, never register writes.
       ack_value=ack_value&&bytes==1&&software_reset&&shift==8'h06;
       reset_pending=ack_value;
      end else if(ack_value)begin rx_count++;if(need_ptr)begin ptr=shift;need_ptr=0;end else begin mem[ptr]=shift;ptr=increment(ptr);end end
      if(!ack_value)selected=0;
    end
    bytes++;state=1;ack_active=0;count=0;
   end
  end else if(state==2)begin count++;if(count==8)begin state=3;count=0;reading_ack=0;end end
  else if(state==3&&reading_ack)begin if(sda===0)begin ptr=increment(ptr);tx=mem[ptr];state=2;count=0;end else state=4;end
 end
 always @(negedge scl)if(active)begin
  // Deliberate early-ACK fault: after seven samples, pull for clock 8.
  // The address prefix is already known; its R/W bit has not arrived yet.
  if(state==0&&count==7&&ack_wrong_clock)begin
   if(bytes==0)sda_low=!busy_nack&&((general_call&&shift[6:0]==0)||(TEN_BIT?(shift[6:2]==5'b11110&&shift[1:0]==ADDRESS[9:8]):(shift[6:0]==ADDRESS[6:0]||(wrong_addr_ack&&shift[6:0]==ADDRESS[6:0]+1'b1))));
   else sda_low=selected&&(rx_limit<0||rx_count<rx_limit);
  end else if(state==1)begin
   if(!ack_active)begin
    sda_low=ack_value&&!ack_wrong_clock;ack_active=1;
   end else begin
    sda_low=ack_not_released&&ack_value;ack_active=0;
    if(reset_pending)begin
     // Finish the ninth ACK clock before resetting and releasing the bus.
     for(int i=0;i<256;i++)mem[i]=i[7:0];
     ptr=0;shift=0;tx=0;count=0;bytes=0;rx_count=0;need_ptr=1;
     selected=0;reading=0;reading_ack=0;low_addr_next=0;addressed10=0;
     general_selected=0;reset_pending=0;resets++;sda_low=0;scl_low=0;state=4;
    end else if(!selected)state=4;
    else if(reading)begin state=2;tx=mem[ptr];count=0;sda_low=!tx[7];end
    else state=0;
    // Byte-level stretch after the ACK, as in UM10204 §3.1.9. Prepare
    // outgoing data before releasing SCL; overlap the controller's LOW.
    if(selected&&ack_value&&(stretch_ns>0||stretch_never_released))begin scl_low=1;if(!stretch_never_released)begin #(stretch_ns);scl_low=0;end end
   end
  end else if(state==2)sda_low=!tx[7-count];
  else if(state==3)begin sda_low=0;reading_ack=1;end
  else if(state==4)sda_low=0;
 end
endmodule
Read included file: i2c_checker.sv
`timescale 1ns/1ns
// Logical Fm timing only. Instantaneous digital pins cannot measure RC rise/fall or
// establish which high-SCL SDA edge was intended as data; driver intent is separate.
module i2c_checker #(parameter time TLOW=1300,THIGH=600,TBUF=1300,THD_STA=600,TSU_STA=600,TSU_DAT=100,THD_DAT=0,TSU_STO=600)
 (input wire scl,sda,input wire [2:0] sda_lows,scl_lows,input wire high_drive);
 int broken=0,starts=0,stops=0,pulses=0;bit active=0,seen_rise=0,seen_fall=0,start_pending=0;
 time rise=0,fall=0,changed=0,stop_time=0,start_time=0;
 task automatic bad(input string rule);broken++;$display("I2C CHECK %s at %0t",rule,$time);endtask
 always @(posedge scl)begin
  if(seen_fall&&$time-fall<TLOW)bad("tlow");
  if(active&&$time-changed<TSU_DAT)bad("tsu_dat");rise=$time;seen_rise=1;if(active)pulses++;
 end
 always @(negedge scl)begin
  if(active&&seen_rise&&$time-rise<THIGH)bad("thigh");
  if(start_pending&&$time-start_time<THD_STA)bad("thd_sta");start_pending=0;fall=$time;seen_fall=1;
 end
 always @(sda)begin
  if(scl===1)begin
   if(sda===0)begin
    if(!active&&stops>0&&$time-stop_time<TBUF)bad("tbuf");
    if(active&&$time-rise<TSU_STA)bad("tsu_sta");starts++;active=1;start_pending=1;start_time=$time;
   end else if(sda===1&&active)begin if($time-rise<TSU_STO)bad("tsu_sto");stops++;active=0;stop_time=$time;end
  end else if(seen_fall&&$time-fall<THD_DAT)bad("thd_dat");
  changed=$time;
 end
 always @(high_drive or sda_lows)begin #1;if(high_drive&&|sda_lows)bad("push_pull_contention");end
 // A separate intent-aware bench checks ACK ownership/ninth slot, lost-controller
 // withdrawal, timeouts and accidental START/STOP. Those are not inferable from SDA alone.
endmodule
Read included file: i2c_mon.sv
`timescale 1ns/1ns
module i2c_mon(input wire scl,sda);
 bit active=0,header=1,trace=0;int count=0,frames=0,starts=0,stops=0;reg [7:0] value=0;
 // Assemble the whole S ... Sr ... P transaction before one atomic report.
 // Incomplete/stuck transactions produce no line; counters remain live.
 string text="";
 always @(negedge sda)if(scl===1)begin if(active)text={text," Sr"};else text="I2C S";active=1;header=1;count=0;starts++;end
 always @(posedge sda)if(scl===1&&active)begin if(trace)$display("%s P",text);text="";active=0;count=0;frames++;stops++;end
 always @(posedge scl)if(active)begin
  if(count<8)begin value={value[6:0],sda};count++;end
  else begin
   if(header)text=$sformatf("%s %02h %s",text,value[7:1],value[0]?"R":"W");
   else text=$sformatf("%s %02h",text,value);
   text={text,sda?" NA":" A"};header=0;count=0;
  end
 end
endmodule
// Trusted recovery observer. Driver intent distinguishes the STOP setup edge
// from recovery pulses, including when a stuck target prevents an actual STOP.
module i2c_clear_mon(input wire scl,sda,ctrl_sda_low,enabled,done,timed_out);
 int pulses=0;bit stopped=0;
 always @(posedge enabled)begin pulses=0;stopped=0;end
 always @(posedge scl)if(enabled&&!ctrl_sda_low)pulses++;
 always @(posedge sda)if(enabled&&scl===1)stopped=1;
 always @(posedge done)$display("I2C CLEAR PULSES %0d STOP %0d CLEARED %0d TIMEOUT %0d",pulses,stopped,(sda===1&&!timed_out),timed_out);
endmodule
Read included file: i2c_sva.sv
`timescale 1ns/1ns
module i2c_sva #(parameter integer REPORT_LIMIT=200)(input wire tick,rst_n,enabled,scl,sda,cdl,tdl,c2dl,high_drive,
 input wire [1:0] condition, // 0 data, 1 completed S/Sr, 2 completed P
 input wire ack_slot,target_tx,ack_expected,lost,free_bus,start_seen,
 input wire [3:0] target_count,input wire byte_done,input wire [7:0] byte_value);
 int broken=0;bit previous_scl=0,previous_cdl=0,previous_tdl=0,primed=0;
 bit header=1,need_low_address=0,selected10=0;
 // Limit printed reports per rule per reset epoch; checking and broken keep
 // counting every violation. REPORT_LIMIT=0 is silent, <0 is unlimited.
 int reports[0:9];
 initial for(int r=0;r<10;r++)reports[r]=0;
 task automatic bad(input int rule_id,input string rule);
  broken++;
  if(REPORT_LIMIT<0||reports[rule_id]<REPORT_LIMIT)begin
   $display("I2C INTENT %s at %0t",rule,$time);reports[rule_id]++;
  end
 endtask
 always @(posedge tick)begin
  if(!rst_n)begin for(int r=0;r<10;r++)reports[r]=0;broken=0;primed=0;header=1;need_low_address=0;selected10=0;end
  else if(enabled)begin
   if(primed&&previous_scl&&scl)begin
    assert((previous_tdl==tdl)&&(condition!=0||previous_cdl==cdl))else bad(0,"sda_stable");
   end
   if(condition==1)begin assert(scl&&!sda)else bad(1,"start_shape");end
   if(condition==2)begin assert(scl&&sda)else bad(2,"stop_shape");end
   if(scl&&ack_slot)begin
    assert((target_tx?!tdl:!cdl)&&((target_tx?cdl:tdl)==ack_expected))else bad(3,"ack_owner");
   end
   if(scl&&!ack_slot&&!target_tx&&condition==0)begin
    assert(!tdl)else bad(4,"ack_slot_only");
   end
   if(lost&&!free_bus)begin assert(!cdl)else bad(5,"loser_releases");end
   if(free_bus)begin assert(!cdl&&!tdl&&!high_drive)else bad(6,"no_drive_when_idle");end
   assert(!(high_drive&&(cdl||tdl||c2dl)))else bad(7,"push_pull_contention");
   if(start_seen)begin
    assert(target_count==0)else bad(8,"reset_on_start");
    assert(!need_low_address)else bad(9,"ten_bit_header");
    header=1;need_low_address=0;
   end
   if(byte_done)begin
    if(header)begin
     if(byte_value[7:3]==5'b11110)begin
      assert(!byte_value[0]||selected10)else bad(9,"ten_bit_header");
      need_low_address=!byte_value[0];
     end
     header=0;
    end else if(need_low_address)begin need_low_address=0;selected10=1;end
   end
   if(condition==2)begin header=1;need_low_address=0;selected10=0;end
   previous_scl=scl;previous_cdl=cdl;previous_tdl=tdl;primed=1;
  end
 end
endmodule
Read included file: p8_intent.sv
`timescale 1ns/1ns
// p8_intent.sv: decodes the wire on its own and supplies i2c_sva's intent inputs every 50 ns.
//   condition  1 for an S or Sr, 2 for a P: an SDA edge under a high SCL between bytes. The same edge
//              inside a byte is a data edge that moved at the wrong time, so it stays 0.
//   ack_slot   the ninth SCL high of a byte;   target_tx  data bytes of a read the target accepted
//   ack_expected   the plan: the target accepts ADDRESS and the general call (or, with TEN, the 10-bit address
//              A10 in its two bytes); on reads the controller's own ACK is trusted
//   lost       the controller let SDA go (sent 1), read 0, and nobody else it knows of pulled: another controller
module p8_intent #(parameter [6:0] ADDRESS = 7'h50, parameter TEN = 0, parameter [9:0] A10 = 10'h2aa)
  (input wire scl, sda, cdl, tdl, c2dl, high_drive, input wire [3:0] target_count);
  reg tick = 0, rst_n = 0;
  always #25 tick = ~tick;
  initial #100 rst_n = 1;
  reg [1:0] condition = 0;
  reg start_seen = 0, byte_done = 0, active = 0, header = 1, reading = 0, selected = 0, lost = 0, low_next = 0, addressed10 = 0;
  reg [7:0] shift = 0, byte_value = 0;
  int count = 0;
  time set_at = 0, last_tick = 0;
  wire ack_slot = active && count == 9;
  wire target_tx = active && reading && !header && selected;
  wire hdr10 = shift[7:3] == 5'b11110 && shift[2:1] == A10[9:8];
  wire accepts = !TEN ? (shift[7:1] == ADDRESS || shift == 8'h00) : low_next ? shift == A10[7:0] : hdr10 && (!shift[0] || addressed10);
  wire ack_expected = target_tx ? cdl : (header || low_next) ? accepts : selected;
  wire free_bus = !active;
  always @(negedge sda) if (scl === 1'b1 && count <= 1) begin
    condition = 1; start_seen = 1; set_at = $time; active = 1; header = 1; count = 0; lost = 0; low_next = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active && count <= 1) begin
    condition = 2; set_at = $time; active = 0; count = 0; selected = 0; addressed10 = 0;
  end
  always @(posedge scl) if (active) begin
    count++;
    if (count <= 8) begin
      shift = {shift[6:0], sda};
      if (!target_tx && cdl === 1'b0 && sda === 1'b0 && tdl === 1'b0) lost = 1;
    end else begin byte_done = 1; byte_value = shift; set_at = $time; end
  end
  always @(negedge scl) if (active && count == 9) begin
    if (header) begin reading = shift[0]; selected = accepts; header = 0; low_next = TEN && hdr10 && !shift[0]; end
    else if (low_next) begin selected = accepts; addressed10 = accepts; low_next = 0; end
    count = 0;
  end
  // A flag lives until one sampling edge has seen it.
  always @(posedge tick) last_tick = $time;
  always @(negedge tick) if (set_at < last_tick) begin condition = 0; start_seen = 0; byte_done = 0; end
  i2c_sva ck(tick, rst_n, 1'b1, scl, sda, cdl, tdl, c2dl, high_drive, condition, ack_slot, target_tx, ack_expected,
             lost, free_bus, start_seen, target_count, byte_done, byte_value);
endmodule
Read included file: p8_mon.sv
`timescale 1ns/1ns
// p8_mon.sv: one line per transaction, printed at the STOP, in the library decoder's grammar.
// ten_bit = 1 decodes a 1111 0XX header and its low byte as 10:<address>. report_at_start = 1 is the twin
// that prints at the START, before a single address bit has been sampled.
module p8_mon(input wire scl, sda);
  bit report_at_start = 0, ten_bit = 1;
  string text = "";
  bit active = 0, header = 1, low_next = 0, top_ack = 0;
  int count = 0;
  reg [7:0] value = 0; reg [9:0] addr10 = 0;
  function automatic string ack(input bit level); if (level) return "NA"; return "A"; endfunction
  function automatic string dir(input bit rd); if (rd) return "R"; return "W"; endfunction
  always @(negedge sda) if (scl === 1'b1) begin
    low_next = 0;
    if (report_at_start) $display("I2C S %02h", value[7:1]);
    else if (active) text = {text, " Sr"};
    else text = "I2C S";
    active = 1; header = 1; count = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active) begin
    if (!report_at_start) $display("%s P", text);
    active = 0; count = 0;
  end
  always @(posedge scl) if (active) begin
    if (count < 8) begin value = {value[6:0], sda}; count++; end
    else begin
      if (header && ten_bit && value[7:3] == 5'b11110 && !value[0]) begin addr10[9:8] = value[2:1]; low_next = 1; top_ack = !sda; end
      else if (header && ten_bit && value[7:3] == 5'b11110) text = $sformatf("%s 10:%03h R %s", text, addr10, ack(sda));
      else if (low_next) begin addr10[7:0] = value; low_next = 0; text = $sformatf("%s 10:%03h W %s", text, addr10, ack(sda || !top_ack)); end
      else if (header) text = $sformatf("%s %02h %s %s", text, value[7:1], dir(value[0]), ack(sda));
      else text = $sformatf("%s %02h %s", text, value, ack(sda));
      header = 0; count = 0;
    end
  end
endmodule
Spot the bugEach option is a candidate rule "a target never pulls SDA during the controller's bits", checked 5 ns after each SCL rise and counting in fired. On a correct bus (a stretched register read and a write) a good rule stays at 0. Which rule raises a false alarm?
Read included file: i2c_ctrl.sv
`timescale 1ns/1ns
module i2c_ctrl(inout wire scl,sda,output reg scl_low=0,sda_low=0);
 parameter time TLOW=1300,THIGH=1200,HOLD=100,TBUF=1300,TIMEOUT=20000,THD_STA=600,TSU_STA=600,TSU_STO=600;
 bit ctrl_ignores_scl=0,ctrl_ignores_sda=0,eight_bit_addr=0,stop_between=0,data_change_scl_high=0,clear_eight_clocks=0,no_stop_after_clear=0,hold_zero_early=0,short_tbuf=0,fast_scl=0;
 bit loser_keeps_clocking_past_byte=0,loser_no_target_mode=0,drops_on_identical=0,arb_on_scl_low=0;
 bit lost=0,timed_out=0,active=0,target_mode=0;int clear_pulses=0;time stopped=0;
 assign scl=scl_low?1'b0:1'bz;assign sda=sda_low?1'b0:1'bz;
 task automatic wait_clock();time began;began=$time;
  if(!ctrl_ignores_scl)while(scl!==1'b1&&!timed_out)begin #10;if($time-began>=TIMEOUT)begin timed_out=1;scl_low=0;sda_low=0;end end
 endtask
 task automatic high_time();time began;began=$time-1;while(scl===1'b1&&$time-began<(fast_scl?100:THIGH))#1;endtask
 task automatic start();
  if(!active)begin lost=0;timed_out=0;target_mode=0;if($time-stopped<(short_tbuf?100:TBUF))#((short_tbuf?100:TBUF)-($time-stopped));end
  else begin scl_low=1;#(TLOW);end
  sda_low=0;#(HOLD);scl_low=0;wait_clock();#(TSU_STA);
  if(!timed_out)begin sda_low=1;#(THD_STA);scl_low=1;active=1;end
 endtask
 task automatic stop();
  if(!lost&&!timed_out)begin scl_low=1;#(HOLD);sda_low=1;#(TLOW);scl_low=0;wait_clock();#(TSU_STO);sda_low=0;stopped=$time;#1;end
  active=0;scl_low=0;sda_low=0;
 endtask
 task automatic bit_cycle(input bit value,input bit arbitrate,output bit sampled);
  sampled=1;
  if(!timed_out&&(!lost||loser_keeps_clocking_past_byte))begin
   scl_low=1;#(hold_zero_early?1:HOLD);sda_low=!value;
   if(arb_on_scl_low&&arbitrate&&value&&sda===0)lost=1;
   #(fast_scl?100:TLOW-(hold_zero_early?1:HOLD));scl_low=0;wait_clock();#1;sampled=sda;
   if(arbitrate&&!ctrl_ignores_sda&&((value&&!sampled)||drops_on_identical))begin
    lost=1;sda_low=0;target_mode=!loser_no_target_mode;
   end
   if(data_change_scl_high)begin #100;sda_low=!sda_low;end
   high_time();scl_low=1;
   if(lost&&!loser_keeps_clocking_past_byte)begin scl_low=0;sda_low=0;end
  end
 endtask
 task automatic write_byte(input reg [7:0] value,output bit ack);bit sampled;
  for(int b=7;b>=0;b--)bit_cycle(value[b],1,sampled);
  bit_cycle(1,0,sampled);ack=!sampled;sda_low=0;
 endtask
 task automatic read_byte(input bit ack,output reg [7:0] value);bit sampled;
  for(int b=7;b>=0;b--)begin bit_cycle(1,0,sampled);value[b]=sampled;end
  bit_cycle(!ack,0,sampled);sda_low=0;
 endtask
 task automatic address7(input reg [6:0] addr,input bit rd,output bit ack);
  write_byte(eight_bit_addr?{1'b0,addr}:{addr,rd},ack);
 endtask
 task automatic combined_read(input reg [6:0] addr,input reg [7:0] pointer,output reg [7:0] value);
  bit ack;start();address7(addr,0,ack);write_byte(pointer,ack);if(stop_between)stop();start();address7(addr,1,ack);read_byte(0,value);stop();
 endtask
 task automatic bus_clear(output bit cleared);
  bit sampled;clear_pulses=0;lost=0;timed_out=0;active=1;sda_low=0;scl_low=0;wait_clock();
  while(sda===0&&clear_pulses<(clear_eight_clocks?8:9)&&!timed_out)begin bit_cycle(1,0,sampled);#(HOLD);if(!timed_out)clear_pulses++;end
  cleared=sda===1&&!timed_out;
  if(!no_stop_after_clear)stop();else begin active=0;scl_low=0;sda_low=0;end
 endtask
endmodule
Read included file: i2c_target.sv
`timescale 1ns/1ns
module i2c_target #(parameter [9:0] ADDRESS=10'h050,parameter TEN_BIT=0)
 (inout wire scl,sda,output reg scl_low=0,sda_low=0,output wire high_drive);
 bit ack_not_released=0,ack_wrong_clock=0,stretch_never_released=0,wrong_addr_ack=0,no_stop_detect=0,autoinc_wrap_bug=0,push_pull_target=0,ignores_sr=0;
 // Legal receiver responses, not faults. rx_limit counts post-address bytes,
 // including the pointer byte; -1 is unlimited. A busy address is declined once.
 bit busy_nack=0;int rx_limit=-1,rx_count=0;
 // Optional general-call software reset: UM10204 §§3.1.13–14. This model
 // restores mem[i]=i and ptr=0; ADDRESS is fixed, not a programmable register.
 // Fault switches and bench configuration are not device reset state.
 time stretch_ns=0;bit general_call=1,software_reset=1,general_selected=0,reset_pending=0;int resets=0;
 reg [7:0] mem[0:255];reg [7:0] ptr=0,shift=0,tx=0;
 int state=0,count=0,bytes=0;bit active=0,selected=0,reading=0,need_ptr=1,ack_value=0,ack_active=0,low_addr_next=0,addressed10=0,reading_ack=0;
 // state: 0 receive, 1 receiver ACK, 2 transmit, 3 controller ACK, 4 ignore.
 assign high_drive=push_pull_target&&!sda_low;
 assign sda=sda_low?1'b0:high_drive?1'b1:1'bz;assign scl=scl_low?1'b0:1'bz;
 initial for(int i=0;i<256;i++)mem[i]=i[7:0];
 function automatic [7:0] increment(input reg [7:0] p);increment=autoinc_wrap_bug?((p+1)&8'h0f):p+1'b1;endfunction
 always @(negedge sda)if(scl===1)begin
  if(!(ignores_sr&&active))begin active=1;selected=0;state=0;count=0;bytes=0;rx_count=0;shift=0;reading=0;ack_active=0;sda_low=0;low_addr_next=0;general_selected=0;reset_pending=0;end
 end
 always @(posedge sda)if(scl===1&&active&&!no_stop_detect)begin active=0;selected=0;state=0;count=0;sda_low=0;addressed10=0;end
 always @(posedge scl)if(active)begin
  if(state==0)begin
   shift={shift[6:0],sda};count++;
   if(count==8)begin
    ack_value=0;
    if(bytes==0)begin
      reading=shift[0];
      general_selected=general_call&&shift==0;
      if(general_selected)begin selected=1;low_addr_next=0;end
      else if(TEN_BIT)begin selected=(shift[7:3]==5'b11110&&shift[2:1]==ADDRESS[9:8]&&(!reading||addressed10));low_addr_next=!reading;end
      else selected=(shift[7:1]==ADDRESS[6:0]||(wrong_addr_ack&&shift[7:1]==ADDRESS[6:0]+1'b1));
      if(selected&&busy_nack)begin selected=0;busy_nack=0;end
      ack_value=selected;need_ptr=1;
    end else if(low_addr_next)begin selected=selected&&shift==ADDRESS[7:0];ack_value=selected;addressed10=selected;low_addr_next=0;end
    else if(selected)begin
      ack_value=rx_limit<0||rx_count<rx_limit;
      if(general_selected)begin
       // Unsupported general-call commands are NACKed, never register writes.
       ack_value=ack_value&&bytes==1&&software_reset&&shift==8'h06;
       reset_pending=ack_value;
      end else if(ack_value)begin rx_count++;if(need_ptr)begin ptr=shift;need_ptr=0;end else begin mem[ptr]=shift;ptr=increment(ptr);end end
      if(!ack_value)selected=0;
    end
    bytes++;state=1;ack_active=0;count=0;
   end
  end else if(state==2)begin count++;if(count==8)begin state=3;count=0;reading_ack=0;end end
  else if(state==3&&reading_ack)begin if(sda===0)begin ptr=increment(ptr);tx=mem[ptr];state=2;count=0;end else state=4;end
 end
 always @(negedge scl)if(active)begin
  // Deliberate early-ACK fault: after seven samples, pull for clock 8.
  // The address prefix is already known; its R/W bit has not arrived yet.
  if(state==0&&count==7&&ack_wrong_clock)begin
   if(bytes==0)sda_low=!busy_nack&&((general_call&&shift[6:0]==0)||(TEN_BIT?(shift[6:2]==5'b11110&&shift[1:0]==ADDRESS[9:8]):(shift[6:0]==ADDRESS[6:0]||(wrong_addr_ack&&shift[6:0]==ADDRESS[6:0]+1'b1))));
   else sda_low=selected&&(rx_limit<0||rx_count<rx_limit);
  end else if(state==1)begin
   if(!ack_active)begin
    sda_low=ack_value&&!ack_wrong_clock;ack_active=1;
   end else begin
    sda_low=ack_not_released&&ack_value;ack_active=0;
    if(reset_pending)begin
     // Finish the ninth ACK clock before resetting and releasing the bus.
     for(int i=0;i<256;i++)mem[i]=i[7:0];
     ptr=0;shift=0;tx=0;count=0;bytes=0;rx_count=0;need_ptr=1;
     selected=0;reading=0;reading_ack=0;low_addr_next=0;addressed10=0;
     general_selected=0;reset_pending=0;resets++;sda_low=0;scl_low=0;state=4;
    end else if(!selected)state=4;
    else if(reading)begin state=2;tx=mem[ptr];count=0;sda_low=!tx[7];end
    else state=0;
    // Byte-level stretch after the ACK, as in UM10204 §3.1.9. Prepare
    // outgoing data before releasing SCL; overlap the controller's LOW.
    if(selected&&ack_value&&(stretch_ns>0||stretch_never_released))begin scl_low=1;if(!stretch_never_released)begin #(stretch_ns);scl_low=0;end end
   end
  end else if(state==2)sda_low=!tx[7-count];
  else if(state==3)begin sda_low=0;reading_ack=1;end
  else if(state==4)sda_low=0;
 end
endmodule
Read included file: i2c_checker.sv
`timescale 1ns/1ns
// Logical Fm timing only. Instantaneous digital pins cannot measure RC rise/fall or
// establish which high-SCL SDA edge was intended as data; driver intent is separate.
module i2c_checker #(parameter time TLOW=1300,THIGH=600,TBUF=1300,THD_STA=600,TSU_STA=600,TSU_DAT=100,THD_DAT=0,TSU_STO=600)
 (input wire scl,sda,input wire [2:0] sda_lows,scl_lows,input wire high_drive);
 int broken=0,starts=0,stops=0,pulses=0;bit active=0,seen_rise=0,seen_fall=0,start_pending=0;
 time rise=0,fall=0,changed=0,stop_time=0,start_time=0;
 task automatic bad(input string rule);broken++;$display("I2C CHECK %s at %0t",rule,$time);endtask
 always @(posedge scl)begin
  if(seen_fall&&$time-fall<TLOW)bad("tlow");
  if(active&&$time-changed<TSU_DAT)bad("tsu_dat");rise=$time;seen_rise=1;if(active)pulses++;
 end
 always @(negedge scl)begin
  if(active&&seen_rise&&$time-rise<THIGH)bad("thigh");
  if(start_pending&&$time-start_time<THD_STA)bad("thd_sta");start_pending=0;fall=$time;seen_fall=1;
 end
 always @(sda)begin
  if(scl===1)begin
   if(sda===0)begin
    if(!active&&stops>0&&$time-stop_time<TBUF)bad("tbuf");
    if(active&&$time-rise<TSU_STA)bad("tsu_sta");starts++;active=1;start_pending=1;start_time=$time;
   end else if(sda===1&&active)begin if($time-rise<TSU_STO)bad("tsu_sto");stops++;active=0;stop_time=$time;end
  end else if(seen_fall&&$time-fall<THD_DAT)bad("thd_dat");
  changed=$time;
 end
 always @(high_drive or sda_lows)begin #1;if(high_drive&&|sda_lows)bad("push_pull_contention");end
 // A separate intent-aware bench checks ACK ownership/ninth slot, lost-controller
 // withdrawal, timeouts and accidental START/STOP. Those are not inferable from SDA alone.
endmodule
Read included file: i2c_mon.sv
`timescale 1ns/1ns
module i2c_mon(input wire scl,sda);
 bit active=0,header=1,trace=0;int count=0,frames=0,starts=0,stops=0;reg [7:0] value=0;
 // Assemble the whole S ... Sr ... P transaction before one atomic report.
 // Incomplete/stuck transactions produce no line; counters remain live.
 string text="";
 always @(negedge sda)if(scl===1)begin if(active)text={text," Sr"};else text="I2C S";active=1;header=1;count=0;starts++;end
 always @(posedge sda)if(scl===1&&active)begin if(trace)$display("%s P",text);text="";active=0;count=0;frames++;stops++;end
 always @(posedge scl)if(active)begin
  if(count<8)begin value={value[6:0],sda};count++;end
  else begin
   if(header)text=$sformatf("%s %02h %s",text,value[7:1],value[0]?"R":"W");
   else text=$sformatf("%s %02h",text,value);
   text={text,sda?" NA":" A"};header=0;count=0;
  end
 end
endmodule
// Trusted recovery observer. Driver intent distinguishes the STOP setup edge
// from recovery pulses, including when a stuck target prevents an actual STOP.
module i2c_clear_mon(input wire scl,sda,ctrl_sda_low,enabled,done,timed_out);
 int pulses=0;bit stopped=0;
 always @(posedge enabled)begin pulses=0;stopped=0;end
 always @(posedge scl)if(enabled&&!ctrl_sda_low)pulses++;
 always @(posedge sda)if(enabled&&scl===1)stopped=1;
 always @(posedge done)$display("I2C CLEAR PULSES %0d STOP %0d CLEARED %0d TIMEOUT %0d",pulses,stopped,(sda===1&&!timed_out),timed_out);
endmodule
Read included file: i2c_sva.sv
`timescale 1ns/1ns
module i2c_sva #(parameter integer REPORT_LIMIT=200)(input wire tick,rst_n,enabled,scl,sda,cdl,tdl,c2dl,high_drive,
 input wire [1:0] condition, // 0 data, 1 completed S/Sr, 2 completed P
 input wire ack_slot,target_tx,ack_expected,lost,free_bus,start_seen,
 input wire [3:0] target_count,input wire byte_done,input wire [7:0] byte_value);
 int broken=0;bit previous_scl=0,previous_cdl=0,previous_tdl=0,primed=0;
 bit header=1,need_low_address=0,selected10=0;
 // Limit printed reports per rule per reset epoch; checking and broken keep
 // counting every violation. REPORT_LIMIT=0 is silent, <0 is unlimited.
 int reports[0:9];
 initial for(int r=0;r<10;r++)reports[r]=0;
 task automatic bad(input int rule_id,input string rule);
  broken++;
  if(REPORT_LIMIT<0||reports[rule_id]<REPORT_LIMIT)begin
   $display("I2C INTENT %s at %0t",rule,$time);reports[rule_id]++;
  end
 endtask
 always @(posedge tick)begin
  if(!rst_n)begin for(int r=0;r<10;r++)reports[r]=0;broken=0;primed=0;header=1;need_low_address=0;selected10=0;end
  else if(enabled)begin
   if(primed&&previous_scl&&scl)begin
    assert((previous_tdl==tdl)&&(condition!=0||previous_cdl==cdl))else bad(0,"sda_stable");
   end
   if(condition==1)begin assert(scl&&!sda)else bad(1,"start_shape");end
   if(condition==2)begin assert(scl&&sda)else bad(2,"stop_shape");end
   if(scl&&ack_slot)begin
    assert((target_tx?!tdl:!cdl)&&((target_tx?cdl:tdl)==ack_expected))else bad(3,"ack_owner");
   end
   if(scl&&!ack_slot&&!target_tx&&condition==0)begin
    assert(!tdl)else bad(4,"ack_slot_only");
   end
   if(lost&&!free_bus)begin assert(!cdl)else bad(5,"loser_releases");end
   if(free_bus)begin assert(!cdl&&!tdl&&!high_drive)else bad(6,"no_drive_when_idle");end
   assert(!(high_drive&&(cdl||tdl||c2dl)))else bad(7,"push_pull_contention");
   if(start_seen)begin
    assert(target_count==0)else bad(8,"reset_on_start");
    assert(!need_low_address)else bad(9,"ten_bit_header");
    header=1;need_low_address=0;
   end
   if(byte_done)begin
    if(header)begin
     if(byte_value[7:3]==5'b11110)begin
      assert(!byte_value[0]||selected10)else bad(9,"ten_bit_header");
      need_low_address=!byte_value[0];
     end
     header=0;
    end else if(need_low_address)begin need_low_address=0;selected10=1;end
   end
   if(condition==2)begin header=1;need_low_address=0;selected10=0;end
   previous_scl=scl;previous_cdl=cdl;previous_tdl=tdl;primed=1;
  end
 end
endmodule
Read included file: p8_intent.sv
`timescale 1ns/1ns
// p8_intent.sv: decodes the wire on its own and supplies i2c_sva's intent inputs every 50 ns.
//   condition  1 for an S or Sr, 2 for a P: an SDA edge under a high SCL between bytes. The same edge
//              inside a byte is a data edge that moved at the wrong time, so it stays 0.
//   ack_slot   the ninth SCL high of a byte;   target_tx  data bytes of a read the target accepted
//   ack_expected   the plan: the target accepts ADDRESS and the general call (or, with TEN, the 10-bit address
//              A10 in its two bytes); on reads the controller's own ACK is trusted
//   lost       the controller let SDA go (sent 1), read 0, and nobody else it knows of pulled: another controller
module p8_intent #(parameter [6:0] ADDRESS = 7'h50, parameter TEN = 0, parameter [9:0] A10 = 10'h2aa)
  (input wire scl, sda, cdl, tdl, c2dl, high_drive, input wire [3:0] target_count);
  reg tick = 0, rst_n = 0;
  always #25 tick = ~tick;
  initial #100 rst_n = 1;
  reg [1:0] condition = 0;
  reg start_seen = 0, byte_done = 0, active = 0, header = 1, reading = 0, selected = 0, lost = 0, low_next = 0, addressed10 = 0;
  reg [7:0] shift = 0, byte_value = 0;
  int count = 0;
  time set_at = 0, last_tick = 0;
  wire ack_slot = active && count == 9;
  wire target_tx = active && reading && !header && selected;
  wire hdr10 = shift[7:3] == 5'b11110 && shift[2:1] == A10[9:8];
  wire accepts = !TEN ? (shift[7:1] == ADDRESS || shift == 8'h00) : low_next ? shift == A10[7:0] : hdr10 && (!shift[0] || addressed10);
  wire ack_expected = target_tx ? cdl : (header || low_next) ? accepts : selected;
  wire free_bus = !active;
  always @(negedge sda) if (scl === 1'b1 && count <= 1) begin
    condition = 1; start_seen = 1; set_at = $time; active = 1; header = 1; count = 0; lost = 0; low_next = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active && count <= 1) begin
    condition = 2; set_at = $time; active = 0; count = 0; selected = 0; addressed10 = 0;
  end
  always @(posedge scl) if (active) begin
    count++;
    if (count <= 8) begin
      shift = {shift[6:0], sda};
      if (!target_tx && cdl === 1'b0 && sda === 1'b0 && tdl === 1'b0) lost = 1;
    end else begin byte_done = 1; byte_value = shift; set_at = $time; end
  end
  always @(negedge scl) if (active && count == 9) begin
    if (header) begin reading = shift[0]; selected = accepts; header = 0; low_next = TEN && hdr10 && !shift[0]; end
    else if (low_next) begin selected = accepts; addressed10 = accepts; low_next = 0; end
    count = 0;
  end
  // A flag lives until one sampling edge has seen it.
  always @(posedge tick) last_tick = $time;
  always @(negedge tick) if (set_at < last_tick) begin condition = 0; start_seen = 0; byte_done = 0; end
  i2c_sva ck(tick, rst_n, 1'b1, scl, sda, cdl, tdl, c2dl, high_drive, condition, ack_slot, target_tx, ack_expected,
             lost, free_bus, start_seen, target_count, byte_done, byte_value);
endmodule
Read included file: p8_mon.sv
`timescale 1ns/1ns
// p8_mon.sv: one line per transaction, printed at the STOP, in the library decoder's grammar.
// ten_bit = 1 decodes a 1111 0XX header and its low byte as 10:<address>. report_at_start = 1 is the twin
// that prints at the START, before a single address bit has been sampled.
module p8_mon(input wire scl, sda);
  bit report_at_start = 0, ten_bit = 1;
  string text = "";
  bit active = 0, header = 1, low_next = 0, top_ack = 0;
  int count = 0;
  reg [7:0] value = 0; reg [9:0] addr10 = 0;
  function automatic string ack(input bit level); if (level) return "NA"; return "A"; endfunction
  function automatic string dir(input bit rd); if (rd) return "R"; return "W"; endfunction
  always @(negedge sda) if (scl === 1'b1) begin
    low_next = 0;
    if (report_at_start) $display("I2C S %02h", value[7:1]);
    else if (active) text = {text, " Sr"};
    else text = "I2C S";
    active = 1; header = 1; count = 0;
  end
  always @(posedge sda) if (scl === 1'b1 && active) begin
    if (!report_at_start) $display("%s P", text);
    active = 0; count = 0;
  end
  always @(posedge scl) if (active) begin
    if (count < 8) begin value = {value[6:0], sda}; count++; end
    else begin
      if (header && ten_bit && value[7:3] == 5'b11110 && !value[0]) begin addr10[9:8] = value[2:1]; low_next = 1; top_ack = !sda; end
      else if (header && ten_bit && value[7:3] == 5'b11110) text = $sformatf("%s 10:%03h R %s", text, addr10, ack(sda));
      else if (low_next) begin addr10[7:0] = value; low_next = 0; text = $sformatf("%s 10:%03h W %s", text, addr10, ack(sda || !top_ack)); end
      else if (header) text = $sformatf("%s %02h %s %s", text, value[7:1], dir(value[0]), ack(sda));
      else text = $sformatf("%s %02h %s", text, value, ack(sda));
      header = 0; count = 0;
    end
  end
endmodule
ConceptReal silicon has no ctrl_low or tgt_low pins. Why bind a checker to them?
ConceptWhen should an I2C monitor publish a transaction?

Remember

  • A monitor samples data at SCL rises, detects S and P from SDA edges under a high SCL, and reports at Sr or P.
  • Some rules need only the wires; ACK ownership, overstaying ACKs and losers that keep driving need the driver view.
  • The intent harness decodes the wire itself and feeds i2c_sva; legal stretches, STOPs and 10-bit reads stay silent.
  • Each fault twin trips the rule written for it, and the wire checker misses all but the glitch.
  • A UVM driver on I2C drives only the low lanes; one agent serves as controller or target.

Practice