I2C in One Picture
Draw the two I2C lines with their pull-ups and both drivers, say why any device pulling low wins, and read a measured rise time against the Fast-mode limit.
- Lesson
- P8.1
- Time
- 25 min
- Runs on
- Your browser
- Check
- 4 questions
A board with two I2C devices worked until firmware enabled the second one. Then SDA sat between 0.3 and 0.7 VDD and nothing decoded. One part drove a 1 "for speed" while the other pulled 0, and every receiver read a different value.
A low lane of 1 pulls the net down. The pull-up raises it only when every device releases.
- Cycle 0
Both devices let go. The pull-up resistor holds SDA high: the idle bus.
- Cycle 1
The controller pulls. One pull is enough: SDA is low.
- Cycle 2
Both pull. Nothing fights; the line is simply low.
- Cycle 3
The controller lets go, but the target still pulls. SDA stays low, and the controller can see it by reading the pin.
- Cycle 4
The target lets go too. Only now does the resistor pull SDA back up.
SDA is high only when every device has let go: a wired-AND. Clock stretching and arbitration both come from this one property.
One Line, Many Hands
No I2C device ever drives a 1. Each one can pull a line low or let go, and a resistor pulls the line high when everyone has let go (§3.1.1). Step through the scene above: the line is the AND of every device's release.
Both lines, SCL and SDA, work this way, and both are high when the bus is free. The bench writes this as tri1 scl, sda; with every device driving 0 or z.
Who Pulls, and When
The bus hides who is pulling. The bench shows it: ctrl_low and tgt_low are 1 while that device pulls SDA. Every I2C figure in this module has these lanes, because a logic analyser cannot show them.
A low lane of 1 pulls the net down. The pull-up raises it only when every device releases.
Read phases and sample points (quarter bit)
- START: columns 2 through 3.
- 0xa0 + ACK: columns 4 through 39.
- 0x3c + ACK: columns 40 through 75.
- STOP: columns 76 through 79.
- SDA sampled at 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62, 66, 70, 74.
- Cycle 1
START: the controller pulls SDA while SCL is high.
- Cycle 6
Address bits: the controller pulls for every 0 and lets go for every 1. The target only listens.
- Cycle 38
The ninth clock:
ctrl_lowis 0 andtgt_lowis 1. The target pulls SDA to acknowledge. - Cycle 74
The data byte has its own ninth clock, and the target pulls again.
- Cycle 78
STOP: the controller lets SDA rise while SCL is high.
Generated from the transaction and decoded back from SCL and SDA. Dots mark where SDA is sampled, once per SCL high.
The bench sends the same address and prints what its decoder saw: I2C S 50 W A P. In the address bits the controller pulls; in the ninth clock the target pulls. P8.3 is about that handover.
The Slow Edge
Falling is fast: a transistor discharges the bus. Rising is slow: the pull-up resistor charges the bus capacitance. UM10204 measures both between 0.3 VDD and 0.7 VDD, the input thresholds (§3.1.2).
- Time 167.6 nsv(sda) = 990 mV
SDA crosses 0.3 VDD at 168 ns. Below this, every receiver reads 0.
- Time 565.9 nsv(sda) = 2.31 V
It crosses 0.7 VDD at 566 ns: a rise time of 398 ns. Fast-mode allows 300.
- Time 1.01 µsv(sda) = 1.453 V
The pull-down closes. SDA falls through the same band in 11.7 ns.
Read the plot as a table
| Time | v(sda) |
|---|---|
| 167.6 ns | 990 mV |
| 200 ns | 1.144 V |
| 400 ns | 1.891 V |
| 565.9 ns | 2.31 V |
| 600 ns | 2.379 V |
| 800 ns | 2.698 V |
| 1 µs | 2.907 V |
| 1.01 µs | 1.453 V |
| 1.2 µs | 90.81 mV |
Simulated on the Analog engine (ngspice 42) from the library's pull-up netlist. The pull-down is a switch with 133 Ω on-resistance (0.4 V at 3 mA, Table 10).
The charge takes 0.8473 RC between the two thresholds (§7.1): 398 ns for 4.7 kΩ and 100 pF. The run measures 398 ns. Table 11 allows 1000 ns in Standard-mode and 300 ns in Fast-mode, so this pull-up is legal at 100 kHz only. P8.7 sizes one properly.
When a Device Drives a 1
Set target.push_pull_target = 1; and the target drives SDA high whenever it is not pulling. The first time the controller pulls against it, the simulator resolves the fight honestly: sda is x at 2700: cdl = 1, high_drive = 1.
sda is x at 2700: cdl = 1, high_drive = 1
I2C CHECK push_pull_contention at 2701
I2C CHECK push_pull_contention at 5911
I2C CHECK push_pull_contention at 10931
I2C CHECK push_pull_contention at 25991
ack = 0, ck.broken = 4The checker counts 4 contentions in one address byte. On a board the line sits at whatever voltage the two transistors agree on, often inside the band where no receiver is sure.
What Is Mandatory, and How Fast
| Feature | Single controller | Multi-controller | Target |
|---|---|---|---|
| START, STOP, acknowledge | M | M | M |
| 7-bit target address | M | M | M |
| Synchronization, arbitration | n/a | M | n/a |
| Clock stretching | O (note 2) | O (note 2) | O |
| 10-bit address, general call, software reset | O | O | O |
| START byte | n/a | O | n/a |
| Device ID | n/a | n/a | O |
UM10204 Table 3, grouped: M mandatory, O optional. Note 2: a controller on a bus where no target stretches need not handle stretching. The lessons follow this order, mandatory first.
| Mode | Rate | Rise time limit | Bus | Lesson |
|---|---|---|---|---|
| Standard-mode (Sm) | 100 kbit/s | 1000 ns | open-drain, wired-AND | P8.2 to P8.8 |
| Fast-mode (Fm) | 400 kbit/s | 300 ns | open-drain, wired-AND | the bench default |
| Fast-mode Plus (Fm+) | 1 Mbit/s | 120 ns | open-drain, 20 mA drive | P8.9 |
| High-speed mode (Hs) | 3.4 Mbit/s | Table 13 | current-source pull-up on SCLH | P8.9 |
| Ultra Fast-mode (UFm) | 5 Mbit/s | 50 ns (Table 15) | push-pull, one direction | P8.12 |
UM10204 §5, Table 11, Table 13 and Table 15. Sm, Fm, Fm+ and Hs share one protocol and are downward compatible. UFm is a different, write-only bus.
- The protocol
Bits, bytes and addresses: everything Table 3 marks mandatory for every device.
- The shared clock
Stretching and arbitration both use the wired-AND on SCL and SDA.
- The analog side
Rise times, pull-ups, recovery and the faster modes.
- Verification
A monitor, a checker that sees the driver view, and bug hunts.
Read the diagram as text
- Bits and bytes: P8.2 START/STOP, P8.3 ACK
- Addressing: P8.4 7-bit, 10-bit, reserved
- Clock stretching: P8.5 a target holds SCL
- Arbitration: P8.6 two controllers
- Timing and pull-ups: P8.7 Table 11, Rp window
- Stuck buses: P8.8 nine clocks
- Fm+ and Hs: P8.9 what changes
- Checkers and bug hunts: P8.10, P8.11
- Bits and bytes → Addressing
- Addressing → Clock stretching
- Clock stretching → Arbitration
- Arbitration → Timing and pull-ups
- Timing and pull-ups → Stuck buses
- Stuck buses → Fm+ and Hs
- Fm+ and Hs → Checkers and bug hunts
Every lesson runs on the same bench: one controller, a register target at 0x50 and a Fast-mode checker.
Try It
The controller sends START, address 0x50 with the write bit, and STOP. The decoder mon prints one line per transaction; the checker ck counts Fast-mode timing misses in ck.broken.
Add target.push_pull_target = 1; before #2000; and set mon.trace = 0; to read the checker lines and the x on SDA.
I2C S 50 W A P ack = 1, 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
Check Yourself
Remember
- Open-drain outputs and a pull-up make a wired-AND: any device pulling low wins, and the line is high only when all let go.
- The bus hides who pulls; the driver-view lanes ctrl_low and tgt_low show it.
- The rise is an RC charge, 0.8473 Rp × Cb between 0.3 and 0.7 VDD, and it sets the speed limit.
- Table 3 marks START, STOP, ACK and 7-bit addressing mandatory; synchronization and arbitration only for several controllers.
- Sm, Fm, Fm+ and Hs share one protocol; UFm is a different, push-pull bus.
Practice
- Read the Open-Drain Driver View Implement · Easy