Capstone: I2C Bug Hunts
Match each I2C fault to the stimulus and the checker that expose it, read two measured fault matrices, and explain why a green cell is not proof and why a checker must infer state from the wire.
- Lesson
- P8.11
- Time
- 45 min
- Runs on
- Your browser
- Check
- 4 questions
A register target passed its vendor's suite: writes, reads and increments at one address, with a STOP between every step. In the product the host read across the 0xFF wrap with repeated STARTs while a second controller shared the bus. The target wrapped at 0x0F and treated the Sr as data, bugs that STOP-only, one-controller tests can never reach.
| Fault | T1: write, then read back | T2: read across 0x0F and 0xFF | T3: combined read with Sr | T4: STOP mid-byte, then a new read | T5: general call | T6: wrong address | T7: stretched read | T8: two controllers |
|---|---|---|---|---|---|---|---|---|
| none (clean) | – | – | – | – | – | – | – | – |
wrong_addr_ack | – | – | – | – | – | data, wire, driver view | – | – |
stretch_never_released | data, timeout | data, timeout | data, timeout | data, timeout | data, timeout | – | data, timeout | data, timeout |
no_stop_detect | – | – | – | – | – | – | – | – |
autoinc_wrap_bug | – | data | – | – | – | – | – | – |
ack_wrong_clock | data, driver view | data, driver view | data, driver view | data | data, driver view | – | data, driver view | data, driver view |
ignores_sr | data, driver view | data, driver view | data, driver view | – | – | – | data, driver view | data, driver view |
ack_not_released | data, driver view | data, driver view | data, driver view | data | data, driver view | – | data, driver view | data, driver view |
push_pull_target | data, wire, driver view | data, wire, driver view | data, wire, driver view | data, wire, driver view | data, wire, driver view | wire, driver view | data, wire, driver view | data, wire, driver view |
Measured: one Icarus run per cell. A cell names the eyes that saw the fault. Data is the program's own check; wire means more Fast-mode checker lines than the clean run; driver view means more intent violations; timeout is the controller giving up. A dash means this program let the fault through. T4 aborts a byte with a STOP, which the intent checker reports even on a correct target, so only extra violations count there.
The Target Matrix
Eight faults, eight programs. A wrong-address ACK needs a wrong address. A pointer that wraps early needs a read across 0x0F. A target that ignores Sr needs a combined read, and contention needs the driver view or the wire checker.
autoinc_wrap_bug, T2:
read 0f 00, then ff 00
RESULT data WRONG wire 16 intent 0 timeout 0
ignores_sr, T3:
read ff
RESULT data WRONG wire 2 intent 25 timeout 0One row stays green: no_stop_detect. Every new transaction starts with START, and every device resets on START (§3.1.10 note 4), so a target that misses STOP behaves correctly here. A green row says only that these programs did not provoke the fault.
The Controller Matrix
| Fault | C1: two register reads | C2: stretching target | C3: two controllers, 0x50 against 0x51 | C4: two controllers, identical | C5: stuck bus, needs 9 clocks |
|---|---|---|---|---|---|
| none (clean) | – | – | – | – | – |
eight_bit_addr | data | data | data | data | – |
ctrl_ignores_scl | – | data, driver view | data, driver view | data, driver view | – |
ctrl_ignores_sda | – | – | data | – | – |
data_change_scl_high | data, wire, driver view | data, wire, driver view | data, wire, driver view | data, wire, driver view | data, driver view |
clear_eight_clocks | – | – | – | – | data, driver view |
hold_zero_early | wire | wire | wire | wire | – |
short_tbuf | wire | – | – | – | – |
loser_keeps_clocking_past_byte | – | – | data | – | – |
loser_no_target_mode | – | – | data | – | – |
Measured likewise. In the two-controller programs both controllers carry the fault, as two copies of one design would. The wire checker runs with a 50 ns hold minimum, so hold_zero_early can be seen.
An 8-bit address fails every program, because nothing answers. A controller that ignores SCL needs a stretching target, and the loser faults need a second controller. The glitch, the hold and tBUF faults need the wire checker; the short bus clear needs a stuck bus.
Four Eyes
- Results and shapes
A data check catches what changes the answer; the wire checker catches Table 11 and the START/STOP shapes.
- Drivers and rivals
Only the driver view says who pulled; only a second controller makes arbitration faults happen at all.
Read the diagram as text
- Wrong result: wrap, Sr ignored, 8-bit address
- Bad timing or shape: glitch, hold, tBUF
- Wrong driver: ACK owner, overstay, push-pull
- Arbitration: losers that misbehave
- Data check: expected value from the wire
- Wire checker: Table 11 edges
- Driver-view checker: i2c_sva + intent
- Two-controller bench: a second controller
- Wrong result → Data check
- Bad timing or shape → Wire checker
- Wrong driver → Driver-view checker
- Arbitration → Two-controller bench
A capstone checker that uses one eye is a weak set; the table below says what each one alone misses.
| Stimulus or checker set | Faults it misses (both banks) |
|---|---|
| Write and read back only (T1, C1) | wrong_addr_ack, no_stop_detect, autoinc_wrap_bug, ctrl_ignores_scl, ctrl_ignores_sda, clear_eight_clocks, loser_keeps_clocking_past_byte, loser_no_target_mode |
| One controller only (no T8, C3, C4) | no_stop_detect, ctrl_ignores_sda, loser_keeps_clocking_past_byte, loser_no_target_mode |
| Data checks only, every program | no_stop_detect, hold_zero_early, short_tbuf |
| Wire checker only, every program | stretch_never_released, no_stop_detect, autoinc_wrap_bug, ack_wrong_clock, ignores_sr, ack_not_released, eight_bit_addr, ctrl_ignores_scl, ctrl_ignores_sda, clear_eight_clocks, loser_keeps_clocking_past_byte, loser_no_target_mode |
Computed from the two matrices. No program here catches no_stop_detect.
The Bench Rule
The checker reads the bus and the driver-view lanes, never the target's internals. The pointer is inferred from the wire: the byte after the address of a write is the pointer, each data byte advances it, and STOP or Sr leaves it alone. With the target holding mem[i] = i, the expected value of every read follows from the wire alone.
A checker that read target.ptr would agree with the target's own belief about itself, and a wrong target would pass. The problems for this lesson reject any submission that reaches into the device.
Try It
The program writes pointer 0x0F, reads two bytes with Sr, then does the same from 0xFF. The target holds its own index, so the wire says what each byte must be. The RESULT line reports all four eyes.
Change target.autoinc_wrap_bug to any other fault from the bank, or switch T2 for another program from the matrix, and compare with its cell.
I2C CHECK thd_dat at 25890 I2C CHECK thd_dat at 45970 I2C CHECK thd_dat at 48480 I2C CHECK thd_dat at 71170 I2C CHECK thd_dat at 83720 I2C CHECK thd_dat at 96270 I2C CHECK thd_dat at 96270 I2C CHECK thd_dat at 116350 I2C CHECK thd_dat at 146060 I2C CHECK thd_dat at 166140 I2C CHECK thd_dat at 168650 I2C CHECK thd_dat at 191340 I2C CHECK thd_dat at 193850 I2C CHECK thd_dat at 216440 I2C CHECK thd_dat at 216440 I2C CHECK thd_dat at 236520 read 0f 00, then ff 00 RESULT data WRONG wire 16 intent 0 timeout 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
- Each fault needs a specific program: a wrong address, a wrap, a repeated START, a stretch, a second controller, a stuck bus.
- Four eyes cover the banks: data checks, the wire checker, the driver-view checker and the two-controller bench.
- A green cell or row shows only what the programs did not provoke.
- The checker infers the target's state from the wire and never reads its internals.
- Legal variations, a stretch or a STOP between pointer and read on one controller, must stay green.
Practice
- Verify the Target Across Its Transaction Boundaries Verify · Hard
- A Small Program for Controller and Target Faults Break it · Hard
- Locate the Misread Most-Significant Bit Debug · Hard
- Keep a Combined Read on the Busy Bus Fix · Medium
- Shrink the Bit Controller Without Losing Stretch State Hard