Quad I/O on the Wire: Four Bidirectional Pins

Say who drives IO0 to IO3 at every clock of a quad read, where the lanes turn around, and what QE changes on the pins.

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

A quad flash controller worked at 50 MHz and failed at 100 MHz, with hot flash pins. In the data phase its output enables stayed on for one clock while the flash began to drive. Nobody had drawn who drives IO0 to IO3 at each clock.

The Controller Still Drives When the Flash StartsEBh at 0x001014 with ctrl.drive_through_turnaround = 1: the controller turns its enables back on for the data phase.
01234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950CS_nCLKIO0IO1IO2IO3ctrl_oe000000011111000011110000flash_oe000011110000turnaroundboth drive
  1. Cycle 17

    Address and mode: the controller drives all four lanes, the flash listens.

  2. Cycle 33

    Four dummy clocks with both enables off. This is where the lanes change hands.

  3. Cycle 41

    The flash starts driving the data, and the controller drives too. Where their bits disagree the lane is x.

Measured on the bench. The checker prints 4 lines of contention mask=1111, one per data clock; every lane where the two drivers disagree is x.

Who Drives Each Clock

In quad mode every IO pin carries data both ways, so each one has a driver at the controller and another at the flash. Exactly one may be on at a time. The phase of the command decides which.

Four Shared Lanes, Two Drivers, One RuleEach IO pin has a driver at both ends. The phase decides which end may switch its driver on.
Four shared lanes, two drivers, one ruleEach IO pin has a driver at both ends. The phase decides which end may switch its driver on. component Controller ctrl_oe[3:0] bus IO0 to IO3 four bidirectionallanes component Flash flash_oe[3:0] component Contentionchecker ctrl_oe & flash_oe command, address,modedatactrl_oeflash_oe
  1. Controller phases

    Command, address and mode byte: only the controller drives.

  2. Turnaround

    Dummy clocks: neither side drives. The lanes float until the flash starts.

  3. Flash phase

    Data: only the flash drives.

  4. The checker

    Any clock where both enables share a 1 is contention, printed with the lanes it hit.

Read the diagram as text
  • Controller: ctrl_oe[3:0]
  • IO0 to IO3: four bidirectional lanes
  • Flash: flash_oe[3:0]
  • Contention checker: ctrl_oe & flash_oe
  • Controller → IO0 to IO3: command, address, mode
  • Flash → IO0 to IO3: data
  • Controller → Contention checker: ctrl_oe
  • Flash → Contention checker: flash_oe

Arrows are drivers onto the lanes and the enables the checker reads.

Who Drives IO0 to IO3 During EBhThe controller drives through the command, address and mode byte. Nobody drives the four dummy clocks. The flash drives the data.
commandaddressmodedummydata 012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849CS_nCLKIO0IO1IO2IO3dirreleasedcontrollerturnaroundflashreleasedctrl_oe0000000111110000flash_oe000011110000
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 48.
  1. Cycle 1

    The instruction goes out on IO0 alone: ctrl_oe is 0001.

  2. Cycle 17

    From the address on, the controller drives all four lanes: ctrl_oe is 1111.

  3. Cycle 33

    The turnaround. Both enables are 0 and the lanes float. The controller has let go; the flash has not started.

  4. Cycle 41

    The flash drives from the falling edge after the last dummy clock: flash_oe is 1111.

Generated from the transaction and decoded back. The two enable lanes never have a 1 in the same position: the bench's clean run prints no contention line.

The datasheet's rule is one sentence: the IO pins must be high-impedance before the falling edge of the first data-out clock. The dummy clocks are where the controller lets go.

Four Lanes, a Nibble per Clock

On four lanes each clock carries a nibble. The high nibble goes first, and the top bit of each nibble rides IO3. The address, the mode byte and the data all follow the same map.

ClockIO3IO2IO1IO0
9 (address)A23 = 0A22 = 0A21 = 0A20 = 0
10 (address)A19 = 0A18 = 0A17 = 0A16 = 0
11 (address)A15 = 0A14 = 0A13 = 0A12 = 1
12 (address)A11 = 0A10 = 0A9 = 0A8 = 0
13 (address)A7 = 0A6 = 0A5 = 0A4 = 1
14 (address)A3 = 0A2 = 1A1 = 0A0 = 0
15 (mode)M7 = 1M6 = 1M5 = 1M4 = 1
16 (mode)M3 = 0M2 = 0M1 = 0M0 = 0
21 (data, 0x14)D7 = 0D6 = 0D5 = 0D4 = 1
22 (data, 0x14)D3 = 0D2 = 1D1 = 0D0 = 0

High nibble first, the top bit of each nibble on IO3. The address 0x001014 takes clocks 9 to 14.

A controller that wires the lanes the other way round reverses every nibble. It reads 0x14 as 0x82, and no wire rule complains, because every lane was driven at the right time.

When Both Sides Drive

Contention shows as x in simulation wherever the two drivers disagree; on a board it is a mid-rail voltage and a hot pin. The checker does not wait for an x: it flags every clock where both enables are on.

RunFirst clashClocksLanesController read
EBh, cleannone0none14 15
EBh, controller drives into the dataclock 2141111XX XX
EBh, flash drives during address and modeclock 981111ff ff
BBh, controller drives into the dataclock 2540011XX

Measured in the example below. Clocks count from 1 at the first instruction clock; X marks a nibble with an unknown bit.

EBh has four dummy clocks, so a slow release still has room. BBh has none: the data starts on the clock after the mode byte, and the controller must let go on that edge. The flash may drive as early as 1.5 ns after it (tCLQX), so a late release is a fight.

QE Turns Two Pins into Data Lanes

IO2 and IO3 exist only while the Quad Enable bit (QE, status register 2 bit 1) is 1. With QE clear, the pins are /WP and /HOLD, and the flash ignores every quad instruction.

PinStandard and dual SPIQuad SPI (QE = 1)
DI / IO0data into the flashIO0, both directions
DO / IO1data out of the flashIO1, both directions
/WP / IO2write protect inputIO2, both directions
/HOLD or /RESET / IO3hold or reset inputIO3, both directions

Quad instructions use all four pins, so the hold and write-protect functions are gone while QE is 1 (W25Q128JV §3.3 notes, §4.2, §4.4).

The example clears QE: the flash stays silent and the read returns xx. Firmware sets QE with 06h then 31h, polls BUSY, and reads status register 2 back as 02. QE is non-volatile, and its factory value depends on the part number, so a driver reads it before its first quad read.

A Proof That the Enables Never Overlap

A simulation shows one run. A bounded proof checks every input sequence up to its depth. For one EBh command it shows that the controller's enable is 0 on every data clock, the flash's is 0 outside the data phase, and the two never share a 1.

RunTaskResultTime
The two enables, one EBh commandbmc, depth 64PASS672 ms
Controller releases one clock latebmc, depth 64FAIL at step 22520 ms
Reachability: the command completescover, depth 64reached at step 29498 ms

SymbiYosys on the site's formal runner (data/P7-7-measured.json). The model is a clocked abstraction of one EBh command, not the event-driven bench.

The failing run is the useful one: the counterexample names the clock where the late release overlaps the flash's first data clock. Module F2 of the Formal track explains bounded proofs.

Where You Will Meet It

Every quad and octal flash controller, and every bidirectional bus, has this turnaround. A bench for one traces both enables, flags any clock where they overlap, and runs the commands with no dummy clocks, where the margin is smallest.

Asked in interviews: quad I/O and turnaround
  1. Who drives IO0 to IO3 during a quad I/O read?

    The controller during the command, address and mode byte; nobody during the dummy clocks; the flash during the data. The pins must be released before the falling edge of the first data clock.

  2. What happens if the controller releases late?

    Both drivers are on during the first data clocks. Simulation shows x where they disagree; silicon shows a mid-rail level, wrong bits and heat.

  3. Why does /HOLD stop working in quad mode?

    With QE set the /HOLD pin is IO3, a data lane. One pin cannot be a control input and a data lane at once.

  4. Which way round are the nibble lanes?

    High nibble first, and the top bit of each nibble on IO3, for the address, the mode byte and the data alike.

Try It

The probe beside the bench counts the clocks of each frame and prints where the first clash happened, how many clocks it lasted and which lanes it hit. The checker prints one line per clashing clock.

The flash twin corrupts the address itself: the flash pulls the lanes low while the controller sends the address, so the flash decodes a different address and returns FFh. Try flash.qe_ignored = 1; with QE clear: the flash answers anyway, a lenient model that would hide a controller that forgot QE.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: clean: 14 15 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 clash from clock 21, 4 clocks, mask 1111 controller twin: XX XX QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 QSPI CHECK contention mask=1111 clash from clock 9, 8 clocks, mask 1111 flash twin: ff ff QSPI CHECK contention mask=0011 QSPI CHECK contention mask=0011 QSPI CHECK contention mask=0011 QSPI CHECK contention mask=0011 clash from clock 25, 4 clocks, mask 0011 BBh controller twin: XX QE clear: xx SR2 = 02 QE set: 14
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, but QE is clear (flash.sr2 = 0) and the flash trace is off. The controller reads one byte at 0x001014 with EBh and prints it with %h. What does it print?
`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
  qspi_checker ck   (cs_n, clk, ctrl_oe, flash_oe); // prints QSPI CHECK when both drive a lane
  initial begin
    flash.sr2 = 8'h02;                       // QE = 1
    for (int i = 0; i < 256; i++) flash.mem_write(24'h001000 + i, i);   // byte = its low address
    #10;
    flash.sr2 = 0;
    ctrl.read_begin(8'heb, 24'h001014, 8'hf0);
    ctrl.receive(4, value);
    ctrl.cs_hi();
    $display("%h", value);
    $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
Predict the outputBench defaults, controller twin drive_through_turnaround, EBh with two data bytes, the contention probe on and the checker left out. Clocks count from 1 at the first instruction clock. What does the probe print?
`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
  int clk_in_frame = 0, first_clash = 0, clashes = 0; reg [3:0] clash_mask = 0;
  always @(negedge cs_n) begin clk_in_frame = 0; first_clash = 0; clashes = 0; end
  always @(posedge clk) if (!cs_n) begin
    clk_in_frame++;
    if (ctrl_oe & flash_oe) begin
      clashes++;
      if (!first_clash) begin first_clash = clk_in_frame; clash_mask = ctrl_oe & flash_oe; end
    end
  end
  always @(posedge cs_n) if (clashes) $display("clash from clock %0d, %0d clocks, mask %b", first_clash, clashes, clash_mask);
  initial begin
    flash.sr2 = 8'h02;                       // QE = 1
    for (int i = 0; i < 256; i++) flash.mem_write(24'h001000 + i, i);   // byte = its low address
    #10;
    ctrl.drive_through_turnaround = 1;
    ctrl.read_begin(8'heb, 24'h001014, 8'hf0);
    ctrl.receive(4, value); 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
Spot the bugAfter the dummy clocks, each option assembles the data byte from the four pins, one nibble per clock. The byte at 0x001014 is 0x14. Which option reads 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 is the /HOLD function unavailable while QE is set?
Read the waveformIn the driver-view figure of EBh, during which phase are both ctrl_oe and flash_oe 0000?
Who Drives IO0 to IO3 During EBhThe controller drives through the command, address and mode byte. Nobody drives the four dummy clocks. The flash drives the data.
commandaddressmodedummydata 012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849CS_nCLKIO0IO1IO2IO3dirreleasedcontrollerturnaroundflashreleasedctrl_oe0000000111110000flash_oe000011110000
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 48.
  1. Cycle 1

    The instruction goes out on IO0 alone: ctrl_oe is 0001.

  2. Cycle 17

    From the address on, the controller drives all four lanes: ctrl_oe is 1111.

  3. Cycle 33

    The turnaround. Both enables are 0 and the lanes float. The controller has let go; the flash has not started.

  4. Cycle 41

    The flash drives from the falling edge after the last dummy clock: flash_oe is 1111.

Generated from the transaction and decoded back. The two enable lanes never have a 1 in the same position: the bench's clean run prints no contention line.

Remember

  • In a quad read the controller drives the command, address and mode byte, nobody drives the dummy clocks, and the flash drives the data.
  • The IO pins must be released before the falling edge of the first data clock; BBh has no dummy clocks, so its margin is smallest.
  • Four lanes carry a nibble per clock, high nibble first, with the top bit of each nibble on IO3.
  • QE turns /WP and /HOLD into IO2 and IO3; with QE clear the flash ignores quad instructions and the lanes float.
  • Contention is any clock where both enables share a lane: x in simulation, heat in silicon, and a bounded proof can rule it out.

Practice