VHDL · Chapter 20.2 · Capstone Projects
Capstone: SPI Controller
The second capstone is an SPI master, which adds configurable clock polarity and phase to the serial-transceiver skills from the UART. SPI is full-duplex, so every clock pulse simultaneously shifts a bit out on MOSI and samples a bit in on MISO, letting a single shift register exchange a word in each direction at once, most-significant-bit first by convention. The master generates its serial clock from a clock-enable tick rather than a divided clock, frames each transfer by asserting chip-select, and counts bits with a small state machine. The subtle part is the four modes set by clock polarity and clock phase, which decide the idle clock level and which edge shifts versus samples. Getting the shift edge and sample edge right for the selected mode is the whole correctness question. Generics make it reusable, and verification runs across all four modes and chip-select timing.
Intermediate16 min readVHDLCapstoneSPICPOL/CPHAFull-DuplexSerial
1. Engineering intuition — one clock, two directions, four phase choices
SPI is simpler than UART in one way — there is a shared clock (SCLK), so the receiver does not have to recover timing — and more configurable in another: the master and slave must agree on when data is driven and when it is sampled relative to SCLK. That agreement is the CPOL/CPHA mode. Picture SCLK as a metronome: on one edge each side changes its output bit; on the other edge each side reads the incoming bit. Which edge does which, and whether SCLK idles high or low, are the four modes. Because the link is full-duplex, the same shift register both pushes a bit out on MOSI and pulls a bit in on MISO every beat — send and receive are the same operation. Hold that picture — a shift register clocked by SCLK, with the shift edge and sample edge set by the mode — and the design follows.
2. Formal explanation — the SPI master architecture
-- GENERICS: rate + word width + mode select.
entity spi_master is
generic ( CLK_FREQ : positive := 50_000_000;
SCLK_FREQ : positive := 1_000_000;
WIDTH : positive := 8;
CPOL : std_logic := '0'; -- idle clock level
CPHA : std_logic := '0' ); -- sample on first or second edge
port ( clk, rst : in std_logic;
start : in std_logic; busy : out std_logic;
tx_data : in std_logic_vector(WIDTH-1 downto 0);
rx_data : out std_logic_vector(WIDTH-1 downto 0);
sclk, mosi, cs_n : out std_logic; -- to the slave
miso : in std_logic ); -- from the slave
end entity;
-- SCLK from a clock-ENABLE tick (NOT a divided clock, 17.4). Idle level = CPOL.
-- CPOL/CPHA -> which SCLK edge SHIFTS (drives MOSI) and which SAMPLES (reads MISO):
-- CPHA=0: sample on the FIRST edge, shift on the second. CPHA=1: shift first, sample second.
-- CPOL sets the idle level (and thus whether 'first edge' is rising or falling).
-- FSM: IDLE -> assert CS_n='0' -> run WIDTH SCLK pulses (shift out MOSI / sample in MISO) -> deassert CS.The SPI master is generics + an SCLK tick (clock enable) + a shift register (MOSI out, MISO in, simultaneously) + CS framing + a bit-counter FSM, with CPOL/CPHA deciding the shift edge and sample edge. Full-duplex means one shift operation serves both directions.
3. Production usage — full-duplex shift on the mode's edges
-- One transfer: assert CS, then for each of WIDTH SCLK pulses, SHIFT out on one edge, SAMPLE in on the other.
process (clk) begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; cs_n <= '1'; sclk <= CPOL; busy <= '0';
elsif sclk_tick = '1' then
case state is
when IDLE =>
sclk <= CPOL; cs_n <= '1';
if start = '1' then shreg <= tx_data; bitc <= 0; cs_n <= '0'; busy <= '1'; state <= XFER; end if;
when XFER =>
sclk <= not sclk; -- toggle SCLK each tick
-- on the SHIFT edge: drive mosi <= shreg(WIDTH-1); shift shreg left.
-- on the SAMPLE edge: shreg(0) <= miso; (full-duplex: out and in on the same word)
if bitc = WIDTH-1 and sample_edge then state <= DONE; else bitc <= bitc + 1; end if;
when DONE =>
cs_n <= '1'; sclk <= CPOL; rx_data <= shreg; busy <= '0'; state <= IDLE;
end case;
end if;
end if;
end process;
-- 'sample_edge' / 'shift_edge' are derived from CPOL and CPHA -> selects which SCLK transition does which.What hardware does this become? A serial master: an SCLK generator (toggled on the tick, idling at CPOL),
one shift register that drives MOSI from its top bit and captures MISO into its bottom bit, a CS
output framing the word, and a bit-counter FSM. The decisive logic is the CPOL/CPHA-derived edge selection:
which SCLK transition is the shift edge (change MOSI) and which is the sample edge (read MISO). Because the
exchange is full-duplex, the same register both transmits tx_data and assembles rx_data — there is no
separate receive path. Generics make one source serve any clock ratio, word width, and mode, exactly like the UART.
4. Structural interpretation — the SPI master
5. Simulation interpretation — a mode-0 SPI transfer
SPI mode 0 (CPOL=0, CPHA=0): MOSI changes on falling SCLK, MISO sampled on rising
8 cycles6. Debugging example — wrong CPOL/CPHA edge (and a missed full-duplex capture)
Expected: correct data exchange with the slave. Observed: the master reads garbage on MISO (or the slave misreads MOSI), often off by one bit or completely scrambled, and it works with one slave but not another. Root cause: the shift and sample edges did not match the selected CPOL/CPHA mode — the master sampled MISO on the wrong SCLK edge (when the data was still changing) or shifted MOSI on the sampling edge, so each side read the other mid-transition; or the design treated SPI as half-duplex and failed to capture MISO while transmitting. Fix: derive the sample edge and shift edge strictly from CPOL and CPHA (sample when data is stable, shift on the opposite edge), and capture MISO into the same shift register on the sample edge so the transfer is truly full-duplex. Engineering takeaway: SPI correctness is entirely about matching the shift vs sample edge to the CPOL/CPHA mode and capturing MISO simultaneously with driving MOSI — a wrong edge or a missed capture scrambles the data and breaks interoperability.
-- BUG: sample MISO on the same edge MOSI changes -> read mid-transition -> garbage.
-- FIX: sample on the STABLE edge, shift on the opposite, both derived from CPOL/CPHA; capture MISO full-duplex.
-- if sample_edge then shreg(0) <= miso; end if; -- read when stable
-- if shift_edge then mosi <= shreg(WIDTH-1); shreg <= shreg(WIDTH-2 downto 0) & '0'; end if;7. Common mistakes & what to watch for
- Wrong shift/sample edge for the mode. Derive both strictly from CPOL/CPHA; sample when data is stable, shift on the opposite edge — a wrong edge scrambles data.
- Treating SPI as half-duplex. Capture MISO into the shift register on the sample edge while driving MOSI — every beat exchanges both directions.
- Dividing the clock for SCLK. Generate SCLK from a clock-enable tick idling at CPOL, not a fabric-divided clock (17.4).
- CS timing errors. Assert CS before the first edge and hold it through the whole word; deassert only after the last bit (some slaves need setup/hold margins).
- Skipping the mode matrix in verification. Test all four CPOL/CPHA modes, CS framing, and back-to-back transfers, not just mode 0.
8. Engineering insight & continuity
The SPI capstone adds configurable phase/polarity to serial design: an SCLK tick (clock enable, idling at CPOL), a full-duplex shift register exchanging MOSI and MISO every beat, CS framing, a bit-counter FSM, and — the crux — CPOL/CPHA-derived shift and sample edges, all verified across the four-mode matrix. It is the model for any clocked serial master. The next capstone returns to a pure datapath/control storage block built for robustness — the next lesson, Capstone: Parameterized FIFO, a fully generic, verified queue.