Program, Erase and the Status Register

Drive a program and an erase correctly with WEL and BUSY, explain the 256-byte page wrap, and say what the flash does with bits it cannot set.

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

A firmware updater wrote 300 bytes in one go, because a bit more than 256 should be fine. The flash wrapped at the page end and overwrote the first 44 bytes with the last 44. The image header was replaced by its tail, the checksum failed, and the updater retried forever.

300 Bytes into a 256-byte PageThe flash counts the address inside the page only. Byte 256 lands on offset 00 and overwrites byte 0.
300 bytes into a 256-byte pageThe flash counts the address inside the page only. Byte 256 lands on offset 00 and overwrites byte 0. component Bytes 0 to 43 the header component Bytes 44 to 255 the body component Bytes 256 to299 the tail component Offsets 00 to 2B end up holding the tail component Offsets 2C to FF hold the body component Next page untouched: FFh firstwraps, overwrites
  1. First pass

    Bytes 0 to 255 fill the page from offset 00.

  2. The wrap

    Byte 256 goes back to offset 00. The 44 tail bytes replace the 44 header bytes; the next page never sees them.

Read the diagram as text
  • Bytes 0 to 43: the header
  • Bytes 44 to 255: the body
  • Bytes 256 to 299: the tail
  • Offsets 00 to 2B: end up holding the tail
  • Offsets 2C to FF: hold the body
  • Next page: untouched: FFh
  • Bytes 0 to 43 → Offsets 00 to 2B: first
  • Bytes 44 to 255 → Offsets 2C to FF
  • Bytes 256 to 299 → Offsets 00 to 2B: wraps, overwrites

Arrows are where the payload bytes land. Measured on the bench: offset 00 reads 50h, the first tail byte.

Two Latches Guard Every Write

WEL must be set by 06h right before every program, erase or status write, and the flash clears it when that operation ends. BUSY is 1 from CS rising on the operation until the internal cycle is over. Meanwhile the flash ignores every instruction except the status reads.

One Program, Frame by FrameOne column pair per CS frame: the frame, then the gap after it. WEL and BUSY are the flash's latches after each frame.
01234567891011121314151617181920212223242526272829CS_ncommand06020503WELBUSY
  1. Cycle 0

    06h, Write Enable: WEL goes to 1.

  2. Cycle 2

    02h with the address and one byte. When CS rises the internal program starts: BUSY goes to 1.

  3. Cycle 4

    The controller polls 05h. While BUSY is 1 the flash ignores everything but the status reads.

  4. Cycle 26

    The program is over: BUSY and WEL are both 0. The next write needs a new 06h.

  5. Cycle 28

    Now the flash accepts a read.

Measured on the bench: program_byte(24'h002000, 8'h5a), then a 03h read that returns 5a. 12 Read Status frames find BUSY set until the compressed tPP of 2 µs is over.

So the controller's loop is 06h, the operation, then 05h until BUSY reads 0. Without 06h the flash ignores the program: one poll finds BUSY clear and the byte still reads ff.

The Status Registers

Status register 1 holds the two latches. Status register 2 holds QE, which P7.7 used. Instructions 05h, 35h and 15h read the three registers, and 05h works in the middle of a program.

BitMeaningSet byCleared byRead with
S0 BUSYan internal program, erase or status write is runningCS rising on that instructionthe end of the operation05h, at any time
S1 WELthe next write is allowed06h Write Enable04h, and the end of every program, erase or status write05h
S9 QEquad instructions allowed; IO2 and IO3 replace /WP and /HOLD06h then 31h (or 01h)the same, with the bit 035h

The bench models these three and the rest of the status bits; P7.11 covers the protection bits, P7.12 the SUS bit.

A Read Status frame repeats its byte until CS rises, so one long frame polls without reselecting. In the bench a program took 12 separate 05h frames, or 25 status bytes in one frame.

A Page Is 256 Bytes, and the Address Wraps

Page Program writes 1 to 256 bytes into one page. The flash counts the address inside the page: past offset FFh it returns to offset 00h, and later bytes replace earlier ones. Nothing ever reaches the next page.

AddressPayload byte that lands thereRead backWith flash.no_page_wrap
0x003000byte 0 (A0h), then byte 256 (50h)50a0
0x00302Bbyte 43 (ABh), then byte 299 (5Bh)5bab, never overwritten
0x00302Cbyte 44 (ACh) onlyacac
0x003100none: the next pageff50

Measured: one 02h frame of 300 bytes from 0x003000. The flash keeps the last 256 bytes it was sent, at the page offsets they wrapped to. The twin spills into the next page instead.

An updater splits its image into page-sized programs, each starting on a page or ending before the page does. CS must also rise after the eighth bit of the last byte, or the whole program is dropped.

Programming Clears Bits; Erasing Sets Them

The datasheet promises a program only into erased bytes (FFh). The bench model ANDs the new data into the old: 5Ah programmed with 0Fh reads 0a. Treat programming over data as unspecified, and erase first.

SequenceRead back
program 5Ah into FFh5a
then program 0Fh over it0a
then erase the sectorff
erase with flash.erase_to_zero00

Measured at 0x002000. The second row is the model's AND, not a datasheet promise.

Erase is the only way back to FFh, in 4 KB, 32 KB or 64 KB blocks or the whole chip. A sector erase took 23 polls and read back ff. The twin flash.erase_to_zero reads 00 instead.

OperationDatasheet typical / maximumBench model
Page program (tPP)0.4 ms / 3 ms2 µs
Sector erase, 4 KB (tSE)45 ms / 400 ms4 µs
Block erase, 64 KB (tBE2)150 ms / 2,000 ms8 µs
Chip erase (tCE)40 s / 200 s10 µs
Write status register (tW)10 ms / 15 ms2 µs

The bench compresses every internal operation so a lab runs in microseconds. Poll counts on this page are for the compressed times.

A real program lasts milliseconds and an erase up to seconds: a thousand times longer than the command that starts it. After such a frame CS must also stay high for 50 ns (tSHSL2) before the next one.

Where You Will Meet It

Every flash driver, bootloader and updater runs this sequence, and every flash model in a verification library must refuse a program without WEL. The twin flash.no_wel_check accepts one: the byte reads 5a, and a controller that forgot 06h passes.

Asked in interviews: program, erase and status
  1. What must precede every program or erase?

    A Write Enable (06h), which sets WEL. The flash clears WEL when the operation ends, so every write needs a fresh 06h.

  2. How does firmware know a program has finished?

    It reads status register 1 with 05h until BUSY (bit 0) is 0. While BUSY is 1 the flash ignores all but the status reads.

  3. What happens if you program 300 bytes from the start of a page?

    The address wraps inside the page: bytes 256 to 299 overwrite bytes 0 to 43, and the next page is untouched.

  4. Can you program a 1 over a 0?

    No. Programming can only clear bits; only an erase returns them to 1, and it works on whole sectors or blocks.

Try It

The probe counts every 05h frame. The helper tasks above the stimulus use only the controller's tasks: peek reads a byte with 03h, erase_sector sends 06h and 20h and polls, program_300 sends one 02h frame of 300 bytes.

Try the twins one at a time. flash.busy_never_set = 1 makes the status say ready while the program runs: wait_ready returns after one poll, and a read straight after it returns xx, then 5a once the program is over.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: 1. Program one byte: 06h, 02h, then poll 05h polls 12 002000 holds 5a 2. The same without 06h polls 1 002001 holds ff 3. Program 0x0f over 0x5a: bits only clear 002000 holds 0a 4. Erase the 4 KB sector polls 23 002000 holds ff 5. 300 bytes in one frame from 0x003000 003000 holds 50 00302b holds 5b 00302c holds ac 003100 holds ff 6. JEDEC ID (9Fh) ef 40 18 7. Reset (66h, 99h) clears WEL and the wrap setting before: WEL 1, wrap 40 after: WEL 0, wrap 10
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 with every page erased. program_300 sends one 02h frame from 0x003000: bytes 0 to 255 are A0h + (i & 15), bytes 256 to 299 are 50h + (i & 15). The bench prints the bytes at 0x003000 and 0x00302C. 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
  reg [7:0] first;
  task automatic program_300(input [23:0] a);      // one 02h frame of 300 bytes:
    ctrl.command(8'h06);                           // bytes 0-255 are A0h + (i & 15),
    ctrl.cs_lo(); ctrl.send(8'h02, 8, 1); ctrl.send(a, 24, 1);   // bytes 256-299 are 50h + (i & 15)
    for (int i = 0; i < 300; i++) ctrl.send(i < 256 ? 8'ha0 + (i & 15) : 8'h50 + (i & 15), 8, 1);
    ctrl.cs_hi(); ctrl.wait_ready();
  endtask
  initial begin
    flash.sr2 = 8'h02;                       // QE = 1; every page starts erased (FFh)
    #10;
    program_300(24'h003000);
    ctrl.read_begin(8'h03, 24'h003000, 8'hf0); ctrl.receive(1, value); ctrl.cs_hi();
    first = value;
    ctrl.read_begin(8'h03, 24'h00302c, 8'hf0); ctrl.receive(1, value); ctrl.cs_hi();
    $display("%h %h", first, 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. program_byte is told to skip its 06h, then sends 02h with 5Ah to 0x002000 and polls 05h until BUSY clears. The bench prints the byte read back and the number of polls. 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
  int polls = 0;
  always @(posedge cs_n) if (flash.cmd == 8'h05) polls++;
  initial begin
    flash.sr2 = 8'h02;                       // QE = 1; every page starts erased (FFh)
    #10;
    ctrl.no_wel_before_program = 1;     // program_byte skips its 06h
    ctrl.program_byte(24'h002000, 8'h5a);
    ctrl.read_begin(8'h03, 24'h002000, 8'hf0); ctrl.receive(1, value); ctrl.cs_hi();
    $display("%h %0d", value, polls);
    $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 bugThe sector at 0x002000 holds data. Each option tries to erase it. Which one leaves the data in place?
Read included file: qspi_flash.sv
`timescale 1ns/1ns
module qspi_flash #(parameter PAGES=64, parameter time TPP=2000,TSE=4000,TBE1=6000,TBE2=8000,TCE=10000,TW=2000,TRES=100,TRST=100,
  TSUS=100,TVSL=200,TPUW=5000,TRESET=1000, parameter [63:0] UID=64'hd2_6b_15_0e_a7_3c_49_81)
 (input wire cs_n,clk,inout wire [3:0] io,output reg [3:0] oe=0);
 // Sparse full 24-bit address space: absent pages read FF. Capacity exhaustion is fatal,
 // never address truncation. Icarus 12 does not support associative arrays.
 reg [7:0] memory[0:PAGES*256-1]; int tags[0:PAGES-1]; int used=0;
 reg [3:0] out=0;
 for(genvar g=0;g<4;g++)begin:pad assign io[g]=oe[g]?out[g]:1'bz;end
 bit no_wel_check=0,no_page_wrap=0,busy_never_set=0,dummy_minus_one=0,wrap_ignored=0,mode_byte_stale=0,qe_ignored=0,drive_through_turnaround=0,erase_to_zero=0;
 bit wel=0,volatile_enable=0,busy=0,asleep=0,reset_enable=0,trace=0;
 // Fault twins for P7.11-P7.14 (QSPI_FAULTS_EXT); all 0 is the datasheet behaviour.
 bit cmp_ignored=0,lock_ignored=0,wp_ignored=0,suspend_restarts=0,suspend_ban_ignored=0,reset_keeps_volatile=0,lb_ignored=0,wake_on_any=0,no_tpuw=0;
 bit has_address=0,has_mode=0;string trace_address,trace_mode;
 reg [7:0] sr2=0,sr3=0,nv2=0,nv3=0,wrap_cfg=8'h10,wrap_in=8'h10;
 // Write protection (§7.1, §7.1.8-10): SR1 SRP SEC TB BP2-0, SR2 CMP LB3-1 QE SRL, SR3 WPS; the
 // individual locks (one per 64 KB block 1-254, one per 4 KB sector of blocks 0 and 255) power up set.
 reg [7:0] sr1=0,nv1=0,pending1;int pending_which=0;bit sus=0,lock_blk[0:255],lock_sec[0:31];
 // Security registers 1-3 at 001000h, 002000h, 003000h (§8.2.29-31); erased at the start.
 reg [7:0] secreg[0:767];
 // Hardware /RESET pin of the SOIC-16 and TFBGA parts (§4.6, §6.4); internal pull-up, so 1 when unused.
 bit reset_n=1;time reset_fell=0;
 // Power-up gate (§6.5, §9.3): power_cycle() starts it. Not powered by default: the bench starts ready.
 bit power_gate=0,blocked=0,wp_at_select=1;time powered_at=0;
 // In-flight operation, for suspend (§8.2.19-20) and for aborts by reset.
 int gen=0,cur_kind=0,susp_kind=0,susp_size=0;time cur_end=0,cur_time=0,susp_left=0,resumed_at=0;bit resumed=0;
 reg [23:0] susp_addr;reg [7:0] susp_buf[0:1023];bit susp_touched[0:1023];
 initial begin for(int i=0;i<256;i++)lock_blk[i]=1;for(int i=0;i<32;i++)lock_sec[i]=1;for(int i=0;i<768;i++)secreg[i]=8'hff;end
 localparam CMD=0,ADDR=1,MODE=2,DUMMY=3,WRITE=4,READ=5,WRAP=6,IGNORE=7;
 int state=IGNORE,got=0,width=1,data_width=1,dummies=0,clocks=0,output_bits=0,output_bytes=0,prog_count=0,status_count=0;
 reg [31:0] shift=0;reg [7:0] cmd=0,mode=8'hf0,status_in[0:1];reg [23:0] address=0,read_address=0;
 reg [7:0] pagebuf[0:1023];bit touched[0:1023];
 reg [7:0] pending2,pending3;bit pending_volatile;int pending_kind=0,erase_size=0;reg [23:0] pending_addr;
 time operation_time;
 function automatic [7:0] mem_read(input reg [23:0] a);
   mem_read=8'hff;for(int i=0;i<used;i++)if(tags[i]==(a>>8))mem_read=memory[i*256+a[7:0]];
 endfunction
 task automatic mem_write(input reg [23:0] a,input reg [7:0] v);
   int slot;slot=-1;for(int i=0;i<used;i++)if(tags[i]==(a>>8))slot=i;
   if(slot<0)begin if(used==PAGES)$fatal(1,"flash sparse capacity exceeded; raise PAGES");slot=used;used++;tags[slot]=a>>8;for(int j=0;j<256;j++)memory[slot*256+j]=8'hff;end
   memory[slot*256+a[7:0]]=v;
 endtask
 // §7.1.8: the range the BP bits name with CMP = 0, as [lo, hi). SEC = 1 with BP = 110 is not in the
 // table; the bench treats it like 10X (32 KB).
 function automatic int bp_size();
  int bp;bp=sr1[4:2];
  if(bp==0)bp_size=0;else if(bp==7)bp_size=16777216;
  else if(!sr1[6])bp_size=262144<<(bp-1);else bp_size=bp>=4?32768:4096<<(bp-1);
 endfunction
 function automatic bit lock_at(input reg [23:0] a);
  lock_at=(a[23:16]==0||a[23:16]==255)?lock_sec[(a[23:16]==255?16:0)+a[15:12]]:lock_blk[a[23:16]];
 endfunction
 // Is any byte of [a, a+n) protected? (§7.1.8 note 3: then the program or erase is ignored.)
 function automatic bit is_protected(input reg [23:0] a,input int n);
  int size,lo,hi;is_protected=0;
  if(sr3[2])begin if(!lock_ignored)for(int x=a;x<a+n;x+=4096)if(lock_at(x))is_protected=1;end
  else begin
   size=bp_size();lo=sr1[5]?0:16777216-size;hi=lo+size;
   if(!sr2[6]||cmp_ignored)is_protected=size>0&&a<hi&&a+n>lo;
   else is_protected=!(size>0&&a>=lo&&a+n<=hi);
  end
 endfunction
 function automatic bit secreg_ok(input reg [23:0] a);
  secreg_ok=a[23:16]==0&&a[11:8]==0&&a[15:12]>=1&&a[15:12]<=3;
 endfunction
 function automatic int secreg_index(input reg [23:0] a);secreg_index=(a[15:12]-1)*256+a[7:0];endfunction
 function automatic [7:0] read_value();
  case(cmd)
   8'h05:read_value={sr1[7:2],wel,(busy&&!busy_never_set)};
   8'h35:read_value={sus,sr2[6:0]};8'h15:read_value=sr3;
   8'h9f:case(output_bytes%3)0:read_value=8'hef;1:read_value=8'h40;default:read_value=8'h18;endcase
   8'h90,8'h92,8'h94:read_value=((output_bytes+address[0])%2)==0?8'hef:8'h17;
   8'h4b:read_value=UID>>(8*(7-output_bytes%8));
   8'h48:read_value=secreg_ok(read_address)?secreg[secreg_index(read_address)]:8'hff;
   8'h3d:read_value={7'b0,lock_at(address)};
   8'hab:read_value=8'h17;
   default:read_value=mem_read(read_address);
  endcase
 endfunction
 task automatic next_address();
  int n;n=8<<wrap_cfg[6:5];
  if(cmd==8'heb&&!wrap_cfg[4]&&!wrap_ignored)read_address=(read_address&~(n-1))|((read_address+1)&(n-1));
  else if(cmd==8'h48)read_address[7:0]=read_address[7:0]+1;
  else read_address=read_address+1;
 endtask
 task automatic decode_command();
   got=0;shift=0;width=1;data_width=1;dummies=0;
   blocked=!reset_n||(power_gate&&$time-powered_at<TVSL)
    ||(power_gate&&!no_tpuw&&$time-powered_at<TPUW&&(cmd==8'h06||cmd==8'h02||cmd==8'h32||cmd==8'h20||cmd==8'h52||cmd==8'hd8||cmd==8'hc7||cmd==8'h60||cmd==8'h01||cmd==8'h31||cmd==8'h11))
    ||(sus&&!suspend_ban_ignored&&susp_kind==2&&(cmd==8'h01||cmd==8'h20||cmd==8'h52||cmd==8'hd8||cmd==8'hc7||cmd==8'h60||cmd==8'h44))
    ||(sus&&!suspend_ban_ignored&&susp_kind==1&&(cmd==8'h01||cmd==8'h02||cmd==8'h32||cmd==8'h42));
   if(asleep&&wake_on_any)asleep=0;
   if((busy&&cmd!=8'h05&&cmd!=8'h35&&cmd!=8'h15)||(asleep&&cmd!=8'hab)||blocked)state=IGNORE;
   else case(cmd)
    8'h05,8'h35,8'h15,8'h9f:state=READ;
    8'h03,8'h0b,8'h3b,8'h6b,8'hbb,8'heb,8'h02,8'h32,8'h20,8'h52,8'hd8,8'h90,8'hab,
    8'h92,8'h94,8'h42,8'h44,8'h48,8'h36,8'h39,8'h3d:begin
      state=ADDR;width=(cmd==8'hbb||cmd==8'h92)?2:(cmd==8'heb||cmd==8'h94)?4:1;
      data_width=(cmd==8'h6b||cmd==8'heb||cmd==8'h32||cmd==8'h94)?4:(cmd==8'h3b||cmd==8'hbb||cmd==8'h92)?2:1;
      if((cmd==8'h6b||cmd==8'heb||cmd==8'h32||cmd==8'h94)&&!sr2[1]&&!qe_ignored)state=IGNORE;
    end
    8'h4b:begin state=DUMMY;dummies=32;end
    8'h77:begin state=sr2[1]||qe_ignored?DUMMY:IGNORE;dummies=6;width=4;end
    8'h01,8'h31,8'h11:begin state=WRITE;width=1;end
    default:state=IGNORE;
   endcase
 endtask
 always @(negedge cs_n)begin
   state=CMD;got=0;shift=0;cmd=0;clocks=0;output_bits=0;output_bytes=0;prog_count=0;status_count=0;width=1;oe=0;has_address=0;has_mode=0;blocked=0;
   if(!busy)for(int i=0;i<1024;i++)touched[i]=0;
 end
 always @(posedge clk)if(!cs_n)begin
  clocks++;
  if(state==CMD||state==ADDR||state==MODE||state==WRITE||state==WRAP)begin
   shift=(shift<<width)|({28'b0,io}&((1<<width)-1));got+=width;
   if(state==CMD&&got==8)begin cmd=shift[7:0];decode_command();end
   else if(state==ADDR&&got==24)begin
    address=shift[23:0];read_address=shift[23:0];has_address=1;got=0;shift=0;
    case(cmd)
     8'hbb,8'heb,8'h92,8'h94:state=MODE;
     8'h0b,8'h3b,8'h6b,8'h48:begin state=DUMMY;dummies=8-(dummy_minus_one?1:0);end
     8'h02,8'h32,8'h42:begin state=WRITE;width=data_width;end
     8'h20,8'h52,8'hd8,8'h44,8'h36,8'h39:state=IGNORE;
     default:state=READ;
    endcase
   end else if(state==MODE&&got==8)begin
     mode=shift[7:0];has_mode=1;got=0;shift=0;
     // Rev F figures 22a/24a require Fxh; other mode values are outside this model.
     if((mode&8'hf0)!=8'hf0||mode_byte_stale)state=IGNORE;
     else if(cmd==8'heb||cmd==8'h94)begin state=DUMMY;dummies=4-(dummy_minus_one?1:0);end else state=READ;
   end else if(state==WRAP&&got==8)begin wrap_in=shift[7:0];state=IGNORE;got=0;end
   else if(state==WRITE&&got==8)begin
     if(cmd==8'h02||cmd==8'h32||cmd==8'h42)begin
       if(no_page_wrap)begin if(address[7:0]+prog_count>=1024)$fatal(1,"program fixture too large");pagebuf[address[7:0]+prog_count]=shift[7:0];touched[address[7:0]+prog_count]=1;end
       else begin pagebuf[(address[7:0]+prog_count)%256]=shift[7:0];touched[(address[7:0]+prog_count)%256]=1;end
       prog_count++;
     end else begin if(status_count<2)status_in[status_count]=shift[7:0];status_count++;end
     got=0;shift=0;
   end
  end else if(state==DUMMY)begin
    dummies--;if(dummies==0)begin state=cmd==8'h77 ?WRAP:READ;got=0;shift=0;end
  end
 end
 always @(negedge clk)if(!cs_n)begin
  if(state==READ)begin
   oe=data_width==4?4'hf:data_width==2?4'h3:4'h2;
   if(data_width==1)out={2'b0,((read_value()>>(7-output_bits))&1'b1),1'b0};
   else out=(read_value()>>(8-data_width-output_bits))&((1<<data_width)-1);
   output_bits+=data_width;if(output_bits==8)begin output_bits=0;output_bytes++;next_address();end
  end else if(drive_through_turnaround&&(state==ADDR||state==MODE))begin oe=4'hf;out=0;end
  else oe=0;
 end
 always @(posedge cs_n)begin
  oe=0;
  if(trace&&clocks>=8)begin
   if(has_address)trace_address=$sformatf("%06h",address);else trace_address="------";
   if(has_mode)trace_mode=$sformatf("%02h",mode);else trace_mode="--";
   $display("QSPI %02h %s M %s C%0d B%0d",cmd,trace_address,trace_mode,clocks,output_bytes);
  end
  // Suspend is the one instruction besides the status reads that BUSY lets through (§7.1.1, §8.2.19).
  if(clocks==8&&cmd==8'h75&&!blocked&&!asleep)try_suspend();
  if(clocks==8&&cmd==8'h7a&&!blocked&&!asleep&&!busy&&sus)resume();
  if(!busy&&clocks>=8&&!blocked)begin
   if(cmd!=8'h66&&cmd!=8'h99)reset_enable=0;
   if(!asleep||cmd==8'hab)begin
    if(cmd==8'h77&&clocks==16&&(sr2[1]||qe_ignored))wrap_cfg=wrap_in;
    if(clocks==8)case(cmd)
     8'h06:wel=1;8'h04:wel=0;8'h50:volatile_enable=1;8'hb9:asleep=1;
     8'h66:reset_enable=1;
     8'h7e,8'h98:if(wel)for(int i=0;i<256;i++)begin lock_blk[i]=cmd==8'h7e;if(i<32)lock_sec[i]=cmd==8'h7e;end
     8'h99:if(reset_enable)begin pending_kind=4;operation_time=TRST;busy=1;start_op();reset_enable=0;end
     default:;
    endcase
    // tRES1/tRES2 apply only when ABh releases power-down (§8.2.22); an ID read while awake needs no wait.
    if(cmd==8'hab&&asleep)begin asleep=0;pending_kind=5;operation_time=TRES;busy=1;start_op();end
    if((cmd==8'h02||cmd==8'h32)&&(wel||no_wel_check)&&prog_count>0&&got==0&&state==WRITE
      &&!is_protected({address[23:8],8'h0},256)&&!in_suspended({address[23:8],8'h0},256))begin pending_kind=1;pending_addr={address[23:8],8'h0};operation_time=TPP;busy=1;start_op();end
    if((wel||no_wel_check)&&((clocks==32&&(cmd==8'h20||cmd==8'h52||cmd==8'hd8))||(clocks==8&&(cmd==8'hc7||cmd==8'h60))))begin
     erase_size=cmd==8'h20 ?4096:cmd==8'h52 ?32768:cmd==8'hd8 ?65536:16777216;
     pending_addr=address&~(erase_size-1);
     if(!is_protected(pending_addr,erase_size)&&!in_suspended(pending_addr,erase_size))begin
      pending_kind=2;operation_time=cmd==8'h20 ?TSE:cmd==8'h52 ?TBE1:cmd==8'hd8 ?TBE2:TCE;busy=1;start_op();
     end
    end
    // Security registers (§8.2.29, §8.2.30): WEL, a valid register address, its LB bit clear.
    if(cmd==8'h42&&wel&&prog_count>0&&got==0&&state==WRITE&&secreg_ok(address)&&(!sr2[2+address[13:12]]||lb_ignored))begin
     pending_kind=6;pending_addr={address[23:8],8'h0};operation_time=TPP;busy=1;start_op();
    end
    if(cmd==8'h44&&wel&&clocks==32&&secreg_ok(address)&&(!sr2[2+address[13:12]]||lb_ignored))begin
     pending_kind=7;pending_addr={address[23:8],8'h0};operation_time=TSE;busy=1;start_op();
    end
    // Individual locks (§8.2.32, §8.2.33): WEL, then the address. §7.1.2 does not list them among the
    // instructions that clear WEL, so the bench leaves WEL set.
    if((cmd==8'h36||cmd==8'h39)&&wel&&clocks==32)begin
     if(address[23:16]==0||address[23:16]==255)lock_sec[(address[23:16]==255?16:0)+address[15:12]]=cmd==8'h36;
     else lock_blk[address[23:16]]=cmd==8'h36;
    end
    // Write Status (§8.2.5): SRL = 1 locks it; SRP = 1 with /WP low locks it in Standard/Dual SPI
    // (§7.1.1 table, §4.3). /WP is IO2, read at CS falling and rising. Writable bits only; LB3-1 and
    // SRL never go from 1 to 0. Each register's non-volatile copy changes only when it is written.
    if((cmd==8'h01||cmd==8'h31||cmd==8'h11)&&(wel||volatile_enable)&&got==0&&(status_count==1||(cmd==8'h01&&status_count==2))
      &&!sr2[0]&&!(sr1[7]&&!sr2[1]&&!wp_ignored&&(wp_at_select===1'b0||io[2]===1'b0)))begin
     pending1=sr1;pending2=sr2;pending3=sr3;pending_which=0;
     if(cmd==8'h01)begin pending1=(sr1&8'h03)|(status_in[0]&8'hfc);pending_which=1;end
     if(cmd==8'h31)begin pending2=status_in[0];pending_which|=2;end
     if(cmd==8'h01&&status_count==2)begin pending2=status_in[1];pending_which|=2;end
     if(pending_which&2)pending2=(sr2&8'h84)|(pending2&8'h7b)|(sr2&8'h39);
     if(cmd==8'h11)begin pending3=(sr3&8'h9b)|(status_in[0]&8'h64);pending_which=4;end
     pending_volatile=volatile_enable;pending_kind=3;operation_time=volatile_enable?1:TW;
     if(!volatile_enable)busy=1;start_op();volatile_enable=0;
    end
   end
  end
 end
 always @(negedge cs_n)wp_at_select=io[2]!==1'b0;
 // One process per internal operation. Suspend, reset and power cycles bump gen, so an aborted
 // operation's process ends without completing.
 task start_op();cur_kind=pending_kind;cur_end=$time+operation_time;cur_time=operation_time;fork run_op(gen,operation_time,pending_kind);join_none endtask
 task automatic run_op(input int g,input time t,input int kind);
  #(t);
  if(g==gen)begin
   case(kind)
    1:for(int i=0;i<1024;i++)if(touched[i])mem_write(pending_addr+i,mem_read(pending_addr+i)&pagebuf[i]);
    2:for(int p=0;p<used;p++)if(((tags[p]<<8)&~(erase_size-1))==pending_addr)for(int j=0;j<256;j++)memory[p*256+j]=erase_to_zero?8'h00:8'hff;
    3:begin sr1=pending1;sr2=pending2;sr3=pending3;
       if(!pending_volatile)begin if(pending_which&1)nv1=sr1;if(pending_which&2)nv2=sr2;if(pending_which&4)nv3=sr3;end end
    4:volatile_reset();
    6:for(int i=0;i<256;i++)if(touched[i])secreg[secreg_index(pending_addr)+i]&=pagebuf[i];
    7:for(int i=0;i<256;i++)secreg[secreg_index(pending_addr)+i]=8'hff;
    default:;
   endcase
   if(kind!=5&&kind!=8&&!(kind==3&&pending_volatile))wel=0;
   busy=0;cur_kind=0;
  end
 endtask
 // Software reset, /RESET and power-up all return the volatile state to its power-on values (§8.2.37).
 task automatic volatile_reset();
  if(!reset_keeps_volatile)begin sr1=nv1;sr2=nv2;sr3=nv3;end wel=0;volatile_enable=0;wrap_cfg=8'h10;asleep=0;
  sus=0;susp_kind=0;resumed=0;for(int i=0;i<256;i++)lock_blk[i]=1;for(int i=0;i<32;i++)lock_sec[i]=1;
 endtask
 function automatic bit in_suspended(input reg [23:0] a,input int n);
  int lo,size;size=susp_kind==1?256:susp_size;lo=susp_addr;
  in_suspended=sus&&a<lo+size&&a+n>lo;
 endfunction
 // §8.2.19: only a sector or block erase, or a page program, can be suspended; SUS must be 0 and BUSY 1.
 // A suspend sooner than TSUS after a resume breaks the datasheet's rule; the bench ignores it.
 task automatic try_suspend();
  if(!sus&&busy&&(cur_kind==1||(cur_kind==2&&erase_size<16777216))&&!(resumed&&$time-resumed_at<TSUS))begin
   gen++;susp_left=suspend_restarts?cur_time:cur_end-$time;susp_kind=cur_kind;susp_addr=pending_addr;susp_size=erase_size;
   for(int i=0;i<1024;i++)begin susp_buf[i]=pagebuf[i];susp_touched[i]=touched[i];end
   sus=1;pending_kind=8;operation_time=TSUS;start_op();
  end
 endtask
 // §8.2.20: SUS clears at once, BUSY sets, the operation finishes the time it had left.
 task automatic resume();
  sus=0;resumed=1;resumed_at=$time;pending_addr=susp_addr;erase_size=susp_size;
  for(int i=0;i<1024;i++)begin pagebuf[i]=susp_buf[i];touched[i]=susp_touched[i];end
  pending_kind=susp_kind;operation_time=susp_left;susp_kind=0;busy=1;start_op();
 endtask
 // /RESET held low at least TRESET (§4.6, §6.4) ends any operation and resets the device; while it is
 // low, frames are ignored. A shorter pulse is ignored (the datasheet only recommends the minimum).
 always @(negedge reset_n)reset_fell=$time;
 always @(posedge reset_n)if($time-reset_fell>=TRESET)begin gen++;busy=0;cur_kind=0;reset_enable=0;volatile_reset();end
 // Power-up (§6.5, §9.3): reset state, then no instruction until TVSL and no write until TPUW. SRL
 // returns to 0 (§7.1.1 note 1); suspended work is lost (§8.2.19).
 task automatic power_cycle();
  gen++;busy=0;cur_kind=0;reset_enable=0;nv2[0]=0;volatile_reset();power_gate=1;powered_at=$time;
 endtask
endmodule
Read included file: qspi_ctrl.sv
`timescale 1ns/1ns
// Task-based pin sequencer, not synthesizable RTL; problems can replace it with an FSM.
module qspi_ctrl(output reg cs_n=1,clk=0,inout wire [3:0] io,output reg [3:0] oe=0);
 reg [3:0] out=0;int half_period=5,clocks=0,polls=0;reg [7:0] line[0:31];
 bit cs_too_early=0,dummy_minus_one=0,drive_through_turnaround=0,wrap_ignored=0,mode_byte_stale=0,no_wel_before_program=0;
 // Receiver intent: high on clocks whose input is consumed as data, not dummy.
 // A passive wire observer cannot distinguish these two released-bus phases.
 bit sample_data=0;
 // W25Q128JV Rev F §9.6: 03h <= 50 MHz; /CS high >= 50 ns after writes.
 bit fast_read=0,short_cs_high=0;
 int frame_half_period=5;reg [7:0] frame_command=0;
 time cs_rose=0;bit frame_seen=0;
 function automatic bit write_frame(input reg [7:0] c);
  return c==8'h02||c==8'h32||c==8'h20||c==8'h52||c==8'hd8||c==8'h60||c==8'hc7||c==8'h01||c==8'h31||c==8'h11||c==8'h42||c==8'h44;
 endfunction
 for(genvar g=0;g<4;g++)begin:pad assign io[g]=oe[g]?out[g]:1'bz;end
 task automatic cs_lo();
  int gap;sample_data=0;clk=0;#(half_period);
  gap=write_frame(frame_command)&&!short_cs_high?50:10;
  if(frame_seen&&!short_cs_high&&$time-cs_rose<gap)#(gap-($time-cs_rose));
  cs_n=0;clocks=0;frame_half_period=half_period;
 endtask
 task automatic cs_hi();sample_data=0;#(half_period);cs_n=1;cs_rose=$time;frame_seen=1;oe=0;#(half_period);endtask
 task automatic send(input reg [31:0] value,input int nbits,input int width);
  if(clocks==0)begin
   frame_command=value[7:0];
   if(frame_command==8'h03&&!fast_read&&half_period<10)frame_half_period=10;
  end
  sample_data=0;oe=(1<<width)-1;
  for(int b=nbits-width;b>=0;b-=width)begin
   clk=0;out=(value>>b)&((1<<width)-1);#(frame_half_period);clk=1;clocks++;#(frame_half_period);
  end
 endtask
 task automatic dummy(input int n);sample_data=0;oe=0;repeat(n)begin clk=0;#(frame_half_period);clk=1;clocks++;#(frame_half_period);end endtask
 task automatic receive(input int width,output reg [7:0] value);
  sample_data=1;value=0;oe=drive_through_turnaround?4'hf:0;out=0;
  for(int b=0;b<8/width;b++)begin clk=0;#(frame_half_period);clk=1;clocks++;#1;value=(value<<width)|(width==1?{7'b0,io[1]}:({4'b0,io}&((1<<width)-1)));#(frame_half_period-1);end
 endtask
 task automatic command(input reg [7:0] c);cs_lo();send(c,8,1);cs_hi();endtask
 task automatic status(input reg [7:0] c,output reg [7:0] v);cs_lo();send(c,8,1);receive(1,v);cs_hi();endtask
 task automatic wait_ready();reg [7:0] v;polls=0;v=1;while(v[0]&&polls<200)begin status(8'h05,v);polls++;end if(v[0])$fatal(1,"BUSY timeout");endtask
 task automatic read_begin(input reg [7:0] c,input reg [23:0] a,input reg [7:0] m);
  int w,n;w=c==8'heb ?4:c==8'hbb ?2:1;n=c==8'heb ?4:(c==8'h0b||c==8'h3b||c==8'h6b)?8:0;
  cs_lo();send(c,8,1);send(a,24,w);
  if(c==8'heb||c==8'hbb)send(mode_byte_stale?8'h00:m,8,w);
  if(n>0)dummy(n-(dummy_minus_one?1:0));
 endtask
 task automatic fetch_line(input reg [23:0] a,input bit quad);
  reg [7:0] v;int w;w=quad?4:1;read_begin(quad?8'heb:8'h03,wrap_ignored?{a[23:5],5'b0}:a,8'hf0);
  for(int i=0;i<32;i++)begin if(cs_too_early&&i==31)cs_hi();receive(w,v);line[i]=v;end
  cs_hi();$display("QSPI LINE %06h C%0d",a,clocks);
 endtask
 task automatic program_byte(input reg [23:0] a,input reg [7:0] v);
  if(!no_wel_before_program)command(8'h06);cs_lo();send(8'h02,8,1);send(a,24,1);send(v,8,1);cs_hi();wait_ready();
 endtask
 task automatic set_wrap(input int length);
  reg [7:0] w;
  case(length)0:w=8'h10;8:w=0;16:w=8'h20;32:w=8'h40;64:w=8'h60;default:$fatal(1,"wrap length must be 0/8/16/32/64");endcase
  cs_lo();send(8'h77,8,1);dummy(6);send(w,8,4);cs_hi();
 endtask
 // Teaching pattern changes each 256-byte lap, so page-buffer wrap is visible.
 function automatic [7:0] program_pattern(input reg [7:0] pattern,input int i);
  program_pattern=((pattern+(i&255))&255)^((i>>8)*8'h5a);
 endfunction
 task automatic program_bytes(input reg [23:0] a,input int n,input reg [7:0] pattern);
  if(n<1||n>768)$fatal(1,"program fixture length 1..768");
  if(!no_wel_before_program)command(8'h06);cs_lo();send(8'h02,8,1);send(a,24,1);
  for(int i=0;i<n;i++)send(program_pattern(pattern,i),8,1);cs_hi();wait_ready();
 endtask
endmodule
Read included file: qspi_checker.sv
`timescale 1ns/1ns
module qspi_checker(input wire cs_n,clk,input wire [3:0] ctrl_oe,flash_oe);
 int broken=0,clocks=0;bit collided=0;
 always @(posedge clk)begin if(!cs_n)begin clocks++;if((ctrl_oe&flash_oe)!=0)begin broken++;collided=1;$display("QSPI CHECK contention mask=%b",ctrl_oe&flash_oe);end end end
 always @(posedge cs_n)begin #1;if(flash_oe!=0)begin broken++;$display("QSPI CHECK flash drives deselected");end end
endmodule
Read included file: qspi_rules.sv
`timescale 1ns/1ns
module qspi_rules #(parameter CHECK_WEL=1)(input wire cs_n,clk,input wire [3:0] io,ctrl_oe,flash_oe,input wire sample_data);
 int broken=0,clocks=0,preamble=8,dummies=0,data_width=1;
 reg [7:0] command=0;bit read_command=0,first_sample=0,write_enable=0,opened=0;
 task automatic bad(input string rule);broken++;$display("QSPI RULE %s",rule);endtask
 always @(negedge cs_n)begin
  clocks=0;command=0;preamble=8;dummies=0;data_width=1;read_command=0;first_sample=0;opened=1;
 end
 always @(posedge clk)if(!cs_n)begin
  clocks++;
  assert((ctrl_oe&flash_oe)==0)else bad("contention");
  if(clocks<=8)begin
   command={command[6:0],io[0]};
   if(clocks==8)begin
    case(command)
     8'h03:begin read_command=1;preamble=32;end
     8'h0b,8'h3b,8'h6b:begin read_command=1;preamble=32;dummies=8;end
     8'hbb:begin read_command=1;preamble=24;end
     8'heb:begin read_command=1;preamble=16;dummies=4;end
     8'h05,8'h35,8'h15,8'h9f:read_command=1;
     8'h90,8'hab:begin read_command=1;preamble=32;end
     8'h02,8'h32,8'h20,8'h52,8'hd8:preamble=32;
     default:;
    endcase
    if(command==8'h32)data_width=4;
    if(CHECK_WEL&&(command==8'h02||command==8'h32||command==8'h20||command==8'h52||command==8'hd8||command==8'h60||command==8'hc7))begin
     assert(write_enable)else bad("wel_before_program");
    end
   end
  end
  if(sample_data)begin
   assert(read_command)else bad("read_command");
   if(!first_sample)begin
    assert(clocks==preamble+dummies+1)else bad("dummy_count");
    first_sample=1;
   end
   assert(ctrl_oe==0)else bad("turnaround");
  end
 end
 always @(posedge cs_n)if(opened)begin
  if(command==8'h02||command==8'h32||command==8'h01||command==8'h31||command==8'h11)begin
   assert(clocks>=preamble+8/data_width&&(clocks-preamble)%(8/data_width)==0)else bad("byte_boundary");
  end
  if(command==8'h20||command==8'h52||command==8'hd8)begin assert(clocks==32)else bad("byte_boundary");end
  if(command==8'h60||command==8'hc7)begin assert(clocks==8)else bad("byte_boundary");end
  if(command==8'h06&&clocks==8)write_enable=1;
  if((command==8'h04||command==8'h99)&&clocks==8)write_enable=0;
  if((command==8'h02||command==8'h32)&&clocks>=preamble+8/data_width&&(clocks-preamble)%(8/data_width)==0)write_enable=0;
  if((command==8'h01||command==8'h31||command==8'h11)&&clocks>=16&&clocks%8==0)write_enable=0;
  if((command==8'h20||command==8'h52||command==8'hd8)&&clocks==32)write_enable=0;
  if((command==8'h60||command==8'hc7)&&clocks==8)write_enable=0;
  #1;assert(flash_oe==0)else bad("miso_released");
 end
endmodule
ConceptWhy does the flash clear WEL at the end of every program or erase, instead of leaving it set?
Read the waveformIn the measured frame-by-frame trace, what clears WEL?
One Program, Frame by FrameOne column pair per CS frame: the frame, then the gap after it. WEL and BUSY are the flash's latches after each frame.
01234567891011121314151617181920212223242526272829CS_ncommand06020503WELBUSY
  1. Cycle 0

    06h, Write Enable: WEL goes to 1.

  2. Cycle 2

    02h with the address and one byte. When CS rises the internal program starts: BUSY goes to 1.

  3. Cycle 4

    The controller polls 05h. While BUSY is 1 the flash ignores everything but the status reads.

  4. Cycle 26

    The program is over: BUSY and WEL are both 0. The next write needs a new 06h.

  5. Cycle 28

    Now the flash accepts a read.

Measured on the bench: program_byte(24'h002000, 8'h5a), then a 03h read that returns 5a. 12 Read Status frames find BUSY set until the compressed tPP of 2 µs is over.

Remember

  • Every program, erase and status write needs a 06h right before it; the flash clears WEL when the operation ends.
  • BUSY is 1 for the whole internal operation, and the flash then ignores everything but the status reads; poll 05h until it clears.
  • A page is 256 bytes and the program address wraps inside it: later bytes overwrite earlier ones and never reach the next page.
  • Programming can only clear bits; erase returns a sector or block to FFh, and real times are milliseconds to seconds.
  • A flash model that accepts a program without WEL hides every controller that forgot 06h.

Practice