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
lenbits 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.
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, twice3. 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:
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-alignedOne 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
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:
reverse both sides correct LSB-first
reverse neither side MSB-first, with the order bit ignoredBoth 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:
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
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.
// 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// 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// 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// 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-- 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;-- 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
// 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// 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);
}
endgroup9. 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:
sent 0x8D, wire carries 1000 1101 -> the reversal is not happening at all
sent 0x8D, wire carries 1011 0001 -> transmit is right; receive reverses tooThe 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
Related tutorials
- Related topic
MOSI Capture and Bit Counting
Why the bit counter takes its zero from chip select and nothing else, why a one-bit slip produces well-formed words at the wrong offsets, the complete taxonomy of which configuration mismatches a slave can detect, and a receive path verified in three HDLs across 210 words.
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
- Related topic
Deriving Mode Behaviour from CPOL and CPHA
The four SPI modes are a two-bit truth table you can rebuild in seconds. The standard numbering, the derivation, the complete mode decoder in three HDLs, and the assertions that keep a configurable design honest.
- Related topic
Bit Ordering — MSB-First and LSB-First
Which end of the shift register goes out first, the two multiplexers that make the order configurable in three HDLs, and why a bit-order bug is perfectly deterministic and yet invisible on certain data.
