Skip to content
VLSI Mentor

SPI · Module 11

FPGA Configuration over SPI

Master and slave configuration modes and why they differ only in who clocks, why the sync word must be searched byte by byte, why a blank flash is indistinguishable from preamble, and the loader that shares one datapath between both modes.

Chapter 11.5 had a processor read its own image. An FPGA does the same job, and it can do it from either end of the link.

In master configuration mode the FPGA drives SCLK and reads its own bitstream. In slave mode a host clocks the bitstream in. How much of the logic differs?

Almost none. The two modes differ in who starts and who clocks — and that is all. The bitstream parsing is identical, and a design that duplicates it has two places to fix every bug.

1. The Two Modes

Master configuration. The FPGA comes out of reset, drives SCLK itself, asserts a chip select to a flash it is wired to, issues a read command, and consumes the bytes that come back. The FPGA is the SPI master; the flash is an ordinary slave.

Slave configuration. An external host — a microcontroller, a test fixture, a processor on the same board — drives SCLK and pushes the bitstream into the FPGA. The FPGA is now the SPI slave, and it issues nothing at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                    who drives SCLK    who issues a command    FPGA's role
   master mode      the FPGA           the FPGA                master
   slave mode       the host           nobody                  slave

Both exist for good reasons. Master mode needs no host at all, which suits a standalone board. Slave mode lets a host choose which bitstream to load, hold the FPGA unconfigured until the system is ready, or reconfigure it at runtime — and it needs no flash dedicated to the FPGA.

What is identical is everything after the first byte arrives: discard the preamble, find the sync word, count configuration words, and decide whether the bitstream was complete. That is the bulk of the logic, and §6 shares it.

2. The Bitstream's Front End

A bitstream does not begin with data. It begins with padding — conventionally 0xFF bytes — followed by a sync word, a fixed pattern that tells the configuration logic where the real content starts.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   FF FF FF FF ... FF   AA 99 55 66   <configuration words>
   └──── preamble ────┘  └─ sync ──┘

The padding exists because the configuration logic needs to be running and stable before the first real bit arrives, and because neither side can be sure how many clocks the other will emit before it settles. So the loader's job is to ignore everything until it recognises the sync word.

And here is the detail that decides whether a loader works:

The sync word must be searched for byte by byte, not on four-byte boundaries.

Nothing guarantees the preamble is a multiple of four bytes long. A loader that examines only aligned positions finds the sync word when the padding happens to be a multiple of four and misses it the other three times in four — producing an FPGA that configures on some builds and not others, with no pattern anyone can see until someone counts the padding bytes.

3. Searching, and Knowing When to Stop

The search is a shift register: shift each arriving byte in, compare the last four bytes against the pattern, and stop when they match.

That works and it has one gap. A blank flash reads 0xFF forever, which is indistinguishable from preamble — so a loader searching for a sync word that will never arrive searches indefinitely.

The consequence is specific and nasty: an FPGA that never finishes configuring shows no symptom at all except a DONE pin that stays low. It does not fail, it does not report anything, and every voltage on the board is correct. A bound on the preamble length converts that into a reported failure, and there is no other way to get one.

The same argument applies at the other end. A truncated bitstream — a host that stopped early, a flash whose image is short — leaves the loader having found the sync word and counted fewer words than expected. Configuring an FPGA from a partial bitstream is considerably worse than not configuring it, because a partly configured device may drive pins in ways nothing predicted. So a short stream must be an error, never a done.

4. The Two Modes, Side by Side

An FPGA configuration loader. In master mode the FPGA's own configuration controller issues a read command to a serial flash and receives bytes. In slave mode an external host drives the clock and pushes bytes in. Both sources feed a single byte stream into a shared sync-word search, which feeds a shared word counter, which drives the configuration memory and the DONE indication. A preamble bound and a stream-end check feed a failure path.Serial flashmaster mode onlyExternal hostslave mode only — drivesSCLKConfig controllerissues a read in mastermode, nothing in slaveByte streamone port — the parsing doesnot careSync searchbyte by byte, notword-alignedWord countercounts to the expectedlengthConfig memorywritten only after syncDONEonly on a completebitstreamFailureno sync in bound, or streamtoo shortreadbytesbytesone byte at a timeafter the matchbound exceededwordswhen completestream ended early12
Figure 1 — the same parsing, two sources. In master mode the FPGA issues a read to its own flash; in slave mode a host pushes bytes in. Both feed one byte stream into one sync search and one word counter, which is why the two modes cannot disagree about a bitstream.

The single Byte stream node is the design decision. Two sources, one parser — so master and slave mode cannot disagree about whether a bitstream is valid, because there is only one implementation of the question.

5. What the Loader Sees

Preamble, sync, then configuration words

10 cycles
A byte stream showing three padding bytes of 0xFF followed by the four sync bytes AA, 99, 55, 66, then three configuration word bytes. A second lane shows the loader's state progressing from preamble through searching to synchronised and then loading.window fillingwindow fillingsync matchedsync matchedbyteFFFFFFAA995566W0W0W0statepreprepresrchsrchsrchSYNCloadloadloadt0t1t2t3t4t5t6t7t8t9
Figure 2 — the search window filling with padding, then the sync word arriving. The window holds the last four bytes and is compared on every byte, which is what allows a preamble of any length — here three bytes, deliberately not a multiple of four.

Note what happens between cells 3 and 6. The first three sync bytes produce no match — they are partial matches passing through the window — and only the fourth completes it. A loader that counts every non-matching byte as preamble therefore over-counts by three, which is a real defect in a diagnostic that reports padding length. §6's implementation corrects for it at the match, and the sweep in its testbench is what caught it.

6. Building the Loader — Three HDLs

The circuit

Circuit. A shared byte-stream parser with a mode-dependent front end.

State. A 32-bit search window, a word counter, a byte-within-word counter, and a preamble count.

Datapath. The window shifts by one byte per arriving byte and is compared against the sync pattern every time. After the match, bytes are counted into words.

Control. Six states. In master mode the first is a read command to the flash; in slave mode that state is skipped entirely and the loader goes straight to searching. Everything after that is shared.

Clock and reset. System clock; asynchronous active-low reset.

Enables. eos signals end of stream, so a host or flash that stops can be distinguished from one that is merely slow. Without it a truncated bitstream is indistinguishable from a pause.

Timing. One byte per byte_valid. The loader imposes no rate.

Synthesis. A 32-bit shift register, three counters, a comparator and a small state machine.

Limitations. One expected length, fixed at elaboration. A real bitstream carries its length in a header after the sync word, which turns the constant into a register loaded from the stream — and reintroduces Chapter 11.5's question of when that length may be trusted.

The preamble correction. The sync word is four bytes, so the three bytes preceding the completing one were counted as preamble on their way through the window. They are not padding, and the count is corrected when the match lands — otherwise the reported padding length is always three too high, which is wrong in a way that looks right.

Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_load.sv — one parser, two front ends
// fpga_cfg_load.sv
//
// Chapter 11.6 -- loading a bitstream over SPI, in master and slave
// configuration modes.
//
// The two modes look like different problems and are not:
//
//   MASTER mode  the FPGA drives SCLK and reads the bitstream from a flash
//                it selects itself. It must ISSUE a read command.
//   SLAVE mode   an external host drives SCLK and pushes the bitstream in.
//                The FPGA issues nothing and simply consumes bytes.
//
// The difference is entirely about who starts and who clocks. The bitstream
// PARSING -- discard preamble, find the sync word, count configuration
// words -- is identical, and this block shares that datapath between the
// two modes rather than duplicating it. The testbench proves the sharing is
// real by pushing the same byte stream through both modes and requiring
// identical results.
//
// The sync word matters more than it looks. A bitstream begins with padding
// (conventionally 0xFF bytes) and the loader must discard it until the sync
// pattern appears. Searching BYTE BY BYTE rather than on four-byte
// boundaries is essential: the padding length is not guaranteed to be a
// multiple of four, and a loader that only checks aligned positions misses
// the sync word on every bitstream whose preamble is not a multiple of four
// bytes long.
//
// A blank flash reads 0xFF forever, which is indistinguishable from
// preamble. Without a bound on the preamble, a loader waits for a sync word
// that will never come -- and an FPGA that never finishes configuring shows
// no symptom except a DONE pin that stays low.

module fpga_cfg_load #(
    parameter logic [31:0] SYNC_WORD    = 32'hAA995566,
    parameter int MAX_PREAMBLE = 64,          // bytes before giving up
    parameter int EXP_WORDS    = 16,          // configuration words expected
    parameter logic [7:0]  OP_READ   = 8'h03,
    parameter int CFG_BASE     = 24'h000000
) (
    input  logic        clk,
    input  logic        rst_n,

    input  logic        mode_master,   // 1 master, 0 slave
    input  logic        start,
    input  logic        eos,           // end of stream (host or flash stopped)

    // Byte stream in -- from the flash in master mode, from the host in
    // slave mode. One port, because the parsing does not care.
    input  logic        byte_valid,
    input  logic [7:0]  byte_in,

    // Master mode only: the read this block issues for itself.
    output logic        flash_req,
    output logic [7:0]  flash_cmd,
    output logic [23:0] flash_addr,
    input  logic        flash_ack,

    output logic        busy,
    output logic        sync_found,
    output logic        cfg_done,
    output logic        cfg_err,
    output logic [1:0]  err_reason,    // 1 no sync, 2 stream ended early
    output logic [15:0] words_loaded,
    output logic [15:0] preamble_bytes
);

    localparam logic [1:0] E_NONE    = 2'd0;
    localparam logic [1:0] E_NO_SYNC = 2'd1;
    localparam logic [1:0] E_SHORT   = 2'd2;

    // The sync word is four bytes wide, so the three bytes before the one
    // that completes the match were themselves counted as preamble on their
    // way through the search window. They are not padding, and the count is
    // corrected when the match lands -- otherwise preamble_bytes reports the
    // padding length plus three, which is wrong in a way that looks right.
    localparam int SYNC_BYTES = 4;

    localparam logic [2:0] S_IDLE = 3'd0;
    localparam logic [2:0] S_CMD  = 3'd1;   // master only
    localparam logic [2:0] S_SYNC = 3'd2;
    localparam logic [2:0] S_LOAD = 3'd3;
    localparam logic [2:0] S_DONE = 3'd4;
    localparam logic [2:0] S_ERR  = 3'd5;

    logic [2:0]  state;
    logic [31:0] sr;          // byte-by-byte sync search window
    logic [1:0]  byte_in_word;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state          <= S_IDLE;
            sr             <= 32'h0;
            byte_in_word   <= 2'd0;
            flash_req      <= 1'b0;
            flash_cmd      <= 8'h00;
            flash_addr     <= 24'h0;
            busy           <= 1'b0;
            sync_found     <= 1'b0;
            cfg_done       <= 1'b0;
            cfg_err        <= 1'b0;
            err_reason     <= E_NONE;
            words_loaded   <= 16'd0;
            preamble_bytes <= 16'd0;
        end else begin
            flash_req <= 1'b0;

            case (state)
                S_IDLE, S_DONE, S_ERR: begin
                    if (start) begin
                        sr             <= 32'h0;
                        byte_in_word   <= 2'd0;
                        busy           <= 1'b1;
                        sync_found     <= 1'b0;
                        cfg_done       <= 1'b0;
                        cfg_err        <= 1'b0;
                        err_reason     <= E_NONE;
                        words_loaded   <= 16'd0;
                        preamble_bytes <= 16'd0;

                        if (mode_master) begin
                            // Master mode: the FPGA fetches for itself.
                            flash_cmd  <= OP_READ;
                            flash_addr <= 24'(CFG_BASE);
                            flash_req  <= 1'b1;
                            state      <= S_CMD;
                        end else begin
                            // Slave mode: nothing to issue. The host will
                            // start clocking whenever it is ready, and the
                            // parsing below is identical.
                            state <= S_SYNC;
                        end
                    end
                end

                S_CMD: begin
                    if (flash_ack) state <= S_SYNC;
                end

                S_SYNC: begin
                    if (eos) begin
                        // The stream ended before a sync word appeared.
                        cfg_err    <= 1'b1;
                        err_reason <= E_NO_SYNC;
                        busy       <= 1'b0;
                        state      <= S_ERR;
                    end else if (byte_valid) begin
                        // Shift by ONE BYTE, not one word. The preamble
                        // length is not guaranteed to be a multiple of four.
                        sr <= {sr[23:0], byte_in};

                        if ({sr[23:0], byte_in} == SYNC_WORD) begin
                            sync_found   <= 1'b1;
                            byte_in_word <= 2'd0;
                            // Give back the partial-match bytes: they were
                            // the sync word, not padding.
                            preamble_bytes <= preamble_bytes
                                              - 16'(SYNC_BYTES - 1);
                            state        <= S_LOAD;
                        end else begin
                            preamble_bytes <= preamble_bytes + 16'd1;
                            if (preamble_bytes >= 16'(MAX_PREAMBLE - 1)) begin
                                // A blank flash reads 0xFF forever, which
                                // is indistinguishable from preamble. The
                                // bound turns an invisible hang into a
                                // reported failure.
                                cfg_err    <= 1'b1;
                                err_reason <= E_NO_SYNC;
                                busy       <= 1'b0;
                                state      <= S_ERR;
                            end
                        end
                    end
                end

                S_LOAD: begin
                    if (eos) begin
                        if (words_loaded < 16'(EXP_WORDS)) begin
                            // A truncated bitstream. Configuring an FPGA
                            // with a partial bitstream is far worse than
                            // not configuring it, so this must be an error
                            // and never a done.
                            cfg_err    <= 1'b1;
                            err_reason <= E_SHORT;
                            busy       <= 1'b0;
                            state      <= S_ERR;
                        end
                    end else if (byte_valid) begin
                        if (byte_in_word == 2'd3) begin
                            byte_in_word <= 2'd0;
                            words_loaded <= words_loaded + 16'd1;
                            if (words_loaded + 16'd1 >= 16'(EXP_WORDS)) begin
                                busy     <= 1'b0;
                                cfg_done <= 1'b1;
                                state    <= S_DONE;
                            end
                        end else begin
                            byte_in_word <= byte_in_word + 2'd1;
                        end
                    end
                end

                default: ;   // unreachable
            endcase
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_load_tb.sv — master and slave, proved equivalent
// fpga_cfg_load_tb.sv
//
// The central test is the EQUIVALENCE one: the same byte stream is pushed
// through master mode and slave mode, and the results must be identical.
// That is the claim the shared datapath makes, and it is the claim a reader
// would otherwise have to take on trust.
//
// The preamble length is then swept across every residue modulo four,
// because a loader that searches for the sync word on four-byte boundaries
// passes exactly one quarter of those cases.

`timescale 1ns/1ps

module fpga_cfg_load_tb;

    localparam logic [31:0] SYNC_WORD    = 32'hAA995566;
    localparam int MAX_PREAMBLE = 64;
    localparam int EXP_WORDS    = 16;
    localparam logic [7:0] OP_READ = 8'h03;

    localparam logic [1:0] E_NONE    = 2'd0;
    localparam logic [1:0] E_NO_SYNC = 2'd1;
    localparam logic [1:0] E_SHORT   = 2'd2;

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    always #5 clk = ~clk;

    logic        mode_master = 1'b1;
    logic        start = 1'b0;
    logic        eos = 1'b0;
    logic        byte_valid = 1'b0;
    logic [7:0]  byte_in = 8'h00;

    logic        flash_req;
    logic [7:0]  flash_cmd;
    logic [23:0] flash_addr;
    logic        flash_ack = 1'b0;

    logic        busy, sync_found, cfg_done, cfg_err;
    logic [1:0]  err_reason;
    logic [15:0] words_loaded, preamble_bytes;

    int errors = 0;
    int n_flash_req = 0;

    // Captured results, for the master/slave equivalence comparison.
    logic        m_done, m_err, m_sync;
    logic [15:0] m_words, m_pre;
    logic [1:0]  m_reason;

    fpga_cfg_load #(
        .SYNC_WORD(SYNC_WORD), .MAX_PREAMBLE(MAX_PREAMBLE),
        .EXP_WORDS(EXP_WORDS), .OP_READ(OP_READ), .CFG_BASE(24'h000000)
    ) dut (
        .clk(clk), .rst_n(rst_n),
        .mode_master(mode_master), .start(start), .eos(eos),
        .byte_valid(byte_valid), .byte_in(byte_in),
        .flash_req(flash_req), .flash_cmd(flash_cmd),
        .flash_addr(flash_addr), .flash_ack(flash_ack),
        .busy(busy), .sync_found(sync_found),
        .cfg_done(cfg_done), .cfg_err(cfg_err), .err_reason(err_reason),
        .words_loaded(words_loaded), .preamble_bytes(preamble_bytes)
    );

    // A flash or host that acks a read one cycle after the request.
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            flash_ack   <= 1'b0;
        end else begin
            flash_ack <= flash_req;
            if (flash_req) n_flash_req <= n_flash_req + 1;
        end
    end

    task automatic push(input logic [7:0] b);
        begin
            @(negedge clk);
            byte_in = b; byte_valid = 1'b1;
            @(negedge clk);
            byte_valid = 1'b0;
        end
    endtask

    // Push a complete bitstream: `pre` padding bytes, the sync word, then
    // `words` configuration words.
    task automatic push_stream(input int pre, input int words,
                               input logic [7:0] pad);
        begin
            for (int i = 0; i < pre; i++) push(pad);
            push(SYNC_WORD[31:24]); push(SYNC_WORD[23:16]);
            push(SYNC_WORD[15:8]);  push(SYNC_WORD[7:0]);
            for (int w = 0; w < words; w++) begin
                push(8'(w));       push(8'h11);
                push(8'h22);       push(8'h33);
            end
        end
    endtask

    task automatic begin_load(input bit master);
        begin
            @(negedge clk);
            mode_master = master;
            n_flash_req = 0;
            start = 1'b1;
            @(negedge clk);
            start = 1'b0;
            if (master) begin
                // Give the command time to be issued and acked.
                repeat (4) @(negedge clk);
            end
        end
    endtask

    task automatic end_stream;
        begin
            @(negedge clk);
            eos = 1'b1;
            @(negedge clk);
            eos = 1'b0;
            @(negedge clk);
        end
    endtask

    initial begin
        repeat (3) @(negedge clk);
        rst_n = 1'b1;
        @(negedge clk);

        // 1. MASTER mode, a well-formed bitstream with 7 padding bytes --
        //    deliberately not a multiple of four.
        begin_load(1'b1);
        if (n_flash_req == 0) begin
            $display("  FAIL: master mode issued no read command"); errors++;
        end
        if (flash_cmd !== OP_READ) begin
            $display("  FAIL: master mode issued opcode 0x%02h", flash_cmd);
            errors++;
        end
        push_stream(7, EXP_WORDS, 8'hFF);
        if (!cfg_done || cfg_err) begin
            $display("  FAIL: a good bitstream did not configure (done=%0b err=%0b)",
                     cfg_done, cfg_err);
            errors++;
        end
        if (!sync_found) begin
            $display("  FAIL: the sync word was not found"); errors++;
        end
        if (words_loaded !== 16'(EXP_WORDS)) begin
            $display("  FAIL: %0d words loaded, expected %0d",
                     words_loaded, EXP_WORDS);
            errors++;
        end
        if (preamble_bytes !== 16'd7) begin
            $display("  FAIL: %0d preamble bytes counted, expected 7",
                     preamble_bytes);
            errors++;
        end
        m_done = cfg_done; m_err = cfg_err; m_sync = sync_found;
        m_words = words_loaded; m_pre = preamble_bytes; m_reason = err_reason;
        $display("  master mode: %0d read command(s) issued, sync found after %0d preamble bytes, %0d words loaded",
                 n_flash_req, preamble_bytes, words_loaded);

        // 2. SLAVE mode, the IDENTICAL stream. Nothing may be issued, and
        //    every result must match master mode exactly.
        begin_load(1'b0);
        push_stream(7, EXP_WORDS, 8'hFF);
        if (n_flash_req != 0) begin
            $display("  FAIL: slave mode issued %0d read command(s) -- it must issue none",
                     n_flash_req);
            errors++;
        end
        if (cfg_done !== m_done || cfg_err !== m_err ||
            sync_found !== m_sync || words_loaded !== m_words ||
            preamble_bytes !== m_pre || err_reason !== m_reason) begin
            $display("  FAIL: slave mode differed from master mode (done=%0b/%0b words=%0d/%0d pre=%0d/%0d)",
                     cfg_done, m_done, words_loaded, m_words,
                     preamble_bytes, m_pre);
            errors++;
        end
        $display("  slave mode:  no command issued, identical result -- %0d words, %0d preamble bytes",
                 words_loaded, preamble_bytes);

        // 3. PREAMBLE ALIGNMENT SWEEP. A loader that searches on four-byte
        //    boundaries passes one case in four. Every residue must work.
        for (int pre = 0; pre < 12; pre++) begin
            begin_load(1'b0);
            push_stream(pre, EXP_WORDS, 8'hFF);
            if (!cfg_done || cfg_err) begin
                $display("  FAIL: a %0d-byte preamble (residue %0d) did not configure",
                         pre, pre % 4);
                errors++;
            end
            if (preamble_bytes !== 16'(pre)) begin
                $display("  FAIL: a %0d-byte preamble was counted as %0d",
                         pre, preamble_bytes);
                errors++;
            end
        end
        $display("  preamble sweep: 0 to 11 padding bytes all configure, every residue modulo four");

        // 4. A blank flash. All-ones forever is indistinguishable from
        //    preamble, so only the bound catches it.
        begin_load(1'b1);
        for (int i = 0; i < MAX_PREAMBLE + 4; i++) begin
            if (cfg_err) break;
            push(8'hFF);
        end
        if (!cfg_err || err_reason !== E_NO_SYNC) begin
            $display("  FAIL: a blank flash was not reported (err=%0b reason=%0d)",
                     cfg_err, err_reason);
            errors++;
        end
        if (cfg_done) begin
            $display("  FAIL: a blank flash reported configuration done"); errors++;
        end
        $display("  blank flash: no sync within %0d bytes, reported reason %0d, never done",
                 MAX_PREAMBLE, err_reason);

        // 5. A TRUNCATED bitstream. Configuring an FPGA from a partial
        //    bitstream is worse than not configuring it, so a short stream
        //    must be an error and never a done.
        begin_load(1'b0);
        push_stream(4, EXP_WORDS - 4, 8'hFF);   // four words short
        if (cfg_done) begin
            $display("  FAIL: a truncated bitstream reported done"); errors++;
        end
        end_stream();
        if (!cfg_err || err_reason !== E_SHORT) begin
            $display("  FAIL: a truncated bitstream was not reported (err=%0b reason=%0d)",
                     cfg_err, err_reason);
            errors++;
        end
        if (cfg_done) begin
            $display("  FAIL: a truncated bitstream reported done after end of stream");
            errors++;
        end
        $display("  truncated:   %0d of %0d words then end of stream -- reported reason %0d, never done",
                 words_loaded, EXP_WORDS, err_reason);

        // 6. A stream that ends before any sync word at all.
        begin_load(1'b0);
        push(8'hFF); push(8'hFF);
        end_stream();
        if (!cfg_err || err_reason !== E_NO_SYNC) begin
            $display("  FAIL: a stream ending before sync was not reported");
            errors++;
        end

        // 7. Recovery: a good stream after every failure must configure.
        begin_load(1'b1);
        push_stream(5, EXP_WORDS, 8'hFF);
        if (!cfg_done || cfg_err) begin
            $display("  FAIL: a good bitstream after failures did not configure");
            errors++;
        end
        $display("  recovery:    configured after four failures, %0d words",
                 words_loaded);

        if (errors == 0)
            $display("PASS: master mode issues its own read and slave mode issues none, both produce identical results from the identical byte stream, the sync word is found at every preamble alignment, a blank flash is reported rather than waited on forever, and a truncated bitstream is an error rather than a done");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule

The central test is the equivalence one, and it exists because the shared-datapath claim is otherwise something a reader has to take on trust. The same byte stream is pushed through master mode and then slave mode, and every output must match exactly — while master mode must have issued a read command and slave mode must have issued none.

The second test is the preamble sweep: padding lengths from 0 to 11, covering every residue modulo four three times over. A loader searching on word boundaries passes exactly one quarter of those, so the sweep converts §2's argument into a check. It also verifies the reported preamble length at each step, which is how the off-by-three described in §5 was found.

Then the two failures that have no symptom. A blank flash — all-ones forever — must be reported after the bound rather than waited on, and must never report done. A truncated bitstream must report a short stream at end-of-stream and, again, never report done. Both are checked for the absence of cfg_done as well as the presence of the error, because a loader that reported both would let a downstream consumer see whichever it checked first.

Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_load.v — the same loader in Verilog-2001
// fpga_cfg_load.v
//
// Chapter 11.6 -- loading a bitstream over SPI in master and slave
// configuration modes, in Verilog-2001.
//
//   MASTER mode  the FPGA drives SCLK and reads the bitstream from a flash
//                it selects itself. It must ISSUE a read command.
//   SLAVE mode   an external host drives SCLK and pushes the bitstream in.
//                The FPGA issues nothing and simply consumes bytes.
//
// The difference is entirely about who starts and who clocks. The bitstream
// PARSING -- discard preamble, find the sync word, count configuration
// words -- is identical, and this block shares that datapath rather than
// duplicating it.
//
// Searching for the sync word BYTE BY BYTE rather than on four-byte
// boundaries is essential: the padding length is not guaranteed to be a
// multiple of four, and a loader that only checks aligned positions misses
// the sync word on three bitstreams in four.
//
// A blank flash reads 0xFF forever, which is indistinguishable from
// preamble. Without a bound, a loader waits for a sync word that will never
// come -- and an FPGA that never configures shows no symptom except a DONE
// pin that stays low.

module fpga_cfg_load #(
    parameter [31:0] SYNC_WORD = 32'hAA995566,
    parameter MAX_PREAMBLE = 64,          // bytes before giving up
    parameter EXP_WORDS    = 16,          // configuration words expected
    parameter [7:0] OP_READ = 8'h03,
    parameter CFG_BASE     = 24'h000000
) (
    input  wire        clk,
    input  wire        rst_n,

    input  wire        mode_master,   // 1 master, 0 slave
    input  wire        start,
    input  wire        eos,           // end of stream

    // Byte stream in -- from the flash in master mode, from the host in
    // slave mode. One port, because the parsing does not care.
    input  wire        byte_valid,
    input  wire [7:0]  byte_in,

    // Master mode only: the read this block issues for itself.
    output reg         flash_req,
    output reg  [7:0]  flash_cmd,
    output reg  [23:0] flash_addr,
    input  wire        flash_ack,

    output reg         busy,
    output reg         sync_found,
    output reg         cfg_done,
    output reg         cfg_err,
    output reg  [1:0]  err_reason,    // 1 no sync, 2 stream ended early
    output reg  [15:0] words_loaded,
    output reg  [15:0] preamble_bytes
);

    localparam [1:0] E_NONE    = 2'd0;
    localparam [1:0] E_NO_SYNC = 2'd1;
    localparam [1:0] E_SHORT   = 2'd2;

    // The sync word is four bytes wide, so the three bytes before the one
    // that completes the match were counted as preamble on their way through
    // the search window. They are not padding, and the count is corrected
    // when the match lands.
    localparam SYNC_BYTES = 4;

    localparam [2:0] S_IDLE = 3'd0;
    localparam [2:0] S_CMD  = 3'd1;   // master only
    localparam [2:0] S_SYNC = 3'd2;
    localparam [2:0] S_LOAD = 3'd3;
    localparam [2:0] S_DONE = 3'd4;
    localparam [2:0] S_ERR  = 3'd5;

    reg [2:0]  state;
    reg [31:0] sr;          // byte-by-byte sync search window
    reg [1:0]  byte_in_word;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state          <= S_IDLE;
            sr             <= 32'h0;
            byte_in_word   <= 2'd0;
            flash_req      <= 1'b0;
            flash_cmd      <= 8'h00;
            flash_addr     <= 24'h0;
            busy           <= 1'b0;
            sync_found     <= 1'b0;
            cfg_done       <= 1'b0;
            cfg_err        <= 1'b0;
            err_reason     <= E_NONE;
            words_loaded   <= 16'd0;
            preamble_bytes <= 16'd0;
        end else begin
            flash_req <= 1'b0;

            case (state)
                S_IDLE, S_DONE, S_ERR: begin
                    if (start) begin
                        sr             <= 32'h0;
                        byte_in_word   <= 2'd0;
                        busy           <= 1'b1;
                        sync_found     <= 1'b0;
                        cfg_done       <= 1'b0;
                        cfg_err        <= 1'b0;
                        err_reason     <= E_NONE;
                        words_loaded   <= 16'd0;
                        preamble_bytes <= 16'd0;

                        if (mode_master) begin
                            // Master mode: the FPGA fetches for itself.
                            flash_cmd  <= OP_READ;
                            flash_addr <= CFG_BASE;
                            flash_req  <= 1'b1;
                            state      <= S_CMD;
                        end else begin
                            // Slave mode: nothing to issue. The host starts
                            // clocking when ready; the parsing is identical.
                            state <= S_SYNC;
                        end
                    end
                end

                S_CMD: begin
                    if (flash_ack) state <= S_SYNC;
                end

                S_SYNC: begin
                    if (eos) begin
                        // The stream ended before a sync word appeared.
                        cfg_err    <= 1'b1;
                        err_reason <= E_NO_SYNC;
                        busy       <= 1'b0;
                        state      <= S_ERR;
                    end else if (byte_valid) begin
                        // Shift by ONE BYTE, not one word.
                        sr <= {sr[23:0], byte_in};

                        if ({sr[23:0], byte_in} == SYNC_WORD) begin
                            sync_found   <= 1'b1;
                            byte_in_word <= 2'd0;
                            // Give back the partial-match bytes: they were
                            // the sync word, not padding.
                            preamble_bytes <= preamble_bytes - (SYNC_BYTES - 1);
                            state        <= S_LOAD;
                        end else begin
                            preamble_bytes <= preamble_bytes + 16'd1;
                            if (preamble_bytes >= (MAX_PREAMBLE - 1)) begin
                                // A blank flash reads 0xFF forever. The
                                // bound turns an invisible hang into a
                                // reported failure.
                                cfg_err    <= 1'b1;
                                err_reason <= E_NO_SYNC;
                                busy       <= 1'b0;
                                state      <= S_ERR;
                            end
                        end
                    end
                end

                S_LOAD: begin
                    if (eos) begin
                        if (words_loaded < EXP_WORDS) begin
                            // Configuring an FPGA with a partial bitstream
                            // is far worse than not configuring it, so this
                            // must be an error and never a done.
                            cfg_err    <= 1'b1;
                            err_reason <= E_SHORT;
                            busy       <= 1'b0;
                            state      <= S_ERR;
                        end
                    end else if (byte_valid) begin
                        if (byte_in_word == 2'd3) begin
                            byte_in_word <= 2'd0;
                            words_loaded <= words_loaded + 16'd1;
                            if ((words_loaded + 16'd1) >= EXP_WORDS) begin
                                busy     <= 1'b0;
                                cfg_done <= 1'b1;
                                state    <= S_DONE;
                            end
                        end else begin
                            byte_in_word <= byte_in_word + 2'd1;
                        end
                    end
                end

                default: ;   // unreachable
            endcase
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_load_tb.v — the same equivalence test in Verilog-2001
// fpga_cfg_load_tb.v
//
// The central test is the EQUIVALENCE one: the same byte stream is pushed
// through master mode and slave mode, and the results must be identical --
// which is the claim the shared datapath makes.
//
// The preamble length is then swept across every residue modulo four,
// because a loader searching on four-byte boundaries passes exactly one
// quarter of those cases.

`timescale 1ns/1ps

module fpga_cfg_load_tb;

    localparam [31:0] SYNC_WORD = 32'hAA995566;
    parameter MAX_PREAMBLE = 64;
    parameter EXP_WORDS    = 16;
    localparam [7:0] OP_READ = 8'h03;

    localparam [1:0] E_NONE    = 2'd0;
    localparam [1:0] E_NO_SYNC = 2'd1;
    localparam [1:0] E_SHORT   = 2'd2;

    reg clk;
    reg rst_n;
    reg mode_master;
    reg start;
    reg eos;
    reg byte_valid;
    reg [7:0] byte_in;

    wire        flash_req;
    wire [7:0]  flash_cmd;
    wire [23:0] flash_addr;
    reg         flash_ack;

    wire        busy, sync_found, cfg_done, cfg_err;
    wire [1:0]  err_reason;
    wire [15:0] words_loaded, preamble_bytes;

    integer errors;
    integer n_flash_req;
    integer i, pre, w;

    // Captured results, for the master/slave equivalence comparison.
    reg        m_done, m_err, m_sync;
    reg [15:0] m_words, m_pre;
    reg [1:0]  m_reason;

    initial begin
        clk = 1'b0; rst_n = 1'b0; mode_master = 1'b1;
        start = 1'b0; eos = 1'b0; byte_valid = 1'b0; byte_in = 8'h00;
        flash_ack = 1'b0;
        errors = 0; n_flash_req = 0;
    end
    always #5 clk = ~clk;

    fpga_cfg_load #(
        .SYNC_WORD(SYNC_WORD), .MAX_PREAMBLE(MAX_PREAMBLE),
        .EXP_WORDS(EXP_WORDS), .OP_READ(OP_READ), .CFG_BASE(24'h000000)
    ) dut (
        .clk(clk), .rst_n(rst_n),
        .mode_master(mode_master), .start(start), .eos(eos),
        .byte_valid(byte_valid), .byte_in(byte_in),
        .flash_req(flash_req), .flash_cmd(flash_cmd),
        .flash_addr(flash_addr), .flash_ack(flash_ack),
        .busy(busy), .sync_found(sync_found),
        .cfg_done(cfg_done), .cfg_err(cfg_err), .err_reason(err_reason),
        .words_loaded(words_loaded), .preamble_bytes(preamble_bytes)
    );

    // A flash or host that acks a read one cycle after the request.
    always @(posedge clk) begin
        if (!rst_n) begin
            flash_ack <= 1'b0;
        end else begin
            flash_ack <= flash_req;
            if (flash_req) n_flash_req = n_flash_req + 1;
        end
    end

    task push;
        input [7:0] b;
        begin
            @(negedge clk);
            byte_in = b; byte_valid = 1'b1;
            @(negedge clk);
            byte_valid = 1'b0;
        end
    endtask

    task push_stream;
        input integer pre_n;
        input integer words;
        input [7:0]   pad;
        integer k, ww;
        begin
            for (k = 0; k < pre_n; k = k + 1) push(pad);
            push(SYNC_WORD[31:24]); push(SYNC_WORD[23:16]);
            push(SYNC_WORD[15:8]);  push(SYNC_WORD[7:0]);
            for (ww = 0; ww < words; ww = ww + 1) begin
                push(ww[7:0]); push(8'h11);
                push(8'h22);   push(8'h33);
            end
        end
    endtask

    task begin_load;
        input master;
        begin
            @(negedge clk);
            mode_master = master;
            n_flash_req = 0;
            start = 1'b1;
            @(negedge clk);
            start = 1'b0;
            if (master) repeat (4) @(negedge clk);
        end
    endtask

    task end_stream;
        begin
            @(negedge clk);
            eos = 1'b1;
            @(negedge clk);
            eos = 1'b0;
            @(negedge clk);
        end
    endtask

    initial begin
        repeat (3) @(negedge clk);
        rst_n = 1'b1;
        @(negedge clk);

        // 1. MASTER mode, 7 padding bytes -- deliberately not a multiple
        //    of four.
        begin_load(1'b1);
        if (n_flash_req == 0) begin
            $display("  FAIL: master mode issued no read command");
            errors = errors + 1;
        end
        if (flash_cmd !== OP_READ) begin
            $display("  FAIL: master mode issued opcode 0x%02h", flash_cmd);
            errors = errors + 1;
        end
        push_stream(7, EXP_WORDS, 8'hFF);
        if (!cfg_done || cfg_err) begin
            $display("  FAIL: a good bitstream did not configure (done=%0b err=%0b)",
                     cfg_done, cfg_err);
            errors = errors + 1;
        end
        if (!sync_found) begin
            $display("  FAIL: the sync word was not found"); errors = errors + 1;
        end
        if (words_loaded !== EXP_WORDS) begin
            $display("  FAIL: %0d words loaded, expected %0d",
                     words_loaded, EXP_WORDS);
            errors = errors + 1;
        end
        if (preamble_bytes !== 16'd7) begin
            $display("  FAIL: %0d preamble bytes counted, expected 7",
                     preamble_bytes);
            errors = errors + 1;
        end
        m_done = cfg_done; m_err = cfg_err; m_sync = sync_found;
        m_words = words_loaded; m_pre = preamble_bytes; m_reason = err_reason;
        $display("  master mode: %0d read command(s) issued, sync found after %0d preamble bytes, %0d words loaded",
                 n_flash_req, preamble_bytes, words_loaded);

        // 2. SLAVE mode, the IDENTICAL stream.
        begin_load(1'b0);
        push_stream(7, EXP_WORDS, 8'hFF);
        if (n_flash_req != 0) begin
            $display("  FAIL: slave mode issued %0d read command(s) -- it must issue none",
                     n_flash_req);
            errors = errors + 1;
        end
        if (cfg_done !== m_done || cfg_err !== m_err ||
            sync_found !== m_sync || words_loaded !== m_words ||
            preamble_bytes !== m_pre || err_reason !== m_reason) begin
            $display("  FAIL: slave mode differed from master mode");
            errors = errors + 1;
        end
        $display("  slave mode:  no command issued, identical result -- %0d words, %0d preamble bytes",
                 words_loaded, preamble_bytes);

        // 3. PREAMBLE ALIGNMENT SWEEP.
        for (pre = 0; pre < 12; pre = pre + 1) begin
            begin_load(1'b0);
            push_stream(pre, EXP_WORDS, 8'hFF);
            if (!cfg_done || cfg_err) begin
                $display("  FAIL: a %0d-byte preamble did not configure", pre);
                errors = errors + 1;
            end
            if (preamble_bytes !== pre) begin
                $display("  FAIL: a %0d-byte preamble was counted as %0d",
                         pre, preamble_bytes);
                errors = errors + 1;
            end
        end
        $display("  preamble sweep: 0 to 11 padding bytes all configure, every residue modulo four");

        // 4. A blank flash.
        begin_load(1'b1);
        i = 0;
        while (i < MAX_PREAMBLE + 4 && !cfg_err) begin
            push(8'hFF);
            i = i + 1;
        end
        if (!cfg_err || err_reason !== E_NO_SYNC) begin
            $display("  FAIL: a blank flash was not reported (err=%0b reason=%0d)",
                     cfg_err, err_reason);
            errors = errors + 1;
        end
        if (cfg_done) begin
            $display("  FAIL: a blank flash reported configuration done");
            errors = errors + 1;
        end
        $display("  blank flash: no sync within %0d bytes, reported reason %0d, never done",
                 MAX_PREAMBLE, err_reason);

        // 5. A TRUNCATED bitstream.
        begin_load(1'b0);
        push_stream(4, EXP_WORDS - 4, 8'hFF);   // four words short
        if (cfg_done) begin
            $display("  FAIL: a truncated bitstream reported done");
            errors = errors + 1;
        end
        end_stream;
        if (!cfg_err || err_reason !== E_SHORT) begin
            $display("  FAIL: a truncated bitstream was not reported (err=%0b reason=%0d)",
                     cfg_err, err_reason);
            errors = errors + 1;
        end
        if (cfg_done) begin
            $display("  FAIL: a truncated bitstream reported done after end of stream");
            errors = errors + 1;
        end
        $display("  truncated:   %0d of %0d words then end of stream -- reported reason %0d, never done",
                 words_loaded, EXP_WORDS, err_reason);

        // 6. A stream that ends before any sync word at all.
        begin_load(1'b0);
        push(8'hFF); push(8'hFF);
        end_stream;
        if (!cfg_err || err_reason !== E_NO_SYNC) begin
            $display("  FAIL: a stream ending before sync was not reported");
            errors = errors + 1;
        end

        // 7. Recovery.
        begin_load(1'b1);
        push_stream(5, EXP_WORDS, 8'hFF);
        if (!cfg_done || cfg_err) begin
            $display("  FAIL: a good bitstream after failures did not configure");
            errors = errors + 1;
        end
        $display("  recovery:    configured after four failures, %0d words",
                 words_loaded);

        if (errors == 0)
            $display("PASS: master mode issues its own read and slave mode issues none, both produce identical results from the identical byte stream, the sync word is found at every preamble alignment, a blank flash is reported rather than waited on forever, and a truncated bitstream is an error rather than a done");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_load.vhd — the same loader in VHDL
-- fpga_cfg_load.vhd
--
-- Chapter 11.6 -- loading a bitstream over SPI in master and slave
-- configuration modes, in VHDL.
--
--   MASTER mode  the FPGA drives SCLK and reads the bitstream from a flash
--                it selects itself. It must ISSUE a read command.
--   SLAVE mode   an external host drives SCLK and pushes the bitstream in.
--                The FPGA issues nothing and simply consumes bytes.
--
-- The difference is entirely about who starts and who clocks. The bitstream
-- PARSING -- discard preamble, find the sync word, count configuration
-- words -- is identical, and this block shares that datapath rather than
-- duplicating it.
--
-- Searching for the sync word BYTE BY BYTE rather than on four-byte
-- boundaries is essential: the padding length is not guaranteed to be a
-- multiple of four, and a loader that only checks aligned positions misses
-- the sync word on three bitstreams in four.
--
-- A blank flash reads 0xFF forever, which is indistinguishable from
-- preamble. Without a bound, a loader waits for a sync word that will never
-- come.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity fpga_cfg_load is
    generic (
        -- An unsigned rather than a natural: 0xAA995566 exceeds VHDL's
        -- NATURAL range, so a sync word with its top bit set cannot be
        -- written as an integer generic at all.
        SYNC_WORD    : unsigned(31 downto 0) := x"AA995566";
        MAX_PREAMBLE : positive := 64;         -- bytes before giving up
        EXP_WORDS    : positive := 16;         -- configuration words expected
        OP_READ      : natural  := 16#03#;
        CFG_BASE     : natural  := 16#000000#
    );
    port (
        clk         : in  std_logic;
        rst_n       : in  std_logic;

        mode_master : in  std_logic;   -- 1 master, 0 slave
        start       : in  std_logic;
        eos         : in  std_logic;   -- end of stream

        -- Byte stream in -- from the flash in master mode, from the host in
        -- slave mode. One port, because the parsing does not care.
        byte_valid  : in  std_logic;
        byte_in     : in  unsigned(7 downto 0);

        -- Master mode only: the read this block issues for itself.
        flash_req   : out std_logic;
        flash_cmd   : out unsigned(7 downto 0);
        flash_addr  : out unsigned(23 downto 0);
        flash_ack   : in  std_logic;

        busy        : out std_logic;
        sync_found  : out std_logic;
        cfg_done    : out std_logic;
        cfg_err     : out std_logic;
        err_reason  : out unsigned(1 downto 0);  -- 1 no sync, 2 short
        words_loaded   : out unsigned(15 downto 0);
        preamble_bytes : out unsigned(15 downto 0)
    );
end entity;

architecture rtl of fpga_cfg_load is

    constant E_NONE    : unsigned(1 downto 0) := "00";
    constant E_NO_SYNC : unsigned(1 downto 0) := "01";
    constant E_SHORT   : unsigned(1 downto 0) := "10";

    -- The sync word is four bytes wide, so the three bytes before the one
    -- that completes the match were counted as preamble on their way through
    -- the search window. They are not padding, and the count is corrected
    -- when the match lands.
    constant SYNC_BYTES : natural := 4;

    type state_t is (S_IDLE, S_CMD, S_SYNC, S_LOAD, S_DONE, S_ERR);
    signal state : state_t := S_IDLE;

    signal sr           : unsigned(31 downto 0) := (others => '0');
    signal byte_in_word : unsigned(1 downto 0)  := (others => '0');

    signal req_r  : std_logic := '0';
    signal cmd_r  : unsigned(7 downto 0) := (others => '0');
    signal addr_r : unsigned(23 downto 0) := (others => '0');
    signal busy_r : std_logic := '0';
    signal sync_r : std_logic := '0';
    signal done_r : std_logic := '0';
    signal err_r  : std_logic := '0';
    signal why_r  : unsigned(1 downto 0) := E_NONE;
    signal wds_r  : unsigned(15 downto 0) := (others => '0');
    signal pre_r  : unsigned(15 downto 0) := (others => '0');

begin

    load : process (clk, rst_n)
        variable nxt_sr : unsigned(31 downto 0);
    begin
        if rst_n = '0' then
            state        <= S_IDLE;
            sr           <= (others => '0');
            byte_in_word <= (others => '0');
            req_r        <= '0';
            cmd_r        <= (others => '0');
            addr_r       <= (others => '0');
            busy_r       <= '0';
            sync_r       <= '0';
            done_r       <= '0';
            err_r        <= '0';
            why_r        <= E_NONE;
            wds_r        <= (others => '0');
            pre_r        <= (others => '0');
        elsif rising_edge(clk) then
            req_r <= '0';

            case state is
                when S_IDLE | S_DONE | S_ERR =>
                    if start = '1' then
                        sr           <= (others => '0');
                        byte_in_word <= (others => '0');
                        busy_r       <= '1';
                        sync_r       <= '0';
                        done_r       <= '0';
                        err_r        <= '0';
                        why_r        <= E_NONE;
                        wds_r        <= (others => '0');
                        pre_r        <= (others => '0');

                        if mode_master = '1' then
                            -- Master mode: the FPGA fetches for itself.
                            cmd_r  <= to_unsigned(OP_READ, 8);
                            addr_r <= to_unsigned(CFG_BASE, 24);
                            req_r  <= '1';
                            state  <= S_CMD;
                        else
                            -- Slave mode: nothing to issue. The host starts
                            -- clocking when ready; the parsing is identical.
                            state <= S_SYNC;
                        end if;
                    end if;

                when S_CMD =>
                    if flash_ack = '1' then
                        state <= S_SYNC;
                    end if;

                when S_SYNC =>
                    if eos = '1' then
                        -- The stream ended before a sync word appeared.
                        err_r  <= '1';
                        why_r  <= E_NO_SYNC;
                        busy_r <= '0';
                        state  <= S_ERR;
                    elsif byte_valid = '1' then
                        -- Shift by ONE BYTE, not one word.
                        nxt_sr := sr(23 downto 0) & byte_in;
                        sr <= nxt_sr;

                        if nxt_sr = SYNC_WORD then
                            sync_r       <= '1';
                            byte_in_word <= (others => '0');
                            -- Give back the partial-match bytes: they were
                            -- the sync word, not padding.
                            pre_r <= pre_r - to_unsigned(SYNC_BYTES - 1, 16);
                            state <= S_LOAD;
                        else
                            pre_r <= pre_r + 1;
                            if to_integer(pre_r) >= MAX_PREAMBLE - 1 then
                                -- A blank flash reads 0xFF forever. The
                                -- bound turns an invisible hang into a
                                -- reported failure.
                                err_r  <= '1';
                                why_r  <= E_NO_SYNC;
                                busy_r <= '0';
                                state  <= S_ERR;
                            end if;
                        end if;
                    end if;

                when S_LOAD =>
                    if eos = '1' then
                        if to_integer(wds_r) < EXP_WORDS then
                            -- Configuring an FPGA with a partial bitstream
                            -- is far worse than not configuring it, so this
                            -- must be an error and never a done.
                            err_r  <= '1';
                            why_r  <= E_SHORT;
                            busy_r <= '0';
                            state  <= S_ERR;
                        end if;
                    elsif byte_valid = '1' then
                        if byte_in_word = 3 then
                            byte_in_word <= (others => '0');
                            wds_r        <= wds_r + 1;
                            if to_integer(wds_r) + 1 >= EXP_WORDS then
                                busy_r <= '0';
                                done_r <= '1';
                                state  <= S_DONE;
                            end if;
                        else
                            byte_in_word <= byte_in_word + 1;
                        end if;
                    end if;
            end case;
        end if;
    end process;

    flash_req      <= req_r;
    flash_cmd      <= cmd_r;
    flash_addr     <= addr_r;
    busy           <= busy_r;
    sync_found     <= sync_r;
    cfg_done       <= done_r;
    cfg_err        <= err_r;
    err_reason     <= why_r;
    words_loaded   <= wds_r;
    preamble_bytes <= pre_r;

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_load_tb.vhd — the same equivalence test in VHDL
-- fpga_cfg_load_tb.vhd
--
-- The central test is the EQUIVALENCE one: the same byte stream is pushed
-- through master mode and slave mode, and the results must be identical --
-- which is the claim the shared datapath makes.
--
-- The preamble length is then swept across every residue modulo four,
-- because a loader searching on four-byte boundaries passes exactly one
-- quarter of those cases.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity fpga_cfg_load_tb is
end entity;

architecture sim of fpga_cfg_load_tb is

    constant SYNC_W       : unsigned(31 downto 0) := x"AA995566";
    constant MAX_PREAMBLE : positive := 64;
    constant EXP_WORDS    : positive := 16;
    constant OP_READ      : natural  := 16#03#;

    constant E_NONE    : unsigned(1 downto 0) := "00";
    constant E_NO_SYNC : unsigned(1 downto 0) := "01";
    constant E_SHORT   : unsigned(1 downto 0) := "10";

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal halt  : boolean   := false;

    signal mode_master : std_logic := '1';
    signal start       : std_logic := '0';
    signal eos         : std_logic := '0';
    signal byte_valid  : std_logic := '0';
    signal byte_in     : unsigned(7 downto 0) := (others => '0');

    signal flash_req  : std_logic;
    signal flash_cmd  : unsigned(7 downto 0);
    signal flash_addr : unsigned(23 downto 0);
    signal flash_ack  : std_logic := '0';

    signal busy, sync_found, cfg_done, cfg_err : std_logic;
    signal err_reason     : unsigned(1 downto 0);
    signal words_loaded   : unsigned(15 downto 0);
    signal preamble_bytes : unsigned(15 downto 0);

    signal n_flash_req : natural   := 0;
    signal clr_count   : std_logic := '0';

    signal errors : natural := 0;

begin

    clk <= not clk after 5 ns when not halt else '0';

    dut : entity work.fpga_cfg_load
        generic map (SYNC_WORD => SYNC_W, MAX_PREAMBLE => MAX_PREAMBLE,
                     EXP_WORDS => EXP_WORDS, OP_READ => OP_READ,
                     CFG_BASE => 16#000000#)
        port map (
            clk => clk, rst_n => rst_n,
            mode_master => mode_master, start => start, eos => eos,
            byte_valid => byte_valid, byte_in => byte_in,
            flash_req => flash_req, flash_cmd => flash_cmd,
            flash_addr => flash_addr, flash_ack => flash_ack,
            busy => busy, sync_found => sync_found,
            cfg_done => cfg_done, cfg_err => cfg_err,
            err_reason => err_reason,
            words_loaded => words_loaded, preamble_bytes => preamble_bytes
        );

    -- A flash or host that acks a read one cycle after the request, and
    -- counts the requests. The clear arrives on its own signal because two
    -- processes driving one signal is a multiple-driver error in VHDL.
    responder : process (clk, rst_n)
    begin
        if rst_n = '0' then
            flash_ack   <= '0';
            n_flash_req <= 0;
        elsif rising_edge(clk) then
            flash_ack <= flash_req;
            if clr_count = '1' then
                n_flash_req <= 0;
            elsif flash_req = '1' then
                n_flash_req <= n_flash_req + 1;
            end if;
        end if;
    end process;

    stim : process
        variable errs : natural := 0;
        variable m_done, m_err, m_sync : std_logic;
        variable m_words, m_pre : unsigned(15 downto 0);
        variable m_reason : unsigned(1 downto 0);
        variable i : natural;

        procedure push(b : unsigned(7 downto 0)) is
        begin
            wait until falling_edge(clk);
            byte_in    <= b;
            byte_valid <= '1';
            wait until falling_edge(clk);
            byte_valid <= '0';
        end procedure;

        procedure push_stream(pre_n : natural; words : natural;
                              pad : unsigned(7 downto 0)) is
        begin
            for k in 1 to pre_n loop
                push(pad);
            end loop;
            push(SYNC_W(31 downto 24)); push(SYNC_W(23 downto 16));
            push(SYNC_W(15 downto 8));  push(SYNC_W(7 downto 0));
            for ww in 0 to words - 1 loop
                push(to_unsigned(ww mod 256, 8)); push(x"11");
                push(x"22");                      push(x"33");
            end loop;
        end procedure;

        procedure begin_load(master : std_logic) is
        begin
            wait until falling_edge(clk);
            mode_master <= master;
            clr_count   <= '1';
            wait until falling_edge(clk);
            clr_count   <= '0';
            start       <= '1';
            wait until falling_edge(clk);
            start       <= '0';
            if master = '1' then
                for k in 0 to 3 loop
                    wait until falling_edge(clk);
                end loop;
            end if;
        end procedure;

        procedure end_stream is
        begin
            wait until falling_edge(clk);
            eos <= '1';
            wait until falling_edge(clk);
            eos <= '0';
            wait until falling_edge(clk);
        end procedure;
    begin
        for k in 0 to 2 loop
            wait until falling_edge(clk);
        end loop;
        rst_n <= '1';
        wait until falling_edge(clk);

        -- 1. MASTER mode, 7 padding bytes -- not a multiple of four.
        begin_load('1');
        if n_flash_req = 0 then
            report "  FAIL: master mode issued no read command";
            errs := errs + 1;
        end if;
        if flash_cmd /= to_unsigned(OP_READ, 8) then
            report "  FAIL: master mode issued the wrong opcode";
            errs := errs + 1;
        end if;
        push_stream(7, EXP_WORDS, x"FF");
        if cfg_done /= '1' or cfg_err = '1' then
            report "  FAIL: a good bitstream did not configure";
            errs := errs + 1;
        end if;
        if sync_found /= '1' then
            report "  FAIL: the sync word was not found"; errs := errs + 1;
        end if;
        if to_integer(words_loaded) /= EXP_WORDS then
            report "  FAIL: the wrong number of words was loaded";
            errs := errs + 1;
        end if;
        if to_integer(preamble_bytes) /= 7 then
            report "  FAIL: " & integer'image(to_integer(preamble_bytes)) &
                   " preamble bytes counted, expected 7";
            errs := errs + 1;
        end if;
        m_done := cfg_done; m_err := cfg_err; m_sync := sync_found;
        m_words := words_loaded; m_pre := preamble_bytes;
        m_reason := err_reason;
        report "  master mode: " & integer'image(n_flash_req) &
               " read command(s) issued, sync found after " &
               integer'image(to_integer(preamble_bytes)) &
               " preamble bytes, " &
               integer'image(to_integer(words_loaded)) & " words loaded";

        -- 2. SLAVE mode, the IDENTICAL stream.
        begin_load('0');
        push_stream(7, EXP_WORDS, x"FF");
        if n_flash_req /= 0 then
            report "  FAIL: slave mode issued a read command -- it must issue none";
            errs := errs + 1;
        end if;
        if cfg_done /= m_done or cfg_err /= m_err or
           sync_found /= m_sync or words_loaded /= m_words or
           preamble_bytes /= m_pre or err_reason /= m_reason then
            report "  FAIL: slave mode differed from master mode";
            errs := errs + 1;
        end if;
        report "  slave mode:  no command issued, identical result -- " &
               integer'image(to_integer(words_loaded)) & " words, " &
               integer'image(to_integer(preamble_bytes)) & " preamble bytes";

        -- 3. PREAMBLE ALIGNMENT SWEEP.
        for pre in 0 to 11 loop
            begin_load('0');
            push_stream(pre, EXP_WORDS, x"FF");
            if cfg_done /= '1' or cfg_err = '1' then
                report "  FAIL: a preamble of " & integer'image(pre) &
                       " bytes did not configure";
                errs := errs + 1;
            end if;
            if to_integer(preamble_bytes) /= pre then
                report "  FAIL: a preamble of " & integer'image(pre) &
                       " bytes was counted as " &
                       integer'image(to_integer(preamble_bytes));
                errs := errs + 1;
            end if;
        end loop;
        report "  preamble sweep: 0 to 11 padding bytes all configure, every residue modulo four";

        -- 4. A blank flash.
        begin_load('1');
        i := 0;
        while i < MAX_PREAMBLE + 4 and cfg_err = '0' loop
            push(x"FF");
            i := i + 1;
        end loop;
        if cfg_err /= '1' or err_reason /= E_NO_SYNC then
            report "  FAIL: a blank flash was not reported"; errs := errs + 1;
        end if;
        if cfg_done = '1' then
            report "  FAIL: a blank flash reported configuration done";
            errs := errs + 1;
        end if;
        report "  blank flash: no sync within " &
               integer'image(MAX_PREAMBLE) & " bytes, reported reason " &
               integer'image(to_integer(err_reason)) & ", never done";

        -- 5. A TRUNCATED bitstream.
        begin_load('0');
        push_stream(4, EXP_WORDS - 4, x"FF");   -- four words short
        if cfg_done = '1' then
            report "  FAIL: a truncated bitstream reported done";
            errs := errs + 1;
        end if;
        end_stream;
        if cfg_err /= '1' or err_reason /= E_SHORT then
            report "  FAIL: a truncated bitstream was not reported";
            errs := errs + 1;
        end if;
        if cfg_done = '1' then
            report "  FAIL: a truncated bitstream reported done after end of stream";
            errs := errs + 1;
        end if;
        report "  truncated:   " & integer'image(to_integer(words_loaded)) &
               " of " & integer'image(EXP_WORDS) &
               " words then end of stream -- reported reason " &
               integer'image(to_integer(err_reason)) & ", never done";

        -- 6. A stream that ends before any sync word at all.
        begin_load('0');
        push(x"FF"); push(x"FF");
        end_stream;
        if cfg_err /= '1' or err_reason /= E_NO_SYNC then
            report "  FAIL: a stream ending before sync was not reported";
            errs := errs + 1;
        end if;

        -- 7. Recovery.
        begin_load('1');
        push_stream(5, EXP_WORDS, x"FF");
        if cfg_done /= '1' or cfg_err = '1' then
            report "  FAIL: a good bitstream after failures did not configure";
            errs := errs + 1;
        end if;
        report "  recovery:    configured after four failures, " &
               integer'image(to_integer(words_loaded)) & " words";

        errors <= errs;
        if errs = 0 then
            report "PASS: master mode issues its own read and slave mode issues none, both produce identical results from the identical byte stream, the sync word is found at every preamble alignment, a blank flash is reported rather than waited on forever, and a truncated bitstream is an error rather than a done";
        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 loader: identical ports and generics, a read command issued in master mode and none in slave, a byte-by-byte sync search, a corrected preamble count, a bounded search, and a short stream reported as an error rather than a done. All three testbenches run the same equivalence test, the same twelve-length preamble sweep and the same two failure cases, reporting identical results — seven preamble bytes and sixteen words in both modes, a no-sync error after 64 bytes, and twelve of sixteen words on the truncated stream.

One VHDL detail is worth noting because it is a language constraint rather than a design choice: the sync word 0xAA995566 exceeds VHDL's NATURAL range, so it cannot be an integer generic at all. It is declared as an unsigned instead — which is the correct form for any pattern whose top bit may be set.

7. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_load.sva — equivalence, and the two silent failures
   // 1. MODE EQUIVALENCE. The parsing outputs depend only on the byte
   //    stream, not on the mode. This is the property the shared datapath
   //    claims, and it is the one a duplicated implementation breaks.
   //    Checked in the testbench by replay; stated here as the intent.
   a_mode_irrelevant : assert property (
       @(posedge clk) disable iff (!rst_n)
           (sync_found) |-> (preamble_bytes == expected_preamble))
       else $error("the preamble count depended on the mode");

   // 2. SLAVE MODE ISSUES NOTHING. An FPGA in slave configuration is a
   //    slave: a read command driven onto a bus a host is also driving is
   //    contention, which Chapter 8.5 covers and which damages drivers.
   a_slave_silent : assert property (
       @(posedge clk) disable iff (!rst_n)
           (!mode_master) |-> !flash_req)
       else $error("slave mode issued a command");

   // 3. NO CONFIGURATION BEFORE SYNC. Preamble bytes must not reach the
   //    configuration memory. Writing padding into it is how a partially
   //    configured device starts driving pins.
   a_no_early_write : assert property (
       @(posedge clk) disable iff (!rst_n)
           (cfg_write) |-> sync_found)
       else $error("a byte was written to configuration memory before sync");

   // 4. DONE AND ERROR ARE EXCLUSIVE. A consumer that sees both believes
   //    whichever it checks first, and half the consumers check done.
   a_exclusive_verdict : assert property (
       @(posedge clk) disable iff (!rst_n)
           !(cfg_done && cfg_err))
       else $error("both done and error were reported");

   // 5. NEVER DONE ON A SHORT STREAM. A partially configured FPGA may
   //    drive pins unpredictably, so this is worse than not configuring.
   a_no_partial_done : assert property (
       @(posedge clk) disable iff (!rst_n)
           (cfg_done) |-> (words_loaded >= EXP_WORDS))
       else $error("done was asserted with fewer words than expected");

   // 6. TERMINATION. The search always ends -- by finding the sync word,
   //    by exceeding the bound, or at end of stream. A loader that
   //    searches forever shows no symptom but a DONE pin that stays low.
   a_search_terminates : assert property (
       @(posedge clk) disable iff (!rst_n)
           (searching) |-> ##[1:$] (sync_found || cfg_err))
       else $error("the sync search never terminated");

Property 1 is the one worth generalising: when a design claims two configurations share behaviour, that claim is itself a property. The natural way to check it is replay — run the same input through both and compare — and the natural way to break it is a duplicated datapath where one copy is fixed and the other is not.

Property 5 states the asymmetry explicitly, and it is the reason a short stream cannot be treated leniently. A partially configured FPGA is not a device that does less; it is a device whose outputs are undefined, and it may drive a pin against something else on the board.

Property 6 is another liveness property, for the same reason as Chapter 11.4's: the failure it prevents is an absence of anything happening, which no safety property can express.

Coverage must cross the mode with the alignment, because that is where the real bug lives:

Azvya Education Pvt. Ltd.VLSI Mentor
fpga_cfg_cg.sv — preamble residue is the axis that matters
   covergroup fpga_cfg_cg @(posedge clk iff verdict);
       cp_mode : coverpoint mode_master { bins master = {1}; bins slave = {0}; }

       // The residue modulo four is the axis a word-aligned search fails
       // on. Covering preamble LENGTHS without covering residues can hit
       // twenty lengths and one residue.
       cp_pre_residue : coverpoint (preamble_bytes % 4) {
           bins r0 = {0}; bins r1 = {1}; bins r2 = {2}; bins r3 = {3};
       }

       cp_pre_len : coverpoint preamble_bytes {
           bins none    = {0};             // sync word first, no padding
           bins short   = {[1:7]};
           bins typical = {[8:32]};
           bins at_bound = {[MAX_PREAMBLE-1:MAX_PREAMBLE]};
       }

       cp_verdict : coverpoint verdict_class {
           bins configured = {V_DONE};
           bins no_sync    = {V_NO_SYNC};   // needs a blank flash
           bins truncated  = {V_SHORT};     // needs a stopped host
       }

       // The cross that matters. Both modes at every residue -- because a
       // suite that tests master mode at residue 0 and slave mode at
       // residue 0 has tested one alignment twice.
       x_mode_residue : cross cp_mode, cp_pre_residue;
       x_mode_verdict : cross cp_mode, cp_verdict;
   endgroup

cp_pre_residue is the coverpoint this chapter exists to motivate. Covering preamble lengths is easy and nearly useless — twenty lengths that are all multiples of four cover one residue. The residue is the axis the bug lives on, and it has exactly four values.

x_mode_verdict catches the other comfortable gap: failure cases tested in one mode only. A blank flash is a master-mode scenario and a stopped host is a slave-mode one, so a suite that tests each failure in its "natural" mode never checks that the failure paths are shared either.

8. Why an FPGA or ASIC Engineer Cares

Share the parser between modes. Two copies is two places to fix the sync search, and the copy exercised less is the one that stays broken. One datapath with a mode-dependent front end is both smaller and correct by construction.

Search byte by byte. The padding length is a property of whatever generated the bitstream, not something you control. A word-aligned search works on one bitstream in four and fails with no pattern a person can see.

Bound the search. An FPGA that never finishes configuring has no symptom beyond a DONE pin that stays low, and a blank flash is indistinguishable from preamble. The bound is the only thing that turns this into a reportable event.

Treat a short bitstream as an error, loudly. A partially configured device may drive pins unpredictably, which can damage other devices. Never assert DONE on a short stream, and prefer holding the device in reset.

Make the failure reason observable. Two bits distinguishing "no sync found" from "stream too short" separate a blank flash from a stopped host — which are diagnosed in completely different places.

Report the preamble length, correctly. It is a free diagnostic that tells you the bitstream's front end is what you think it is, and the off-by-three of §5 makes it wrong in a way that looks plausible.

In slave mode, drive nothing. An FPGA in slave configuration that asserts a chip select or drives a clock is contending with the host, which is Chapter 8.5's failure with real current behind it.

9. Failure Signature — A Bitstream That Loads on the Bench and Not in Production

Symptom. A design is developed with the FPGA configured in slave mode from a host during bring-up. It works. The production board uses master mode from a flash. The identical bitstream, written to the flash, never configures: DONE stays low, no error is reported anywhere, and every supply and clock measures correctly.

What "the same bitstream works in slave mode" establishes. The bitstream is valid and the parsing logic can handle it — because slave mode parsed it successfully. So the fault is in what is different: the front end, the flash, or the read.

Plausible mechanisms.

  • The flash read never happens or returns nothing. A wrong opcode, a wrong start address, or a chip select not reaching the flash. DONE staying low with no error fits, because the loader is waiting in its search state.
  • The flash is blank or holds a different image at the address the loader reads from — the bitstream was written to the wrong offset.
  • The FPGA's master-mode clock is out of the flash's range, so the read returns corrupted bytes and the sync word never matches.
  • A duplicated parser where the master-mode copy has the word-aligned search bug and the slave-mode copy does not. This fits precisely and is the reason §6 shares one datapath.
  • The flash left in a mode the loader does not expect, as Chapter 11.5's warm-boot case describes.

The discriminating observation. DONE low with no error reported is the key detail, and it says the loader is still searching — it has neither found the sync word nor exceeded its bound. That immediately splits the candidates:

  • If the bound was exceeded and no error appeared, the bound is not implemented, and that is the first bug regardless of anything else.
  • If the loader is genuinely still receiving bytes, put a scope on MISO: 0xFF forever means a blank flash or a read that is not landing; plausible-looking varied bytes mean the read works and the sync search is failing.

That second case is the one that points at the aligned-search bug, and it can be confirmed without a scope: pad the bitstream with one extra 0xFF byte and try again. If it configures with some padding lengths and not others, the search is word-aligned — and that experiment takes minutes.

The fix. Share the parser, search byte by byte, and implement the bound so the next occurrence reports rather than hangs.

Why this pattern recurs. Because slave mode is how boards are brought up and master mode is how they ship, so the two paths get radically different amounts of exercise. Anything not shared between them is tested once and shipped untested — which is the general argument for sharing rather than duplicating, stated in a form that applies well beyond FPGA configuration.

10. Common Misconceptions

11. Reason It Through

Work this before reading the answer.

An FPGA in master configuration mode shares its flash with a processor on the same board — both have chip selects to it, and the processor reads its own firmware from a different region of the same device.

What must be true for this to work, and what happens at power-on?

Start with what is shared. One flash, one SCLK net, one MOSI net, one MISO net, and two chip selects. So this is Chapter 8.2's topology — except that both potential masters want to drive SCLK and MOSI.

And there is the first problem. SPI has exactly one master. Two devices driving SCLK is contention on a clock line, which is the worst line to have contention on: Chapter 8.5 describes the currents, and a clock with two drivers produces edges that neither device intended.

So what must be true? One of three arrangements, and only one is really satisfactory.

Arrangement one: strict time separation. The FPGA configures first, from power-on, while the processor is held in reset. Then the FPGA releases the processor and stops driving the bus forever, tri-stating its SCLK, MOSI and CS outputs. This works, and it requires:

  • the processor's reset to be held until the FPGA's DONE asserts — a physical dependency, not a software one;
  • the FPGA's configuration pins to become high-impedance after configuration, which most families do but must be confirmed;
  • and the FPGA to never need to re-read the flash afterwards.

Arrangement two: the FPGA becomes a slave. The processor boots first from the flash, then configures the FPGA in slave mode by reading the bitstream itself and pushing it in. Now there is only ever one master, and the contention question does not arise. This is why slave mode exists, and it is the cleaner arrangement whenever a processor is present.

Arrangement three: a bus switch. A multiplexer gives the flash to whichever side needs it. It works and it costs a part, a control signal and a new question about who owns the switch.

What happens at power-on in arrangement one? Both devices come out of reset at roughly the same time, and the processor's boot ROM starts issuing reads immediately — because that is what boot ROMs do. So:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   t=0    both release from reset
   t=0+   FPGA starts driving SCLK to read its bitstream
   t=0+   processor ALSO starts driving SCLK to read its firmware
          → contention on the clock line, both transfers corrupted

Neither device boots. And the failure is intermittent in the worst way, because it depends on the relative timing of two power-on reset circuits — so some boards work, and the same board works sometimes.

Which is why the processor's reset must be held by DONE, in hardware. Not "the processor's firmware waits" — the firmware has not run yet. A physical dependency from the FPGA's DONE pin to the processor's reset input is the only thing that orders the two.

The general lesson. Two masters on one SPI bus is not a topology, it is a scheduling problem — and the schedule has to be enforced by something that works before either device is running. When a processor is available, arrangement two is better: let it boot first and configure the FPGA in slave mode, and the question disappears entirely rather than being managed.

12. Understanding Check

13. Summary

Master and slave configuration differ in who starts and who clocks, and in nothing else. Master mode issues a read to its own flash; slave mode issues nothing and is clocked by a host. Everything after the first byte — discard the preamble, find the sync word, count words, decide completeness — is identical, which is why one shared datapath is both smaller and correct by construction where two copies are not.

A bitstream begins with padding and then a sync word, and the sync word must be searched for byte by byte. The padding length is an artefact of the generating tool, not a multiple of four, so a word-aligned search works on one bitstream in four — and the failing bitstreams are perfectly valid, which sends the investigation to the wrong place.

The search must be bounded, because a blank flash reads 0xFF forever and is indistinguishable from padding, and an FPGA that never finishes configuring has no symptom but a DONE pin that stays low.

A truncated bitstream must be an error, never a done: a partially configured device has undefined outputs and may drive against something else on the board.

The preamble count needs correcting at the match, because the three bytes before the completing sync byte pass through the window and are otherwise counted as padding.

For verification, the shared-behaviour claim is itself a property, checked by replaying one stream through both modes. The coverage axis that matters is the preamble's residue modulo four — covering lengths is easy and nearly useless — crossed with the mode, because a failure tested only in its natural mode never shows the failure paths are shared either.

14. What Comes Next

The module has built the pieces: geometry, read commands, program and erase, the write protocol, boot and configuration. What remains is reading a real capture and saying what it shows.

Chapter 11.7 — Boot and Flash Transaction Analysis takes a 25-series boot capture back to the datasheet. It classifies each frame from its opcode, checks each frame's length against the shape that opcode implies, and — the part no single-frame analysis can do — tracks the write-enable latch across frames, so a perfectly well-formed page program that follows no WREN is reported. That cross-frame check is the commonest real fault in a flash capture, and the chapter builds the decoder that finds it, in all three HDLs.

Continue learning