RTL Design Patterns · Chapter 13 · Case Studies
SPI Master (FSM + Datapath + Handshake)
This case study is the mirror image of the UART: SPI is source-synchronous, so the master owns the clock and sends it alongside the data, and there is nothing to recover, only edges to place. That makes SPI small, fast, and full-duplex, exchanging a byte out on MOSI and a byte in on MISO in the time it takes to clock one byte. Nothing here is new. The SCLK generator is a clock divider, the shift datapath is a shift register serializing and deserializing in lockstep, the control FSM frames the transfer with chip-select, and a valid/ready handshake wraps it. The one genuinely new idea is CPOL and CPHA, the four SPI modes, which reduce to one rule: shift MOSI on one edge and sample MISO on the other, never sampling while the line is transitioning. Built and loopback-verified in SystemVerilog, Verilog, and VHDL.
Advanced20 min readRTL Design PatternsSPISCLK GeneratorShift RegisterFSMCPOL/CPHAFull-Duplexvalid/ready Handshake
Chapter 13 · Section 13.2 · Case Studies
1. The Engineering Problem
You have a master and a peripheral — a flash chip, an ADC, a sensor — and between them run four wires: SCLK (the clock), MOSI (master-out), MISO (master-in), and CS_N (chip-select, active-low). Unlike the UART, there is a shared clock: the master generates SCLK and sends it to the slave, so the slave never has to recover timing — it just uses the edges the master hands it. That is what source-synchronous means, and it is why SPI is simpler and faster than a UART: no oversampling, no baud tolerance, no framing recovery. In exchange, SPI is full-duplex — on every clock beat the master shifts one bit out on MOSI and captures one bit in on MISO simultaneously, so a byte is exchanged, not merely sent.
The transfer is framed by CS_N. The line rests with CS_N high (deasserted) and SCLK at its idle level. To move a byte the master asserts CS_N (drives it low), clocks DATA_BITS bit-beats — one SCLK period per bit, MSB-first — during which MOSI carries the outgoing byte and MISO carries the slave's reply bit-by-bit, and then deasserts CS_N (drives it high) to end the frame. CS_N is the envelope: the slave uses its falling edge to load its own shift register and its high level to reset. Drop CS_N at the wrong instant — a beat early, or not held across the whole frame — and the slave aborts or mis-frames the byte.
The subtlety that makes SPI a capstone and not a fourth shift-register exercise lives in the two mode bits, CPOL and CPHA. CPOL (clock polarity) sets the level SCLK idles at: 0 (idle low) or 1 (idle high). CPHA (clock phase) sets which of a beat's two SCLK edges the data is sampled on: the leading (first) edge or the trailing (second) edge. Together they make four modes, 0 through 3. The rule underneath all of them is one sentence: the master shifts MOSI (changes the output bit) on one edge of each beat and SAMPLES MISO (latches the input bit) on the OTHER edge. Get that backward — sample MISO on the same edge you shift MOSI — and you latch the slave's line at the exact instant it is transitioning, reading a value that is still settling. This is the signature SPI bug, and it hides in a lenient loopback exactly the way the UART's off-center sample hid in a shared-clock loopback.
// ---- command / result handshake (9.1) ----
input wire tx_valid; // caller offers a byte to exchange
input wire [7:0] tx_data; // the byte to shift OUT on MOSI (MSB-first)
output wire tx_ready; // master can accept a byte this cycle (idle)
output wire [7:0] rx_data; // the byte shifted IN from MISO
output wire rx_valid; // one-cycle strobe: rx_data is a fresh, complete byte
// ---- SPI bus (to the slave) ----
output wire sclk; // master-generated clock; idles at CPOL
output wire cs_n; // chip-select, ACTIVE-LOW: low = frame in progress
output wire mosi; // master out: shift_reg[MSB], MSB-first
input wire miso; // master in: sampled into shift_reg[LSB]
// One transfer, CPOL=0/CPHA=0 (Mode 0), CS_N framing a byte:
// CS_N: 1 ...___________________________________... 1 (low for the whole frame)
// SCLK: idle 0, then 8 pulses (one beat per bit), back to idle 0
// MOSI: shifts OUT on the TRAILING edge; MISO: SAMPLED on the LEADING edge2. Mental Model
3. Pattern Anatomy
The SPI master is four blocks bound by SCLK edges and CS framing. Take the frame first (what appears on the four wires across the four modes), then the block diagram (who drives whom), then the two pieces of reasoning that decide correctness: the shift-vs-sample edge rule and the CS setup/hold around the frame.
The frame and the four modes. CS_N idles high and SCLK idles at CPOL. A transfer asserts CS_N low, then clocks DATA_BITS bit-beats — each beat is one full SCLK period, two edges — MSB-first, then deasserts CS_N. Within a beat there are two edges: a leading edge (the first transition away from idle) and a trailing edge (the return to idle). CPHA decides which edge samples the incoming bit and which edge shifts the outgoing bit, and they are always opposite:
- CPHA = 0 — sample on the leading edge, shift on the trailing edge. The first data bit must be presented before the first
SCLKedge, i.e. asCS_Nfalls, because the leading edge already samples it. - CPHA = 1 — shift on the leading edge, sample on the trailing edge. The first data bit is presented on the leading edge, and sampling waits for the trailing edge.
CPOL merely flips the physical direction of "leading" and "trailing": with CPOL = 0 the leading edge is a rising edge and the trailing a falling edge; with CPOL = 1 they swap. So the four modes are two orthogonal choices — idle level and which edge samples — and the master's whole correctness is generating the right two edges per beat and reading MISO on the sample edge.
The four SPI modes — CPOL sets SCLK idle level, CPHA sets which edge samples vs shifts
data flowThe block diagram — one SCLK generator, a full-duplex shift datapath, a CS/bit-beat FSM, a handshake. The SCLK generator is a clock divider (3.5): a modulo-DIV counter that toggles an internal sclk every DIV system clocks and holds it at CPOL when idle, producing exactly two edges per bit-beat. The shift datapath is one shift register (2.6): MOSI = shift_reg[MSB], and on the sample edge MISO is captured into shift_reg[LSB] as the register rotates. The FSM (4.6) is the control: IDLE (waiting on the handshake), LOAD (CS_N falls, byte latched), XFER (run DATA_BITS beats, one shift-and-sample per beat), DONE (CS_N rises, rx_valid strobes). The handshake (9.1) accepts a byte on tx_valid && tx_ready in IDLE and publishes rx_data/rx_valid in DONE.
Block diagram — SCLK generator + full-duplex shift datapath + CS/bit-beat FSM + valid/ready handshake
data flowThe shift-vs-sample edge rule. This is the SPI equivalent of the UART's mid-bit sample point — the one thing the whole design turns on. Each bit-beat has two SCLK edges. On one of them the master shifts: it rotates the register so a new bit appears on MOSI. On the other it samples: it latches MISO into the register. These must be the opposite edges of the beat, because the slave (and the master) each drive their output on the shift edge and expect it sampled on the far edge — half a bit-time later — when the line has settled. If the master samples MISO on the same edge it shifts MOSI, it reads MISO at the instant the slave is also changing its output, catching a value that is still transitioning. CPHA is exactly the knob that assigns "sample" to the leading edge (CPHA 0) or the trailing edge (CPHA 1); the shift edge is always the other one.
The CS setup/hold — framing the frame. CS_N is the envelope and it has strict timing. It must fall (assert) before the first SCLK edge — in CPHA 0 with enough setup that the first data bit is already valid on MOSI when the leading edge samples — and it must stay low for the entire frame, including after the last sample edge, before it rises. Deassert CS_N on the last shift edge (one beat too early) and the final bit's sample edge never happens under an asserted CS_N, so the last bit is lost or the slave aborts the byte. The FSM's job is to make CS_N a clean envelope around all DATA_BITS beats: assert in LOAD, hold across all of XFER, deassert only in DONE after the last sample.
The bit-beat count. A byte is DATA_BITS beats; the FSM counts them with a bit_cnt that decrements (or increments) once per beat and leaves XFER when the last bit has been sampled, not merely shifted. Off-by-one here — leaving on the shift edge of the last beat — is the second DebugLab row and the same crime as dropping CS_N early: the last incoming bit is never captured.
4. Real RTL Implementation
This is the core of the page. We build the SPI master incrementally — (a) the SCLK generator, (b) the full-duplex shift datapath, (c) the CS/bit-beat FSM plus the valid/ready handshake, then (d) the integrated, parameterized spi_master — and show the integrated block plus its loopback self-checking testbench and the CPHA wrong-edge bug-vs-fix in SystemVerilog, Verilog, and VHDL. The idea is identical across the three; only the spelling differs.
Three conventions run through every block, each an earlier lesson made concrete:
- One system clock; SCLK is a divided enable, not a second clock. The SCLK generator is a 3.5 divider that toggles a register every
DIVsystem clocks; every flop in the design is clocked by the fast systemclk, and edges ofsclkare detected assclktransitions in that domain — no second clock domain, no gated clock into flops. - Synchronous, active-high reset to a safe idle. After reset
cs_nis high (deasserted),sclksits atCPOL, the FSM is inIDLE, and the master presentstx_readyhigh. The incomingMISOis registered before use so the sample is clean. - Shift on one edge, SAMPLE on the OTHER, per CPOL/CPHA. The receiver's whole correctness is the pair of
sclk-edge predicates: the master detects the leading and trailingsclkedges and, from CPHA, routes one to the shift and the other to the MISO sample. Get them the same and you sample a transitioning line (§7).
4a. Building block (i) — the SCLK generator (a divider, from 3.5)
The generator is a single counter that toggles an internal sclk every DIV system clocks while the FSM has it enabled, and forces sclk to the CPOL idle level while disabled. Each toggle is one sclk edge; two toggles are one bit-beat. It also emits lead_edge and trail_edge one-cycle strobes — the two events per beat that the datapath keys off.
module spi_sclk_gen #(
parameter int DIV = 4, // system clocks per SCLK half-period
parameter int CPOL = 0 // idle level of SCLK
)(
input logic clk,
input logic rst, // synchronous, active-high
input logic run, // FSM enables the clock during XFER
output logic sclk, // the divided clock (idles at CPOL)
output logic lead_edge, // 1-cycle strobe: first edge of a beat
output logic trail_edge // 1-cycle strobe: second edge of a beat
);
// 3.5: a modulo-DIV counter. Each time it wraps, TOGGLE sclk -> one edge. Two
// toggles = one bit-beat. lead_edge marks the transition AWAY from idle (CPOL),
// trail_edge the return TO idle. While !run, hold sclk at CPOL (clean idle).
logic [$clog2(DIV)-1:0] cnt;
always_ff @(posedge clk) begin
if (rst) begin
cnt <= '0; sclk <= CPOL[0]; lead_edge <= 1'b0; trail_edge <= 1'b0;
end else begin
lead_edge <= 1'b0; trail_edge <= 1'b0; // default: strobes 1 cycle
if (!run) begin
cnt <= '0; sclk <= CPOL[0]; // parked at the idle level
end else if (cnt == DIV-1) begin // WRAP at DIV-1 -> exact half-period
cnt <= '0;
sclk <= ~sclk; // one SCLK edge
// moving AWAY from idle (CPOL) is the LEADING edge; toward it, TRAILING
if (sclk == CPOL[0]) lead_edge <= 1'b1;
else trail_edge <= 1'b1;
end else begin
cnt <= cnt + 1'b1;
end
end
end
endmoduleThe rest of §4 uses sclk, lead_edge, and trail_edge as given. CPHA then decides which of the two edge strobes samples MISO and which shifts MOSI — that routing is the only place CPHA appears.
4b. Building block (ii) — the full-duplex shift datapath (from 2.6)
One shift register serves both directions. MOSI is always the register's MSB. On the shift edge the register shifts left by one (MSB-first), bringing the next transmit bit to the top; on the sample edge the MISO pin is captured into the register's LSB, so after DATA_BITS beats the register holds the fully-received byte. Because the outgoing byte has shifted out by then, the same register can present the received byte to rx_data. This is the 2.6 shift register used as a PISO (parallel-in, serial-out on MOSI) and a SIPO (serial-in, parallel-out from MISO) at once — the essence of full-duplex.
module spi_shift #(parameter int DATA_BITS = 8) (
input logic clk, rst,
input logic load, // latch tx_data at frame start
input logic [DATA_BITS-1:0] tx_data,
input logic shift_now, // the SHIFT edge (per CPOL/CPHA)
input logic sample_now, // the SAMPLE edge (the OTHER edge)
input logic miso, // registered slave line
output logic mosi, // = shift_reg[MSB], MSB-first
output logic [DATA_BITS-1:0] rx_data // received byte after DATA_BITS beats
);
logic [DATA_BITS-1:0] sh;
assign mosi = sh[DATA_BITS-1]; // MSB drives MOSI
assign rx_data = sh; // after the last sample, sh IS the RX byte
always_ff @(posedge clk) begin
if (rst) sh <= '0;
else if (load) sh <= tx_data; // load the transmit byte
else begin
// SAMPLE and SHIFT are OPPOSITE edges; both can happen within one beat but
// never on the same edge. Sample lands MISO in the LSB; shift moves MSB->MOSI.
if (sample_now) sh[0] <= miso; // capture MISO -> LSB
if (shift_now) sh <= {sh[DATA_BITS-2:0], miso}; // shift left
end
end
endmoduleTwo lines carry the datapath. mosi = sh[MSB] makes the register self-serializing MSB-first; the sample_now/shift_now split is the CPOL/CPHA rule made structural — they are different sclk edges, so MISO is always latched on the edge the line is settled, never the edge it moves. (The integrated module below folds these into one clocked block so the sample and shift land in the correct order within a beat.)
4c. Building block (iii) — the CS/bit-beat FSM + valid/ready handshake (from 4.6 and 9.1)
The control is a small FSM on the system clock. IDLE: cs_n = 1, sclk = CPOL, tx_ready = 1; on tx_valid && tx_ready latch tx_data, go to LOAD. LOAD: assert cs_n = 0 and, for CPHA 0, present the first MOSI bit now (before any edge); enable the SCLK generator, go to XFER. XFER: run DATA_BITS beats; each beat samples MISO on the sample edge and shifts on the shift edge; leave when the last bit has been sampled (bit_cnt == 0 after the last sample). DONE: stop the clock, hold cs_n = 0 for one more cycle of hold, then raise cs_n = 1, strobe rx_valid, publish rx_data, return to IDLE. The handshake is the 9.1 producer/consumer wrapper: tx_ready is high only in IDLE (the byte is accepted exactly on tx_valid && tx_ready), and rx_valid is a one-cycle strobe in DONE. The full RTL is in the integrated module below.
4d. The integrated SPI master — parameterized, in SystemVerilog
One module: spi_sclk_gen + the shift datapath + the CS/bit-beat FSM + the valid/ready handshake, parameterized on CPOL, CPHA, DIV, and DATA_BITS. The CPHA routing is one conditional assignment: which sclk edge is sample_now and which is shift_now.
module spi_master #(
parameter int CPOL = 0,
parameter int CPHA = 0,
parameter int DIV = 4, // system clocks per SCLK half-period
parameter int DATA_BITS = 8
)(
input logic clk,
input logic rst, // synchronous, active-high
// command / result handshake (9.1)
input logic tx_valid,
input logic [DATA_BITS-1:0] tx_data,
output logic tx_ready,
output logic [DATA_BITS-1:0] rx_data,
output logic rx_valid,
// SPI bus
output logic sclk,
output logic cs_n,
output logic mosi,
input logic miso
);
// ---- SCLK generator (3.5): two edge strobes per beat, idle at CPOL ----
logic run, lead_edge, trail_edge;
spi_sclk_gen #(.DIV(DIV), .CPOL(CPOL)) gen (
.clk(clk), .rst(rst), .run(run),
.sclk(sclk), .lead_edge(lead_edge), .trail_edge(trail_edge));
// CPHA routes the two edges: CPHA=0 SAMPLES on the leading edge, SHIFTS on the
// trailing; CPHA=1 SHIFTS on the leading edge, SAMPLES on the trailing. Always OPPOSITE.
logic sample_now, shift_now;
assign sample_now = (CPHA == 0) ? lead_edge : trail_edge;
assign shift_now = (CPHA == 0) ? trail_edge : lead_edge;
// ---- register the asynchronous MISO before sampling it ----
logic miso_s1, miso_s;
always_ff @(posedge clk) begin
if (rst) begin miso_s1 <= 1'b0; miso_s <= 1'b0; end
else begin miso_s1 <= miso; miso_s <= miso_s1; end
end
// ---- control FSM (4.6) + shift datapath (2.6) + handshake (9.1) ----
typedef enum logic [1:0] { S_IDLE, S_LOAD, S_XFER, S_DONE } state_t;
state_t state;
logic [DATA_BITS-1:0] sh; // full-duplex shift register
logic [$clog2(DATA_BITS+1)-1:0] bit_cnt; // bits still to SAMPLE
assign mosi = sh[DATA_BITS-1]; // MSB-first out
assign rx_data = sh; // after last sample, sh is the RX byte
assign tx_ready = (state == S_IDLE) && !rst; // accept only when idle
always_ff @(posedge clk) begin
if (rst) begin
state <= S_IDLE; cs_n <= 1'b1; run <= 1'b0;
sh <= '0; bit_cnt <= '0; rx_valid <= 1'b0;
end else begin
rx_valid <= 1'b0; // default: strobe one cycle
case (state)
S_IDLE: begin
cs_n <= 1'b1; run <= 1'b0;
if (tx_valid) begin // tx_valid && tx_ready -> accept
sh <= tx_data; // load the byte (MOSI = MSB now)
bit_cnt <= DATA_BITS[$bits(bit_cnt)-1:0];
state <= S_LOAD;
end
end
S_LOAD: begin
cs_n <= 1'b0; // assert CS_N BEFORE the first edge
run <= 1'b1; // start the clock; CPHA=0 first bit is ready
state <= S_XFER;
end
S_XFER: begin
cs_n <= 1'b0; // HOLD CS_N low across the whole frame
// SAMPLE first (capture the settled MISO into the LSB position that the
// shift is about to fill), then SHIFT on the other edge. Opposite edges.
if (sample_now) begin
sh[0] <= miso_s; // latch MISO on the SAMPLE edge
if (bit_cnt == 1) begin
// last bit has just been SAMPLED -> frame is complete
bit_cnt <= '0;
run <= 1'b0; // stop SCLK after the last SAMPLE, not shift
state <= S_DONE;
end else begin
bit_cnt <= bit_cnt - 1'b1;
end
end
if (shift_now && bit_cnt != 1) // do NOT shift out after the last sample
sh <= {sh[DATA_BITS-2:0], miso_s};
end
S_DONE: begin
cs_n <= 1'b1; // deassert only AFTER the last sample
rx_valid <= 1'b1; // publish the received byte
state <= S_IDLE;
end
endcase
end
end
endmoduleTwo lines carry the master's correctness. sample_now/shift_now route the two sclk edges by CPHA so MISO is latched on the edge opposite the one that moves MOSI — never a transitioning line. And the bit_cnt == 1 guard leaves XFER on the last sample edge (stopping the clock and holding cs_n low into DONE), not on the last shift — the framing fix the second §7 row turns on. The loopback testbench wires mosi straight into miso and proves a random byte survives the round trip: with MOSI fed back to MISO, the byte the master receives is the byte it sent, delayed by the pipeline, so rx_data equals tx_data shifted through the register.
module spi_master_loopback_tb;
localparam int CPOL = 0, CPHA = 0, DIV = 4, DATA_BITS = 8;
logic clk = 1'b0, rst, tx_valid, tx_ready, rx_valid;
logic [DATA_BITS-1:0] tx_data, rx_data;
logic sclk, cs_n, mosi, miso;
int errors = 0;
// LOOPBACK slave model: MISO echoes MOSI (a wire-tie slave). The master shifts a
// byte out and, because MISO == MOSI, shifts the SAME bits back in one beat later,
// so the received byte equals the transmitted byte for MSB-first Mode 0.
assign miso = mosi;
spi_master #(.CPOL(CPOL), .CPHA(CPHA), .DIV(DIV), .DATA_BITS(DATA_BITS)) dut (
.clk(clk), .rst(rst), .tx_valid(tx_valid), .tx_data(tx_data), .tx_ready(tx_ready),
.rx_data(rx_data), .rx_valid(rx_valid),
.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso));
always #5 clk = ~clk;
logic [DATA_BITS-1:0] expected;
always @(posedge clk) if (rx_valid) begin
// CS_N must be HIGH (deasserted) exactly when the frame reports done.
assert (cs_n === 1'b1) else begin $error("CS_N low at rx_valid"); errors++; end
if (rx_data !== expected) begin
$error("rx=0x%02h exp=0x%02h", rx_data, expected); errors++;
end
end
task automatic xfer(input logic [DATA_BITS-1:0] b);
@(posedge clk); wait (tx_ready); // wait until the master is idle
tx_data = b; expected = b; tx_valid = 1'b1; // in wire-loopback, rx == tx
@(posedge clk); tx_valid = 1'b0;
wait (rx_valid); // frame complete
@(posedge clk);
endtask
initial begin
rst = 1'b1; tx_valid = 1'b0; tx_data = '0;
repeat (4) @(posedge clk); rst = 1'b0;
xfer(8'h00); xfer(8'hFF); xfer(8'hB5); xfer(8'h01); xfer(8'h80); xfer(8'h6A);
for (int i = 0; i < 16; i++) xfer($urandom);
if (errors == 0) $display("PASS: all bytes survived MOSI->MISO loopback (rx==tx, CS framed)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmodule4e. The signature bug — sampling MISO on the SHIFT edge, not the SAMPLE edge (buggy vs fixed)
Every SPI failure that survives a lenient bench test lives in the sample edge. The sharpest is sampling MISO on the same edge that shifts MOSI (getting the CPHA routing backward): the master latches MISO at the instant the slave is changing its output, so it reads a bit that is still settling. It 'works' only against a loopback where MISO is a static tie to MOSI (which never transitions relative to the sampling edge) — and reads intermittently wrong bits against a real slave whose MISO transitions on its own shift edge.
// BUGGY CPHA routing: sample_now and shift_now are the SAME edge, so MISO is latched
// at the exact instant MOSI (and the slave's output) is changing. Against a real
// slave the sampled bit is the value while the line is transitioning -> wrong bits.
module cpha_route_bad #(parameter int CPHA = 0) (
input logic lead_edge, trail_edge,
output logic sample_now, shift_now
);
// BUG: both derived from the SAME edge -> sample on the shift edge.
assign shift_now = (CPHA == 0) ? trail_edge : lead_edge;
assign sample_now = shift_now; // WRONG: same edge as the shift
endmodule
// FIXED CPHA routing: sample and shift are OPPOSITE edges of the beat. MISO is latched
// half a beat after the slave drove it, when the line is settled.
module cpha_route_good #(parameter int CPHA = 0) (
input logic lead_edge, trail_edge,
output logic sample_now, shift_now
);
// FIX: sample on the leading edge for CPHA 0, trailing for CPHA 1; shift is the OTHER.
assign sample_now = (CPHA == 0) ? lead_edge : trail_edge;
assign shift_now = (CPHA == 0) ? trail_edge : lead_edge;
endmodule module spi_sample_edge_tb;
// A real slave drives MISO one beat delayed and CHANGES it on the shift edge, so a
// master that samples on that same edge catches MISO mid-transition. The correct
// (opposite-edge) master samples half a beat later and reads it clean.
localparam int CPOL = 0, CPHA = 0, DIV = 4, DATA_BITS = 8;
logic clk = 1'b0, rst, tx_valid, tx_ready, rx_valid;
logic [DATA_BITS-1:0] tx_data, rx_data;
logic sclk, cs_n, mosi, miso;
logic [DATA_BITS-1:0] slave_sr = 8'h6A; // the byte the slave will return
int errors = 0;
spi_master #(.CPOL(CPOL), .CPHA(CPHA), .DIV(DIV), .DATA_BITS(DATA_BITS)) dut (
.clk(clk), .rst(rst), .tx_valid(tx_valid), .tx_data(tx_data), .tx_ready(tx_ready),
.rx_data(rx_data), .rx_valid(rx_valid),
.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso));
// Real slave: drives MISO from its own shift register, updated on the SHIFT edge.
// (Its MISO transitions exactly on the master's shift edge; only a master sampling
// on the OPPOSITE edge reads it settled. A same-edge master reads the transition.)
assign miso = slave_sr[DATA_BITS-1];
logic sclk_d;
always @(posedge clk) sclk_d <= sclk;
always @(posedge clk)
if (!cs_n && (sclk & ~sclk_d)) // slave shifts on the leading (rising) edge
slave_sr <= {slave_sr[DATA_BITS-2:0], mosi};
always #5 clk = ~clk;
always @(posedge clk) if (rx_valid) begin
if (rx_data !== 8'h6A) begin
$error("garbled: rx=0x%02h (a same-edge sampler reads a transitioning MISO)", rx_data);
errors++;
end
end
initial begin
rst = 1'b1; tx_valid = 1'b0; tx_data = 8'hB5;
repeat (4) @(posedge clk); rst = 1'b0;
@(posedge clk); wait (tx_ready); tx_valid = 1'b1;
@(posedge clk); tx_valid = 1'b0;
wait (rx_valid); repeat (4) @(posedge clk);
// The FIXED (opposite-edge) master reads 0x6A. Rebuilt with sample==shift edge,
// the same stimulus latches MISO mid-transition and garbles the byte.
if (errors == 0) $display("PASS: opposite-edge sampler reads MISO settled -> 0x6A");
else $display("FAIL: %0d — sampling on the shift edge reads a transitioning MISO", errors);
$finish;
end
endmodule4f. The integrated SPI master in Verilog
The same four blocks with reg/wire typing and localparam states. The CPHA edge routing and the last-sample framing guard are unchanged; only the spelling differs.
module spi_master #(
parameter CPOL = 0, CPHA = 0, DIV = 4, DATA_BITS = 8
)(
input wire clk, rst,
input wire tx_valid,
input wire [DATA_BITS-1:0] tx_data,
output wire tx_ready,
output reg [DATA_BITS-1:0] rx_data,
output reg rx_valid,
output reg sclk,
output reg cs_n,
output wire mosi,
input wire miso
);
// ---- SCLK generator (3.5): toggle SCLK every DIV clocks, idle at CPOL ----
reg [15:0] cnt; reg run; reg lead_edge, trail_edge;
always @(posedge clk) begin
if (rst) begin cnt<=0; sclk<=CPOL[0]; lead_edge<=0; trail_edge<=0; end
else begin
lead_edge<=0; trail_edge<=0;
if (!run) begin cnt<=0; sclk<=CPOL[0]; end
else if (cnt == DIV-1) begin // WRAP at DIV-1 -> exact half-period
cnt<=0; sclk<=~sclk; // one SCLK edge
if (sclk == CPOL[0]) lead_edge<=1'b1; // away from idle = LEADING
else trail_edge<=1'b1; // toward idle = TRAILING
end else cnt<=cnt+1'b1;
end
end
// CPHA routes the two edges (always OPPOSITE): sample vs shift.
wire sample_now = (CPHA==0) ? lead_edge : trail_edge;
wire shift_now = (CPHA==0) ? trail_edge : lead_edge;
// ---- register MISO before sampling ----
reg miso1, misos;
always @(posedge clk) begin
if (rst) begin miso1<=0; misos<=0; end
else begin miso1<=miso; misos<=miso1; end
end
// ---- FSM (4.6) + shift datapath (2.6) + handshake (9.1) ----
localparam [1:0] SI=0, SL=1, SX=2, SD=3;
reg [1:0] st; reg [DATA_BITS-1:0] sh; reg [4:0] bcnt;
assign mosi = sh[DATA_BITS-1]; // MSB-first
assign tx_ready = (st==SI) && !rst;
always @(posedge clk) begin
if (rst) begin st<=SI; cs_n<=1'b1; run<=1'b0; sh<=0; bcnt<=0; rx_valid<=0; rx_data<=0; end
else begin
rx_valid<=1'b0;
case (st)
SI: begin cs_n<=1'b1; run<=1'b0;
if (tx_valid) begin sh<=tx_data; bcnt<=DATA_BITS[4:0]; st<=SL; end end // accept
SL: begin cs_n<=1'b0; run<=1'b1; st<=SX; end // assert CS before 1st edge
SX: begin cs_n<=1'b0; // HOLD CS across frame
if (sample_now) begin
sh[0]<=misos; // sample MISO on SAMPLE edge
if (bcnt==1) begin bcnt<=0; run<=1'b0; st<=SD; end // last SAMPLE -> done
else bcnt<=bcnt-1'b1;
end
if (shift_now && bcnt!=1) sh<={sh[DATA_BITS-2:0], misos}; // shift on the OTHER edge
end
SD: begin cs_n<=1'b1; rx_data<=sh; rx_valid<=1'b1; st<=SI; end // deassert AFTER last sample
endcase
end
end
endmodule module spi_master_loopback_tb;
parameter CPOL=0, CPHA=0, DIV=4, DATA_BITS=8;
reg clk, rst, tx_valid; reg [7:0] tx_data;
wire tx_ready, rx_valid, sclk, cs_n, mosi; wire [7:0] rx_data;
reg [7:0] expected;
integer errors;
wire miso = mosi; // wire-loopback slave: MISO echoes MOSI
spi_master #(.CPOL(CPOL),.CPHA(CPHA),.DIV(DIV),.DATA_BITS(DATA_BITS)) dut (
.clk(clk),.rst(rst),.tx_valid(tx_valid),.tx_data(tx_data),.tx_ready(tx_ready),
.rx_data(rx_data),.rx_valid(rx_valid),
.sclk(sclk),.cs_n(cs_n),.mosi(mosi),.miso(miso));
initial clk=1'b0; always #5 clk=~clk;
always @(posedge clk) if (rx_valid) begin
if (cs_n !== 1'b1) begin $display("FAIL: CS_N low at rx_valid"); errors=errors+1; end
if (rx_data !== expected) begin $display("FAIL: rx=%02h exp=%02h", rx_data, expected); errors=errors+1; end
end
task xfer; input [7:0] b; begin
@(posedge clk); while (!tx_ready) @(posedge clk);
tx_data=b; expected=b; tx_valid=1'b1; // rx==tx in wire-loopback
@(posedge clk); tx_valid=1'b0;
while (!rx_valid) @(posedge clk);
@(posedge clk);
end endtask
initial begin
errors=0; rst=1'b1; tx_valid=1'b0; tx_data=8'h00;
repeat (4) @(posedge clk); rst=1'b0;
xfer(8'h00); xfer(8'hFF); xfer(8'hB5); xfer(8'h01); xfer(8'h80); xfer(8'h6A);
if (errors==0) $display("PASS: all bytes survived MOSI->MISO loopback (rx==tx, CS framed)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog CPHA bug-vs-fix pair is the same one-line change: tie sample_now to the shift edge (broken) or to the opposite edge (correct).
// BUGGY: sample_now == shift_now -> MISO latched while it transitions; garbles vs a real slave.
module cpha_route_bad #(parameter CPHA=0) (
input wire lead_edge, trail_edge, output wire sample_now, shift_now );
assign shift_now = (CPHA==0) ? trail_edge : lead_edge;
assign sample_now = shift_now; // BUG: same edge as the shift
endmodule
// FIXED: sample and shift are OPPOSITE edges -> MISO read settled, half a beat later.
module cpha_route_good #(parameter CPHA=0) (
input wire lead_edge, trail_edge, output wire sample_now, shift_now );
assign sample_now = (CPHA==0) ? lead_edge : trail_edge;
assign shift_now = (CPHA==0) ? trail_edge : lead_edge; // FIX: the other edge
endmodule4g. The integrated SPI master in VHDL
VHDL states the same machines with an enumerated state type, numeric_std counters, and rising_edge(clk). The CPHA edge routing and the last-sample framing guard are identical; only the spelling differs.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_master is
generic ( CPOL : integer := 0; CPHA : integer := 0;
DIV : integer := 4; DATA_BITS : integer := 8 );
port (
clk, rst : in std_logic;
tx_valid : in std_logic;
tx_data : in std_logic_vector(DATA_BITS-1 downto 0);
tx_ready : out std_logic;
rx_data : out std_logic_vector(DATA_BITS-1 downto 0);
rx_valid : out std_logic;
sclk : out std_logic;
cs_n : out std_logic;
mosi : out std_logic;
miso : in std_logic );
end entity;
architecture rtl of spi_master is
function cpol_lvl return std_logic is
begin if CPOL = 1 then return '1'; else return '0'; end if; end function;
signal cnt : integer range 0 to DIV-1 := 0;
signal sclk_i : std_logic := cpol_lvl;
signal run : std_logic := '0';
signal lead_e, trail_e : std_logic := '0';
signal sample_now, shift_now : std_logic;
signal miso1, misos : std_logic := '0';
type state_t is (S_IDLE, S_LOAD, S_XFER, S_DONE);
signal st : state_t := S_IDLE;
signal sh : std_logic_vector(DATA_BITS-1 downto 0) := (others=>'0');
signal bcnt : integer range 0 to DATA_BITS := 0;
begin
-- CPHA routes the two edges (always OPPOSITE): sample vs shift.
sample_now <= lead_e when CPHA = 0 else trail_e;
shift_now <= trail_e when CPHA = 0 else lead_e;
mosi <= sh(DATA_BITS-1); -- MSB-first
tx_ready <= '1' when (st = S_IDLE and rst = '0') else '0';
sclk <= sclk_i;
-- ---- SCLK generator (3.5): toggle every DIV clocks, idle at CPOL ----
gen_p : process (clk) begin
if rising_edge(clk) then
if rst = '1' then
cnt <= 0; sclk_i <= cpol_lvl; lead_e <= '0'; trail_e <= '0';
else
lead_e <= '0'; trail_e <= '0';
if run = '0' then cnt <= 0; sclk_i <= cpol_lvl;
elsif cnt = DIV-1 then -- WRAP at DIV-1 -> exact half-period
cnt <= 0; sclk_i <= not sclk_i;
if sclk_i = cpol_lvl then lead_e <= '1'; -- away from idle = LEADING
else trail_e <= '1'; end if; -- toward idle = TRAILING
else cnt <= cnt + 1; end if;
end if;
end if;
end process;
-- ---- register MISO ----
sync_p : process (clk) begin
if rising_edge(clk) then
if rst = '1' then miso1 <= '0'; misos <= '0';
else miso1 <= miso; misos <= miso1; end if;
end if;
end process;
-- ---- FSM (4.6) + shift datapath (2.6) + handshake (9.1) ----
ctrl_p : process (clk) begin
if rising_edge(clk) then
if rst = '1' then
st <= S_IDLE; cs_n <= '1'; run <= '0';
sh <= (others=>'0'); bcnt <= 0; rx_valid <= '0'; rx_data <= (others=>'0');
else
rx_valid <= '0';
case st is
when S_IDLE =>
cs_n <= '1'; run <= '0';
if tx_valid = '1' then -- tx_valid and tx_ready -> accept
sh <= tx_data; bcnt <= DATA_BITS; st <= S_LOAD;
end if;
when S_LOAD =>
cs_n <= '0'; run <= '1'; st <= S_XFER; -- assert CS before 1st edge
when S_XFER =>
cs_n <= '0'; -- HOLD CS across the frame
if sample_now = '1' then
sh(0) <= misos; -- sample MISO on SAMPLE edge
if bcnt = 1 then bcnt <= 0; run <= '0'; st <= S_DONE; -- last sample
else bcnt <= bcnt - 1; end if;
end if;
if shift_now = '1' and bcnt /= 1 then
sh <= sh(DATA_BITS-2 downto 0) & misos; -- shift on the OTHER edge
end if;
when S_DONE =>
cs_n <= '1'; rx_data <= sh; rx_valid <= '1'; st <= S_IDLE; -- deassert after last sample
end case;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_master_loopback_tb is end entity;
architecture sim of spi_master_loopback_tb is
constant DATA_BITS : integer := 8;
signal clk : std_logic := '0';
signal rst, tx_valid, tx_ready, rx_valid, sclk, cs_n, mosi, miso : std_logic := '0';
signal tx_data, rx_data : std_logic_vector(DATA_BITS-1 downto 0);
signal expected : std_logic_vector(DATA_BITS-1 downto 0) := (others=>'0');
begin
miso <= mosi; -- wire-loopback slave: MISO echoes MOSI
dut : entity work.spi_master
generic map (CPOL=>0, CPHA=>0, DIV=>4, DATA_BITS=>DATA_BITS)
port map (clk=>clk, rst=>rst, tx_valid=>tx_valid, tx_data=>tx_data, tx_ready=>tx_ready,
rx_data=>rx_data, rx_valid=>rx_valid,
sclk=>sclk, cs_n=>cs_n, mosi=>mosi, miso=>miso);
clk <= not clk after 5 ns;
check : process (clk) begin
if rising_edge(clk) and rx_valid = '1' then
assert cs_n = '1' report "CS_N low at rx_valid" severity error;
assert rx_data = expected report "rx_data /= sent byte" severity error;
end if;
end process;
stim : process
procedure xfer (b : std_logic_vector(DATA_BITS-1 downto 0)) is
begin
wait until rising_edge(clk); loop exit when tx_ready = '1'; wait until rising_edge(clk); end loop;
tx_data <= b; expected <= b; tx_valid <= '1'; -- rx = tx in wire-loopback
wait until rising_edge(clk); tx_valid <= '0';
loop exit when rx_valid = '1'; wait until rising_edge(clk); end loop;
wait until rising_edge(clk);
end procedure;
begin
rst <= '1'; tx_valid <= '0'; tx_data <= (others=>'0');
for i in 0 to 3 loop wait until rising_edge(clk); end loop; rst <= '0';
xfer(x"00"); xfer(x"FF"); xfer(x"B5"); xfer(x"01"); xfer(x"80"); xfer(x"6A");
report "spi_master loopback self-check complete (rx == tx, CS framed)" severity note;
wait;
end process;
end architecture;The VHDL CPHA bug-vs-fix pair is the same one-line change to the sample edge: tie sample_now to the shift edge and it reads a transitioning MISO; tie it to the opposite edge and it reads it settled.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: sample_now = shift_now -> MISO latched mid-transition; garbles vs a real slave.
entity cpha_route_bad is
generic ( CPHA : integer := 0 );
port ( lead_e, trail_e : in std_logic; sample_now, shift_now : out std_logic );
end entity;
architecture rtl of cpha_route_bad is
signal sh : std_logic;
begin
sh <= trail_e when CPHA = 0 else lead_e;
shift_now <= sh;
sample_now <= sh; -- BUG: same edge as the shift
end architecture;
-- FIXED: sample and shift are OPPOSITE edges -> MISO read settled, half a beat later.
entity cpha_route_good is
generic ( CPHA : integer := 0 );
port ( lead_e, trail_e : in std_logic; sample_now, shift_now : out std_logic );
end entity;
architecture rtl of cpha_route_good is
begin
sample_now <= lead_e when CPHA = 0 else trail_e;
shift_now <= trail_e when CPHA = 0 else lead_e; -- FIX: the other edge
end architecture;Across all three languages the master's whole correctness is one sentence: generate two SCLK edges per beat at the CPOL idle level, shift MOSI on one edge and SAMPLE MISO on the OTHER (which edge is which is CPHA), hold CS_N low across the whole frame and deassert only after the LAST sample.
5. Verification Strategy
An SPI master's correctness is not 'did the first few bits look right' — it is 'does a byte pushed in come back exchanged with the slave's byte exactly, MSB-first, with CS_N and SCLK framed correctly, across the CPOL/CPHA modes and the SCLK divisors you will actually use.' The one invariant, and the whole §4 loopback testbench, is:
Present a byte with
tx_validwhiletx_readyis high; whenrx_validstrobes, the received byte (rx_data) must equal what the slave shifted back (in a MOSI→MISO wire-loopback, the byte you sent), withCS_Ndeasserted at that instant andSCLKhaving produced exactlyDATA_BITSbeats at the CPOL idle level — for every byte, in every supported CPOL/CPHA mode, at every supportedDIV.
Everything below is that invariant made checkable, and it maps onto the loopback testbenches in §4.
- Loopback self-check (the core test). Tie
mositomisoand, on eachrx_valid, comparerx_dataagainst the byte you sent (in a wire-loopback the master receives its own byte back). Sweep corner bytes —0x00,0xFF,0xB5,0x01,0x80,0x6A— plus random bytes, because an all-0or all-1byte hides an MSB/LSB-order or bit-count error that0xB5/0x6Aexpose. The three testbenches do exactly this (SVassert (rx_data === expected), Verilogif (rx_data !== expected) $display("FAIL…"), VHDLassert rx_data = expected … severity error). - All four CPOL/CPHA modes. Re-run the loopback with
CPOL/CPHAset to each of the four modes. A master that passes Mode 0 can fail Mode 1 or 3 if the CPHA edge routing or the first-bit-presentation timing was hard-coded to one mode — verify every mode, do not assume one. - A real slave model (the sample-edge payoff). Beyond the lenient wire-loopback, instantiate a behavioural slave that drives
MISOfrom its own shift register updated on the shift edge (as §4e does) and returns a known byte. A correct opposite-edge master reads it clean; the §7 same-edge master reads a transitioningMISOand garbles it. This is the test that distinguishes a master that works against a wire-tie from one that works against a chip. - Multiple SCLK divisors. Re-run at a couple of
DIVvalues (e.g.DIV = 2andDIV = 8). A design that passes at one divisor can fail at another if the edge-strobe or bit-beat counting assumed a specificDIV— verify generic, do not assume generic. - CS/SCLK framing checks. Assert
CS_Nis low for the entire run ofSCLKbeats and rises only after the last sample (catches the §7 row-2 early-deassert), and thatSCLKrests at the CPOL level wheneverCS_Nis high (catches a wrong idle level — an extra or missing edge). Count theSCLKedges per frame and assert exactly2*DATA_BITS. - Handshake protocol. Confirm the byte is accepted exactly on
tx_valid && tx_ready(not while busy), thattx_readyis low throughout a transfer, and thatrx_validis a one-cycle strobe withrx_datastable when it fires. Back-to-back transfers (asserttx_validagain as soon astx_readyreturns) must both complete intact. - Expected waveform.
CS_Nfalls,SCLKrunsDATA_BITSclean beats at the CPOL idle level,MOSIpresents the byte MSB-first (changing on the shift edge),MISOis sampled on the opposite edge,CS_Nrises after the last sample, andrx_validpulses once with the received byte. Arx_datathat is garbled only against a real slave (never against a wire-loopback) is the visual signature of the §7 same-edge bug; a lost last bit or aCS_Nthat rises during the last beat is the early-deassert bug.
6. Common Mistakes
Sampling MISO on the wrong edge for the configured CPHA — the signature bug. The reason the master is a capstone. Sampling MISO on the same SCLK edge that shifts MOSI (getting the CPHA routing backward) latches the slave's line at the instant it is transitioning, so the bit read is still settling. It 'passes' against a wire-loopback slave — where MISO is a static tie to MOSI that never transitions relative to the sampling edge — and reads intermittently wrong bits (mode-dependent) against a real slave that drives MISO on its own shift edge. Sample on the edge opposite the shift: CPHA 0 samples the leading edge, CPHA 1 the trailing, and the shift is always the other. Full post-mortem in §7 row 1; buggy-vs-fixed RTL in §4e.
CS_N deasserted too early / not held across the frame. CS_N is the frame envelope, and the last sample edge must occur under an asserted CS_N. Drop CS_N on the last shift edge (a beat too early) and the final bit's sample never happens inside the frame — the last incoming bit is lost, and a real slave, seeing CS_N rise mid-byte, aborts the transfer and may leave its shift register mis-aligned for the next frame too. Hold CS_N low from LOAD through the last sample in XFER; deassert only in DONE. Post-mortem in §7 row 2.
Wrong SCLK idle level (CPOL) — an extra or missing edge. SCLK must rest at the CPOL level between frames. If the generator parks SCLK at the wrong level (or does not force it to CPOL while idle), the first transition into the frame is in the wrong direction, so what should be the leading edge is actually a trailing edge — every bit is off by one edge, and the slave samples the wrong half of each beat. Force SCLK = CPOL whenever the clock is not running.
MSB/LSB order flipped. SPI is conventionally MSB-first. If the datapath drives shift_reg[LSB] onto MOSI (or deserializes into the wrong end), every byte arrives bit-reversed. Drive MOSI = shift_reg[MSB] and shift left, capturing MISO into the LSB, so the first bit out is the MSB and the first bit in becomes the MSB of the received byte.
Not registering MISO before use. MISO is driven by another chip and is asynchronous to the master's clock; sampling the raw pin can catch it mid-transition or metastable. Register MISO through a flop (a two-flop synchronizer for a genuinely asynchronous slave) and sample the registered signal on the sample edge, never the raw pin.
Leaving XFER on the last shift instead of the last sample. The bit-beat counter must retire the transfer when the last bit has been sampled, not merely shifted out. Counting down and exiting on the shift edge of the last beat skips the final MISO capture — the received byte is one bit short, and the symptom looks exactly like the early-CS_N bug because both drop the last incoming bit.
7. DebugLab
The SPI master that passes a loopback and dies against a real slave — a same-edge sample and an early CS deassert
The engineering lesson: an SPI master is correct only if MISO is sampled on the edge the line is settled and CS_N stays low across every last sample — and both signature bugs are the same crime, a capture placed where the data is not yet valid, committed at two different stations. The first samples MISO on the shift edge (where the slave is still driving), collapsing the half-beat settling margin to zero; the second stops the frame on the last shift edge, so the last MISO bit is captured outside CS_N (or not at all). Both hide behind a lenient wire-loopback and both surface the instant a real, independently-clocked slave drives MISO on its own edges. Three habits make the master trustworthy, identical across SystemVerilog, Verilog, and VHDL: shift on one edge and SAMPLE on the OTHER (route the two SCLK edges by CPHA, never the same edge for both), retire the transfer on the last SAMPLE edge (and hold CS_N low into DONE, deasserting only after), and verify against a slave MODEL that drives MISO on its shift edge, not just a wire-loopback — that model is the test that separates a master that works against a wire-tie from one that works against a chip.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses composing the pieces and getting the edge/framing accounting right.
Exercise 1 — Draw the four modes
For a single-byte transfer of 0xB5, draw CS_N, SCLK, and MOSI for all four CPOL/CPHA modes, MSB-first. Mark on each the sample edge and the shift edge of every beat, and mark where the first MOSI bit must become valid. State in one line per mode which physical edge (rising/falling) samples and which shifts, and why CPHA 0 needs the first bit presented before the first edge.
Exercise 2 — Predict the same-edge failure
An engineer samples MISO on the shift edge in Mode 0 (the §7 bug). Describe precisely (i) what a MOSI→MISO wire-loopback reports, (ii) what happens against a real slave that drives MISO on the shift edge and returns 0x6A, and (iii) which bits garble and why the corruption is non-deterministic. Then give the one-line routing fix and the testbench change (the slave model) that would have exposed it.
Exercise 3 — Make DIV and DATA_BITS generic and re-verify
The master is parameterized on DIV and DATA_BITS. State (i) how the SCLK-edge counting and the bit-beat counter change as DIV and DATA_BITS vary, (ii) why a design that passes at DIV = 4, DATA_BITS = 8 can still fail at DIV = 2 or DATA_BITS = 16, and (iii) exactly what you must re-verify (which sweeps, which framing assertions) to trust the generic version. Give the number of SCLK edges per frame as an expression in DATA_BITS.
Exercise 4 — Add multi-byte transfers with CS held across bytes
Many SPI slaves need a multi-byte transaction (a command byte then N data bytes) with CS_N held low across all of them. Extend the master to exchange NBYTES bytes under one CS_N assertion. State (i) where the FSM changes (a byte counter around the bit-beat counter, and where CS_N is deasserted), (ii) how the handshake changes (streaming tx_data/accepting each byte between bytes, and how rx_valid fires per byte), and (iii) the new framing assertion verification must add (CS_N stays low across all NBYTES byte-frames, rising only after the last sample of the last byte).
10. Related Tutorials
The patterns this SPI master composes (the lessons you are assembling here):
- Shift Registers — Chapter 2.6; the full-duplex datapath — one register serializing
MOSI(PISO) and deserializingMISO(SIPO) in the same shift. - Clock Dividers & Timers — Chapter 3.5; the SCLK generator —
DIVsystem clocks per SCLK half-period, parked at theCPOLidle level. - FSM + Datapath — Chapter 4.6; the control FSM asserting
CS_N, counting bit-beats, and driving the shift/sample datapath. - Datapath / Control Split — Chapter 4.x; the discipline of separating the sequencing brain (FSM,
CS_N, bit counter) from the shifting/sampling datapath. - The valid/ready Handshake — Chapter 9.1; the command/result wrapper —
tx_valid && tx_readyaccepts the byte,rx_validpublishes the result. - ALU Construction — Chapter 5.5; the datapath-composition discipline (functional units feeding a shared result) that the shift register and result path echo here.
- Parameter Patterns — Chapter 6.x; how
CPOL,CPHA,DIV, andDATA_BITSparameterize one module across all four modes, clock rates, and word widths.
Backward / method:
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this case study walks end to end.
- What Is an RTL Design Pattern? — Chapter 0.1; why composing patterns into a real peripheral — not inventing a new one — is the design skill this capstone rehearses.
The sibling case study:
- UART Case Study — Chapter 13.1; the same FSM + shift-register composition, but asynchronous (no shared clock, oversampled, mid-bit sampled) — the instructive contrast to SPI's source-synchronous edge placement.
Forward — the other case studies (unlock as they ship):
- Streaming FIFO Case Study (
/rtl-design-patterns/case-study-streaming-fifo) — Chapter 13.3; buffering the byte stream a master like this produces and consumes. - Arbitered Bus Case Study (
/rtl-design-patterns/case-study-arbitered-bus) — Chapter 13.4; many masters, one shared bus, resolved by an arbiter. - CDC Stream Case Study (
/rtl-design-patterns/case-study-cdc-stream) — Chapter 13.5; moving a stream safely across two unrelated clocks.
Verilog v1 prerequisites (the grammar these controllers transcribe into):
- Case Statements — the construct behind the FSM's next-state and output
caseblocks. - If-Else Statements — the conditional edge routing and framing guards in the datapath and FSM.
- Blocking and Non-Blocking Assignments —
<=in the clocked FSM, SCLK generator, and shift datapath throughout the design.
11. Summary
- An SPI master is patterns composed into a real peripheral, not a new pattern. It is an SCLK generator (3.5, carrying CPOL) feeding a full-duplex shift datapath (2.6,
MOSI= MSB,MISO→ LSB, one shift per beat), sequenced by a CS/bit-beat FSM (4.6) and wrapped by a valid/ready handshake (9.1). Each block is a lesson you already had; the capstone is wiring them with the edges exact. - SPI is source-synchronous. The master generates
SCLKand sends it with the data, so there is nothing to recover — no oversampling, no baud tolerance, no framing recovery. Correctness is not a sample point in time (as in the UART) but a sample edge — exact, decided by CPHA. - CPOL/CPHA is the whole subtlety, and the rule is one sentence.
CPOLsets theSCLKidle level;CPHAsets whether the leading or trailing edge of each beat samplesMISO. Across all four modes: shiftMOSIon one edge and SAMPLEMISOon the OTHER — sample on the shift edge and you latch a transitioning line, the bug that passes a wire-loopback and dies against a real slave (§7 row 1). CS_Nframes the frame. AssertCS_Nbefore the firstSCLKedge, hold it low across every bit-beat, and deassert only after the last sample. Retire the transfer on the last sample edge, not the last shift — dropCS_Na beat early and the last incoming bit is lost and a real slave aborts the byte (§7 row 2). RegisterMISObefore sampling; parkSCLKatCPOLwhen idle.- Prove it against a real slave model, not just a wire-loopback. Tie
mositomisoand assertrx_data == tx_datawithCS_Nframed — but also drive a behavioural slave that changesMISOon its shift edge, across all four CPOL/CPHA modes and a couple ofDIVvalues. That slave-model test is what separates a master that works against a wire-tie from one that works against a chip — self-checking loopback testbenches shown in SystemVerilog, Verilog, and VHDL.