SPI · Module 14
CPOL/CPHA Handling in a Slave
Three mismatches with three observabilities: polarity from a level before a bit moves, phase from a motion and provably not from the edge count, bit order not at all. Includes the wrong answer this chapter shipped first, why it was wrong, and mode logic verified in three HDLs against both a combinational and a registered master output.
Chapter 14.1 already consumed CPOL. Its edge strobes mean "SCLK left its idle level" and "SCLK returned to idle" rather than rising and falling, so every block downstream sees the same strobe sequence in both polarities.
That leaves this chapter with one configuration bit for the mapping — and with the much harder question of what happens when the master's configuration and the slave's disagree.
A master and a slave are configured with different CPHA. What does the slave observe?
This chapter shipped the wrong answer to that question first. The wrong answer is more plausible than the right one, it is what most references imply, and the design built on it would have reported nothing at all. The correction is left visible here rather than tidied away, because the reasoning that exposed it is the most useful thing in the chapter.
1. The Mapping, Which Is The Easy Half
With CPOL already consumed, the whole mode table is two rows:
CPHA = 0 capture on every LEADING edge; preload once at the start
CPHA = 1 capture on every TRAILING edge; never preloadAnd the launch is the other one:
cap_stb = txn_active & (cpha ? edge_b_stb : edge_a_stb)
launch_stb = txn_active & (cpha ? edge_a_stb : edge_b_stb)Two expressions, and CPOL appears in neither. That is the payoff from Chapter 14.1's decision: inverting CPOL on both sides leaves the strobe counts identical, which the testbench checks directly, so a four-row table became a two-row one and two of the four modes need no logic of their own.
The preload deserves one note, because it is the source of Chapter 14.4's lead requirement. Its trigger is txn_start_stb — the transaction boundary — rather than a command, because the slave has no command. That is what makes the lead SYNC_N + 2 rather than SYNC_N + 1.
2. Polarity: Detectable From A Level
The polarity check is the easy one, and it is the only mismatch in the module that is caught before a single bit has moved:
at cs_assert_stb: if sclk_q != cpol -> cpol_mismatchSCLK must be at its configured idle level when chip select asserts, because idle is what idle means. If it is not, the master's CPOL differs from this slave's.
Two things make this check trustworthy, and both are properties of Chapter 14.1's front end rather than of this block:
Both signals come through the same synchroniser depth. So comparing sclk_q against cpol at cs_assert_stb compares two values that were contemporaneous at the pins. A design with mismatched depths would be comparing SCLK at one moment against a select from another, and the check would fire spuriously at tight ratios.
The check is at the assert, not continuous. This is deliberate and it matters:
3. Phase: The Wrong Answer, And Why It Is Wrong
Here is the argument this chapter originally shipped:
A CPHA=1 master captures on trailing edges, so it deasserts promptly after its own last capture — which is a trailing edge. A CPHA=0 slave is still waiting for the leading edge that would complete its Nth bit. Every transaction therefore ends one bit short, and a run of truncated transactions diagnoses the mismatch.
It is wrong. The arithmetic takes one line:
a CPHA=1 master sending N bits produces 2N edges
a CPHA=0 slave captures on leading edges;
there are exactly N of them in 2N N bits capturedA whole number of frames, on every transaction, in both phases. Chapter 14.2's edge count agrees. txn_clean fires. Truncation never happens.
So the design built on that argument would have reported nothing — and worse, would have actively asserted txn_clean, telling the integrator the transaction was fine.
4. Phase: The Right Answer, Which Is Physical
What a phase mismatch actually does is not arithmetical, it is physical:
The slave captures on the edge the master launches on.
So MOSI is changing at the instant the slave samples it. That is not a statistic. It is an observation the slave can make directly:
a sample at which mosi_q differs from its value one cycle earlier
means MOSI moved within one cycle of the sampleAnd the margin is provable. A matched master launches half a period from this slave's capture, and MOSI is therefore stable for at least HALF_MIN - 1 cycles before the sample. At HALF_MIN = 3 that is 2 cycles — outside a one-cycle window, by exactly one cycle.
So for a matched pair the count of motion events is exactly zero, not merely small. That is what makes the detector exact rather than statistical, and it is why the threshold of three exists only to ignore a one-off glitch rather than to separate two distributions.
The window, and the second thing integration found
The first implementation of the right answer tested for exact coincidence: MOSI changing on the very cycle of the capture. Against the unit testbench it worked perfectly — six motion events in a mismatched transaction, zero in a matched one.
Against a real master it found nothing, and Chapter 14.10 is where that was measured. The reason is that a real master registers its MOSI output, so the bit appears one cycle after the edge that launched it. A mismatched master therefore does not change MOSI at the slave's sample — it changes it one cycle later.
measured against Chapter 13's master, at every divisor:
matched pair MOSI settles 3 cycles before the capture
mismatched pair MOSI changes 1 cycle AFTER the captureSo the test is a window rather than a coincidence:
in_motion = (cap_stb & (mosi_chg | mosi_chg_d)) // at, or just before
| (cap_stb_d & mosi_chg) // just afterOne cycle either side. The window is one cycle wide because that is the widest window the legal minimum half-period still clears — HALF_MIN - 1 = 2 is outside it, with nothing to spare. Three registers, and between them they make the test.
The unit bench now drives both output lags — zero and one — for the matched and mismatched cases. A detector that handled only the exact-coincidence case passes a bench without that parameter and finds nothing on a bus.
5. Bit Order: Not Detectable, And That Is The Answer
Both orders produce a well-formed word of the right length at the right time. There is no observation the slave can make that distinguishes them: no parity, no delimiter, no reserved value, no redundancy anywhere in SPI.
So there is no flag, and the chapter argues that is correct rather than a gap. A detector that guessed from the data — "this looks like reversed ASCII" — would be right often enough to be trusted and wrong often enough to mislead, which is worse than an honest silence.
The three mismatches together:
mismatch observable as when it is caught
----------- --------------- --------------------------------------
POLARITY a LEVEL at the assert, before any bit moves
PHASE a MOTION within one transaction, once the data
has a transition in it
BIT ORDER nothing neverLevel, relationship, ordering — decreasing observability, and it is not a coincidence. Each one has less to be compared against than the last: a level against a constant, a relationship against the signal's own recent history, and an ordering against nothing at all.
The phase detector's one honest limitation belongs here too: the observation needs a transition to make. A data pattern of all zeros or all ones is sampled at the wrong moment and looks fine, so the diagnosis appears once the data has any variety in it. trunc_run is still published by this block, but for aborts (Chapter 14.7) rather than for the phase.
6. The State Diagram
7. Building the Mode Logic — Three HDLs
The circuit
Two combinational expressions for the strobes, a level comparison at the assert, and a three-register window that counts samples with MOSI in motion. The truncation run is still maintained, for aborts rather than for the phase.
// spi_slave_mode.sv
//
// Chapter 14.6 -- all four modes in a slave, and which mismatches it can see.
//
// The mapping is Chapter 13.5's, unchanged, and for the same reason: the front end
// of 14.1 names its edges against the configured idle level, so CPOL is already
// consumed and the assignment depends on CPHA alone.
//
// CPHA = 0 capture on the LEADING edge, launch on the TRAILING edge
// CPHA = 1 launch on the LEADING edge, capture on the TRAILING edge
//
// Two rows, not four, and a slave that cases on {cpol, cpha} has the same twice-the-
// logic problem a master would.
//
// WHERE THE SLAVE DIFFERS FROM THE MASTER.
//
// The master's CPHA=0 preload is triggered by its own start command. The slave's is
// triggered by the transaction boundary, which is Chapter 14.2's registered strobe --
// so the preload inherits the recovery latency, and that is the whole of Chapter
// 14.4's lead requirement. Same mechanism, different trigger, and the difference is
// a number the master has to respect.
//
// The master also suppresses its final launch. The slave cannot: it does not know
// which launch is the last, because a trailing edge is indistinguishable from the
// start of another word (Chapter 14.4). So the two blocks look alike and differ in
// both directions.
//
// WHAT A MISCONFIGURED SLAVE CAN DETECT -- AND THE POINT OF DETECTING IT.
//
// A slave cannot renegotiate. There is no mechanism in SPI by which it could tell
// the master it is in the wrong mode, and no mode it could fall back to. So
// detection is purely DIAGNOSTIC, and that is not a reason to skip it: the three
// mismatches produce indistinguishable symptoms at the system level -- plausible
// wrong data -- and a device that can say which one it is turns a week of bisecting
// a driver into a register read.
//
// POLARITY. DETECTABLE, IMMEDIATELY. SCLK must be at the configured idle level
// when chip select asserts, because idle is what idle means. If it is
// not, the master's CPOL differs from this slave's -- and this is the
// one mismatch that is caught before a single bit has moved.
//
// PHASE. DETECTABLE, AND NOT BY COUNTING EDGES. This is worth getting right
// because the plausible answer is wrong.
//
// The tempting argument is that a phase mismatch truncates: the master
// deasserts after ITS last capture, so the slave is left waiting for one
// more edge. It does not. A CPHA=1 master sending N bits produces 2N
// edges, and a CPHA=0 slave takes the N leading ones -- a whole number of
// frames, on every transaction. The edge count AGREES, and truncation
// never happens. A design that relied on it would report nothing at all.
//
// What a phase mismatch actually does is physical: the slave captures on
// the edge the master LAUNCHES on. MOSI is changing at the instant the
// slave samples it -- which is not a statistic, it is an observation the
// slave can make directly.
//
// The observation has to be a WINDOW rather than a coincidence, and the
// reason is worth stating because it is the kind of thing only
// integration finds. A real master registers its MOSI output, so the bit
// appears one cycle AFTER the edge that launched it. A mismatched master
// therefore does not change MOSI exactly at this slave's sample -- it
// changes it one cycle later. Testing for exact coincidence finds
// nothing, and the chapter that claimed it would have been wrong for the
// second time.
//
// So the test is:
//
// MOSI moved within one cycle EITHER SIDE of the sample
//
// and the margin is provable. A matched master launches half a period
// from this slave's capture, and with its registered output the bit
// lands HALF_MIN - 1 cycles before the sample at the very worst. With
// HALF_MIN = 3 that is 2 -- outside a one-cycle window, by exactly one
// cycle. The window is one cycle wide because that is the widest window
// the legal minimum half-period still clears.
//
// For a matched pair the count is therefore exactly ZERO, not merely
// small. The threshold of three exists only to ignore a one-off glitch.
//
// Its one limitation, stated because it is real: the observation needs a
// TRANSITION to make. A data pattern with no transitions at all -- all
// zeros, all ones -- is sampled at the wrong moment and looks fine, so the
// diagnosis appears once the data has any variety in it. `trunc_run` is
// still published, but for aborts (Chapter 14.7) rather than for this.
//
// BIT ORDER. NOT DETECTABLE. Both orders produce a well-formed word of the right
// length at the right time. There is no observation available, so
// there is no flag -- and inventing one that guessed from the data
// would be worse than none.
//
// The asymmetry is worth carrying. The polarity mismatch is a LEVEL, comparable
// against a constant, and it is caught before a bit has moved. The phase mismatch is
// a TIMING RELATIONSHIP, comparable against the signal's own recent history, and it is
// caught within one transaction once the data has a transition in it. The order
// mismatch is an ORDERING, comparable against nothing, and it is never caught.
//
// Level, relationship, ordering -- decreasing observability, and not a coincidence:
// each one has less to be compared against than the last, and the third has nothing.
module spi_slave_mode #(
parameter int SUSPECT_N = 3, // captures in motion that mean a mismatch
parameter int CNT_W = 4
) (
input wire clk,
input wire rst_n,
// --- configuration ----------------------------------------------------
input wire cpol,
input wire cpha,
// --- from the front end of 14.1 ---------------------------------------
input wire sclk_q,
input wire mosi_q, // delay-matched, from 14.1
input wire cs_assert_stb,
input wire edge_a_stb,
input wire edge_b_stb,
// --- from the transaction detector of 14.2 ----------------------------
input wire txn_active,
input wire txn_start_stb,
// The REPORT strobe, not the END strobe. `txn_end_stb` fires a cycle before
// the classification is assigned, so sampling the verdict there reads the
// PREVIOUS transaction's -- and the symptom is a diagnosis that is always one
// transaction behind, which looks like a threshold that is off by one.
input wire txn_report_stb,
input wire txn_clean,
input wire txn_trunc,
// --- the strobes the datapath uses ------------------------------------
output wire cap_stb,
output wire launch_stb,
output wire preload_stb,
// --- diagnosis ---------------------------------------------------------
output reg cpol_mismatch, // sticky: SCLK not idle at assert
output reg phase_suspect, // sticky: MOSI moving at the sample
output reg [CNT_W-1:0] moved_run, // captures with MOSI in motion
output reg [CNT_W-1:0] trunc_run, // consecutive truncations (for aborts)
input wire clr_flags
);
// THE mapping. Two terms, and CPOL appears in neither.
assign cap_stb = txn_active & (cpha ? edge_b_stb : edge_a_stb);
assign launch_stb = txn_active & (cpha ? edge_a_stb : edge_b_stb);
// The preload's trigger is the transaction boundary rather than a command,
// which is what makes Chapter 14.4's lead requirement SYNC_N + 2.
assign preload_stb = txn_start_stb & ~cpha;
// MOSI one cycle back, the change one cycle back, and the capture one cycle
// back. Three registers, and between them they make the one-cycle window: a
// change at the sample, one cycle before it, or one cycle after it.
reg mosi_q_d;
reg mosi_chg_d;
reg cap_stb_d;
wire mosi_chg = (mosi_q != mosi_q_d);
wire in_motion = (cap_stb & (mosi_chg | mosi_chg_d)) // at, or just before
| (cap_stb_d & mosi_chg); // just after
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cpol_mismatch <= 1'b0;
phase_suspect <= 1'b0;
moved_run <= {CNT_W{1'b0}};
trunc_run <= {CNT_W{1'b0}};
mosi_q_d <= 1'b0;
mosi_chg_d <= 1'b0;
cap_stb_d <= 1'b0;
end else begin
mosi_q_d <= mosi_q;
mosi_chg_d <= mosi_chg;
cap_stb_d <= cap_stb;
if (clr_flags) begin
cpol_mismatch <= 1'b0;
phase_suspect <= 1'b0;
moved_run <= {CNT_W{1'b0}};
trunc_run <= {CNT_W{1'b0}};
end
// --- the phase diagnosis ---------------------------------------
// A sample with MOSI in motion within one cycle of it. For a matched pair
// this count is exactly zero, because MOSI settles at least HALF_MIN - 1
// cycles before the sample and HALF_MIN - 1 is at least 2.
if (in_motion) begin
if (moved_run != {CNT_W{1'b1}})
moved_run <= moved_run + 1'b1;
if (moved_run >= (SUSPECT_N - 1))
phase_suspect <= 1'b1;
end
// --- the polarity check ----------------------------------------
// Checked at the ASSERT, against the synchronised SCLK. Both signals
// come through the same front end with the same latency, so the
// comparison is of two values that were contemporaneous at the pins --
// which is the whole reason it can be trusted.
//
// It is deliberately NOT checked continuously. Between the last edge
// and the deassert, SCLK sits at whatever the master left it at, and
// on an abort (Chapter 14.7) that may legitimately not be the idle
// level. A continuous check would fire on every aborted transaction
// and be switched off.
if (cs_assert_stb && (sclk_q != cpol))
cpol_mismatch <= 1'b1;
// --- the truncation run ----------------------------------------
// Still published, but for ABORTS rather than for the phase (Chapter
// 14.7 consumes it). The counter saturates rather than wrapping: at
// CNT_W = 4 a wrap after sixteen consecutive truncations would take the
// run back below any threshold a consumer had set on it.
if (txn_report_stb) begin
if (txn_trunc) begin
if (trunc_run != {CNT_W{1'b1}})
trunc_run <= trunc_run + 1'b1;
end else if (txn_clean) begin
// A clean transaction resets the run. Neither latched flag
// clears with it: a driver that happened to send one
// whole-frame transaction should not erase the record of
// twenty broken ones.
trunc_run <= {CNT_W{1'b0}};
end
end
end
end
`ifdef SPI_CHECKS
always_ff @(posedge clk) if (rst_n) begin
if (cap_stb && launch_stb)
$fatal(1, "capture and launch on the same cycle");
if (preload_stb && cpha)
$fatal(1, "a preload in CPHA=1");
end
`endif
endmodule// spi_slave_mode.v
//
// Chapter 14.6 -- all four modes in a slave, and which mismatches it can see.
//
// The mapping is Chapter 13.5's, unchanged, and for the same reason: the front end
// of 14.1 names its edges against the configured idle level, so CPOL is already
// consumed and the assignment depends on CPHA alone.
//
// CPHA = 0 capture on the LEADING edge, launch on the TRAILING edge
// CPHA = 1 launch on the LEADING edge, capture on the TRAILING edge
//
// Two rows, not four, and a slave that cases on {cpol, cpha} has the same twice-the-
// logic problem a master would.
//
// WHERE THE SLAVE DIFFERS FROM THE MASTER.
//
// The master's CPHA=0 preload is triggered by its own start command. The slave's is
// triggered by the transaction boundary, which is Chapter 14.2's registered strobe --
// so the preload inherits the recovery latency, and that is the whole of Chapter
// 14.4's lead requirement. Same mechanism, different trigger, and the difference is
// a number the master has to respect.
//
// The master also suppresses its final launch. The slave cannot: it does not know
// which launch is the last, because a trailing edge is indistinguishable from the
// start of another word (Chapter 14.4). So the two blocks look alike and differ in
// both directions.
//
// WHAT A MISCONFIGURED SLAVE CAN DETECT -- AND THE POINT OF DETECTING IT.
//
// A slave cannot renegotiate. There is no mechanism in SPI by which it could tell
// the master it is in the wrong mode, and no mode it could fall back to. So
// detection is purely DIAGNOSTIC, and that is not a reason to skip it: the three
// mismatches produce indistinguishable symptoms at the system level -- plausible
// wrong data -- and a device that can say which one it is turns a week of bisecting
// a driver into a register read.
//
// POLARITY. DETECTABLE, IMMEDIATELY. SCLK must be at the configured idle level
// when chip select asserts, because idle is what idle means. If it is
// not, the master's CPOL differs from this slave's -- and this is the
// one mismatch that is caught before a single bit has moved.
//
// PHASE. DETECTABLE, AND NOT BY COUNTING EDGES. This is worth getting right
// because the plausible answer is wrong.
//
// The tempting argument is that a phase mismatch truncates: the master
// deasserts after ITS last capture, so the slave is left waiting for one
// more edge. It does not. A CPHA=1 master sending N bits produces 2N
// edges, and a CPHA=0 slave takes the N leading ones -- a whole number of
// frames, on every transaction. The edge count AGREES, and truncation
// never happens. A design that relied on it would report nothing at all.
//
// What a phase mismatch actually does is physical: the slave captures on
// the edge the master LAUNCHES on. MOSI is changing at the instant the
// slave samples it -- which is not a statistic, it is an observation the
// slave can make directly.
//
// The observation has to be a WINDOW rather than a coincidence, and the
// reason is worth stating because it is the kind of thing only
// integration finds. A real master registers its MOSI output, so the bit
// appears one cycle AFTER the edge that launched it. A mismatched master
// therefore does not change MOSI exactly at this slave's sample -- it
// changes it one cycle later. Testing for exact coincidence finds
// nothing, and the chapter that claimed it would have been wrong for the
// second time.
//
// So the test is:
//
// MOSI moved within one cycle EITHER SIDE of the sample
//
// and the margin is provable. A matched master launches half a period
// from this slave's capture, and with its registered output the bit
// lands HALF_MIN - 1 cycles before the sample at the very worst. With
// HALF_MIN = 3 that is 2 -- outside a one-cycle window, by exactly one
// cycle. The window is one cycle wide because that is the widest window
// the legal minimum half-period still clears.
//
// For a matched pair the count is therefore exactly ZERO, not merely
// small. The threshold of three exists only to ignore a one-off glitch.
//
// Its one limitation, stated because it is real: the observation needs a
// TRANSITION to make. A data pattern with no transitions at all -- all
// zeros, all ones -- is sampled at the wrong moment and looks fine, so the
// diagnosis appears once the data has any variety in it. `trunc_run` is
// still published, but for aborts (Chapter 14.7) rather than for this.
//
// BIT ORDER. NOT DETECTABLE. Both orders produce a well-formed word of the right
// length at the right time. There is no observation available, so
// there is no flag -- and inventing one that guessed from the data
// would be worse than none.
//
// The asymmetry is worth carrying. The polarity mismatch is a LEVEL, comparable
// against a constant, and it is caught before a bit has moved. The phase mismatch is
// a TIMING RELATIONSHIP, comparable against the signal's own recent history, and it is
// caught within one transaction once the data has a transition in it. The order
// mismatch is an ORDERING, comparable against nothing, and it is never caught.
//
// Level, relationship, ordering -- decreasing observability, and not a coincidence:
// each one has less to be compared against than the last, and the third has nothing.
module spi_slave_mode #(
parameter SUSPECT_N = 3, // captures in motion that mean a mismatch
parameter CNT_W = 4
) (
input wire clk,
input wire rst_n,
// --- configuration ----------------------------------------------------
input wire cpol,
input wire cpha,
// --- from the front end of 14.1 ---------------------------------------
input wire sclk_q,
input wire mosi_q, // delay-matched, from 14.1
input wire cs_assert_stb,
input wire edge_a_stb,
input wire edge_b_stb,
// --- from the transaction detector of 14.2 ----------------------------
input wire txn_active,
input wire txn_start_stb,
// The REPORT strobe, not the END strobe. `txn_end_stb` fires a cycle before
// the classification is assigned, so sampling the verdict there reads the
// PREVIOUS transaction's -- and the symptom is a diagnosis that is always one
// transaction behind, which looks like a threshold that is off by one.
input wire txn_report_stb,
input wire txn_clean,
input wire txn_trunc,
// --- the strobes the datapath uses ------------------------------------
output wire cap_stb,
output wire launch_stb,
output wire preload_stb,
// --- diagnosis ---------------------------------------------------------
output reg cpol_mismatch, // sticky: SCLK not idle at assert
output reg phase_suspect, // sticky: MOSI moving at the sample
output reg [CNT_W-1:0] moved_run, // captures with MOSI in motion
output reg [CNT_W-1:0] trunc_run, // consecutive truncations (for aborts)
input wire clr_flags
);
// THE mapping. Two terms, and CPOL appears in neither.
assign cap_stb = txn_active & (cpha ? edge_b_stb : edge_a_stb);
assign launch_stb = txn_active & (cpha ? edge_a_stb : edge_b_stb);
// The preload's trigger is the transaction boundary rather than a command,
// which is what makes Chapter 14.4's lead requirement SYNC_N + 2.
assign preload_stb = txn_start_stb & ~cpha;
// MOSI one cycle back, the change one cycle back, and the capture one cycle
// back. Three registers, and between them they make the one-cycle window: a
// change at the sample, one cycle before it, or one cycle after it.
reg mosi_q_d;
reg mosi_chg_d;
reg cap_stb_d;
wire mosi_chg = (mosi_q != mosi_q_d);
wire in_motion = (cap_stb & (mosi_chg | mosi_chg_d)) // at, or just before
| (cap_stb_d & mosi_chg); // just after
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cpol_mismatch <= 1'b0;
phase_suspect <= 1'b0;
moved_run <= {CNT_W{1'b0}};
trunc_run <= {CNT_W{1'b0}};
mosi_q_d <= 1'b0;
mosi_chg_d <= 1'b0;
cap_stb_d <= 1'b0;
end else begin
mosi_q_d <= mosi_q;
mosi_chg_d <= mosi_chg;
cap_stb_d <= cap_stb;
if (clr_flags) begin
cpol_mismatch <= 1'b0;
phase_suspect <= 1'b0;
moved_run <= {CNT_W{1'b0}};
trunc_run <= {CNT_W{1'b0}};
end
// --- the phase diagnosis ---------------------------------------
// A sample with MOSI in motion within one cycle of it. For a matched pair
// this count is exactly zero, because MOSI settles at least HALF_MIN - 1
// cycles before the sample and HALF_MIN - 1 is at least 2.
if (in_motion) begin
if (moved_run != {CNT_W{1'b1}})
moved_run <= moved_run + 1'b1;
if (moved_run >= (SUSPECT_N - 1))
phase_suspect <= 1'b1;
end
// --- the polarity check ----------------------------------------
// Checked at the ASSERT, against the synchronised SCLK. Both signals
// come through the same front end with the same latency, so the
// comparison is of two values that were contemporaneous at the pins --
// which is the whole reason it can be trusted.
//
// It is deliberately NOT checked continuously. Between the last edge
// and the deassert, SCLK sits at whatever the master left it at, and
// on an abort (Chapter 14.7) that may legitimately not be the idle
// level. A continuous check would fire on every aborted transaction
// and be switched off.
if (cs_assert_stb && (sclk_q != cpol))
cpol_mismatch <= 1'b1;
// --- the truncation run ----------------------------------------
// Still published, but for ABORTS rather than for the phase (Chapter
// 14.7 consumes it). The counter saturates rather than wrapping: at
// CNT_W = 4 a wrap after sixteen consecutive truncations would take the
// run back below any threshold a consumer had set on it.
if (txn_report_stb) begin
if (txn_trunc) begin
if (trunc_run != {CNT_W{1'b1}})
trunc_run <= trunc_run + 1'b1;
end else if (txn_clean) begin
// A clean transaction resets the run. Neither latched flag
// clears with it: a driver that happened to send one
// whole-frame transaction should not erase the record of
// twenty broken ones.
trunc_run <= {CNT_W{1'b0}};
end
end
end
end
`ifdef SPI_CHECKS
always @(posedge clk) if (rst_n) begin
if (cap_stb && launch_stb)
$fatal(1, "capture and launch on the same cycle");
if (preload_stb && cpha)
$fatal(1, "a preload in CPHA=1");
end
`endif
endmodule-- spi_slave_mode.vhd
--
-- Chapter 14.6 -- all four modes in a slave, and which mismatches it can see.
--
-- The mapping is Chapter 13.5's, unchanged, and for the same reason: the front end
-- of 14.1 names its edges against the configured idle level, so CPOL is already
-- consumed and the assignment depends on CPHA alone.
--
-- CPHA = 0 capture on the LEADING edge, launch on the TRAILING edge
-- CPHA = 1 launch on the LEADING edge, capture on the TRAILING edge
--
-- Two rows, not four, and a slave that cases on {cpol, cpha} has the same
-- twice-the-logic problem a master would.
--
-- WHERE THE SLAVE DIFFERS FROM THE MASTER.
--
-- The master's CPHA=0 preload is triggered by its own start command. The slave's is
-- triggered by the transaction boundary, which is Chapter 14.2's registered strobe --
-- so the preload inherits the recovery latency, and that is the whole of Chapter
-- 14.4's lead requirement. Same mechanism, different trigger, and the difference is a
-- number the master has to respect.
--
-- The master also suppresses its final launch. The slave cannot: it does not know
-- which launch is the last, because a trailing edge is indistinguishable from the
-- start of another word (Chapter 14.4). So the two blocks look alike and differ in
-- both directions.
--
-- WHAT A MISCONFIGURED SLAVE CAN DETECT -- AND THE POINT OF DETECTING IT.
--
-- A slave cannot renegotiate. There is no mechanism in SPI by which it could tell the
-- master it is in the wrong mode, and no mode it could fall back to. So detection is
-- purely DIAGNOSTIC, and that is not a reason to skip it: the three mismatches
-- produce indistinguishable symptoms at the system level -- plausible wrong data --
-- and a device that can say which one it is turns a week of bisecting a driver into a
-- register read.
--
-- POLARITY. DETECTABLE, IMMEDIATELY. SCLK must be at the configured idle level
-- when chip select asserts, because idle is what idle means. If it is
-- not, the master's CPOL differs from this slave's -- and this is the
-- one mismatch caught before a single bit has moved.
--
-- PHASE. DETECTABLE, AND NOT BY COUNTING EDGES. This is worth getting right
-- because the plausible answer is wrong.
--
-- The tempting argument is that a phase mismatch truncates: the master
-- deasserts after ITS last capture, so the slave is left waiting for one
-- more edge. It does not. A CPHA=1 master sending N bits produces 2N
-- edges, and a CPHA=0 slave takes the N leading ones -- a whole number
-- of frames, on every transaction. The edge count AGREES, and truncation
-- never happens. A design that relied on it would report nothing at all.
--
-- What a phase mismatch actually does is physical: the slave captures on
-- the edge the master LAUNCHES on. MOSI is changing at the instant the
-- slave samples it -- which is not a statistic, it is an observation the
-- slave can make directly.
--
-- The observation has to be a WINDOW rather than a coincidence, and the
-- reason is worth stating because it is the kind of thing only
-- integration finds. A real master registers its MOSI output, so the bit
-- appears one cycle AFTER the edge that launched it. A mismatched master
-- therefore does not change MOSI exactly at this slave's sample -- it
-- changes it one cycle later. Testing for exact coincidence finds
-- nothing, and the chapter that claimed it would have been wrong for the
-- second time.
--
-- So the test is:
--
-- MOSI moved within one cycle EITHER SIDE of the sample
--
-- and the margin is provable. A matched master launches half a period
-- from this slave's capture, and with its registered output the bit
-- lands HALF_MIN - 1 cycles before the sample at the very worst. With
-- HALF_MIN = 3 that is 2 -- outside a one-cycle window, by exactly one
-- cycle. The window is one cycle wide because that is the widest window
-- the legal minimum half-period still clears.
--
-- For a matched pair the count is therefore exactly ZERO, not merely
-- small. The threshold of three exists only to ignore a one-off glitch.
--
-- Its one limitation, stated because it is real: the observation needs a
-- TRANSITION to make. A data pattern with no transitions at all -- all
-- zeros, all ones -- is sampled at the wrong moment and looks fine, so
-- the diagnosis appears once the data has any variety in it.
-- `trunc_run` is still published, but for aborts (Chapter 14.7) rather
-- than for this.
--
-- BIT ORDER. NOT DETECTABLE. Both orders produce a well-formed word of the right
-- length at the right time. There is no observation available, so there
-- is no flag -- and inventing one that guessed from the data would be
-- worse than none.
--
-- The asymmetry is worth carrying. The polarity mismatch is a LEVEL, comparable
-- against a constant, and it is caught before a bit has moved. The phase mismatch is
-- a TIMING RELATIONSHIP, comparable against the signal's own recent history, and it
-- is caught within one transaction once the data has a transition in it. The order
-- mismatch is an ORDERING, comparable against nothing, and it is never caught.
--
-- Level, relationship, ordering -- decreasing observability, and not a coincidence:
-- each one has less to be compared against than the last, and the third has nothing.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_mode is
generic (
SUSPECT_N : positive := 3; -- captures in motion meaning a mismatch
CNT_W : positive := 4
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- configuration
cpol : in std_logic;
cpha : in std_logic;
-- from the front end of 14.1
sclk_q : in std_logic;
mosi_q : in std_logic; -- delay-matched, from 14.1
cs_assert_stb : in std_logic;
edge_a_stb : in std_logic;
edge_b_stb : in std_logic;
-- from the transaction detector of 14.2. The REPORT strobe, not the END
-- strobe: `txn_end_stb` fires a cycle before the classification is assigned,
-- so sampling the verdict there reads the PREVIOUS transaction's -- and the
-- symptom is a diagnosis always one transaction behind, which looks like a
-- threshold that is off by one.
txn_active : in std_logic;
txn_start_stb : in std_logic;
txn_report_stb : in std_logic;
txn_clean : in std_logic;
txn_trunc : in std_logic;
-- the strobes the datapath uses
cap_stb : out std_logic;
launch_stb : out std_logic;
preload_stb : out std_logic;
-- diagnosis
cpol_mismatch : out std_logic; -- sticky: SCLK not idle at assert
phase_suspect : out std_logic; -- sticky: MOSI moving at the sample
moved_run : out unsigned(CNT_W - 1 downto 0);
trunc_run : out unsigned(CNT_W - 1 downto 0);
clr_flags : in std_logic
);
end entity;
architecture rtl of spi_slave_mode is
signal cpol_r : std_logic := '0';
signal phase_r : std_logic := '0';
signal run_r : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal moved_r : unsigned(CNT_W - 1 downto 0) := (others => '0');
-- MOSI one cycle back, the change one cycle back, and the capture one cycle
-- back. Three registers, and between them they make the one-cycle window: a
-- change at the sample, one cycle before it, or one cycle after it.
signal mosi_q_d : std_logic := '0';
signal mosi_chg_d : std_logic := '0';
signal cap_stb_d : std_logic := '0';
signal mosi_chg : std_logic;
signal in_motion : std_logic;
constant ALL_ONES : unsigned(CNT_W - 1 downto 0) := (others => '1');
begin
-- THE mapping. Two terms, and CPOL appears in neither.
cap_stb <= txn_active and edge_b_stb when cpha = '1'
else txn_active and edge_a_stb;
launch_stb <= txn_active and edge_a_stb when cpha = '1'
else txn_active and edge_b_stb;
-- The preload's trigger is the transaction boundary rather than a command, which
-- is what makes Chapter 14.4's lead requirement SYNC_N + 2.
preload_stb <= txn_start_stb and (not cpha);
cpol_mismatch <= cpol_r;
phase_suspect <= phase_r;
trunc_run <= run_r;
moved_run <= moved_r;
mosi_chg <= '1' when mosi_q /= mosi_q_d else '0';
in_motion <= (cap_stb and (mosi_chg or mosi_chg_d)) -- at, or just before
or (cap_stb_d and mosi_chg); -- just after
diagnose : process (clk, rst_n)
begin
if rst_n = '0' then
cpol_r <= '0';
phase_r <= '0';
run_r <= (others => '0');
moved_r <= (others => '0');
mosi_q_d <= '0';
mosi_chg_d <= '0';
cap_stb_d <= '0';
elsif rising_edge(clk) then
mosi_q_d <= mosi_q;
mosi_chg_d <= mosi_chg;
cap_stb_d <= cap_stb;
if clr_flags = '1' then
cpol_r <= '0';
phase_r <= '0';
run_r <= (others => '0');
moved_r <= (others => '0');
end if;
-- the phase diagnosis: a sample with MOSI in motion within one cycle of
-- it. For a matched pair this count is exactly zero, because MOSI settles
-- at least HALF_MIN - 1 cycles before the sample and HALF_MIN - 1 is at
-- least 2.
if in_motion = '1' then
if moved_r /= ALL_ONES then
moved_r <= moved_r + 1;
end if;
if to_integer(moved_r) >= (SUSPECT_N - 1) then
phase_r <= '1';
end if;
end if;
-- the polarity check, at the ASSERT and against the synchronised SCLK.
-- Both signals come through the same front end with the same latency, so
-- the comparison is of two values contemporaneous at the pins.
--
-- Deliberately NOT continuous. Between the last edge and the deassert
-- SCLK sits wherever the master left it, and on an abort (Chapter 14.7)
-- that may legitimately not be the idle level. A continuous check would
-- fire on every aborted transaction and be switched off.
if cs_assert_stb = '1' and sclk_q /= cpol then
cpol_r <= '1';
end if;
-- the truncation run, still published but for ABORTS rather than for
-- the phase (Chapter 14.7 consumes it). The counter saturates rather
-- than wrapping -- a wrap would take the run back below any threshold a
-- consumer had set on it.
if txn_report_stb = '1' then
if txn_trunc = '1' then
if run_r /= ALL_ONES then
run_r <= run_r + 1;
end if;
elsif txn_clean = '1' then
-- A clean transaction resets the run. Neither latched flag
-- clears with it: a driver that happened to send one whole-frame
-- transaction should not erase the record of twenty broken ones.
run_r <= (others => '0');
end if;
end if;
end if;
end process;
check : process (clk)
begin
if rising_edge(clk) and rst_n = '1' then
assert not (cap_stb = '1' and launch_stb = '1')
report "capture and launch on the same cycle" severity failure;
assert not (preload_stb = '1' and cpha = '1')
report "a preload in CPHA=1" severity failure;
end if;
end process;
end architecture;The testbench
Nine tests. The interesting group is 5 through 7b, which together establish that the detector is exact rather than statistical and that it works against a real master's output timing.
- The mapping, both phases. CPHA=0 captures on all eight leading edges and preloads once; CPHA=1 captures on all eight trailing edges and never preloads. Counts, not spot checks.
- CPOL is absent from the mapping. Inverting CPOL on both sides leaves the strobe counts identical, which is the property Chapter 14.1 bought.
- A polarity mismatch is reported at the assert, with no bit having moved.
- An abort leaving SCLK away from idle is not reported as a polarity mismatch, because the check is at the assert rather than continuous.
- A matched phase never sees MOSI in motion — four transactions, two of them with the one-cycle output lag a real master has, and the count is exactly zero. Establishing the zero is the whole basis of the detector.
- A mismatched phase is seen immediately, inside one transaction, at six motion events.
- The edge count agrees, which is the negative half of the claim: that same transaction is classified
txn_cleanby Chapter 14.2, so no amount of edge counting would have found it. - (7b) The same mismatch from a master with a registered output — MOSI one cycle after the launching edge, which is what Chapter 14.10 measured — is still caught, because the window is a window.
- A bit-order mismatch produces nothing, and that is the assertion. Four well-formed transactions, no flag, no count.
All three languages agree on the numbers: zero motion events for a matched pair at both output lags, six for a mismatched pair at both, and the mismatched transaction classified clean.
// spi_slave_mode_tb.sv
//
// Three experiments, one per mismatch, and the third is the interesting one because
// its conclusion is negative.
//
// POLARITY. Drive a master that idles SCLK at the wrong level and check the flag
// fires before any bit has moved -- and, just as importantly, that it does NOT
// fire for a matched polarity, or for an aborted transaction that happens to leave
// SCLK away from idle.
//
// PHASE. Drive a MATCHED master four times -- two of them with the one-cycle output
// lag a real master has -- and check the motion count is exactly ZERO. Then drive ONE
// mismatched master and check the diagnosis appears inside that single transaction,
// at both lags. Then check the very same transaction was classified CLEAN, which is
// the negative half of the claim: no amount of edge counting would have found it.
//
// BIT ORDER. Drive a mismatched order and check that NOTHING fires -- which is not
// a gap in the design, it is the claim that there is no observation available. A
// test that asserts a flag is absent is worth as much as one that asserts it is
// present, and rather rarer.
`timescale 1ns/1ps
module spi_slave_mode_tb;
localparam int SUSPECT_N = 3;
localparam int CNT_W = 4;
localparam int LEN_W = 6;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
// --- the slave's configuration ------------------------------------------
logic cpol = 1'b0;
logic cpha = 1'b0;
logic clr_flags = 1'b0;
// --- the pins -----------------------------------------------------------
logic sclk_pin = 1'b0;
logic cs_n_pin = 1'b1;
logic mosi_pin = 1'b0;
wire sclk_q, cs_active, mosi_q;
wire edge_a_stb, edge_b_stb, cs_assert_stb, cs_deassert_stb;
wire [11:0] min_half;
wire ratio_err;
spi_slave_frontend #(.SYNC_N(2), .HALF_MIN(3), .CNT_W(12)) u_fe (
.clk(clk), .rst_n(rst_n), .cpol(cpol),
.sclk_pin(sclk_pin), .cs_n_pin(cs_n_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q), .cs_active(cs_active), .mosi_q(mosi_q),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.min_half(min_half), .ratio_err(ratio_err), .clr_flags(1'b0)
);
logic [LEN_W-1:0] len = 6'd8;
wire txn_active, txn_start_stb, txn_end_stb;
wire [11:0] edges_in_txn, frames_in_txn;
wire txn_clean, txn_trunc, txn_empty, txn_report_stb;
wire [2:0] cs_state;
spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(12)) u_cs (
.clk(clk), .rst_n(rst_n),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb), .len(len),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_end_stb(txn_end_stb),
.edges_in_txn(edges_in_txn), .frames_in_txn(frames_in_txn),
.txn_clean(txn_clean), .txn_trunc(txn_trunc), .txn_empty(txn_empty),
.txn_report_stb(txn_report_stb), .state_id(cs_state)
);
wire cap_stb, launch_stb, preload_stb;
wire cpol_mismatch, phase_suspect;
wire [CNT_W-1:0] moved_run, trunc_run;
spi_slave_mode #(.SUSPECT_N(SUSPECT_N), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.cpol(cpol), .cpha(cpha),
.sclk_q(sclk_q), .mosi_q(mosi_q), .cs_assert_stb(cs_assert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_report_stb(txn_report_stb), .txn_clean(txn_clean),
.txn_trunc(txn_trunc),
.cap_stb(cap_stb), .launch_stb(launch_stb), .preload_stb(preload_stb),
.cpol_mismatch(cpol_mismatch), .phase_suspect(phase_suspect),
.moved_run(moved_run), .trunc_run(trunc_run), .clr_flags(clr_flags)
);
// --- monitors -----------------------------------------------------------
integer n_cap, n_launch, n_preload, both;
integer cap_on_lead, cap_on_trail;
always_ff @(posedge clk) begin
if (rst_n) begin
if (cap_stb) n_cap <= n_cap + 1;
if (launch_stb) n_launch <= n_launch + 1;
if (preload_stb) n_preload <= n_preload + 1;
if (cap_stb && launch_stb) both <= both + 1;
if (cap_stb && edge_a_stb) cap_on_lead <= cap_on_lead + 1;
if (cap_stb && edge_b_stb) cap_on_trail <= cap_on_trail + 1;
end
end
integer errors = 0;
task automatic adv(input integer n);
begin repeat (n) @(negedge clk); end
endtask
task automatic clear_counts;
begin
n_cap = 0; n_launch = 0; n_preload = 0;
cap_on_lead = 0; cap_on_trail = 0;
end
endtask
task automatic clear_dut;
begin
clr_flags = 1'b1; adv(1); clr_flags = 1'b0; adv(1);
end
endtask
// `m_pol` and `m_pha` are the MASTER's mode, which is allowed to differ from the
// slave's -- that difference is the subject of the chapter. `nedges` lets the
// master send a whole number of frames or one edge short.
task automatic drive(input bit m_pol, input integer nedges,
input integer half);
integer i;
begin
sclk_pin = m_pol;
cs_n_pin = 1'b1;
adv(10);
cs_n_pin = 1'b0;
adv(6);
for (i = 0; i < nedges; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? ~m_pol : m_pol;
adv(half);
end
adv(4);
cs_n_pin = 1'b1;
adv(8);
sclk_pin = m_pol;
adv(8);
end
endtask
// Drives a transaction in the MASTER's phase, which may differ from the slave's --
// that difference is the subject of the chapter. MOSI is launched on the edge the
// master's phase says, which is what puts it in motion near the wrong slave's
// sample.
//
// `lag` is the number of cycles by which the bit appears AFTER the edge that
// launched it, which is what a real master's registered output does. Chapter 14.10
// found that a live master has lag = 1, so both values are exercised here: a
// detector that only handled lag = 0 would pass this bench and then find nothing
// at all on the bus.
task automatic drive_mode(input bit m_pha, input bit m_pol,
input integer nbits, input integer half,
input [7:0] pattern, input integer lag);
integer i;
begin
cpol = m_pol; // the slave's polarity matches here
sclk_pin = m_pol;
cs_n_pin = 1'b1;
adv(10);
cs_n_pin = 1'b0;
if (!m_pha) mosi_pin = pattern[nbits-1];
adv(6);
for (i = 0; i < nbits; i = i + 1) begin
sclk_pin = ~m_pol; // leading
if (m_pha) begin // CPHA=1 launches here
adv(lag);
mosi_pin = pattern[nbits-1-i];
adv(half - lag);
end else adv(half);
sclk_pin = m_pol; // trailing
if (!m_pha) begin // CPHA=0 launches here
adv(lag);
if (i < nbits-1) mosi_pin = pattern[nbits-2-i];
adv(half - lag);
end else adv(half);
end
adv(4);
cs_n_pin = 1'b1;
adv(10);
sclk_pin = m_pol;
adv(8);
end
endtask
integer k;
initial begin
clear_counts();
both = 0;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. THE MAPPING, both phases. CPHA=0 must capture on leading edges only and
// CPHA=1 on trailing edges only, and the preload must exist in exactly
// one of them.
cpol = 1'b0; cpha = 1'b0; clear_dut(); clear_counts();
drive(1'b0, 16, 4);
if (n_cap != 8 || cap_on_lead != 8 || cap_on_trail != 0) begin
$display(" FAIL: CPHA=0 captured %0d times, %0d on leading and %0d on trailing",
n_cap, cap_on_lead, cap_on_trail);
errors = errors + 1;
end
if (n_preload != 1) begin
$display(" FAIL: CPHA=0 produced %0d preloads", n_preload);
errors = errors + 1;
end
cpha = 1'b1; clear_dut(); clear_counts();
drive(1'b0, 16, 4);
if (n_cap != 8 || cap_on_trail != 8 || cap_on_lead != 0) begin
$display(" FAIL: CPHA=1 captured %0d times, %0d on leading and %0d on trailing",
n_cap, cap_on_lead, cap_on_trail);
errors = errors + 1;
end
if (n_preload != 0) begin
$display(" FAIL: CPHA=1 produced %0d preloads", n_preload);
errors = errors + 1;
end
$display(" the mapping: CPHA=0 captures on all eight leading edges and preloads once; CPHA=1 captures on all eight trailing edges and never preloads");
// 2. CPOL IS ABSENT FROM THE MAPPING. The same frame under both polarities --
// with the slave configured to match each -- must give identical strobe
// counts, which is the claim that CPOL was consumed by the front end.
cpha = 1'b0;
cpol = 1'b0; clear_dut(); clear_counts();
drive(1'b0, 16, 4);
k = cap_on_lead;
cpol = 1'b1; clear_dut(); clear_counts();
drive(1'b1, 16, 4);
if (cap_on_lead != k || cap_on_trail != 0) begin
$display(" FAIL: inverting CPOL changed the mapping: %0d leading captures against %0d",
cap_on_lead, k);
errors = errors + 1;
end
if (cpol_mismatch) begin
$display(" FAIL: a matched polarity was reported as a mismatch");
errors = errors + 1;
end
$display(" inverting CPOL on both sides leaves the mapping identical -- %0d leading captures either way -- and reports no mismatch",
cap_on_lead);
// 3. A POLARITY MISMATCH is caught at the assert, before a bit moves.
cpol = 1'b0; cpha = 1'b0; clear_dut(); clear_counts();
// The master idles SCLK HIGH while this slave expects it low.
sclk_pin = 1'b1; cs_n_pin = 1'b1; adv(10);
cs_n_pin = 1'b0; adv(6);
if (!cpol_mismatch) begin
$display(" FAIL: an idle level of 1 against a configured 0 was not reported");
errors = errors + 1;
end
if (n_cap != 0) begin
$display(" FAIL: the mismatch was reported only after %0d captures",
n_cap);
errors = errors + 1;
end
cs_n_pin = 1'b1; adv(10); sclk_pin = 1'b0; adv(10);
$display(" a polarity mismatch is reported at the assert, with no bit having moved");
// 4. AN ABORT LEAVING SCLK AWAY FROM IDLE must NOT be reported as a polarity
// mismatch. The check is at the assert and not continuous, and this is the
// stimulus that distinguishes the two designs.
clear_dut();
sclk_pin = 1'b0; cs_n_pin = 1'b1; adv(10);
cs_n_pin = 1'b0; adv(6);
// Five edges: an odd number, so the transaction ends with SCLK high.
for (k = 0; k < 5; k = k + 1) begin
sclk_pin = (k % 2 == 0) ? 1'b1 : 1'b0;
adv(4);
end
cs_n_pin = 1'b1; // deasserted with SCLK still high
adv(10);
if (cpol_mismatch) begin
$display(" FAIL: an abort that left SCLK away from idle was reported as a polarity mismatch");
errors = errors + 1;
end
sclk_pin = 1'b0; adv(10);
$display(" a transaction aborted with SCLK left high is not reported as a polarity mismatch, because the check is at the assert rather than continuous");
// 5. A MATCHED PHASE NEVER SEES MOSI IN MOTION. Establishing the zero is
// the whole basis of the detector -- a count that were merely "usually
// small" would make the threshold a guess.
clear_dut();
for (k = 0; k < 4; k = k + 1)
drive_mode(1'b0, 1'b0, 8, 4, 8'h6B, k % 2); // matched, lag 0 and lag 1
if (moved_run != 0) begin
$display(" FAIL: a matched phase saw MOSI in motion %0d times", moved_run);
errors = errors + 1;
end
if (phase_suspect) begin
$display(" FAIL: a matched phase was diagnosed as a phase mismatch");
errors = errors + 1;
end
$display(" four transactions with a matched phase, two of them with the one-cycle output lag a real master has: MOSI was in motion within a cycle of zero of the samples, which is what makes the detector exact rather than statistical");
// 6. A MISMATCHED PHASE IS SEEN IMMEDIATELY. The master launches on the edge
// the slave samples, so MOSI is moving at every sample where the bit changes.
clear_dut();
drive_mode(1'b1, 1'b0, 8, 4, 8'h6B, 0); // master CPHA=1, slave CPHA=0
if (moved_run == 0) begin
$display(" FAIL: a phase mismatch produced no captures with MOSI in motion");
errors = errors + 1;
end
if (!phase_suspect) begin
$display(" FAIL: a phase mismatch was not diagnosed after one transaction, with %0d motion events",
moved_run);
errors = errors + 1;
end
$display(" one transaction from a phase-mismatched master: MOSI was in motion at %0d captures and the mismatch is diagnosed within that single transaction",
moved_run);
// 7. THE EDGE COUNT AGREES, WHICH IS WHY COUNTING EDGES CANNOT DETECT IT. This
// is the negative half of the claim and the reason the detector is what it is.
if (txn_trunc) begin
$display(" FAIL: a phase mismatch truncated the transaction after all");
errors = errors + 1;
end
if (!txn_clean) begin
$display(" FAIL: a phase-mismatched transaction was not a whole number of frames");
errors = errors + 1;
end
$display(" and that same transaction was classified CLEAN -- a whole number of frames -- so no amount of edge counting would have found it");
// 7b. THE SAME MISMATCH FROM A MASTER WITH A REGISTERED OUTPUT, which is the
// case a real bus presents and the reason the window is a window. The bit
// lands one cycle after the edge that launched it, so a detector testing
// for exact coincidence would see nothing here.
clear_dut();
drive_mode(1'b1, 1'b0, 8, 4, 8'h6B, 1);
if (moved_run == 0) begin
$display(" FAIL: a phase mismatch from a master with a one-cycle output lag produced no motion events, which is the case that matters on a real bus");
errors = errors + 1;
end
if (!phase_suspect) begin
$display(" FAIL: a phase mismatch from a master with a one-cycle output lag was not diagnosed");
errors = errors + 1;
end
$display(" the same mismatch from a master whose MOSI appears one cycle after the launching edge -- which is what a registered output does and what Chapter 14.10 actually measured -- is still caught, at %0d samples, because the window is one cycle wide either side rather than an exact coincidence",
moved_run);
// 8. A BIT-ORDER MISMATCH PRODUCES NOTHING, and that is the claim. Every
// edge arrives, every frame completes, the idle level is right -- there is
// no observation available, so no flag may fire.
clear_dut();
for (k = 0; k < 4; k = k + 1)
drive_mode(1'b0, 1'b0, 8, 4, 8'h6B, k % 2);
if (cpol_mismatch || phase_suspect || trunc_run != 0 || moved_run != 0) begin
$display(" FAIL: well-formed transactions produced a diagnosis: cpol=%0b phase=%0b trunc=%0d moved=%0d",
cpol_mismatch, phase_suspect, trunc_run, moved_run);
errors = errors + 1;
end
$display(" four well-formed transactions -- which is what a bit-order mismatch looks like -- produce no diagnosis at all, because none is available");
// 9. CAPTURE AND LAUNCH NEVER COINCIDE.
if (both != 0) begin
$display(" FAIL: %0d cycles carried both a capture and a launch", both);
errors = errors + 1;
end
$display(" across the whole run no cycle carried both a capture and a launch");
if (errors == 0)
$display("PASS: the mapping is Chapter 13.5's, with CPOL already consumed by the front end, so CPHA=0 captures on every leading edge and preloads once while CPHA=1 captures on every trailing edge and never preloads, and inverting CPOL on both sides leaves the strobe counts identical -- a polarity mismatch is caught at the assert before a single bit has moved while a transaction aborted with SCLK left away from idle is correctly not reported, because the check is at the assert rather than continuous -- a phase mismatch is diagnosed by noticing that MOSI is IN MOTION within one cycle of the moment the slave samples it, which is exact rather than statistical because a matched pair produces that observation exactly zero times whether the master's output is combinational or registered, and it is diagnosed within a single transaction while the same transaction is classified CLEAN, so no amount of edge counting would ever have found it -- and the window is one cycle wide either side rather than an exact coincidence because a real master's registered output puts the bit one cycle after the edge that launched it, which is the margin HALF_MIN - 1 still clears -- and four well-formed transactions, which is exactly what a bit-order mismatch looks like, produce nothing at all, because for ordering there is no observation available and a flag that guessed would be worse than none");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_slave_mode_tb.v
//
// Three experiments, one per mismatch, and the third is the interesting one because
// its conclusion is negative.
//
// POLARITY. Drive a master that idles SCLK at the wrong level and check the flag
// fires before any bit has moved -- and, just as importantly, that it does NOT
// fire for a matched polarity, or for an aborted transaction that happens to leave
// SCLK away from idle.
//
// PHASE. Drive a MATCHED master four times -- two of them with the one-cycle output
// lag a real master has -- and check the motion count is exactly ZERO. Then drive ONE
// mismatched master and check the diagnosis appears inside that single transaction,
// at both lags. Then check the very same transaction was classified CLEAN, which is
// the negative half of the claim: no amount of edge counting would have found it.
//
// BIT ORDER. Drive a mismatched order and check that NOTHING fires -- which is not
// a gap in the design, it is the claim that there is no observation available. A
// test that asserts a flag is absent is worth as much as one that asserts it is
// present, and rather rarer.
`timescale 1ns/1ps
module spi_slave_mode_tb;
localparam SUSPECT_N = 3;
localparam CNT_W = 4;
localparam LEN_W = 6;
reg clk;
reg rst_n;
always #5 clk = ~clk;
// --- the slave's configuration ------------------------------------------
reg cpol;
reg cpha;
reg clr_flags;
// --- the pins -----------------------------------------------------------
reg sclk_pin;
reg cs_n_pin;
reg mosi_pin;
wire sclk_q, cs_active, mosi_q;
wire edge_a_stb, edge_b_stb, cs_assert_stb, cs_deassert_stb;
wire [11:0] min_half;
wire ratio_err;
spi_slave_frontend #(.SYNC_N(2), .HALF_MIN(3), .CNT_W(12)) u_fe (
.clk(clk), .rst_n(rst_n), .cpol(cpol),
.sclk_pin(sclk_pin), .cs_n_pin(cs_n_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q), .cs_active(cs_active), .mosi_q(mosi_q),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.min_half(min_half), .ratio_err(ratio_err), .clr_flags(1'b0)
);
reg [LEN_W-1:0] len;
wire txn_active, txn_start_stb, txn_end_stb;
wire [11:0] edges_in_txn, frames_in_txn;
wire txn_clean, txn_trunc, txn_empty, txn_report_stb;
wire [2:0] cs_state;
spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(12)) u_cs (
.clk(clk), .rst_n(rst_n),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb), .len(len),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_end_stb(txn_end_stb),
.edges_in_txn(edges_in_txn), .frames_in_txn(frames_in_txn),
.txn_clean(txn_clean), .txn_trunc(txn_trunc), .txn_empty(txn_empty),
.txn_report_stb(txn_report_stb), .state_id(cs_state)
);
wire cap_stb, launch_stb, preload_stb;
wire cpol_mismatch, phase_suspect;
wire [CNT_W-1:0] moved_run, trunc_run;
spi_slave_mode #(.SUSPECT_N(SUSPECT_N), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.cpol(cpol), .cpha(cpha),
.sclk_q(sclk_q), .mosi_q(mosi_q), .cs_assert_stb(cs_assert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_report_stb(txn_report_stb), .txn_clean(txn_clean),
.txn_trunc(txn_trunc),
.cap_stb(cap_stb), .launch_stb(launch_stb), .preload_stb(preload_stb),
.cpol_mismatch(cpol_mismatch), .phase_suspect(phase_suspect),
.moved_run(moved_run), .trunc_run(trunc_run), .clr_flags(clr_flags)
);
// --- monitors -----------------------------------------------------------
integer n_cap, n_launch, n_preload, both;
integer cap_on_lead, cap_on_trail;
always @(posedge clk) begin
if (rst_n) begin
if (cap_stb) n_cap <= n_cap + 1;
if (launch_stb) n_launch <= n_launch + 1;
if (preload_stb) n_preload <= n_preload + 1;
if (cap_stb && launch_stb) both <= both + 1;
if (cap_stb && edge_a_stb) cap_on_lead <= cap_on_lead + 1;
if (cap_stb && edge_b_stb) cap_on_trail <= cap_on_trail + 1;
end
end
integer errors;
task adv;
input integer n;
begin repeat (n) @(negedge clk); end
endtask
task clear_counts;
begin
n_cap = 0; n_launch = 0; n_preload = 0;
cap_on_lead = 0; cap_on_trail = 0;
end
endtask
task clear_dut;
begin
clr_flags = 1'b1; adv(1); clr_flags = 1'b0; adv(1);
end
endtask
// `m_pol` and `m_pha` are the MASTER's mode, which is allowed to differ from the
// slave's -- that difference is the subject of the chapter. `nedges` lets the
// master send a whole number of frames or one edge short.
task drive;
input m_pol;
input integer nedges;
input integer half;
integer i;
begin
sclk_pin = m_pol;
cs_n_pin = 1'b1;
adv(10);
cs_n_pin = 1'b0;
adv(6);
for (i = 0; i < nedges; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? ~m_pol : m_pol;
adv(half);
end
adv(4);
cs_n_pin = 1'b1;
adv(8);
sclk_pin = m_pol;
adv(8);
end
endtask
// Drives a transaction in the MASTER's phase, which may differ from the slave's --
// that difference is the subject of the chapter. MOSI is launched on the edge the
// master's phase says, which is what puts it in motion near the wrong slave's
// sample.
//
// `lag` is the number of cycles by which the bit appears AFTER the edge that
// launched it, which is what a real master's registered output does. Chapter 14.10
// found that a live master has lag = 1, so both values are exercised here: a
// detector that only handled lag = 0 would pass this bench and then find nothing
// at all on the bus.
task drive_mode;
input m_pha;
input m_pol;
input integer nbits;
input integer half;
input [7:0] pattern;
input integer lag;
integer i;
begin
cpol = m_pol; // the slave's polarity matches here
sclk_pin = m_pol;
cs_n_pin = 1'b1;
adv(10);
cs_n_pin = 1'b0;
if (!m_pha) mosi_pin = pattern[nbits-1];
adv(6);
for (i = 0; i < nbits; i = i + 1) begin
sclk_pin = ~m_pol; // leading
if (m_pha) begin // CPHA=1 launches here
adv(lag);
mosi_pin = pattern[nbits-1-i];
adv(half - lag);
end else adv(half);
sclk_pin = m_pol; // trailing
if (!m_pha) begin // CPHA=0 launches here
adv(lag);
if (i < nbits-1) mosi_pin = pattern[nbits-2-i];
adv(half - lag);
end else adv(half);
end
adv(4);
cs_n_pin = 1'b1;
adv(10);
sclk_pin = m_pol;
adv(8);
end
endtask
integer k;
initial begin
clear_counts();
both = 0;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. THE MAPPING, both phases. CPHA=0 must capture on leading edges only and
// CPHA=1 on trailing edges only, and the preload must exist in exactly
// one of them.
cpol = 1'b0; cpha = 1'b0; clear_dut(); clear_counts();
drive(1'b0, 16, 4);
if (n_cap != 8 || cap_on_lead != 8 || cap_on_trail != 0) begin
$display(" FAIL: CPHA=0 captured %0d times, %0d on leading and %0d on trailing",
n_cap, cap_on_lead, cap_on_trail);
errors = errors + 1;
end
if (n_preload != 1) begin
$display(" FAIL: CPHA=0 produced %0d preloads", n_preload);
errors = errors + 1;
end
cpha = 1'b1; clear_dut(); clear_counts();
drive(1'b0, 16, 4);
if (n_cap != 8 || cap_on_trail != 8 || cap_on_lead != 0) begin
$display(" FAIL: CPHA=1 captured %0d times, %0d on leading and %0d on trailing",
n_cap, cap_on_lead, cap_on_trail);
errors = errors + 1;
end
if (n_preload != 0) begin
$display(" FAIL: CPHA=1 produced %0d preloads", n_preload);
errors = errors + 1;
end
$display(" the mapping: CPHA=0 captures on all eight leading edges and preloads once; CPHA=1 captures on all eight trailing edges and never preloads");
// 2. CPOL IS ABSENT FROM THE MAPPING. The same frame under both polarities --
// with the slave configured to match each -- must give identical strobe
// counts, which is the claim that CPOL was consumed by the front end.
cpha = 1'b0;
cpol = 1'b0; clear_dut(); clear_counts();
drive(1'b0, 16, 4);
k = cap_on_lead;
cpol = 1'b1; clear_dut(); clear_counts();
drive(1'b1, 16, 4);
if (cap_on_lead != k || cap_on_trail != 0) begin
$display(" FAIL: inverting CPOL changed the mapping: %0d leading captures against %0d",
cap_on_lead, k);
errors = errors + 1;
end
if (cpol_mismatch) begin
$display(" FAIL: a matched polarity was reported as a mismatch");
errors = errors + 1;
end
$display(" inverting CPOL on both sides leaves the mapping identical -- %0d leading captures either way -- and reports no mismatch",
cap_on_lead);
// 3. A POLARITY MISMATCH is caught at the assert, before a bit moves.
cpol = 1'b0; cpha = 1'b0; clear_dut(); clear_counts();
// The master idles SCLK HIGH while this slave expects it low.
sclk_pin = 1'b1; cs_n_pin = 1'b1; adv(10);
cs_n_pin = 1'b0; adv(6);
if (!cpol_mismatch) begin
$display(" FAIL: an idle level of 1 against a configured 0 was not reported");
errors = errors + 1;
end
if (n_cap != 0) begin
$display(" FAIL: the mismatch was reported only after %0d captures",
n_cap);
errors = errors + 1;
end
cs_n_pin = 1'b1; adv(10); sclk_pin = 1'b0; adv(10);
$display(" a polarity mismatch is reported at the assert, with no bit having moved");
// 4. AN ABORT LEAVING SCLK AWAY FROM IDLE must NOT be reported as a polarity
// mismatch. The check is at the assert and not continuous, and this is the
// stimulus that distinguishes the two designs.
clear_dut();
sclk_pin = 1'b0; cs_n_pin = 1'b1; adv(10);
cs_n_pin = 1'b0; adv(6);
// Five edges: an odd number, so the transaction ends with SCLK high.
for (k = 0; k < 5; k = k + 1) begin
sclk_pin = (k % 2 == 0) ? 1'b1 : 1'b0;
adv(4);
end
cs_n_pin = 1'b1; // deasserted with SCLK still high
adv(10);
if (cpol_mismatch) begin
$display(" FAIL: an abort that left SCLK away from idle was reported as a polarity mismatch");
errors = errors + 1;
end
sclk_pin = 1'b0; adv(10);
$display(" a transaction aborted with SCLK left high is not reported as a polarity mismatch, because the check is at the assert rather than continuous");
// 5. A MATCHED PHASE NEVER SEES MOSI IN MOTION. Establishing the zero is
// the whole basis of the detector -- a count that were merely "usually
// small" would make the threshold a guess.
clear_dut();
for (k = 0; k < 4; k = k + 1)
drive_mode(1'b0, 1'b0, 8, 4, 8'h6B, k % 2); // matched, lag 0 and lag 1
if (moved_run != 0) begin
$display(" FAIL: a matched phase saw MOSI in motion %0d times", moved_run);
errors = errors + 1;
end
if (phase_suspect) begin
$display(" FAIL: a matched phase was diagnosed as a phase mismatch");
errors = errors + 1;
end
$display(" four transactions with a matched phase, two of them with the one-cycle output lag a real master has: MOSI was in motion within a cycle of zero of the samples, which is what makes the detector exact rather than statistical");
// 6. A MISMATCHED PHASE IS SEEN IMMEDIATELY. The master launches on the edge
// the slave samples, so MOSI is moving at every sample where the bit changes.
clear_dut();
drive_mode(1'b1, 1'b0, 8, 4, 8'h6B, 0); // master CPHA=1, slave CPHA=0
if (moved_run == 0) begin
$display(" FAIL: a phase mismatch produced no captures with MOSI in motion");
errors = errors + 1;
end
if (!phase_suspect) begin
$display(" FAIL: a phase mismatch was not diagnosed after one transaction, with %0d motion events",
moved_run);
errors = errors + 1;
end
$display(" one transaction from a phase-mismatched master: MOSI was in motion at %0d captures and the mismatch is diagnosed within that single transaction",
moved_run);
// 7. THE EDGE COUNT AGREES, WHICH IS WHY COUNTING EDGES CANNOT DETECT IT. This
// is the negative half of the claim and the reason the detector is what it is.
if (txn_trunc) begin
$display(" FAIL: a phase mismatch truncated the transaction after all");
errors = errors + 1;
end
if (!txn_clean) begin
$display(" FAIL: a phase-mismatched transaction was not a whole number of frames");
errors = errors + 1;
end
$display(" and that same transaction was classified CLEAN -- a whole number of frames -- so no amount of edge counting would have found it");
// 7b. THE SAME MISMATCH FROM A MASTER WITH A REGISTERED OUTPUT, which is the
// case a real bus presents and the reason the window is a window. The bit
// lands one cycle after the edge that launched it, so a detector testing
// for exact coincidence would see nothing here.
clear_dut();
drive_mode(1'b1, 1'b0, 8, 4, 8'h6B, 1);
if (moved_run == 0) begin
$display(" FAIL: a phase mismatch from a master with a one-cycle output lag produced no motion events, which is the case that matters on a real bus");
errors = errors + 1;
end
if (!phase_suspect) begin
$display(" FAIL: a phase mismatch from a master with a one-cycle output lag was not diagnosed");
errors = errors + 1;
end
$display(" the same mismatch from a master whose MOSI appears one cycle after the launching edge -- which is what a registered output does and what Chapter 14.10 actually measured -- is still caught, at %0d samples, because the window is one cycle wide either side rather than an exact coincidence",
moved_run);
// 8. A BIT-ORDER MISMATCH PRODUCES NOTHING, and that is the claim. Every
// edge arrives, every frame completes, the idle level is right -- there is
// no observation available, so no flag may fire.
clear_dut();
for (k = 0; k < 4; k = k + 1)
drive_mode(1'b0, 1'b0, 8, 4, 8'h6B, k % 2);
if (cpol_mismatch || phase_suspect || trunc_run != 0 || moved_run != 0) begin
$display(" FAIL: well-formed transactions produced a diagnosis: cpol=%0b phase=%0b trunc=%0d moved=%0d",
cpol_mismatch, phase_suspect, trunc_run, moved_run);
errors = errors + 1;
end
$display(" four well-formed transactions -- which is what a bit-order mismatch looks like -- produce no diagnosis at all, because none is available");
// 9. CAPTURE AND LAUNCH NEVER COINCIDE.
if (both != 0) begin
$display(" FAIL: %0d cycles carried both a capture and a launch", both);
errors = errors + 1;
end
$display(" across the whole run no cycle carried both a capture and a launch");
if (errors == 0)
$display("PASS: the mapping is Chapter 13.5's, with CPOL already consumed by the front end, so CPHA=0 captures on every leading edge and preloads once while CPHA=1 captures on every trailing edge and never preloads, and inverting CPOL on both sides leaves the strobe counts identical -- a polarity mismatch is caught at the assert before a single bit has moved while a transaction aborted with SCLK left away from idle is correctly not reported, because the check is at the assert rather than continuous -- a phase mismatch is diagnosed by noticing that MOSI is IN MOTION within one cycle of the moment the slave samples it, which is exact rather than statistical because a matched pair produces that observation exactly zero times whether the master's output is combinational or registered, and it is diagnosed within a single transaction while the same transaction is classified CLEAN, so no amount of edge counting would ever have found it -- and the window is one cycle wide either side rather than an exact coincidence because a real master's registered output puts the bit one cycle after the edge that launched it, which is the margin HALF_MIN - 1 still clears -- and four well-formed transactions, which is exactly what a bit-order mismatch looks like, produce nothing at all, because for ordering there is no observation available and a flag that guessed would be worse than none");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b0;
cpol = 1'b0;
cpha = 1'b0;
clr_flags = 1'b0;
sclk_pin = 1'b0;
cs_n_pin = 1'b1;
mosi_pin = 1'b0;
len = 6'd8;
errors = 0;
end
endmodule-- spi_slave_mode_tb.vhd
--
-- Three experiments, one per mismatch, and the third is the interesting one because
-- its conclusion is negative.
--
-- POLARITY. Drive a master that idles SCLK at the wrong level and check the flag
-- fires before any bit has moved -- and, just as importantly, that it does NOT fire
-- for a matched polarity, or for an aborted transaction that happens to leave SCLK
-- away from idle.
--
-- PHASE. Drive a MATCHED master four times -- two of them with the one-cycle output
-- lag a real master has -- and check the motion count is exactly ZERO. Then drive ONE
-- mismatched master and check the diagnosis appears inside that single transaction, at
-- both lags. Then check the very same transaction was classified CLEAN, which is the
-- negative half of the claim: no amount of edge counting would have found it.
--
-- BIT ORDER. Drive well-formed transactions and check that NOTHING fires -- which is
-- not a gap in the design, it is the claim that there is no observation available. A
-- test that asserts a flag is absent is worth as much as one that asserts it is
-- present, and rather rarer.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_mode_tb is
end entity;
architecture sim of spi_slave_mode_tb is
constant SUSPECT_N : positive := 3;
constant CNT_W : positive := 4;
constant LEN_W : positive := 6;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal cpol : std_logic := '0';
signal cpha : std_logic := '0';
signal clr_flags : std_logic := '0';
signal sclk_pin : std_logic := '0';
signal cs_n_pin : std_logic := '1';
signal mosi_pin : std_logic := '0';
signal sclk_q, cs_active, mosi_q : std_logic;
signal edge_a_stb, edge_b_stb, cs_assert_stb, cs_deassert_stb : std_logic;
signal min_half : unsigned(11 downto 0);
signal ratio_err : std_logic;
signal len : unsigned(LEN_W - 1 downto 0) := to_unsigned(8, LEN_W);
signal txn_active, txn_start_stb, txn_end_stb : std_logic;
signal edges_in_txn, frames_in_txn : unsigned(11 downto 0);
signal txn_clean, txn_trunc, txn_empty, txn_report_stb : std_logic;
signal cs_state : unsigned(2 downto 0);
signal cap_stb, launch_stb, preload_stb : std_logic;
signal cpol_mismatch, phase_suspect : std_logic;
signal trunc_run, moved_run : unsigned(CNT_W - 1 downto 0);
signal n_cap, n_launch, n_preload, both : natural := 0;
signal cap_on_lead, cap_on_trail : natural := 0;
signal clr_cnt : std_logic := '0';
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
u_fe : entity work.spi_slave_frontend
generic map (SYNC_N => 2, HALF_MIN => 3, CNT_W => 12)
port map (clk => clk, rst_n => rst_n, cpol => cpol,
sclk_pin => sclk_pin, cs_n_pin => cs_n_pin,
mosi_pin => mosi_pin,
sclk_q => sclk_q, cs_active => cs_active, mosi_q => mosi_q,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
cs_assert_stb => cs_assert_stb,
cs_deassert_stb => cs_deassert_stb,
min_half => min_half, ratio_err => ratio_err,
clr_flags => '0');
u_cs : entity work.spi_slave_cs
generic map (LEN_W => LEN_W, CNT_W => 12)
port map (clk => clk, rst_n => rst_n,
cs_assert_stb => cs_assert_stb,
cs_deassert_stb => cs_deassert_stb,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
len => len,
txn_active => txn_active, txn_start_stb => txn_start_stb,
txn_end_stb => txn_end_stb,
edges_in_txn => edges_in_txn, frames_in_txn => frames_in_txn,
txn_clean => txn_clean, txn_trunc => txn_trunc,
txn_empty => txn_empty, txn_report_stb => txn_report_stb,
state_id => cs_state);
dut : entity work.spi_slave_mode
generic map (SUSPECT_N => SUSPECT_N, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
cpol => cpol, cpha => cpha,
sclk_q => sclk_q, mosi_q => mosi_q,
cs_assert_stb => cs_assert_stb,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
txn_active => txn_active, txn_start_stb => txn_start_stb,
txn_report_stb => txn_report_stb,
txn_clean => txn_clean, txn_trunc => txn_trunc,
cap_stb => cap_stb, launch_stb => launch_stb,
preload_stb => preload_stb,
cpol_mismatch => cpol_mismatch, phase_suspect => phase_suspect,
moved_run => moved_run, trunc_run => trunc_run,
clr_flags => clr_flags);
monitor : process (clk)
begin
if rising_edge(clk) then
if clr_cnt = '1' then
n_cap <= 0; n_launch <= 0; n_preload <= 0;
cap_on_lead <= 0; cap_on_trail <= 0;
elsif rst_n = '1' then
if cap_stb = '1' then n_cap <= n_cap + 1; end if;
if launch_stb = '1' then n_launch <= n_launch + 1; end if;
if preload_stb = '1' then n_preload <= n_preload + 1; end if;
if cap_stb = '1' and launch_stb = '1' then
both <= both + 1;
end if;
if cap_stb = '1' and edge_a_stb = '1' then
cap_on_lead <= cap_on_lead + 1;
end if;
if cap_stb = '1' and edge_b_stb = '1' then
cap_on_trail <= cap_on_trail + 1;
end if;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable k : natural;
procedure adv(n : natural) is
begin
for j in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
procedure clear_counts is
begin
clr_cnt <= '1';
wait until falling_edge(clk);
clr_cnt <= '0';
end procedure;
procedure clear_dut is
begin
clr_flags <= '1'; adv(1); clr_flags <= '0'; adv(1);
end procedure;
-- `m_pol` is the MASTER's idle level, which is allowed to differ from the
-- slave's -- that difference is the subject of the chapter.
procedure drive(m_pol : std_logic; nedges : natural; half : natural) is
begin
sclk_pin <= m_pol;
cs_n_pin <= '1';
adv(10);
cs_n_pin <= '0';
adv(6);
for i in 0 to nedges - 1 loop
if (i mod 2) = 0 then
sclk_pin <= not m_pol;
else
sclk_pin <= m_pol;
end if;
adv(half);
end loop;
adv(4);
cs_n_pin <= '1';
adv(8);
sclk_pin <= m_pol;
adv(8);
end procedure;
-- Drives a transaction in the MASTER's phase, which may differ from the
-- slave's -- that difference is the subject of the chapter. MOSI is launched
-- on the edge the master's phase says, which is exactly what puts it in
-- motion near the wrong slave's sample.
--
-- `lag` is the number of cycles by which the bit appears AFTER the edge that
-- launched it, which is what a real master's registered output does. Chapter
-- 14.10 found that a live master has lag = 1, so both values are exercised
-- here: a detector that only handled lag = 0 would pass this bench and then
-- find nothing at all on the bus.
procedure drive_mode(m_pha : std_logic; m_pol : std_logic;
nbits : natural; half : natural;
pattern : std_logic_vector(7 downto 0);
lag : natural) is
begin
cpol <= m_pol; -- the slave's polarity matches here
sclk_pin <= m_pol;
cs_n_pin <= '1';
adv(10);
cs_n_pin <= '0';
if m_pha = '0' then
mosi_pin <= pattern(nbits - 1);
end if;
adv(6);
for i in 0 to nbits - 1 loop
sclk_pin <= not m_pol; -- leading
if m_pha = '1' then -- CPHA=1 launches
adv(lag);
mosi_pin <= pattern(nbits - 1 - i);
adv(half - lag);
else
adv(half);
end if;
sclk_pin <= m_pol; -- trailing
if m_pha = '0' then -- CPHA=0 launches
adv(lag);
if i < nbits - 1 then
mosi_pin <= pattern(nbits - 2 - i);
end if;
adv(half - lag);
else
adv(half);
end if;
end loop;
adv(4);
cs_n_pin <= '1';
adv(10);
sclk_pin <= m_pol;
adv(8);
end procedure;
begin
adv(3);
rst_n <= '1';
adv(2);
-- 1. THE MAPPING, both phases.
cpol <= '0'; cpha <= '0'; clear_dut; clear_counts;
drive('0', 16, 4);
if n_cap /= 8 or cap_on_lead /= 8 or cap_on_trail /= 0 then
report " FAIL: CPHA=0 captured " & integer'image(n_cap) &
" times, " & integer'image(cap_on_lead) & " on leading and " &
integer'image(cap_on_trail) & " on trailing";
errs := errs + 1;
end if;
if n_preload /= 1 then
report " FAIL: CPHA=0 produced " & integer'image(n_preload) &
" preloads";
errs := errs + 1;
end if;
cpha <= '1'; clear_dut; clear_counts;
drive('0', 16, 4);
if n_cap /= 8 or cap_on_trail /= 8 or cap_on_lead /= 0 then
report " FAIL: CPHA=1 captured " & integer'image(n_cap) &
" times, " & integer'image(cap_on_lead) & " on leading and " &
integer'image(cap_on_trail) & " on trailing";
errs := errs + 1;
end if;
if n_preload /= 0 then
report " FAIL: CPHA=1 produced " & integer'image(n_preload) &
" preloads";
errs := errs + 1;
end if;
report " the mapping: CPHA=0 captures on all eight leading edges and preloads once; CPHA=1 captures on all eight trailing edges and never preloads";
-- 2. CPOL IS ABSENT FROM THE MAPPING.
cpha <= '0';
cpol <= '0'; clear_dut; clear_counts;
drive('0', 16, 4);
k := cap_on_lead;
cpol <= '1'; clear_dut; clear_counts;
drive('1', 16, 4);
if cap_on_lead /= k or cap_on_trail /= 0 then
report " FAIL: inverting CPOL changed the mapping";
errs := errs + 1;
end if;
if cpol_mismatch = '1' then
report " FAIL: a matched polarity was reported as a mismatch";
errs := errs + 1;
end if;
report " inverting CPOL on both sides leaves the mapping identical -- " &
integer'image(cap_on_lead) &
" leading captures either way -- and reports no mismatch";
-- 3. A POLARITY MISMATCH is caught at the assert, before a bit moves.
cpol <= '0'; cpha <= '0'; clear_dut; clear_counts;
sclk_pin <= '1'; cs_n_pin <= '1'; adv(10);
cs_n_pin <= '0'; adv(6);
if cpol_mismatch /= '1' then
report " FAIL: an idle level of 1 against a configured 0 was not reported";
errs := errs + 1;
end if;
if n_cap /= 0 then
report " FAIL: the mismatch was reported only after " &
integer'image(n_cap) & " captures";
errs := errs + 1;
end if;
cs_n_pin <= '1'; adv(10); sclk_pin <= '0'; adv(10);
report " a polarity mismatch is reported at the assert, with no bit having moved";
-- 4. AN ABORT LEAVING SCLK AWAY FROM IDLE must NOT be reported.
clear_dut;
sclk_pin <= '0'; cs_n_pin <= '1'; adv(10);
cs_n_pin <= '0'; adv(6);
for j in 0 to 4 loop
if (j mod 2) = 0 then sclk_pin <= '1'; else sclk_pin <= '0'; end if;
adv(4);
end loop;
cs_n_pin <= '1'; -- deasserted with SCLK still high
adv(10);
if cpol_mismatch = '1' then
report " FAIL: an abort that left SCLK away from idle was reported as a polarity mismatch";
errs := errs + 1;
end if;
sclk_pin <= '0'; adv(10);
report " a transaction aborted with SCLK left high is not reported as a polarity mismatch, because the check is at the assert rather than continuous";
-- 5. A MATCHED PHASE NEVER SEES MOSI IN MOTION. Establishing the zero is
-- the whole basis of the detector -- a count that were merely "usually
-- small" would make the threshold a guess.
clear_dut;
for kk in 0 to 3 loop
drive_mode('0', '0', 8, 4, x"6B", kk mod 2); -- matched, lag 0 and lag 1
end loop;
if to_integer(moved_run) /= 0 then
report " FAIL: a matched phase saw MOSI in motion " &
integer'image(to_integer(moved_run)) & " times";
errs := errs + 1;
end if;
if phase_suspect = '1' then
report " FAIL: a matched phase was diagnosed as a phase mismatch";
errs := errs + 1;
end if;
report " four transactions with a matched phase, two of them with the one-cycle output lag a real master has: MOSI was in motion within a cycle of zero of the samples, which is what makes the detector exact rather than statistical";
-- 6. A MISMATCHED PHASE IS SEEN IMMEDIATELY. The master launches on the edge
-- the slave samples, so MOSI is moving at every sample where the bit
-- changes -- and one transaction's worth of changes is already enough.
clear_dut;
drive_mode('1', '0', 8, 4, x"6B", 0); -- master CPHA=1, slave CPHA=0
if to_integer(moved_run) = 0 then
report " FAIL: a phase mismatch produced no captures with MOSI in motion";
errs := errs + 1;
end if;
if phase_suspect /= '1' then
report " FAIL: a phase mismatch was not diagnosed after one transaction, with " &
integer'image(to_integer(moved_run)) & " motion events";
errs := errs + 1;
end if;
report " one transaction from a phase-mismatched master: MOSI was in motion at " &
integer'image(to_integer(moved_run)) &
" captures and the mismatch is diagnosed within that single transaction";
-- 7. THE EDGE COUNT AGREES, WHICH IS WHY COUNTING EDGES CANNOT DETECT IT.
-- This is the negative half of the claim, and the reason the detector had
-- to be built out of a motion rather than out of a count.
if txn_trunc = '1' then
report " FAIL: a phase mismatch truncated the transaction after all";
errs := errs + 1;
end if;
if txn_clean /= '1' then
report " FAIL: a phase-mismatched transaction was not a whole number of frames";
errs := errs + 1;
end if;
report " and that same transaction was classified CLEAN -- a whole number of frames -- so no amount of edge counting would have found it";
-- 7b. THE SAME MISMATCH FROM A MASTER WITH A REGISTERED OUTPUT, which is the
-- case a real bus presents and the reason the window is a window. The bit
-- lands one cycle after the edge that launched it, so a detector testing
-- for exact coincidence would see nothing here.
clear_dut;
drive_mode('1', '0', 8, 4, x"6B", 1);
if to_integer(moved_run) = 0 then
report " FAIL: a phase mismatch from a master with a one-cycle output lag produced no motion events, which is the case that matters on a real bus";
errs := errs + 1;
end if;
if phase_suspect /= '1' then
report " FAIL: a phase mismatch from a master with a one-cycle output lag was not diagnosed";
errs := errs + 1;
end if;
report " the same mismatch from a master whose MOSI appears one cycle after the launching edge -- which is what a registered output does and what Chapter 14.10 actually measured -- is still caught, at " &
integer'image(to_integer(moved_run)) &
" samples, because the window is one cycle wide either side rather than an exact coincidence";
-- 8. A BIT-ORDER MISMATCH PRODUCES NOTHING, and that is the claim. Every
-- edge arrives, every frame completes, the idle level is right, MOSI is
-- never in motion at a sample -- there is no observation available, so no
-- flag may fire.
clear_dut;
for j in 0 to 3 loop
drive_mode('0', '0', 8, 4, x"6B", j mod 2);
end loop;
if cpol_mismatch = '1' or phase_suspect = '1' or
to_integer(trunc_run) /= 0 or to_integer(moved_run) /= 0 then
report " FAIL: well-formed transactions produced a diagnosis";
errs := errs + 1;
end if;
report " four well-formed transactions -- which is what a bit-order mismatch looks like -- produce no diagnosis at all, because none is available";
-- 9. CAPTURE AND LAUNCH NEVER COINCIDE.
if both /= 0 then
report " FAIL: " & integer'image(both) &
" cycles carried both a capture and a launch";
errs := errs + 1;
end if;
report " across the whole run no cycle carried both a capture and a launch";
errors <= errs;
if errs = 0 then
report "PASS: the mapping is Chapter 13.5's, with CPOL already consumed by the front end, so CPHA=0 captures on every leading edge and preloads once while CPHA=1 captures on every trailing edge and never preloads, and inverting CPOL on both sides leaves the strobe counts identical -- a polarity mismatch is caught at the assert before a single bit has moved while a transaction aborted with SCLK left away from idle is correctly not reported, because the check is at the assert rather than continuous -- a phase mismatch is diagnosed by noticing that MOSI is IN MOTION within one cycle of the moment the slave samples it, which is exact rather than statistical because a matched pair produces that observation exactly zero times whether the master's output is combinational or registered, and it is diagnosed within a single transaction while the same transaction is classified CLEAN, so no amount of edge counting would ever have found it -- and the window is one cycle wide either side rather than an exact coincidence because a real master's registered output puts the bit one cycle after the edge that launched it, which is the margin HALF_MIN - 1 still clears -- and four well-formed transactions, which is exactly what a bit-order mismatch looks like, produce nothing at all, because for ordering there is no observation available and a flag that guessed would be worse than none";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;8. Why a Verification Engineer Cares
Test 5 is the most important test in the chapter and it asserts a zero. The claim is not "a matched pair rarely sees motion" — it is "a matched pair sees motion exactly zero times, provably, because the settle time is at least HALF_MIN - 1 and the window is one cycle". A bench that asserts moved_run < 3 would pass a design whose matched count was 2, and that design's threshold would be a guess.
The master model needs an output-lag parameter, and the bench must sweep it. This is the lesson from §4's second correction, expressed as a bench requirement. A master model that changes MOSI exactly on the edge is a model of no real master, and a detector verified only against it finds nothing on a bus. lag is a first-class parameter of the drive task, and both 0 and 1 are exercised in the matched and mismatched cases.
Test 9 asserts that nothing is reported, and it must not be deleted. It is the test that stops someone adding a bit-order heuristic later. A suite whose only tests are of things that fire cannot express "and this must not".
Test 4 is the one that keeps the flag usable. Checking that a legitimate condition does not set a flag is worth as much as checking that an illegitimate one does, because a flag with false positives gets masked and takes the true positives with it.
// Properties for the mode logic.
property p_cap_launch_exclusive;
// Capture and launch never coincide. They are defined against opposite
// edges, so a cycle carrying both means the edge strobes coincided -- which
// is the failure Chapter 14.1's ratio precondition exists to prevent.
@(posedge clk) disable iff (!rst_n)
not (cap_stb && launch_stb);
endproperty
property p_no_preload_in_cpha1;
// The preload exists only in CPHA=0. A preload in CPHA=1 drives a bit the
// master will never sample and consumes one from the shift register, so
// every subsequent bit is shifted.
@(posedge clk) disable iff (!rst_n)
preload_stb |-> !cpha;
endproperty
property p_strobes_gated_by_txn;
// Neither strobe fires outside a transaction, which is what lets every
// downstream block treat them as unconditional.
@(posedge clk) disable iff (!rst_n)
(cap_stb || launch_stb) |-> txn_active;
endproperty
property p_cpol_checked_only_at_assert;
// The flag can only be set on an assert cycle. This is the property that
// keeps it clear through an abort that left SCLK away from idle.
@(posedge clk) disable iff (!rst_n)
(!cpol_mismatch && !cs_assert_stb) |=> !cpol_mismatch;
endproperty
property p_diagnoses_sticky;
// Neither diagnosis clears except on command. An intermittent mismatch
// reads clear from a live flag whenever anybody looks.
@(posedge clk) disable iff (!rst_n)
!clr_flags |=> (cpol_mismatch >= $past(cpol_mismatch) &&
phase_suspect >= $past(phase_suspect));
endproperty
property p_matched_phase_never_moves;
// The exactness claim, as a property rather than a bench count: with a
// legal half-period, a sample never coincides with a MOSI transition
// within one cycle. Bind this with the master's phase forced equal to the
// slave's and it must hold for the whole run.
@(posedge clk) disable iff (!rst_n)
(cap_stb && phases_match) |-> (moved_run == $past(moved_run));
endproperty// Coverage. The axes are the two configuration bits on each side -- four
// combinations of agreement, not two -- crossed with the master's output lag,
// because the lag is what decides whether the detector sees anything.
covergroup cg_mode @(posedge clk iff txn_report_stb);
option.per_instance = 1;
// Agreement is the axis, not the value: both-zero and both-one are the same
// case for this block, and the two disagreements are different from each
// other.
phase_rel: coverpoint {master_cpha, cpha} {
bins match_0 = {2'b00};
bins match_1 = {2'b11};
bins mism_m1_s0 = {2'b10};
bins mism_m0_s1 = {2'b01};
}
pol_rel: coverpoint {master_cpol, cpol} {
bins match_0 = {2'b00};
bins match_1 = {2'b11};
bins mism_a = {2'b10};
bins mism_b = {2'b01};
}
// The master's MOSI output lag in system clocks. Zero is a model; one is
// what a real master does; two would be a master with an extra pipeline
// stage and is worth covering because the window is one cycle wide.
lag: coverpoint master_output_lag {
bins combinational = {0};
bins registered = {1};
bins pipelined = {2};
bins slow = {[3:$]};
}
// Whether the data had transitions at all, because the detector needs one.
variety: coverpoint mosi_transitions_in_txn {
bins none = {0}; // all-zeros or all-ones: blind
bins one = {1};
bins some = {[2:5]};
bins many = {[6:$]};
}
x_phase_lag: cross phase_rel, lag;
x_phase_variety: cross phase_rel, variety;
endgroup9. Why an FPGA or ASIC Engineer Cares
The two strobe expressions are the only combinational logic in the block and they are three gates each. txn_active & (cpha ? edge_b_stb : edge_a_stb) is a mux and an AND. That is the entire cost of supporting all four SPI modes in a slave, and it is worth stating because mode support is often assumed to be expensive.
The phase detector is three flops and four gates. mosi_q_d, mosi_chg_d, cap_stb_d, and the three-term OR. An exact diagnosis of the hardest-to-attribute configuration fault in SPI costs less than a byte of the receive buffer.
Nothing here is on a critical path, and the block has no arithmetic at all beyond two saturating counters of CNT_W = 4. If a slave fails timing it is not here.
The one implementation hazard is a synthesis tool merging mosi_q_d into the front end's synchroniser chain. It is a flop whose input is a flop's output with no logic between them — the exact shape a retiming or SRL-inference pass looks for. Merging it does not change behaviour, but it does change which flops carry the ASYNC_REG attribute, and a tool that pulls mosi_q_d into the chain and then retimes the whole thing has altered the synchroniser depth the design's correctness rests on. The defence is the same as Chapter 14.1's: attribute the chain explicitly, and keep this block's register out of it.
10. Failure Signature — Plausible Data From A Device That Reports Nothing Wrong
The symptom:
"The device responds. The data is wrong in a way that is not obviously random — some bytes are right.
txn_cleanis set, the edge count is exactly what we expect, and no fault flag is raised."
What is happening: a CPHA mismatch, on a design without §4's detector. The edge count agrees because a mismatched pair exchanges a whole number of frames. The data is nearly right because the slave samples each bit at the moment it changes, so it reads the old value or the new one depending on where the master's output happened to be — which for a pattern with few transitions is correct most of the time.
Why this is the worst diagnostic position in the module: every observation the system can make says the transaction was fine. The flags are clear, the count is right, and the data is plausible. There is nothing to escalate.
How §4's detector changes that: phase_suspect fires within one transaction, moved_seen reports how many samples caught MOSI moving, and the two together say "the clock and the data are moving at the same time" — which points directly at CPHA and at nothing else. And the reason it must be a motion detector rather than a count is that the count will never disagree.
11. Common Misconceptions
"A CPHA mismatch truncates every transaction." It does not. A CPHA=1 master sending N bits produces 2N edges and a CPHA=0 slave takes the N leading ones — a whole number of frames, every time. This chapter shipped that claim and had to withdraw it; the arithmetic is one line and it is worth doing rather than trusting.
"A phase mismatch can be found by counting something." Not by counting edges, frames or words, because all three agree. It is found by observing that MOSI is in motion at the sample, which is a relationship rather than a count.
"The phase detector is statistical, so the threshold is a tuning parameter." It is exact: a matched pair produces zero motion events, provably, because MOSI settles at least HALF_MIN - 1 cycles before the sample and the window is one cycle wide. The threshold of three ignores a one-off glitch; it does not separate two distributions.
"Testing for MOSI changing exactly at the capture is the same as testing for a window." It is not, and the difference is the whole difference between a detector that works on a bus and one that works only against an idealised model. A real master registers its output, so the change lands one cycle after the edge.
"A continuous polarity check is strictly better than one at the assert." It fires on every aborted transaction, where SCLK legitimately sits away from idle, so it is a flag with false positives — and a flag with false positives is masked, which loses the true positives too.
"The slave should at least warn about a possible bit-order mismatch." On what evidence? Both orders produce a well-formed word of the right length at the right time. A heuristic would be right often enough to be trusted and wrong often enough to mislead.
12. Reason It Through
Q. A CPHA=1 master sends four 8-bit words to a CPHA=0 slave. How many edges arrive, how many words does the slave assemble, and what does Chapter 14.2 report?
64 edges, four words, and txn_clean with frames_in_txn = 4. Every count agrees with what a correctly configured pair would produce. The four words are wrong — each bit was sampled as it changed — but nothing arithmetical distinguishes this from a correct transaction, which is exactly why the detection has to be physical.
Q. Why is the motion count for a matched pair exactly zero rather than merely small?
Because a matched master launches half a period from the slave's capture, and the half-period is at least HALF_MIN cycles. With a registered output the bit lands HALF_MIN - 1 cycles before the sample at worst, which at HALF_MIN = 3 is 2 cycles. The window covers one cycle either side of the sample, so 2 is outside it. The zero is a consequence of the ratio precondition, not an empirical observation.
Q. Why is the window one cycle wide rather than two? What would widening it cost?
Because HALF_MIN - 1 = 2 at the legal minimum half-period, so a two-cycle window would include the matched case and the detector would fire on correct configurations. One cycle is the widest window the precondition still clears, and it clears it by exactly one cycle. Widening it would trade a false-negative risk for a false-positive certainty at the minimum ratio — and a flag that fires on a correct master is worse than one that occasionally misses.
Q. The master sends 0x00 repeatedly to a phase-mismatched slave. What does the detector report, and is that a bug?
Nothing, and it is a stated limitation rather than a bug. The detector needs a transition to observe; a constant pattern is sampled at the wrong moment and looks identical to being sampled at the right one. The diagnosis appears as soon as the data has any variety in it, which in practice is the first real payload. Documenting that is the honest response; the alternative would be a detector that inferred a mismatch from the absence of evidence.
Q. A reviewer asks why trunc_run still exists in this block if truncation does not indicate a phase mismatch. What is the answer?
Because a run of truncated transactions still means something — it means a run of transactions that ended mid-frame, which is what a repeatedly aborting master or a driver with a wrong byte count produces. Chapter 14.7 consumes it for that. What changed is the interpretation: the counter is not evidence about the phase and was never evidence about the phase, and the block's comments now say so where they previously said the opposite.
13. Understanding Check
14. Summary
CPOL was consumed in Chapter 14.1, so the mode table is two rows: CPHA=0 captures on leading edges and preloads once; CPHA=1 captures on trailing edges and never preloads. CPOL appears in neither expression, and supporting all four modes costs about six gates.
A polarity mismatch is detectable from a level — SCLK not at its idle level when the select asserts — and it is the only mismatch caught before a bit has moved. The check is at the assert rather than continuous, because a continuous check fires on every abort and a flag with false positives gets masked.
A phase mismatch is not detectable from the edge count, and this chapter shipped the opposite claim first. A CPHA=1 master sending N bits produces 2N edges and a CPHA=0 slave takes the N leading ones — a whole number of frames, every time, classified clean. The wrong version passed its tests; what found the error was doing the arithmetic.
What does detect it is physical: MOSI is in motion within one cycle of the sample. It is exact — a matched pair produces exactly zero motion events, because the bit settles at least HALF_MIN - 1 cycles early and the window is one cycle wide. The window is a window rather than a coincidence because a real master registers its MOSI output, so the change lands one cycle after the edge — which only integration found.
A bit-order mismatch is not detectable at all, and reporting nothing is the right answer rather than a gap. Level, relationship, ordering: decreasing observability, because each has less to be compared against than the last.
The detector's honest limit is that it needs a transition to observe, so a constant data pattern is blind. trunc_run remains published, for aborts rather than for the phase.
For verification: assert the zero, not a small number; parameterise the master model's output lag and sweep it, because the default of zero verifies a detector that does not work; and keep the test that asserts a bit-order mismatch produces nothing.
For implementation: two muxes for the mapping, three flops for the detector, no arithmetic worth constraining — and one hazard, a synthesis tool merging mosi_q_d into the front end's synchroniser chain and retiming the result.
15. What Comes Next
The slave handles all four modes and reports the two mismatches it can see. It has been assuming that transactions contain a whole number of whole words.
Chapter 14.7 — Transfer Width, Partial Frames, and Aborts removes that assumption. A master may deselect mid-word, and the bits already captured are real data that something has to decide about. The chapter's central decision is that complete words and partial ones travel on separate channels — so that a consumer cannot accidentally treat a fragment as a word — and it derives why a single channel with a length field is the more obvious design and the worse one.
Continue learning
Related tutorials
- Related topic
Identifying the Required SPI Mode
The two observations that read CPOL and CPHA off any vendor timing diagram, why the picture is more trustworthy than the prose beside it, why trying all four modes cannot work, and the observer that infers the mode from a live capture.
- Related topic
CPOL/CPHA-Aware Edge Control
All four SPI modes from one datapath: why the launch and capture assignment depends on CPHA alone, why the two phases are not symmetric, and why CPHA=0 needs both a preload and a suppressed final launch.
- Related topic
Slave Microarchitecture and Clocking Assumptions
The two architectures available to a slave that does not own its clock, the one precondition the chosen architecture rests on and why no simulation can test it, why MOSI must pass through exactly as many flops as SCLK, and a delay-matched front end verified in three HDLs.
- Related topic
CS Detection and Transaction Boundaries
Chip select is SPI's only framing and therefore its only resynchronisation point: why counters must reset on assert, how to classify a transaction with a running remainder instead of a divider, why a CPHA mismatch is invisible to the edge count, and a transaction detector verified in three HDLs.
