In-Band Interrupts and Hot-Join
Raise an interrupt on a free bus and during the controller's own header, say who wins a collision and why, and walk a hot-join from request to address.
- Lesson
- P9.6
- Time
- 35 min
- Runs on
- Your browser
- Check
- 4 questions
A target raised an interrupt just as the controller started a transfer. Both pulled SDA in the same instant: one START, two intentions. The target's firmware assumed "I pulled first, so I own the bus" and drove push-pull over the controller's header. The rule is I2C's: arbitrate bit by bit in open drain, and the loser waits (MIPI §5.1.2.2.1).
- Cycle 8
Bit A6: the controller sends 1 (lets go); target2 sends 0 (pulls). SDA is 0: the controller has lost (MIPI §5.1.2.2.1).
- Cycle 26
The controller stays silent for the rest of the header, but keeps clocking: only it drives SCL.
- Cycle 50
RnW = 1: an interrupt request from 0x09, not a controller-role request.
- Cycle 56
The controller ACKs and will read the mandatory byte before it goes back to its own write.
Measured: I3C S 09 R A a5 T0 Sr 7e W A Sr 09 W A 10 T0 Sr 09 R A 5a T0 P. The broadcast address 7E has lower priority than any interrupt (§5.1.6.4).
The Request
- Cycle 5
The bus has been free for longer than tAVAL (1 µs, Table 58). Target1 pulls SDA low: a START request.
- Cycle 7
The controller pulls SDA too and then SCL, which completes the START (MIPI §5.1.2.2).
- Cycle 17
Header 08/R in open drain. The controller sends 7E and lets go on its first bit; target1's 0 wins.
- Cycle 59
The controller ACKs: an accepted IBI. After the SCL rise the target takes SDA over (§5.1.2.3.2).
- Cycle 67
The mandatory byte 0xA5 in push-pull, then T = 0 and STOP. BCR bit 2 promised this byte (Table 6).
Measured: CTRL START request from a target at 47316, CTRL IBI from 08 accepted, MDB a5 T0, I3C S 08 R A a5 T0 P.
Two ways in: join the arbitrable header after any START, or, if none comes within Bus Available, make the START yourself by pulling SDA (MIPI §5.1.6.2). Only the controller drives SCL, so a target can ask, never clock.
Outcomes
| Outcome | Bench run | Measured |
|---|---|---|
| Accepted, with the mandatory byte | target1 raises, controller accepts | I3C S 08 R A a5 T0 P |
| Refused (NACK), retried later | ctrl.ibi_accept = 0, then 1 | CTRL request from 08 R refused (NACK) then I3C S 08 R NA P then CTRL IBI from 08 accepted, MDB a5 T0 then I3C S 08 R A a5 T0 P then IBIs accepted 1 |
| Lost to a lower address | both targets raise during a write | I3C S 08 R A a5 T0 Sr 7e W A Sr 09 W A 10 T0 Sr 09 R A 5a T0 P then, after Bus Available, I3C S 09 R A a5 T0 P |
| Hot-join | a new target calls hot_join() | I3C S 02 W A Sr 7e W A 07 T0 Sr 7e R A id 02aa01233000 bcr 06 dcr 00 da 0a par 1 A Sr 7e R NA P, then joiner 0a |
Measured. The NACKed target tries again after the next Bus Available Condition (MIPI §5.1.6.2 action 2). The loser of a collision waits for Bus Available too, so its request becomes the next frame.
The controller has three answers (§5.1.6.2). It can ACK and read the mandatory byte if BCR bit 2 says one follows, NACK and let the target retry, or NACK and send DISEC to stop it asking. Lower addresses win collisions, which is why DAA gives urgent targets low addresses (§5.1.6.1).
Hot-Join
- Wait
The joiner waits for a Bus Idle Condition, at least tIDLE of Bus Available (MIPI §5.1.5 step 1, §5.1.3.4, Table 58).
- Request
It issues 7'h02 with W after a START, through the normal arbitrable header (step 3).
- Assign
The controller ACKs and runs ENTDAA; only targets without an address take part (step 4c).
Read the diagram as text
- Wait tIDLE: 200 µs of Bus Available
- S 02/W: the reserved hot-join address
- Controller ACK: or NACK: try at the next START
- Sr 7E/W ENTDAA: only unaddressed targets answer
- Address 0x0a: the next free one
- Wait tIDLE → S 02/W
- S 02/W → Controller ACK
- Controller ACK → Sr 7E/W ENTDAA
- Sr 7E/W ENTDAA → Address 0x0a
MIPI I3C Basic v1.0 §5.1.5. Measured: the joiner's START request came at 247316 ns, 200 µs after joiner.hot_join() was called on an idle bus.
A device that joins a powered bus must not load it while it is still unpowered. Its SCL and SDA pads have to be failsafe, drawing no extra leakage whether the lines are high or low (MIPI §5.1.5.1). That is an electrical rule the logic bench cannot measure.
When a Target May Ask: Bus Conditions and Activity States
| Bus condition | Holds after | Who may do what |
|---|---|---|
| Bus Free | STOP, for tCAS on a pure bus or tBUF on a mixed bus | the controller may START |
| Bus Available | Bus Free sustained for tAVAL (1 µs) | a target may pull SDA to request a START |
| Bus Idle | Bus Available sustained for tIDLE (200 µs) | a hot-joining target may request |
| measured | target2 raised its IBI during a write on the mixed bench | START request 1009 ns after the STOP |
MIPI I3C Basic v1.0 §5.1.3.2 to §5.1.3.4, Table 58. The 9 ns over 1 µs come from the bench target's 10 ns polling step. On a mixed bus tAVAL is 300 ns shorter than tBUF (Table 58 note 7), so a waiting target always gets in before the controller's next START.
| Step | GETSTATUS from 0x08 (MSB, LSB) | Bits 7:6 activity, bit 5 protocol error (Table 41) |
|---|---|---|
| after ENTAS2 (broadcast 0x04) | 0x0080 | activity 2, error 0 |
| after one write with a parity error (S2) | 0x00a0 | activity 2, error 1 |
| read again | 0x0080 | activity 2, error 0: the flag clears on a read |
Measured. ENTAS0 to ENTAS3 tell targets the bus will be quiet for at least 1 µs, 100 µs, 2 ms or 50 ms, so they may save power. They also set how long the controller may take (tCAS) to answer a START request (§5.1.3.5, Table 12, Table 58). Targets need not support them.
Two Broken Targets
| Twin | Measured |
|---|---|
target1.ibi_without_mdb = 1: BCR promises a byte, the target sends none | I3C S 08 R A P then I3C CHECK ibi_mdb_present at 49488 then CTRL IBI from 08 accepted, MDB ff T1 then I3C S P |
target2.ibi_loses_and_retries_wrong = 1: loses, then tries again after Sr | I3C S 7e W A Sr 09 W A 10 T0 5a T1 P then CTRL IBI from 08 accepted, MDB a5 T0 then I3C CHECK no_pp_contention at 55077 then x on SDA at 55077 then I3C S 08 R A a5 T0 Sr xX W NA Sr 09 W A 10 T0 Sr 09 R NA P then read xx, IBIs accepted 1 |
Measured. Retrying after a repeated START is forbidden: no target may send its address after Sr (MIPI §5.1.2.2.4), and the loser must wait for Bus Available (§5.1.6.2).
The missing byte is worse than it looks. The controller releases SDA after handing over (§5.1.2.3.2); with nobody taking it, SDA rises while SCL is high, which is a STOP on the wire. The decoder closes the frame there, and the controller goes on to read 0xFF from the keeper.
Try It
First target1 interrupts alone. Then both targets raise at once: the controller serves 0x08 first, and 0x09, having lost, asks again after the next Bus Available.
Swap the order of the two raise_ibi() calls: nothing changes, because the address, not the order of the calls, decides the arbitration.
CTRL START request from a target at 47316 CTRL IBI from 08 accepted, MDB a5 T0 I3C S 08 R A a5 T0 P CTRL START request from a target at 51332 CTRL IBI from 08 accepted, MDB a5 T0 I3C S 08 R A a5 T0 P CTRL START request from a target at 55348 CTRL IBI from 09 accepted, MDB a5 T0 I3C S 09 R A a5 T0 PRead included file: i3c_ctrl.sv
`timescale 1ns/1ns
// I3C controller (v1.0: Master), SDR. Task calls drive the pins; SCL is push-pull and driven by this device
// alone (§5.1.2). SDA is open drain in the arbitrable header after S, in every ACK slot and in DAA; push-pull
// in the header after Sr and in write data (§5.1.2.2, §5.1.2.3, §5.1.4.2).
module i3c_ctrl(inout wire scl,sda,output reg sda_low=0,sda_high=0);
parameter time PP_LOW=40,PP_HIGH=40,OD_LOW=200,OD_HIGH=40,HOLD=5,T_CAS=40,TBUF=1300,
FM_LOW=1300,FM_HIGH=1200,FM_HOLD=100,FM_SETUP=600,T_MMOVERLAP=220;
// START_AS_CONTROLLER = 0: a secondary controller's controller half, SCL released until a handoff (§5.1.6.3).
parameter START_AS_CONTROLLER=1;
// Fault switches.
bit pp_too_early=0,od_always=0,directed_as_broadcast=0;
// Policy and an error injector: flip_mask flips bits of the next written word on the wire (noise);
// the T-bit is still the parity of the intended byte.
bit ibi_accept=1,hj_accept=1,trace=0;reg [7:0] flip_mask=0;
// Mastership: accept requests from secondary controllers; this device's own dynamic address for DEFSLVS.
// Clock stalling (§5.1.2.5.1): extra SCL low in the next header ACK slot, at most 100 µs (Table 11).
bit mr_accept=1,mr_seen=0,handed_over=0,scl_en=START_AS_CONTROLLER;reg [6:0] mr_addr=0,own_da=7'h20;
int stall_ack_ns=0;time ext_low=0;reg [6:0] leg_addr=0;reg [7:0] leg_lvr=0;
// State.
reg scl_out=1;bit active=0,sr_done=0,lost=0,serviced=0,hj_seen=0,daa_error=0,legacy=0;time stopped=0;
reg [6:0] req_addr=0;bit req_rw=0;logic [7:0] mdb;logic tbit;
int daa_count=0,daa_expect=0,ibis=0;reg [6:0] daa_next=7'h08;
reg [6:0] daa_addr[0:7];reg [47:0] daa_pid[0:7];reg [7:0] daa_bcr[0:7],daa_dcr[0:7];
reg [7:0] wbuf[0:7];logic [7:0] rbuf[0:7];int rcount=0;
assign scl=scl_en?scl_out:1'bz;assign sda=sda_low?1'b0:sda_high?1'b1:1'bz;
localparam PP=0,OD=1,FM=2,REL=0,DRV_OD=1,DRV_PP=2;
task automatic set_sda(input int how,input logic v);
if(how==REL)begin sda_low=0;sda_high=0;end else if(how==DRV_OD)begin sda_low=!v;sda_high=0;end else begin sda_low=!v;sda_high=v;end
endtask
// One SCL pulse; SCL is low on entry and on exit. post: 0 nothing; 1 pull SDA low just after the rise
// when the sample is 0 (ACK to write data handoff, §5.1.2.3.1); 2 release 12 ns after the rise (IBI ACK
// to mandatory byte handoff, §5.1.2.3.2).
task automatic clock(input int speed,input int how,input logic v,input int post,output logic s);
time lo,hi,hd;lo=speed==FM?FM_LOW:speed==OD?OD_LOW:PP_LOW;hi=speed==FM?FM_HIGH:speed==OD?OD_HIGH:PP_HIGH;hd=speed==FM?FM_HOLD:HOLD;
#(hd);set_sda(how,v);#(lo-hd+ext_low);ext_low=0;scl_out=1;#1;s=sda;
if(post==1&&s===1'b0)begin sda_low=1;sda_high=0;end
if(post==2)begin #11;sda_low=0;#(hi-12);end else #(hi-1);
scl_out=0;
endtask
// Next free dynamic address, skipping the reserved values of Table 9.
function automatic [6:0] next_free(input [6:0] a);
reg [6:0] n;n=a+1;while(n==7'h3e||n==7'h5e||n==7'h6e||n==7'h76||n==7'h7a||n==7'h7c||n>=7'h7e)n=n+1;next_free=n;
endfunction
function automatic [7:0] bcr_of(input [6:0] a);
bcr_of=0;for(int i=0;i<daa_count;i++)if(daa_addr[i]==a)bcr_of=daa_bcr[i];
endfunction
function automatic bit known(input [6:0] a);
known=0;for(int i=0;i<daa_count;i++)if(daa_addr[i]==a)known=1;
endfunction
task automatic start();
if(stopped>0&&$time-stopped<TBUF)#(TBUF-($time-stopped));
serviced=0;lost=0;sr_done=0;legacy=0;
if(trace&&sda===1'b0)$display("CTRL START request from a target at %0t",$time);
sda_low=1;sda_high=0;#(T_CAS);scl_out=0;active=1;
endtask
// Repeated START. A read abort (T-bit) has already made one: then there is nothing to do.
task automatic restart(input bit od);
if(sr_done)sr_done=0;
else begin
#(HOLD);if(od)set_sda(REL,1);else set_sda(DRV_PP,1);#((od?OD_LOW:PP_LOW)-HOLD);
scl_out=1;#(T_CAS/2);sda_low=1;sda_high=0;#(T_CAS);scl_out=0;
end
endtask
task automatic stop();
sr_done=0;#(legacy?FM_HOLD:HOLD);sda_low=1;sda_high=0;#(legacy?FM_LOW:PP_LOW);
scl_out=1;#(legacy?FM_SETUP:T_CAS);sda_low=0;sda_high=0;stopped=$time;active=0;legacy=0;#1;
endtask
// Read one SDR word: eight data bits and the target's T-bit (§5.1.2.3.4). T = 0 ends the read: the
// controller overlaps it low after the rise. T = 1 offers more: continue (more = 1) or abort with Sr.
task automatic read_word(output logic [7:0] d,output logic t,input bit more);
logic s;
for(int b=7;b>=0;b--)begin clock(PP,REL,1,0,s);d[b]=s;end
#(HOLD);set_sda(REL,1);#(PP_LOW-HOLD);scl_out=1;#1;t=sda;
if(t===1'b0)begin sda_low=1;#(PP_HIGH-1);scl_out=0;end
else if(more)begin #(PP_HIGH-1);scl_out=0;end
else begin #9;sda_low=1;#(T_CAS);scl_out=0;sr_done=1;end
endtask
// Write one SDR word: eight data bits and the odd-parity T-bit, XOR(data, 1) (§5.1.2.3.3).
task automatic write_word(input [7:0] d);
logic s;reg [7:0] w;w=d^flip_mask;flip_mask=0;
for(int b=7;b>=0;b--)clock(od_always?OD:PP,od_always?DRV_OD:DRV_PP,w[b],0,s);
clock(od_always?OD:PP,od_always?DRV_OD:DRV_PP,~^d,0,s);
endtask
// Address header. arb = 1 after S: open drain, and a target may win it (§5.1.2.2.1). arb = 0 after Sr:
// push-pull, never arbitrated (§5.1.2.2.4). The ACK slot is always open drain.
task automatic header(input [6:0] addr,input bit rw,input bit arb,output bit ack);
reg [7:0] v,seen;logic s;v={addr,rw};lost=0;ack=0;
for(int b=7;b>=0;b--)begin
if(arb)begin clock(OD,lost?REL:DRV_OD,v[b],0,s);if(!lost&&v[b]&&s===1'b0)lost=1;end
else clock(PP,DRV_PP,v[b],0,s);
seen[b]=(s===1'b1);
end
if(!lost)begin
ext_low=stall_ack_ns;clock(OD,(pp_too_early&&!arb)?DRV_PP:REL,1,rw?0:1,s);ack=(s===1'b0);
end else begin
// A target won the header: an In-Band Interrupt (its address, R), a Hot-Join (7'h02, W) or a
// controller-role request (its address, W) (§5.1.2.2).
req_addr=seen[7:1];req_rw=seen[0];serviced=1;
if(req_rw&&ibi_accept&&known(req_addr))begin
clock(OD,DRV_OD,0,(bcr_of(req_addr)&8'h04)!=0 ? 2 : 0,s);ack=1;ibis++;
if(bcr_of(req_addr)&8'h04)begin read_word(mdb,tbit,0);if(trace)$display("CTRL IBI from %02h accepted, MDB %02h T%0d",req_addr,mdb,tbit);end
else if(trace)$display("CTRL IBI from %02h accepted, no MDB (BCR[2] = 0)",req_addr);
end else if(!req_rw&&req_addr!=7'h02&&mr_accept&&known(req_addr)&&(bcr_of(req_addr)>>6)==2'b01)begin
// A controller-role request from a secondary controller (BCR[7:6] = 01): ACK, then GETACCMST (§5.1.6.3).
clock(OD,DRV_OD,0,0,s);ack=1;mr_seen=1;mr_addr=req_addr;if(trace)$display("CTRL controller-role request from %02h accepted",req_addr);
end else if(!req_rw&&req_addr==7'h02&&hj_accept)begin
clock(OD,DRV_OD,0,0,s);ack=1;hj_seen=1;if(trace)$display("CTRL Hot-Join request accepted");
end else begin
clock(OD,REL,1,0,s);if(trace)$display("CTRL request from %02h %s refused (NACK)",req_addr,req_rw?"R":"W");
end
end
endtask
// S 7'h7E/W, arbitrable. A request that wins is served first; then 7'h7E/W is sent again after Sr.
task automatic open_frame(output bit ack);
start();header(7'h7e,0,1,ack);
if(serviced)begin restart(0);header(7'h7e,0,0,ack);end
endtask
// Broadcast CCC (codes 0x00 to 0x7F): S 7'h7E/W, code, optional byte, P (§5.1.9.1, Figure 21).
task automatic ccc_broadcast(input [7:0] code,input bit has_data,input [7:0] data);
bit ack;open_frame(ack);if(ack)begin write_word(code);if(has_data)write_word(data);end stop();
endtask
// Direct CCC (codes 0x80 to 0xFE): S 7'h7E/W, code, Sr, address/W, optional byte, P (Figure 22).
// directed_as_broadcast: sends the broadcast form instead (code with bit 7 clear, the byte, P).
task automatic ccc_direct(input [7:0] code,input [6:0] addr,input bit has_data,input [7:0] data,output bit ack);
bit a;open_frame(a);
if(directed_as_broadcast)begin write_word(code&8'h7f);if(has_data)write_word(data);ack=a;end
else begin write_word(code);restart(0);header(addr,0,0,ack);if(ack&&has_data)write_word(data);end
stop();
endtask
// Direct GET CCC: S 7'h7E/W, code, Sr, address/R, n bytes into rbuf (the target ends with T = 0).
task automatic ccc_get(input [7:0] code,input [6:0] addr,input int n,output bit ack);
bit a;logic t;logic [7:0] rb;open_frame(a);write_word(code);restart(0);header(addr,1,0,ack);rcount=0;
if(ack)begin t=1;for(int i=0;i<n&&t!==1'b0;i++)begin read_word(rb,t,i<n-1);rbuf[i]=rb;rcount++;end end
stop();
endtask
// Private transfers in the form that starts with 7'h7E (MIPI v1.0 Figure 47): S 7'h7E/W, Sr, address.
task automatic write_regs(input [6:0] da,input [7:0] ptr,input int n,output bit ack);
bit a;open_frame(a);restart(0);header(da,0,0,ack);
if(ack)begin write_word(ptr);for(int i=0;i<n;i++)write_word(wbuf[i]);end
stop();
endtask
task automatic write_reg(input [6:0] da,input [7:0] ptr,input [7:0] d,output bit ack);wbuf[0]=d;write_regs(da,ptr,1,ack);endtask
// Pointer write, Sr, then read until the target ends (T = 0) or n bytes are in rbuf.
task automatic read_regs(input [6:0] da,input [7:0] ptr,input int n,output bit ack);
bit a;logic t;logic [7:0] rb;open_frame(a);restart(0);header(da,0,0,ack);rcount=0;
if(ack)begin write_word(ptr);restart(0);header(da,1,0,ack);
if(ack)begin t=1;for(int i=0;i<n&&t!==1'b0;i++)begin read_word(rb,t,i<n-1);rbuf[i]=rb;rcount++;end end
end
stop();
endtask
task automatic read_reg(input [6:0] da,input [7:0] ptr,output logic [7:0] v);bit ack;rbuf[0]=8'hxx;read_regs(da,ptr,1,ack);v=rbuf[0];endtask
// Legacy I2C message to a static address at Fm timing (MIPI v1.0 Table 57): open drain, ninth bit = ACK.
task automatic i2c_start();
if(stopped>0&&$time-stopped<TBUF)#(TBUF-($time-stopped));
serviced=0;lost=0;sr_done=0;legacy=1;sda_low=1;sda_high=0;#(FM_SETUP);scl_out=0;active=1;
endtask
task automatic i2c_byte(input [7:0] v,output bit ack);logic s;
for(int b=7;b>=0;b--)clock(FM,DRV_OD,v[b],0,s);clock(FM,REL,1,0,s);ack=(s===1'b0);
endtask
task automatic i2c_write(input [6:0] a,input [7:0] ptr,input [7:0] d,output bit ack);
bit k;i2c_start();i2c_byte({a,1'b0},ack);if(ack)begin i2c_byte(ptr,k);i2c_byte(d,k);end stop();
endtask
task automatic i2c_read(input [6:0] a,input [7:0] ptr,output logic [7:0] v);
bit ack;logic s;i2c_start();i2c_byte({a,1'b0},ack);i2c_byte(ptr,ack);
#(FM_HOLD);set_sda(REL,1);#(FM_LOW-FM_HOLD);scl_out=1;#(FM_SETUP);sda_low=1;#(FM_SETUP);scl_out=0;
i2c_byte({a,1'b1},ack);for(int b=7;b>=0;b--)begin clock(FM,REL,1,0,s);v[b]=s;end clock(FM,REL,1,0,s);stop();
endtask
// ENTDAA (§5.1.4.2, Figure 19): broadcast 0x07, then rounds of Sr 7'h7E/R (open drain), 64 arbitrated bits
// (provisional ID, BCR, DCR) from the targets, a 7-bit address plus odd parity from the controller and the
// winner's ACK. A NACKed address is offered once more; a second NACK ends DAA (§5.1.4.2 notes).
task automatic entdaa();
bit ack;open_frame(ack);if(ack)write_word(8'h07);daa_rounds();
endtask
task automatic daa_rounds();
bit ack,a2,retry,done;logic s;reg [63:0] id;reg [7:0] w;
retry=0;done=0;
while(!done)begin
restart(1);header(7'h7e,1,1,ack);
if(!ack)done=1;
else begin
for(int k=63;k>=0;k--)begin clock(OD,REL,1,0,s);id[k]=(s===1'b1);end
w={daa_next,~^daa_next};
for(int k=7;k>=0;k--)clock(OD,DRV_OD,w[k],0,s);
clock(OD,REL,1,0,s);a2=(s===1'b0);
if(a2)begin
daa_addr[daa_count]=daa_next;daa_pid[daa_count]=id[63:16];daa_bcr[daa_count]=id[15:8];daa_dcr[daa_count]=id[7:0];
if(trace)$display("CTRL DAA PID %012h BCR %02h DCR %02h -> %02h",id[63:16],id[15:8],id[7:0],daa_next);
daa_count++;daa_next=next_free(daa_next);retry=0;
end else if(!retry)begin retry=1;if(trace)$display("CTRL DAA %02h NACKed, offering it again",daa_next);end
else begin daa_error=1;done=1;if(trace)$display("CTRL DAA %02h NACKed twice: DAA ends with an error",daa_next);end
end
end
stop();
if(daa_expect&&daa_count<daa_expect&&trace)$display("CTRL DAA assigned %0d of %0d expected addresses",daa_count,daa_expect);
endtask
// Wait for a target's START request on a free bus, then clock it out (§5.1.2.2 condition 2, §5.1.6.2).
// A Hot-Join is answered with ENTDAA in the same frame (§5.1.5 step 4c).
task automatic serve_request(input time limit);
time t0;bit ack;t0=$time;
while(sda!==1'b0&&$time-t0<limit)#10;
if(sda===1'b0)begin
start();header(7'h7e,0,1,ack);
if(hj_seen)begin hj_seen=0;restart(0);header(7'h7e,0,0,ack);if(ack)write_word(8'h07);daa_rounds();end
else if(mr_seen)begin mr_seen=0;getaccmst(mr_addr,1,ack);end
else stop();
end else if(trace)$display("CTRL no request within %0t ns",limit);
endtask
// GETACCMST (0x91, §5.1.9.3.16): a direct GET to a secondary controller, after its request (in the same
// frame, after Sr) or as an offer. An accepted reply is its own address with a parity bit (the bench uses the
// odd parity of §5.1.4.2); then STOP, and this controller releases SCL after tMMoverlap (§5.1.6.3, Figure 44).
// A NACK or a wrong address cancels the handoff (Tables 43, 44).
task automatic getaccmst(input [6:0] a,input bit in_frame,output bit ok);
bit ack;logic [7:0] b;logic t;b=8'hxx;
if(in_frame)begin restart(0);header(7'h7e,0,0,ack);end else open_frame(ack);
write_word(8'h91);restart(0);header(a,1,0,ack);
if(ack)read_word(b,t,0);
ok=ack&&(b==={a,~^a});
stop();
if(ok)begin #(T_MMOVERLAP);scl_en=0;handed_over=1;end
if(trace)begin
if(!ack)$display("CTRL GETACCMST %02h NACKed: mastership kept",a);
else if(ok)$display("CTRL GETACCMST %02h replied %02h: SCL released",a,b);
else $display("CTRL GETACCMST %02h replied %02h, not %02h: mastership kept",a,b,{a,~^a});
end
endtask
// DEFSLVS (0x08, §5.1.9.3.7, Table 30): a count, four bytes for this controller (its address, DCR, BCR, static
// 7'h7E), then four per target (address<<1, DCR, BCR, static<<1); a legacy target has address 0 and its LVR.
task automatic defslvs();
bit ack;open_frame(ack);write_word(8'h08);write_word(daa_count+(leg_addr!=0));
write_word({own_da,1'b0});write_word(8'h00);write_word(8'h40);write_word({7'h7e,1'b0});
for(int i=0;i<daa_count;i++)begin write_word({daa_addr[i],1'b0});write_word(daa_dcr[i]);write_word(daa_bcr[i]);write_word(8'h00);end
if(leg_addr!=0)begin write_word(8'h00);write_word(leg_lvr);write_word(8'h00);write_word({leg_addr,1'b0});end
stop();
endtask
// Learn a target's BCR with GETBCR, for a target given its address by SETAASA or SETDASA (no DAA record).
task automatic learn(input [6:0] a);bit ack;ccc_get(8'h8e,a,1,ack);
if(ack)begin daa_addr[daa_count]=a;daa_bcr[daa_count]=rbuf[0];daa_pid[daa_count]=0;daa_dcr[daa_count]=0;daa_count++;end
endtask
// ENTHDRx (codes 0x20 to 0x27, Table 15). Basic v1.0 has no HDR mode, so nothing is sent in HDR.
task automatic hdr_enter(input [2:0] mode);bit ack;open_frame(ack);write_word(8'h20|mode);endtask
// HDR Exit Pattern (§5.2.1.1, Figure 25): SCL low, SDA falls four times at least tDIG_H apart, then SCL
// rises and SDA rises: a STOP.
task automatic hdr_exit();
#(HOLD);sda_low=0;sda_high=1;#(PP_LOW);
repeat(3)begin sda_low=1;sda_high=0;#(PP_HIGH);sda_low=0;sda_high=1;#(PP_HIGH);end
sda_low=1;sda_high=0;#(PP_HIGH);scl_out=1;#(T_CAS);sda_low=0;sda_high=0;stopped=$time;active=0;#1;
endtask
// HDR Restart Pattern (§5.2.1.2, Figure 26): SCL low, SDA high, then fall, rise, fall, rise at least tDIG_H
// apart; the next edge is SCL rising. The bus stays in HDR: a new HDR command would follow.
task automatic hdr_restart();
#(HOLD);sda_low=0;sda_high=1;#(PP_LOW);
repeat(2)begin sda_low=1;sda_high=0;#(PP_HIGH);sda_low=0;sda_high=1;#(PP_HIGH);end
scl_out=1;#(PP_HIGH);scl_out=0;
endtask
// A stand-in for HDR traffic: no HDR coding, only the property of §5.2.2's introduction that SDA may change
// after either SCL edge. The 16 bits of w go out one per SCL edge, so SDA also changes while SCL is high.
task automatic hdr_burst(input [15:0] w);
for(int b=15;b>=0;b--)begin #(HOLD);sda_low=!w[b];sda_high=w[b];#(PP_LOW-HOLD);scl_out=!scl_out;end
if(scl_out)begin #(PP_LOW);scl_out=0;end
endtask
// Error Type M2 recovery: S 7'h7E/W, ACK or NACK ignored, HDR Exit Pattern, STOP (§5.1.10.2.3, §5.1.10.2.4).
task automatic recover();bit ack;start();header(7'h7e,0,1,ack);hdr_exit();endtask
endmodule
Read included file: i3c_target.sv
`timescale 1ns/1ns
// I3C target (v1.0: Slave), SDR. A register map like the P8 target: mem[i] = i ^ MEM_XOR; a private write's
// first byte is the pointer. It answers 7'h7E and its dynamic address only (§5.1.2.1), takes part in ENTDAA
// while it has no address, raises In-Band Interrupts with a mandatory byte (BCR[2]) and recognises the HDR
// Exit Pattern. HOT_JOIN targets stay silent until hot_join() is called (§5.1.5).
module i3c_target #(parameter [47:0] PID=48'h0,parameter [7:0] BCR=8'h06,DCR=8'h00,parameter [6:0] STATIC_ADDR=7'h00,
parameter [7:0] MEM_XOR=8'h00,parameter HOT_JOIN=0,parameter time T_AVAL=1000,T_IDLE=200000)
(inout wire scl,sda,output reg sda_low=0,sda_high=0);
// Fault switches.
bit pid_tie_bug=0,addr_parity_wrong=0,ibi_without_mdb=0,misses_hdr_exit=0,t_bit_wrong=0,pp_too_early=0,ibi_loses_and_retries_wrong=0,restart_as_exit=0,accmst_wrong_addr=0;
// A secondary controller's target half (BCR[7:6] = 01): controller-role request, DEFSLVS list, GETACCMST.
bit mr_req=0,mr_en=1,accept_mastership=1,accmst_sent=0,became_current=0,my_rw=1;reg [7:0] defs[0:39];int ndefs=0;
// Activity state (ENTASx, §5.1.3.5) and the protocol-error flag GETSTATUS reports (Table 41).
reg [1:0] activity=0;bit perr=0;
// Counters: SDR STARTs acted on, HDR Restart patterns seen while in HDR.
int sdr_starts=0,hdr_restarts=0;bit poss_restart=0;time requested=0,req_gap=0; // when, and how long after the bus went free, it last pulled SDA
// Configuration and state. max_read: bytes returned per private read before T = 0.
int max_read=1;reg [7:0] mdb=8'ha5;
reg [6:0] da=0;bit has_da=0,ibi_en=1,hj_en=1,powered=!HOT_JOIN,ibi_req=0,hj_req=0;
reg [7:0] mem[0:255];reg [7:0] ptr=0;
int st=0,cnt=0,sent=0,errors=0;reg [8:0] sh=0;reg [63:0] daa_word;reg [7:0] tx;reg [7:0] code=0;
bit active=0,after_s=0,arb=0,arb_lost=0,lost_this_frame=0,daa_mode=0,direct=0,code_next=0,need_ptr=1,ccc_target=0,self_start=0;
bit hdr_mode=0,err_exit=0,write_ack_release=0,reading=0,daa_won=0,t_ack=0;int falls=0;reg [6:0] my_hdr;time free_since=0;
bit was_ibi=0,ibi_more=0,ack_pending=0,daa_hdr=0;
// st: 0 idle, 1 header, 2 header ACK, 3 write word, 4 read word, 5 ignore until Sr/P, 6 DAA ID, 7 DAA address, 8 DAA ACK
localparam IDLE=0,HDR=1,HACK=2,WR=3,RD=4,IGN=5,DID=6,DAD=7,DACK=8;
assign sda=sda_low?1'b0:sda_high?1'b1:1'bz;
initial for(int i=0;i<256;i++)mem[i]=i[7:0]^MEM_XOR;
task automatic release_sda();sda_low=0;sda_high=0;endtask
task automatic raise_ibi();ibi_req=1;endtask
task automatic request_mastership();mr_req=1;endtask
task automatic hot_join();powered=1;hj_req=1;free_since=$time;endtask
function automatic bit bad_addr(input [6:0] a);bad_addr=a==7'h3e||a==7'h5e||a==7'h6e||a==7'h76||a==7'h7a||a==7'h7c||a==7'h7f;endfunction
// START or Repeated START.
always @(negedge sda)if(scl===1'b1&&powered&&!hdr_mode&&!err_exit)begin
after_s=!active;sdr_starts++;
if(!active)begin daa_mode=0;direct=0;lost_this_frame=0;end
active=1;st=HDR;cnt=0;sh=0;code_next=0;ccc_target=0;reading=0;was_ibi=0;
// A request rides only on the header after S (§5.1.2.2); ibi_loses_and_retries_wrong also tries after Sr.
arb=0;arb_lost=0;
if(has_da&&ibi_req&&ibi_en&&(after_s||(ibi_loses_and_retries_wrong&&lost_this_frame)))begin arb=1;my_hdr=da;my_rw=1;end
else if(has_da&&mr_req&&mr_en&&after_s)begin arb=1;my_hdr=da;my_rw=0;end
else if(hj_req&&hj_en&&after_s)begin arb=1;my_hdr=7'h02;my_rw=0;end
if(accmst_sent&&!after_s)accmst_sent=0; // Sr after GETACCMST cancels it (§5.1.9.3.16 step 4)
end
// STOP.
always @(posedge sda)if(scl===1'b1&&powered&&!hdr_mode&&!err_exit&&active)begin
if(accmst_sent)begin accmst_sent=0;became_current=1;end
active=0;st=IDLE;daa_mode=0;direct=0;release_sda();free_since=$time;self_start=0;
end
// HDR Exit Pattern detector (§5.2.1.3, Figure 27): SDA falls counted while SCL stays low; SCL high resets.
always @(negedge sda)if((hdr_mode||err_exit)&&scl===1'b0)begin falls++;
if(falls==4&&!misses_hdr_exit)begin hdr_mode=0;err_exit=0;active=1;st=IGN;falls=0;end
end
// Restart (§5.2.1.4): exactly two falls, then an SDA rise, then SCL rises. A Basic target supports no HDR mode,
// so it notes the Restart and stays in HDR until the Exit; restart_as_exit leaves HDR on it (a fault).
always @(posedge sda)if((hdr_mode||err_exit)&&scl===1'b0)poss_restart=(falls==2);
always @(posedge scl)if(hdr_mode||err_exit)begin
if(poss_restart&&hdr_mode)begin hdr_restarts++;if(restart_as_exit)begin hdr_mode=0;active=1;st=IGN;end end
poss_restart=0;falls=0;
end
// Bus Available (tAVAL) or Bus Idle (tIDLE for Hot-Join): pull SDA low to request a START (§5.1.3, §5.1.5).
always #10 if(powered&&!active&&!self_start&&scl===1'b1&&sda===1'b1&&!hdr_mode&&!err_exit)begin
if(has_da&&(ibi_req&&ibi_en||mr_req&&mr_en)&&$time-free_since>=T_AVAL)begin self_start=1;sda_low=1;requested=$time;req_gap=$time-free_since;end
else if(hj_req&&hj_en&&$time-free_since>=T_IDLE)begin self_start=1;sda_low=1;end
end
// Sample on the rising edge.
always @(posedge scl)if(powered&&active&&!hdr_mode&&!err_exit)begin
case(st)
HDR:begin
sh={sh[7:0],sda};cnt++;
if(arb&&!arb_lost&&my_bit(cnt-1)&&sda===1'b0)begin arb_lost=1;lost_this_frame=1;end
if(cnt==8)decode_header();
end
HACK:begin
if(arb&&!arb_lost)begin
// My request won; the controller ACKs or NACKs it.
if(sda===1'b0)begin
if(my_hdr==7'h02)begin hj_req=0;st=IGN;end
else if(!my_rw)begin mr_req=0;st=IGN;end
else begin ibi_req=0;was_ibi=1;
if(BCR[2]&&!ibi_without_mdb)begin #2;sda_low=1;sda_high=0;tx=mdb;st=RD;cnt=0;end else st=IGN;
end
end else st=IGN;
self_start=0;
end else if(daa_hdr)begin daa_hdr=0;st=DID;cnt=0;daa_word={PID,BCR,DCR};end
else if(write_ack_release)begin #2;release_sda();write_ack_release=0;end
end
WR:begin
sh={sh[7:0],sda};cnt++;
if(cnt==9)begin cnt=0;
if(sh[0]!==~^sh[8:1])begin errors++;
perr=1;if(code_next)begin err_exit=1;falls=0;st=IGN;end else st=IGN; // S1: wait for the HDR Exit Pattern; S2: wait for Sr/P
end else if(code_next)begin code_next=0;do_ccc(sh[8:1]);end
else if(ccc_target)begin ccc_data(sh[8:1]);end
else if(need_ptr)begin ptr=sh[8:1];need_ptr=0;end
else begin mem[ptr]=sh[8:1];ptr=ptr+1;end
end
end
RD:begin
cnt++;
if(cnt==9)begin cnt=0;#2;release_sda();
if(ccc_target&&code==8'h91)accmst_sent=1; // GETACCMST answered: mastership passes at the STOP
if(!ibi_more)st=IGN;
end
end
DID:begin
if(!pid_tie_bug&&daa_word[63-cnt]&&sda===1'b0)begin st=IGN;end // lost: withdraw until the next Sr
cnt++;if(cnt==64&&st==DID)begin st=DAD;cnt=0;sh=0;end
end
DAD:begin sh={sh[7:0],sda};cnt++;if(cnt==8)begin st=DACK;daa_won=((^sh[7:0])==1'b1)^addr_parity_wrong;end end
DACK:begin if(daa_won)begin da=sh[7:1];has_da=1;end st=IGN;end
endcase
end
function automatic bit my_bit(input int i);reg [7:0] v;v={my_hdr,my_rw};my_bit=v[7-i];endfunction
task automatic decode_header();
reg [6:0] a;bit rw;a=sh[7:1];rw=sh[0];cnt=0;
if(arb&&!arb_lost)begin st=HACK;end
else if(a==7'h7e&&!rw)begin st=HACK;sda_ack(1);code_next=1;need_ptr=1;end
else if(a==7'h7e&&rw&&daa_mode&&!has_da&&!hj_req)begin st=HACK;sda_ack(1);daa_hdr=1;end // then 64 bits (§5.1.4.2 steps 4 to 8)
else if(a==7'h7e&&rw&&!daa_mode)begin errors++;err_exit=1;falls=0;st=IGN;end // S0: 7'h7E/R outside DAA
else if(daa_mode)begin st=IGN;end // S4 or already assigned
else if(direct&&((has_da&&a==da)||(code==8'h87&&STATIC_ADDR!=0&&a==STATIC_ADDR)))begin st=HACK;sda_ack(1);ccc_target=1;reading=rw;
if(code==8'h86)begin has_da=0;da=0;end
if(code>=8'h82&&code<=8'h85)activity=code-8'h82;
if(code==8'h91&&(BCR[7:6]!=2'b01||!accept_mastership))begin ack_pending=0;st=IGN;ccc_target=0;end end
else if(has_da&&a==da)begin st=HACK;sda_ack(1);reading=rw;need_ptr=!rw;end
else if(bad_addr(a)&&!rw)begin errors++;err_exit=1;falls=0;st=IGN;end // S0: broken 7'h7E/W
else st=IGN;
endtask
task automatic sda_ack(input bit a);ack_pending=a;endtask
// Drive 6 ns after the falling edge, one after the controller's 5 ns hold, so no handoff overlaps a 1 and a 0.
always @(negedge scl)if(powered&&active&&!hdr_mode&&!err_exit)begin
#6;
case(st)
HDR:begin
if(arb&&!arb_lost)begin
if(pp_too_early)begin sda_low=!my_bit(cnt);sda_high=my_bit(cnt);end else begin sda_low=!my_bit(cnt);sda_high=0;end
end else if(arb&&arb_lost)release_sda();
end
HACK:begin
if(arb&&!arb_lost)release_sda();
else if(ack_pending)begin sda_low=1;sda_high=0;ack_pending=0;
// Write: release just after the rise (handoff, §5.1.2.3.1). Read: keep low, then send data.
if(!reading)write_ack_release=1;
end else if(reading)begin
st=RD;cnt=0;ibi_more=0;
if(ccc_target)begin tx=ccc_reply(0);end else begin tx=mem[ptr];ptr=ptr+1;end
sent=0;drive_bit(tx[7]);
end else begin
st=WR;cnt=0;sh=0;
end
end
WR:begin
release_sda();
if(cnt==8&&t_bit_wrong&&!code_next&&!ccc_target)sda_low=1; // the I2C habit: ACK the ninth bit, hold it until SCL falls
end
RD:begin
if(cnt<8)drive_bit(tx[7-cnt]);
else begin
// T-bit: 1 = more data (continue), 0 = end of data.
sent++;ibi_more=was_ibi?0:(ccc_target?ccc_more(sent):sent<max_read);
sda_low=!ibi_more;sda_high=ibi_more;
end
end
DID:begin sda_low=!daa_word[63-cnt];sda_high=0;end
DAD:release_sda();
DACK:begin sda_low=daa_won;sda_high=0;end
IGN:release_sda();
endcase
end
task automatic drive_bit(input bit v);sda_low=!v;sda_high=v;endtask
// After a T = 1 the controller either lets the next byte come or pulls SDA low (Sr = abort).
always @(negedge scl)if(powered&&active&&st==RD&&cnt==0&&ibi_more)begin
if(sda===1'b0)st=IGN;
else begin if(ccc_target)tx=ccc_reply(sent);else begin tx=mem[ptr];ptr=ptr+1;end end
end
task automatic do_ccc(input [7:0] c);
code=c;direct=c[7];
if(c==8'h07)daa_mode=1;
else if(c==8'h06)begin has_da=0;da=0;end
else if(c==8'h29&&STATIC_ADDR!=0&&!has_da)begin da=STATIC_ADDR;has_da=1;end
else if(c>=8'h20&&c<=8'h27)begin hdr_mode=1;falls=0;active=0;st=IDLE;end
else if(c==8'h00||c==8'h01)begin ccc_target=1;end // event byte follows
else if(c>=8'h02&&c<=8'h05)activity=c-8'h02;
else if(c==8'h08&&BCR[7:6]==2'b01)begin ccc_target=1;ndefs=0;end // DEFSLVS: only secondary controllers keep it
endtask
task automatic ccc_data(input [7:0] d);
if(code==8'h00||code==8'h80)begin if(d[0])ibi_en=1;if(d[1])mr_en=1;if(d[3])hj_en=1;end
else if(code==8'h01||code==8'h81)begin if(d[0])ibi_en=0;if(d[1])mr_en=0;if(d[3])hj_en=0;end
else if(code==8'h08)begin if(ndefs<40)defs[ndefs]=d;ndefs++;end
else if(code==8'h87||code==8'h88)begin da=d[7:1];has_da=1;end
endtask
function automatic [7:0] ccc_reply(input int i);
case(code)
8'h8d:ccc_reply=PID>>(8*(5-i));
8'h8e:ccc_reply=BCR;
8'h8f:ccc_reply=DCR;
8'h90:begin ccc_reply=i==0?8'h00:{activity,perr,5'b0};if(i==1)perr=0;end // GETSTATUS MSB, LSB (Table 41)
8'h91:ccc_reply={da^{6'b0,accmst_wrong_addr},~^da};
default:ccc_reply=8'hff;
endcase
endfunction
function automatic bit ccc_more(input int n);ccc_more=(code==8'h8d)?n<6:(code==8'h90)?n<2:0;endfunction
endmodule
Read included file: i3c_hdr_detect.sv
`timescale 1ns/1ns
module i3c_hdr_detect(input wire scl,sda,in_hdr);
bit misses_restart=0,restart_as_exit=0;int restarts=0,exits=0,falls=0;bit poss=0;
always @(negedge sda)if(in_hdr&&scl===1'b0)begin falls++;if(falls==4)exits++;end
always @(posedge sda)if(in_hdr&&scl===1'b0)poss=(falls==2);
always @(posedge scl)begin
if(in_hdr&&poss&&!misses_restart)begin if(restart_as_exit)exits++;else restarts++;end
poss=0;falls=0;
end
endmodule
Read included file: i3c_secondary.sv
`timescale 1ns/1ns
module i3c_secondary #(parameter [47:0] PID=48'h0,parameter [7:0] BCR=8'h46,DCR=8'h00)
(inout wire scl,sda,output wire tgt_low,tgt_high,ctl_low,ctl_high);
bit takes_scl_early=0;
i3c_target #(.PID(PID),.BCR(BCR),.DCR(DCR)) tgt(scl,sda,tgt_low,tgt_high);
i3c_ctrl #(.START_AS_CONTROLLER(0)) ctl(scl,sda,ctl_low,ctl_high);
always @(posedge tgt.accmst_sent)if(takes_scl_early)ctl.scl_en=1;
always @(posedge tgt.became_current)begin
#12;ctl.scl_en=1;ctl.stopped=$time;ctl.daa_count=0;ctl.own_da=tgt.da;
for(int g=5;g+3<tgt.ndefs&&g+3<40;g+=4)if(tgt.defs[g]!=0&&tgt.defs[g][7:1]!=tgt.da)begin
ctl.daa_addr[ctl.daa_count]=tgt.defs[g][7:1];ctl.daa_dcr[ctl.daa_count]=tgt.defs[g+1];ctl.daa_bcr[ctl.daa_count]=tgt.defs[g+2];ctl.daa_count++;
end
end
endmodule
Read included file: i2c_legacy.sv
`timescale 1ns/1ns
module i2c_legacy #(parameter [6:0] ADDRESS=7'h50,parameter time FILTER=50,STRETCH=3000)
(inout wire scl,sda,output wire scl_low,sda_low);
bit no_spike_filter=0,legacy_stretches=0;int starts=0,clocks=0,pulls=0;
wire scl_f,sda_f;assign #(FILTER) scl_f=scl;assign #(FILTER) sda_f=sda;
tri1 lscl,lsda;wire hd;
assign lscl=((no_spike_filter?scl:scl_f)===1'b0)?1'b0:1'bz;
assign lsda=((no_spike_filter?sda:sda_f)===1'b0)?1'b0:1'bz;
i2c_target #(.ADDRESS({3'b000,ADDRESS})) dev(lscl,lsda,scl_low,sda_low,hd);
assign scl=scl_low?1'b0:1'bz;assign sda=sda_low?1'b0:1'bz;
always @(legacy_stretches)dev.stretch_ns=legacy_stretches?STRETCH:0;
always @(negedge lsda)if(lscl===1'b1)starts++;
always @(posedge lscl)clocks++;
always @(posedge sda_low)pulls++;
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: i3c_mon.sv
`timescale 1ns/1ns
// Bus decoder: one line per frame, from SCL and SDA only. The ninth bit is an ACK after an address and a
// T-bit after a data word (§5.1.2.3); frames to the legacy address are decoded as I2C. phase (an output
// the checker reads): 0 idle, 1 header after S, 2 header after Sr, 3 ACK, 4 write word, 5 read word,
// 6 DAA ID, 7 DAA address, 8 DAA ACK, 9 HDR, 10 I2C byte, 11 I2C ACK, 12 waiting for Sr or P.
module i3c_mon #(parameter [6:0] LEGACY=7'h50)(input wire scl,sda,output reg [3:0] phase=0);
bit trace=0,active=0,daa=0,hdr=0,legacy=0,code_next=0;int cnt=0,frames=0,starts=0,exits=0,falls=0;
reg [63:0] sh=0;reg [6:0] addr=0;bit rw=0;string text="";
always @(negedge sda)if(scl===1'b1&&!hdr)begin
if(active)text={text," Sr"};else begin text=" S";daa=0;legacy=0;end
phase=active?2:1;active=1;cnt=0;sh=0;starts++;code_next=0;
end
always @(posedge sda)if(scl===1'b1&&active&&!hdr)begin
text={text," P"};if(trace)$display("%s%s",legacy?"I2C":"I3C",text);text="";active=0;phase=0;frames++;
end
// HDR Exit Pattern: four SDA falls while SCL stays low (§5.2.1.1). Seen in or out of HDR (M2 recovery).
always @(negedge sda)if(scl===1'b0&&(hdr||active))begin falls++;if(falls==4)begin hdr=0;falls=0;text={text," EXIT"};exits++;phase=12;end end
bit poss=0;int restarts=0;
always @(posedge sda)if(scl===1'b0&&hdr)poss=(falls==2);
always @(posedge scl)begin if(hdr&&poss)begin text={text," RESTART"};restarts++;end poss=0;falls=0;end
always @(posedge scl)if(active&&!hdr)begin
sh={sh[62:0],sda};cnt++;
case(phase)
1,2:if(cnt==8)begin addr=sh[7:1];rw=sh[0];cnt=0;
if(phase==1&&addr==LEGACY)legacy=1;
text={text,$sformatf(" %02h %s",addr,rw?"R":"W")};phase=legacy?11:3;end
3:begin cnt=0;text={text,sda===1'b0 ? " A" : sda===1'b1 ? " NA" : " A=x"};
if(sda!==1'b0)phase=12;
else if(daa&&addr==7'h7e&&rw)phase=6;
else if(!rw)begin phase=4;code_next=(addr==7'h7e);end
else phase=5;
end
4,5:if(cnt==9)begin cnt=0;text={text,$sformatf(" %02h T%0d",sh[8:1],sh[0])};
if(phase==4&&code_next)begin code_next=0;
if(sh[8:1]==8'h07)daa=1;
if(sh[8:1]>=8'h20&&sh[8:1]<=8'h27)begin hdr=1;falls=0;phase=9;text={text," HDR"};end
end
if(phase==5&&sh[0]===1'b0)phase=12;
end
6:if(cnt==64)begin cnt=0;text={text,$sformatf(" id %012h bcr %02h dcr %02h",sh[63:16],sh[15:8],sh[7:0])};phase=7;end
7:if(cnt==8)begin cnt=0;text={text,$sformatf(" da %02h par %0d",sh[7:1],sh[0])};phase=8;end
8:begin cnt=0;text={text,sda===1'b0 ? " A" : sda===1'b1 ? " NA" : " A=x"};phase=12;end
10:if(cnt==8)begin cnt=0;text={text,$sformatf(" %02h",sh[7:0])};phase=11;end
11:begin cnt=0;text={text,sda===1'b0 ? " A" : sda===1'b1 ? " NA" : " A=x"};phase=10;end
default:cnt=0;
endcase
end
endmodule
Read included file: i3c_checker.sv
`timescale 1ns/1ns
// Wire rules with the driver view. Index 0 of lows/highs is the controller; the others are targets.
// Each rule prints at most once per frame (S to P) and counts every time it fires.
// no_pp_contention one device drives SDA high while another pulls it low (§5.1.2.3: handoffs are specified)
// od_in_header a device drives SDA high in the arbitrable header after S or its ACK (§5.1.2.2.1)
// pp_in_data the controller sends a 1 in a write word without driving it high (§5.1.2: data is push-pull)
// t_bit the ninth bit of a write word is not XOR(data, 1) (§5.1.2.3.3)
// ibi_mdb_present no target drives the byte after an accepted header after S with R (BCR[2] = 1, §5.1.6.2)
// legacy_no_stretch a legacy target pulls SCL low (MIPI v1.0 Table 56: no clock stretching on I3C)
module i3c_checker #(parameter N=4)(input wire scl,sda,input wire [N-1:0] lows,highs,input wire [3:0] phase,input wire legacy_scl_low);
int broken=0,contention=0,od_header=0,pp_data=0,tbit=0,mdb=0,stretch=0;bit quiet=0; // quiet: count, do not print
reg [5:0] said=0;reg [3:0] ph=0,prev=0;reg [8:0] w=0;int k=0,hk=0,mk=0;bit arb_ack=0,hdr_rw=0,mdb_seen=0,mdb_watch=0,viol=0;
task automatic bad(input int i,input string rule);broken++;if(!said[i]&&!quiet)$display("I3C CHECK %s at %0t",rule,$time);said[i]=1;endtask
always @(negedge sda)if(scl===1'b1)begin k=0;viol=0;end
always @(posedge sda)if(scl===1'b1)said=0;
always @(negedge scl)begin prev=ph;ph=phase;if(ph!=prev)begin k=0;hk=0;end
if(ph!=prev&&ph==3)arb_ack=(prev==1);
if(ph==5&&prev==3&&arb_ack&&hdr_rw)begin mdb_watch=1;mdb_seen=0;mk=0;end
end
always @(lows or highs)begin #1;if(|lows&&|highs)begin contention++;bad(0,"no_pp_contention");end end
always @(posedge scl)begin
if((ph==1||(ph==3&&arb_ack))&&|highs)begin od_header++;bad(1,"od_in_header");end
if(ph==1)begin hk++;if(hk==8)hdr_rw=(sda===1'b1);end
if(ph==4)begin k++;w={w[7:0],sda};
if(k<=8&&sda===1'b1&&!highs[0])viol=1;
if(k==9)begin if(viol)begin pp_data++;bad(2,"pp_in_data");end if(w[0]!==~^w[8:1])begin tbit++;bad(3,"t_bit");end k=0;viol=0;end
end
if(mdb_watch)begin mk++;if(|lows[N-1:1]|||highs[N-1:1])mdb_seen=1;
if(mk==8)begin mdb_watch=0;if(!mdb_seen)begin mdb++;bad(4,"ibi_mdb_present");end end
end
end
always @(posedge legacy_scl_low)begin stretch++;bad(5,"legacy_no_stretch");end
// A missing mandatory byte turns the controller's handoff release into a STOP inside the ACK slot.
always @(posedge sda)if(scl===1'b1&&ph==3&&arb_ack&&hdr_rw)begin mdb++;bad(4,"ibi_mdb_present");end
endmodule
Check Yourself
Remember
- A target interrupts by sending its address with R in the arbitrable header, or by pulling SDA on an available bus.
- The lower address wins a collision, and any interrupt beats the controller's 7E.
- The controller ACKs and reads the mandatory byte, or NACKs; a refused or losing target waits for Bus Available.
- Hot-join uses the reserved address 02 with W and ends in ENTDAA.
- A missing mandatory byte turns the handoff into a STOP on the wire.
Practice
- Send the Mandatory IBI Byte Fix · Medium