Choosing a Serial Protocol
Compare UART, SPI, QSPI, I2C and I3C on wires, measured speed, reach, devices, duplex, error detection and flow control, then pick a link for four jobs on one SoC and check each pick against a run.
- Lesson
- P5.11
- Time
- 30 min
- Runs on
- Your browser
- Check
- 4 questions
A team put a sensor hub with six sensors on the I2C bus because the power controller was already there. Six interrupt lines then needed six more pins, and polling at Fast-mode ate the time budget of the fastest sensor. I2C was right for the power controller and wrong for the hub.
- Debug console
A human types; speed hardly matters. Two wires and no clock reach any terminal.
- Boot flash
The flash speaks SPI and QSPI, and quad reads refill a cache line in a fraction of the time.
- Power controller
A few registers, written rarely, on two wires shared with other slow parts. Every byte is acknowledged.
- Sensor hub
Many sensors on two wires, faster than I2C, and interrupts without a pin each.
Read the diagram as text
- SoC: four off-chip jobs
- Debug console: UART
- Boot flash: QSPI, quad reads
- Power controller: I2C
- Sensor hub: I3C
- SoC → Debug console: two wires, no clock
- SoC → Boot flash: six wires, fastest
- SoC → Power controller: shared, acknowledged
- SoC → Sensor hub: shared, fast, interrupts
Module P6.1 drew the same SoC from the link side. Here the job comes first.
The Decision Table
Each column is one question this module asked. Read across a row and you have the link; read down a column and you see who answers that question well.
| Link | Wires | Speed, measured | Reach set by | Devices | Duplex | Errors caught | Flow control |
|---|---|---|---|---|---|---|---|
| UART | TX, RX | 10 bit-times a byte: 86.8 µs at 115200 baud | the line driver and cable: no clock to skew | 2 | full | parity: one flipped bit; framing | RTS/CTS |
| SPI | CLK, CS_n, MOSI, MISO, plus a CS_n per device | 8 clocks a byte | the round trip inside half a clock (P7.3) | one per CS_n | full | none | none: the controller paces |
| QSPI | CLK, CS_n, IO0–IO3 | 2 clocks a byte; a 32-byte line in 855 ns | as SPI | one flash per CS_n | half | none | none: poll the busy bit |
| I2C | SCL, SDA | 9 clocks a byte; a register read in 95.1 µs (Fast-mode) | bus capacitance: 400 pF in Fast-mode | 112 7-bit addresses | half | ACK or NACK per byte | clock stretching |
| I3C | SCL, SDA | the same read in 5.7 µs (SDR) | push-pull edges; legacy I2C filters | dynamic addresses | half | parity on written bytes | no stretching; in-band interrupts |
Every speed is measured (next table). Spec limits: UM10204 Table 11 (400 pF), §3.1.12 Table 4 (16 of 128 addresses reserved); MIPI I3C Basic v1.0 §5.1.2.3.3 (parity) and Table 4 (no clock stretch on an I3C bus).
| Number on this page | Value | Measured in |
|---|---|---|
| UART 8N1 byte | 10 bit-times | data/P6-runs.json, p61.predict |
| UART bit-period mismatch, first failure (8N1, back to back; + is a slower transmitter) | -6 % and +4.5 % | data/P6-4.json, thresholds |
| UART parity: one flip, two flips | RX a5 PE, RX a6 ok | data/P6-runs.json, p62.wrongsense, p62.twoflips |
| UART bytes lost with RTS/CTS, without | 0, 8 | data/P6-runs.json, p66.honoured, p66.ignored |
| SPI, QSPI, I2C clocks per byte | 8, 2, 9 | serial-trace.mjs figures, decoded back (as in P6.1) |
| QSPI 32-byte line, 03h and EBh | 288 clocks, 5775 ns; 84 clocks, 855 ns | data/P5-runs.json, p511.example (this page) |
| I2C register read, Fast-mode and Fast-mode Plus | 95100 ns, 38831 ns | data/P9-runs.json p91.i2c; data/P8-runs.json p89.plus |
| I2C read without and with a 3 µs stretch | 96281 ns, 101281 ns | data/P8-runs.json, p85.time |
| I3C register read (SDR) | 5685 ns | data/P9-runs.json, p91.i3c |
| I3C parity and IBI | register 0x10 = 10, target1 parity errors 1; CTRL IBI from 08 accepted, MDB a5 T0 | data/P9-runs.json, p92.tbit, p96.example |
The UART time per byte is the measured bit count divided by the baud rate. The I2C and I3C reads are one register read on the same P9 bench.
Four Jobs on One SoC
Debug console: UART
Two pins, no clock, and a terminal at the far end that only has to agree on the rate. The P6 bench decodes
UART a5 okandRX a5 ok, and back-to-back 8N1 frames first fail at a bit-period mismatch of -6 % and +4.5 % rate error.Boot flash: QSPI with quad reads
The flash speaks SPI and QSPI. A 32-byte line takes 288 clocks and 5775 ns by the plain 03h read, 84 clocks and 855 ns by EBh: the runnable example below.
Power controller: I2C
A few registers, written rarely, on two wires shared with other slow parts. Each byte comes back acknowledged:
I2C S 50 W A 3c A P. A Fast-mode Plus register read takes 38.8 µs, ample for a voltage change.Sensor hub: I3C
Many sensors on the same two wires. One register read takes 5.7 µs against 95.1 µs for I2C Fast-mode on the same bench, and a sensor raises its interrupt in band:
CTRL IBI from 08 accepted, MDB a5 T0.
Nothing here is a ranking. QSPI is the fastest and the worst choice for six sensors; UART is the slowest and the best choice for a console. Pick from the job.
Where It Shows Up
| Lesson | What you meet there |
|---|---|
| P6.1 Serial links in one picture | the same five links from the wire side, with one byte on each |
| P6.6 Flow control, FIFOs and baud divisors | RTS/CTS measured: bytes lost with and without it |
| P7.6 One dummy clock too few on a Fast Read | the read commands that set a flash's byte time |
| P7.9 XIP, continuous read and burst with wrap | cache-line refills straight from the boot flash |
| P8.4 Addressing | the 7-bit address space and its reserved addresses |
| P9.1 I3C in one picture | the I2C and I3C reads timed on one bench |
| P9.6 In-band interrupts and hot-join | interrupts without a pin per sensor |
Every module after this one answers the same questions for one link.
Asked in interviews: choosing a link
Why is a debug console almost always a UART?
Two pins, no clock, and any terminal can talk to it once the rates agree.
Why not put everything on I2C?
It is slow, half duplex and needs a pin per interrupt; good for configuration, poor for bulk data or many fast sensors.
What does I3C keep from I2C?
The two wires and legacy I2C targets on the same bus; it adds push-pull speed, dynamic addresses and in-band interrupts.
Try It
The flash model prints one line per command (QSPI 03 ...), the controller one per line refill (QSPI LINE ... C288), and the program times each refill. The plain read runs on one lane at 50 MHz, the flash's limit for 03h; the quad read uses four lanes at the bench's 100 MHz.
Try ctrl.half_period = 10; before the second refill: EBh at 50 MHz. Then read 64 bytes with your own loop of ctrl.receive(4, value) after ctrl.read_begin(8'heb, 24'h001000, 8'hf0).
QSPI 03 001000 M -- C288 B32 QSPI LINE 001000 C288 03h line: 5775 ns QSPI eb 001000 M f0 C84 B32 QSPI LINE 001000 C84 EBh line: 855 ns 0 wrong bytesRead included file: qspi_flash.sv
`timescale 1ns/1ns
module qspi_flash #(parameter PAGES=64, parameter time TPP=2000,TSE=4000,TBE1=6000,TBE2=8000,TCE=10000,TW=2000,TRES=100,TRST=100,
TSUS=100,TVSL=200,TPUW=5000,TRESET=1000, parameter [63:0] UID=64'hd2_6b_15_0e_a7_3c_49_81)
(input wire cs_n,clk,inout wire [3:0] io,output reg [3:0] oe=0);
// Sparse full 24-bit address space: absent pages read FF. Capacity exhaustion is fatal,
// never address truncation. Icarus 12 does not support associative arrays.
reg [7:0] memory[0:PAGES*256-1]; int tags[0:PAGES-1]; int used=0;
reg [3:0] out=0;
for(genvar g=0;g<4;g++)begin:pad assign io[g]=oe[g]?out[g]:1'bz;end
bit no_wel_check=0,no_page_wrap=0,busy_never_set=0,dummy_minus_one=0,wrap_ignored=0,mode_byte_stale=0,qe_ignored=0,drive_through_turnaround=0,erase_to_zero=0;
bit wel=0,volatile_enable=0,busy=0,asleep=0,reset_enable=0,trace=0;
// Fault twins for P7.11-P7.14 (QSPI_FAULTS_EXT); all 0 is the datasheet behaviour.
bit cmp_ignored=0,lock_ignored=0,wp_ignored=0,suspend_restarts=0,suspend_ban_ignored=0,reset_keeps_volatile=0,lb_ignored=0,wake_on_any=0,no_tpuw=0;
bit has_address=0,has_mode=0;string trace_address,trace_mode;
reg [7:0] sr2=0,sr3=0,nv2=0,nv3=0,wrap_cfg=8'h10,wrap_in=8'h10;
// Write protection (§7.1, §7.1.8-10): SR1 SRP SEC TB BP2-0, SR2 CMP LB3-1 QE SRL, SR3 WPS; the
// individual locks (one per 64 KB block 1-254, one per 4 KB sector of blocks 0 and 255) power up set.
reg [7:0] sr1=0,nv1=0,pending1;int pending_which=0;bit sus=0,lock_blk[0:255],lock_sec[0:31];
// Security registers 1-3 at 001000h, 002000h, 003000h (§8.2.29-31); erased at the start.
reg [7:0] secreg[0:767];
// Hardware /RESET pin of the SOIC-16 and TFBGA parts (§4.6, §6.4); internal pull-up, so 1 when unused.
bit reset_n=1;time reset_fell=0;
// Power-up gate (§6.5, §9.3): power_cycle() starts it. Not powered by default: the bench starts ready.
bit power_gate=0,blocked=0,wp_at_select=1;time powered_at=0;
// In-flight operation, for suspend (§8.2.19-20) and for aborts by reset.
int gen=0,cur_kind=0,susp_kind=0,susp_size=0;time cur_end=0,cur_time=0,susp_left=0,resumed_at=0;bit resumed=0;
reg [23:0] susp_addr;reg [7:0] susp_buf[0:1023];bit susp_touched[0:1023];
initial begin for(int i=0;i<256;i++)lock_blk[i]=1;for(int i=0;i<32;i++)lock_sec[i]=1;for(int i=0;i<768;i++)secreg[i]=8'hff;end
localparam CMD=0,ADDR=1,MODE=2,DUMMY=3,WRITE=4,READ=5,WRAP=6,IGNORE=7;
int state=IGNORE,got=0,width=1,data_width=1,dummies=0,clocks=0,output_bits=0,output_bytes=0,prog_count=0,status_count=0;
reg [31:0] shift=0;reg [7:0] cmd=0,mode=8'hf0,status_in[0:1];reg [23:0] address=0,read_address=0;
reg [7:0] pagebuf[0:1023];bit touched[0:1023];
reg [7:0] pending2,pending3;bit pending_volatile;int pending_kind=0,erase_size=0;reg [23:0] pending_addr;
time operation_time;
function automatic [7:0] mem_read(input reg [23:0] a);
mem_read=8'hff;for(int i=0;i<used;i++)if(tags[i]==(a>>8))mem_read=memory[i*256+a[7:0]];
endfunction
task automatic mem_write(input reg [23:0] a,input reg [7:0] v);
int slot;slot=-1;for(int i=0;i<used;i++)if(tags[i]==(a>>8))slot=i;
if(slot<0)begin if(used==PAGES)$fatal(1,"flash sparse capacity exceeded; raise PAGES");slot=used;used++;tags[slot]=a>>8;for(int j=0;j<256;j++)memory[slot*256+j]=8'hff;end
memory[slot*256+a[7:0]]=v;
endtask
// §7.1.8: the range the BP bits name with CMP = 0, as [lo, hi). SEC = 1 with BP = 110 is not in the
// table; the bench treats it like 10X (32 KB).
function automatic int bp_size();
int bp;bp=sr1[4:2];
if(bp==0)bp_size=0;else if(bp==7)bp_size=16777216;
else if(!sr1[6])bp_size=262144<<(bp-1);else bp_size=bp>=4?32768:4096<<(bp-1);
endfunction
function automatic bit lock_at(input reg [23:0] a);
lock_at=(a[23:16]==0||a[23:16]==255)?lock_sec[(a[23:16]==255?16:0)+a[15:12]]:lock_blk[a[23:16]];
endfunction
// Is any byte of [a, a+n) protected? (§7.1.8 note 3: then the program or erase is ignored.)
function automatic bit is_protected(input reg [23:0] a,input int n);
int size,lo,hi;is_protected=0;
if(sr3[2])begin if(!lock_ignored)for(int x=a;x<a+n;x+=4096)if(lock_at(x))is_protected=1;end
else begin
size=bp_size();lo=sr1[5]?0:16777216-size;hi=lo+size;
if(!sr2[6]||cmp_ignored)is_protected=size>0&&a<hi&&a+n>lo;
else is_protected=!(size>0&&a>=lo&&a+n<=hi);
end
endfunction
function automatic bit secreg_ok(input reg [23:0] a);
secreg_ok=a[23:16]==0&&a[11:8]==0&&a[15:12]>=1&&a[15:12]<=3;
endfunction
function automatic int secreg_index(input reg [23:0] a);secreg_index=(a[15:12]-1)*256+a[7:0];endfunction
function automatic [7:0] read_value();
case(cmd)
8'h05:read_value={sr1[7:2],wel,(busy&&!busy_never_set)};
8'h35:read_value={sus,sr2[6:0]};8'h15:read_value=sr3;
8'h9f:case(output_bytes%3)0:read_value=8'hef;1:read_value=8'h40;default:read_value=8'h18;endcase
8'h90,8'h92,8'h94:read_value=((output_bytes+address[0])%2)==0?8'hef:8'h17;
8'h4b:read_value=UID>>(8*(7-output_bytes%8));
8'h48:read_value=secreg_ok(read_address)?secreg[secreg_index(read_address)]:8'hff;
8'h3d:read_value={7'b0,lock_at(address)};
8'hab:read_value=8'h17;
default:read_value=mem_read(read_address);
endcase
endfunction
task automatic next_address();
int n;n=8<<wrap_cfg[6:5];
if(cmd==8'heb&&!wrap_cfg[4]&&!wrap_ignored)read_address=(read_address&~(n-1))|((read_address+1)&(n-1));
else if(cmd==8'h48)read_address[7:0]=read_address[7:0]+1;
else read_address=read_address+1;
endtask
task automatic decode_command();
got=0;shift=0;width=1;data_width=1;dummies=0;
blocked=!reset_n||(power_gate&&$time-powered_at<TVSL)
||(power_gate&&!no_tpuw&&$time-powered_at<TPUW&&(cmd==8'h06||cmd==8'h02||cmd==8'h32||cmd==8'h20||cmd==8'h52||cmd==8'hd8||cmd==8'hc7||cmd==8'h60||cmd==8'h01||cmd==8'h31||cmd==8'h11))
||(sus&&!suspend_ban_ignored&&susp_kind==2&&(cmd==8'h01||cmd==8'h20||cmd==8'h52||cmd==8'hd8||cmd==8'hc7||cmd==8'h60||cmd==8'h44))
||(sus&&!suspend_ban_ignored&&susp_kind==1&&(cmd==8'h01||cmd==8'h02||cmd==8'h32||cmd==8'h42));
if(asleep&&wake_on_any)asleep=0;
if((busy&&cmd!=8'h05&&cmd!=8'h35&&cmd!=8'h15)||(asleep&&cmd!=8'hab)||blocked)state=IGNORE;
else case(cmd)
8'h05,8'h35,8'h15,8'h9f:state=READ;
8'h03,8'h0b,8'h3b,8'h6b,8'hbb,8'heb,8'h02,8'h32,8'h20,8'h52,8'hd8,8'h90,8'hab,
8'h92,8'h94,8'h42,8'h44,8'h48,8'h36,8'h39,8'h3d:begin
state=ADDR;width=(cmd==8'hbb||cmd==8'h92)?2:(cmd==8'heb||cmd==8'h94)?4:1;
data_width=(cmd==8'h6b||cmd==8'heb||cmd==8'h32||cmd==8'h94)?4:(cmd==8'h3b||cmd==8'hbb||cmd==8'h92)?2:1;
if((cmd==8'h6b||cmd==8'heb||cmd==8'h32||cmd==8'h94)&&!sr2[1]&&!qe_ignored)state=IGNORE;
end
8'h4b:begin state=DUMMY;dummies=32;end
8'h77:begin state=sr2[1]||qe_ignored?DUMMY:IGNORE;dummies=6;width=4;end
8'h01,8'h31,8'h11:begin state=WRITE;width=1;end
default:state=IGNORE;
endcase
endtask
always @(negedge cs_n)begin
state=CMD;got=0;shift=0;cmd=0;clocks=0;output_bits=0;output_bytes=0;prog_count=0;status_count=0;width=1;oe=0;has_address=0;has_mode=0;blocked=0;
if(!busy)for(int i=0;i<1024;i++)touched[i]=0;
end
always @(posedge clk)if(!cs_n)begin
clocks++;
if(state==CMD||state==ADDR||state==MODE||state==WRITE||state==WRAP)begin
shift=(shift<<width)|({28'b0,io}&((1<<width)-1));got+=width;
if(state==CMD&&got==8)begin cmd=shift[7:0];decode_command();end
else if(state==ADDR&&got==24)begin
address=shift[23:0];read_address=shift[23:0];has_address=1;got=0;shift=0;
case(cmd)
8'hbb,8'heb,8'h92,8'h94:state=MODE;
8'h0b,8'h3b,8'h6b,8'h48:begin state=DUMMY;dummies=8-(dummy_minus_one?1:0);end
8'h02,8'h32,8'h42:begin state=WRITE;width=data_width;end
8'h20,8'h52,8'hd8,8'h44,8'h36,8'h39:state=IGNORE;
default:state=READ;
endcase
end else if(state==MODE&&got==8)begin
mode=shift[7:0];has_mode=1;got=0;shift=0;
// Rev F figures 22a/24a require Fxh; other mode values are outside this model.
if((mode&8'hf0)!=8'hf0||mode_byte_stale)state=IGNORE;
else if(cmd==8'heb||cmd==8'h94)begin state=DUMMY;dummies=4-(dummy_minus_one?1:0);end else state=READ;
end else if(state==WRAP&&got==8)begin wrap_in=shift[7:0];state=IGNORE;got=0;end
else if(state==WRITE&&got==8)begin
if(cmd==8'h02||cmd==8'h32||cmd==8'h42)begin
if(no_page_wrap)begin if(address[7:0]+prog_count>=1024)$fatal(1,"program fixture too large");pagebuf[address[7:0]+prog_count]=shift[7:0];touched[address[7:0]+prog_count]=1;end
else begin pagebuf[(address[7:0]+prog_count)%256]=shift[7:0];touched[(address[7:0]+prog_count)%256]=1;end
prog_count++;
end else begin if(status_count<2)status_in[status_count]=shift[7:0];status_count++;end
got=0;shift=0;
end
end else if(state==DUMMY)begin
dummies--;if(dummies==0)begin state=cmd==8'h77 ?WRAP:READ;got=0;shift=0;end
end
end
always @(negedge clk)if(!cs_n)begin
if(state==READ)begin
oe=data_width==4?4'hf:data_width==2?4'h3:4'h2;
if(data_width==1)out={2'b0,((read_value()>>(7-output_bits))&1'b1),1'b0};
else out=(read_value()>>(8-data_width-output_bits))&((1<<data_width)-1);
output_bits+=data_width;if(output_bits==8)begin output_bits=0;output_bytes++;next_address();end
end else if(drive_through_turnaround&&(state==ADDR||state==MODE))begin oe=4'hf;out=0;end
else oe=0;
end
always @(posedge cs_n)begin
oe=0;
if(trace&&clocks>=8)begin
if(has_address)trace_address=$sformatf("%06h",address);else trace_address="------";
if(has_mode)trace_mode=$sformatf("%02h",mode);else trace_mode="--";
$display("QSPI %02h %s M %s C%0d B%0d",cmd,trace_address,trace_mode,clocks,output_bytes);
end
// Suspend is the one instruction besides the status reads that BUSY lets through (§7.1.1, §8.2.19).
if(clocks==8&&cmd==8'h75&&!blocked&&!asleep)try_suspend();
if(clocks==8&&cmd==8'h7a&&!blocked&&!asleep&&!busy&&sus)resume();
if(!busy&&clocks>=8&&!blocked)begin
if(cmd!=8'h66&&cmd!=8'h99)reset_enable=0;
if(!asleep||cmd==8'hab)begin
if(cmd==8'h77&&clocks==16&&(sr2[1]||qe_ignored))wrap_cfg=wrap_in;
if(clocks==8)case(cmd)
8'h06:wel=1;8'h04:wel=0;8'h50:volatile_enable=1;8'hb9:asleep=1;
8'h66:reset_enable=1;
8'h7e,8'h98:if(wel)for(int i=0;i<256;i++)begin lock_blk[i]=cmd==8'h7e;if(i<32)lock_sec[i]=cmd==8'h7e;end
8'h99:if(reset_enable)begin pending_kind=4;operation_time=TRST;busy=1;start_op();reset_enable=0;end
default:;
endcase
// tRES1/tRES2 apply only when ABh releases power-down (§8.2.22); an ID read while awake needs no wait.
if(cmd==8'hab&&asleep)begin asleep=0;pending_kind=5;operation_time=TRES;busy=1;start_op();end
if((cmd==8'h02||cmd==8'h32)&&(wel||no_wel_check)&&prog_count>0&&got==0&&state==WRITE
&&!is_protected({address[23:8],8'h0},256)&&!in_suspended({address[23:8],8'h0},256))begin pending_kind=1;pending_addr={address[23:8],8'h0};operation_time=TPP;busy=1;start_op();end
if((wel||no_wel_check)&&((clocks==32&&(cmd==8'h20||cmd==8'h52||cmd==8'hd8))||(clocks==8&&(cmd==8'hc7||cmd==8'h60))))begin
erase_size=cmd==8'h20 ?4096:cmd==8'h52 ?32768:cmd==8'hd8 ?65536:16777216;
pending_addr=address&~(erase_size-1);
if(!is_protected(pending_addr,erase_size)&&!in_suspended(pending_addr,erase_size))begin
pending_kind=2;operation_time=cmd==8'h20 ?TSE:cmd==8'h52 ?TBE1:cmd==8'hd8 ?TBE2:TCE;busy=1;start_op();
end
end
// Security registers (§8.2.29, §8.2.30): WEL, a valid register address, its LB bit clear.
if(cmd==8'h42&&wel&&prog_count>0&&got==0&&state==WRITE&&secreg_ok(address)&&(!sr2[2+address[13:12]]||lb_ignored))begin
pending_kind=6;pending_addr={address[23:8],8'h0};operation_time=TPP;busy=1;start_op();
end
if(cmd==8'h44&&wel&&clocks==32&&secreg_ok(address)&&(!sr2[2+address[13:12]]||lb_ignored))begin
pending_kind=7;pending_addr={address[23:8],8'h0};operation_time=TSE;busy=1;start_op();
end
// Individual locks (§8.2.32, §8.2.33): WEL, then the address. §7.1.2 does not list them among the
// instructions that clear WEL, so the bench leaves WEL set.
if((cmd==8'h36||cmd==8'h39)&&wel&&clocks==32)begin
if(address[23:16]==0||address[23:16]==255)lock_sec[(address[23:16]==255?16:0)+address[15:12]]=cmd==8'h36;
else lock_blk[address[23:16]]=cmd==8'h36;
end
// Write Status (§8.2.5): SRL = 1 locks it; SRP = 1 with /WP low locks it in Standard/Dual SPI
// (§7.1.1 table, §4.3). /WP is IO2, read at CS falling and rising. Writable bits only; LB3-1 and
// SRL never go from 1 to 0. Each register's non-volatile copy changes only when it is written.
if((cmd==8'h01||cmd==8'h31||cmd==8'h11)&&(wel||volatile_enable)&&got==0&&(status_count==1||(cmd==8'h01&&status_count==2))
&&!sr2[0]&&!(sr1[7]&&!sr2[1]&&!wp_ignored&&(wp_at_select===1'b0||io[2]===1'b0)))begin
pending1=sr1;pending2=sr2;pending3=sr3;pending_which=0;
if(cmd==8'h01)begin pending1=(sr1&8'h03)|(status_in[0]&8'hfc);pending_which=1;end
if(cmd==8'h31)begin pending2=status_in[0];pending_which|=2;end
if(cmd==8'h01&&status_count==2)begin pending2=status_in[1];pending_which|=2;end
if(pending_which&2)pending2=(sr2&8'h84)|(pending2&8'h7b)|(sr2&8'h39);
if(cmd==8'h11)begin pending3=(sr3&8'h9b)|(status_in[0]&8'h64);pending_which=4;end
pending_volatile=volatile_enable;pending_kind=3;operation_time=volatile_enable?1:TW;
if(!volatile_enable)busy=1;start_op();volatile_enable=0;
end
end
end
end
always @(negedge cs_n)wp_at_select=io[2]!==1'b0;
// One process per internal operation. Suspend, reset and power cycles bump gen, so an aborted
// operation's process ends without completing.
task start_op();cur_kind=pending_kind;cur_end=$time+operation_time;cur_time=operation_time;fork run_op(gen,operation_time,pending_kind);join_none endtask
task automatic run_op(input int g,input time t,input int kind);
#(t);
if(g==gen)begin
case(kind)
1:for(int i=0;i<1024;i++)if(touched[i])mem_write(pending_addr+i,mem_read(pending_addr+i)&pagebuf[i]);
2:for(int p=0;p<used;p++)if(((tags[p]<<8)&~(erase_size-1))==pending_addr)for(int j=0;j<256;j++)memory[p*256+j]=erase_to_zero?8'h00:8'hff;
3:begin sr1=pending1;sr2=pending2;sr3=pending3;
if(!pending_volatile)begin if(pending_which&1)nv1=sr1;if(pending_which&2)nv2=sr2;if(pending_which&4)nv3=sr3;end end
4:volatile_reset();
6:for(int i=0;i<256;i++)if(touched[i])secreg[secreg_index(pending_addr)+i]&=pagebuf[i];
7:for(int i=0;i<256;i++)secreg[secreg_index(pending_addr)+i]=8'hff;
default:;
endcase
if(kind!=5&&kind!=8&&!(kind==3&&pending_volatile))wel=0;
busy=0;cur_kind=0;
end
endtask
// Software reset, /RESET and power-up all return the volatile state to its power-on values (§8.2.37).
task automatic volatile_reset();
if(!reset_keeps_volatile)begin sr1=nv1;sr2=nv2;sr3=nv3;end wel=0;volatile_enable=0;wrap_cfg=8'h10;asleep=0;
sus=0;susp_kind=0;resumed=0;for(int i=0;i<256;i++)lock_blk[i]=1;for(int i=0;i<32;i++)lock_sec[i]=1;
endtask
function automatic bit in_suspended(input reg [23:0] a,input int n);
int lo,size;size=susp_kind==1?256:susp_size;lo=susp_addr;
in_suspended=sus&&a<lo+size&&a+n>lo;
endfunction
// §8.2.19: only a sector or block erase, or a page program, can be suspended; SUS must be 0 and BUSY 1.
// A suspend sooner than TSUS after a resume breaks the datasheet's rule; the bench ignores it.
task automatic try_suspend();
if(!sus&&busy&&(cur_kind==1||(cur_kind==2&&erase_size<16777216))&&!(resumed&&$time-resumed_at<TSUS))begin
gen++;susp_left=suspend_restarts?cur_time:cur_end-$time;susp_kind=cur_kind;susp_addr=pending_addr;susp_size=erase_size;
for(int i=0;i<1024;i++)begin susp_buf[i]=pagebuf[i];susp_touched[i]=touched[i];end
sus=1;pending_kind=8;operation_time=TSUS;start_op();
end
endtask
// §8.2.20: SUS clears at once, BUSY sets, the operation finishes the time it had left.
task automatic resume();
sus=0;resumed=1;resumed_at=$time;pending_addr=susp_addr;erase_size=susp_size;
for(int i=0;i<1024;i++)begin pagebuf[i]=susp_buf[i];touched[i]=susp_touched[i];end
pending_kind=susp_kind;operation_time=susp_left;susp_kind=0;busy=1;start_op();
endtask
// /RESET held low at least TRESET (§4.6, §6.4) ends any operation and resets the device; while it is
// low, frames are ignored. A shorter pulse is ignored (the datasheet only recommends the minimum).
always @(negedge reset_n)reset_fell=$time;
always @(posedge reset_n)if($time-reset_fell>=TRESET)begin gen++;busy=0;cur_kind=0;reset_enable=0;volatile_reset();end
// Power-up (§6.5, §9.3): reset state, then no instruction until TVSL and no write until TPUW. SRL
// returns to 0 (§7.1.1 note 1); suspended work is lost (§8.2.19).
task automatic power_cycle();
gen++;busy=0;cur_kind=0;reset_enable=0;nv2[0]=0;volatile_reset();power_gate=1;powered_at=$time;
endtask
endmodule
Read included file: qspi_ctrl.sv
`timescale 1ns/1ns
// Task-based pin sequencer, not synthesizable RTL; problems can replace it with an FSM.
module qspi_ctrl(output reg cs_n=1,clk=0,inout wire [3:0] io,output reg [3:0] oe=0);
reg [3:0] out=0;int half_period=5,clocks=0,polls=0;reg [7:0] line[0:31];
bit cs_too_early=0,dummy_minus_one=0,drive_through_turnaround=0,wrap_ignored=0,mode_byte_stale=0,no_wel_before_program=0;
// Receiver intent: high on clocks whose input is consumed as data, not dummy.
// A passive wire observer cannot distinguish these two released-bus phases.
bit sample_data=0;
// W25Q128JV Rev F §9.6: 03h <= 50 MHz; /CS high >= 50 ns after writes.
bit fast_read=0,short_cs_high=0;
int frame_half_period=5;reg [7:0] frame_command=0;
time cs_rose=0;bit frame_seen=0;
function automatic bit write_frame(input reg [7:0] c);
return c==8'h02||c==8'h32||c==8'h20||c==8'h52||c==8'hd8||c==8'h60||c==8'hc7||c==8'h01||c==8'h31||c==8'h11||c==8'h42||c==8'h44;
endfunction
for(genvar g=0;g<4;g++)begin:pad assign io[g]=oe[g]?out[g]:1'bz;end
task automatic cs_lo();
int gap;sample_data=0;clk=0;#(half_period);
gap=write_frame(frame_command)&&!short_cs_high?50:10;
if(frame_seen&&!short_cs_high&&$time-cs_rose<gap)#(gap-($time-cs_rose));
cs_n=0;clocks=0;frame_half_period=half_period;
endtask
task automatic cs_hi();sample_data=0;#(half_period);cs_n=1;cs_rose=$time;frame_seen=1;oe=0;#(half_period);endtask
task automatic send(input reg [31:0] value,input int nbits,input int width);
if(clocks==0)begin
frame_command=value[7:0];
if(frame_command==8'h03&&!fast_read&&half_period<10)frame_half_period=10;
end
sample_data=0;oe=(1<<width)-1;
for(int b=nbits-width;b>=0;b-=width)begin
clk=0;out=(value>>b)&((1<<width)-1);#(frame_half_period);clk=1;clocks++;#(frame_half_period);
end
endtask
task automatic dummy(input int n);sample_data=0;oe=0;repeat(n)begin clk=0;#(frame_half_period);clk=1;clocks++;#(frame_half_period);end endtask
task automatic receive(input int width,output reg [7:0] value);
sample_data=1;value=0;oe=drive_through_turnaround?4'hf:0;out=0;
for(int b=0;b<8/width;b++)begin clk=0;#(frame_half_period);clk=1;clocks++;#1;value=(value<<width)|(width==1?{7'b0,io[1]}:({4'b0,io}&((1<<width)-1)));#(frame_half_period-1);end
endtask
task automatic command(input reg [7:0] c);cs_lo();send(c,8,1);cs_hi();endtask
task automatic status(input reg [7:0] c,output reg [7:0] v);cs_lo();send(c,8,1);receive(1,v);cs_hi();endtask
task automatic wait_ready();reg [7:0] v;polls=0;v=1;while(v[0]&&polls<200)begin status(8'h05,v);polls++;end if(v[0])$fatal(1,"BUSY timeout");endtask
task automatic read_begin(input reg [7:0] c,input reg [23:0] a,input reg [7:0] m);
int w,n;w=c==8'heb ?4:c==8'hbb ?2:1;n=c==8'heb ?4:(c==8'h0b||c==8'h3b||c==8'h6b)?8:0;
cs_lo();send(c,8,1);send(a,24,w);
if(c==8'heb||c==8'hbb)send(mode_byte_stale?8'h00:m,8,w);
if(n>0)dummy(n-(dummy_minus_one?1:0));
endtask
task automatic fetch_line(input reg [23:0] a,input bit quad);
reg [7:0] v;int w;w=quad?4:1;read_begin(quad?8'heb:8'h03,wrap_ignored?{a[23:5],5'b0}:a,8'hf0);
for(int i=0;i<32;i++)begin if(cs_too_early&&i==31)cs_hi();receive(w,v);line[i]=v;end
cs_hi();$display("QSPI LINE %06h C%0d",a,clocks);
endtask
task automatic program_byte(input reg [23:0] a,input reg [7:0] v);
if(!no_wel_before_program)command(8'h06);cs_lo();send(8'h02,8,1);send(a,24,1);send(v,8,1);cs_hi();wait_ready();
endtask
task automatic set_wrap(input int length);
reg [7:0] w;
case(length)0:w=8'h10;8:w=0;16:w=8'h20;32:w=8'h40;64:w=8'h60;default:$fatal(1,"wrap length must be 0/8/16/32/64");endcase
cs_lo();send(8'h77,8,1);dummy(6);send(w,8,4);cs_hi();
endtask
// Teaching pattern changes each 256-byte lap, so page-buffer wrap is visible.
function automatic [7:0] program_pattern(input reg [7:0] pattern,input int i);
program_pattern=((pattern+(i&255))&255)^((i>>8)*8'h5a);
endfunction
task automatic program_bytes(input reg [23:0] a,input int n,input reg [7:0] pattern);
if(n<1||n>768)$fatal(1,"program fixture length 1..768");
if(!no_wel_before_program)command(8'h06);cs_lo();send(8'h02,8,1);send(a,24,1);
for(int i=0;i<n;i++)send(program_pattern(pattern,i),8,1);cs_hi();wait_ready();
endtask
endmodule
Read included file: qspi_checker.sv
`timescale 1ns/1ns
module qspi_checker(input wire cs_n,clk,input wire [3:0] ctrl_oe,flash_oe);
int broken=0,clocks=0;bit collided=0;
always @(posedge clk)begin if(!cs_n)begin clocks++;if((ctrl_oe&flash_oe)!=0)begin broken++;collided=1;$display("QSPI CHECK contention mask=%b",ctrl_oe&flash_oe);end end end
always @(posedge cs_n)begin #1;if(flash_oe!=0)begin broken++;$display("QSPI CHECK flash drives deselected");end end
endmodule
Read included file: qspi_rules.sv
`timescale 1ns/1ns
module qspi_rules #(parameter CHECK_WEL=1)(input wire cs_n,clk,input wire [3:0] io,ctrl_oe,flash_oe,input wire sample_data);
int broken=0,clocks=0,preamble=8,dummies=0,data_width=1;
reg [7:0] command=0;bit read_command=0,first_sample=0,write_enable=0,opened=0;
task automatic bad(input string rule);broken++;$display("QSPI RULE %s",rule);endtask
always @(negedge cs_n)begin
clocks=0;command=0;preamble=8;dummies=0;data_width=1;read_command=0;first_sample=0;opened=1;
end
always @(posedge clk)if(!cs_n)begin
clocks++;
assert((ctrl_oe&flash_oe)==0)else bad("contention");
if(clocks<=8)begin
command={command[6:0],io[0]};
if(clocks==8)begin
case(command)
8'h03:begin read_command=1;preamble=32;end
8'h0b,8'h3b,8'h6b:begin read_command=1;preamble=32;dummies=8;end
8'hbb:begin read_command=1;preamble=24;end
8'heb:begin read_command=1;preamble=16;dummies=4;end
8'h05,8'h35,8'h15,8'h9f:read_command=1;
8'h90,8'hab:begin read_command=1;preamble=32;end
8'h02,8'h32,8'h20,8'h52,8'hd8:preamble=32;
default:;
endcase
if(command==8'h32)data_width=4;
if(CHECK_WEL&&(command==8'h02||command==8'h32||command==8'h20||command==8'h52||command==8'hd8||command==8'h60||command==8'hc7))begin
assert(write_enable)else bad("wel_before_program");
end
end
end
if(sample_data)begin
assert(read_command)else bad("read_command");
if(!first_sample)begin
assert(clocks==preamble+dummies+1)else bad("dummy_count");
first_sample=1;
end
assert(ctrl_oe==0)else bad("turnaround");
end
end
always @(posedge cs_n)if(opened)begin
if(command==8'h02||command==8'h32||command==8'h01||command==8'h31||command==8'h11)begin
assert(clocks>=preamble+8/data_width&&(clocks-preamble)%(8/data_width)==0)else bad("byte_boundary");
end
if(command==8'h20||command==8'h52||command==8'hd8)begin assert(clocks==32)else bad("byte_boundary");end
if(command==8'h60||command==8'hc7)begin assert(clocks==8)else bad("byte_boundary");end
if(command==8'h06&&clocks==8)write_enable=1;
if((command==8'h04||command==8'h99)&&clocks==8)write_enable=0;
if((command==8'h02||command==8'h32)&&clocks>=preamble+8/data_width&&(clocks-preamble)%(8/data_width)==0)write_enable=0;
if((command==8'h01||command==8'h31||command==8'h11)&&clocks>=16&&clocks%8==0)write_enable=0;
if((command==8'h20||command==8'h52||command==8'hd8)&&clocks==32)write_enable=0;
if((command==8'h60||command==8'hc7)&&clocks==8)write_enable=0;
#1;assert(flash_oe==0)else bad("miso_released");
end
endmodule
Check Yourself
Remember
- Choose from the job: wires, speed, reach, number of devices, duplex, error detection and flow control.
- A UART byte is 10 bit-times with no clock wire; it suits consoles, not bulk data.
- Quad reads refill a 32-byte flash line in 84 clocks against 288 for the plain read.
- I2C shares two wires among many slow parts and acknowledges every byte; I3C keeps the wires, runs faster and adds in-band interrupts.
- Every speed in a comparison should come from a measurement on one bench, never from a table of top rates.