Skip to content

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.

the-interface.v — a byte-exchange master: command in, four SPI wires out, a received byte back
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // ---- 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 edge

2. 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 SCLK edge, i.e. as CS_N falls, 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 flow
The four SPI modes — CPOL sets SCLK idle level, CPHA sets which edge samples vs shiftsCPHA flips sample/shift edgeCPHA flipssample/shift…CPOL flips SCLK idle levelCPOL flipsSCLK idle…CPHA flips sample/shift edgeCPHA flipssample/shift…all four obeyMode 0 (0,0)SCLK idle 0; SAMPLE on leading (rising), SHIFT on trailing (falling)Mode 1 (0,1)SCLK idle 0; SHIFT on leading (rising), SAMPLE on trailing (falling)Mode 2 (1,0)SCLK idle 1; SAMPLE on leading (falling), SHIFT on trailing (rising)Mode 3 (1,1)SCLK idle 1; SHIFT on leading (falling), SAMPLE on trailing (rising)the invariantshift MOSI on one edge, SAMPLE MISO on the OTHER — always opposite
The four SPI modes are two orthogonal bits. CPOL is the level SCLK rests at when idle (0 = idle low, 1 = idle high); CPHA is which of a beat's two edges samples the incoming bit (0 = the leading / first edge, 1 = the trailing / second edge). CPOL only relabels which physical edge is leading: with CPOL 0 the leading edge is a rising edge, with CPOL 1 it is a falling edge. Underneath every mode is one invariant the master must honour: shift MOSI (change the outgoing bit) on one edge of the beat and SAMPLE MISO (latch the incoming bit) on the OTHER edge — never the same edge, or you read the line while it is transitioning. In CPHA 0 the first bit must be valid as CS_N falls, before the first edge; in CPHA 1 it appears on the leading edge. This table is identical across SystemVerilog, Verilog, and VHDL; only the RTL that produces the edges differs in spelling.

The 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 flow
Block diagram — SCLK generator + full-duplex shift datapath + CS/bit-beat FSM + valid/ready handshakeaccept tx_data / raise rx_validaccept tx_data/ raise…run / gate SCLKload / shift_en per beatload /shift_en pe…sample edge/ shift edgeMOSI out / MISO inhandshake (9.1)tx_valid && tx_ready accepts tx_data; rx_valid publishes rx_datacontrol FSM (4.6)IDLE/LOAD/XFER/DONE; asserts CS_N, counts DATA_BITS bit-beatsSCLK gen (3.5)DIV divider -> two edges/beat; idle level = CPOLshift datapath(2.6)MOSI = shift_reg[MSB]; MISO -> shift_reg[LSB]; one shift/beatSPI busSCLK, CS_N, MOSI out, MISO in — to the slave
Four blocks, one bus. The valid/ready handshake accepts a byte when the master is idle and announces the received byte when the frame closes. The control FSM is the 4.6 fsm-plus-datapath brain: it drops CS_N to open the frame, gates the SCLK generator to run exactly DATA_BITS bit-beats, drives the shift datapath one shift-and-sample per beat, then raises CS_N and strobes rx_valid. The SCLK generator is a 3.5 clock divider that turns DIV system clocks into one SCLK half-period and rests SCLK at the CPOL level between frames. The shift datapath is a single 2.6 shift register serving both directions: its MSB drives MOSI while MISO is captured into its LSB, so the transmit byte serializes and the receive byte deserializes in the SAME shift. Nothing here knows it is SPI; it divides, shifts, counts, and samples on command. Identical in structure across SystemVerilog, Verilog, and VHDL.

The 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 DIV system clocks; every flop in the design is clocked by the fast system clk, and edges of sclk are detected as sclk transitions in that domain — no second clock domain, no gated clock into flops.
  • Synchronous, active-high reset to a safe idle. After reset cs_n is high (deasserted), sclk sits at CPOL, the FSM is in IDLE, and the master presents tx_ready high. The incoming MISO is 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 trailing sclk edges 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.

spi_sclk_gen.sv — divider: toggles SCLK every DIV clocks, idles at CPOL, flags the two edges
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

spi_shift.sv — one register, both directions: MOSI = MSB (shift), MISO -> LSB (sample)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

spi_master.sv — integrated master: SCLK gen (CPOL) + full-duplex shift + CS/bit-beat FSM + handshake
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

spi_master_loopback_tb.sv — MOSI -> MISO loopback: exchange random bytes, assert rx_data == expected
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

4e. 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.

spi_sample_edge.sv — BUGGY (sample on the SHIFT edge) vs FIXED (sample on the OTHER edge)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
spi_sample_edge_tb.sv — a real slave that drives MISO on its shift edge: the same-edge sampler garbles
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

4f. 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.

spi_master.v — integrated SPI master in Verilog (localparam states, CPHA edge routing)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
spi_master_loopback_tb.v — self-checking Verilog loopback: rx==tx over several bytes, print PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

spi_sample_edge.v — BUGGY (sample on shift edge) vs FIXED (sample on the other edge)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
   endmodule

4g. 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.

spi_master.vhd — integrated SPI master in VHDL (numeric_std, CPHA edge routing)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
spi_master_loopback_tb.vhd — self-checking VHDL loopback: MISO = MOSI, assert rx_data = sent (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

spi_sample_edge.vhd — BUGGY (sample on shift edge) vs FIXED (sample on the other edge)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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_valid while tx_ready is high; when rx_valid strobes, the received byte (rx_data) must equal what the slave shifted back (in a MOSI→MISO wire-loopback, the byte you sent), with CS_N deasserted at that instant and SCLK having produced exactly DATA_BITS beats at the CPOL idle level — for every byte, in every supported CPOL/CPHA mode, at every supported DIV.

Everything below is that invariant made checkable, and it maps onto the loopback testbenches in §4.

  • Loopback self-check (the core test). Tie mosi to miso and, on each rx_valid, compare rx_data against 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-0 or all-1 byte hides an MSB/LSB-order or bit-count error that 0xB5/0x6A expose. The three testbenches do exactly this (SV assert (rx_data === expected), Verilog if (rx_data !== expected) $display("FAIL…"), VHDL assert rx_data = expected … severity error).
  • All four CPOL/CPHA modes. Re-run the loopback with CPOL/CPHA set 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 MISO from 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 transitioning MISO and 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 DIV values (e.g. DIV = 2 and DIV = 8). A design that passes at one divisor can fail at another if the edge-strobe or bit-beat counting assumed a specific DIV — verify generic, do not assume generic.
  • CS/SCLK framing checks. Assert CS_N is low for the entire run of SCLK beats and rises only after the last sample (catches the §7 row-2 early-deassert), and that SCLK rests at the CPOL level whenever CS_N is high (catches a wrong idle level — an extra or missing edge). Count the SCLK edges per frame and assert exactly 2*DATA_BITS.
  • Handshake protocol. Confirm the byte is accepted exactly on tx_valid && tx_ready (not while busy), that tx_ready is low throughout a transfer, and that rx_valid is a one-cycle strobe with rx_data stable when it fires. Back-to-back transfers (assert tx_valid again as soon as tx_ready returns) must both complete intact.
  • Expected waveform. CS_N falls, SCLK runs DATA_BITS clean beats at the CPOL idle level, MOSI presents the byte MSB-first (changing on the shift edge), MISO is sampled on the opposite edge, CS_N rises after the last sample, and rx_valid pulses once with the received byte. A rx_data that 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 a CS_N that 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).

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 deserializing MISO (SIPO) in the same shift.
  • Clock Dividers & Timers — Chapter 3.5; the SCLK generator — DIV system clocks per SCLK half-period, parked at the CPOL idle 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_ready accepts the byte, rx_valid publishes 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, and DATA_BITS parameterize 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):

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 SCLK and 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. CPOL sets the SCLK idle level; CPHA sets whether the leading or trailing edge of each beat samples MISO. Across all four modes: shift MOSI on one edge and SAMPLE MISO on 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_N frames the frame. Assert CS_N before the first SCLK edge, 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 — drop CS_N a beat early and the last incoming bit is lost and a real slave aborts the byte (§7 row 2). Register MISO before sampling; park SCLK at CPOL when idle.
  • Prove it against a real slave model, not just a wire-loopback. Tie mosi to miso and assert rx_data == tx_data with CS_N framed — but also drive a behavioural slave that changes MISO on its shift edge, across all four CPOL/CPHA modes and a couple of DIV values. 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.