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.
| Run | Intent rules that fired (up to 200 reports per rule) | Totals |
|---|---|---|
| none (clean) | none | intent broken 0, ck.broken 0 |
| target stretches 3 µs (legal) | none | intent broken 0, ck.broken 0 |
| stop_between (legal on one controller) | none | intent broken 0, ck.broken 0 |
| ack_not_released | ack_slot_only | intent broken 24, ck.broken 0 |
| ack_wrong_clock | ack_slot_only, ack_owner | intent broken 122, ck.broken 0 |
| push_pull_target | no_drive_when_idle, sda_stable, push_pull_contention | intent broken 2785, ck.broken 9 |
| ctrl_ignores_sda (ctrl 0x51 against 0x50) | loser_releases | intent broken 141, ck.broken 0 |
| data_change_scl_high | no_drive_when_idle, sda_stable | intent broken 244, ck.broken 21 |
| ignores_sr | reset_on_start, ack_slot_only | intent broken 25, ck.broken 0 |
| 10-bit read (legal) | none | intent broken 0, ck.broken 0 |
| 10-bit header, then Sr at once | ten_bit_header | intent 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).
- 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.
- 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).
- 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.
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 3cThe 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 alone | Needs the driver view (ctrl_low, tgt_low) |
|---|---|
| START and STOP shapes; Sr | which device pulled the ACK low |
| SDA steady while SCL is high | an ACK that overstays into the next byte |
| nine clocks per byte, ACK or NACK | a target that pulls in the wrong clock |
| the Table 11 edge times (P8.7) | a controller that keeps driving after it lost |
| tBUF between transactions | a 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
- Never drive a 1
The interface resolves
tri1 scl, sdafrom every agent'sscl_lowandsda_low. A driver that assignedsda = 1would be a push-pull output. - 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). - 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.
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=0The 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
`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
`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
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 0Read 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
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
- Decode Seven-Bit Ten-Bit and General-Call Traffic Implement · Hard