Skip to content
VLSI Mentor

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 WREN first. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   WREN  0x06    set WEL
   WRDI  0x04    clear WEL
   RDSR  0x05    read the status byte:  bit 0 = WIP,  bit 1 = WEL

And 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 cycles
A sequence of frames over time. A WREN frame sets the write-enable latch. A page program frame follows, after which work-in-progress is set. Three status read frames follow while work-in-progress remains set. Finally both work-in-progress and the write-enable latch return low.confirm WEL setconfirm WEL setdevice clears bothdevice clears bothframeWRENRDSRPPRDSRRDSRRDSR----WELWIPt0t1t2t3t4t5t6t7
Figure 1 — the five steps of one modifying operation. WREN sets the latch; a status read confirms it; the operation is issued; status is polled until WIP clears; and the device clears both bits itself, so the next operation needs its own WREN.

Three 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   page program     hundreds of µs
   sector erase     tens of ms
   block erase      hundreds of ms
   chip erase       seconds

An 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.

Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_seq.sv — confirm the latch, then issue, then bound the wait
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_seq_tb.sv — a device that ignores WREN, and one that never finishes
// 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

endmodule

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_seq.v — the same sequencer in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_seq_tb.v — the same behavioural flash in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_seq.vhd — the same sequencer in VHDL
-- 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;
Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_seq_tb.vhd — the same behavioural flash in VHDL
-- 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

Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_seq.sva — the safety property and the liveness one
   // 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:

Azvya Education Pvt. Ltd.VLSI Mentor
flash_wip_cg.sv — the paths a good device never takes
   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;
   endgroup

cp_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.

  • WREN issued 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 WREN had 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 WREN before the erase and none afterwards. Erase and programs all complete without error.

What is in the sector afterwards, and how many WREN commands 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