Power-Up, Pin Timing and DC Levels
Respect the power-up window, check a controller against the input timing the flash asks for, read the output promises and the DC table, and see why a timing checker must sit beside a zero-delay model.
- Lesson
- P7.14
- Time
- 35 min
- Runs on
- Your browser
- Check
- 5 questions
A bootloader read its image with 03h at 80 MHz and passed every simulation. On the bench one board in fifty booted garbage. The datasheet allows 03h only up to 50 MHz; the faster reads need 0Bh and its dummy byte. The model had no clock limit, so nothing failed until real silicon did.
- Selecting
CS must stay high long enough after the last frame, then fall at least 3 ns before the first rising edge.
- Clocking in
Each input bit must be stable 1 ns before the rising edge and 2 ns after it, at no more than 133 MHz (50 MHz for 03h).
- Clocking out
The flash promises its output bit within 6 ns of the falling edge, and holds the old one for 1.5 ns.
- Deselecting
CS rises at least 3 ns after the last rising edge; the outputs float within 7 ns.
Read the diagram as text
- CS high: tSHSL1 10 ns, tSHSL2 50 ns
- CS falls: tCHSL, tSLCH 3 ns
- Clock: high, low 45 % of 1/FR; FR 133 MHz, fR 50 MHz
- Data in: tDVCH 1 ns before, tCHDX 2 ns after
- Data out: tCLQV 6 ns max, tCLQX 1.5 ns min
- CS rises: tCHSH, tSHCH 3 ns; tSHQZ 7 ns
- CS high → CS falls
- CS falls → Clock
- Clock → Data in: rising edge samples
- Clock → Data out: falling edge launches
- Data out → CS rises
- CS rises → CS high: next frame
The figures of §9.7 (output) and §9.8 (input) with the values of §9.6, at 3.0 to 3.6 V. Those two figures carry no figure number in Rev F.
Power-Up: Wait, Then Write
While VCC climbs the flash holds itself in reset. Reads are allowed tVSL after VCC reaches its minimum; program, erase and status writes only after tPUW. CS must follow VCC up and down, which a pull-up on CS does (§4.1).
| Parameter (§9.3) | Datasheet | Bench | Measured on the bench |
|---|---|---|---|
| VWI: below it the flash is held in reset | 1.0 V to 2.0 V | not modelled | - |
| tVSL: VCC minimum to CS low | 20 µs minimum | 200 ns | 05h at 100 ns: xx |
| tPUW: before a write instruction | 5 ms minimum | 5000 ns | 06h at 400 ns: WEL 0; at 5.6 µs: WEL 1 |
| The same, flash.no_tpuw | - | - | 06h at 400 ns: WEL 1 |
The two times are characterised, not tested (§9.3 note 1). Figure 58a: reads are allowed after tVSL, writes after tPUW. The bench starts each run already powered; flash.power_cycle() opens the window.
What the Flash Asks of the Controller
Every input rule is a minimum the controller must meet. The flash checks none of them: after a violation its behaviour is simply not specified. So the bench adds qspi_timing, a checker that timestamps every pin edge.
| Symbol (§9.6) | Minimum | Checker rule | Bench default controller |
|---|---|---|---|
| tSLCH, CS low to first rising edge | 3 ns | tslch | 5 ns |
| tCHSL, last rising edge to CS low | 3 ns | tchsl | more than 5 ns |
| tDVCH, data set up before the rising edge | 1 ns | tdvch | 5 ns |
| tCHDX, data held after the rising edge | 2 ns | tchdx | 5 ns |
| tCHSH, last rising edge to CS high | 3 ns | tchsh | 10 ns |
| tSHCH, CS high to the next rising edge | 3 ns | tshch | 15 ns |
| tSHSL1, CS high after a read | 10 ns | tshsl1 | 10 ns |
| tSHSL2, CS high after a program, erase or status write | 50 ns | tshsl2 | 50 ns |
| tCLH, tCLL: 45 % of 1/FR (FR = 133 MHz) | 3.38 ns | tclh, tcll, fr | 5 ns at 100 MHz |
| tCRLH, tCRLL: 45 % of 1/fR for 03h (fR = 50 MHz) | 9 ns | tcrlh, tcrll, fr_03h | 10 ns at 50 MHz |
| tWHSL, tSHWL: /WP around a status write, SRP = 1 | 20 ns, 100 ns | twhsl, tshwl | not driven |
| tRESET, /RESET low | 1 µs (recommended) | treset | not driven |
qspi_timing in qspi-shared.mjs checks every row at the pins, with the datasheet numbers unscaled. Clock rise and fall slopes (tCLCH, tCHCL, 0.1 V/ns) need an analog model and are left out.
The Bench Controller, Checked
The bench controller now runs 03h at 50 MHz and holds CS high at least 50 ns after writes. This example explicitly restores the original 100 MHz clock and 10 ns write gap with fast_read and short_cs_high. The checker finds two problems: 03h at that clock breaks fR, and polling right after a program breaks tSHSL2. The flash model ignores both, so the earlier measurements stand; real silicon might not.
The fixes are a slower clock for 03h, or 0Bh instead, and 40 ns more of CS high after each write frame. The example runs both versions.
Faults a Zero-Delay Model Hides
The flash model samples on the rising edge with no setup or hold window. A data change in the same instant scrambles the command; one a nanosecond later passes unnoticed. The checker reports both.
| Controller fault | Checker lines | What the zero-delay model did |
|---|---|---|
| 0Bh at 250 MHz (half period 1 ns) | tslch 1 ns, tchdx 1 ns, tchsh 2 ns, tclh 1 ns, tcll 1 ns, fr 2 ns | read correctly |
| Data changed on the rising edge itself | tdvch 0 ns | read xx: the command bits raced the clock |
| Data changed 1 ns after the rising edge | tchdx 1 ns | read ef: the model has no hold window |
| /WP raised 5 ns before CS, dropped 35 ns after | twhsl 5 ns, tshwl 35 ns | accepted the write: SR1 84 |
| /RESET low for 300 ns | treset 300 ns | ignored the pulse |
Measured. Each rule prints once per frame; the counts in the example add every violation. A pin-level model that ignores time passes most of these faults, which is why the checker sits beside it.
What the Flash Promises Back
Output rules bind the flash. A controller budgets them: data is valid 6 ns after the falling edge, which P7.3 turned into the half-cycle margin.
| Symbol (§9.6, §9.7) | Limit | Where it is measured |
|---|---|---|
| tCLQV, falling edge to output valid | 6 ns maximum | P7.3, on the SPI peripheral with its launch delay |
| tCLQX, output held after the falling edge | 1.5 ns minimum | P7.3 |
| tSHQZ, CS high to outputs floating | 7 ns maximum | P7.4; the QSPI model floats at once |
| Output load, input slopes (§9.5) | CL 30 pF; TR, TF 5 ns | the conditions the times are measured under |
The QSPI flash model drives on the falling edge with zero delay, so these are promises the bench does not exercise here.
| Quantity (§9.4, §9.2, §9.1) | Value |
|---|---|
| Supply for 133 MHz / for 104 MHz | 3.0-3.6 V / 2.7-3.0 V |
| Absolute maximum supply | -0.6 V to 4.6 V |
| Input low / high | up to 0.3 VCC / from 0.7 VCC |
| Output low / high at 100 µA | at most 0.2 V / at least VCC - 0.2 V |
| Input / output capacitance | 6 pF / 8 pF maximum |
| Leakage, inputs and I/O | ±2 µA |
| Standby / power-down current | 10 / 1 µA typical |
| Read current at 104 MHz | 12 mA typical, 20 mA maximum |
| Program, erase or status write current | 20 mA typical, 25 mA maximum |
Selected rows, in our words. A logic bench cannot measure any of them.
Try It
tm is the pin-timing checker. It prints one QSPI TIMING line per broken rule per frame; note prints how many violations it counted since the last note.
Set ctrl.half_period = 2; before step 1 to run at 250 MHz, and read which rules break.
1. 0Bh at the bench default, 100 MHz 0Bh: 0 timing violations 2. 03h, also at 100 MHz QSPI TIMING tcrlh 5.000 ns, limit 9.000 ns QSPI TIMING tcrll 5.000 ns, limit 9.000 ns QSPI TIMING fr_03h 10.000 ns, limit 20.000 ns 03h: 3 timing violations 3. 03h at 50 MHz 03h: 0 timing violations 4. Page program, then poll at once QSPI TIMING tshsl2 10.000 ns, limit 50.000 ns 02h: 1 timing violations 5. Page program, CS high 50 ns, then poll 02h: 0 timing violationsRead 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
Read included file: qspi_timing.sv
`timescale 1ns/1ps
module qspi_timing #(parameter real TSLCH=3,TCHSL=3,TCHSH=3,TSHCH=3,TDVCH=1,TCHDX=2,TSHSL1=10,TSHSL2=50,
PC=7.519,PC_READ=20,TWHSL=20,TSHWL=100,TRESET=1000)
(input wire cs_n,clk,input wire [3:0] io,ctrl_oe,input wire reset_n,srp,qe);
// broken counts every violation; each rule prints once per CS frame.
int broken=0,clocks=0;reg [7:0] command=0;bit first=1,long_gap=0,wrsr_guard=0,wp_high_at_fall=1;reg [31:0] shown=0;
function automatic string name(input int r);
case(r)0:name="tslch";1:name="tchsl";2:name="tchsh";3:name="tshch";4:name="tdvch";5:name="tchdx";6:name="tshsl1";7:name="tshsl2";8:name="tclh";9:name="tcll";10:name="tcrlh";11:name="tcrll";12:name="fr";13:name="fr_03h";14:name="twhsl";15:name="tshwl";16:name="treset";default:name="?";endcase
endfunction
realtime cs_fell=-1e9,cs_rose=-1e9,rise=-1e9,fall=-1e9,wp_changed=-1e9,wp_at_fall=-1e9,reset_fell=0;
realtime changed[0:3],high_min,low_min,period_min,pc;reg [3:0] sampled=0;
initial for(int k=0;k<4;k++)changed[k]=-1e9;
task automatic bad(input int rule,input real got,input real limit);
broken++;if(!shown[rule])$display("QSPI TIMING %s %0.3f ns, limit %0.3f ns",name(rule),got,limit);shown[rule]=1;
endtask
// tCHDX: a lane the flash sampled at the last rising edge must keep its value this long. A change in
// the same instant as the edge is a setup violation (tDVCH = 0), whichever process runs first.
task automatic lane(input int k);
if(!cs_n&&ctrl_oe[k]&&$realtime==rise)bad(4,0,TDVCH);
else if(!cs_n&&sampled[k]&&$realtime-rise<TCHDX)bad(5,$realtime-rise,TCHDX);
changed[k]=$realtime;
endtask
always @(io[0])lane(0);
always @(io[1])lane(1);
always @(io[3])lane(3);
always @(io[2])begin
lane(2);wp_changed=$realtime;
if(wrsr_guard&&io[2]===1'b0&&$realtime-cs_rose<TSHWL)bad(15,$realtime-cs_rose,TSHWL);
end
always @(negedge cs_n)begin
if(!first&&$realtime-cs_rose<(long_gap?TSHSL2:TSHSL1))bad(long_gap?7:6,$realtime-cs_rose,long_gap?TSHSL2:TSHSL1);
if($realtime-rise<TCHSL)bad(1,$realtime-rise,TCHSL);
shown=0;cs_fell=$realtime;wp_at_fall=wp_changed;wp_high_at_fall=io[2]!==1'b0;
clocks=0;command=0;wrsr_guard=0;sampled=0;high_min=1e9;low_min=1e9;period_min=1e9;
end
always @(posedge clk)begin
if(cs_n&&$realtime-cs_rose<TSHCH)bad(3,$realtime-cs_rose,TSHCH);
if(!cs_n)begin
if(clocks==0&&$realtime-cs_fell<TSLCH)bad(0,$realtime-cs_fell,TSLCH);
if(clocks>0)begin if($realtime-fall<low_min)low_min=$realtime-fall;if($realtime-rise<period_min)period_min=$realtime-rise;end
for(int k=0;k<4;k++)if(ctrl_oe[k]&&$realtime-changed[k]<TDVCH)bad(4,$realtime-changed[k],TDVCH);
sampled=ctrl_oe;clocks++;if(clocks<=8)command={command[6:0],io[0]};
end
rise=$realtime;
end
always @(negedge clk)begin if(!cs_n&&clocks>0&&$realtime-rise<high_min)high_min=$realtime-rise;fall=$realtime;end
always @(posedge cs_n)begin
if(clocks>0&&$realtime-rise<TCHSH)bad(2,$realtime-rise,TCHSH);
long_gap=0;
if(clocks>=8)begin
// Clock high and low at least 45 % of the shortest allowed period (§9.6 note 1), and the period.
pc=command==8'h03 ? PC_READ : PC;
if(high_min<0.45*pc)bad(command==8'h03 ? 10 : 8,high_min,0.45*pc);
if(low_min<0.45*pc)bad(command==8'h03 ? 11 : 9,low_min,0.45*pc);
if(period_min<pc)bad(command==8'h03 ? 13 : 12,period_min,pc);
// tSHSL2 after a program, erase or status write; tSHSL1 after anything else.
long_gap=command==8'h02||command==8'h32||command==8'h42||command==8'h20||command==8'h52||command==8'hd8
||command==8'hc7||command==8'h60||command==8'h44||command==8'h01||command==8'h31||command==8'h11;
// /WP setup before CS falls and hold after it rises: a Write Status frame with SRP = 1, QE = 0.
if((command==8'h01||command==8'h31||command==8'h11)&&srp&&!qe&&wp_high_at_fall)begin
if(cs_fell-wp_at_fall<TWHSL)bad(14,cs_fell-wp_at_fall,TWHSL);
wrsr_guard=1;
end
end
cs_rose=$realtime;first=0;
end
always @(negedge reset_n)reset_fell=$realtime;
always @(posedge reset_n)if($realtime-reset_fell<TRESET)bad(16,$realtime-reset_fell,TRESET);
endmodule
Check Yourself
Remember
- After power-up, wait tVSL (20 µs) before any instruction and tPUW (5 ms) before any write; keep CS high with VCC.
- Input rules bind the controller: tSLCH, tCHSH 3 ns, tDVCH 1 ns, tCHDX 2 ns, clock 133 MHz or 50 MHz for 03h.
- CS stays high 10 ns after a read and 50 ns after a program, erase or status write.
- A zero-delay model hides timing faults; a checker that timestamps the pins finds them.
Practice
- Honor Opcode-Dependent Flash Timing Fix · Medium