From SPI to QSPI: The Flash Command Model

Read a flash command on the wire phase by phase, name the lanes of each phase, and count what a byte and a cache line cost.

Lesson
P7.6
Time
40 min
Runs on
Your browser
Check
5 questions

A boot ROM read its first instruction with Fast Read and got every bit one place late. Its author had counted the dummy phase as seven clocks instead of eight. The controller took the flash's last silent clock as bit 7, and the chip booted into garbage.

One Dummy Clock Too Few on a Fast ReadThe controller sends seven dummy clocks, then samples IO1 on every rising edge. The flash still waits for eight.
0123456789101112131415161718192021222324252627282930313233343536CS_nCLKIO0IO1ctrl_oe00010000flash_oe000000100000first sample: zflash drives D7
  1. Cycle 2

    The last address bits. The controller then releases IO0 and counts its dummy clocks.

  2. Cycle 20

    The controller's seventh dummy clock is over, so this rising edge is its first data sample. The flash is still in its eighth dummy clock: IO1 floats, and bit 7 reads x.

  3. Cycle 21

    Now the flash drives D7. The controller files it as bit 6, and every later bit lands one place to the right.

  4. Cycle 35

    CS rises after the controller's eighth sample. The flash had sent seven bits: its log counts no whole byte.

Measured on the bench with ctrl.dummy_minus_one = 1. The controller logs x0001010 for 0x14; the flash logs QSPI 0b 001014 M -- C47 B0: it never sent bit 0.

A Flash Command Is SPI with a Grammar

A flash read is one SPI frame with fixed fields. CS falls, eight clocks carry the instruction, 24 carry the address, and then the flash answers. It keeps answering, its address counting up by itself, until CS rises.

03h Read Data: One Byte from 0x001014IO0 carries the instruction and the address from the controller; IO1 carries the data from the flash.
commandaddressdata 0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081CS_nCLKIO0IO1IO2IO3dirreleasedcontrollerflashreleased
Read phases and sample points (half clock)
  • command: columns 1 through 16.
  • address: columns 17 through 64.
  • data: columns 65 through 80.
  1. Cycle 1

    CS falls: the frame starts. The controller shifts 0x03 out on IO0, most significant bit first.

  2. Cycle 17

    Twenty-four clocks of address, one bit per clock: 0x001014.

  3. Cycle 65

    With no gap, the flash drives IO1 from the falling edge after the last address bit. It keeps counting up until CS rises.

Generated from the transaction and decoded back; the bench's pins decode to the same frame (QSPI 03 001014 M -- C40 B1).

Read Data (03h) is the plain form, with no gap between the address and the data. It is also the one instruction limited to 50 MHz; the rest of the instruction set runs at up to 133 MHz.

Dummy Clocks Buy Time at Full Speed

Fast Read (0Bh) inserts eight clocks after the address in which nobody needs to drive. The flash uses them to set up the first address, so the whole frame can run at the full clock. A slower clock would cost time on every data byte; dummy clocks cost it once.

0Bh Fast Read: Eight Dummy Clocks Before the DataThe same frame as 03h with eight clocks between the address and the data, in which nobody drives.
commandaddressdummydata 012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697CS_nCLKIO0IO1IO2IO3dirreleasedcontrollerturnaroundflashreleased
Read phases and sample points (half clock)
  • command: columns 1 through 16.
  • address: columns 17 through 64.
  • dummy: columns 65 through 80.
  • data: columns 81 through 96.
  1. Cycle 65

    Eight dummy clocks. The controller has released IO0 and the flash has not started: both lines float.

  2. Cycle 81

    The first data bit, launched on the falling edge after the eighth dummy clock.

Generated and decoded back; the bench measured QSPI 0b 001014 M -- C48 B1.

The Phase Machine Every Flash Controller RunsEach read command walks the same phases; the command decides how many lanes each phase uses and whether the mode and dummy phases exist.
The phase machine every flash controller runsEach read command walks the same phases; the command decides how many lanes each phase uses and whether the mode and dummy phases exist. component Command 8 clocks on IO0 component Address 24 bits on 1, 2 or 4lanes component Mode byte BBh, EBh only: Fxh component Dummy 8, or 4 for EBh component Data until CS rises component CS rises the frame ends all readsBBh, EBh0Bh, 3Bh, 6Bh03hEBhBBh
  1. 03h

    Read Data goes straight from the address to the data. The price is its 50 MHz limit.

  2. 0Bh, 3Bh, 6Bh

    Fast Read and its output-only variants add eight dummy clocks and may run at the full clock.

  3. BBh

    Dual I/O sends a mode byte after the address and starts the data straight after it.

  4. EBh

    Quad I/O sends the mode byte, then four dummy clocks, then the data.

Read the diagram as text
  • Command: 8 clocks on IO0
  • Address: 24 bits on 1, 2 or 4 lanes
  • Mode byte: BBh, EBh only: Fxh
  • Dummy: 8, or 4 for EBh
  • Data: until CS rises
  • CS rises: the frame ends
  • Command → Address: all reads
  • Address → Mode byte: BBh, EBh
  • Address → Dummy: 0Bh, 3Bh, 6Bh
  • Address → Data: 03h
  • Mode byte → Dummy: EBh
  • Mode byte → Data: BBh
  • Dummy → Data
  • Data → CS rises

Arrows are the order of phases inside one CS frame.

The dummy count belongs to the command: eight for 0Bh, 3Bh and 6Bh, four for EBh, none for 03h and BBh. The mode byte is a phase of its own, not part of the dummy count.

Lanes per Phase: 1-1-4 Against 1-4-4

The notation x-y-z counts the lanes for command, address and data. In every command here the instruction stays on IO0. Output commands (3Bh, 6Bh) widen only the data. I/O commands (BBh, EBh) widen the address and the mode byte too.

EBh Fast Read Quad I/O: 1-4-4The command on IO0; the address, the mode byte and the data on all four lanes, a nibble per clock.
commandaddressmodedummydata 0123456789101112131415161718192021222324252627282930313233343536373839404142434445CS_nCLKIO0IO1IO2IO3dirreleasedcontrollerturnaroundflashreleased
Read phases and sample points (half clock)
  • command: columns 1 through 16.
  • address: columns 17 through 28.
  • mode: columns 29 through 32.
  • dummy: columns 33 through 40.
  • data: columns 41 through 44.
  1. Cycle 17

    The address on four lanes: six clocks, A23..A20 first, the top bit of each nibble on IO3.

  2. Cycle 29

    The mode byte F0h, two clocks. This datasheet requires its upper nibble to be F.

  3. Cycle 33

    Four dummy clocks.

  4. Cycle 41

    The byte 0x14 in two clocks: nibble 1 then nibble 4.

Generated and decoded back; the bench measured QSPI eb 001014 M f0 C22 B1.

Data lanesIO0IO1IO2IO3
1 (03h, 0Bh)none: the controller's lineD7 … D0nonenone
2 (3Bh, BBh)D6, D4, D2, D0D7, D5, D3, D1nonenone
4 (6Bh, EBh)D4, D0D5, D1D6, D2D7, D3

Every byte goes high bits first; on two or four lanes the top bit of each group rides the highest IO.

So 1-1-4 (6Bh) spends 32 clocks on the instruction and address and eight on dummies. 1-4-4 (EBh) spends 14 on the instruction and address, two on the mode byte and four on dummies.

Open the 1-2-2 frame (BBh)
BBh Fast Read Dual I/O: 1-2-2
commandaddressmodedata 0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657CS_nCLKIO0IO1IO2IO3dirreleasedcontrollerflashreleased
Read phases and sample points (half clock)
  • command: columns 1 through 16.
  • address: columns 17 through 40.
  • mode: columns 41 through 48.
  • data: columns 49 through 56.

Generated and decoded back; the bench measured QSPI bb 001014 M f0 C28 B1.

BBh has a mode byte and no dummy clocks: the data starts on the clock after the mode byte.

The Clock Bill

Clocks before the data plus clocks per byte decide what a read costs. The flash's trace line counts every clock of the frame in its C field.

CommandLanesClocks before dataPer byteOne byte32 bytes
03h Read Data1-1-18 + 24 = 328C40C288
0Bh Fast Read1-1-18 + 24 + 8 (dummy) = 408C48C296
3Bh Fast Read Dual Output1-1-28 + 24 + 8 (dummy) = 404C44C168
BBh Fast Read Dual I/O1-2-28 + 12 + 4 (mode) = 244C28C152
6Bh Fast Read Quad Output1-1-48 + 24 + 8 (dummy) = 402C42C104
EBh Fast Read Quad I/O1-4-48 + 6 + 2 (mode) + 4 (dummy) = 202C22C84

The last two columns are the C field of the flash's trace line in the example run below. The arithmetic beside them is the instruction table's clock rows (W25Q128JV Table 8.1.2 and 8.1.3).

For one byte, EBh needs 22 clocks against 40 for 03h. For a 32-byte cache line it needs 84 against 288. The dummy clocks look like waste on one byte and vanish into a stream. Lesson P7.9 turns this table into the cost of a cache miss.

Where You Will Meet It

Every boot ROM, every execute-in-place controller and every flash driver implements this phase machine. A bench for one reads the same address with every command, checks the byte and the clock count, and moves the dummy count by one in each direction.

Asked in interviews: the flash command model
  1. What are the phases of a quad flash read?

    Command, address, mode byte, dummy clocks and data, all inside one CS frame. The command decides the lane count of each phase and whether the mode and dummy phases exist.

  2. What does 1-4-4 mean?

    One lane for the command, four for the address (and mode byte), four for the data. EBh is 1-4-4; 6Bh is 1-1-4, with the address on one lane.

  3. Why does Fast Read have dummy clocks?

    They give the flash time to fetch the first byte after the last address bit. The clock stays at full speed for the rest of the frame; 03h has none and is limited to 50 MHz instead.

  4. What does an off-by-one in the dummy count do?

    The controller samples one clock early or late. With one dummy too few, the first sampled bit is undriven and every bit lands one place late; on four lanes, every nibble does.

Try It

The flash prints one line per command when CS rises: the instruction, the address, the mode byte, the clocks it counted and the whole bytes it sent. 03h and 0Bh print the mode byte too, though they never sent one: the field keeps its last value.

Add ctrl.dummy_minus_one = 1; before the loop. 0Bh then counts C47 and its byte reads x0001010 in binary. On EBh the same fault shifts whole nibbles: four bytes read x1415161 instead of 14151617.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: QSPI 03 001014 M -- C40 B1 QSPI 0b 001014 M -- C48 B1 QSPI 3b 001014 M -- C44 B1 QSPI bb 001014 M f0 C28 B1 QSPI 6b 001014 M -- C42 B1 QSPI eb 001014 M f0 C22 B1 QSPI 03 001014 M -- C288 B32 QSPI 0b 001014 M -- C296 B32 QSPI 3b 001014 M -- C168 B32 QSPI bb 001014 M f0 C152 B32 QSPI 6b 001014 M -- C104 B32 QSPI eb 001014 M f0 C84 B32
Read 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

Predict the outputBench defaults: QE set, mode byte F0h, page 0x001000 holds its own low address bytes, flash trace on, the contention checker left out. The controller reads four bytes with EBh. What does the flash's trace line say?
`timescale 1ns/1ns
module tb;
  wire cs_n, clk; tri [3:0] io; wire [3:0] ctrl_oe, flash_oe;
  reg [7:0] value;
  qspi_ctrl    ctrl (cs_n, clk, io, ctrl_oe);    // the controller: task calls drive the pins
  qspi_flash   flash(cs_n, clk, io, flash_oe);   // the W25Q128JV teaching model
  initial begin
    flash.sr2 = 8'h02;                       // QE = 1: quad instructions allowed
    flash.trace = 1;                         // one line per command when CS rises
    for (int i = 0; i < 256; i++) flash.mem_write(24'h001000 + i, i);   // byte = its low address
    #10;
    ctrl.read_begin(8'heb, 24'h001014, 8'hf0);
    repeat (4) ctrl.receive(4, value);
    ctrl.cs_hi();
    $finish;
  end
  initial begin #2000000; $fatal(1, "watchdog"); end
endmodule
Read 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
ConceptDuring a 6Bh Fast Read Quad Output, which bits of each data byte appear on IO0, in order?
Spot the bugEach option reads the byte at 0x001014, which holds 0x14. Which sequence returns the wrong byte?
Read 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
ConceptWhy does Fast Read add dummy clocks instead of asking the controller to run a slower clock?
Read the waveformIn the measured trace of the twin, why does the controller read bit 7 as x?
One Dummy Clock Too Few on a Fast ReadThe controller sends seven dummy clocks, then samples IO1 on every rising edge. The flash still waits for eight.
0123456789101112131415161718192021222324252627282930313233343536CS_nCLKIO0IO1ctrl_oe00010000flash_oe000000100000first sample: zflash drives D7
  1. Cycle 2

    The last address bits. The controller then releases IO0 and counts its dummy clocks.

  2. Cycle 20

    The controller's seventh dummy clock is over, so this rising edge is its first data sample. The flash is still in its eighth dummy clock: IO1 floats, and bit 7 reads x.

  3. Cycle 21

    Now the flash drives D7. The controller files it as bit 6, and every later bit lands one place to the right.

  4. Cycle 35

    CS rises after the controller's eighth sample. The flash had sent seven bits: its log counts no whole byte.

Measured on the bench with ctrl.dummy_minus_one = 1. The controller logs x0001010 for 0x14; the flash logs QSPI 0b 001014 M -- C47 B0: it never sent bit 0.

Remember

  • A flash read is one CS frame: command, address, then an optional mode byte and dummy clocks, then data until CS rises.
  • x-y-z counts the lanes of command, address and data: 6Bh is 1-1-4, EBh is 1-4-4, and the instruction stays on IO0.
  • Dummy clocks give the flash its setup time once per frame, so 0Bh runs at 133 MHz while 03h is limited to 50 MHz.
  • One byte costs 40 clocks with 03h and 22 with EBh; a 32-byte line costs 288 against 84.
  • On two or four lanes the top bit of each group rides the highest IO, high bits first.

Practice