Ultra Fast-Mode: The Write-Only Bus

Explain what UFm keeps from I2C and what it drops, run a 5 MHz write with its checkers, and say why any target that drives, or any second controller, breaks a push-pull bus.

Lesson
P8.12
Time
35 min
Runs on
Your browser
Check
4 questions

A display board used UFm for its LED drivers: 7421 ns per four-byte update, 5.1 times faster than Fm+. Then firmware wanted to read a driver's fault flag. UFm cannot read: its targets never drive a line, so the flag needed its own pin.

A UFm BusOne controller drives both lines; every arrow points away from it.
A UFm busOne controller drives both lines; every arrow points away from it. component Controller ASIC push-pull USCL,USDA queue One-hot mux only if a secondcontroller exists component LED controller 1 receives only component LCD driver receives only component GPO receives only USDA, USCL
  1. One talker

    The controller addresses a target, sends data and stops. Targets never answer.

  2. A second controller

    Two push-pull outputs on one wire would fight, so a second controller must be fully cut off by a one-hot mux.

Read the diagram as text
  • Controller ASIC: push-pull USCL, USDA
  • One-hot mux: only if a second controller exists
  • LED controller 1: receives only
  • LCD driver: receives only
  • GPO: receives only
  • Controller ASIC → LED controller 1: USDA, USCL
  • Controller ASIC → LCD driver
  • Controller ASIC → GPO
  • One-hot mux → Controller ASIC

After UM10204 Fig. 21 and §3.2. When the bus is free the controller's upper transistors hold both lines HIGH (§3.2.1); there are no pull-up resistors.

What UFm Keeps and Drops

UFm is a push-pull bus from DC to 5 MHz that carries data one way (§3.2). It keeps the frame: START, address, eight data bits, a ninth clock, STOP. It drops everything that needs a second driver on a wire (§3.2.1, §5.4).

FeatureSm, Fm, Fm+ (Table 3)UFm (Table 7)
START, STOP, 7-bit addressmandatorymandatory
Acknowledgemandatorynot possible
Synchronisation, arbitrationmandatory with several controllersnot possible
Clock stretchingoptionalnot possible
10-bit address, general call, software resetoptionaloptional
START byteoptional with several controllersoptional
Device IDoptional in a targetnot possible

UM10204 Table 3 and Table 7. UFm has one configuration, a single controller; everything that needs a target or a second controller to drive a line is gone.

UFm devices are not compatible with the bidirectional buses (§5.4), so a UFm segment holds only UFm devices.

The Frame on the Wire

Data is stable while USCL is HIGH and changes while it is LOW (§3.2.3). START and STOP are the same edges as on any I2C bus (§3.2.4). The controller drives USDA HIGH on every ninth clock, so each byte ends in a NACK (§3.2.5, §3.2.6).

START, the Address Byte and the Ninth ClockMeasured on the UFm bench, 25 ns per column: USCL, USDA and who drives them.
0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081USCLUSDActrl drivestgt drivesS9th
  1. Cycle 1

    START: USDA falls under a high USCL, as on every I2C bus.

  2. Cycle 32

    The address 0x50 and W go out MSB first, one bit per 200 ns clock.

  3. Cycle 72

    The ninth clock: the controller itself drives USDA HIGH. The target never drives.

Measured. The decoder logs I2C S 50 W NA 10 NA 5a NA a5 NA P: every ninth bit reads as NA, because it is a HIGH the controller drives.

ParameterTable 15Bench (shortest measured)
fUSCL0 to 5000 kHzperiod 200 ns (5000 kHz)
tHD;STA50 ns min60 ns
tLOW50 ns min100 ns
tHIGH50 ns min100 ns
tHD;DAT10 ns min20 ns
tSU;DAT30 ns min80 ns
tSU;STO50 ns min60 ns
tBUF, tSU;STA80 ns, 50 ns mincontroller settings 100 ns and 60 ns
tr, tf50 ns max (typical 25 ns)not measurable on digital pins

UM10204 Table 15. Measured from the write above, whose checkers are clean. The write takes 7421 ns; the same bytes on the Fm+ bench take 37751 ns. tVD;DAT in Table 15 is a minimum of 10 ns, not a maximum.

Addresses and Special Bytes

The R/W bit must be W: a read is not supported, except as the low bit of the START byte (§3.2.7). 10-bit addresses work in the write format only (§3.2.8). General call, software reset and the START byte keep their F/S meaning; no device may acknowledge the START byte (§3.2.10 to §3.2.12).

AddressR/WSm, Fm, Fm+ (Table 4)UFm (Table 8)
0000 0000general callgeneral call
0000 0001START byteSTART byte
0000 001XCBUS addressreserved for future
0000 010Xdifferent bus formatreserved for future
0000 011Xreserved for futurereserved for future
0000 1XXXHs-mode controller codereserved for future
1111 1XX1 (Table 4), X (Table 8)device IDreserved for future
1111 0XXX10-bit addressing10-bit addressing (write only)

UM10204 Table 4 and Table 8 (§3.1.12, §3.2.9). UFm has a different physical layer, so its address range differs: the CBUS, other-format, Hs-code and device-ID entries become reserved for future use. As on the other buses, a reserved address may serve as a target address if it will never be used for its purpose.

TransferDecoderTarget
Address 0x50 with R, then a byteI2C S 50 R NA 10 NA Preads_refused 1, led1 ptr 00, mem[10] 10
START byte, Sr, then a writeI2C S 00 R NA Sr 50 W NA 20 NA 77 NA Pled1 mem[20] 77
Two targets in one transfer, joined by SrI2C S 50 W NA 00 NA 11 NA Sr 51 W NA 00 NA 22 NA Pled1 mem[0] 11, led2 mem[0] 22
General call, then 06hI2C S 00 W NA 06 NA P06h: resets 1 and 1, led1 mem[06] 06, ptr 00
General call, then 04h, 00h and 07hthree transfers like the one above04h: 1, ignored: 2, resets 1

Measured on the bench targets at 0x50 and 0x51. The code 06h resets both and restores their registers (§3.2.11). The code 04h writes the programmable address part without a reset. The code 00h is not allowed and 07h has B = 1, so both are ignored (§3.2.10).

Who May Drive

Only the controller drives, and only one controller may be connected at a time (§3.2). On open drain, a target pulling low is an ACK or a stretch. On push-pull it fights the controller's HIGH, so the line goes to x.

The drives_ack Twin: A Target Tries to ACKMeasured, 20 ns per column, around the ninth clock of the address byte.
012345678910111213141516171819202122232425262728293031323334USCLUSDActrl drivestgt drivestarget drives
  1. Cycle 15

    USCL falls after the eighth bit and the target switches its driver on, as an open-drain target would for an ACK.

  2. Cycle 16

    The controller drives HIGH for the ninth bit and the target drives LOW: the line resolves to x.

Measured. On a wired-AND bus this would be an ACK; on push-pull it is a short between two outputs. Checker lines: thd_dat ×5, target_drives_usda ×4, contention_usda ×4, ninth_bit ×4.

SwitchChecker linesRule broken
led1.drives_ack = 1thd_dat ×5, target_drives_usda ×4, contention_usda ×4, ninth_bit ×4targets never drive USDA (§3.2.6)
led1.stretches = 1target_drives_uscl ×4, contention_uscl ×4a target may not hold USCL LOW (§3.2, §3.2.5)
other.enabled = 1contention_usda ×13, contention_uscl ×37one controller at a time, behind a one-hot mux (§3.2)
ctrl.ninth_low = 1ninth_bit ×4the ninth bit is always HIGH (§3.2.6)
ctrl.fast_uscl = 1 (40 ns phases)tlow ×36, tsu_dat ×23, thigh ×36, fuscl ×36fUSCL, tLOW, tHIGH, tSU;DAT (Table 15)
ctrl.short_hold = 1thd_dat ×23tHD;DAT 10 ns min (Table 15)

Measured over the same write. With the second controller enabled the target reads led1 mem[10] 10, mem[11] 11: its registers were never written. With the ninth bit low the decoder logs I2C S 50 W A 10 A 5a A a5 A P, an ACK nobody sent.

Resets, IDs and Electrics

§3.2 has no bus-clear procedure: a target cannot hold a line it never drives. If a target stops responding, found by some means other than the bus, reset it with the software reset or its reset pin, or cycle power for its power-on reset (§3.2.13). The Device ID needs a read, so UFm has none (§3.2.14).

SymbolTable 14 (UFm)Compare Sm, Fm, Fm+ (Table 10)
VIL, VIH0.3 VDD, 0.7 VDDthe same
Vhys0.05 VDD minthe same for Fm and Fm+; no Sm requirement
VOL0.4 V at 4 mA sink0.4 V at 3 mA (20 mA for Fm+)
VOHVDD − 0.4 V at 4 mA sourcenone: open drain cannot drive HIGH
Leakage±1 µA at 3.6 V, ±10 µA at 5.5 V±10 µA input current
Ci10 pF max10 pF max
tSP10 ns max50 ns for Fm and Fm+; no Sm requirement

UM10204 Table 14 and Table 10, VDD > 2 V for VOL and VOH. The VOH row is the whole difference in one line: a UFm output drives both levels.

Try It

The controller writes register pointer 0x10 and two data bytes to LED controller 1. ck is the shared edge-time checker set to the Table 15 minima; uck watches for contention, target drivers, the ninth bit and fUSCL.

Set led1.drives_ack = 1; before the write and read the checker lines: the bus goes to x on every ninth clock.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: I2C S 50 W NA 10 NA 5a NA a5 NA P 7421 ns led1 mem[10] 5a, mem[11] a5 | ck.broken 0, uck.broken 0
Read included file: i2c_ufm_ctrl.sv
`timescale 1ns/1ns
// UFm controller (§3.2.1): push-pull on both lines, write only. While enabled it always drives USCL and
// USDA, high when the bus is free. enabled=0 models the isolated side of a one-hot mux (§3.2).
module i2c_ufm_ctrl(output wire uscl,usda,output wire drives);
 parameter time TLOW=100,THIGH=100,HOLD=20,TBUF=100,THD_STA=60,TSU_STA=60,TSU_STO=60;
 bit ninth_low=0,fast_uscl=0,short_hold=0;     // fault switches
 bit enabled=1,active=0;reg scl_v=1,sda_v=1;time stopped=0;int bytes=0;
 assign drives=enabled;assign uscl=enabled?scl_v:1'bz;assign usda=enabled?sda_v:1'bz;
 task automatic start();
  if(!active)begin if($time-stopped<TBUF)#(TBUF-($time-stopped));end
  else begin scl_v=0;#(HOLD);sda_v=1;#(TLOW-HOLD);scl_v=1;#(TSU_STA);end   // Sr: USDA high, USCL high, setup
  sda_v=0;#(THD_STA);scl_v=0;active=1;
 endtask
 task automatic bit_out(input bit v);time h,l,t;
  h=short_hold?0:HOLD;l=fast_uscl?40:TLOW;t=fast_uscl?40:THIGH;
  scl_v=0;#(h);sda_v=v;#(l-h);scl_v=1;#(t);scl_v=0;
 endtask
 // Eight data bits, then the ninth clock with USDA driven HIGH by the controller (§3.2.5, §3.2.6).
 task automatic write_byte(input reg [7:0] value);
  for(int b=7;b>=0;b--)bit_out(value[b]);bit_out(!ninth_low);bytes++;
 endtask
 task automatic address7(input reg [6:0] addr,input bit rd);write_byte({addr,rd});endtask
 task automatic stop();
  scl_v=0;#(HOLD);sda_v=0;#(TLOW-HOLD);scl_v=1;#(TSU_STO);sda_v=1;stopped=$time;active=0;#1;
 endtask
endmodule
Read included file: i2c_ufm_target.sv
`timescale 1ns/1ns
// UFm target (§3.2): a receiver only, such as an LED driver. It never drives USDA or USCL; the fault
// switches make it try (an ACK, a stretch), which collides with the controller's push-pull outputs.
// Table 8: 0000 000+W is the general call (optional, §3.2.10), 0000 000+R the START byte (ignored), the
// rest of 0000 XXX and 1111 XXX are not this target's address. A READ header is not supported (§3.2.7).
module i2c_ufm_target #(parameter [6:0] ADDRESS=7'h50)(inout wire uscl,usda,output reg sda_en=0,scl_en=0);
 bit drives_ack=0,stretches=0;                 // fault switches
 bit general_call=1,software_reset=1;           // optional features (Table 7)
 reg [7:0] mem[0:255];reg [7:0] ptr=0,shift=0;int resets=0,prog_writes=0,ignored=0,reads_refused=0,count=0,bytes=0;
 bit active=0,selected=0,need_ptr=1,gc=0,ninth_seen=0;
 assign usda=sda_en?1'b0:1'bz;assign uscl=scl_en?1'b0:1'bz;
 initial for(int i=0;i<256;i++)mem[i]=i[7:0];
 always @(negedge usda)if(uscl===1)begin active=1;selected=0;gc=0;count=0;bytes=0;need_ptr=1;end   // S or Sr
 always @(posedge usda)if(uscl===1&&active)begin active=0;selected=0;gc=0;count=0;end              // P
 always @(posedge uscl)if(active)begin
  if(count<8)begin shift={shift[6:0],usda};count++;
   if(count==8)begin
    if(bytes==0)begin
     if(shift[7:1]==ADDRESS)begin selected=!shift[0];if(shift[0])reads_refused++;end
     else gc=general_call&&shift==8'h00;
    end else if(gc&&bytes==1)begin
     if(shift==8'h06&&software_reset)begin for(int i=0;i<256;i++)mem[i]=i[7:0];ptr=0;resets++;end   // §3.2.11
     else if(shift==8'h04)prog_writes++;                                                             // §3.2.10
     else ignored++;                                     // 00h is not allowed; other codes and B = 1 are ignored
     gc=0;
    end else if(selected)begin if(need_ptr)begin ptr=shift;need_ptr=0;end else begin mem[ptr]=shift;ptr=ptr+1'b1;end end
    bytes++;
   end
  end else begin count=0;ninth_seen=usda;end    // the ninth clock: the controller drives USDA HIGH
 end
 always @(negedge uscl)if(active)begin
  if(count==8&&drives_ack&&selected)sda_en=1;   // fault: tries to ACK
  else sda_en=0;
  if(count==0&&bytes>0&&selected&&stretches&&!scl_en)begin scl_en=1;#300;scl_en=0;end   // fault: tries to stretch
 end
endmodule
Read included file: i2c_ufm_checker.sv
`timescale 1ns/1ns
// What a push-pull bus adds to the shared i2c_checker (which measures the Table 15 edge times):
// contention (a line at x: two drivers disagree), a target driver switched on, a ninth bit that is not
// HIGH (§3.2.6) and a USCL period under 200 ns (fUSCL 5000 kHz, Table 15).
module i2c_ufm_checker(input wire uscl,usda,input wire [1:0] target_drives);
 int broken=0,pulses=0;bit active=0,x_sda=0,x_scl=0;time rise=0;
 task automatic bad(input string rule);broken++;$display("UFM CHECK %s at %0t",rule,$time);endtask
 always @(usda)begin if(usda===1'bx&&!x_sda)bad("contention_usda");x_sda=usda===1'bx;
  if(uscl===1&&usda===0)begin active=1;pulses=0;end else if(uscl===1&&usda===1)active=0;end
 always @(uscl)begin if(uscl===1'bx&&!x_scl)bad("contention_uscl");x_scl=uscl===1'bx;end
 always @(posedge target_drives[0])bad("target_drives_usda");
 always @(posedge target_drives[1])bad("target_drives_uscl");
 always @(posedge uscl)begin
  if(rise&&$time-rise<200&&active)bad("fuscl");rise=$time;
  if(active)begin pulses++;if(pulses%9==0&&usda!==1'b1)bad("ninth_bit");end
 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

Check Yourself

Predict the outputBench defaults. The controller writes two bytes to LED controller 2: ctrl.start(); ctrl.address7(7'h51, 0); ctrl.write_byte(8'h02); ctrl.write_byte(8'hc3); ctrl.stop();. The decoder prints one line. What is it?
`timescale 1ns/1ns
module tb;
  wire uscl, usda;                        // push-pull lines: no pull-up, the controller drives both levels
  wire c_drives, c2_drives;               // 1 while that controller's outputs are enabled
  wire t_sda_en, t_scl_en, t2_sda_en, t2_scl_en;   // a target driver switched on: only fault switches do that
  i2c_ufm_ctrl   ctrl(uscl, usda, c_drives);                 // controller: write only, 5 MHz
  assign c2_drives = 0;                   // no second controller
  i2c_ufm_target #(.ADDRESS(7'h50)) led1(uscl, usda, t_sda_en, t_scl_en);     // LED controller 1
  i2c_ufm_target #(.ADDRESS(7'h51)) led2(uscl, usda, t2_sda_en, t2_scl_en);   // LED controller 2
  i2c_checker    #(.TLOW(50), .THIGH(50), .TBUF(80), .THD_STA(50), .TSU_STA(50), .TSU_DAT(30), .THD_DAT(10), .TSU_STO(50))
                 ck(uscl, usda, 3'b000, 3'b000, 1'b0);       // Table 15 edge times: prints I2C CHECK <rule>
  i2c_ufm_checker uck(uscl, usda, {t_scl_en | t2_scl_en, t_sda_en | t2_sda_en});   // prints UFM CHECK <rule>
  i2c_mon        mon(uscl, usda);                             // prints I2C S 50 W NA ... P
  initial begin
    mon.trace = 1;
    #200;
    ctrl.start(); ctrl.address7(7'h51, 0); ctrl.write_byte(8'h02); ctrl.write_byte(8'hc3); ctrl.stop();
    #200;
    $finish;
  end
  initial begin #1000000; $fatal(1, "UFm watchdog"); end
endmodule
Read included file: i2c_ufm_ctrl.sv
`timescale 1ns/1ns
// UFm controller (§3.2.1): push-pull on both lines, write only. While enabled it always drives USCL and
// USDA, high when the bus is free. enabled=0 models the isolated side of a one-hot mux (§3.2).
module i2c_ufm_ctrl(output wire uscl,usda,output wire drives);
 parameter time TLOW=100,THIGH=100,HOLD=20,TBUF=100,THD_STA=60,TSU_STA=60,TSU_STO=60;
 bit ninth_low=0,fast_uscl=0,short_hold=0;     // fault switches
 bit enabled=1,active=0;reg scl_v=1,sda_v=1;time stopped=0;int bytes=0;
 assign drives=enabled;assign uscl=enabled?scl_v:1'bz;assign usda=enabled?sda_v:1'bz;
 task automatic start();
  if(!active)begin if($time-stopped<TBUF)#(TBUF-($time-stopped));end
  else begin scl_v=0;#(HOLD);sda_v=1;#(TLOW-HOLD);scl_v=1;#(TSU_STA);end   // Sr: USDA high, USCL high, setup
  sda_v=0;#(THD_STA);scl_v=0;active=1;
 endtask
 task automatic bit_out(input bit v);time h,l,t;
  h=short_hold?0:HOLD;l=fast_uscl?40:TLOW;t=fast_uscl?40:THIGH;
  scl_v=0;#(h);sda_v=v;#(l-h);scl_v=1;#(t);scl_v=0;
 endtask
 // Eight data bits, then the ninth clock with USDA driven HIGH by the controller (§3.2.5, §3.2.6).
 task automatic write_byte(input reg [7:0] value);
  for(int b=7;b>=0;b--)bit_out(value[b]);bit_out(!ninth_low);bytes++;
 endtask
 task automatic address7(input reg [6:0] addr,input bit rd);write_byte({addr,rd});endtask
 task automatic stop();
  scl_v=0;#(HOLD);sda_v=0;#(TLOW-HOLD);scl_v=1;#(TSU_STO);sda_v=1;stopped=$time;active=0;#1;
 endtask
endmodule
Read included file: i2c_ufm_target.sv
`timescale 1ns/1ns
// UFm target (§3.2): a receiver only, such as an LED driver. It never drives USDA or USCL; the fault
// switches make it try (an ACK, a stretch), which collides with the controller's push-pull outputs.
// Table 8: 0000 000+W is the general call (optional, §3.2.10), 0000 000+R the START byte (ignored), the
// rest of 0000 XXX and 1111 XXX are not this target's address. A READ header is not supported (§3.2.7).
module i2c_ufm_target #(parameter [6:0] ADDRESS=7'h50)(inout wire uscl,usda,output reg sda_en=0,scl_en=0);
 bit drives_ack=0,stretches=0;                 // fault switches
 bit general_call=1,software_reset=1;           // optional features (Table 7)
 reg [7:0] mem[0:255];reg [7:0] ptr=0,shift=0;int resets=0,prog_writes=0,ignored=0,reads_refused=0,count=0,bytes=0;
 bit active=0,selected=0,need_ptr=1,gc=0,ninth_seen=0;
 assign usda=sda_en?1'b0:1'bz;assign uscl=scl_en?1'b0:1'bz;
 initial for(int i=0;i<256;i++)mem[i]=i[7:0];
 always @(negedge usda)if(uscl===1)begin active=1;selected=0;gc=0;count=0;bytes=0;need_ptr=1;end   // S or Sr
 always @(posedge usda)if(uscl===1&&active)begin active=0;selected=0;gc=0;count=0;end              // P
 always @(posedge uscl)if(active)begin
  if(count<8)begin shift={shift[6:0],usda};count++;
   if(count==8)begin
    if(bytes==0)begin
     if(shift[7:1]==ADDRESS)begin selected=!shift[0];if(shift[0])reads_refused++;end
     else gc=general_call&&shift==8'h00;
    end else if(gc&&bytes==1)begin
     if(shift==8'h06&&software_reset)begin for(int i=0;i<256;i++)mem[i]=i[7:0];ptr=0;resets++;end   // §3.2.11
     else if(shift==8'h04)prog_writes++;                                                             // §3.2.10
     else ignored++;                                     // 00h is not allowed; other codes and B = 1 are ignored
     gc=0;
    end else if(selected)begin if(need_ptr)begin ptr=shift;need_ptr=0;end else begin mem[ptr]=shift;ptr=ptr+1'b1;end end
    bytes++;
   end
  end else begin count=0;ninth_seen=usda;end    // the ninth clock: the controller drives USDA HIGH
 end
 always @(negedge uscl)if(active)begin
  if(count==8&&drives_ack&&selected)sda_en=1;   // fault: tries to ACK
  else sda_en=0;
  if(count==0&&bytes>0&&selected&&stretches&&!scl_en)begin scl_en=1;#300;scl_en=0;end   // fault: tries to stretch
 end
endmodule
Read included file: i2c_ufm_checker.sv
`timescale 1ns/1ns
// What a push-pull bus adds to the shared i2c_checker (which measures the Table 15 edge times):
// contention (a line at x: two drivers disagree), a target driver switched on, a ninth bit that is not
// HIGH (§3.2.6) and a USCL period under 200 ns (fUSCL 5000 kHz, Table 15).
module i2c_ufm_checker(input wire uscl,usda,input wire [1:0] target_drives);
 int broken=0,pulses=0;bit active=0,x_sda=0,x_scl=0;time rise=0;
 task automatic bad(input string rule);broken++;$display("UFM CHECK %s at %0t",rule,$time);endtask
 always @(usda)begin if(usda===1'bx&&!x_sda)bad("contention_usda");x_sda=usda===1'bx;
  if(uscl===1&&usda===0)begin active=1;pulses=0;end else if(uscl===1&&usda===1)active=0;end
 always @(uscl)begin if(uscl===1'bx&&!x_scl)bad("contention_uscl");x_scl=uscl===1'bx;end
 always @(posedge target_drives[0])bad("target_drives_usda");
 always @(posedge target_drives[1])bad("target_drives_uscl");
 always @(posedge uscl)begin
  if(rise&&$time-rise<200&&active)bad("fuscl");rise=$time;
  if(active)begin pulses++;if(pulses%9==0&&usda!==1'b1)bad("ninth_bit");end
 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
Predict the outputBench defaults: the targets start with mem[i] = i and take the first data byte as a register pointer. The controller sends address 0x50 with R, then 0x10 and 0x99: ctrl.start(); ctrl.address7(7'h50, 1); ctrl.write_byte(8'h10); ctrl.write_byte(8'h99); ctrl.stop();. The program prints led1.mem[16] in hex. What does it print?
`timescale 1ns/1ns
module tb;
  wire uscl, usda;                        // push-pull lines: no pull-up, the controller drives both levels
  wire c_drives, c2_drives;               // 1 while that controller's outputs are enabled
  wire t_sda_en, t_scl_en, t2_sda_en, t2_scl_en;   // a target driver switched on: only fault switches do that
  i2c_ufm_ctrl   ctrl(uscl, usda, c_drives);                 // controller: write only, 5 MHz
  assign c2_drives = 0;                   // no second controller
  i2c_ufm_target #(.ADDRESS(7'h50)) led1(uscl, usda, t_sda_en, t_scl_en);     // LED controller 1
  i2c_ufm_target #(.ADDRESS(7'h51)) led2(uscl, usda, t2_sda_en, t2_scl_en);   // LED controller 2
  i2c_checker    #(.TLOW(50), .THIGH(50), .TBUF(80), .THD_STA(50), .TSU_STA(50), .TSU_DAT(30), .THD_DAT(10), .TSU_STO(50))
                 ck(uscl, usda, 3'b000, 3'b000, 1'b0);       // Table 15 edge times: prints I2C CHECK <rule>
  i2c_ufm_checker uck(uscl, usda, {t_scl_en | t2_scl_en, t_sda_en | t2_sda_en});   // prints UFM CHECK <rule>
  i2c_mon        mon(uscl, usda);                             // prints I2C S 50 W NA ... P
  initial begin
    #200;
    ctrl.start(); ctrl.address7(7'h50, 1); ctrl.write_byte(8'h10); ctrl.write_byte(8'h99); ctrl.stop();
    $display("%h", led1.mem[16]);
    #200;
    $finish;
  end
  initial begin #1000000; $fatal(1, "UFm watchdog"); end
endmodule
Read included file: i2c_ufm_ctrl.sv
`timescale 1ns/1ns
// UFm controller (§3.2.1): push-pull on both lines, write only. While enabled it always drives USCL and
// USDA, high when the bus is free. enabled=0 models the isolated side of a one-hot mux (§3.2).
module i2c_ufm_ctrl(output wire uscl,usda,output wire drives);
 parameter time TLOW=100,THIGH=100,HOLD=20,TBUF=100,THD_STA=60,TSU_STA=60,TSU_STO=60;
 bit ninth_low=0,fast_uscl=0,short_hold=0;     // fault switches
 bit enabled=1,active=0;reg scl_v=1,sda_v=1;time stopped=0;int bytes=0;
 assign drives=enabled;assign uscl=enabled?scl_v:1'bz;assign usda=enabled?sda_v:1'bz;
 task automatic start();
  if(!active)begin if($time-stopped<TBUF)#(TBUF-($time-stopped));end
  else begin scl_v=0;#(HOLD);sda_v=1;#(TLOW-HOLD);scl_v=1;#(TSU_STA);end   // Sr: USDA high, USCL high, setup
  sda_v=0;#(THD_STA);scl_v=0;active=1;
 endtask
 task automatic bit_out(input bit v);time h,l,t;
  h=short_hold?0:HOLD;l=fast_uscl?40:TLOW;t=fast_uscl?40:THIGH;
  scl_v=0;#(h);sda_v=v;#(l-h);scl_v=1;#(t);scl_v=0;
 endtask
 // Eight data bits, then the ninth clock with USDA driven HIGH by the controller (§3.2.5, §3.2.6).
 task automatic write_byte(input reg [7:0] value);
  for(int b=7;b>=0;b--)bit_out(value[b]);bit_out(!ninth_low);bytes++;
 endtask
 task automatic address7(input reg [6:0] addr,input bit rd);write_byte({addr,rd});endtask
 task automatic stop();
  scl_v=0;#(HOLD);sda_v=0;#(TLOW-HOLD);scl_v=1;#(TSU_STO);sda_v=1;stopped=$time;active=0;#1;
 endtask
endmodule
Read included file: i2c_ufm_target.sv
`timescale 1ns/1ns
// UFm target (§3.2): a receiver only, such as an LED driver. It never drives USDA or USCL; the fault
// switches make it try (an ACK, a stretch), which collides with the controller's push-pull outputs.
// Table 8: 0000 000+W is the general call (optional, §3.2.10), 0000 000+R the START byte (ignored), the
// rest of 0000 XXX and 1111 XXX are not this target's address. A READ header is not supported (§3.2.7).
module i2c_ufm_target #(parameter [6:0] ADDRESS=7'h50)(inout wire uscl,usda,output reg sda_en=0,scl_en=0);
 bit drives_ack=0,stretches=0;                 // fault switches
 bit general_call=1,software_reset=1;           // optional features (Table 7)
 reg [7:0] mem[0:255];reg [7:0] ptr=0,shift=0;int resets=0,prog_writes=0,ignored=0,reads_refused=0,count=0,bytes=0;
 bit active=0,selected=0,need_ptr=1,gc=0,ninth_seen=0;
 assign usda=sda_en?1'b0:1'bz;assign uscl=scl_en?1'b0:1'bz;
 initial for(int i=0;i<256;i++)mem[i]=i[7:0];
 always @(negedge usda)if(uscl===1)begin active=1;selected=0;gc=0;count=0;bytes=0;need_ptr=1;end   // S or Sr
 always @(posedge usda)if(uscl===1&&active)begin active=0;selected=0;gc=0;count=0;end              // P
 always @(posedge uscl)if(active)begin
  if(count<8)begin shift={shift[6:0],usda};count++;
   if(count==8)begin
    if(bytes==0)begin
     if(shift[7:1]==ADDRESS)begin selected=!shift[0];if(shift[0])reads_refused++;end
     else gc=general_call&&shift==8'h00;
    end else if(gc&&bytes==1)begin
     if(shift==8'h06&&software_reset)begin for(int i=0;i<256;i++)mem[i]=i[7:0];ptr=0;resets++;end   // §3.2.11
     else if(shift==8'h04)prog_writes++;                                                             // §3.2.10
     else ignored++;                                     // 00h is not allowed; other codes and B = 1 are ignored
     gc=0;
    end else if(selected)begin if(need_ptr)begin ptr=shift;need_ptr=0;end else begin mem[ptr]=shift;ptr=ptr+1'b1;end end
    bytes++;
   end
  end else begin count=0;ninth_seen=usda;end    // the ninth clock: the controller drives USDA HIGH
 end
 always @(negedge uscl)if(active)begin
  if(count==8&&drives_ack&&selected)sda_en=1;   // fault: tries to ACK
  else sda_en=0;
  if(count==0&&bytes>0&&selected&&stretches&&!scl_en)begin scl_en=1;#300;scl_en=0;end   // fault: tries to stretch
 end
endmodule
Read included file: i2c_ufm_checker.sv
`timescale 1ns/1ns
// What a push-pull bus adds to the shared i2c_checker (which measures the Table 15 edge times):
// contention (a line at x: two drivers disagree), a target driver switched on, a ninth bit that is not
// HIGH (§3.2.6) and a USCL period under 200 ns (fUSCL 5000 kHz, Table 15).
module i2c_ufm_checker(input wire uscl,usda,input wire [1:0] target_drives);
 int broken=0,pulses=0;bit active=0,x_sda=0,x_scl=0;time rise=0;
 task automatic bad(input string rule);broken++;$display("UFM CHECK %s at %0t",rule,$time);endtask
 always @(usda)begin if(usda===1'bx&&!x_sda)bad("contention_usda");x_sda=usda===1'bx;
  if(uscl===1&&usda===0)begin active=1;pulses=0;end else if(uscl===1&&usda===1)active=0;end
 always @(uscl)begin if(uscl===1'bx&&!x_scl)bad("contention_uscl");x_scl=uscl===1'bx;end
 always @(posedge target_drives[0])bad("target_drives_usda");
 always @(posedge target_drives[1])bad("target_drives_uscl");
 always @(posedge uscl)begin
  if(rise&&$time-rise<200&&active)bad("fuscl");rise=$time;
  if(active)begin pulses++;if(pulses%9==0&&usda!==1'b1)bad("ninth_bit");end
 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
Spot the bugEach option overrides one parameter of the UFm controller with defparam. The harness runs the four-byte write and passes when both checkers are clean. Which override fails?
Read included file: i2c_ufm_ctrl.sv
`timescale 1ns/1ns
// UFm controller (§3.2.1): push-pull on both lines, write only. While enabled it always drives USCL and
// USDA, high when the bus is free. enabled=0 models the isolated side of a one-hot mux (§3.2).
module i2c_ufm_ctrl(output wire uscl,usda,output wire drives);
 parameter time TLOW=100,THIGH=100,HOLD=20,TBUF=100,THD_STA=60,TSU_STA=60,TSU_STO=60;
 bit ninth_low=0,fast_uscl=0,short_hold=0;     // fault switches
 bit enabled=1,active=0;reg scl_v=1,sda_v=1;time stopped=0;int bytes=0;
 assign drives=enabled;assign uscl=enabled?scl_v:1'bz;assign usda=enabled?sda_v:1'bz;
 task automatic start();
  if(!active)begin if($time-stopped<TBUF)#(TBUF-($time-stopped));end
  else begin scl_v=0;#(HOLD);sda_v=1;#(TLOW-HOLD);scl_v=1;#(TSU_STA);end   // Sr: USDA high, USCL high, setup
  sda_v=0;#(THD_STA);scl_v=0;active=1;
 endtask
 task automatic bit_out(input bit v);time h,l,t;
  h=short_hold?0:HOLD;l=fast_uscl?40:TLOW;t=fast_uscl?40:THIGH;
  scl_v=0;#(h);sda_v=v;#(l-h);scl_v=1;#(t);scl_v=0;
 endtask
 // Eight data bits, then the ninth clock with USDA driven HIGH by the controller (§3.2.5, §3.2.6).
 task automatic write_byte(input reg [7:0] value);
  for(int b=7;b>=0;b--)bit_out(value[b]);bit_out(!ninth_low);bytes++;
 endtask
 task automatic address7(input reg [6:0] addr,input bit rd);write_byte({addr,rd});endtask
 task automatic stop();
  scl_v=0;#(HOLD);sda_v=0;#(TLOW-HOLD);scl_v=1;#(TSU_STO);sda_v=1;stopped=$time;active=0;#1;
 endtask
endmodule
Read included file: i2c_ufm_target.sv
`timescale 1ns/1ns
// UFm target (§3.2): a receiver only, such as an LED driver. It never drives USDA or USCL; the fault
// switches make it try (an ACK, a stretch), which collides with the controller's push-pull outputs.
// Table 8: 0000 000+W is the general call (optional, §3.2.10), 0000 000+R the START byte (ignored), the
// rest of 0000 XXX and 1111 XXX are not this target's address. A READ header is not supported (§3.2.7).
module i2c_ufm_target #(parameter [6:0] ADDRESS=7'h50)(inout wire uscl,usda,output reg sda_en=0,scl_en=0);
 bit drives_ack=0,stretches=0;                 // fault switches
 bit general_call=1,software_reset=1;           // optional features (Table 7)
 reg [7:0] mem[0:255];reg [7:0] ptr=0,shift=0;int resets=0,prog_writes=0,ignored=0,reads_refused=0,count=0,bytes=0;
 bit active=0,selected=0,need_ptr=1,gc=0,ninth_seen=0;
 assign usda=sda_en?1'b0:1'bz;assign uscl=scl_en?1'b0:1'bz;
 initial for(int i=0;i<256;i++)mem[i]=i[7:0];
 always @(negedge usda)if(uscl===1)begin active=1;selected=0;gc=0;count=0;bytes=0;need_ptr=1;end   // S or Sr
 always @(posedge usda)if(uscl===1&&active)begin active=0;selected=0;gc=0;count=0;end              // P
 always @(posedge uscl)if(active)begin
  if(count<8)begin shift={shift[6:0],usda};count++;
   if(count==8)begin
    if(bytes==0)begin
     if(shift[7:1]==ADDRESS)begin selected=!shift[0];if(shift[0])reads_refused++;end
     else gc=general_call&&shift==8'h00;
    end else if(gc&&bytes==1)begin
     if(shift==8'h06&&software_reset)begin for(int i=0;i<256;i++)mem[i]=i[7:0];ptr=0;resets++;end   // §3.2.11
     else if(shift==8'h04)prog_writes++;                                                             // §3.2.10
     else ignored++;                                     // 00h is not allowed; other codes and B = 1 are ignored
     gc=0;
    end else if(selected)begin if(need_ptr)begin ptr=shift;need_ptr=0;end else begin mem[ptr]=shift;ptr=ptr+1'b1;end end
    bytes++;
   end
  end else begin count=0;ninth_seen=usda;end    // the ninth clock: the controller drives USDA HIGH
 end
 always @(negedge uscl)if(active)begin
  if(count==8&&drives_ack&&selected)sda_en=1;   // fault: tries to ACK
  else sda_en=0;
  if(count==0&&bytes>0&&selected&&stretches&&!scl_en)begin scl_en=1;#300;scl_en=0;end   // fault: tries to stretch
 end
endmodule
Read included file: i2c_ufm_checker.sv
`timescale 1ns/1ns
// What a push-pull bus adds to the shared i2c_checker (which measures the Table 15 edge times):
// contention (a line at x: two drivers disagree), a target driver switched on, a ninth bit that is not
// HIGH (§3.2.6) and a USCL period under 200 ns (fUSCL 5000 kHz, Table 15).
module i2c_ufm_checker(input wire uscl,usda,input wire [1:0] target_drives);
 int broken=0,pulses=0;bit active=0,x_sda=0,x_scl=0;time rise=0;
 task automatic bad(input string rule);broken++;$display("UFM CHECK %s at %0t",rule,$time);endtask
 always @(usda)begin if(usda===1'bx&&!x_sda)bad("contention_usda");x_sda=usda===1'bx;
  if(uscl===1&&usda===0)begin active=1;pulses=0;end else if(uscl===1&&usda===1)active=0;end
 always @(uscl)begin if(uscl===1'bx&&!x_scl)bad("contention_uscl");x_scl=uscl===1'bx;end
 always @(posedge target_drives[0])bad("target_drives_usda");
 always @(posedge target_drives[1])bad("target_drives_uscl");
 always @(posedge uscl)begin
  if(rise&&$time-rise<200&&active)bad("fuscl");rise=$time;
  if(active)begin pulses++;if(pulses%9==0&&usda!==1'b1)bad("ninth_bit");end
 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
ConceptA UFm design needs a second controller for a service port. What does the specification allow?

Remember

  • UFm is push-pull and unidirectional, up to 5 MHz: one controller writes, and targets only receive.
  • The frame keeps S, address, eight bits, the ninth clock and P; the controller drives the ninth bit HIGH.
  • No ACK, stretching, synchronisation, arbitration, reads or Device ID: anything that needs a target to drive is gone.
  • A target that drives, or a second connected controller, collides with push-pull outputs and the line goes to x.
  • Table 8 frees the CBUS, other-format, Hs-code and device-ID addresses; a stuck target is reset by software, a reset pin or power.

Practice