Capstone: A QSPI Controller and Flash Bug Hunt

Hunt eleven faults across a controller and a flash model, tell the ones a wire checker sees from the ones only a readback sees, and pick the programs that catch them all.

Lesson
P7.10
Time
45 min
Runs on
Your browser
Check
5 questions

A flash model in a company's verification library accepted Page Program without Write Enable. Every controller verified against it passed, including one that forgot 06h in one code path. In the field the flash ignored those programs and the device's settings never saved.

The Bug-Hunt BenchStimulus programs drive the controller; the flash answers; the checker watches the enables; each program reads its result back.
The bug-hunt benchStimulus programs drive the controller; the flash answers; the checker watches the enables; each program reads its result back. component Stimulus programs seven tasks component Controller six faults component Flash model five faults component Contentionchecker ctrl_oe & flash_oe component Readback data ok or WRONG component Flash trace line command, clocks,bytes task callspinsctrl_oeflash_oeexpected bytes
  1. The wire eye

    The checker reads both enables on every clock. It catches a fight for the lanes and nothing else.

  2. The readback eye

    Each program knows what the flash should hold and reads it back. It catches what the flash did, legal wire or not.

  3. The record

    The flash prints a line per command. It rules on nothing; it is the evidence the other two eyes point at.

Read the diagram as text
  • Stimulus programs: seven tasks
  • Controller: six faults
  • Flash model: five faults
  • Contention checker: ctrl_oe & flash_oe
  • Readback: data ok or WRONG
  • Flash trace line: command, clocks, bytes
  • Stimulus programs → Controller: task calls
  • Controller → Flash model: pins
  • Controller → Contention checker: ctrl_oe
  • Flash model → Contention checker: flash_oe
  • Flash model → Flash trace line
  • Stimulus programs → Readback: expected bytes

Arrows are calls, pins and the values each observer reads.

Two Kinds of Eyes

A wire checker rules on the pins: who drives, when, and in what order. A readback rules on the result: what the flash holds after the command. A fault can break either one, or both, and a bench needs both eyes.

The contention checker from P7.7 is a pure wire eye. Each stimulus program below ends in a readback eye: it knows the bytes the flash should hold and reads them back.

The Fault Matrix

Faultreadslineprogramno 06h300 byteseraseQE clear
none (clean)·······
ctrl.cs_too_early·data·····
ctrl.dummy_minus_onedatadata·····
ctrl.drive_through_turnarounddata + 28 checksdata + 64 checksdata + 8 checksdata + 8 checksdata + 8 checksdata + 16 checksdata
ctrl.wrap_ignored·data·····
ctrl.mode_byte_staledatadata·····
ctrl.no_wel_before_program··data····
flash.no_wel_check···data···
flash.no_page_wrap····data··
flash.busy_never_set··datadatadatadata·
flash.qe_ignored······data
flash.erase_to_zero·····data·

Measured: each row is one bench run with one fault on, all seven programs in order. "data" means a readback differed; "checks" counts contention lines. A dot means the program saw nothing.

The checker printed lines for 1 fault of 11: ctrl.drive_through_turnaround. Every other fault kept the wire legal and showed only in a readback. Every fault was caught by at least one program.

A Wire-Visible Cell

ctrl.drive_through_turnaround is the loudest row: 64 checker lines on the line program alone, one per data clock. Even the QE-clear read goes wrong without a single check. There the controller drives the lanes it should be reading, and the flash is silent, so nobody fights.

Clean

line_wrap();

Prints line: data ok, checker lines 0.

Controller drives through the turnaround

ctrl.drive_through_turnaround = 1;
line_wrap();

Prints line: data WRONG, checker lines 64.

A Readback-Only Cell

flash.no_wel_check breaks nothing on the wire, because the controller's frames are legal. Only one program sees it: "no 06h", which sends a program without 06h on purpose and expects the flash to refuse.

A strict flash

no_wel();

Prints no 06h: data ok, checker lines 0.

flash.no_wel_check = 1

flash.no_wel_check = 1;
no_wel();

Prints no 06h: data WRONG, checker lines 0.

A lenient model is a fault of its own: it hides every controller that forgets 06h. flash.qe_ignored is the same kind of fault, and only "QE clear" sees it. Both need stimulus the datasheet forbids.

Choosing Programs That Cover the Matrix

No single program catches everything. The smallest set that catches all 11 faults, found by trying every combination, has 6 programs: "line", "program", "no 06h", "300 bytes", "erase", "QE clear".

ProgramFaults it catches
reads3
line5
program3
no 06h3
300 bytes3
erase3
QE clear2

Counted from the matrix. Green rows are the smallest covering set.

A stronger wire eye would shrink the readback work. Three more rules would turn several readback cells into checker lines: the dummy count of each command, a program that ends on a whole byte, and 06h before every 02h.

Where You Will Meet It

Every flash controller sign-off and every flash model in a verification library needs this matrix. The faults change; the method does not: one switch per fault, one row per run, and stimulus chosen until every row has a mark.

Asked in interviews: the bug hunt
  1. Why is a clean wire not enough?

    A flash that programs without WEL, erases to zero or never reports BUSY keeps every frame legal. Only a readback against the datasheet's rule shows what it did.

  2. How do you test a lenient flash model?

    Send stimulus the datasheet forbids, such as a program without 06h or a quad read with QE clear, and expect the flash to refuse it.

  3. Why keep a record that gives no verdict?

    The flash trace line is the evidence a human or a scoreboard reads. A record that also gave verdicts would hide the data whenever its rule was wrong.

  4. How do you choose stimulus for a fault list?

    Build the fault-against-program matrix, then pick programs until every fault has a mark. Faults no program sees are the next stimulus to write.

Try It

The seven tasks beside the stimulus are the programs. Each one prints a verdict line: its readback, and the number of checker lines it caused. fetch_line adds its own line report.

Uncomment one fault at a time and compare the verdicts with the matrix. Then turn on two faults together, ctrl.no_wel_before_program and flash.no_wel_check: the program row comes back clean, because the lenient flash hides the controller's bug.

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

Remember

  • A wire checker rules on who drives and when; a readback rules on what the flash did. Most flash faults keep the wire legal.
  • On this bench the contention checker saw 1 fault of 11; readbacks caught the rest.
  • A lenient flash model is a fault of its own: only stimulus the datasheet forbids, expecting a refusal, can expose it.
  • Build the fault-against-program matrix and choose programs until every fault has a mark.
  • Keep the record that gives no verdict beside the checks that do.

Practice