SPI · Module 11
Status Registers, WIP/WEL, and Polling
Why a flash write without the write-enable latch is discarded silently, why the device clears the latch itself so every operation needs its own WREN, why a busy wait must be bounded, and the sequencer that confirms the latch before issuing anything.
Chapter 11.3 preceded every write with a WREN and followed it with a busy wait, and treated both as ceremony. They are not.
You issue a page program without a
WRENfirst. The device acknowledges all 260 bytes and returns no error. What happened to your data?
Nothing happened to it. The device discarded the entire operation, silently, and a read-back returns the old contents. This is the single most common silent failure in flash programming, and it looks exactly like a read problem.
1. The Write-Enable Latch
A flash has an internal one-bit latch — WEL, write-enable latch — that gates every modifying operation. WREN (0x06) sets it. Nothing else does.
If WEL is clear when a program or erase arrives, the device accepts the command, accepts the address, accepts the data, and does nothing. There is no error bit, no NAK — SPI has no such thing — and no indication on the wires. The transaction looks identical to a successful one.
WREN 0x06 set WEL
WRDI 0x04 clear WEL
RDSR 0x05 read the status byte: bit 0 = WIP, bit 1 = WELAnd the property that catches everyone:
The device clears WEL itself when the operation completes. So the latch is not a mode you enter once — it is a one-shot, and every program and every erase needs its own WREN. A driver that arms it at initialisation works exactly once.
2. Reading the Status Register
RDSR is the only command a flash will answer meaningfully while busy, and two of its bits matter here.
WIP — work in progress. Set while a program or erase is running; cleared when it finishes. This is what the busy wait polls.
WEL — write-enable latch. The state of the latch from §1. Reading it back is how you discover that your WREN had no effect.
That second use is the one almost always skipped, and skipping it is why the failure is silent. WREN is acknowledged whether or not it did anything — a write-protected device accepts it happily and leaves WEL clear. The only way to know the latch actually took is to read it.
3. The Full Sequence
WREN, command, poll — and the latch clearing itself
8 cyclesThree things the figure makes concrete.
The status read at cell 1 is the step that is normally omitted. Without it, a device that ignored the WREN proceeds to cell 2 and the program is discarded.
WIP does not rise until the operation is issued. A driver that polls before issuing sees WIP clear, concludes the device is idle, and may conclude wrongly that a previous operation finished.
Both bits fall together at the end, because the device clears them. The WEL trace returning to 0 without any WRDI is the property that forces a fresh WREN next time.
4. Why the Busy Wait Must Be Bounded
The durations are wide:
page program hundreds of µs
sector erase tens of ms
block erase hundreds of ms
chip erase secondsAn unbounded poll loop is the obvious implementation and it is wrong for one reason: a device that has failed looks exactly like a device that is still busy. A part that has lost power mid-erase, or been damaged, or is simply not responding, holds MISO in a state that reads as WIP set — and an unbounded loop waits forever.
Worse, it waits forever inside a driver, usually with interrupts disabled or a lock held, so the symptom is not "flash timeout" but "the system stopped".
A bound converts that into a reported failure. Choosing it is the only subtlety: it must exceed the worst-case duration of the longest operation the driver issues, taken from the datasheet's maximum rather than its typical — and those differ by a factor of two or more for erase operations.
5. Building the Write Sequencer — Three HDLs
The circuit
Circuit. A seven-state machine over an abstract command channel.
State. The pending opcode, a poll counter, and the verdict flags.
Datapath. No arithmetic beyond the poll counter and its comparison against the bound.
Control. WREN → status read → check WEL → the operation → poll until WIP clears. The check is the state the whole block exists for: without WEL observed set, the operation is never issued.
Clock and reset. System clock; asynchronous active-low reset.
Enables. op_req is a single-cycle pulse, not a held request. Holding it would reissue the same command repeatedly — harmless for a WREN and catastrophic for an erase.
Timing. One command per handshake; the poll loop reissues RDSR until WIP clears or the bound is reached.
Synthesis. A small state machine, a counter and two comparators.
Limitations. One bound for all operations, which §4 argues should scale with the operation. It also cannot distinguish a device that is write-protected from one that is absent — both fail to set WEL — which is a real limit: err_no_wel says the latch did not take, not why.
Why the states accepting a new request include the terminal ones. S_IDLE, S_DONE and S_ERR all accept start. The verdict flags stay set until a new request arrives, so software has time to read them, and a start pulse is never swallowed by whatever state the previous operation left behind — a bug that makes the second operation appear to be ignored.
// flash_wip_seq.sv
//
// Chapter 11.4 -- the write-enable latch and the busy-poll loop.
//
// Every modifying operation on a serial flash is a FIVE-step protocol, not
// one command:
//
// 1. WREN set the write-enable latch
// 2. read status confirm WEL actually latched
// 3. the operation program or erase
// 4. poll status until WIP clears -- microseconds to seconds
// 5. WEL clears the DEVICE clears it when the operation completes
//
// Step 2 is the one everybody skips, and step 5 is the one everybody
// forgets. Together they produce the failure this block exists to prevent:
// a write issued without WEL set is DISCARDED SILENTLY. The device
// acknowledges every bit, drives no error, and simply does not perform the
// operation. The next read returns the old data, and the bug looks like a
// read problem.
//
// So this sequencer refuses to issue the operation unless it has SEEN WEL
// set in a status read. That is the safety property, and the testbench
// checks it by giving the sequencer a device that ignores WREN.
//
// The poll loop needs a TIMEOUT for a reason specific to flash: a chip
// erase can take seconds, and a device that has failed looks exactly like
// a device that is still busy. Without a bound, a failed erase hangs the
// system forever; with one, it reports a failure.
module flash_wip_seq #(
parameter int CNT_W = 24,
parameter int MAX_POLLS = 1000, // bound on the busy wait
parameter logic [7:0] OP_WREN = 8'h06,
parameter logic [7:0] OP_RDSR = 8'h05,
parameter int WIP_BIT = 0, // 25-series status bit positions
parameter int WEL_BIT = 1
) (
input logic clk,
input logic rst_n,
input logic start,
input logic [7:0] op_code_in, // the program or erase opcode
// Abstract command channel to whatever issues SPI frames.
output logic op_req,
output logic [7:0] op_code,
input logic op_ack,
input logic [7:0] op_rdata, // status byte, valid with ack
output logic busy,
output logic done,
output logic err_no_wel, // the device did not latch WREN
output logic err_timeout, // WIP never cleared
output logic [CNT_W-1:0] poll_count, // status reads this operation took
output logic wel_seen // diagnostic: WEL was observed set
);
localparam logic [2:0] S_IDLE = 3'd0;
localparam logic [2:0] S_WREN = 3'd1;
localparam logic [2:0] S_CHK = 3'd2;
localparam logic [2:0] S_CMD = 3'd3;
localparam logic [2:0] S_POLL = 3'd4;
localparam logic [2:0] S_DONE = 3'd5;
localparam logic [2:0] S_ERR = 3'd6;
logic [2:0] state;
logic [7:0] pending_op;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
op_req <= 1'b0;
op_code <= 8'h00;
pending_op <= 8'h00;
busy <= 1'b0;
done <= 1'b0;
err_no_wel <= 1'b0;
err_timeout <= 1'b0;
poll_count <= {CNT_W{1'b0}};
wel_seen <= 1'b0;
end else begin
// op_req is a single-cycle request. Holding it would issue the
// same command repeatedly -- and repeating a WREN is harmless
// while repeating an ERASE is not.
op_req <= 1'b0;
done <= 1'b0;
case (state)
// S_IDLE, S_DONE and S_ERR all accept a new request. The
// verdict flags are sticky until one arrives, so software
// has time to read them, and a start pulse is never
// swallowed by the state the previous operation left behind.
S_IDLE, S_DONE, S_ERR: begin
if (start) begin
pending_op <= op_code_in;
op_code <= OP_WREN;
op_req <= 1'b1;
busy <= 1'b1;
err_no_wel <= 1'b0;
err_timeout <= 1'b0;
wel_seen <= 1'b0;
poll_count <= {CNT_W{1'b0}};
state <= S_WREN;
end
end
S_WREN: begin
if (op_ack) begin
// Read status to CONFIRM the latch took. WREN is
// acknowledged whether or not it had any effect --
// a write-protected device accepts it and does
// nothing.
op_code <= OP_RDSR;
op_req <= 1'b1;
state <= S_CHK;
end
end
S_CHK: begin
if (op_ack) begin
if (op_rdata[WEL_BIT]) begin
wel_seen <= 1'b1;
op_code <= pending_op;
op_req <= 1'b1;
state <= S_CMD;
end else begin
// THE SAFETY PATH. Without WEL the operation
// would be discarded silently, so it is never
// issued at all and the failure is reported.
err_no_wel <= 1'b1;
busy <= 1'b0;
state <= S_ERR;
end
end
end
S_CMD: begin
if (op_ack) begin
op_code <= OP_RDSR;
op_req <= 1'b1;
state <= S_POLL;
end
end
S_POLL: begin
if (op_ack) begin
poll_count <= poll_count + 1'b1;
if (!op_rdata[WIP_BIT]) begin
// WIP cleared: the operation completed. The
// device has also cleared WEL by itself, which
// is why the next operation needs its own WREN.
busy <= 1'b0;
done <= 1'b1;
state <= S_DONE;
end else if (poll_count >= CNT_W'(MAX_POLLS - 1)) begin
// A failed device is indistinguishable from a
// busy one. The bound turns an unbounded hang
// into a reported failure.
err_timeout <= 1'b1;
busy <= 1'b0;
state <= S_ERR;
end else begin
op_code <= OP_RDSR;
op_req <= 1'b1;
end
end
end
default: ; // unreachable
endcase
end
end
endmodule// flash_wip_seq_tb.sv
//
// The testbench contains a behavioural flash with a real write-enable
// latch and a real busy time, and it RECORDS the opcode sequence the
// sequencer issues. The order is the specification, so checking it against
// a recorded list is the only way to test this block properly.
//
// The critical case is the third: a device that ignores WREN. The
// sequencer must never issue the operation, because a write without WEL is
// discarded silently and the data loss is invisible.
`timescale 1ns/1ps
module flash_wip_seq_tb;
localparam int CNT_W = 24;
localparam int MAX_POLLS = 1000;
localparam logic [7:0] OP_WREN = 8'h06;
localparam logic [7:0] OP_RDSR = 8'h05;
localparam logic [7:0] OP_PP = 8'h02; // page program
localparam logic [7:0] OP_SE = 8'h20; // sector erase
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
logic start = 1'b0;
logic [7:0] op_code_in = OP_PP;
logic op_req;
logic [7:0] op_code;
logic op_ack;
logic [7:0] op_rdata;
logic busy, done, err_no_wel, err_timeout, wel_seen;
logic [CNT_W-1:0] poll_count;
int errors = 0;
// ---- behavioural flash ----------------------------------------------
// wel and wip are the two status bits that matter. prog_cycles models a
// real program time; obey_wren models a write-protected or unresponsive
// part; stuck_wip models a device that has failed mid-operation.
logic f_wel = 1'b0;
logic f_wip = 1'b0;
int f_countdown = 0;
bit obey_wren = 1'b1;
bit stuck_wip = 1'b0;
int prog_cycles = 40;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
op_ack <= 1'b0;
op_rdata <= 8'h00;
f_wel <= 1'b0;
f_wip <= 1'b0;
f_countdown <= 0;
end else begin
// One-cycle ack, one cycle after the request.
op_ack <= op_req;
if (op_req) begin
case (op_code)
OP_WREN: if (obey_wren) f_wel <= 1'b1;
OP_RDSR: op_rdata <= {6'b0, f_wel, f_wip};
default: begin
// A modifying operation. WEL gates it -- and a
// device WITHOUT WEL set accepts the command and
// does nothing, which is the whole danger.
if (f_wel) begin
f_wip <= 1'b1;
f_countdown <= prog_cycles;
end
end
endcase
end
// The operation runs to completion in its own time, then the
// DEVICE clears both WIP and WEL.
if (f_wip && !stuck_wip) begin
if (f_countdown > 1) begin
f_countdown <= f_countdown - 1;
end else begin
f_wip <= 1'b0;
f_wel <= 1'b0;
end
end
end
end
flash_wip_seq #(
.CNT_W(CNT_W), .MAX_POLLS(MAX_POLLS),
.OP_WREN(OP_WREN), .OP_RDSR(OP_RDSR), .WIP_BIT(0), .WEL_BIT(1)
) dut (
.clk(clk), .rst_n(rst_n),
.start(start), .op_code_in(op_code_in),
.op_req(op_req), .op_code(op_code),
.op_ack(op_ack), .op_rdata(op_rdata),
.busy(busy), .done(done),
.err_no_wel(err_no_wel), .err_timeout(err_timeout),
.poll_count(poll_count), .wel_seen(wel_seen)
);
// ---- opcode recorder -------------------------------------------------
logic [7:0] seq_log [0:2047];
int seq_n = 0;
int first_mod = -1; // index of the first modifying opcode
int first_wren = -1;
// done is a single-cycle pulse, so it must be LATCHED to be observed.
// Sampling it after the busy-wait loop has exited would always miss it
// -- which is a testbench bug, not a design one: a pulse is the right
// shape for a completion event.
bit saw_done = 1'b0;
always_ff @(posedge clk) begin
if (rst_n && done) saw_done <= 1'b1;
if (rst_n && op_req && seq_n < 2048) begin
seq_log[seq_n] <= op_code;
if (op_code == OP_WREN && first_wren < 0) first_wren <= seq_n;
if (op_code != OP_WREN && op_code != OP_RDSR && first_mod < 0)
first_mod <= seq_n;
seq_n <= seq_n + 1;
end
end
task automatic run(input logic [7:0] code, input int limit);
int guard;
begin
@(negedge clk);
seq_n = 0; first_mod = -1; first_wren = -1; saw_done = 1'b0;
op_code_in = code;
start = 1'b1;
@(negedge clk);
start = 1'b0;
guard = 0;
while (busy && guard < limit) begin
@(negedge clk);
guard++;
end
if (guard >= limit) begin
$display(" FAIL: the sequencer never left busy within %0d cycles", limit);
errors++;
end
@(negedge clk);
end
endtask
initial begin
repeat (3) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// 1. A normal page program. The order is the specification:
// WREN, then a status read, then the operation, then polling.
obey_wren = 1'b1; stuck_wip = 1'b0; prog_cycles = 40;
run(OP_PP, 5000);
if (!saw_done || err_no_wel || err_timeout) begin
$display(" FAIL: a normal program did not complete cleanly (done=%0b no_wel=%0b to=%0b)",
saw_done, err_no_wel, err_timeout);
errors++;
end
if (seq_log[0] !== OP_WREN) begin
$display(" FAIL: the first opcode was 0x%02h, not WREN", seq_log[0]);
errors++;
end
if (seq_log[1] !== OP_RDSR) begin
$display(" FAIL: WREN was not followed by a status read");
errors++;
end
if (seq_log[2] !== OP_PP) begin
$display(" FAIL: the third opcode was 0x%02h, not the program", seq_log[2]);
errors++;
end
if (!wel_seen) begin
$display(" FAIL: the program was issued without WEL having been observed");
errors++;
end
$display(" page program: %0d opcodes issued, %0d status polls, sequence starts 0x%02h 0x%02h 0x%02h",
seq_n, poll_count, seq_log[0], seq_log[1], seq_log[2]);
// 2. ORDERING INVARIANT. The modifying opcode must come after a
// WREN, always -- checked from the recorded log rather than by
// inspection.
if (first_wren < 0 || first_mod < 0 || first_wren >= first_mod) begin
$display(" FAIL: the modifying opcode at %0d did not follow the WREN at %0d",
first_mod, first_wren);
errors++;
end
// 3. The device clears WEL itself when the operation finishes, so a
// second operation needs its own WREN. A sequencer that assumed
// WEL persisted would work once and fail forever after.
if (f_wel) begin
$display(" FAIL: the model's WEL was still set after completion");
errors++;
end
run(OP_PP, 5000);
if (!saw_done) begin
$display(" FAIL: a second program after the first did not complete");
errors++;
end
if (seq_log[0] !== OP_WREN) begin
$display(" FAIL: the second operation did not begin with its own WREN");
errors++;
end
$display(" second program: begins with its own WREN, %0d polls", poll_count);
// 4. THE SAFETY CASE. A device that ignores WREN -- write
// protected, or simply not responding. The operation must NEVER
// be issued, because it would be discarded silently.
obey_wren = 1'b0;
run(OP_PP, 5000);
if (!err_no_wel) begin
$display(" FAIL: a device that ignored WREN was not reported");
errors++;
end
if (saw_done) begin
$display(" FAIL: an operation that never ran was reported done");
errors++;
end
if (first_mod >= 0) begin
$display(" FAIL: the program opcode 0x%02h was issued without WEL",
seq_log[first_mod]);
errors++;
end
if (wel_seen) begin
$display(" FAIL: WEL was reported seen on a device that never set it");
errors++;
end
$display(" WREN ignored: reported err_no_wel, and the program was never issued (%0d opcodes, all WREN or RDSR)",
seq_n);
// 5. A device stuck busy. The poll loop must give up and report,
// not hang -- a failed erase is indistinguishable from a slow one.
obey_wren = 1'b1; stuck_wip = 1'b1;
run(OP_PP, 20000);
if (!err_timeout) begin
$display(" FAIL: a device stuck busy did not time out"); errors++;
end
if (saw_done) begin
$display(" FAIL: a timed-out operation was reported done"); errors++;
end
if (poll_count < CNT_W'(MAX_POLLS - 1)) begin
$display(" FAIL: timed out after only %0d polls, bound is %0d",
poll_count, MAX_POLLS);
errors++;
end
$display(" stuck busy: timed out after %0d polls and released busy",
poll_count);
// 6. Recovery. A working device after a timeout must succeed --
// the error flags must not be sticky across a new request.
stuck_wip = 1'b0;
run(OP_SE, 5000);
if (!saw_done || err_timeout || err_no_wel) begin
$display(" FAIL: a good erase after a timeout did not recover cleanly");
errors++;
end
if (seq_log[2] !== OP_SE) begin
$display(" FAIL: the erase opcode was not issued third");
errors++;
end
$display(" sector erase after recovery: opcode 0x%02h issued, %0d polls",
seq_log[2], poll_count);
// 7. A longer operation needs more polls but the same sequence --
// the protocol does not change with the duration.
prog_cycles = 400;
run(OP_SE, 20000);
if (!saw_done || err_timeout) begin
$display(" FAIL: a long erase did not complete"); errors++;
end
$display(" long erase: %0d polls for a 10x longer operation", poll_count);
if (errors == 0)
$display("PASS: every operation begins with WREN and a status read that confirms the latch, the modifying opcode is never issued unless WEL was observed set, the device clearing WEL on completion forces a fresh WREN for the next operation, a device stuck busy times out and releases rather than hanging, and a working device recovers cleanly afterwards");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmoduleThe testbench contains a behavioural flash with a real latch and a real busy time, and it records the opcode sequence — because the order is the specification, and an ordering claim can only be checked against a recorded log.
Four of its cases carry the weight.
The normal program must issue WREN, then RDSR, then the operation, in that order, verified from the log rather than by inspection.
The second program must begin with its own WREN, because the model's WEL was cleared by the device on completion — and the testbench asserts the model's latch really did clear, so the scenario is genuine rather than assumed.
A device that ignores WREN must produce err_no_wel and, critically, the log must contain no modifying opcode at all. That is the safety property, and checking it requires the log: an output-only check cannot distinguish "issued and failed" from "never issued".
A device stuck busy must time out after the bound, release busy, and never report done. The printed poll count confirms the bound was actually reached rather than the loop exiting early for another reason.
One detail in the testbench is worth naming because it is a testbench bug people write rather than a design one. done is a single-cycle pulse, which is the right shape for a completion event; sampling it after the busy-wait loop exits always misses it. The fix is to latch it in the testbench, not to widen it in the design.
// flash_wip_seq.v
//
// Chapter 11.4 -- the write-enable latch and the busy-poll loop, in
// Verilog-2001.
//
// Every modifying operation on a serial flash is a FIVE-step protocol:
//
// 1. WREN set the write-enable latch
// 2. read status confirm WEL actually latched
// 3. the operation program or erase
// 4. poll status until WIP clears
// 5. WEL clears the DEVICE clears it when the operation completes
//
// A write issued without WEL set is DISCARDED SILENTLY -- the device
// acknowledges every bit, drives no error, and does not perform the
// operation. So this sequencer refuses to issue the operation unless it has
// SEEN WEL set in a status read.
//
// The poll loop needs a TIMEOUT because a failed device looks exactly like
// a busy one, and a chip erase can legitimately take seconds.
module flash_wip_seq #(
parameter CNT_W = 24,
parameter MAX_POLLS = 1000, // bound on the busy wait
parameter [7:0] OP_WREN = 8'h06,
parameter [7:0] OP_RDSR = 8'h05,
parameter WIP_BIT = 0, // 25-series status bit positions
parameter WEL_BIT = 1
) (
input wire clk,
input wire rst_n,
input wire start,
input wire [7:0] op_code_in, // the program or erase opcode
// Abstract command channel to whatever issues SPI frames.
output reg op_req,
output reg [7:0] op_code,
input wire op_ack,
input wire [7:0] op_rdata, // status byte, valid with ack
output reg busy,
output reg done,
output reg err_no_wel, // the device did not latch WREN
output reg err_timeout, // WIP never cleared
output reg [CNT_W-1:0] poll_count, // status reads this operation took
output reg wel_seen // diagnostic: WEL was observed set
);
localparam [2:0] S_IDLE = 3'd0;
localparam [2:0] S_WREN = 3'd1;
localparam [2:0] S_CHK = 3'd2;
localparam [2:0] S_CMD = 3'd3;
localparam [2:0] S_POLL = 3'd4;
localparam [2:0] S_DONE = 3'd5;
localparam [2:0] S_ERR = 3'd6;
reg [2:0] state;
reg [7:0] pending_op;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
op_req <= 1'b0;
op_code <= 8'h00;
pending_op <= 8'h00;
busy <= 1'b0;
done <= 1'b0;
err_no_wel <= 1'b0;
err_timeout <= 1'b0;
poll_count <= {CNT_W{1'b0}};
wel_seen <= 1'b0;
end else begin
// op_req is a single-cycle request. Holding it would issue the
// same command repeatedly -- and repeating a WREN is harmless
// while repeating an ERASE is not.
op_req <= 1'b0;
done <= 1'b0;
case (state)
// S_IDLE, S_DONE and S_ERR all accept a new request. The
// verdict flags are sticky until one arrives, so a start
// pulse is never swallowed by the state the previous
// operation left behind.
S_IDLE, S_DONE, S_ERR: begin
if (start) begin
pending_op <= op_code_in;
op_code <= OP_WREN;
op_req <= 1'b1;
busy <= 1'b1;
err_no_wel <= 1'b0;
err_timeout <= 1'b0;
wel_seen <= 1'b0;
poll_count <= {CNT_W{1'b0}};
state <= S_WREN;
end
end
S_WREN: begin
if (op_ack) begin
// Read status to CONFIRM the latch took. WREN is
// acknowledged whether or not it had any effect --
// a write-protected device accepts it and does
// nothing.
op_code <= OP_RDSR;
op_req <= 1'b1;
state <= S_CHK;
end
end
S_CHK: begin
if (op_ack) begin
if (op_rdata[WEL_BIT]) begin
wel_seen <= 1'b1;
op_code <= pending_op;
op_req <= 1'b1;
state <= S_CMD;
end else begin
// THE SAFETY PATH. Without WEL the operation
// would be discarded silently, so it is never
// issued and the failure is reported.
err_no_wel <= 1'b1;
busy <= 1'b0;
state <= S_ERR;
end
end
end
S_CMD: begin
if (op_ack) begin
op_code <= OP_RDSR;
op_req <= 1'b1;
state <= S_POLL;
end
end
S_POLL: begin
if (op_ack) begin
poll_count <= poll_count + 1'b1;
if (!op_rdata[WIP_BIT]) begin
// WIP cleared: the operation completed. The
// device has also cleared WEL by itself, which
// is why the next operation needs its own WREN.
busy <= 1'b0;
done <= 1'b1;
state <= S_DONE;
end else if (poll_count >= (MAX_POLLS - 1)) begin
// A failed device is indistinguishable from a
// busy one. The bound turns an unbounded hang
// into a reported failure.
err_timeout <= 1'b1;
busy <= 1'b0;
state <= S_ERR;
end else begin
op_code <= OP_RDSR;
op_req <= 1'b1;
end
end
end
default: ; // unreachable
endcase
end
end
endmodule// flash_wip_seq_tb.v
//
// The same checks as the SystemVerilog testbench: a behavioural flash with
// a real write-enable latch and a real busy time, and a recorded opcode log
// -- because the ORDER is the specification.
//
// The critical case is the fourth: a device that ignores WREN. The
// sequencer must never issue the operation, because a write without WEL is
// discarded silently.
`timescale 1ns/1ps
module flash_wip_seq_tb;
parameter CNT_W = 24;
parameter MAX_POLLS = 1000;
localparam [7:0] OP_WREN = 8'h06;
localparam [7:0] OP_RDSR = 8'h05;
localparam [7:0] OP_PP = 8'h02; // page program
localparam [7:0] OP_SE = 8'h20; // sector erase
reg clk;
reg rst_n;
reg start;
reg [7:0] op_code_in;
wire op_req;
wire [7:0] op_code;
reg op_ack;
reg [7:0] op_rdata;
wire busy, done, err_no_wel, err_timeout, wel_seen;
wire [CNT_W-1:0] poll_count;
integer errors;
integer guard;
// ---- behavioural flash ----------------------------------------------
reg f_wel;
reg f_wip;
integer f_countdown;
reg obey_wren;
reg stuck_wip;
integer prog_cycles;
initial begin
clk = 1'b0; rst_n = 1'b0; start = 1'b0; op_code_in = OP_PP;
op_ack = 1'b0; op_rdata = 8'h00;
f_wel = 1'b0; f_wip = 1'b0; f_countdown = 0;
obey_wren = 1'b1; stuck_wip = 1'b0; prog_cycles = 40;
errors = 0;
end
always #5 clk = ~clk;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
op_ack <= 1'b0;
op_rdata <= 8'h00;
f_wel <= 1'b0;
f_wip <= 1'b0;
f_countdown <= 0;
end else begin
// One-cycle ack, one cycle after the request.
op_ack <= op_req;
if (op_req) begin
case (op_code)
OP_WREN: if (obey_wren) f_wel <= 1'b1;
OP_RDSR: op_rdata <= {6'b0, f_wel, f_wip};
default: begin
// A modifying operation. WEL gates it -- and a
// device WITHOUT WEL set accepts the command and
// does nothing, which is the whole danger.
if (f_wel) begin
f_wip <= 1'b1;
f_countdown <= prog_cycles;
end
end
endcase
end
// The operation runs to completion in its own time, then the
// DEVICE clears both WIP and WEL.
if (f_wip && !stuck_wip) begin
if (f_countdown > 1) begin
f_countdown <= f_countdown - 1;
end else begin
f_wip <= 1'b0;
f_wel <= 1'b0;
end
end
end
end
flash_wip_seq #(
.CNT_W(CNT_W), .MAX_POLLS(MAX_POLLS),
.OP_WREN(OP_WREN), .OP_RDSR(OP_RDSR), .WIP_BIT(0), .WEL_BIT(1)
) dut (
.clk(clk), .rst_n(rst_n),
.start(start), .op_code_in(op_code_in),
.op_req(op_req), .op_code(op_code),
.op_ack(op_ack), .op_rdata(op_rdata),
.busy(busy), .done(done),
.err_no_wel(err_no_wel), .err_timeout(err_timeout),
.poll_count(poll_count), .wel_seen(wel_seen)
);
// ---- opcode recorder -------------------------------------------------
reg [7:0] seq_log [0:2047];
integer seq_n;
integer first_mod;
integer first_wren;
// done is a single-cycle pulse, so it must be LATCHED to be observed.
// Sampling it after the busy-wait loop has exited would always miss it
// -- a testbench bug, not a design one: a pulse is the right shape for
// a completion event.
reg saw_done;
initial begin
seq_n = 0; first_mod = -1; first_wren = -1; saw_done = 1'b0;
end
always @(posedge clk) begin
if (rst_n && done) saw_done <= 1'b1;
if (rst_n && op_req && seq_n < 2048) begin
seq_log[seq_n] <= op_code;
if (op_code == OP_WREN && first_wren < 0) first_wren <= seq_n;
if (op_code != OP_WREN && op_code != OP_RDSR && first_mod < 0)
first_mod <= seq_n;
seq_n <= seq_n + 1;
end
end
task run;
input [7:0] code;
input integer limit;
begin
@(negedge clk);
seq_n = 0; first_mod = -1; first_wren = -1; saw_done = 1'b0;
op_code_in = code;
start = 1'b1;
@(negedge clk);
start = 1'b0;
guard = 0;
while (busy && guard < limit) begin
@(negedge clk);
guard = guard + 1;
end
if (guard >= limit) begin
$display(" FAIL: the sequencer never left busy within %0d cycles", limit);
errors = errors + 1;
end
@(negedge clk);
end
endtask
initial begin
repeat (3) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// 1. A normal page program. The order is the specification.
obey_wren = 1'b1; stuck_wip = 1'b0; prog_cycles = 40;
run(OP_PP, 5000);
if (!saw_done || err_no_wel || err_timeout) begin
$display(" FAIL: a normal program did not complete cleanly (done=%0b no_wel=%0b to=%0b)",
saw_done, err_no_wel, err_timeout);
errors = errors + 1;
end
if (seq_log[0] !== OP_WREN) begin
$display(" FAIL: the first opcode was 0x%02h, not WREN", seq_log[0]);
errors = errors + 1;
end
if (seq_log[1] !== OP_RDSR) begin
$display(" FAIL: WREN was not followed by a status read");
errors = errors + 1;
end
if (seq_log[2] !== OP_PP) begin
$display(" FAIL: the third opcode was 0x%02h, not the program", seq_log[2]);
errors = errors + 1;
end
if (!wel_seen) begin
$display(" FAIL: the program was issued without WEL having been observed");
errors = errors + 1;
end
$display(" page program: %0d opcodes issued, %0d status polls, sequence starts 0x%02h 0x%02h 0x%02h",
seq_n, poll_count, seq_log[0], seq_log[1], seq_log[2]);
// 2. ORDERING INVARIANT, from the recorded log.
if (first_wren < 0 || first_mod < 0 || first_wren >= first_mod) begin
$display(" FAIL: the modifying opcode at %0d did not follow the WREN at %0d",
first_mod, first_wren);
errors = errors + 1;
end
// 3. The device clears WEL itself, so a second operation needs its
// own WREN. A sequencer assuming WEL persisted would work once
// and fail forever after.
if (f_wel) begin
$display(" FAIL: the model's WEL was still set after completion");
errors = errors + 1;
end
run(OP_PP, 5000);
if (!saw_done) begin
$display(" FAIL: a second program after the first did not complete");
errors = errors + 1;
end
if (seq_log[0] !== OP_WREN) begin
$display(" FAIL: the second operation did not begin with its own WREN");
errors = errors + 1;
end
$display(" second program: begins with its own WREN, %0d polls", poll_count);
// 4. THE SAFETY CASE. A device that ignores WREN.
obey_wren = 1'b0;
run(OP_PP, 5000);
if (!err_no_wel) begin
$display(" FAIL: a device that ignored WREN was not reported");
errors = errors + 1;
end
if (saw_done) begin
$display(" FAIL: an operation that never ran was reported done");
errors = errors + 1;
end
if (first_mod >= 0) begin
$display(" FAIL: the program opcode 0x%02h was issued without WEL",
seq_log[first_mod]);
errors = errors + 1;
end
if (wel_seen) begin
$display(" FAIL: WEL was reported seen on a device that never set it");
errors = errors + 1;
end
$display(" WREN ignored: reported err_no_wel, and the program was never issued (%0d opcodes, all WREN or RDSR)",
seq_n);
// 5. A device stuck busy must give up and report, not hang.
obey_wren = 1'b1; stuck_wip = 1'b1;
run(OP_PP, 20000);
if (!err_timeout) begin
$display(" FAIL: a device stuck busy did not time out");
errors = errors + 1;
end
if (saw_done) begin
$display(" FAIL: a timed-out operation was reported done");
errors = errors + 1;
end
if (poll_count < (MAX_POLLS - 1)) begin
$display(" FAIL: timed out after only %0d polls, bound is %0d",
poll_count, MAX_POLLS);
errors = errors + 1;
end
$display(" stuck busy: timed out after %0d polls and released busy",
poll_count);
// 6. Recovery: the error flags must not be sticky across a new
// request.
stuck_wip = 1'b0;
run(OP_SE, 5000);
if (!saw_done || err_timeout || err_no_wel) begin
$display(" FAIL: a good erase after a timeout did not recover cleanly");
errors = errors + 1;
end
if (seq_log[2] !== OP_SE) begin
$display(" FAIL: the erase opcode was not issued third");
errors = errors + 1;
end
$display(" sector erase after recovery: opcode 0x%02h issued, %0d polls",
seq_log[2], poll_count);
// 7. A longer operation needs more polls but the same sequence.
prog_cycles = 400;
run(OP_SE, 20000);
if (!saw_done || err_timeout) begin
$display(" FAIL: a long erase did not complete");
errors = errors + 1;
end
$display(" long erase: %0d polls for a 10x longer operation", poll_count);
if (errors == 0)
$display("PASS: every operation begins with WREN and a status read that confirms the latch, the modifying opcode is never issued unless WEL was observed set, the device clearing WEL on completion forces a fresh WREN for the next operation, a device stuck busy times out and releases rather than hanging, and a working device recovers cleanly afterwards");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule-- flash_wip_seq.vhd
--
-- Chapter 11.4 -- the write-enable latch and the busy-poll loop, in VHDL.
--
-- Every modifying operation on a serial flash is a FIVE-step protocol:
--
-- 1. WREN set the write-enable latch
-- 2. read status confirm WEL actually latched
-- 3. the operation program or erase
-- 4. poll status until WIP clears
-- 5. WEL clears the DEVICE clears it when the operation completes
--
-- A write issued without WEL set is DISCARDED SILENTLY -- the device
-- acknowledges every bit, drives no error, and does not perform the
-- operation. So this sequencer refuses to issue the operation unless it has
-- SEEN WEL set in a status read.
--
-- The poll loop needs a TIMEOUT because a failed device looks exactly like
-- a busy one, and a chip erase can legitimately take seconds.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity flash_wip_seq is
generic (
CNT_W : positive := 24;
MAX_POLLS : positive := 1000; -- bound on the busy wait
OP_WREN : natural := 16#06#;
OP_RDSR : natural := 16#05#;
WIP_BIT : natural := 0; -- 25-series status bit positions
WEL_BIT : natural := 1
);
port (
clk : in std_logic;
rst_n : in std_logic;
start : in std_logic;
op_code_in : in unsigned(7 downto 0); -- program or erase opcode
-- Abstract command channel to whatever issues SPI frames.
op_req : out std_logic;
op_code : out unsigned(7 downto 0);
op_ack : in std_logic;
op_rdata : in std_logic_vector(7 downto 0); -- status, with ack
busy : out std_logic;
done : out std_logic;
err_no_wel : out std_logic; -- the device did not latch WREN
err_timeout : out std_logic; -- WIP never cleared
poll_count : out unsigned(CNT_W - 1 downto 0);
wel_seen : out std_logic -- diagnostic: WEL was observed set
);
end entity;
architecture rtl of flash_wip_seq is
type state_t is (S_IDLE, S_WREN, S_CHK, S_CMD, S_POLL, S_DONE, S_ERR);
signal state : state_t := S_IDLE;
signal pending_op : unsigned(7 downto 0) := (others => '0');
signal req_r : std_logic := '0';
signal code_r : unsigned(7 downto 0) := (others => '0');
signal busy_r : std_logic := '0';
signal done_r : std_logic := '0';
signal nowel_r : std_logic := '0';
signal to_r : std_logic := '0';
signal polls_r : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal wel_r : std_logic := '0';
begin
seq : process (clk, rst_n)
begin
if rst_n = '0' then
state <= S_IDLE;
req_r <= '0';
code_r <= (others => '0');
pending_op <= (others => '0');
busy_r <= '0';
done_r <= '0';
nowel_r <= '0';
to_r <= '0';
polls_r <= (others => '0');
wel_r <= '0';
elsif rising_edge(clk) then
-- op_req is a single-cycle request. Holding it would issue the
-- same command repeatedly -- and repeating a WREN is harmless
-- while repeating an ERASE is not.
req_r <= '0';
done_r <= '0';
case state is
-- S_IDLE, S_DONE and S_ERR all accept a new request. The
-- verdict flags are sticky until one arrives, so a start
-- pulse is never swallowed by the state the previous
-- operation left behind.
when S_IDLE | S_DONE | S_ERR =>
if start = '1' then
pending_op <= op_code_in;
code_r <= to_unsigned(OP_WREN, 8);
req_r <= '1';
busy_r <= '1';
nowel_r <= '0';
to_r <= '0';
wel_r <= '0';
polls_r <= (others => '0');
state <= S_WREN;
end if;
when S_WREN =>
if op_ack = '1' then
-- Read status to CONFIRM the latch took. WREN is
-- acknowledged whether or not it had any effect --
-- a write-protected device accepts it and does
-- nothing.
code_r <= to_unsigned(OP_RDSR, 8);
req_r <= '1';
state <= S_CHK;
end if;
when S_CHK =>
if op_ack = '1' then
if op_rdata(WEL_BIT) = '1' then
wel_r <= '1';
code_r <= pending_op;
req_r <= '1';
state <= S_CMD;
else
-- THE SAFETY PATH. Without WEL the operation
-- would be discarded silently, so it is never
-- issued and the failure is reported.
nowel_r <= '1';
busy_r <= '0';
state <= S_ERR;
end if;
end if;
when S_CMD =>
if op_ack = '1' then
code_r <= to_unsigned(OP_RDSR, 8);
req_r <= '1';
state <= S_POLL;
end if;
when S_POLL =>
if op_ack = '1' then
polls_r <= polls_r + 1;
if op_rdata(WIP_BIT) = '0' then
-- WIP cleared: the operation completed. The
-- device has also cleared WEL by itself, which
-- is why the next operation needs its own WREN.
busy_r <= '0';
done_r <= '1';
state <= S_DONE;
elsif to_integer(polls_r) >= MAX_POLLS - 1 then
-- A failed device is indistinguishable from a
-- busy one. The bound turns an unbounded hang
-- into a reported failure.
to_r <= '1';
busy_r <= '0';
state <= S_ERR;
else
code_r <= to_unsigned(OP_RDSR, 8);
req_r <= '1';
end if;
end if;
end case;
end if;
end process;
op_req <= req_r;
op_code <= code_r;
busy <= busy_r;
done <= done_r;
err_no_wel <= nowel_r;
err_timeout <= to_r;
poll_count <= polls_r;
wel_seen <= wel_r;
end architecture;-- flash_wip_seq_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: a
-- behavioural flash with a real write-enable latch and a real busy time,
-- and a recorded opcode log -- because the ORDER is the specification.
--
-- The critical case is the fourth: a device that ignores WREN. The
-- sequencer must never issue the operation, because a write without WEL is
-- discarded silently.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity flash_wip_seq_tb is
end entity;
architecture sim of flash_wip_seq_tb is
constant CNT_W : positive := 24;
constant MAX_POLLS : positive := 1000;
constant OP_WREN : natural := 16#06#;
constant OP_RDSR : natural := 16#05#;
constant OP_PP : natural := 16#02#; -- page program
constant OP_SE : natural := 16#20#; -- sector erase
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal start : std_logic := '0';
signal op_code_in : unsigned(7 downto 0) := to_unsigned(OP_PP, 8);
signal op_req : std_logic;
signal op_code : unsigned(7 downto 0);
signal op_ack : std_logic := '0';
signal op_rdata : std_logic_vector(7 downto 0) := (others => '0');
signal busy, done, err_no_wel, err_timeout, wel_seen : std_logic;
signal poll_count : unsigned(CNT_W - 1 downto 0);
-- behavioural flash state
signal f_wel : std_logic := '0';
signal f_wip : std_logic := '0';
signal f_countdown : natural := 0;
signal obey_wren : boolean := true;
signal stuck_wip : boolean := false;
signal prog_cycles : natural := 40;
-- opcode log
type log_t is array (0 to 2047) of unsigned(7 downto 0);
signal seq_log : log_t;
signal seq_n : natural := 0;
signal first_mod : integer := -1;
signal first_wren : integer := -1;
signal clr_log : std_logic := '0';
-- done is a single-cycle pulse, so it must be LATCHED to be observed.
-- Sampling it after the busy-wait loop has exited would always miss it
-- -- a testbench bug, not a design one: a pulse is the right shape for
-- a completion event.
signal saw_done : std_logic := '0';
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
-- Behavioural flash.
model : process (clk, rst_n)
begin
if rst_n = '0' then
op_ack <= '0';
op_rdata <= (others => '0');
f_wel <= '0';
f_wip <= '0';
f_countdown <= 0;
elsif rising_edge(clk) then
-- One-cycle ack, one cycle after the request.
op_ack <= op_req;
if op_req = '1' then
if op_code = to_unsigned(OP_WREN, 8) then
if obey_wren then f_wel <= '1'; end if;
elsif op_code = to_unsigned(OP_RDSR, 8) then
op_rdata <= "000000" & f_wel & f_wip;
else
-- A modifying operation. WEL gates it -- and a device
-- WITHOUT WEL set accepts the command and does nothing,
-- which is the whole danger.
if f_wel = '1' then
f_wip <= '1';
f_countdown <= prog_cycles;
end if;
end if;
end if;
-- The operation runs to completion in its own time, then the
-- DEVICE clears both WIP and WEL.
if f_wip = '1' and not stuck_wip then
if f_countdown > 1 then
f_countdown <= f_countdown - 1;
else
f_wip <= '0';
f_wel <= '0';
end if;
end if;
end if;
end process;
dut : entity work.flash_wip_seq
generic map (CNT_W => CNT_W, MAX_POLLS => MAX_POLLS,
OP_WREN => OP_WREN, OP_RDSR => OP_RDSR,
WIP_BIT => 0, WEL_BIT => 1)
port map (
clk => clk, rst_n => rst_n,
start => start, op_code_in => op_code_in,
op_req => op_req, op_code => op_code,
op_ack => op_ack, op_rdata => op_rdata,
busy => busy, done => done,
err_no_wel => err_no_wel, err_timeout => err_timeout,
poll_count => poll_count, wel_seen => wel_seen
);
-- Opcode recorder. The clear request arrives on its own signal because
-- two processes driving one signal is a multiple-driver error in VHDL.
recorder : process (clk)
begin
if rising_edge(clk) then
if clr_log = '1' then
seq_n <= 0;
first_mod <= -1;
first_wren <= -1;
saw_done <= '0';
elsif rst_n = '1' then
if done = '1' then
saw_done <= '1';
end if;
if op_req = '1' and seq_n < 2048 then
seq_log(seq_n) <= op_code;
if op_code = to_unsigned(OP_WREN, 8) and first_wren < 0 then
first_wren <= seq_n;
end if;
if op_code /= to_unsigned(OP_WREN, 8) and
op_code /= to_unsigned(OP_RDSR, 8) and first_mod < 0 then
first_mod <= seq_n;
end if;
seq_n <= seq_n + 1;
end if;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable guard : natural;
procedure run(code : natural; limit : natural) is
begin
wait until falling_edge(clk);
clr_log <= '1';
wait until falling_edge(clk);
clr_log <= '0';
op_code_in <= to_unsigned(code, 8);
start <= '1';
wait until falling_edge(clk);
start <= '0';
guard := 0;
while busy = '1' and guard < limit loop
wait until falling_edge(clk);
guard := guard + 1;
end loop;
if guard >= limit then
report " FAIL: the sequencer never left busy in time";
errs := errs + 1;
end if;
wait until falling_edge(clk);
end procedure;
begin
for i in 0 to 2 loop
wait until falling_edge(clk);
end loop;
rst_n <= '1';
wait until falling_edge(clk);
-- 1. A normal page program. The order is the specification.
obey_wren <= true; stuck_wip <= false; prog_cycles <= 40;
run(OP_PP, 5000);
if saw_done /= '1' or err_no_wel = '1' or err_timeout = '1' then
report " FAIL: a normal program did not complete cleanly";
errs := errs + 1;
end if;
if seq_log(0) /= to_unsigned(OP_WREN, 8) then
report " FAIL: the first opcode was not WREN"; errs := errs + 1;
end if;
if seq_log(1) /= to_unsigned(OP_RDSR, 8) then
report " FAIL: WREN was not followed by a status read";
errs := errs + 1;
end if;
if seq_log(2) /= to_unsigned(OP_PP, 8) then
report " FAIL: the third opcode was not the program";
errs := errs + 1;
end if;
if wel_seen /= '1' then
report " FAIL: the program was issued without WEL having been observed";
errs := errs + 1;
end if;
report " page program: " & integer'image(seq_n) &
" opcodes issued, " &
integer'image(to_integer(poll_count)) &
" status polls, sequence starts 0x06 0x05 0x02";
-- 2. ORDERING INVARIANT, from the recorded log.
if first_wren < 0 or first_mod < 0 or first_wren >= first_mod then
report " FAIL: the modifying opcode did not follow the WREN";
errs := errs + 1;
end if;
-- 3. The device clears WEL itself, so a second operation needs its
-- own WREN.
if f_wel = '1' then
report " FAIL: the model's WEL was still set after completion";
errs := errs + 1;
end if;
run(OP_PP, 5000);
if saw_done /= '1' then
report " FAIL: a second program after the first did not complete";
errs := errs + 1;
end if;
if seq_log(0) /= to_unsigned(OP_WREN, 8) then
report " FAIL: the second operation did not begin with its own WREN";
errs := errs + 1;
end if;
report " second program: begins with its own WREN, " &
integer'image(to_integer(poll_count)) & " polls";
-- 4. THE SAFETY CASE. A device that ignores WREN.
obey_wren <= false;
run(OP_PP, 5000);
if err_no_wel /= '1' then
report " FAIL: a device that ignored WREN was not reported";
errs := errs + 1;
end if;
if saw_done = '1' then
report " FAIL: an operation that never ran was reported done";
errs := errs + 1;
end if;
if first_mod >= 0 then
report " FAIL: the program opcode was issued without WEL";
errs := errs + 1;
end if;
if wel_seen = '1' then
report " FAIL: WEL was reported seen on a device that never set it";
errs := errs + 1;
end if;
report " WREN ignored: reported err_no_wel, and the program was never issued (" &
integer'image(seq_n) & " opcodes, all WREN or RDSR)";
-- 5. A device stuck busy must give up and report, not hang.
obey_wren <= true; stuck_wip <= true;
run(OP_PP, 20000);
if err_timeout /= '1' then
report " FAIL: a device stuck busy did not time out";
errs := errs + 1;
end if;
if saw_done = '1' then
report " FAIL: a timed-out operation was reported done";
errs := errs + 1;
end if;
if to_integer(poll_count) < MAX_POLLS - 1 then
report " FAIL: timed out after too few polls"; errs := errs + 1;
end if;
report " stuck busy: timed out after " &
integer'image(to_integer(poll_count)) &
" polls and released busy";
-- 6. Recovery: the error flags must not be sticky across a new
-- request.
stuck_wip <= false;
run(OP_SE, 5000);
if saw_done /= '1' or err_timeout = '1' or err_no_wel = '1' then
report " FAIL: a good erase after a timeout did not recover cleanly";
errs := errs + 1;
end if;
if seq_log(2) /= to_unsigned(OP_SE, 8) then
report " FAIL: the erase opcode was not issued third";
errs := errs + 1;
end if;
report " sector erase after recovery: opcode 0x20 issued, " &
integer'image(to_integer(poll_count)) & " polls";
-- 7. A longer operation needs more polls but the same sequence.
prog_cycles <= 400;
run(OP_SE, 20000);
if saw_done /= '1' or err_timeout = '1' then
report " FAIL: a long erase did not complete"; errs := errs + 1;
end if;
report " long erase: " & integer'image(to_integer(poll_count)) &
" polls for a 10x longer operation";
errors <= errs;
if errs = 0 then
report "PASS: every operation begins with WREN and a status read that confirms the latch, the modifying opcode is never issued unless WEL was observed set, the device clearing WEL on completion forces a fresh WREN for the next operation, a device stuck busy times out and releases rather than hanging, and a working device recovers cleanly afterwards";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;Parity
All three implement the same sequencer: identical ports and generics, a single-cycle command request, a mandatory status read confirming WEL before the operation is issued, a bounded poll loop, sticky verdict flags cleared by a new request, and terminal states that accept start. All three testbenches drive the same behavioural flash and report identical results — 24 opcodes and 21 polls for a normal program, two opcodes and no operation when WREN is ignored, a timeout at exactly 1000 polls, and 17 then 201 polls for the short and long erases.
6. Why a Verification Engineer Cares
// 1. THE SAFETY PROPERTY. A modifying opcode is never issued unless WEL
// was OBSERVED set in a status read. Not "unless WREN was sent" --
// WREN is acknowledged by a write-protected device that ignores it.
a_never_unarmed : assert property (
@(posedge clk) disable iff (!rst_n)
(op_req && is_modifying(op_code)) |-> wel_seen)
else $error("a modifying operation was issued without WEL observed");
// 2. ORDERING. The modifying opcode follows a WREN in the same
// operation. Weaker than property 1 and worth having separately,
// because it catches a reordering that property 1 would permit.
a_wren_first : assert property (
@(posedge clk) disable iff (!rst_n)
(op_req && is_modifying(op_code)) |->
(wren_issued_this_op && rdsr_issued_this_op))
else $error("the operation did not follow WREN and a status read");
// 3. NO REPEATED ISSUE. op_req is a pulse. A held request would reissue
// the command -- harmless for WREN, destructive for an erase.
a_single_pulse : assert property (
@(posedge clk) disable iff (!rst_n)
(op_req) |=> !op_req)
else $error("the command request was held for more than one cycle");
// 4. LIVENESS. The sequencer always leaves busy. This is the property
// that the bound exists to guarantee, and the failure it prevents is
// a system hang rather than a wrong answer -- which is why it needs
// a liveness property rather than a safety one.
a_terminates : assert property (
@(posedge clk) disable iff (!rst_n)
(start && !busy) |-> ##[1:$] (done || err_no_wel || err_timeout))
else $error("the sequencer never reached a verdict");
// 5. EXACTLY ONE VERDICT. done, err_no_wel and err_timeout are mutually
// exclusive -- two set at once means a path that both succeeded and
// failed, and software will believe whichever it checks first.
a_one_verdict : assert property (
@(posedge clk) disable iff (!rst_n)
($onehot0({done, err_no_wel, err_timeout})))
else $error("more than one verdict was reported");
// 6. WEL IS NOT ASSUMED TO PERSIST. After a completed operation the
// sequencer must not treat the latch as still armed.
a_no_stale_wel : assert property (
@(posedge clk) disable iff (!rst_n)
(done) |=> !wel_seen)
else $error("WEL was still considered armed after completion");Properties 1 and 2 look redundant and are not, and the distinction is the chapter's technical heart. Property 2 says a WREN was sent; property 1 says the latch was observed set. A write-protected device satisfies property 2 and fails property 1, and that is precisely the case where the data is silently lost. Sending a command is not evidence that it had an effect.
Property 4 is a liveness property, and it is here for a reason worth generalising: the failure it prevents is a hang, not a wrong answer. Safety properties cannot express "eventually", so a design whose worst failure is waiting forever needs a liveness property or it has no check at all.
Coverage must include the failure paths, which a working device never exercises:
covergroup flash_wip_cg @(posedge clk iff verdict);
cp_verdict : coverpoint verdict_class {
bins completed = {V_DONE};
bins no_wel = {V_NO_WEL}; // needs a protected device
bins timed_out = {V_TIMEOUT}; // needs a stuck device
}
// Poll count spans four orders of magnitude across real operations,
// and the interesting bins are the extremes: an operation that
// completes on the FIRST poll exercises a different path from one
// that polls a thousand times.
cp_polls : coverpoint poll_count {
bins immediate = {1}; // done on the first read
bins few = {[2:16]};
bins many = {[17:512]};
bins at_bound = {[MAX_POLLS-1:MAX_POLLS]};
}
cp_op : coverpoint op_code_in {
bins program = {8'h02};
bins sector = {8'h20};
bins block = {8'hD8};
bins chip = {8'hC7};
}
// Recovery: a good operation AFTER each kind of failure. A suite
// that never does this cannot show the flags are non-sticky.
cp_after : coverpoint prev_verdict {
bins after_ok = {V_DONE};
bins after_no_wel = {V_NO_WEL};
bins after_timeout = {V_TIMEOUT};
}
x_op_verdict : cross cp_op, cp_verdict;
endgroupcp_after is the coverpoint that is almost always missing. The sticky-flag bug — a verdict that persists into the next operation — is only reachable by running a good operation after a failing one, and a suite that groups its failure tests at the end never does.
cp_polls' immediate bin matters more than it looks: an operation that completes on the very first status read takes a different path through the loop than one that iterates, and on a fast simulation model it is the only path taken unless the model is deliberately slowed.
7. Why an FPGA or ASIC Engineer Cares
Confirm the latch; do not assume the WREN worked. One extra status read per write turns a silent data loss into a reported error. It is the cheapest reliability improvement available in a flash driver.
Make WREN part of the operation, not a separate API call. If software can issue a program without a WREN, eventually it will. A controller that emits the whole five-step sequence from one request removes the possibility.
Bound the wait, and scale the bound with the operation. A single timeout sized for a chip erase makes a failed page program hang for seconds. Three bits of operation class and a shift are enough.
Pulse the command request; never hold it. A held request reissues the command. For an erase that means erasing repeatedly, and the wear consequences of Chapter 11.1 arrive very quickly.
Keep the verdict until the next request. Sticky flags let software read them at its own pace. Clearing them on the next start rather than on a read avoids the race where software reads just after the sequencer has moved on.
Expose the poll count. It is a free measurement of how long operations actually take on this device at this temperature, and a poll count that has crept upward over a product's life is an early indication of wear.
8. Failure Signature — A Configuration That Saves Correctly Until the Second Time
Symptom. A device stores calibration in flash. Writing it at the factory works: the values read back correctly and survive power cycles. A field update of the same values appears to succeed — no errors — but the old values persist. A power cycle confirms nothing changed. Repeating the update does not help.
What "the first write worked" establishes. The wiring, the opcode, the addressing and the erase logic are all correct, because a correct write happened once. So the fault is in something that differs between the first write and later ones, and that is a much shorter list than a general write failure.
Plausible mechanisms.
WRENissued once at initialisation. The factory path runs it and writes immediately; the field path writes without one because the driver believes the latch is still armed. The device cleared it after the first operation. This fits perfectly.- A block-protection bit set after the factory write, deliberately or by a status-register write that also set protection bits.
- The status register write-protect bit (SRWD) combined with a WP pin, which locks the protection settings themselves.
- A missing erase on the update path, so the new values are ANDed with the old — but that would produce changed values rather than unchanged ones, so it does not fit.
- The update writing to a different address than the read, which would show the old values at the read address and new ones elsewhere.
The discriminating observation. Read the status register immediately after the WREN in the failing path. There are exactly three informative outcomes:
- WEL clear — the
WRENhad no effect. Either it was never sent, or the device is write-protected. Check whether the driver sends it at all on this path. - WEL set, and WIP never rises after the operation — the operation was accepted and discarded, which points at block protection covering that address.
- WEL set and WIP rises normally — the write really is happening, and the problem is the address, so compare the write and read addresses.
That single read splits the case three ways, which is why §2 argues it belongs in the sequence permanently rather than being added during debugging.
The fix. Emit WREN as part of every modifying operation, and confirm WEL before issuing. If WEL reads clear despite a WREN, read the protection bits — the factory process very often sets them as its last step, which is exactly why the failure appears only on the second write.
Why this is so common. Because "arm the write-enable at startup" reads like sensible initialisation, and it works for the first write. The device clearing the latch is a behaviour that has to be known in advance — nothing in a successful first write hints at it, and the second failure gives no error to follow.
9. Common Misconceptions
10. Reason It Through
Work this before reading the answer.
A driver writes a 4 KB sector: one sector erase followed by sixteen page programs. It issues one
WRENbefore the erase and none afterwards. Erase and programs all complete without error.What is in the sector afterwards, and how many
WRENcommands were actually needed?
Count the modifying operations. One erase plus sixteen programs is seventeen modifying operations, so seventeen WREN commands are needed — one before each.
Now trace what happens with one.
The single WREN sets WEL. The sector erase consumes it: the erase runs, and on completion the device clears WEL. So after the erase, the latch is clear.
Page program 1 arrives with WEL clear. It is accepted, acknowledged, and discarded. WIP never sets, so a poll loop sees WIP clear immediately and concludes the program finished — instantly, which a driver measuring nothing will not notice.
Page programs 2 through 16 are identical: all discarded.
So the sector contains all 0xFF. The erase worked; not one byte of data was written.
And here is the part that makes it hard to find. Every operation reported success. The erase genuinely succeeded. The programs were acknowledged. A driver checking only for errors sees a clean run.
What would a verify step see? It depends entirely on what it compares:
- Reading back and comparing against the intended data catches it immediately — everything is 0xFF.
- Checking that WIP cleared does not, because WIP cleared instantly on every program.
- Checking that the programs were acknowledged does not, because they were.
And the sharpest detail: the programs completed faster than physically possible. Sixteen page programs should take several milliseconds. These took microseconds, because none of them happened. A write that completes too quickly is evidence of a write that did not occur — and a driver recording poll counts would see 1 where it expected dozens, which is why §7 recommends exposing that count.
The general lesson. A one-shot latch cleared by the device means the arming is part of each operation, not part of the session. And the diagnostic signature of a silently discarded write is not an error but an absence of the expected delay — a write cycle that returns immediately did nothing, and that is measurable without reading any data back at all.
11. Understanding Check
12. Summary
Every modifying flash operation is a five-step protocol, not one command: WREN, a status read confirming the latch, the operation, a bounded poll until WIP clears, and the device clearing the latch itself.
A write issued with WEL clear is discarded silently — acknowledged completely, no error, old data on read-back. It is the commonest silent failure in flash programming and it presents as a read problem.
WREN is acknowledged whether or not it worked, so the latch must be read back. A write-protected device accepts the command and leaves WEL clear, and "I sent a WREN" is not evidence that the latch is set.
The device clears the latch on completion, so the arming belongs to each operation rather than to the session. A driver that arms once works exactly once — and the second failure gives no error to follow.
The busy wait must be bounded, because a failed device is indistinguishable from a busy one and an unbounded loop hangs the system rather than reporting a flash error. The bound should scale with the operation, since one sized for a chip erase makes a failed page program hang for seconds.
The command request is a pulse: holding it reissues the command, and for an erase that is destructive.
For verification, the safety property is that a modifying opcode is never issued unless WEL was observed set — distinct from, and stronger than, "a WREN preceded it". A liveness property is required because the worst failure is a hang. And coverage must include a good operation after each failure, since the sticky-flag bug is reachable no other way.
Finally, the field diagnostic: a write cycle that returns faster than physically possible did not happen. That is measurable from the poll count alone, without reading any data back.
13. What Comes Next
Everything so far has assumed software is already running. The hardest flash transaction is the one that happens before any software exists.
Chapter 11.5 — Boot from SPI Flash works the first-fetch sequence a boot ROM issues: waking a part that previous software may have left asleep, verifying it is the device the system was built for, and reading an image header whose length must not be trusted until the header itself has been verified — because a length taken from unvalidated flash decides how much memory to overwrite. It ends with the sequencer that never fetches after any failure, in all three HDLs.
Continue learning
Related tutorials
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
- Related topic
Deriving Mode Behaviour from CPOL and CPHA
The four SPI modes are a two-bit truth table you can rebuild in seconds. The standard numbering, the derivation, the complete mode decoder in three HDLs, and the assertions that keep a configurable design honest.
- Related topic
Command, Address, and Data Phases
How a device layers a transaction onto a raw byte stream: why the opcode decides the shape of everything after it, how a slave tracks phases with no phase marker, and the sequencer that requires in three HDLs.
- Related topic
Dummy Phases and Read Latency
Why a device needs turnaround before it can answer, why dummy is counted in clock cycles rather than bytes, how its length grows with frequency, and the one-byte data offset a mismatch produces.
