Skip to content

An SPI controller on an APB bus is two designs glued at the register file: a CSR block that decodes APB accesses, and a serial engine that turns those register values into clocked bits on a wire. Everything you have learned about APB register banks — decode, write-commit, read-mux, status flags — is the left half of this peripheral. The right half is the SPI machine: a clock divider, two FIFOs, a shift register, and an interrupt block, all configured by, and observed through, the registers. This chapter grounds that split in the real Arm PrimeCell PL022 SSP register map and teaches the two ideas that are not on the datasheet's surface: how the SPO/SPH bits in SSPCR0 select the four SPI modes (clock idle level and sampling edge), and how the serial clock is built from the prescale and rate registers as SCLK = PCLK / (CPSDVSR × (1 + SCR)). The single idea to carry: the APB side is a pure register interface; all SPI behaviour lives in the engines those registers configure — so "program the peripheral" means "write the right values to SSPCR0, SSPCR1, SSPCPSR in the right order, then push data into SSPDR."

1. Problem statement

The problem is exposing a serial protocol — SPI, which is fundamentally a shift register clocked at a chosen rate, polarity, and phase — through a parallel, address-mapped APB register interface, so that firmware configures and drives it purely by reading and writing registers.

SPI itself is dead simple as a wire protocol: a clock (SCLK), data out (MOSI), data in (MISO), and a chip-select (CS), with the controller shifting bits out on one SCLK edge and sampling in on the other. But a CPU cannot wiggle those pins directly at speed; it talks to memory-mapped registers. So an SPI controller is the machine that bridges the two worlds, and its design has to answer a set of concrete questions through the register map:

  • How is the serial clock generated? SCLK is some division of the bus clock PCLK. The register map must expose the divider — and the division is two-stage in the PL022: a prescale (SSPCPSR) and a per-frame rate (SSPCR0.SCR). Firmware needs both to hit a target SCLK.
  • What does an SCLK edge mean? SPI has four modes from two bits: clock polarity (idle high or low) and clock phase (sample on leading or trailing edge). Get these wrong relative to the slave device and every byte is shifted by a bit or sampled on the wrong edge — the data is garbage even though the bus transaction "worked."
  • How does parallel CPU data become serial bits and back? A write of a word to a data register must be buffered (the CPU runs faster than the wire) and shifted out; incoming bits must be assembled and buffered for the CPU to read. That is the job of the TX and RX FIFOs, and their fullness must be visible as status so software (or an interrupt) knows when to push or pop.

So the engineering problem is not "implement SPI" and not "implement an APB slave" — it is mapping the SPI machine onto a register file so that the four modes, the clock divider, the FIFOs, and the interrupt path are all reachable and observable through APB reads and writes, and doing it the way a real, shipping IP block does.

2. Why previous knowledge is insufficient

This module taught you how to build an APB register bank in general — and a PL022 SSP is an APB register bank — but a generic register bank does not teach you what a peripheral register map has to contain or why its fields mean what they mean.

  • CSR fundamentals and register-bank design taught the mechanics — decode, commit, read-mux — but not a real map. They showed you how to wire RW/RO/W1C registers and a PADDR decoder. A real SPI controller has a specific set of registers with specific fields (DSS, FRF, SPO, SPH, SCR, CPSDVSR, the FIFO SSPDR), and their semantics — especially that two bits pick one of four SPI modes — are not implied by "it's an RW register." Knowing how to build a register file is not knowing what this peripheral's register file must be.
  • Status registers and read-only registers taught hardware-owned flags — but not FIFO flags. SSPSR is a status register, but its bits (TFE, TNF, RNE, RFF, BSY) are owned by FIFO occupancy and the shift engine, not by a simple done flag. The status chapters give you the read-only, hardware-updated pattern; this chapter shows what drives those particular bits and why software must consult them before every push and pop.
  • Write logic and the read-data mux committed a register — but SSPDR is a FIFO port, not a register. A write to SSPDR does not overwrite a flop; it pushes into the TX FIFO. A read of SSPDR does not return a stored value; it pops the RX FIFO and has a side effect. The same address read and written behaves as two different queues. The write/read chapters assume a flop behind the address; here the address is a queue port, which changes the commit and read semantics entirely.

The gap is this: prior chapters built the substrate (a correct APB register interface) but stopped at storing and reporting values. A real peripheral adds domain semantics on top of that substrate — a clock divider you compute, mode bits that change the wire protocol, and FIFO-port registers with push/pop side effects and occupancy flags. Filling that gap with the real PL022 map, and the two non-obvious relationships inside it (modes and the divider), is the work of this chapter.

3. Mental model

The model: an SPI controller is a register-configured assembly line. APB writes load the configuration (rate, mode, data size) and the raw material (data words); the line clocks the material out as serial bits and clocks return bits back in; APB reads collect the product and the line's status. The CSR block is the loading dock; the divider, FIFOs, shift register, and interrupt block are the line.

Three refinements make it precise, and the last two are the non-trivial core of the chapter:

  • The register file is the only interface. Firmware never touches SCLK or MOSI directly. It writes SSPCPSR and SSPCR0 to set the rate and mode, writes SSPCR1.SSE to start the line, pushes words into SSPDR (the TX FIFO port), and reads SSPDR (the RX FIFO port) and SSPSR (the flags). Every SPI behaviour is the consequence of a register value — which is exactly why this is an APB peripheral chapter and not an SPI-physics chapter.
  • SPO/SPH select one of four SPI modes — clock idle level and which edge samples. SPO (clock polarity) sets whether SCLK idles low (0) or high (1). SPH (clock phase) sets whether data is captured on the leading edge (0) or the trailing edge (1). The four combinations are the four canonical SPI modes (0–3). This is the field most often gotten wrong: a controller set to mode 0 talking to a slave expecting mode 3 samples on the wrong edge and shifts every byte by a bit — the bus is fine, the data is wrong.
  • The serial clock is a two-stage divide: SCLK = PCLK / (CPSDVSR × (1 + SCR)). SSPCPSR.CPSDVSR is an even prescale divisor (2–254); SSPCR0.SCR is an 8-bit rate (0–255) that further divides by (1 + SCR). To hit a target SCLK you choose CPSDVSR and SCR so their product equals PCLK / SCLK. This formula — not "there's a clock register" — is what you must be able to derive on demand; it is why two registers, not one, set the rate.
A block diagram with the APB bus on the left feeding a CSR decode block listing the PL022 register map, which fans out to four engines — SSPCR0/CR1 config, a clock divider computing SCLK equals PCLK over CPSDVSR times one plus SCR, a TX FIFO feeding a shift register to MOSI, and a shift register from MISO into an RX FIFO — plus an interrupt block, with serial pins SCLK, MOSI, MISO and CS leaving on the right.
Figure 1 — the PL022-style SPI controller decomposed into its APB side and its serial side. On the left, the APB bus (PCLK, PSEL, PENABLE, PWRITE, PADDR, PWDATA, PRDATA, PREADY) drives a CSR block that decodes PADDR into the SSP register map (SSPCR0, SSPCR1, SSPDR, SSPSR, SSPCPSR, the interrupt registers, SSPDMACR). The CSR block fans out to four engines: the SSPCR0/CR1 configuration registers that set DSS, FRF, SPO/SPH (the mode) and SCR; the clock divider that produces SCLK as PCLK/(CPSDVSR×(1+SCR)); the TX path where an SSPDR write pushes a word into the TX FIFO that feeds the shift register out on MOSI; and the RX path where MISO shifts into the RX FIFO that an SSPDR read pops. SSPSR exposes the FIFO flags TFE/TNF/RNE/RFF/BSY, and the interrupt block combines SSPRIS, SSPIMSC and SSPMIS into SSPINTR. The serial pins SCLK, MOSI, MISO and CS leave on the right. The figure makes the central point visual: the APB side is a pure register interface, and all SPI behaviour lives in the engines those registers configure.

4. Real SoC implementation

The PL022 SSP register map is small and entirely APB-mapped. Here is the real map, then the RTL for the APB decode, the SSPCR0/SSPCR1 field decode, the SSPDR FIFO push/pop, and the SSPSR status read — using real APB signal names.

OffsetRegisterAccessResetPurpose
0x00SSPCR0RW0x0000Control 0 — DSS[3:0] data size (4–16 bits), FRF[5:4] frame format (SPI/TI/Microwire), SPO[6] clock polarity, SPH[7] clock phase, SCR[15:8] serial clock rate
0x04SSPCR1RW0x0000Control 1 — LBM[0] loopback, SSE[1] SSP enable, MS[2] master/slave select, SOD[3] slave-output disable
0x08SSPDRRW0x0000Data — write pushes a word into the TX FIFO, read pops a word from the RX FIFO (same address, two queues)
0x0CSSPSRRO0x0003Status — TFE[0] TX FIFO empty, TNF[1] TX FIFO not full, RNE[2] RX FIFO not empty, RFF[3] RX FIFO full, BSY[4] busy/shifting
0x10SSPCPSRRW0x0000Clock prescale — CPSDVSR[7:0], even value 2–254; first divide stage of the SCLK generator
0x14SSPIMSCRW0x0000Interrupt mask set/clear — enables RORIM/RTIM/RXIM/TXIM
0x18SSPRISRO0x0008Raw interrupt status — the unmasked interrupt conditions
0x1CSSPMISRO0x0000Masked interrupt status — SSPRIS & SSPIMSC, the signal that actually drives SSPINTR
0x20SSPICRWO0x0000Interrupt clear — write-1-to-clear the RORIC/RTIC latched interrupts
0x24SSPDMACRRW0x0000DMA control — RXDMAE[0]/TXDMAE[1] enable the DMA request lines
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// PL022-style SSP behind an APB subordinate. Real APB signals:
//   pclk, presetn, psel, penable, pwrite, paddr[7:0], pwdata[31:0],
//   prdata[31:0], pready.  Single-cycle CSR -> pready tied high.
// --------------------------------------------------------------------
// APB access strobes: a write/read commits in the ACCESS phase only.
wire apb_setup  = psel && !penable;                 // SETUP phase
wire apb_access = psel &&  penable;                 // ACCESS phase
wire wr_en      = apb_access && pwrite;             // qualified write strobe
wire rd_en      = apb_access && !pwrite;            // qualified read  strobe
 
// PADDR[7:2] selects the register (word-aligned map, 0x00..0x24).
wire sel_cr0   = (paddr[7:2] == 6'h00);  // 0x00 SSPCR0
wire sel_cr1   = (paddr[7:2] == 6'h01);  // 0x04 SSPCR1
wire sel_dr    = (paddr[7:2] == 6'h02);  // 0x08 SSPDR  (FIFO port)
wire sel_sr    = (paddr[7:2] == 6'h03);  // 0x0C SSPSR  (read-only)
wire sel_cpsr  = (paddr[7:2] == 6'h04);  // 0x10 SSPCPSR
 
// --------------------------------------------------------------------
// SSPCR0 / SSPCR1 config registers + field decode.
logic [15:0] sspcr0, sspcr1, sspcpsr;
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn) begin
    sspcr0  <= 16'h0000;
    sspcr1  <= 16'h0000;
    sspcpsr <= 16'h0000;
  end else begin
    if (wr_en && sel_cr0)  sspcr0  <= pwdata[15:0];
    if (wr_en && sel_cr1)  sspcr1  <= pwdata[15:0];
    if (wr_en && sel_cpsr) sspcpsr <= pwdata[15:0];
  end
end
 
// Field decode — these wires ARE the configuration the serial engine reads.
wire [3:0] dss      = sspcr0[3:0];      // data size = dss+1 bits (4..16)
wire [1:0] frf      = sspcr0[5:4];      // 00 SPI, 01 TI, 10 Microwire
wire       spo      = sspcr0[6];        // clock polarity: 0 idle low, 1 idle high
wire       sph      = sspcr0[7];        // clock phase:    0 leading edge, 1 trailing
wire [7:0] scr      = sspcr0[15:8];     // serial clock rate divisor
wire       sse      = sspcr1[1];        // SSP enable (master line runs only when set)
wire       ms       = sspcr1[2];        // 0 master, 1 slave
wire [7:0] cpsdvsr  = sspcpsr[7:0];     // even prescale 2..254
// The two-stage divider: SCLK = PCLK / (CPSDVSR * (1 + SCR)).
wire [15:0] sclk_div = cpsdvsr * (16'd1 + {8'd0, scr});
 
// --------------------------------------------------------------------
// SSPDR: a write PUSHES the TX FIFO; a read POPS the RX FIFO.
// Same address, two independent queues — not a flop.
wire tx_push = wr_en && sel_dr;          // candidate push
wire rx_pop  = rd_en && sel_dr;          // candidate pop (has a side effect)
 
// Guarded ports: only push when TNF=1 (not full), only pop when RNE=1 (not empty).
wire tx_do_push = tx_push && tnf;        // overflow guard: drop if FIFO full
wire rx_do_pop  = rx_pop  && rne;        // underflow guard: don't pop if empty
// fifo_tx.push(pwdata[15:0]) on tx_do_push;  fifo_rx data on rx_do_pop;
 
// --------------------------------------------------------------------
// SSPSR status read: bits owned by FIFO occupancy and the shift engine.
wire tfe = tx_empty;                     // [0] TX FIFO empty
wire tnf = !tx_full;                     // [1] TX FIFO not full
wire rne = !rx_empty;                    // [2] RX FIFO not empty
wire rff = rx_full;                      // [3] RX FIFO full
wire bsy = shifting || !tx_empty;        // [4] busy while a frame shifts / TX pending
 
// Read-data mux. SSPDR read returns the popped RX word; SSPSR returns flags.
always_comb begin
  prdata = 32'h0;
  unique case (paddr[7:2])
    6'h00: prdata = {16'h0, sspcr0};
    6'h01: prdata = {16'h0, sspcr1};
    6'h02: prdata = {16'h0, rx_rdata};           // SSPDR read = popped RX FIFO word
    6'h03: prdata = {27'h0, bsy, rff, rne, tnf, tfe};  // SSPSR flags
    6'h04: prdata = {16'h0, sspcpsr};
    default: prdata = 32'h0;
  endcase
end
 
assign pready = 1'b1;   // single-cycle CSR; never inserts a wait state

Three facts make this an SPI controller and not just a register file. First, the config registers are inert storage until the serial engine reads themsspcr0/sspcr1/sspcpsr are plain RW flops, but the field-decode wires (spo, sph, scr, cpsdvsr, sse) are what the shift engine and divider consume, and sclk_div = cpsdvsr * (1 + scr) is the whole point of having two rate registers. Second, SSPDR is a FIFO port with side effects, so its write is a guarded push (tx_push && tnf) and its read is a guarded pop (rx_pop && rne) — pushing into a full FIFO or popping an empty one is the canonical overflow/underflow bug, which is why both ports are gated on the SSPSR flags. Third, SSPSR is hardware-owned statusTFE/TNF/RNE/RFF are pure functions of FIFO occupancy and BSY reflects the shift engine, so software must read SSPSR before every push or pop. The commit and read machinery underneath is exactly the write logic and read-data mux from earlier chapters; this peripheral only adds the domain semantics (divider, modes, FIFO ports) on top.

5. Engineering tradeoffs

The PL022 makes a set of deliberate choices that recur in almost every SPI controller. The table is the design-decision catalog — each row a knob, what it buys, and what it costs.

Design choiceWhat it buysThe cost / constraint
Two-stage divider (CPSDVSR × (1+SCR))A wide rate range with fine granularity from two small fields — CPSDVSR (even 2–254) for the coarse step, SCR (0–255) for the fineYou must factor PCLK/SCLK into the two stages; CPSDVSR must be even, so not every ratio is exactly hittable
SPO/SPH as two separate bitsAll four SPI modes from two orthogonal knobs (idle level, sampling edge), matching any slaveTwo bits, four modes — and the wrong combination silently corrupts data with no bus error
Programmable DSS (4–16 bits)One controller serves devices with 8-, 12-, 16-bit frames without re-spinning hardwareThe shift counter and FIFO width must support the max; partial-word framing complicates the data path
TX/RX FIFOs (depth 8 in PL022)Decouples the fast CPU from the slow wire — software pushes a burst and walks away; absorbs bus/interrupt latencyFIFOs cost area and add the overflow/underflow failure mode that the SSPSR flags exist to prevent
Single data address SSPDR for push and popCompact map — one offset is the queue port for both directionsRead and write at the same address mean different things (pop vs push) with side effects; easy to confuse in drivers
Masked + raw interrupt registers (SSPRIS/SSPMIS)Software sees both the raw condition and the masked (post-enable) interrupt, so it can poll raw or take interruptsExtra registers and the discipline of masking with SSPIMSC and clearing latched ones via SSPICR
Single-cycle CSR (PREADY tied high)Lowest-latency register access; the SPI engine runs in its own clocked domain, not the APB wait pathThe serial transfer is asynchronous to the read of SSPSR — software polls flags, it does not block on the bus

The throughline: the PL022 puts every degree of freedom that a real system needs — rate, mode, frame size, buffering, interrupts — behind small register fields, and pays for that flexibility with the burden of programming it correctly. The two-stage divider trades exact-ratio reach for range; the SPO/SPH split trades a silent-failure mode for full mode coverage; the FIFOs trade area and an overflow hazard for CPU decoupling. None of these is "free," and a driver that ignores the constraints (odd CPSDVSR, wrong mode, unguarded FIFO push) produces a peripheral that passes the APB transaction and fails the SPI link.

6. Common RTL mistakes

7. Debugging scenario

The signature SPI-controller bug is "the bus transaction works, but the serial data is wrong" — a mode mismatch from a swapped SPO/SPH — because it passes every APB-level check and shows up only as corrupted bytes on a logic-analyzer capture of the wire.

  • Observed symptom: firmware configures the controller, pushes bytes into SSPDR, and the link "almost works" — the slave responds, SCLK toggles, MOSI carries data, but every received byte is shifted by one bit or the high/low bits are swapped versus what the slave datasheet says it sent. The APB side is flawless: SSPCR0/SSPCR1 read back exactly what was written, SSPSR shows the FIFOs filling and draining, no PSLVERR, no hang.
  • Waveform clue: a logic-analyzer capture of SCLK and MOSI/MISO shows the controller sampling on the wrong edge. The slave drives a new bit on the falling edge expecting capture on the rising edge (mode 0), but the controller idles SCLK high and captures on the falling edge (mode 3). The data eye is misaligned by half a clock — so each captured bit is the previous bit, and the byte is shifted.
  • Root cause: SSPCR0 was written with SPO/SPH swapped — the driver set SPO=1, SPH=1 (mode 3) when the slave device required SPO=0, SPH=0 (mode 0). On the APB side this is a perfectly legal write to a legal field; nothing rejects it. The serial engine faithfully implements mode 3, and the slave faithfully implements mode 0, so the two clock the wire on opposite conventions and the data is corrupted with no error anywhere.
  • Correct fix: set SSPCR0.SPO and SSPCR0.SPH to the mode the slave device specifies, not a guessed default. For a mode-0 device, SPO=0 (idle low) and SPH=0 (sample leading edge): sspcr0[6] = 1'b0; sspcr0[7] = 1'b0;. Confirm against the slave's datasheet timing diagram — which SCLK edge it drives data on and which it expects sampling on — and pick the matching (SPO, SPH) pair.
  • Verification assertion: prove the engine's sampling edge matches the configured mode, and that the configured fields actually reach the shift logic:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // The shift engine must capture MISO on the edge SPO/SPH select, never the other.
    // (mode 0: SPO=0,SPH=0 -> sample on rising SCLK)
    assert property (@(posedge pclk) disable iff (!presetn)
      (sse && !spo && !sph && sclk_rising) |-> miso_sample_en);
    assert property (@(posedge pclk) disable iff (!presetn)
      (sse && !spo && !sph && sclk_falling) |-> !miso_sample_en);
  • Debug habit: when the bus is clean but the serial data is wrong, do not chase the APB path — read back SSPCR0 and decode SPO/SPH against the slave's required mode first. A capture of SCLK versus the data lines that shows sampling on the wrong edge is the fingerprint of a mode mismatch, and the fix is one or two bits in one register. "Works on the bus, wrong on the wire" almost always means the mode bits, the data size (DSS), or the bit order — check the mode first because it is the most common and the most silent.
An APB waveform over eight cycles showing PCLK, PSEL, PENABLE, PWRITE, PADDR and PWDATA for four back-to-back writes — SSPCPSR with CPSDVSR equals 2, SSPCR0 with DSS, SPO, SPH and SCR, SSPCR1 with SSE equals 1, and SSPDR with a TX byte — each completing on a marked commit edge, with an annotation deriving SCLK equals PCLK over four.
Figure 2 — the APB write sequence that brings up the SPI controller, over roughly eight bus cycles. Each access is the standard SETUP-then-ACCESS pair, and the dashed lines mark the commit edges where the register actually updates. Write 1 sets SSPCPSR with CPSDVSR=2 (the even prescale). Write 2 sets SSPCR0 with DSS=7 for an 8-bit frame, SPO=0 and SPH=0 for SPI mode 0, and SCR=1. Write 3 sets SSPCR1.SSE=1 to enable the controller. Write 4 pushes a data byte into SSPDR, loading the TX FIFO and starting the frame. The annotation shows the divider falling straight out of the writes: SCLK = PCLK/(CPSDVSR×(1+SCR)) = PCLK/(2×2) = PCLK/4, and warns that enabling before the prescale and mode are set, or before SSPDR is filled, produces no SCLK — the classic 'TX never starts' bug. The figure makes the ordering discipline concrete: configure rate and mode, enable, then fill the FIFO.

8. Verification perspective

Because the SPI controller is an APB register file plus a serial engine, its verification splits the same way: register-access checks on the CSR side (the substrate), and behavioural/safety checks on the engine side (the domain). The high-value properties guard the two things software can get wrong — overflowing a FIFO and reading stale status — and the two things the hardware must honor — the divider and the mode.

  • FIFO overflow/underflow guard (safety). The flagship property: a write to SSPDR when TNF=0 (FIFO full) must not push, and a read when RNE=0 (empty) must not pop. This is the assertion that catches an unguarded port — the most damaging FIFO bug, because an overflow silently corrupts the queue:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // A write to SSPDR while the TX FIFO is full must not push (overflow guard).
    assert property (@(posedge pclk) disable iff (!presetn)
      (wr_en && sel_dr && !tnf) |-> !tx_do_push);
    // A read of SSPDR while the RX FIFO is empty must not pop (underflow guard).
    assert property (@(posedge pclk) disable iff (!presetn)
      (rd_en && sel_dr && !rne) |-> !rx_do_pop);
  • BSY reflects the shift engine (status correctness). SSPSR.BSY must be high while a frame is shifting and low when the controller is idle with an empty TX FIFO — software relies on it to know when a transfer truly completed before, e.g., deasserting CS:
    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // BSY must be asserted whenever the shift engine is actively shifting a frame.
    assert property (@(posedge pclk) disable iff (!presetn)
      shifting |-> bsy);
    // And BSY must drop once idle with nothing left to send.
    assert property (@(posedge pclk) disable iff (!presetn)
      (!shifting && tx_empty) |-> !bsy);
  • Divider and mode reachability (functional coverage). Cover the programming corners that real bugs live in: CPSDVSR even-only (an odd value rejected or clamped), the full SCLK = PCLK/(CPSDVSR×(1+SCR)) range at the min/max of each field, and all four (SPO, SPH) mode combinations driving the shift engine. A directed test that only exercises mode 0 with one divider proves almost nothing — the mode-mismatch and wrong-rate bugs hide in the corners the happy path never visits.
  • Register-access conformance (substrate). Reuse the standard APB checks — RW registers read back what was written (SSPCR0/SSPCR1/SSPCPSR), RO registers ignore writes (SSPSR/SSPRIS/SSPMIS), WO registers read as defined (SSPICR/SSPDR-write side), and PREADY completes every access — exactly the APB assertions you already build for any register bank.

The point: verify the substrate with the generic APB register conformance you already own, then add the domain properties this peripheral introduces — FIFO push/pop guards, BSY correctness, and full mode/divider coverage. A test that proves the registers read back correctly but never overflows the TX FIFO, never checks BSY against the shift engine, and never sweeps the four modes has verified the easy half and shipped the hard half.

9. Interview discussion

"Walk me through an SPI controller on an APB bus" is a favourite system-integration question because it forces you to connect a bus protocol you know (APB) to a serial protocol you know (SPI) through a register map — and the strong answers reveal the two non-obvious relationships (modes and the divider) that only someone who has actually programmed one carries.

Lead with the decomposition: an SPI controller is an APB register file plus a serial engine — the APB side decodes accesses into the SSP register map, and all SPI behaviour lives in the engines those registers configure. Then name the real map crisply: SSPCR0 (data size DSS, frame format FRF, polarity SPO, phase SPH, rate SCR), SSPCR1 (SSE enable, MS master/slave), SSPDR (the FIFO port — write pushes TX, read pops RX), SSPSR (status flags TFE/TNF/RNE/RFF/BSY), SSPCPSR (the prescale), and the interrupt registers. Deliver the two depth points that separate spec-readers from builders: the four SPI modes come from two bitsSPO (clock idle level) and SPH (sampling edge) — and a swapped pair corrupts every byte with no bus error; and the serial clock is a two-stage divide, SCLK = PCLK / (CPSDVSR × (1 + SCR)), so hitting a target rate means factoring PCLK/SCLK into an even CPSDVSR and an SCR. Land the systems insight: SSPDR is a FIFO port, not a flop (write pushes, read pops, same address, side effects), so software must check SSPSR before every access; and enabling alone does not start a transfer — the master only drives SCLK when SSE is set and the TX FIFO is non-empty. Closing with "and the classic field bug is a mode mismatch — the APB transaction is flawless and the serial data is shifted by a bit, so you debug it on a logic analyzer, not on the bus" signals you have actually shipped one.

10. Practice

  1. Compute the divider. PCLK is 50 MHz and you need SCLK ≈ 1 MHz. Pick a legal CPSDVSR (even, 2–254) and SCR (0–255) so that PCLK/(CPSDVSR×(1+SCR)) is as close to 1 MHz as possible, and state the exact resulting SCLK.
  2. Map the modes. Fill in the table: for each SPI mode 0–3, give (SPO, SPH), the SCLK idle level, and which edge (leading/trailing) the data is sampled on. Then state what happens on the wire when a mode-0 controller talks to a mode-3 slave.
  3. Order the bring-up. Write the exact sequence of APB writes (register, value, why) to configure an 8-bit, mode-0, 1 MHz master and send one byte — and explain why setting SSPCR1.SSE first would be wrong.
  4. Guard the FIFO. Given wr_en && sel_dr as the raw SSPDR write strobe, write the push enable that prevents a TX-FIFO overflow, and the SVA that proves a write while TNF=0 never pushes. State which SSPSR bit each guards.
  5. Decode a wrong byte. A capture shows every received byte shifted right by one bit with no bus error. List the three SSPCR0 fields you would check first and the order you would check them in, and explain why the bus being clean rules out an APB-side fault.

11. Q&A

12. Key takeaways

  • An SPI controller on APB is an APB register file plus a serial engine. The APB side decodes accesses into the PL022 SSP register map; all SPI behaviour (divider, FIFOs, shift register, interrupts) lives in the engines those registers configure. "Program the peripheral" means "write the right registers in the right order, then push data."
  • The four SPI modes come from two bits. SSPCR0.SPO sets the SCLK idle level and SSPCR0.SPH sets the sampling edge; the four (SPO, SPH) combinations are the four SPI modes. A swapped pair corrupts every byte with no bus error — the signature "clean on the bus, wrong on the wire" field bug.
  • The serial clock is a two-stage divide: SCLK = PCLK / (CPSDVSR × (1 + SCR)), with CPSDVSR an even prescale (2–254) in SSPCPSR and SCR the rate (0–255) in SSPCR0. Hitting a target rate means factoring PCLK/SCLK into the two fields — not setting one divider.
  • SSPDR is a FIFO port, not a flop. A write pushes the TX FIFO; a read pops the RX FIFO; same address, two queues, side effects. Software must check SSPSR (TNF before push, RNE before pop), and the hardware should guard both ports so an overflow or underflow is structurally impossible.
  • SSPSR is hardware-owned status (TFE/TNF/RNE/RFF/BSY), driven by FIFO occupancy and the shift engine. Enabling with SSPCR1.SSE is necessary but not sufficient — the master only drives SCLK when enabled and the TX FIFO is non-empty, which is why "enabled but nothing transmits" is almost always an empty FIFO.
  • Verify the substrate and the domain separately: reuse the generic APB register conformance for read-back / read-only / PREADY, then add the domain properties — FIFO overflow/underflow guards, BSY-reflects-shifting, and full mode-and-divider coverage. The hard half of the peripheral is the engine, not the register file.