Erase Sizes, Quad Program, Suspend and Resume
Pick the erase that fits, say which address bits it uses and how long it really takes, know when 32h helps, and park an erase with 75h to serve a read or a program.
- Lesson
- P7.12
- Time
- 35 min
- Runs on
- Your browser
- Check
- 5 questions
A motor controller saved its fault log by erasing a 64 KB block. For up to two seconds the flash ignored everything but status reads, and the code it executed from that flash stalled with it. One 75h would have parked the erase, served the fetch, and let the erase finish later.
- Suspend
75h is the one instruction besides the status reads that BUSY lets through. SUS goes to 1 at once; BUSY falls within tSUS.
- Meanwhile
The flash takes reads and programs to other sectors, each with its own 06h and BUSY. Another erase is refused.
- Resume
7Ah, accepted only with SUS = 1 and BUSY = 0, clears SUS and sets BUSY. The erase runs for the time it had left.
Read the diagram as text
- Erasing: BUSY 1, SUS 0
- Parking: within tSUS
- Suspended: BUSY 0, SUS 1
- Other work: reads; programs to other sectors
- Erasing again: BUSY 1, SUS 0
- Done: BUSY 0, WEL 0
- Erasing → Parking: 75h
- Parking → Suspended
- Suspended → Other work
- Other work → Suspended
- Suspended → Erasing again: 7Ah
- Erasing again → Done: time left runs out
From §8.2.19 and §8.2.20. On the bench the parked erase took its own 4 µs, plus the time it was parked.
Four Erase Sizes
An erase sets every bit of its region back to 1. The region is a 4 KB sector, a 32 KB or 64 KB block, or the whole chip; the low address bits inside it are ignored. Each needs WEL, ends on a byte boundary, and keeps BUSY high for its whole cycle.
| Instruction | Erases | Address bits used | Datasheet typ / max | Bench, CS rise to ready |
|---|---|---|---|---|
| 20h | 4 KB sector | A23-A12 | tSE 45 ms / 400 ms | ns ns |
| 52h | 32 KB block | A23-A15 | tBE1 120 ms / 1,600 ms | ns ns |
| D8h | 64 KB block | A23-A16 | tBE2 150 ms / 2,000 ms | ns ns |
| C7h or 60h | the whole 16 MB | none: one byte only | tCE 40 s / 200 s | ns ns |
Datasheet §9.6; measured on the bench, whose model times are tSE 4000 ns, tBE1 6000 ns, tBE2 8000 ns and tCE 10000 ns, plus the last poll. The bench keeps the order of the times but not their ratios: a real chip erase is about 900 sector erases long.
| Address | Before | After D8h at 123456h |
|---|---|---|
| 11FFFFh | 00 | 00 |
| 120000h | 00 | ff |
| 12FFFFh | 00 | ff |
| 130000h | 00 | 00 |
Measured. The low 16 address bits are ignored: the erase covers 120000h to 12FFFFh, whichever address inside it was sent.
Chip Erase and Protection
Instructions C7h and 60h are the same. A chip erase is refused if any region is protected, by the BP bits or by a lock (§8.2.18). With the top 256 KB protected the bench refused it outright: BUSY never rose, and 130000h still read 00.
Quad Input Page Program
Instruction 32h is 02h with the data on all four lanes. It needs QE = 1 and WEL, and keeps the 256-byte page wrap of 02h. The command and the address still go on IO0 alone.
| 256-byte page | Data lanes | Clocks per frame | At 133 MHz | Then tPP, typical |
|---|---|---|---|---|
| 02h Page Program | IO0 | 2080 | 15.6 µs | 400 µs |
| 32h Quad Input Page Program | IO0 to IO3 | 544 | 4.1 µs | 400 µs |
Clocks measured; the µs column is clocks divided by 133 MHz. Without QE the flash ignores 32h: the byte read back ff.
At 133 MHz either frame is a few per cent of the program time, so 32h saves little. The datasheet says it pays off below about 5 MHz, where clocking the data in is slow (§8.2.14).
Suspend and Resume
Instruction 75h parks a sector erase, a block erase or a page program; a chip erase cannot be parked. SUS (SR2 bit 7) shows the parked state. While it is set, some instructions are refused.
| While suspended from | Not allowed (ignored) | Allowed |
|---|---|---|
| a sector or block erase | 01h, 20h, 52h, D8h, C7h, 60h, 44h | reads; programs to other sectors or blocks |
| a page program (02h or 32h) | 01h, 02h, 32h, 42h | reads |
| a chip erase | 75h itself is ignored: no suspend | - |
§8.2.19 in our words. The text names 01h only, not 31h or 11h. It does not forbid an erase during a program suspend. The bench follows the text, and also refuses any program or erase that touches the parked region.
On the bench the erase was parked 1 µs in. A program to 210000h worked (5a), an erase of 220000h was refused (00), and a program inside the parked sector was refused (ff). After 7Ah the erase finished and 200010h read ff.
| Run | Result |
|---|---|
| 75h during a program, then 02h to another page | the 02h is ignored: ff; after 7Ah the page reads 11 |
| 75h during a chip erase | BUSY 1 SUS 0: ignored |
| 75h with nothing running | BUSY 0 SUS 0: ignored |
| 75h right after 7Ah, then again 200 ns later | first BUSY 1 SUS 0, second BUSY 0 SUS 1 |
| Suspend 1 µs into a 4 µs erase, resume at once | done 4340 ns after it started |
| The same, flash.suspend_restarts | done 5390 ns after it started |
| Erase another sector while suspended, flash.suspend_ban_ignored | accepted: 280000h reads ff |
Measured. The bench parks within its tSUS of 100 ns (datasheet maximum 20 µs) and ignores a 75h sooner than tSUS after a 7Ah, which the datasheet forbids without saying what happens.
Where You Will Meet It
Execute-in-place systems suspend long erases so the processor can fetch code. A flash driver must poll until BUSY is 0 before using the parked flash, and must not suspend again within tSUS of a resume. A reset or a power loss while parked abandons the operation, and its data may be corrupt.
Try It
The setup writes 00h at six probe addresses, so an erase is visible as FFh. erase sends 06h and the erase instruction; sr prints BUSY from 05h and SUS from 35h.
Try flash.suspend_restarts = 1; before step 4: the parked erase starts its whole time again after 7Ah, and the last time line grows.
1. One 256-byte page: 02h, then 32h 02h: 2080 clocks 32h: 544 clocks 010042 holds 42 010142 holds 42 2. D8h at 123456h: the whole 64 KB block around it busy 8090 ns 11ffff holds 00 120000 holds ff 12ffff holds ff 130000 holds 00 3. Chip erase with the top 256 KB protected BUSY 0 SUS 0 130000 holds 00 4. Suspend a sector erase, program elsewhere, resume BUSY 0 SUS 1 200010 holds 00 210000 holds 5a BUSY 0 SUS 1 220000 holds 00 200030 holds ff BUSY 1 SUS 0 erase done 12145 ns after it started 200010 holds ffRead 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
Remember
- 20h, 52h, D8h and C7h/60h erase 4 KB, 32 KB, 64 KB or everything; the address bits below the size are ignored.
- Real erase times are 45 ms to 200 s (§9.6); a chip erase is refused if anything is protected.
- 32h puts the data on four lanes (QE = 1); it saves time only at slow clocks, because tPP dominates.
- 75h parks a sector or block erase or a page program; poll BUSY to 0, then read or program elsewhere.
- 7Ah resumes only with SUS = 1 and BUSY = 0; do not suspend again within tSUS of a resume.
Practice
- Keep Suspended Erase Commands Isolated Fix · Medium