Addressing and Arbitration
Pick between a select line per device and an address on a shared pair, show how two senders settle a wired-AND line without losing a bit, and say how I2C and I3C give out addresses.
- Lesson
- P5.6
- Time
- 25 min
- Runs on
- Your browser
- Check
- 5 questions
A board carried two copies of one temperature sensor on the same I2C bus, both strapped to the same fixed address. Every read was acknowledged, and every read returned a value neither sensor held.
A low lane of 1 pulls the net down. The pull-up raises it only when every device releases.
- Cycle 0
Bit 7: sensor A sends 0, sensor B sends 1. A 0 from either one pulls the line low.
- Cycle 7
Bit 0: the controller has read 0x42, the AND of the two readings. Both sensors acknowledged the address, so nothing flagged it.
Measured on the I2C bench: sensor_a holds 5a, sensor_b holds c3, controller reads 42. The wire carries the AND of the two bytes.
Select Lines or an Address
- A select line
The controller lowers one select line. Only that peripheral listens and drives MISO; the address costs a pin, and no bus time.
- An address byte
Every target hears the same byte after START. The one whose address matches pulls the ninth bit low; the rest wait for the next START.
Read the diagram as text
- SPI controller: a select line per device
- Peripheral 0: CS0 low: it drives MISO
- Peripheral 1: CS1 high: MISO released
- I2C controller: one address byte per transfer
- Target 0x50: matches: pulls ACK
- Target 0x48: no match: stays silent
- SPI controller → Peripheral 0: CS0 low
- SPI controller → Peripheral 1: CS1 high
- I2C controller → Target 0x50: S 50 W
- I2C controller → Target 0x48: S 50 W
A select line is decided before the first clock. An address byte takes 9 clocks of every transfer.
Three peripherals, three select lines (spi-shared)
for (d = 0; d < 3; d++) begin
cs_lo(d); read register 2; cs_hi();
end
Measured: select 0 low: controller reads 11, select 1 low: controller reads 22, select 2 low: controller reads 33.
One target on a shared pair (i2c-shared)
for (a = 'h4f; a <= 'h51; a++)
start; address7(a); stop;
Measured: I2C S 4f W NA P, I2C S 50 W A P, I2C S 51 W NA P. Only 0x50 acknowledges.
| Devices | SPI wires | I2C wires | I3C wires |
|---|---|---|---|
| 1 | 4 (CLK, MOSI, MISO and 1 select) | 2 (SCL, SDA) | 2 (SCL, SDA) |
| 2 | 5 (CLK, MOSI, MISO and 2 selects) | 2 (SCL, SDA) | 2 (SCL, SDA) |
| 4 | 7 (CLK, MOSI, MISO and 4 selects) | 2 (SCL, SDA) | 2 (SCL, SDA) |
| 8 | 11 (CLK, MOSI, MISO and 8 selects) | 2 (SCL, SDA) | 2 (SCL, SDA) |
| 16 | 19 (CLK, MOSI, MISO and 16 selects) | 2 (SCL, SDA) | 2 (SCL, SDA) |
Wires counted on each bus. The price moves into time: an I2C byte to one target takes 9 address clocks plus 9 data clocks, against 8 SPI clocks (counted from generated figures).
I2C has seven address bits, and two groups of eight addresses are reserved (UM10204 §3.1.12, Table 4). The hook shows the trap: two devices with one address both answer, and the bus returns the AND of their replies.
Arbitration on a Wired-AND Line
On an open-drain line a 0 always wins: anyone may pull low, nobody drives high. Two controllers that start together each read the line back. The first one to send a 1 and read a 0 has lost, and it lets go (UM10204 §3.1.8).
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)
- bit 0: columns 1 through 4.
- bit 1: columns 5 through 8.
- bit 2: columns 9 through 12.
- bit 3: columns 13 through 16.
- bit 4: columns 17 through 20.
- bit 5: columns 21 through 24.
- bit 6: columns 25 through 28.
- bit 7: columns 29 through 32.
- SDA sampled at 3, 7, 11, 15, 19, 23, 27, 31.
- Cycle 3
The first bit sent (band
bit 0): both send 1. Nobody pulls, the line reads 1, and both stay in. - Cycle 15
The 4th bit sent (band
bit 3): controller 1 sends 0 and pulls the line low; controller 2 sent 1 and reads 0. It drops out here. - Cycle 31
Controller 1 finishes its byte alone. Its address went out unharmed, so the target it wanted answers.
Generated and decoded back: the decoder finds the drop on the 4th bit sent. The bench measured the same SCL pulse: other drops out at SCL pulse 4.
Both read SDA back
fork
ctrl: S 50 W, 10, 3c, P
other: S 58 W, 10, a5, P
join
Measured: other drops out at SCL pulse 4, ctrl ack 1 lost 0 | other ack 0 lost 1 | target register 10 holds 3c.
other.ctrl_ignores_sda = 1
other.ctrl_ignores_sda = 1;
fork
ctrl: S 50 W, 10, 3c, P
other: S 58 W, 10, a5, P
join
Measured: ctrl ack 0 lost 1 | other ack 1 lost 0 | target register 10 holds 25.
With the rule, the lower address wins and its byte lands whole: 0x3c. Without it, other keeps sending after it lost. The two data bytes merge on the wire, and the honest controller loses partway through the data. The register ends up holding 0x25, a byte neither controller sent.
Addresses Given Out at Run Time
An I2C address is fixed in the part, sometimes with a few bits set by pins. The general call with second byte 04h or 06h tells devices to take in that programmable part (UM10204 §3.1.13). I3C goes further and gives every target its address on the bus.
- Enter the mode
The controller broadcasts ENTDAA, then a repeated START and 7E with the read bit. Every target still without an address acknowledges.
- The contest
The controller only clocks. Each target sends its ID most significant bit first and drops at the first bit where it sent 1 and saw 0.
- The address
The controller sends a 7-bit address and an odd parity bit; the winner acknowledges and keeps it. The next round starts with the rest.
Read the diagram as text
- ENTDAA: broadcast to every target
- Sr, 7E read: targets without an address ACK
- IDs on SDA: 48-bit ID, BCR, DCR, open drain
- Lowest ID wins: losers drop at their first 1-vs-0
- Address + parity: controller sends 7 bits, odd parity
- Winner ACKs: then the next round
- ENTDAA → Sr, 7E read
- Sr, 7E read → IDs on SDA
- IDs on SDA → Lowest ID wins
- Lowest ID wins → Address + parity
- Address + parity → Winner ACKs
- Winner ACKs → Sr, 7E read: repeat until NACK
MIPI I3C Basic §5.1.4.2 (steps 3 to 9) and §5.1.9.3.4 (ENTDAA). A target with a known static address can be given one directly instead (SETDASA, §5.1.9.3.10).
On the I3C bench, target1 carries the higher ID and target2 the lower one. Measured: CTRL DAA PID 02aa01230000 BCR 06 DCR 00 -> 08, CTRL DAA PID 02aa01231000 BCR 06 DCR 00 -> 09, target1 gets 09, target2 gets 08. The IDs first differ at bit 36 of 48, so target2 won the first round and got the first address.
Where This Shows Up
| Idea | Where it returns |
|---|---|
| A select line per device, MISO shared | P7.4 only the selected peripheral drives |
| Address byte, R/W bit, reserved addresses | P8.4 addressing and the combined format |
| Wired-AND arbitration and the loser's duties | P8.6 clock synchronization and arbitration |
| Open drain where arbitration happens | P9.3 open drain and push-pull in I3C |
| Dynamic addresses | P9.4 ENTDAA on the I3C bench |
| Arbitration inside a chip | P1.9 arbiters, fairness and atomic access |
A wire arbitrates by value: the lowest wins every time. An on-chip arbiter can choose a policy instead, such as round robin.
Try It
ctrl and other start a write at the same moment: ctrl to 0x50, other to 0x58. The probe prints the SCL pulse on which other loses, counted from START, so pulse 1 is the first address bit.
Add other.ctrl_ignores_sda = 1; before the fork and run it again. Then try addresses that differ only in the last bit, such as 0x50 and 0x51.
other drops out at SCL pulse 4 ctrl ack 1 lost 0 | other ack 0 lost 1 | target register 10 holds 3cRead 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
- A select line per device costs a pin each and no bus time; an address on a shared pair costs an address byte per transfer.
- Two devices with the same address both answer, and an open-drain bus returns the AND of their replies.
- On a wired-AND line the first sender to put out a 1 and read a 0 has lost; the lowest value wins with no bit lost.
- A loser that never reads the line back corrupts the winner's data.
- I2C addresses are fixed or pin-set; I3C assigns them on the bus by running the same contest on each target's 48-bit ID.