Skip to content
VLSI Mentor

SPI · Module 13

Configurable Transfer Width and Bit Order

Frame width from 1 to 32 bits and a choice of bit order without touching the shift register: why a bidirectional shifter is the wrong trade, why reversing the low bits is its own inverse, and why loopback cannot test bit order at all.

Chapter 13.6 built a shift datapath that is MSB-first and as wide as its registers. Two features are wanted: any frame width from 1 to MAX_W bits, and a choice of MSB-first or LSB-first.

LSB-first needs the register to shift the other way. Where does that mux go?

Nowhere. There is an implementation in which the shift register is not modified at all — and it turns out to need exactly one function, applied identically on both sides.

1. The Obvious Implementation, And Why It Is A Mistake

The natural way to add LSB-first is a bidirectional shift register: shift left and tap the top for MSB-first, shift right and tap the bottom for LSB-first. It is three lines of RTL and it works.

It is a mistake for three reasons, and they are worth being precise about because "it works" is true.

It puts a mux in the shift path. The shift register is the one piece of this design clocked on every SCLK edge at full rate. Everything else — loading, unloading, counting — happens once per frame. Adding logic to the per-bit path to serve a per-frame decision is the wrong trade at any frequency, and it gets worse as the datapath widens because the mux is MAX_W bits wide.

It doubles the verification surface. There are now two shift directions, each with its own end conditions, its own alignment, and its own interaction with Chapter 13.5's preload. That is not one feature with two settings; it is two designs sharing a name, and the second one is the one nobody exercises.

It reopens the alignment argument in mirror image, and the mirror is not symmetric. Chapter 13.6 established that transmit needs a left-align at load and receive needs nothing. Shifting right, transmit needs nothing and receive needs a right-align — except that len is measured from the low end of the word in both orders, so the two alignments are not reflections of each other and both have to be derived separately.

2. The Alternative: One Transform, Both Boundaries

Leave the shift register exactly as Chapter 13.6 left it — one direction, MSB-first, instantiated verbatim — and put a single combinational transform on each side:

reverse the low len bits of the word, leaving the rest alone

Send that instead of the transmit word, and apply it again to the received word. That is the whole of LSB-first.

The transform is its own inverse, so it is the same function on both boundaries — one piece of logic to write, one to review, one to get right. And it sits where nothing is clocked at the SCLK rate: the transmit side is evaluated on the load cycle, the receive side when software reads. Neither is on the per-bit path.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   tx_shaped = lsb_first ? rev_field(tx_data, len) : tx_data
   rx_data   = lsb_first ? rev_field(rx_raw,  len) : rx_raw
                           ^^^^^^^^^  the same function, twice

3. How rev_all(x) >> (MAX_W - len) Reverses Only The Low Bits

The implementation is a fixed full-width reversal followed by a variable right shift, and it is worth walking through because it looks like it should need a loop.

A full-width reversal sends bit 0 to the top, bit 1 next, and so on. So the low len bits land in the top len positions in reversed order — and everything above them is pushed below:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   MAX_W = 8, len = 4,  x = h g f e | d c b a        (a is bit 0)

   rev_all(x)        =  a b c d | e f g h
                        ^^^^^^^  the low 4 bits, reversed, at the top
                                  ^^^^^^^  the rest, now below

   >> (8 - 4)        =  0 0 0 0 | a b c d
                                  ^^^^^^^  reversed low 4 bits, right-aligned

One right shift by MAX_W - len drops exactly the part we do not want and brings the reversed field down to the bottom. The result is the low len bits reversed with zeros above.

The alternative — an explicit variable-width reversal — needs a loop over len, which synthesises to a barrel shifter plus a comparator per bit to decide whether each position participates. This version is a fixed permutation, which is free because it is just wires, plus one barrel shifter. Strictly less logic, and it is the same barrel shifter the alignment in Chapter 13.6 already needs.

4. The Block

A width and bit order wrapper. A transmit word from software goes through a conditional field-reversal, selected by a bit-order input, into the unchanged shift datapath of Chapter 13.6. The datapath drives MOSI and samples MISO. Its received word goes through the same conditional field-reversal to produce the word software reads. A width validity check reports a frame width of zero or one above the datapath width. A clamp keeps the shift amounts in range while an illegal width is being presented.tx_dataright-aligned, fromsoftwarerev_fieldreverse the low len bitslsb_firstone bit, per framelen1 .. MAX_W13.6 datapathinstantiated unchangedClampkeeps shift amounts inrangelen_err0 or above MAX_W: reportrev_fieldthe same function, againrx_dataas software expects itMOSI / MISObits, in the chosen orderselectshaped wordraw wordselectrange12
Figure 1 — width and order as a boundary transform. The frame width feeds both reversals, since it is the width of the field being reversed. Chapter 13.6's datapath is instantiated unchanged in the middle; the only new logic is one self-inverse function applied on each side, selected by the bit-order bit. Nothing was added to the per-bit path, and the frame width is handled by the alignment the datapath already had.

5. Loopback Cannot Test Bit Order At All

This is the most important verification point in the chapter, and it is structural rather than a matter of effort.

Reverse the transmit word and reverse the received word again, and the composition is the identity. So end to end, a loopback test cannot distinguish:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   reverse both sides       correct LSB-first
   reverse neither side     MSB-first, with the order bit ignored

Both return exactly what was sent. A design that ignores lsb_first entirely passes every loopback test ever written, and so does a design that reverses both sides when it should reverse neither.

So the testbench watches the wire. It records the bit sequence actually driven on MOSI, in order, and compares it against what the setting demands:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   MSB-first   wire[i] = tx_data[len-1-i]
   LSB-first   wire[i] = tx_data[i]

Those two agree only on palindromes and one-bit frames, so the check has real teeth. 0x8D is 1000_1101, whose reverse is 1011_0001 — visibly different, which is why it is the chapter's example value.

6. The Two Wires, Side By Side

Nineteen cycles across four rows. An SCLK row shows eight clock periods at a divisor of two. An MSB-first MOSI row carries the bits one, zero, zero, zero, one, one, zero, one. An LSB-first MOSI row carries the same byte reversed: one, zero, one, one, zero, zero, zero, one. A capture row marks the eight sampling points.both send bit 7 or bit 0 firstboth send bit 7 or bit 0firstthe two wires diverge herethe two wires diverge herelast bit — 0x8d vs 0xb1last bit — 0x8d vs 0xb1sclkmsb_mosilsb_mosicapturet0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15t16t17t18
Figure 2 — the byte 0x8D sent both ways. The two MOSI rows are bit-reversals of each other, which is the only observation that distinguishes the two settings — and it is visible on the wire and nowhere else. Both loop back to 0x8D, because the transform is its own inverse and a loopback path applies it twice.

7. Building the Width and Order Wrapper — Three HDLs

The circuit

Two conditional applications of one function, one validity check, and an instance of Chapter 13.6's datapath — unchanged. That the datapath needed no edit to gain two features is the point of the chapter.

Two details:

A width of zero or above MAX_W is reported rather than absorbed. Zero would make len - 1 wrap and the frame never end; above MAX_W would silently truncate. Both are configuration errors, and reporting them is the same argument as Chapter 13.4's illegal divisor.

The shift amount is clamped, not just reported. While an illegal len is being presented, MAX_W - len must still be a legal shift amount, or the reversal produces something undefined that could propagate. Clamping costs two comparators and keeps the block's output defined for every input, which matters because len comes from a register software can write at any time.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order.sv — one self-inverse transform, and the datapath untouched
// spi_width_order.sv
//
// Chapter 13.8 -- configurable frame width and bit order, WITHOUT touching
// the shift register.
//
// Two features are wanted here. Frames of any width from 1 to MAX_W bits, and
// a choice of MSB-first or LSB-first. The obvious implementation of the second
// is a bidirectional shift register: shift left and tap the top for MSB-first,
// shift right and tap the bottom for LSB-first.
//
// That implementation is a mistake, and it is worth being precise about why.
//
//   - It puts a MUX IN THE SHIFT PATH. The shift register is the one piece of
//     this design clocked on every SCLK edge at full rate. Everything else --
//     loading, unloading, counting -- happens once per frame. Adding logic to
//     the per-bit path to serve a per-frame decision is the wrong trade at any
//     frequency.
//   - It DOUBLES the verification surface. There are now two shift directions,
//     each with its own end conditions, its own alignment and its own
//     interaction with the preload of Chapter 13.5. That is not one feature
//     with two settings; it is two designs sharing a name.
//   - It REOPENS 13.6's alignment argument in mirror image, and the mirrored
//     version is not symmetric, because `len` is measured from the low end of
//     the word in both orders.
//
// THE ALTERNATIVE: A TRANSFORM AT THE BOUNDARIES.
//
// Leave the shift register exactly as Chapter 13.6 left it -- one direction,
// MSB-first, untouched, this module instantiates it verbatim -- and put a
// single combinational transform on each side:
//
//     reverse the low `len` bits of the word, leaving the rest alone
//
// Send that instead of the transmit word and apply it again to the received
// word, and the whole of LSB-first is done. The transform is its own inverse,
// so it is the SAME function on both boundaries -- one piece of logic to
// write, one to review, one to get right.
//
// And it sits where nothing is clocked at SCLK rate: the transmit side is
// evaluated on the load cycle, the receive side when the CPU reads. Neither is
// on the per-bit path. `len` can be any width and bit order is free.
//
// WHY `rev(x) >> (MAX_W - len)` REVERSES ONLY THE LOW `len` BITS.
//
// A full-width reversal sends bit 0 to the top, bit 1 next, and so on, so the
// low `len` bits land in the top `len` positions in reversed order -- and
// everything above them is pushed below. One right shift by MAX_W - len drops
// exactly those and brings the reversed field down to the bottom. The result
// is the low `len` bits reversed and zeros above. Doing it as an explicit
// variable-width reversal instead needs a loop over `len`, which synthesises
// to a barrel shifter plus a comparator per bit; this is a fixed permutation
// (free, just wires) and one barrel shifter.

module spi_width_order #(
    parameter int MAX_W = 32,
    parameter int LEN_W = 6
) (
    input  wire               clk,
    input  wire               rst_n,

    input  wire [MAX_W-1:0]   tx_data,
    input  wire [LEN_W-1:0]   len,
    input  wire               lsb_first,
    input  wire               load_stb,

    input  wire               preload_stb,   // from 13.5
    input  wire               launch_stb,
    input  wire               capture_stb,

    input  wire               miso,

    output wire               mosi,
    output wire [MAX_W-1:0]   rx_data,
    output wire               rx_valid_stb,
    output wire               len_err        // a width the datapath cannot hold
);

    // A fixed full-width reversal: pure wiring, no logic at all.
    function automatic [MAX_W-1:0] rev_all(input [MAX_W-1:0] x);
        integer b;
        begin
            rev_all = {MAX_W{1'b0}};
            for (b = 0; b < MAX_W; b = b + 1)
                rev_all[b] = x[MAX_W-1-b];
        end
    endfunction

    // Reverse the low `len` bits, zero above. Its own inverse, which is why
    // the transmit and receive sides can share it.
    function automatic [MAX_W-1:0] rev_field(input [MAX_W-1:0] x,
                                             input [LEN_W-1:0] n);
        begin
            rev_field = rev_all(x) >> (MAX_W - n);
        end
    endfunction

    // A width of zero would make `len - 1` wrap and the frame never end; a
    // width above MAX_W would silently truncate. Both are reported rather
    // than absorbed.
    assign len_err = (len == {LEN_W{1'b0}}) || (len > MAX_W);

    wire [MAX_W-1:0] tx_shaped = lsb_first ? rev_field(tx_data, len) : tx_data;
    wire [MAX_W-1:0] rx_raw;

    assign rx_data = lsb_first ? rev_field(rx_raw, len) : rx_raw;

    // Chapter 13.6's datapath, instantiated unchanged. That it needed no
    // edit to gain two features is the point of the chapter.
    spi_shift_datapath #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_shift (
        .clk(clk), .rst_n(rst_n),
        .tx_data(tx_shaped), .len(len), .load_stb(load_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb),
        .miso(miso), .mosi(mosi),
        .rx_data(rx_raw), .rx_valid_stb(rx_valid_stb)
    );

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order_tb.sv — the recorded MOSI sequence, 293 frames
// spi_width_order_tb.sv
//
// Loopback cannot test bit order. Reversing the transmit word and reversing
// the received word again is a no-op end to end, so a design that reversed
// BOTH sides when it should have reversed neither -- or that reversed nothing
// at all -- passes every loopback test ever written.
//
// So this testbench watches the WIRE. It records the bit sequence actually
// driven on MOSI, in order, and compares it against the order the setting
// demands:
//
//     MSB-first   wire[i] = tx_data[len-1-i]
//     LSB-first   wire[i] = tx_data[i]
//
// Those two agree only on palindromes and on one-bit frames, so the check has
// real teeth. The loopback and independent-slave tests then confirm the two
// transforms undo each other, which is a different claim.

`timescale 1ns/1ps

module spi_width_order_tb;

    localparam int MAX_W = 32;
    localparam int LEN_W = 6;
    localparam int DIV_W = 8;

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

    // --- 13.4 / 13.5 ------------------------------------------------------
    logic             en   = 1'b0;
    logic [DIV_W-1:0] div  = 8'd4;
    logic             cpol = 1'b0;
    wire              sclk, edge_a_stb, edge_b_stb, bit_done, div_err;
    wire [DIV_W-1:0]  half_a, half_b;

    spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
        .clk(clk), .rst_n(rst_n), .en(en), .div(div), .cpol(cpol),
        .sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .bit_done(bit_done), .half_a(half_a), .half_b(half_b),
        .div_err(div_err)
    );

    logic             cpha      = 1'b0;
    logic [LEN_W-1:0] len       = 6'd8;
    logic             active    = 1'b0;
    logic             start_stb = 1'b0;
    wire              preload_stb, launch_stb, capture_stb, frame_done;
    wire [LEN_W-1:0]  bit_idx;

    spi_mode_edges #(.LEN_W(LEN_W)) u_mode (
        .clk(clk), .rst_n(rst_n), .cpha(cpha), .len(len),
        .active(active), .start_stb(start_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
    );

    // --- 13.8, the block under test ---------------------------------------
    logic [MAX_W-1:0] tx_data   = 32'h0;
    logic             lsb_first = 1'b0;
    logic             load_stb  = 1'b0;
    wire              miso;
    wire              mosi;
    wire [MAX_W-1:0]  rx_data;
    wire              rx_valid_stb, len_err;

    spi_width_order #(.MAX_W(MAX_W), .LEN_W(LEN_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .tx_data(tx_data), .len(len), .lsb_first(lsb_first),
        .load_stb(load_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb),
        .miso(miso), .mosi(mosi),
        .rx_data(rx_data), .rx_valid_stb(rx_valid_stb), .len_err(len_err)
    );

    // --- the slave ---------------------------------------------------------
    // It drives a fixed bit SEQUENCE, deliberately knowing nothing about bit
    // order: the wire carries bits, and which end of a word they came from is
    // the master's business. That is what makes it a valid reference for the
    // receive-side transform.
    logic             loopback  = 1'b1;
    logic [MAX_W-1:0] slave_sr  = 32'h0;
    logic             slave_bit_r = 1'b0;

    assign miso = loopback ? mosi : slave_bit_r;

    always_ff @(posedge clk) begin
        if (preload_stb || launch_stb) begin
            slave_bit_r <= slave_sr[MAX_W-1];
            slave_sr    <= {slave_sr[MAX_W-2:0], 1'b0};
        end
    end

    // --- the wire recorder ------------------------------------------------
    logic drive_q;
    integer wire_bits [0:63];
    integer wire_n;
    integer recv_bits [0:63];
    integer recv_n;

    always_ff @(posedge clk) begin
        drive_q <= preload_stb | launch_stb;
        // MOSI is registered, so the bit belonging to a drive strobe is on
        // the pin one cycle later.
        if (drive_q && wire_n < 64) begin
            wire_bits[wire_n] <= mosi;
            wire_n            <= wire_n + 1;
        end else if (drive_q) begin
            wire_n <= wire_n + 1;
        end
        // What the slave actually put on MISO, sampled where the master
        // samples it.
        if (capture_stb) begin
            if (recv_n < 64) recv_bits[recv_n] <= miso;
            recv_n <= recv_n + 1;
        end
    end

    integer errors = 0;
    integer n_frames = 0;
    integer i;

    function automatic [MAX_W-1:0] mask_to(input integer nbits);
        begin
            mask_to = (nbits >= MAX_W) ? {MAX_W{1'b1}}
                                       : ((32'h1 << nbits) - 32'h1);
        end
    endfunction

    task automatic run_frame(input integer dv, input bit pol, input bit pha,
                             input integer nbits, input bit lsb,
                             input [MAX_W-1:0] tx, input [MAX_W-1:0] sl,
                             input bit loop_en);
        integer guard;
        begin
            @(negedge clk);
            en = 1'b0; active = 1'b0; start_stb = 1'b0; load_stb = 1'b0;
            div = dv[DIV_W-1:0]; cpol = pol; cpha = pha;
            len = nbits[LEN_W-1:0]; lsb_first = lsb;
            tx_data  = tx & mask_to(nbits);
            loopback = loop_en;
            // The slave's bit sequence, left-aligned so its first bit is
            // ready at the top.
            slave_sr = sl << (MAX_W - nbits);
            repeat (3) @(negedge clk);

            wire_n = 0; recv_n = 0;

            @(negedge clk);
            load_stb = 1'b1;
            @(negedge clk);
            load_stb = 1'b0;
            @(negedge clk);

            start_stb = 1'b1; active = 1'b1;
            @(negedge clk);
            start_stb = 1'b0;
            repeat (3) @(negedge clk);

            en = 1'b1;
            guard = dv * (nbits + 4) + 60;
            while (!frame_done && guard > 0) begin
                @(negedge clk);
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: frame never completed (div=%0d len=%0d)",
                         dv, nbits);
                errors = errors + 1;
            end
            repeat (dv + 2) @(negedge clk);
            en = 1'b0; active = 1'b0;
            repeat (3) @(negedge clk);
            n_frames = n_frames + 1;
        end
    endtask

    // The whole of the bit-order claim, checked against the pin.
    task automatic check_wire(input integer nbits, input bit lsb,
                              input [MAX_W-1:0] tx, input string tag);
        integer k, bad, want;
        begin
            bad = 0;
            if (wire_n != nbits) begin
                $display("  FAIL: %0s drove %0d bits for a %0d-bit frame",
                         tag, wire_n, nbits);
                errors = errors + 1;
            end else begin
                for (k = 0; k < nbits; k = k + 1) begin
                    want = lsb ? tx[k] : tx[nbits-1-k];
                    if (wire_bits[k] !== want) bad = bad + 1;
                end
                if (bad != 0) begin
                    $display("  FAIL: %0s sent %0d of %0d bits in the wrong order",
                             tag, bad, nbits);
                    errors = errors + 1;
                end
            end
        end
    endtask

    // The received word must be assembled from the bits that actually arrived,
    // in the order the setting demands.
    task automatic check_recv(input integer nbits, input bit lsb,
                              input string tag);
        integer k;
        logic [MAX_W-1:0] want;
        begin
            want = {MAX_W{1'b0}};
            for (k = 0; k < nbits; k = k + 1) begin
                if (lsb) want[k]           = recv_bits[k][0];
                else     want[nbits-1-k]   = recv_bits[k][0];
            end
            if (recv_n != nbits) begin
                $display("  FAIL: %0s captured %0d bits for a %0d-bit frame",
                         tag, recv_n, nbits);
                errors = errors + 1;
            end else if (rx_data !== want) begin
                $display("  FAIL: %0s assembled %08h from the wire, expected %08h",
                         tag, rx_data, want);
                errors = errors + 1;
            end
        end
    endtask

    task automatic show_wire(input integer nbits, input string tag);
        integer k;
        begin
            $write("  %0s: ", tag);
            for (k = 0; k < nbits; k = k + 1) $write("%0d", wire_bits[k]);
            $write("\n");
        end
    endtask

    integer lens [0:5];
    integer l, d, p, h, o, seed;
    logic [MAX_W-1:0] tv, sv;

    initial begin
        lens[0] = 1; lens[1] = 2; lens[2] = 5; lens[3] = 8;
        lens[4] = 13; lens[5] = 32;
        seed = 32'h0BAD_F00D;
        wire_n = 0; recv_n = 0;

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

        // 1. THE SAME BYTE, BOTH ORDERS. 0x8D is 1000_1101 -- not a
        //    palindrome, so the two wires must differ visibly.
        run_frame(4, 1'b0, 1'b0, 8, 1'b0, 32'h8D, 32'h0, 1'b1);
        check_wire(8, 1'b0, 32'h8D, "MSB-first 0x8D");
        show_wire(8, "MSB-first 0x8D on the wire");
        if (rx_data[7:0] !== 8'h8D) begin
            $display("  FAIL: MSB-first loopback returned %02h", rx_data[7:0]);
            errors = errors + 1;
        end

        run_frame(4, 1'b0, 1'b0, 8, 1'b1, 32'h8D, 32'h0, 1'b1);
        check_wire(8, 1'b1, 32'h8D, "LSB-first 0x8D");
        show_wire(8, "LSB-first 0x8D on the wire");
        if (rx_data[7:0] !== 8'h8D) begin
            $display("  FAIL: LSB-first loopback returned %02h", rx_data[7:0]);
            errors = errors + 1;
        end
        $display("  the two wires are bit-reversals of each other, and both loop back to 8d -- the transform is its own inverse");

        // 2. A NARROW LSB-FIRST FRAME. 5 bits of 0x15 (10101) is a
        //    palindrome, so use 0x19 (11001) -- reversed it is 10011.
        run_frame(4, 1'b0, 1'b0, 5, 1'b1, 32'h19, 32'h0, 1'b1);
        check_wire(5, 1'b1, 32'h19, "LSB-first 5-bit");
        show_wire(5, "LSB-first 5 bits of 0x19");
        if (rx_data !== 32'h19) begin
            $display("  FAIL: a 5-bit LSB-first frame returned %08h", rx_data);
            errors = errors + 1;
        end
        $display("  a 5-bit LSB-first frame reverses only those five bits and leaves nothing above them");

        // 3. AN INDEPENDENT SLAVE, BOTH ORDERS. The slave drives a bit
        //    SEQUENCE; the master must assemble it into a word the right way
        //    round. This is the check loopback structurally cannot make.
        run_frame(4, 1'b0, 1'b0, 8, 1'b0, 32'h00, 32'hB2, 1'b0);
        check_recv(8, 1'b0, "MSB-first from the slave");
        run_frame(4, 1'b0, 1'b0, 8, 1'b1, 32'h00, 32'hB2, 1'b0);
        check_recv(8, 1'b1, "LSB-first from the slave");
        $display("  the same wire sequence assembles to %08h one way round -- the receive transform is doing work loopback would hide",
                 rx_data);

        // 4. AN ILLEGAL WIDTH is reported, not absorbed.
        @(negedge clk);
        len = 6'd0;
        @(negedge clk);
        if (!len_err) begin
            $display("  FAIL: len=0 was not reported"); errors = errors + 1;
        end
        len = 6'd33;
        @(negedge clk);
        if (!len_err) begin
            $display("  FAIL: len=33 was not reported with a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd32;
        @(negedge clk);
        if (len_err) begin
            $display("  FAIL: len=32 was reported on a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd1;
        @(negedge clk);
        if (len_err) begin
            $display("  FAIL: len=1 was reported"); errors = errors + 1;
        end
        $display("  widths 0 and 33 reported on a 32-bit datapath; 1 and 32 accepted");

        // 5. THE SWEEP. Both orders, six widths, three divisors, all four
        //    modes, against an independent slave, checking the wire order,
        //    the assembled word, and the loopback identity.
        for (o = 0; o <= 1; o = o + 1)
            for (l = 0; l < 6; l = l + 1)
                for (d = 2; d <= 4; d = d + 1)
                    for (p = 0; p <= 1; p = p + 1)
                        for (h = 0; h <= 1; h = h + 1) begin
                            seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                            tv   = seed & mask_to(lens[l]);
                            seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                            sv   = seed & mask_to(lens[l]);

                            run_frame(d, p[0], h[0], lens[l], o[0],
                                      tv, sv, 1'b0);
                            check_wire(lens[l], o[0], tv, "sweep wire");
                            check_recv(lens[l], o[0], "sweep recv");

                            run_frame(d, p[0], h[0], lens[l], o[0],
                                      tv, 32'h0, 1'b1);
                            check_wire(lens[l], o[0], tv, "sweep loop wire");
                            if (rx_data !== tv) begin
                                $display("  FAIL: loopback len=%0d lsb=%0d returned %08h, sent %08h",
                                         lens[l], o, rx_data, tv);
                                errors = errors + 1;
                            end
                        end
        $display("  %0d frames swept: both bit orders, widths 1..32, three divisors, all four modes",
                 n_frames);

        if (errors == 0)
            $display("PASS: bit order is a single reversal of the low `len` bits applied at both boundaries -- the same function, its own inverse -- and the shift register of Chapter 13.6 is instantiated unchanged with no mux anywhere in the per-bit path -- the recorded MOSI sequence matches the order the setting demands for every one of %0d frames spanning both orders, widths from 1 to 32 bits, three divisors and all four modes, the word assembled from the bits a slave actually drove is correct in both orders where a loopback test could not tell the difference, a narrow frame reverses only the bits inside it, and widths of zero or above the datapath width are reported rather than silently wrapping or truncating", n_frames);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order.v — the same wrapper in Verilog-2001
// spi_width_order.v
//
// Chapter 13.8 -- configurable frame width and bit order, WITHOUT touching
// the shift register.
//
// Two features are wanted here. Frames of any width from 1 to MAX_W bits, and
// a choice of MSB-first or LSB-first. The obvious implementation of the second
// is a bidirectional shift register: shift left and tap the top for MSB-first,
// shift right and tap the bottom for LSB-first.
//
// That implementation is a mistake, and it is worth being precise about why.
//
//   - It puts a MUX IN THE SHIFT PATH. The shift register is the one piece of
//     this design clocked on every SCLK edge at full rate. Everything else --
//     loading, unloading, counting -- happens once per frame. Adding logic to
//     the per-bit path to serve a per-frame decision is the wrong trade at any
//     frequency.
//   - It DOUBLES the verification surface. There are now two shift directions,
//     each with its own end conditions, its own alignment and its own
//     interaction with the preload of Chapter 13.5. That is not one feature
//     with two settings; it is two designs sharing a name.
//   - It REOPENS 13.6's alignment argument in mirror image, and the mirrored
//     version is not symmetric, because `len` is measured from the low end of
//     the word in both orders.
//
// THE ALTERNATIVE: A TRANSFORM AT THE BOUNDARIES.
//
// Leave the shift register exactly as Chapter 13.6 left it -- one direction,
// MSB-first, untouched, this module instantiates it verbatim -- and put a
// single combinational transform on each side:
//
//     reverse the low `len` bits of the word, leaving the rest alone
//
// Send that instead of the transmit word and apply it again to the received
// word, and the whole of LSB-first is done. The transform is its own inverse,
// so it is the SAME function on both boundaries -- one piece of logic to
// write, one to review, one to get right.
//
// And it sits where nothing is clocked at SCLK rate: the transmit side is
// evaluated on the load cycle, the receive side when the CPU reads. Neither is
// on the per-bit path. `len` can be any width and bit order is free.
//
// WHY `rev(x) >> (MAX_W - len)` REVERSES ONLY THE LOW `len` BITS.
//
// A full-width reversal sends bit 0 to the top, bit 1 next, and so on, so the
// low `len` bits land in the top `len` positions in reversed order -- and
// everything above them is pushed below. One right shift by MAX_W - len drops
// exactly those and brings the reversed field down to the bottom. The result
// is the low `len` bits reversed and zeros above. Doing it as an explicit
// variable-width reversal instead needs a loop over `len`, which synthesises
// to a barrel shifter plus a comparator per bit; this is a fixed permutation
// (free, just wires) and one barrel shifter.

module spi_width_order #(
    parameter MAX_W = 32,
    parameter LEN_W = 6
) (
    input  wire               clk,
    input  wire               rst_n,

    input  wire [MAX_W-1:0]   tx_data,
    input  wire [LEN_W-1:0]   len,
    input  wire               lsb_first,
    input  wire               load_stb,

    input  wire               preload_stb,   // from 13.5
    input  wire               launch_stb,
    input  wire               capture_stb,

    input  wire               miso,

    output wire               mosi,
    output wire [MAX_W-1:0]   rx_data,
    output wire               rx_valid_stb,
    output wire               len_err        // a width the datapath cannot hold
);

    // A fixed full-width reversal: pure wiring, no logic at all.
        function [MAX_W-1:0] rev_all;
        input [MAX_W-1:0] x;
        integer b;
        begin
            rev_all = {MAX_W{1'b0}};
            for (b = 0; b < MAX_W; b = b + 1)
                rev_all[b] = x[MAX_W-1-b];
        end
    endfunction

    // Reverse the low `len` bits, zero above. Its own inverse, which is why
    // the transmit and receive sides can share it.
        function [MAX_W-1:0] rev_field;
        input [MAX_W-1:0] x;
        input [LEN_W-1:0] n;
        begin
            rev_field = rev_all(x) >> (MAX_W - n);
        end
    endfunction

    // A width of zero would make `len - 1` wrap and the frame never end; a
    // width above MAX_W would silently truncate. Both are reported rather
    // than absorbed.
    assign len_err = (len == {LEN_W{1'b0}}) || (len > MAX_W);

    wire [MAX_W-1:0] tx_shaped = lsb_first ? rev_field(tx_data, len) : tx_data;
    wire [MAX_W-1:0] rx_raw;

    assign rx_data = lsb_first ? rev_field(rx_raw, len) : rx_raw;

    // Chapter 13.6's datapath, instantiated unchanged. That it needed no
    // edit to gain two features is the point of the chapter.
    spi_shift_datapath #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_shift (
        .clk(clk), .rst_n(rst_n),
        .tx_data(tx_shaped), .len(len), .load_stb(load_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb),
        .miso(miso), .mosi(mosi),
        .rx_data(rx_raw), .rx_valid_stb(rx_valid_stb)
    );

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order_tb.v — the same wire-order checks in Verilog-2001
// spi_width_order_tb.v
//
// Loopback cannot test bit order. Reversing the transmit word and reversing
// the received word again is a no-op end to end, so a design that reversed
// BOTH sides when it should have reversed neither -- or that reversed nothing
// at all -- passes every loopback test ever written.
//
// So this testbench watches the WIRE. It records the bit sequence actually
// driven on MOSI, in order, and compares it against the order the setting
// demands:
//
//     MSB-first   wire[i] = tx_data[len-1-i]
//     LSB-first   wire[i] = tx_data[i]
//
// Those two agree only on palindromes and on one-bit frames, so the check has
// real teeth. The loopback and independent-slave tests then confirm the two
// transforms undo each other, which is a different claim.

`timescale 1ns/1ps

module spi_width_order_tb;

    localparam MAX_W = 32;
    localparam LEN_W = 6;
    localparam DIV_W = 8;

    reg clk;
    reg rst_n;
    always #5 clk = ~clk;

    // --- 13.4 / 13.5 ------------------------------------------------------
    reg             en;
    reg [DIV_W-1:0] div;
    reg             cpol;
    wire              sclk, edge_a_stb, edge_b_stb, bit_done, div_err;
    wire [DIV_W-1:0]  half_a, half_b;

    spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
        .clk(clk), .rst_n(rst_n), .en(en), .div(div), .cpol(cpol),
        .sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .bit_done(bit_done), .half_a(half_a), .half_b(half_b),
        .div_err(div_err)
    );

    reg             cpha;
    reg [LEN_W-1:0] len;
    reg             active;
    reg             start_stb;
    wire              preload_stb, launch_stb, capture_stb, frame_done;
    wire [LEN_W-1:0]  bit_idx;

    spi_mode_edges #(.LEN_W(LEN_W)) u_mode (
        .clk(clk), .rst_n(rst_n), .cpha(cpha), .len(len),
        .active(active), .start_stb(start_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
    );

    // --- 13.8, the block under test ---------------------------------------
    reg [MAX_W-1:0] tx_data;
    reg             lsb_first;
    reg             load_stb;
    wire              miso;
    wire              mosi;
    wire [MAX_W-1:0]  rx_data;
    wire              rx_valid_stb, len_err;

    spi_width_order #(.MAX_W(MAX_W), .LEN_W(LEN_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .tx_data(tx_data), .len(len), .lsb_first(lsb_first),
        .load_stb(load_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb),
        .miso(miso), .mosi(mosi),
        .rx_data(rx_data), .rx_valid_stb(rx_valid_stb), .len_err(len_err)
    );

    // --- the slave ---------------------------------------------------------
    // It drives a fixed bit SEQUENCE, deliberately knowing nothing about bit
    // order: the wire carries bits, and which end of a word they came from is
    // the master's business. That is what makes it a valid reference for the
    // receive-side transform.
    reg             loopback;
    reg [MAX_W-1:0] slave_sr;
    reg             slave_bit_r;

    assign miso = loopback ? mosi : slave_bit_r;

    always @(posedge clk) begin
        if (preload_stb || launch_stb) begin
            slave_bit_r <= slave_sr[MAX_W-1];
            slave_sr    <= {slave_sr[MAX_W-2:0], 1'b0};
        end
    end

    // --- the wire recorder ------------------------------------------------
    reg drive_q;
    integer wire_bits [0:63];
    integer wire_n;
    integer recv_bits [0:63];
    integer recv_n;

    always @(posedge clk) begin
        drive_q <= preload_stb | launch_stb;
        // MOSI is registered, so the bit belonging to a drive strobe is on
        // the pin one cycle later.
        if (drive_q && wire_n < 64) begin
            wire_bits[wire_n] <= mosi;
            wire_n            <= wire_n + 1;
        end else if (drive_q) begin
            wire_n <= wire_n + 1;
        end
        // What the slave actually put on MISO, sampled where the master
        // samples it.
        if (capture_stb) begin
            if (recv_n < 64) recv_bits[recv_n] <= miso;
            recv_n <= recv_n + 1;
        end
    end

    integer errors;
    integer n_frames;
    integer i;

        function [MAX_W-1:0] mask_to;
        input integer nbits;
        begin
            mask_to = (nbits >= MAX_W) ? {MAX_W{1'b1}}
                                       : ((32'h1 << nbits) - 32'h1);
        end
    endfunction

        task run_frame;
        input integer dv;
        input pol;
        input pha;
        input integer nbits;
        input lsb;
        input [MAX_W-1:0] tx;
        input [MAX_W-1:0] sl;
        input loop_en;
        integer guard;
        begin
            @(negedge clk);
            en = 1'b0; active = 1'b0; start_stb = 1'b0; load_stb = 1'b0;
            div = dv[DIV_W-1:0]; cpol = pol; cpha = pha;
            len = nbits[LEN_W-1:0]; lsb_first = lsb;
            tx_data  = tx & mask_to(nbits);
            loopback = loop_en;
            // The slave's bit sequence, left-aligned so its first bit is
            // ready at the top.
            slave_sr = sl << (MAX_W - nbits);
            repeat (3) @(negedge clk);

            wire_n = 0; recv_n = 0;

            @(negedge clk);
            load_stb = 1'b1;
            @(negedge clk);
            load_stb = 1'b0;
            @(negedge clk);

            start_stb = 1'b1; active = 1'b1;
            @(negedge clk);
            start_stb = 1'b0;
            repeat (3) @(negedge clk);

            en = 1'b1;
            guard = dv * (nbits + 4) + 60;
            while (!frame_done && guard > 0) begin
                @(negedge clk);
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: frame never completed (div=%0d len=%0d)",
                         dv, nbits);
                errors = errors + 1;
            end
            repeat (dv + 2) @(negedge clk);
            en = 1'b0; active = 1'b0;
            repeat (3) @(negedge clk);
            n_frames = n_frames + 1;
        end
    endtask

    // The whole of the bit-order claim, checked against the pin.
        task check_wire;
        input integer nbits;
        input lsb;
        input [MAX_W-1:0] tx;
        input [8*40:1] tag;
        integer k, bad, want;
        begin
            bad = 0;
            if (wire_n != nbits) begin
                $display("  FAIL: %0s drove %0d bits for a %0d-bit frame",
                         tag, wire_n, nbits);
                errors = errors + 1;
            end else begin
                for (k = 0; k < nbits; k = k + 1) begin
                    want = lsb ? tx[k] : tx[nbits-1-k];
                    if (wire_bits[k] !== want) bad = bad + 1;
                end
                if (bad != 0) begin
                    $display("  FAIL: %0s sent %0d of %0d bits in the wrong order",
                             tag, bad, nbits);
                    errors = errors + 1;
                end
            end
        end
    endtask

    // The received word must be assembled from the bits that actually arrived,
    // in the order the setting demands.
        task check_recv;
        input integer nbits;
        input lsb;
        input [8*40:1] tag;
        integer k;
        reg [MAX_W-1:0] want;
        begin
            want = {MAX_W{1'b0}};
            for (k = 0; k < nbits; k = k + 1) begin
                if (lsb) want[k]           = recv_bits[k][0];
                else     want[nbits-1-k]   = recv_bits[k][0];
            end
            if (recv_n != nbits) begin
                $display("  FAIL: %0s captured %0d bits for a %0d-bit frame",
                         tag, recv_n, nbits);
                errors = errors + 1;
            end else if (rx_data !== want) begin
                $display("  FAIL: %0s assembled %08h from the wire, expected %08h",
                         tag, rx_data, want);
                errors = errors + 1;
            end
        end
    endtask

        task show_wire;
        input integer nbits;
        input [8*40:1] tag;
        integer k;
        begin
            $write("  %0s: ", tag);
            for (k = 0; k < nbits; k = k + 1) $write("%0d", wire_bits[k]);
            $write("\n");
        end
    endtask

    integer lens [0:5];
    integer l, d, p, h, o, seed;
    reg [MAX_W-1:0] tv, sv;

    initial begin
        lens[0] = 1; lens[1] = 2; lens[2] = 5; lens[3] = 8;
        lens[4] = 13; lens[5] = 32;
        seed = 32'h0BAD_F00D;
        wire_n = 0; recv_n = 0;

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

        // 1. THE SAME BYTE, BOTH ORDERS. 0x8D is 1000_1101 -- not a
        //    palindrome, so the two wires must differ visibly.
        run_frame(4, 1'b0, 1'b0, 8, 1'b0, 32'h8D, 32'h0, 1'b1);
        check_wire(8, 1'b0, 32'h8D, "MSB-first 0x8D");
        show_wire(8, "MSB-first 0x8D on the wire");
        if (rx_data[7:0] !== 8'h8D) begin
            $display("  FAIL: MSB-first loopback returned %02h", rx_data[7:0]);
            errors = errors + 1;
        end

        run_frame(4, 1'b0, 1'b0, 8, 1'b1, 32'h8D, 32'h0, 1'b1);
        check_wire(8, 1'b1, 32'h8D, "LSB-first 0x8D");
        show_wire(8, "LSB-first 0x8D on the wire");
        if (rx_data[7:0] !== 8'h8D) begin
            $display("  FAIL: LSB-first loopback returned %02h", rx_data[7:0]);
            errors = errors + 1;
        end
        $display("  the two wires are bit-reversals of each other, and both loop back to 8d -- the transform is its own inverse");

        // 2. A NARROW LSB-FIRST FRAME. 5 bits of 0x15 (10101) is a
        //    palindrome, so use 0x19 (11001) -- reversed it is 10011.
        run_frame(4, 1'b0, 1'b0, 5, 1'b1, 32'h19, 32'h0, 1'b1);
        check_wire(5, 1'b1, 32'h19, "LSB-first 5-bit");
        show_wire(5, "LSB-first 5 bits of 0x19");
        if (rx_data !== 32'h19) begin
            $display("  FAIL: a 5-bit LSB-first frame returned %08h", rx_data);
            errors = errors + 1;
        end
        $display("  a 5-bit LSB-first frame reverses only those five bits and leaves nothing above them");

        // 3. AN INDEPENDENT SLAVE, BOTH ORDERS. The slave drives a bit
        //    SEQUENCE; the master must assemble it into a word the right way
        //    round. This is the check loopback structurally cannot make.
        run_frame(4, 1'b0, 1'b0, 8, 1'b0, 32'h00, 32'hB2, 1'b0);
        check_recv(8, 1'b0, "MSB-first from the slave");
        run_frame(4, 1'b0, 1'b0, 8, 1'b1, 32'h00, 32'hB2, 1'b0);
        check_recv(8, 1'b1, "LSB-first from the slave");
        $display("  the same wire sequence assembles to %08h one way round -- the receive transform is doing work loopback would hide",
                 rx_data);

        // 4. AN ILLEGAL WIDTH is reported, not absorbed.
        @(negedge clk);
        len = 6'd0;
        @(negedge clk);
        if (!len_err) begin
            $display("  FAIL: len=0 was not reported"); errors = errors + 1;
        end
        len = 6'd33;
        @(negedge clk);
        if (!len_err) begin
            $display("  FAIL: len=33 was not reported with a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd32;
        @(negedge clk);
        if (len_err) begin
            $display("  FAIL: len=32 was reported on a 32-bit datapath");
            errors = errors + 1;
        end
        len = 6'd1;
        @(negedge clk);
        if (len_err) begin
            $display("  FAIL: len=1 was reported"); errors = errors + 1;
        end
        $display("  widths 0 and 33 reported on a 32-bit datapath; 1 and 32 accepted");

        // 5. THE SWEEP. Both orders, six widths, three divisors, all four
        //    modes, against an independent slave, checking the wire order,
        //    the assembled word, and the loopback identity.
        for (o = 0; o <= 1; o = o + 1)
            for (l = 0; l < 6; l = l + 1)
                for (d = 2; d <= 4; d = d + 1)
                    for (p = 0; p <= 1; p = p + 1)
                        for (h = 0; h <= 1; h = h + 1) begin
                            seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                            tv   = seed & mask_to(lens[l]);
                            seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
                            sv   = seed & mask_to(lens[l]);

                            run_frame(d, p[0], h[0], lens[l], o[0],
                                      tv, sv, 1'b0);
                            check_wire(lens[l], o[0], tv, "sweep wire");
                            check_recv(lens[l], o[0], "sweep recv");

                            run_frame(d, p[0], h[0], lens[l], o[0],
                                      tv, 32'h0, 1'b1);
                            check_wire(lens[l], o[0], tv, "sweep loop wire");
                            if (rx_data !== tv) begin
                                $display("  FAIL: loopback len=%0d lsb=%0d returned %08h, sent %08h",
                                         lens[l], o, rx_data, tv);
                                errors = errors + 1;
                            end
                        end
        $display("  %0d frames swept: both bit orders, widths 1..32, three divisors, all four modes",
                 n_frames);

        if (errors == 0)
            $display("PASS: bit order is a single reversal of the low `len` bits applied at both boundaries -- the same function, its own inverse -- and the shift register of Chapter 13.6 is instantiated unchanged with no mux anywhere in the per-bit path -- the recorded MOSI sequence matches the order the setting demands for every one of %0d frames spanning both orders, widths from 1 to 32 bits, three divisors and all four modes, the word assembled from the bits a slave actually drove is correct in both orders where a loopback test could not tell the difference, a narrow frame reverses only the bits inside it, and widths of zero or above the datapath width are reported rather than silently wrapping or truncating", n_frames);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        en = 1'b0;
        div = 8'd4;
        cpol = 1'b0;
        cpha = 1'b0;
        len = 6'd8;
        active = 1'b0;
        start_stb = 1'b0;
        tx_data = 32'h0;
        lsb_first = 1'b0;
        load_stb = 1'b0;
        loopback = 1'b1;
        slave_sr = 32'h0;
        slave_bit_r = 1'b0;
        errors = 0;
        n_frames = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order.vhd — the same wrapper in VHDL
-- spi_width_order.vhd
--
-- Chapter 13.8 -- configurable frame width and bit order, WITHOUT touching
-- the shift register.
--
-- The obvious implementation of LSB-first is a bidirectional shift register:
-- shift left and tap the top one way, shift right and tap the bottom the
-- other. That is a mistake, and it is worth being precise about why.
--
--   - It puts a MUX IN THE SHIFT PATH. The shift register is the one piece of
--     this design clocked on every SCLK edge. Everything else -- loading,
--     unloading, counting -- happens once per frame. Adding logic to the
--     per-bit path to serve a per-frame decision is the wrong trade.
--   - It DOUBLES the verification surface: two shift directions, each with its
--     own end conditions, alignment and interaction with 13.5's preload.
--   - It REOPENS 13.6's alignment argument in mirror image, and the mirror is
--     not symmetric, because `len` is measured from the low end either way.
--
-- THE ALTERNATIVE: A TRANSFORM AT THE BOUNDARIES. Leave the shift register
-- exactly as Chapter 13.6 left it -- one direction, MSB-first, instantiated
-- here verbatim -- and put a single combinational transform on each side:
--
--     reverse the low `len` bits of the word, leaving the rest alone
--
-- Send that instead of the transmit word and apply it again to the received
-- word, and the whole of LSB-first is done. The transform is its own inverse,
-- so it is the SAME function on both boundaries. And it sits where nothing is
-- clocked at SCLK rate: the transmit side on the load cycle, the receive side
-- when the CPU reads.
--
-- WHY `rev_all(x) srl (MAX_W - len)` REVERSES ONLY THE LOW `len` BITS. A
-- full-width reversal sends bit 0 to the top, bit 1 next, so the low `len`
-- bits land in the top `len` positions reversed and everything above them is
-- pushed below. One right shift by MAX_W - len drops exactly those and brings
-- the reversed field down to the bottom. Done as an explicit variable-width
-- reversal instead it needs a comparator per bit; this is a fixed permutation
-- (free, just wires) plus one barrel shifter.

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

entity spi_width_order is
    generic (
        MAX_W : positive := 32;
        LEN_W : positive := 6
    );
    port (
        clk          : in  std_logic;
        rst_n        : in  std_logic;

        tx_data      : in  std_logic_vector(MAX_W - 1 downto 0);
        len          : in  unsigned(LEN_W - 1 downto 0);
        lsb_first    : in  std_logic;
        load_stb     : in  std_logic;

        preload_stb  : in  std_logic;   -- from 13.5
        launch_stb   : in  std_logic;
        capture_stb  : in  std_logic;

        miso         : in  std_logic;

        mosi         : out std_logic;
        rx_data      : out std_logic_vector(MAX_W - 1 downto 0);
        rx_valid_stb : out std_logic;
        len_err      : out std_logic    -- a width the datapath cannot hold
    );
end entity;

architecture rtl of spi_width_order is

    -- A fixed full-width reversal: pure wiring, no logic at all.
    function rev_all(x : std_logic_vector(MAX_W - 1 downto 0))
        return std_logic_vector is
        variable r : std_logic_vector(MAX_W - 1 downto 0);
    begin
        for b in 0 to MAX_W - 1 loop
            r(b) := x(MAX_W - 1 - b);
        end loop;
        return r;
    end function;

    -- Reverse the low `len` bits, zero above. Its own inverse, which is why
    -- the transmit and receive sides can share it.
    function rev_field(x : std_logic_vector(MAX_W - 1 downto 0);
                       n : natural) return std_logic_vector is
    begin
        return std_logic_vector(shift_right(unsigned(rev_all(x)), MAX_W - n));
    end function;

    signal tx_shaped : std_logic_vector(MAX_W - 1 downto 0);
    signal rx_raw    : std_logic_vector(MAX_W - 1 downto 0);
    signal n_bits    : natural;

begin

    -- A width of zero would make `len - 1` wrap and the frame never end; a
    -- width above MAX_W would silently truncate. Both are reported rather
    -- than absorbed. `n_bits` is clamped so the shift amounts stay in range
    -- even while an illegal width is being presented.
    len_err <= '1' when (len = 0) or (to_integer(len) > MAX_W) else '0';

    n_bits <= 1 when len = 0
              else MAX_W when to_integer(len) > MAX_W
              else to_integer(len);

    tx_shaped <= rev_field(tx_data, n_bits) when lsb_first = '1' else tx_data;
    rx_data   <= rev_field(rx_raw,  n_bits) when lsb_first = '1' else rx_raw;

    -- Chapter 13.6's datapath, instantiated unchanged. That it needed no edit
    -- to gain two features is the point of the chapter.
    u_shift : entity work.spi_shift_datapath
        generic map (MAX_W => MAX_W, LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  tx_data => tx_shaped, len => len, load_stb => load_stb,
                  preload_stb => preload_stb, launch_stb => launch_stb,
                  capture_stb => capture_stb,
                  miso => miso, mosi => mosi,
                  rx_data => rx_raw, rx_valid_stb => rx_valid_stb);

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order_tb.vhd — the same wire-order checks in VHDL
-- spi_width_order_tb.vhd
--
-- Loopback cannot test bit order. Reversing the transmit word and reversing
-- the received word again is a no-op end to end, so a design that reversed
-- BOTH sides when it should have reversed neither -- or that reversed nothing
-- at all -- passes every loopback test ever written.
--
-- So this testbench watches the WIRE. It records the bit sequence actually
-- driven on MOSI, in order, and compares it against the order the setting
-- demands:
--
--     MSB-first   wire(i) = tx_data(len-1-i)
--     LSB-first   wire(i) = tx_data(i)
--
-- Those agree only on palindromes and one-bit frames, so the check has teeth.

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

entity spi_width_order_tb is
end entity;

architecture sim of spi_width_order_tb is

    constant MAX_W : positive := 32;
    constant LEN_W : positive := 6;
    constant DIV_W : positive := 8;

    type bit_array is array (0 to 63) of std_logic;

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

    signal en   : std_logic := '0';
    signal div  : unsigned(DIV_W - 1 downto 0) := to_unsigned(4, DIV_W);
    signal cpol : std_logic := '0';
    signal sclk, edge_a_stb, edge_b_stb, bit_done, div_err : std_logic;
    signal half_a, half_b : unsigned(DIV_W - 1 downto 0);

    signal cpha      : std_logic := '0';
    signal len       : unsigned(LEN_W - 1 downto 0) := to_unsigned(8, LEN_W);
    signal active    : std_logic := '0';
    signal start_stb : std_logic := '0';
    signal preload_stb, launch_stb, capture_stb, frame_done : std_logic;
    signal bit_idx   : unsigned(LEN_W - 1 downto 0);

    signal tx_data   : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal lsb_first : std_logic := '0';
    signal load_stb  : std_logic := '0';
    signal miso, mosi : std_logic;
    signal rx_data   : std_logic_vector(MAX_W - 1 downto 0);
    signal rx_valid_stb, len_err : std_logic;

    -- The slave drives a fixed bit SEQUENCE, deliberately knowing nothing
    -- about bit order: the wire carries bits, and which end of a word they
    -- came from is the master's business. That is what makes it a valid
    -- reference for the receive-side transform.
    signal loopback    : std_logic := '1';
    signal slave_seed  : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal slave_load  : std_logic := '0';
    signal slave_sr    : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal slave_bit_r : std_logic := '0';

    signal drive_q   : std_logic := '0';
    signal wire_bits : bit_array := (others => '0');
    signal wire_n    : natural   := 0;
    signal recv_bits : bit_array := (others => '0');
    signal recv_n    : natural   := 0;
    signal rec_clr   : std_logic := '0';

    signal errors   : natural := 0;
    signal n_frames : natural := 0;

    function mask_to(nbits : natural) return std_logic_vector is
        variable m : std_logic_vector(MAX_W - 1 downto 0);
    begin
        m := (others => '0');
        for k in 0 to MAX_W - 1 loop
            if k < nbits then m(k) := '1'; end if;
        end loop;
        return m;
    end function;

    function hex8(v : std_logic_vector) return string is
        constant DIGITS : string(1 to 16) := "0123456789abcdef";
        variable u : unsigned(MAX_W - 1 downto 0);
        variable r : string(1 to 8);
    begin
        u := unsigned(v);
        for k in 8 downto 1 loop
            r(k) := DIGITS(to_integer(u(3 downto 0)) + 1);
            u := shift_right(u, 4);
        end loop;
        return r;
    end function;

begin

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

    u_div : entity work.spi_clkdiv_strobe
        generic map (DIV_W => DIV_W)
        port map (clk => clk, rst_n => rst_n, en => en, div => div,
                  cpol => cpol, sclk => sclk,
                  edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
                  bit_done => bit_done, half_a => half_a, half_b => half_b,
                  div_err => div_err);

    u_mode : entity work.spi_mode_edges
        generic map (LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n, cpha => cpha, len => len,
                  active => active, start_stb => start_stb,
                  edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
                  preload_stb => preload_stb, launch_stb => launch_stb,
                  capture_stb => capture_stb, bit_idx => bit_idx,
                  frame_done => frame_done);

    dut : entity work.spi_width_order
        generic map (MAX_W => MAX_W, LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  tx_data => tx_data, len => len, lsb_first => lsb_first,
                  load_stb => load_stb,
                  preload_stb => preload_stb, launch_stb => launch_stb,
                  capture_stb => capture_stb,
                  miso => miso, mosi => mosi,
                  rx_data => rx_data, rx_valid_stb => rx_valid_stb,
                  len_err => len_err);

    miso <= mosi when loopback = '1' else slave_bit_r;

    slave_p : process (clk)
    begin
        if rising_edge(clk) then
            if slave_load = '1' then
                slave_sr    <= slave_seed;
                slave_bit_r <= '0';
            elsif preload_stb = '1' or launch_stb = '1' then
                slave_bit_r <= slave_sr(MAX_W - 1);
                slave_sr    <= slave_sr(MAX_W - 2 downto 0) & '0';
            end if;
        end if;
    end process;

    recorder : process (clk)
    begin
        if rising_edge(clk) then
            drive_q <= preload_stb or launch_stb;
            if rec_clr = '1' then
                wire_n <= 0;
                recv_n <= 0;
            else
                -- MOSI is registered, so the bit belonging to a drive strobe
                -- is on the pin one cycle later.
                if drive_q = '1' then
                    if wire_n < 64 then
                        wire_bits(wire_n) <= mosi;
                    end if;
                    wire_n <= wire_n + 1;
                end if;
                -- What the slave actually put on MISO, sampled where the
                -- master samples it.
                if capture_stb = '1' then
                    if recv_n < 64 then
                        recv_bits(recv_n) <= miso;
                    end if;
                    recv_n <= recv_n + 1;
                end if;
            end if;
        end if;
    end process;

    stim : process
        variable errs  : natural := 0;
        variable guard : natural;
        variable seed  : unsigned(31 downto 0) := x"0BADF00D";
        variable tv, sv : std_logic_vector(MAX_W - 1 downto 0);

        procedure run_frame(dv : natural; pol : std_logic; pha : std_logic;
                            nbits : natural; lsb : std_logic;
                            tx : std_logic_vector(MAX_W - 1 downto 0);
                            sl : std_logic_vector(MAX_W - 1 downto 0);
                            loop_en : std_logic) is
        begin
            wait until falling_edge(clk);
            en <= '0'; active <= '0'; start_stb <= '0'; load_stb <= '0';
            div <= to_unsigned(dv, DIV_W); cpol <= pol; cpha <= pha;
            len <= to_unsigned(nbits, LEN_W); lsb_first <= lsb;
            tx_data  <= tx and mask_to(nbits);
            loopback <= loop_en;
            -- The slave's bit sequence, left-aligned so its first bit is
            -- ready at the top.
            slave_seed <= std_logic_vector(
                              shift_left(unsigned(sl and mask_to(nbits)),
                                         MAX_W - nbits));
            for k in 1 to 3 loop wait until falling_edge(clk); end loop;

            rec_clr <= '1';
            wait until falling_edge(clk);
            rec_clr <= '0';

            wait until falling_edge(clk);
            load_stb <= '1'; slave_load <= '1';
            wait until falling_edge(clk);
            load_stb <= '0'; slave_load <= '0';
            wait until falling_edge(clk);

            start_stb <= '1'; active <= '1';
            wait until falling_edge(clk);
            start_stb <= '0';
            for k in 1 to 3 loop wait until falling_edge(clk); end loop;

            en <= '1';
            guard := dv * (nbits + 4) + 60;
            while frame_done = '0' and guard > 0 loop
                wait until falling_edge(clk);
                guard := guard - 1;
            end loop;
            if guard = 0 then
                report "  FAIL: frame never completed";
                errs := errs + 1;
            end if;
            for k in 1 to dv + 2 loop wait until falling_edge(clk); end loop;
            en <= '0'; active <= '0';
            for k in 1 to 3 loop wait until falling_edge(clk); end loop;
            n_frames <= n_frames + 1;
            wait until falling_edge(clk);
        end procedure;

        -- The whole of the bit-order claim, checked against the pin.
        procedure check_wire(nbits : natural; lsb : std_logic;
                             tx : std_logic_vector(MAX_W - 1 downto 0);
                             tag : string) is
            variable bad  : natural := 0;
            variable want : std_logic;
        begin
            if wire_n /= nbits then
                report "  FAIL: " & tag & " drove " & integer'image(wire_n) &
                       " bits for a " & integer'image(nbits) & "-bit frame";
                errs := errs + 1;
            else
                for k in 0 to nbits - 1 loop
                    if lsb = '1' then
                        want := tx(k);
                    else
                        want := tx(nbits - 1 - k);
                    end if;
                    if wire_bits(k) /= want then bad := bad + 1; end if;
                end loop;
                if bad /= 0 then
                    report "  FAIL: " & tag & " sent " & integer'image(bad) &
                           " of " & integer'image(nbits) &
                           " bits in the wrong order";
                    errs := errs + 1;
                end if;
            end if;
        end procedure;

        -- The received word must be assembled from the bits that actually
        -- arrived, in the order the setting demands.
        procedure check_recv(nbits : natural; lsb : std_logic; tag : string) is
            variable want : std_logic_vector(MAX_W - 1 downto 0);
        begin
            want := (others => '0');
            for k in 0 to nbits - 1 loop
                if lsb = '1' then
                    want(k) := recv_bits(k);
                else
                    want(nbits - 1 - k) := recv_bits(k);
                end if;
            end loop;
            if recv_n /= nbits then
                report "  FAIL: " & tag & " captured " & integer'image(recv_n) &
                       " bits for a " & integer'image(nbits) & "-bit frame";
                errs := errs + 1;
            elsif rx_data /= want then
                report "  FAIL: " & tag & " assembled " & hex8(rx_data) &
                       " from the wire, expected " & hex8(want);
                errs := errs + 1;
            end if;
        end procedure;

        procedure show_wire(nbits : natural; tag : string) is
            variable s : string(1 to 64);
        begin
            for k in 0 to nbits - 1 loop
                if wire_bits(k) = '1' then s(k + 1) := '1';
                else                       s(k + 1) := '0';
                end if;
            end loop;
            report "  " & tag & ": " & s(1 to nbits);
        end procedure;

        procedure next_rand(variable v : out std_logic_vector(MAX_W - 1 downto 0)) is
        begin
            seed := resize(seed * x"0019660D", 32) + x"3C6EF35F";
            v := std_logic_vector(seed);
        end procedure;

        type int_vec is array (natural range <>) of natural;
        constant LENS : int_vec(0 to 5) := (1, 2, 5, 8, 13, 32);
        variable pol_v, pha_v, lsb_v : std_logic;
        constant ZERO : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    begin
        for k in 1 to 3 loop wait until falling_edge(clk); end loop;
        rst_n <= '1';
        wait until falling_edge(clk);

        -- 1. THE SAME BYTE, BOTH ORDERS. 0x8D is 1000_1101 -- not a
        --    palindrome, so the two wires must differ visibly.
        run_frame(4, '0', '0', 8, '0', x"0000008D", ZERO, '1');
        check_wire(8, '0', x"0000008D", "MSB-first 0x8D");
        show_wire(8, "MSB-first 0x8D on the wire");
        if rx_data /= x"0000008D" then
            report "  FAIL: MSB-first loopback returned " & hex8(rx_data);
            errs := errs + 1;
        end if;

        run_frame(4, '0', '0', 8, '1', x"0000008D", ZERO, '1');
        check_wire(8, '1', x"0000008D", "LSB-first 0x8D");
        show_wire(8, "LSB-first 0x8D on the wire");
        if rx_data /= x"0000008D" then
            report "  FAIL: LSB-first loopback returned " & hex8(rx_data);
            errs := errs + 1;
        end if;
        report "  the two wires are bit-reversals of each other, and both loop back to 8d -- the transform is its own inverse";

        -- 2. A NARROW LSB-FIRST FRAME. 5 bits of 0x19 is 11001; reversed it
        --    is 10011.
        run_frame(4, '0', '0', 5, '1', x"00000019", ZERO, '1');
        check_wire(5, '1', x"00000019", "LSB-first 5-bit");
        show_wire(5, "LSB-first 5 bits of 0x19");
        if rx_data /= x"00000019" then
            report "  FAIL: a 5-bit LSB-first frame returned " & hex8(rx_data);
            errs := errs + 1;
        end if;
        report "  a 5-bit LSB-first frame reverses only those five bits and leaves nothing above them";

        -- 3. AN INDEPENDENT SLAVE, BOTH ORDERS. The slave drives a bit
        --    SEQUENCE; the master must assemble it into a word the right way
        --    round. This is the check loopback structurally cannot make.
        run_frame(4, '0', '0', 8, '0', ZERO, x"000000B2", '0');
        check_recv(8, '0', "MSB-first from the slave");
        run_frame(4, '0', '0', 8, '1', ZERO, x"000000B2", '0');
        check_recv(8, '1', "LSB-first from the slave");
        report "  the same wire sequence assembles to " & hex8(rx_data) &
               " one way round -- the receive transform is doing work loopback would hide";

        -- 4. AN ILLEGAL WIDTH is reported, not absorbed.
        wait until falling_edge(clk);
        len <= to_unsigned(0, LEN_W);
        wait until falling_edge(clk);
        if len_err /= '1' then
            report "  FAIL: len=0 was not reported"; errs := errs + 1;
        end if;
        len <= to_unsigned(33, LEN_W);
        wait until falling_edge(clk);
        if len_err /= '1' then
            report "  FAIL: len=33 was not reported with a 32-bit datapath";
            errs := errs + 1;
        end if;
        len <= to_unsigned(32, LEN_W);
        wait until falling_edge(clk);
        if len_err /= '0' then
            report "  FAIL: len=32 was reported on a 32-bit datapath";
            errs := errs + 1;
        end if;
        len <= to_unsigned(1, LEN_W);
        wait until falling_edge(clk);
        if len_err /= '0' then
            report "  FAIL: len=1 was reported"; errs := errs + 1;
        end if;
        report "  widths 0 and 33 reported on a 32-bit datapath; 1 and 32 accepted";

        -- 5. THE SWEEP. Both orders, six widths, three divisors, all four
        --    modes, against an independent slave, checking the wire order, the
        --    assembled word, and the loopback identity.
        for o in 0 to 1 loop
            if o = 1 then lsb_v := '1'; else lsb_v := '0'; end if;
            for l in LENS'range loop
                for d in 2 to 4 loop
                    for p in 0 to 1 loop
                        for h in 0 to 1 loop
                            if p = 1 then pol_v := '1'; else pol_v := '0'; end if;
                            if h = 1 then pha_v := '1'; else pha_v := '0'; end if;
                            next_rand(tv);
                            tv := tv and mask_to(LENS(l));
                            next_rand(sv);
                            sv := sv and mask_to(LENS(l));

                            run_frame(d, pol_v, pha_v, LENS(l), lsb_v,
                                      tv, sv, '0');
                            check_wire(LENS(l), lsb_v, tv, "sweep wire");
                            check_recv(LENS(l), lsb_v, "sweep recv");

                            run_frame(d, pol_v, pha_v, LENS(l), lsb_v,
                                      tv, ZERO, '1');
                            check_wire(LENS(l), lsb_v, tv, "sweep loop wire");
                            if rx_data /= tv then
                                report "  FAIL: loopback returned " &
                                       hex8(rx_data) & ", sent " & hex8(tv);
                                errs := errs + 1;
                            end if;
                        end loop;
                    end loop;
                end loop;
            end loop;
        end loop;
        report "  " & integer'image(n_frames) &
               " frames swept: both bit orders, widths 1..32, three divisors, all four modes";

        errors <= errs;
        if errs = 0 then
            report "PASS: bit order is a single reversal of the low `len` bits applied at both boundaries -- the same function, its own inverse -- and the shift register of Chapter 13.6 is instantiated unchanged with no mux anywhere in the per-bit path -- the recorded MOSI sequence matches the order the setting demands for every one of " & integer'image(n_frames) & " frames spanning both orders, widths from 1 to 32 bits, three divisors and all four modes, the word assembled from the bits a slave actually drove is correct in both orders where a loopback test could not tell the difference, a narrow frame reverses only the bits inside it, and widths of zero or above the datapath width are reported rather than silently wrapping or truncating";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

Parity

All three implementations run 293 frames across both bit orders, widths of 1, 2, 5, 8, 13 and 32 bits, three divisors and all four modes — checking the recorded MOSI sequence against the order the setting demands, the word assembled from the bits a slave actually drove, and the loopback identity, on every frame.

8. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order.sva — the wire's order, and the involution
// The interesting assertions here are about the WIRE, because that is the only
// place the bit order is observable. An assertion about `rx_data` alone cannot
// distinguish correct LSB-first from a design that ignores the setting.

module spi_width_order_sva #(parameter int MAX_W = 32, LEN_W = 6) (
    input logic              clk,
    input logic              rst_n,
    input logic [MAX_W-1:0]  tx_data,
    input logic [LEN_W-1:0]  len,
    input logic              lsb_first,
    input logic              load_stb,
    input logic              preload_stb,
    input logic              launch_stb,
    input logic              mosi,
    input logic [MAX_W-1:0]  rx_data,
    input logic              rx_valid_stb,
    input logic              len_err
);

    default clocking cb @(posedge clk); endclocking
    default disable iff (!rst_n);

    // Snapshot the frame's inputs at load, because `tx_data` and `len` may
    // legitimately change during the frame (Chapter 13.2) and the assertion is
    // about the frame that was loaded.
    logic [MAX_W-1:0] tx_q;
    logic [LEN_W-1:0] len_q;
    logic             lsb_q;
    int               idx;
    always_ff @(posedge clk) begin
        if (load_stb) begin
            tx_q  <= tx_data;
            len_q <= len;
            lsb_q <= lsb_first;
            idx   <= 0;
        end else if ($past(preload_stb || launch_stb)) begin
            idx <= idx + 1;
        end
    end

    // THE property. The bit on the pin, one cycle after each drive strobe, is
    // the bit the order demands -- and this is the only assertion in the module
    // that a loopback test cannot make.
    a_wire_order: assert property (
        $past(preload_stb || launch_stb) && idx < len_q |->
            mosi == (lsb_q ? tx_q[idx] : tx_q[len_q - 1 - idx])
    );

    // The transform is its own inverse, so applying it to a received word twice
    // must give the word back. Stated as a cover rather than an assert: it is a
    // property of the FUNCTION, and covering it proves the suite exercised a
    // case where it mattered.
    c_involution_exercised: cover property (
        rx_valid_stb && lsb_first && len < MAX_W &&
        rx_data != {MAX_W{1'b0}}
    );

    // A narrow frame must leave zeros above it in BOTH orders. In MSB-first that
    // follows from the datapath; in LSB-first it follows from the right shift,
    // and they are different mechanisms with the same obligation.
    a_zero_above: assert property (
        rx_valid_stb && len < MAX_W |-> (rx_data >> len) == '0
    );

    // An illegal width is reported, in both directions.
    a_err_iff: assert property (
        len_err == ((len == 0) || (len > MAX_W))
    );

    // And while an illegal width is presented, the outputs must stay defined --
    // no X from an out-of-range shift.
    a_no_x_on_err: assert property (len_err |-> !$isunknown(rx_data));

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_width_order_cg.sv — order crossed with width, and the palindrome trap
// Bit order has two values, so the coverage question is not "both orders" -- any
// suite gets that -- but "both orders at each width, against data that can tell
// them apart". A palindromic pattern makes the two orders indistinguishable, and
// a suite full of 0x00, 0xFF and 0xAA has tested nothing.

covergroup cg_width_order (int MAX_W) @(posedge clk);

    order: coverpoint lsb_first iff (load_stb) {
        bins msb = {0};
        bins lsb = {1};
    }

    width: coverpoint len iff (load_stb) {
        bins one    = {1};
        bins two    = {2};
        bins narrow = {[3:7]};
        bins byte_  = {8};
        bins odd    = {[9:31]};
        bins full   = {32};
        bins illegal_zero = {0};
        bins illegal_wide = {[33:$]};
    }

    // THE bin that matters. A pattern whose low `len` bits are a palindrome
    // produces identical wires in both orders, so a frame carrying one has not
    // distinguished the settings however carefully it was checked.
    discriminating: coverpoint pattern_is_palindrome iff (load_stb && len > 1) {
        bins distinguishes = {0};
        bins palindrome    = {1};
    }

    // Whether the MOSI sequence was actually recorded and compared, as opposed
    // to only the loopback value being checked. A suite can reach full width and
    // order coverage while never once looking at the wire.
    checked: coverpoint wire_was_compared iff (frame_done) {
        bins wire_checked = {1};
    }

    x_order_width: cross order, width;
    x_order_disc:  cross order, discriminating {
        // Both orders must be exercised with data that can tell them apart.
        ignore_bins dont_care = binsof(discriminating.palindrome);
    }

endgroup

9. Why an FPGA or ASIC Engineer Cares

The per-bit path is untouched. That is the whole implementation argument. The shift register is still tx_sr <= {tx_sr[MAX_W-2:0], 1'b0} — wires only, no mux, no comparator. The bidirectional alternative adds a MAX_W-bit 2:1 mux to that path, which at 32 bits is 32 LUTs on an FPGA sitting directly on the signal that runs at the bit rate.

The reversal is free and the shift is the one that already existed. rev_all is a permutation of wires: zero logic, zero delay beyond routing. The variable right shift is a barrel shifter, and Chapter 13.6's alignment already needed one of the same width — so on the transmit side the two can share, and the marginal cost of LSB-first on that side is one mux on the barrel shifter's input.

The receive-side shifter is new, and it is on the read path. It is evaluated when software reads rx_data, which is once per frame at most and usually less. If it ever mattered, it could be registered — the received word is stable from rx_valid_stb until the next load, so a pipeline stage there costs nothing but a cycle of read latency.

The clamp is worth its two comparators. len arrives from a software-writable register, so MAX_W - len can be negative in the middle of a bad configuration. An out-of-range shift amount in synthesis is not an error, it is an undefined result — and an undefined result propagating into a data path is how a configuration mistake becomes an intermittent data corruption.

Cost. Two MAX_W-wide barrel shifters where one existed, two 2:1 muxes on MAX_W bits, and two comparators. Zero additional flops — the entire feature is combinational, which is what putting it at the boundaries bought.

10. Failure Signature — LSB-First Works In Loopback And Not On The Bus

Symptom. A master's LSB-first mode passes every test in the regression, including a full sweep of widths and patterns in loopback. Against the actual device — an LSB-first ADC — every reading is the bit-reverse of the correct value.

What that immediately establishes. The design is applying the reversal an even number of times. Either twice when it should be once, or zero times when it should be once. Loopback cannot distinguish those and cannot distinguish either from correct behaviour, so the entire loopback regression carries no information about this feature.

The two candidates, and how to separate them. Record the MOSI sequence for a known asymmetric pattern:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   sent 0x8D, wire carries 1000 1101   -> the reversal is not happening at all
   sent 0x8D, wire carries 1011 0001   -> transmit is right; receive reverses too

The first says lsb_first is not reaching the transmit transform — a wiring or decode error. The second says both transforms are applied and one of them should not be, which given the self-inverse structure means someone applied it to rx_raw when the datapath had already been fed a reversed word, or applied it twice on one side.

Why the whole regression was uninformative. Not because it was badly written, but because the property is structurally invisible to the topology it used. This is worth separating from ordinary test-quality problems: no amount of additional loopback cases, patterns, widths or modes would have helped, because the observation that distinguishes the settings does not exist on a loopback path.

The fix to the suite, which matters more than the fix to the design. Add a wire-order check and an independent-slave check, and make them part of the same test rather than separate ones — a design can pass a wire check with a broken receive transform and pass an independent-slave check with a broken transmit transform, and only both together pin down each side.

The general lesson. Before trusting a suite, ask what the topology makes unobservable. A loopback path hides every transformation that is applied symmetrically, which includes bit order, and also includes any pair of encode/decode steps — a scrambler, a CRC inversion, a byte swap. The answer is never more cases; it is a different observation point.

11. Common Misconceptions

"LSB-first needs a bidirectional shift register." It needs a reversal somewhere, and the boundaries are a better place than the shift path: the shift path runs at the bit rate and the boundaries run once per frame. The shift register in this design is not modified at all.

"Two transforms means two chances to get it wrong." It is one transform applied twice, because reversing twice is the identity. A pair of mutually-inverse functions would be two chances; a self-inverse function is one.

"Reversing a variable-width field needs a loop." It needs a fixed full-width reversal — pure wires — followed by one variable right shift. The loop version additionally needs a comparator per bit to decide whether each position participates.

"If the loopback test passes for both bit orders, the feature works." Loopback applies the transform twice and cannot distinguish "twice" from "never". A design that ignores the order bit passes it, and so does one that reverses when it should not.

"An illegal frame width will just produce a short or long transfer." A width of zero makes len - 1 wrap, so the frame never ends and the machine sits in its shift state with a slave selected. A width above the datapath truncates silently. Both are configuration errors and both are reported, and the shift amounts are clamped so the outputs stay defined while the bad value is present.

12. Reason It Through

Why is the transform applied to the transmit side rather than reversing the shift direction?

Because the transmit side is evaluated once per frame, on the load cycle, and the shift direction is evaluated once per bit. Moving a per-frame decision onto a per-bit path is the wrong trade at any frequency, and it is worse as the datapath widens because the mux scales with the width while the frame rate does not.

Show that rev_all(x) >> (MAX_W - len) gives the low len bits reversed with zeros above.

rev_all maps bit i to bit MAX_W-1-i, so the low len bits — positions 0 through len-1 — map to positions MAX_W-1 down to MAX_W-len, in reversed order. Everything originally above len-1 maps below MAX_W-len. Shifting right by MAX_W-len moves positions MAX_W-1 .. MAX_W-len down to len-1 .. 0, preserving their reversed order, and shifts the rest out of the word entirely. Zeros fill from the top.

A design's LSB-first mode is tested only with the patterns 0x00, 0xFF, 0xAA and 0x55. What has it verified?

Nothing about bit order. All four are palindromic under an 8-bit reversal — 0xAA reverses to 0x55 and vice versa, and a suite that sends 0xAA and checks for 0xAA in loopback cannot tell, while a suite that sends 0xAA and expects 0x55 on the wire is checking the right thing with the one pattern where a sign error is invisible. Asymmetric values are required, and hand-written "interesting" lists are overwhelmingly symmetric.

Why is the shift amount clamped as well as the width reported?

Because len comes from a register software can write at any time, so MAX_W - len can be out of range while a bad value is presented. An out-of-range shift is not an error in synthesis, it is an undefined result — and an undefined value propagating from a configuration mistake into a data path produces an intermittent corruption that will be attributed to anything but the register write.

What class of bug is structurally invisible to a loopback topology, beyond bit order?

Anything applied symmetrically on both sides, because the two applications cancel. Bit order is the example here; others are a byte swap applied on transmit and receive, a scrambler and descrambler, a CRC inversion, and a data inversion for DC balance. In every case the loopback path returns the original data whether the pair is present or absent, so the suite cannot distinguish a working feature from an absent one. The remedy is never more loopback cases — it is an observation point between the two applications.

13. Understanding Check

14. Summary

A bidirectional shift register implements LSB-first and is the wrong trade: it puts a mux on the only path clocked at the bit rate, to serve a decision made once per frame, and it doubles the verification surface.

The alternative is one transform at the boundaries — reverse the low len bits — applied to the transmit word before the datapath and the received word after it. The shift register from Chapter 13.6 is instantiated unchanged, which is the point of the chapter.

The transform is its own inverse, so it is the same function on both sides. That is worth more than the gate saving: a pair of mutually-inverse functions is a maintenance hazard whose failures cancel on a loopback path, and a self-inverse function cannot drift out of step with itself.

It is built as a fixed full-width reversal — pure wiring — followed by one variable right shift, which is strictly less logic than a loop over len and shares a barrel shifter with the alignment the datapath already needed.

Loopback cannot test bit order at all. It applies the transform twice and cannot distinguish "twice" from "never", so a design ignoring the order bit passes every loopback test. The suite must watch the wire and must use asymmetric patterns — hand-written "interesting" values are overwhelmingly palindromic. And the receive side needs a slave that drives a bit sequence and knows nothing about word order.

An illegal width is reported and the shift amounts clamped, because len comes from a register software can write at any time and an out-of-range shift gives an undefined result rather than an error.

The feature adds zero flops — one more barrel shifter, two muxes, two comparators, all combinational, all off the per-bit path. And the generalisable lesson is to ask what a test topology makes unobservable: a loopback path hides every symmetrically-applied transformation, and the remedy is a different observation point rather than more cases.

15. What Comes Next

The master handles any width, either bit order, all four modes, any number of frames under one select. Every one of those frames waits for software to produce the next word.

Chapter 13.9 — Multi-Byte and Back-to-Back Transactions closes that gap with one register's worth of flops, and it is the chapter where the throughput arithmetic of Module 9 finally becomes a design decision. It also introduces a distinction worth having: what a deeper buffer actually buys, which is not what most people expect.

Continue learning