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?
SCLKis some division of the bus clockPCLK. 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 targetSCLK. - What does an
SCLKedge 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/W1Cregisters and aPADDRdecoder. A real SPI controller has a specific set of registers with specific fields (DSS,FRF,SPO,SPH,SCR,CPSDVSR, the FIFOSSPDR), and their semantics — especially that two bits pick one of four SPI modes — are not implied by "it's anRWregister." 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.
SSPSRis 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
SSPDRis a FIFO port, not a register. A write toSSPDRdoes not overwrite a flop; it pushes into the TX FIFO. A read ofSSPDRdoes 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
SCLKorMOSIdirectly. It writesSSPCPSRandSSPCR0to set the rate and mode, writesSSPCR1.SSEto start the line, pushes words intoSSPDR(the TX FIFO port), and readsSSPDR(the RX FIFO port) andSSPSR(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/SPHselect one of four SPI modes — clock idle level and which edge samples.SPO(clock polarity) sets whetherSCLKidles 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.CPSDVSRis an even prescale divisor (2–254);SSPCR0.SCRis an 8-bit rate (0–255) that further divides by(1 + SCR). To hit a targetSCLKyou chooseCPSDVSRandSCRso their product equalsPCLK / 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.
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.
| Offset | Register | Access | Reset | Purpose |
|---|---|---|---|---|
0x00 | SSPCR0 | RW | 0x0000 | Control 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 |
0x04 | SSPCR1 | RW | 0x0000 | Control 1 — LBM[0] loopback, SSE[1] SSP enable, MS[2] master/slave select, SOD[3] slave-output disable |
0x08 | SSPDR | RW | 0x0000 | Data — write pushes a word into the TX FIFO, read pops a word from the RX FIFO (same address, two queues) |
0x0C | SSPSR | RO | 0x0003 | Status — 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 |
0x10 | SSPCPSR | RW | 0x0000 | Clock prescale — CPSDVSR[7:0], even value 2–254; first divide stage of the SCLK generator |
0x14 | SSPIMSC | RW | 0x0000 | Interrupt mask set/clear — enables RORIM/RTIM/RXIM/TXIM |
0x18 | SSPRIS | RO | 0x0008 | Raw interrupt status — the unmasked interrupt conditions |
0x1C | SSPMIS | RO | 0x0000 | Masked interrupt status — SSPRIS & SSPIMSC, the signal that actually drives SSPINTR |
0x20 | SSPICR | WO | 0x0000 | Interrupt clear — write-1-to-clear the RORIC/RTIC latched interrupts |
0x24 | SSPDMACR | RW | 0x0000 | DMA control — RXDMAE[0]/TXDMAE[1] enable the DMA request lines |
// 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 stateThree facts make this an SPI controller and not just a register file. First, the config registers are inert storage until the serial engine reads them — sspcr0/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 status — TFE/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 choice | What it buys | The 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 fine | You must factor PCLK/SCLK into the two stages; CPSDVSR must be even, so not every ratio is exactly hittable |
SPO/SPH as two separate bits | All four SPI modes from two orthogonal knobs (idle level, sampling edge), matching any slave | Two 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 hardware | The 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 latency | FIFOs cost area and add the overflow/underflow failure mode that the SSPSR flags exist to prevent |
Single data address SSPDR for push and pop | Compact map — one offset is the queue port for both directions | Read 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 interrupts | Extra 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 path | The 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,SCLKtoggles,MOSIcarries 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/SSPCR1read back exactly what was written,SSPSRshows the FIFOs filling and draining, noPSLVERR, no hang. - Waveform clue: a logic-analyzer capture of
SCLKandMOSI/MISOshows 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 idlesSCLKhigh 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:
SSPCR0was written withSPO/SPHswapped — the driver setSPO=1, SPH=1(mode 3) when the slave device requiredSPO=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.SPOandSSPCR0.SPHto the mode the slave device specifies, not a guessed default. For a mode-0 device,SPO=0(idle low) andSPH=0(sample leading edge):sspcr0[6] = 1'b0; sspcr0[7] = 1'b0;. Confirm against the slave's datasheet timing diagram — whichSCLKedge 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 MentorSnippet
// 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
SSPCR0and decodeSPO/SPHagainst the slave's required mode first. A capture ofSCLKversus 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.
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
SSPDRwhenTNF=0(FIFO full) must not push, and a read whenRNE=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 MentorSnippet// 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); BSYreflects the shift engine (status correctness).SSPSR.BSYmust 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., deassertingCS:Azvya Education Pvt. Ltd.VLSI MentorSnippet// 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:
CPSDVSReven-only (an odd value rejected or clamped), the fullSCLK = 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 —
RWregisters read back what was written (SSPCR0/SSPCR1/SSPCPSR),ROregisters ignore writes (SSPSR/SSPRIS/SSPMIS),WOregisters read as defined (SSPICR/SSPDR-write side), andPREADYcompletes 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 bits — SPO (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
- Compute the divider.
PCLKis 50 MHz and you needSCLK≈ 1 MHz. Pick a legalCPSDVSR(even, 2–254) andSCR(0–255) so thatPCLK/(CPSDVSR×(1+SCR))is as close to 1 MHz as possible, and state the exact resultingSCLK. - Map the modes. Fill in the table: for each SPI mode 0–3, give
(SPO, SPH), theSCLKidle 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. - 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.SSEfirst would be wrong. - Guard the FIFO. Given
wr_en && sel_dras the rawSSPDRwrite strobe, write the push enable that prevents a TX-FIFO overflow, and the SVA that proves a write whileTNF=0never pushes. State whichSSPSRbit each guards. - Decode a wrong byte. A capture shows every received byte shifted right by one bit with no bus error. List the three
SSPCR0fields 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.SPOsets theSCLKidle level andSSPCR0.SPHsets 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)), withCPSDVSRan even prescale (2–254) inSSPCPSRandSCRthe rate (0–255) inSSPCR0. Hitting a target rate means factoringPCLK/SCLKinto the two fields — not setting one divider. SSPDRis 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 checkSSPSR(TNFbefore push,RNEbefore pop), and the hardware should guard both ports so an overflow or underflow is structurally impossible.SSPSRis hardware-owned status (TFE/TNF/RNE/RFF/BSY), driven by FIFO occupancy and the shift engine. Enabling withSSPCR1.SSEis necessary but not sufficient — the master only drivesSCLKwhen 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.