Write Protection: Ranges, Locks and /WP

Read the protected range from the BP, TB, SEC and CMP bits, say who may change them, use the individual locks, and choose between a volatile and a non-volatile status write.

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

A logger kept its boot code in the bottom 256 KB of the flash and set BP = 001 to protect it. Months later a bad pointer erased the boot sector anyway: BP = 001 with TB = 0 protects the top of the array. One status bit separated a safe board from a bricked one.

Two Schemes Guard the Array; Three Switches Guard the SchemeWPS picks which scheme decides whether a program or erase may touch an address. SRL, SRP and /WP decide whether the status register itself may change.
Two schemes guard the array; three switches guard the schemeWPS picks which scheme decides whether a program or erase may touch an address. SRL, SRP and /WP decide whether the status register itself may change. component Write Status 01h, 31h, 11h component SRL, SRP, /WP may the statuschange? component Status registers BP2-0, TB, SEC,CMP, WPS component Individual locks 36h, 39h, 7Eh, 98h component Program or erase an address range component Protection check WPS = 0: range; WPS= 1: locks component Memory array changed, or thecommand ignored allowedWPS = 0WPS = 1no protected byte
  1. Changing the protection

    A status write passes only if SRL is 0, and SRP with /WP low does not lock it.

  2. Scheme one

    With WPS = 0 the BP, TB, SEC and CMP bits name one protected range.

  3. Scheme two

    With WPS = 1 every 64 KB block, and every 4 KB sector of the end blocks, has its own lock bit.

  4. The check

    A program or erase that covers any protected byte is ignored as a whole.

Read the diagram as text
  • Write Status: 01h, 31h, 11h
  • SRL, SRP, /WP: may the status change?
  • Status registers: BP2-0, TB, SEC, CMP, WPS
  • Individual locks: 36h, 39h, 7Eh, 98h
  • Program or erase: an address range
  • Protection check: WPS = 0: range; WPS = 1: locks
  • Memory array: changed, or the command ignored
  • Write Status → SRL, SRP, /WP
  • SRL, SRP, /WP → Status registers: allowed
  • Status registers → Protection check: WPS = 0
  • Individual locks → Protection check: WPS = 1
  • Program or erase → Protection check
  • Protection check → Memory array: no protected byte

W25Q128JV §6.5, §7.1.5 (WPS) and §7.1.8 note 3. The bench models every box.

Eight Guards Against a Stray Write

A flash keeps code and settings across power cycles, so a noisy bus or a firmware bug must not change it by accident. The datasheet lists eight separate guards. Two need power-up timing, one is WEL from P7.8, and four are status bits and locks.

Guard (§6.5.1)What it blocksWhere
Reset while VCC is below VWIevery instruction during power-up and power-downP7.14
Write delay tPUW after power-upWrite Enable, program, erase, status writesP7.14
WEL, cleared after each writeany write without a fresh 06hP7.8
Status bits and the /WP pinprogram and erase in a range; status changesthis lesson
Individual block and sector locksprogram and erase, block by blockthis lesson
Power-downevery instruction except ABhP7.13
SRL lock-downstatus writes until the next power-upthis lesson
One-time-program lockstatus writes, forever (a special Winbond flow)text only

Eight features from §6.5.1, in our words. The last one needs a factory sequence the datasheet does not publish.

BP, TB and SEC Name a Range

BP2-0 sit in SR1 bits 4 to 2, TB in bit 5 and SEC in bit 6. BP sets the size, TB picks the top (0) or the bottom (1), and SEC picks 64 KB blocks (0) or 4 KB sectors (1). The factory value is all zero: nothing protected.

SECTBBP2-0CMP = 0 protectsCMP = 1 protects
xx000nothingall 16 MB
00001top 256 KB: FC0000h-FFFFFFh000000h-FBFFFFh
00110top 8 MB: 800000h-FFFFFFh000000h-7FFFFFh
01001bottom 256 KB: 000000h-03FFFFh040000h-FFFFFFh
10001top 4 KB: FFF000h-FFFFFFh000000h-FFEFFFh
1110xbottom 32 KB: 000000h-007FFFh008000h-FFFFFFh
xx111all 16 MBnothing

Rows picked from the tables in §7.1.8 (CMP = 0) and §7.1.9 (CMP = 1). With SEC = 0 the size doubles from 256 KB (001) to 8 MB (110); with SEC = 1 from 4 KB to 32 KB. SEC = 1 with BP = 110 is not listed; the bench treats it as 32 KB.

A program or erase that touches any protected byte is ignored as a whole (§7.1.8 note 3). On the bench a 32 KB erase from 000000h left 001000h holding 88, because its first 4 KB was protected. With BP = 000 the same erase read back ff.

CMP Turns the Range Inside Out

CMP is SR2 bit 6. With CMP = 1 the bytes the other bits name become the only writable ones, and the rest of the array is protected. BP = 000 with CMP = 1 protects everything; BP = 111 with CMP = 1 protects nothing.

Status bitsProgrammed atRead back
BP = 001, TB = 0FC0000h / FBFFFFhff / 22
BP = 001, TB = 103FFFFh / 040000hff / 44
BP = 001, TB = 1, CMP = 103FFFEh / 040001h55 / ff
BP = 001, TB = 1, CMP = 1, flash.cmp_ignored03FFFDh / 040001hff / 5a
BP = 001, TB = 1, SEC = 1000FFFh / 001000hff / 88

Measured: one-byte programs of 11h to 88h on an erased array; FFh means the flash ignored the program. The twin applies the range without the complement.

Who May Change the Protection

The status register is guarded too. SRP (SR1 bit 7) hands control to the /WP pin, and SRL (SR2 bit 0) locks the register until power is cycled. A locked write is ignored, and BUSY never rises.

SRLSRP/WPThe status register
00xwritable after 06h (factory default)
01lowlocked by the pin
01highwritable after 06h
1xxlocked until the next power-up

The table in §7.1.1 (the SRP and SRL heading), in our words. The pin counts only in Standard and Dual SPI: with QE = 1 it is IO2 (§4.2, §4.3).

Run, with SRP = 1SR1 after writing 84hMeaning
QE = 0, /WP low80refused: BP0 stayed 0
QE = 0, /WP high84written
QE = 1, /WP low84written: the pin is IO2 now
QE = 0, /WP low, flash.wp_ignored84written: the twin's bug

Measured. SRL = 1 then refused every status write: SR2 read 03 after an attempt to clear it, and BP stayed 00. After flash.power_cycle() SR2 read 02 and BP = 111 took (SR1 1c).

The /WP pin must be high 20 ns before CS falls and stay high 100 ns after CS rises (tWHSL, tSHWL, §9.9). These bind only a status write with SRP = 1 (§9.6 note 3).

Individual Locks: WPS = 1

Setting WPS (SR3 bit 2) swaps the range scheme for one lock bit per block. All locks are set at power-up and after a reset, so a board must unlock what it writes. Instructions 36h and 39h lock and unlock one address; 7Eh and 98h lock and unlock all. Each needs WEL first.

Step (WPS = 1)Result
Power-up value of the lock at 100000h (3Dh)1
Program 100000hff
06h, 39h 100000h, 04h; read the lock0
39h 200000h without 06h; read the lock1
Program 100000h, then the next block, 110000h5a, ff
06h, 36h 100000h, 04h (lock it again); read the lock1
Unlock sector 0 only; program 000000h, then 001000h5a, ff
06h, 98h (global unlock); locks at 200000h and FFF000h0, 0
06h, 7Eh (global lock); lock at 100000h1
flash.lock_ignored: program a locked block5a

Measured. Blocks 1 to 254 have one lock each; blocks 0 and 255 have one per 4 KB sector: 126 + 32 = 158 lock bits (§6.5, Figure 4d). 3Dh returns the lock in bit 0 (Figure 55a).

The lock bits are volatile. §7.1.2 does not list the lock instructions among those that clear WEL, so the bench leaves WEL set after them. The helpers send 04h afterwards so no result depends on that.

Volatile or Non-Volatile Status Writes

Every writable status bit has a volatile copy and a non-volatile one. A 06h before 01h, 31h or 11h writes both copies and takes tW. A 50h before the same write changes only the volatile copy, at once. That suits a quick unlock that should not survive a reset.

Write of SR1 = 1Ch (BP = 111)Polls until readySR1 afterA programSR1 after 66h, 99h
50h, then 01h: volatile11cff: refused00
06h, then 01h: non-volatile121crefused1c

Measured. The volatile write never sets BUSY and does not need WEL (§8.2.2); a reset reloads the non-volatile copy (§8.2.5). The bench compresses tW (10 ms typical, 15 ms maximum) to 2 µs.

Each register has its own non-volatile copy. Writing SR3 non-volatile leaves the non-volatile SR1 and SR2 as they were. SRL and LB3-1 can never go from 1 back to 0, whichever way they are written.

Try It

write_status sends 06h, the status instruction and one byte, polls BUSY, then sends 04h. Each probe programs one byte with ctrl.program_byte and reads it back: FFh means the flash ignored the program.

Try flash.cmp_ignored = 1; before step 3. The complement is skipped: 040001h takes the byte, and 03FFFEh, the only writable range, refuses it.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: 1. BP = 001, TB = 0: the top 256 KB SR1 04 SR2 02 SR3 00 fc0000 holds ff fbffff holds 22 2. TB = 1: the bottom 256 KB 03ffff holds ff 040000 holds 44 3. CMP = 1: everything except the bottom 256 KB SR1 24 SR2 42 SR3 00 03fffe holds 55 040001 holds ff 4. CMP = 0, SEC = 1: one 4 KB sector at the bottom SR1 64 SR2 02 SR3 00 000fff holds ff 001000 holds 88 5. A 32 KB erase that reaches into that sector 001000 holds 88 6. BP = 000: nothing protected, the same erase 001000 holds ff
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. write_status sets SR1 to 08h: BP = 010, TB = 0, SEC = 0, and CMP stays 0. The bench programs 5Ah at F80000h and at F7FFFFh and prints both bytes read back. 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 write_status(input [7:0] c, input [7:0] v);   // 06h, 01h/31h/11h, poll, 04h
    ctrl.command(8'h06);
    ctrl.cs_lo(); ctrl.send(c, 8, 1); ctrl.send(v, 8, 1); ctrl.cs_hi();
    ctrl.wait_ready(); ctrl.command(8'h04);         // a refused write leaves no WEL behind
  endtask
  initial begin
    flash.sr2 = 8'h02;                       // QE = 1; every page starts erased (FFh)
    #10;
    write_status(8'h01, 8'h08);          // SR1: BP = 010, TB = 0, SEC = 0
    ctrl.program_byte(24'hf80000, 8'h5a); ctrl.read_begin(8'h03, 24'hf80000, 8'hf0); ctrl.receive(1, value); ctrl.cs_hi(); first = value;
    ctrl.program_byte(24'hf7ffff, 8'h5a); ctrl.read_begin(8'h03, 24'hf7ffff, 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; SR1 starts at 00h in both copies. The bench sends 50h, then 01h with 1Ch (BP = 111), reads SR1, resets the flash with 66h and 99h, and reads SR1 again. It prints both reads. 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;
  initial begin
    flash.sr2 = 8'h02;                       // QE = 1; every page starts erased (FFh)
    #10;
    ctrl.command(8'h50);                 // volatile: no WEL, no BUSY
    ctrl.cs_lo(); ctrl.send(8'h01, 8, 1); ctrl.send(8'h1c, 8, 1); ctrl.cs_hi(); #10;
    ctrl.status(8'h05, first);
    ctrl.command(8'h66); ctrl.command(8'h99); ctrl.wait_ready();
    ctrl.status(8'h05, value);
    $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
Spot the bugWPS is 1, so every lock is set. Each option runs before a one-byte program to 100000h, which passes if the byte reads 5Ah. Which option leaves the program ignored?
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
ConceptSR1 holds SEC = 0, TB = 0, BP = 001, and SR2 holds CMP = 1. Which part of the array can firmware still program?
ConceptA board sets SRP = 1 and ties /WP low to freeze the protection. A later firmware sets QE = 1 to use quad reads. What happens to the freeze?

Remember

  • BP2-0 set the size, TB the end (top 0, bottom 1), SEC blocks or sectors; CMP = 1 protects the complement.
  • A program or erase that covers any protected byte is ignored as a whole, and so is a chip erase with any protection.
  • SRP with /WP low, or SRL, locks the status register; /WP works only while QE = 0.
  • WPS = 1 uses 158 volatile lock bits instead, all set at power-up; 39h or 98h after 06h unlocks.
  • 50h makes the next status write volatile and instant; 06h writes the non-volatile copy and takes tW.

Practice