IDs, Security Registers, Power-Down and Reset
Identify the flash four ways, keep a serial number in a security register and lock it, put the flash to sleep and wake it, and reset it by instruction or by pin.
- Lesson
- P7.13
- Time
- 35 min
- Runs on
- Your browser
- Check
- 5 questions
A board maker stored each board's calibration in the main array. A firmware update erased the array and the calibration with it. The same data in a security register, locked with LB1, would have survived every erase, because no array instruction can reach it.
- Same address, two places
Address 001000h is in the array for 03h and in security register 1 for 48h. The two never overlap.
- Fixed bytes
The ID instructions return factory bytes; the address they carry is 000000h or dummy.
- One-time locks
Setting LB1, LB2 or LB3 makes that register read-only for good.
Read the diagram as text
- Instruction + address: one frame on the pins
- Memory array: 000000h-FFFFFFh: 03h, 02h, 20h ...
- Security registers 1-3: 001000h, 002000h, 003000h: 48h, 42h, 44h
- Identity: EFh, 17h, 4018h, 64-bit ID: 9Fh, 90h, ABh, 4Bh
- LB3-1 in SR2: one-time locks
- Instruction + address → Memory array: array instructions
- Instruction + address → Security registers 1-3: 48h, 42h, 44h
- Instruction + address → Identity: ID instructions
- LB3-1 in SR2 → Security registers 1-3: read-only once set
From §5 (Figure 2), §8.2.29 to §8.2.31 and §7.1.3 (LB). On the bench, 0010FEh read FFh in the array after the security register held A1h there.
Who Are You?
A driver reads an ID before it trusts anything else on the bus. Six instructions answer, with different lanes and different preambles. A monitor must know each preamble to find the clock where the data starts.
| Instruction | Lanes | Sent before the data | Data from clock | Returns |
|---|---|---|---|---|
| 9Fh JEDEC ID | 1-1-1 | nothing | 8 | EF 40 18 |
| 90h | 1-1-1 | address 000000h | 32 | EF 17 |
| 92h | 1-2-2 | address, then M = Fxh | 24 | EF 17 |
| 94h (QE = 1) | 1-4-4 | address, M = Fxh, 4 dummy clocks | 20 | EF 17 |
| ABh | 1-1-1 | 3 dummy bytes | 32 | 17 |
| 4Bh unique ID | 1-1-1 | 4 dummy bytes | 40 | 64 bits; the bench's is D26B150EA73C4981 |
Measured. EFh is Winbond; 17h the device ID; 4018h the JEDEC type and capacity of the IQ and JQ parts (§8.1.1). The 94h text in §8.2.25 puts the dummy clocks before the address; Figure 41 and the table in §8.1.3 put them after M, and the bench follows the figure.
Security Registers
Three 256-byte registers sit outside the array. They take their own erase, program and read instructions, so a chip erase never touches them. Each has a one-time lock bit, LB1 to LB3 in SR2 bits 3 to 5.
| Instruction | Like | Needs | Bench result |
|---|---|---|---|
| 44h + 00n000h: erase register n | 20h, sector erase | WEL; LBn = 0 | all FFh |
| 42h + address + data: program | 02h, with the 256-byte wrap | WEL; LBn = 0 | A1h A2h A3h from 0010FEh: wrap to 001000h |
| 48h + address + 8 dummy clocks: read | 0Bh, wrapping FFh to 00h | nothing | A1 A2 A3 FF |
| 44h after LB1 = 1 | - | - | ignored: 0010FEh still A1 |
| 31h writing LB1 = 0 | - | - | SR2 stays 0A: OTP |
| 44h after LB2 = 1, flash.lb_ignored | - | - | erased: FF |
Measured. Registers 1 to 3 sit at A15-A12 = 1, 2, 3 with A23-A16 and A11-A8 zero (§8.2.29). The datasheet does not say what other addresses do; the bench returns FFh (read at 004000h: FF).
The lock bits are one-time programmable: once set, the register is read-only forever, and a status write cannot clear the bit. Set them only after the data is verified.
Power-Down
B9h cuts the standby current about tenfold. In power-down the flash hears only ABh, so it doubles as a write guard. ABh wakes it, and the next instruction must wait tRES1.
| Datasheet | Bench | |
|---|---|---|
| B9h to power-down (tDP) | 3 µs maximum | at once |
| Current: standby ICC1 / power-down ICC2 | 10 / 1 µA typical; 60 / 20 µA maximum | not modelled |
| Heard in power-down | ABh only; even 05h is ignored | 05h read xx: nobody drives IO1 |
| ABh to ready, no ID read (tRES1) | 3 µs maximum | 100 ns |
| ABh with an ID read, to ready (tRES2) | 1.8 µs maximum | 100 ns |
| ABh while BUSY | ignored, no effect on the operation | ignored: 03 (BUSY, WEL) |
| 06h in power-down, flash.wake_on_any | - | WEL set: SR1 02 |
Datasheet §8.2.21, §8.2.22, §9.4 and §9.6; the bench column is measured or read from the model. Power-down is also a write guard (§6.5): a stray program cannot even be heard.
Software Reset and the /RESET Pin
Two instructions in a row reset the flash: 66h arms, 99h fires. Anything else between them disarms the pair. The reset reloads the non-volatile status bits, clears WEL, SUS and the wrap setting, and sets every individual lock again.
SOIC-16 and TFBGA parts also have a /RESET pin with an internal pull-up. Held low for 1 µs it ends any operation and outranks every other input.
| Sequence (SR1 holds a volatile 1Ch) | SR1 after |
|---|---|
| 99h alone | 1C |
| 66h, then 05h, then 99h | 1C |
| 66h, then 99h, then wait tRST | 00 |
| 66h, 99h with flash.reset_keeps_volatile | 1C |
| /RESET low 1 µs during a sector erase | 00: erase abandoned, 300000h still 00 |
| /RESET low 300 ns during a sector erase | 03: ignored, the erase finished (ff) |
Measured. The bench takes 100 ns for tRST (datasheet about 30 µs) and needs 1000 ns of /RESET low; the datasheet recommends 1 µs and says shorter pulses often work too (§6.4 note 2). The bench ignores them, a choice of the model.
The datasheet both says BUSY blocks all but the status reads and 75h, and that a reset ends a running operation. It advises checking BUSY and SUS first; the bench ignores 66h and 99h while BUSY.
Try It
id_read sends one ID instruction with its preamble, notes the clock count where data starts, and prints the bytes. sec_read is 48h with its 8 dummy clocks; addr_cmd sends 06h and an addressed instruction; st prints SR1 and SR2.
Try flash.reset_keeps_volatile = 1; before step 5: the last reset leaves SR1 at 1Ch, so the volatile protection survives a reset it should not.
1. Who are you? 9f: data from clock 8: ef 40 18 90: data from clock 32: ef 17 92: data from clock 24: ef 17 94: data from clock 20: ef 17 ab: data from clock 32: 17 4b: data from clock 40: d2 6b 15 0e a7 3c 49 81 2. Security register 1: erase, program across its end, read 48h at 0010fe: a1 a2 a3 ff 0010fe holds ff 3. LB1 = 1 locks it for good 48h at 0010fe: a1 SR1 00 SR2 0a 4. Power-down: only ABh is heard 05h reads xx SR1 00 SR2 0a 5. Reset needs 66h then 99h, back to back SR1 1c SR2 0a SR1 1c SR2 0a SR1 1c SR2 0a SR1 00 SR2 0a 6. The /RESET pin during a sector erase SR1 00 SR2 0a 300000 holds 00 SR1 03 SR2 0a 310000 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
- 9Fh, 90h, 92h, 94h, ABh and 4Bh identify the flash; each has its own lanes and preamble.
- Three 256-byte security registers (44h, 42h, 48h) live outside the array; LB1-3 make them read-only forever.
- B9h powers down: only ABh is heard afterwards, and the next instruction waits tRES1.
- 66h then 99h, with nothing between, resets the volatile state; /RESET low for 1 µs ends any operation.
Practice
- Respect Each Security Register Lock Fix · Medium