SPI · Module 10
Extracting Setup, Hold, and Maximum SCLK
Which timing-table rows constrain you and which constrain the device, why a number without its load and corner is not a specification, how a delay on one line alone destroys margin, and the monitor that measures the real link against the datasheet.
Chapter 10.2 got the mode out of the picture. The table beneath the picture holds the rest of pass two — and it is the part most often copied into a spreadsheet without being understood.
The timing table has nine rows. Which of them constrain your design, which constrain the device, and which are only true under conditions your board does not meet?
Getting this wrong does not produce an error message. It produces a design that works on three boards and fails on the fourth.
1. Every Number Constrains Somebody — Work Out Who
The single most useful habit with a timing table is to write, next to each row, whose obligation it is. There are only two answers, and confusing them is the commonest error in the whole exercise.
t_SU (SDI/MOSI setup) YOUR obligation — you must present data this early
t_HD (SDI/MOSI hold) YOUR obligation — you must hold it this long
t_CSS (CS lead) YOUR obligation — CS this far before the first edge
t_CSH (CS lag) YOUR obligation — CS held this far after the last
t_CS (CS high) YOUR obligation — minimum deselect between frames
t_V (SDO/MISO valid) THE DEVICE'S — it promises data by this time
t_DIS (output disable) THE DEVICE'S — it promises high-Z by this time
t_HO (output hold) THE DEVICE'S — it promises to hold this long
f_SCLK(max) BOTH — a limit on you, derived from the
device's internal timingThe asymmetry is the point. Your obligations are things your controller must do; the device's are things you may rely on. A number in the wrong column produces a design that either over-constrains itself for no reason or — far worse — assumes a guarantee the device never made.
The naming makes this harder than it should be. SDI and SDO are named from the device's point of view, so SDI is your MOSI and SDO is your MISO. A row labelled "SDI setup time" constrains what you drive. Chapter 10.2's worked example turned on exactly this distinction, and it catches people just as often here.
2. The Conditions Are Part of the Number
A timing value without its conditions is not a specification. Every table has a header or footnote giving:
load capacitance often 15 pF, sometimes 30 pF
supply voltage a range, with the number quoted at one point
temperature a range, with the number quoted at one point
output drive setting for parts with configurable driveLoad is the one that bites. t_V is measured driving a specified capacitance. Your board presents its own trace capacitance plus every other device's input capacitance plus the controller's input — routinely two or three times the quoted load. The device's output takes longer to reach a valid level, so the effective t_V on your board is larger than the table's, and Chapter 9.4 showed t_V dominates the frequency budget.
Voltage and temperature are the ones that get skipped. A number quoted at 25 °C and nominal supply is a typical-corner number. If your product ships into an enclosure that reaches 70 °C, the slow-corner value is what your design must meet — and that is often 20 to 30 per cent worse.
3. Setup and Hold — What You Must Guarantee
t_SU and t_HD bracket the device's sampling edge. Your controller must present MOSI stable for t_SU before that edge and keep it stable for t_HD after it.
On a normally-clocked master this is nearly free, because the master launches on the opposite edge to the one the device samples on — so both the setup and the hold are naturally close to half a period. The arithmetic only becomes tight in three situations:
- Very high clock rates, where half a period approaches the numbers themselves.
- A master that launches on the same edge the device samples on — a mode error, which Chapter 10.2 showed presents as a rate-dependent failure precisely because this margin collapses.
- Level shifters or buffers on MOSI only, which add delay to the data path without adding it to the clock. That asymmetry eats directly into setup, and it is easy to introduce without noticing because the part looks like a simple wire on the schematic.
The third is worth dwelling on: any delay added to one line and not the other is a direct subtraction from the margin. A single-line buffer, a series resistor on only one net, or a longer route for MOSI than SCLK all do this.
4. t_V — What the Device Guarantees
t_V is the time from the launching edge to MISO being valid. It is the device's promise, it is the largest term in Chapter 9.4's budget, and it is the number to look up before choosing the part if rate matters.
Two subtleties:
t_Vis specified from a particular edge, and which edge depends on the mode. Reading it as "from the sampling edge" instead of "from the launching edge" gives an answer half a period out.- Its companion
t_HO— how long the device holds the previous value — is what guarantees you a hold margin on the return path. A part that specifiest_Vand nott_HOhas told you when data becomes valid and not how long it stays, and at high rates that omission matters.
5. Reading the Three Intervals Off a Capture
t_SU, t_HD and t_V on one bit time
10 cyclesThree intervals, three owners. From the launch to the sampling edge is t_SU and it is yours. From the sampling edge to the next change is t_HD and it is also yours. From the sampling edge to MISO becoming valid is t_V and it is the device's.
Notice that your two obligations together span almost the whole bit period, which is why they are comfortable at moderate rates — and why anything that delays MOSI relative to SCLK subtracts from one without adding to the other.
6. Maximum SCLK Is a Derived Number
The table's f_SCLK(max) is the device's own internal limit. The rate your link can actually run is the smaller of that and what the round trip permits, and Chapter 9.4 gives that arithmetic in full.
What belongs here is the extraction discipline:
- Find which command each rate applies to. A single headline number usually belongs to the fast-read command only.
- Find the load it assumes and compare with your board's.
- Find the corner it assumes and derate for yours.
- Then take it into Chapter 9.4's budget as one term among four, rather than as the answer.
A device rated 104 MHz whose t_V is 7 ns cannot run at 104 MHz on any ordinary board — the two numbers in the same table are describing different things, and only one of them is about your link.
7. Building the Timing Monitor — Three HDLs
The circuit
Circuit. An oversampling instrument that measures the intervals of §5 on the live bus.
State. How long MOSI has been stable; a hold counter; the worst case of each; sticky violation flags; a count of sampling edges.
Datapath. At each sampling edge the stability counter is the setup. Immediately afterwards a second counter runs until MOSI next changes, and that is the hold. Both are compared against parameters carrying the datasheet's numbers.
Control. Which edge samples comes from CPHA — numbering edges from 1, CPHA=0 samples on the odd ones — so this block sits downstream of Chapter 10.2's mode and Chapter 10.1's profile.
Clock and reset. An oversampling clock; asynchronous active-low reset; a clear that restarts the measurement window so one frame's verdict is never carried into the next.
Enables. The violation flags are sticky within a window, because a violation that occurred once and then stopped is still a violation.
Timing. Both the clock edge and the data transition pass through the same two synchroniser stages, so the common delay cancels in the difference. That is the property that makes a synchronised measurement of an interval trustworthy even though a synchronised measurement of an instant is not.
Synthesis. Two counters, two comparators, three synchronisers. Small enough to leave in a debug build permanently.
Limitations. Its resolution is one oversampling period and no finer. A monitor clocked at ten times SCLK can say a setup is about a tenth of a half period; it cannot say whether a 2 ns requirement is met. It catches the gross violations that static timing analysis never sees — a wrong divisor, a wrong mode, a device slower than its datasheet — none of which appear in a timing netlist.
// spi_su_h_monitor.sv
//
// Chapter 10.3 -- turning a timing table into something you can check.
//
// A datasheet gives t_SU and t_HD as numbers. They only become engineering
// when something measures the real link against them. This block
// oversamples the bus and, at every SAMPLING edge, reports:
//
// setup -- how long MOSI had already been stable when the edge arrived,
// hold -- how long MOSI stayed stable after it,
//
// keeping the worst case of each and raising a sticky flag when either
// falls below its limit.
//
// Which edge samples comes from CPHA, so this block is downstream of the
// mode in Chapter 10.2 and of the profile in Chapter 10.1: CPHA=0 samples
// on the leading (odd) edges, CPHA=1 on the trailing (even) ones.
//
// RESOLUTION. Every number here is in oversampling-clock periods, so the
// measurement resolves to one period and no finer. A monitor clocked at
// ten times SCLK can tell you a setup is "about a tenth of a half period";
// it cannot tell you whether a 2 ns requirement is met. Sub-nanosecond
// verification belongs in static timing analysis -- this block is for
// catching the gross violations that STA never sees because they arise
// from a wrong divisor, a wrong mode, or a device slower than its
// datasheet, none of which appear in a timing netlist.
module spi_su_h_monitor #(
parameter int T_W = 12, // width of the measurement counters
parameter int T_SU_MIN = 8, // required setup, in oversample periods
parameter int T_HD_MIN = 4 // required hold, in oversample periods
) (
input logic clk, // oversampling clock
input logic rst_n,
input logic sclk, // observed bus
input logic mosi,
input logic cs_n,
input logic cpol, // from the device profile
input logic cpha,
input logic clear, // restart the measurement window
output logic [T_W-1:0] min_setup,
output logic [T_W-1:0] min_hold,
output logic [T_W-1:0] samples, // sampling edges observed
output logic su_viol, // sticky: a setup fell below T_SU_MIN
output logic hd_viol // sticky: a hold fell below T_HD_MIN
);
logic sclk_s1, sclk_s2, sclk_q;
logic mosi_s1, mosi_s2, mosi_q;
logic cs_s1, cs_s2, cs_q;
logic [T_W-1:0] since_change; // MOSI stable for this many periods
logic [T_W-1:0] hold_cnt;
logic in_hold;
logic any_edge;
logic edge_odd;
wire sclk_edge = (sclk_s2 != sclk_q);
wire mosi_edge = (mosi_s2 != mosi_q);
wire in_frame = (cs_s2 == 1'b0);
wire cs_fall = (cs_q == 1'b1) && (cs_s2 == 1'b0);
wire next_odd = any_edge ? ~edge_odd : 1'b1;
// The sampling edge is the leading edge when CPHA=0 and the trailing
// edge when CPHA=1 -- which, numbering edges from 1, is exactly the odd
// edges for CPHA=0 and the even ones for CPHA=1. CPOL does not enter:
// it decides which physical direction "leading" is, and the parity
// argument is indifferent to that. The port is kept because a reader
// checking this block against a datasheet expects both halves of the
// mode to be present, and because a future revision that timestamps
// rising and falling edges separately will need it.
wire sample_now = in_frame && sclk_edge && (next_odd != cpha);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_s1 <= 1'b0; sclk_s2 <= 1'b0; sclk_q <= 1'b0;
mosi_s1 <= 1'b0; mosi_s2 <= 1'b0; mosi_q <= 1'b0;
cs_s1 <= 1'b1; cs_s2 <= 1'b1; cs_q <= 1'b1;
since_change <= {T_W{1'b0}};
hold_cnt <= {T_W{1'b0}};
in_hold <= 1'b0;
any_edge <= 1'b0;
edge_odd <= 1'b0;
min_setup <= {T_W{1'b1}};
min_hold <= {T_W{1'b1}};
samples <= {T_W{1'b0}};
su_viol <= 1'b0;
hd_viol <= 1'b0;
end else begin
sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
cs_s1 <= cs_n; cs_s2 <= cs_s1; cs_q <= cs_s2;
if (clear) begin
min_setup <= {T_W{1'b1}};
min_hold <= {T_W{1'b1}};
samples <= {T_W{1'b0}};
su_viol <= 1'b0;
hd_viol <= 1'b0;
end
// How long MOSI has been stable. Both this and the SCLK edge
// detection pass through the same two synchroniser stages, so
// the common delay cancels in the difference -- which is why a
// synchronised measurement of an INTERVAL is trustworthy even
// though a synchronised measurement of an INSTANT is not.
// The counter is reloaded with ONE, not zero, on a transition.
// The transition is detected at the end of a period, so the
// first period of stability is the one that has already
// elapsed. Reloading with zero reports every interval one
// period short -- a constant bias that would quietly turn a
// measurement against a datasheet limit into a measurement
// against that limit plus one period.
if (mosi_edge) since_change <= {{(T_W-1){1'b0}}, 1'b1};
else since_change <= since_change + 1'b1;
if (cs_fall) begin
any_edge <= 1'b0;
edge_odd <= 1'b0;
in_hold <= 1'b0;
end else if (in_frame && sclk_edge) begin
any_edge <= 1'b1;
edge_odd <= next_odd;
end
// SETUP, measured at the sampling edge.
if (sample_now) begin
samples <= samples + 1'b1;
if (since_change < min_setup) min_setup <= since_change;
if (since_change < T_W'(T_SU_MIN)) su_viol <= 1'b1;
// Begin measuring the hold of the bit just sampled.
in_hold <= 1'b1;
hold_cnt <= {{(T_W-1){1'b0}}, 1'b1}; // same reasoning
end else if (in_hold) begin
if (mosi_edge) begin
// HOLD ends at the first change after the sampling edge.
if (hold_cnt < min_hold) min_hold <= hold_cnt;
if (hold_cnt < T_W'(T_HD_MIN)) hd_viol <= 1'b1;
in_hold <= 1'b0;
end else begin
hold_cnt <= hold_cnt + 1'b1;
end
end
end
end
endmodule// spi_su_h_monitor_tb.sv
//
// The testbench drives frames whose setup and hold are KNOWN by
// construction, then requires the monitor to measure them back. Checking
// a measurement instrument against a value it was told is circular, so
// every expectation here is computed from the stimulus timing rather than
// from the monitor's own output.
`timescale 1ns/1ps
module spi_su_h_monitor_tb;
localparam int T_W = 12;
localparam int T_SU_MIN = 8;
localparam int T_HD_MIN = 4;
localparam int H = 20; // SCLK half period, ns
localparam int TCLK = 2; // oversampling period, ns
logic clk = 1'b0;
logic rst_n = 1'b0;
always #(TCLK/2) clk = ~clk;
logic sclk = 1'b0;
logic mosi = 1'b0;
logic cs_n = 1'b1;
logic clear = 1'b0;
logic [T_W-1:0] min_setup, min_hold, samples;
logic su_viol, hd_viol;
int errors = 0;
spi_su_h_monitor #(.T_W(T_W), .T_SU_MIN(T_SU_MIN), .T_HD_MIN(T_HD_MIN)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(sclk), .mosi(mosi), .cs_n(cs_n),
.cpol(1'b0), .cpha(1'b0), .clear(clear),
.min_setup(min_setup), .min_hold(min_hold), .samples(samples),
.su_viol(su_viol), .hd_viol(hd_viol)
);
task automatic restart;
begin
@(negedge clk); clear = 1'b1;
@(negedge clk); clear = 1'b0;
@(negedge clk);
end
endtask
// Mode 0. Each bit is launched skew ns AFTER the trailing edge, so the
// setup seen at the next leading edge is H - skew and the hold after
// that leading edge is H + skew.
task automatic drive_setup_case(input int skew, input logic [7:0] data);
int i;
begin
sclk = 1'b0; cs_n = 1'b1; #(H*2);
cs_n = 1'b0;
#(skew); mosi = data[7]; #(H-skew);
for (i = 0; i < 8; i++) begin
sclk = 1'b1; // leading edge: SAMPLE
#H;
sclk = 1'b0; // trailing edge: LAUNCH
if (i < 7) begin
#(skew); mosi = data[6-i]; #(H-skew);
end else begin
#H;
end
end
cs_n = 1'b1; #(H*2);
end
endtask
// A slave with excessive clock-to-out: the bit changes late ns AFTER
// the leading edge that was supposed to sample the previous one. The
// hold is then only late ns, and the setup for the following bit is
// correspondingly generous -- which is exactly why a design can fail
// hold while its setup margin looks comfortable.
task automatic drive_hold_case(input int late, input logic [7:0] data);
int i;
begin
sclk = 1'b0; cs_n = 1'b1; #(H*2);
cs_n = 1'b0;
mosi = data[7];
#H;
for (i = 0; i < 8; i++) begin
sclk = 1'b1; // leading edge: SAMPLE
if (i < 7) begin
#(late); mosi = data[6-i]; #(H-late);
end else begin
#H;
end
sclk = 1'b0; // trailing edge
#H;
end
cs_n = 1'b1; #(H*2);
end
endtask
task automatic expect_near(input string what, input int got, input int want);
begin
// The monitor resolves to its own clock and no finer, so one
// period of tolerance is allowed -- but no systematic offset
// is: these expectations are the true constructed intervals,
// and a constant bias would show up as every one of them
// sitting at the same edge of the window.
if (got < want-1 || got > want+1) begin
$display(" FAIL: %s measured %0d periods, expected %0d +/-1",
what, got, want);
errors++;
end
end
endtask
initial begin
repeat (5) @(negedge clk);
rst_n = 1'b1;
restart();
// 1. A comfortable link. Setup and hold are both a half period.
drive_setup_case(0, 8'hA5);
$display(" clean frame: setup=%0d hold=%0d samples=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, samples, su_viol, hd_viol);
expect_near("clean setup", min_setup, H/TCLK);
expect_near("clean hold", min_hold, H/TCLK);
if (samples !== T_W'(8)) begin
$display(" FAIL: %0d sampling edges counted in an 8-bit frame", samples);
errors++;
end
if (su_viol || hd_viol) begin
$display(" FAIL: a comfortable link reported a violation"); errors++;
end
// 2. Data launched late relative to the trailing edge. Setup shrinks
// by exactly the skew; hold grows by it.
restart();
drive_setup_case(8, 8'hA5);
$display(" 8 ns skew: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, su_viol, hd_viol);
expect_near("skewed setup", min_setup, (H-8)/TCLK);
expect_near("skewed hold", min_hold, (H+8)/TCLK);
if (!su_viol) begin
$display(" FAIL: a setup of %0d periods did not trip T_SU_MIN=%0d",
min_setup, T_SU_MIN);
errors++;
end
if (hd_viol) begin
$display(" FAIL: hold flagged while hold was growing"); errors++;
end
// 3. The converse: a slave whose output changes just after the
// sampling edge. Hold collapses while setup stays generous --
// the failure that looks fine in a setup-only review.
restart();
drive_hold_case(4, 8'hA5);
$display(" 4 ns clk-to-out: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, su_viol, hd_viol);
expect_near("late-launch hold", min_hold, 4/TCLK);
if (min_hold !== T_W'(4/TCLK)) begin
$display(" NOTE: hold measured %0d, constructed %0d", min_hold, 4/TCLK);
end
if (!hd_viol) begin
$display(" FAIL: a hold of %0d periods did not trip T_HD_MIN=%0d",
min_hold, T_HD_MIN);
errors++;
end
if (su_viol) begin
$display(" FAIL: setup flagged on a frame with generous setup");
errors++;
end
// 4. clear restarts the window: the flags and the minima must not
// carry a previous frame's verdict into a new measurement.
restart();
if (su_viol || hd_viol || samples !== T_W'(0)) begin
$display(" FAIL: clear did not restart the measurement window");
errors++;
end
drive_setup_case(0, 8'hA5);
if (su_viol || hd_viol) begin
$display(" FAIL: a clean frame after a violating one still reports a violation");
errors++;
end
$display(" after clear: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, su_viol, hd_viol);
if (errors == 0)
$display("PASS: setup and hold are measured back to within one oversampling period of their constructed values, each limit trips only its own flag, and clear restarts the window");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmoduleThe testbench never checks the monitor against a number the monitor produced. It builds frames whose setup and hold are known from the stimulus timing and requires those values back — which is the only way to test an instrument, since checking a measurement against itself is circular.
One detail in that RTL is worth extracting, because it is a class of bug rather than an instance. The stability counter is reloaded with one, not zero, when MOSI changes: the transition is detected at the end of a period, so the first period of stability has already elapsed. Reloading with zero reports every interval one period short — a constant bias, which is the most dangerous kind of measurement error because every reading stays self-consistent and the whole set is wrong. Compared against a datasheet limit, that bias silently turns the check into one against the limit plus a period.
The two directed cases are chosen to fail differently. Launching data late relative to the trailing edge shrinks setup while growing hold. Launching it just after the sampling edge — a slave with excessive clock-to-out — collapses hold while leaving setup generous. A monitor that conflated the two would pass both.
// spi_su_h_monitor.v
//
// Chapter 10.3 -- turning a timing table into something you can check,
// in Verilog-2001.
//
// At every SAMPLING edge the block reports how long MOSI had been stable
// (setup) and how long it stays stable afterwards (hold), keeps the worst
// case of each, and raises a sticky flag when either falls below its
// limit. Which edge samples comes from CPHA: numbering the edges inside a
// frame from 1, CPHA=0 samples on the odd edges and CPHA=1 on the even.
//
// RESOLUTION. Every number is in oversampling-clock periods, so the
// measurement resolves to one period and no finer. This block catches the
// gross violations that static timing analysis never sees -- a wrong
// divisor, a wrong mode, a device slower than its datasheet -- and is not
// a substitute for STA on the sub-nanosecond numbers.
module spi_su_h_monitor #(
parameter T_W = 12, // width of the measurement counters
parameter T_SU_MIN = 8, // required setup, in oversample periods
parameter T_HD_MIN = 4 // required hold, in oversample periods
) (
input wire clk, // oversampling clock
input wire rst_n,
input wire sclk, // observed bus
input wire mosi,
input wire cs_n,
input wire cpol, // from the device profile
input wire cpha,
input wire clear, // restart the measurement window
output reg [T_W-1:0] min_setup,
output reg [T_W-1:0] min_hold,
output reg [T_W-1:0] samples, // sampling edges observed
output reg su_viol, // sticky: a setup fell below T_SU_MIN
output reg hd_viol // sticky: a hold fell below T_HD_MIN
);
reg sclk_s1, sclk_s2, sclk_q;
reg mosi_s1, mosi_s2, mosi_q;
reg cs_s1, cs_s2, cs_q;
reg [T_W-1:0] since_change; // MOSI stable for this many periods
reg [T_W-1:0] hold_cnt;
reg in_hold;
reg any_edge;
reg edge_odd;
wire sclk_edge = (sclk_s2 != sclk_q);
wire mosi_edge = (mosi_s2 != mosi_q);
wire in_frame = (cs_s2 == 1'b0);
wire cs_fall = (cs_q == 1'b1) && (cs_s2 == 1'b0);
wire next_odd = any_edge ? ~edge_odd : 1'b1;
// CPOL does not enter the parity argument: it decides which physical
// direction "leading" is, and the parity is indifferent to that. The
// port is kept because a reader checking this block against a datasheet
// expects both halves of the mode to be present.
wire sample_now = in_frame && sclk_edge && (next_odd != cpha);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_s1 <= 1'b0; sclk_s2 <= 1'b0; sclk_q <= 1'b0;
mosi_s1 <= 1'b0; mosi_s2 <= 1'b0; mosi_q <= 1'b0;
cs_s1 <= 1'b1; cs_s2 <= 1'b1; cs_q <= 1'b1;
since_change <= {T_W{1'b0}};
hold_cnt <= {T_W{1'b0}};
in_hold <= 1'b0;
any_edge <= 1'b0;
edge_odd <= 1'b0;
min_setup <= {T_W{1'b1}};
min_hold <= {T_W{1'b1}};
samples <= {T_W{1'b0}};
su_viol <= 1'b0;
hd_viol <= 1'b0;
end else begin
sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
cs_s1 <= cs_n; cs_s2 <= cs_s1; cs_q <= cs_s2;
if (clear) begin
min_setup <= {T_W{1'b1}};
min_hold <= {T_W{1'b1}};
samples <= {T_W{1'b0}};
su_viol <= 1'b0;
hd_viol <= 1'b0;
end
// How long MOSI has been stable. Both this and the SCLK edge
// detection pass through the same two synchroniser stages, so
// the common delay cancels in the difference -- a synchronised
// measurement of an INTERVAL is trustworthy even though a
// synchronised measurement of an INSTANT is not.
//
// The counter is reloaded with ONE, not zero: the transition is
// detected at the end of a period, so the first period of
// stability has already elapsed. Reloading with zero reports
// every interval one period short.
if (mosi_edge) since_change <= {{(T_W-1){1'b0}}, 1'b1};
else since_change <= since_change + 1'b1;
if (cs_fall) begin
any_edge <= 1'b0;
edge_odd <= 1'b0;
in_hold <= 1'b0;
end else if (in_frame && sclk_edge) begin
any_edge <= 1'b1;
edge_odd <= next_odd;
end
// SETUP, measured at the sampling edge.
if (sample_now) begin
samples <= samples + 1'b1;
if (since_change < min_setup) min_setup <= since_change;
if (since_change < T_SU_MIN) su_viol <= 1'b1;
in_hold <= 1'b1;
hold_cnt <= {{(T_W-1){1'b0}}, 1'b1}; // same reasoning
end else if (in_hold) begin
if (mosi_edge) begin
// HOLD ends at the first change after the sampling edge.
if (hold_cnt < min_hold) min_hold <= hold_cnt;
if (hold_cnt < T_HD_MIN) hd_viol <= 1'b1;
in_hold <= 1'b0;
end else begin
hold_cnt <= hold_cnt + 1'b1;
end
end
end
end
endmodule// spi_su_h_monitor_tb.v
//
// The same checks as the SystemVerilog testbench: frames whose setup and
// hold are known by construction, measured back exactly; each limit
// tripping only its own flag; and clear restarting the window.
`timescale 1ns/1ps
module spi_su_h_monitor_tb;
parameter T_W = 12;
parameter T_SU_MIN = 8;
parameter T_HD_MIN = 4;
parameter H = 20; // SCLK half period, ns
parameter TCLK = 2; // oversampling period, ns
reg clk;
reg rst_n;
reg sclk;
reg mosi;
reg cs_n;
reg clear;
wire [T_W-1:0] min_setup, min_hold, samples;
wire su_viol, hd_viol;
integer errors;
initial begin
clk = 1'b0; rst_n = 1'b0;
sclk = 1'b0; mosi = 1'b0; cs_n = 1'b1; clear = 1'b0;
errors = 0;
end
always #(TCLK/2) clk = ~clk;
spi_su_h_monitor #(.T_W(T_W), .T_SU_MIN(T_SU_MIN), .T_HD_MIN(T_HD_MIN)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(sclk), .mosi(mosi), .cs_n(cs_n),
.cpol(1'b0), .cpha(1'b0), .clear(clear),
.min_setup(min_setup), .min_hold(min_hold), .samples(samples),
.su_viol(su_viol), .hd_viol(hd_viol)
);
task restart;
begin
@(negedge clk); clear = 1'b1;
@(negedge clk); clear = 1'b0;
@(negedge clk);
end
endtask
// Mode 0. Each bit is launched skew ns AFTER the trailing edge, so the
// setup seen at the next leading edge is H - skew and the hold after
// that leading edge is H + skew.
task drive_setup_case;
input integer skew;
input [7:0] data;
integer i;
begin
sclk = 1'b0; cs_n = 1'b1; #(H*2);
cs_n = 1'b0;
#(skew); mosi = data[7]; #(H-skew);
for (i = 0; i < 8; i = i + 1) begin
sclk = 1'b1; // leading edge: SAMPLE
#(H);
sclk = 1'b0; // trailing edge: LAUNCH
if (i < 7) begin
#(skew); mosi = data[6-i]; #(H-skew);
end else begin
#(H);
end
end
cs_n = 1'b1; #(H*2);
end
endtask
// A slave with excessive clock-to-out: the bit changes late ns AFTER
// the leading edge that sampled the previous one. The hold is then only
// late ns while the setup for the following bit is generous -- which is
// why a design can fail hold with a comfortable-looking setup margin.
task drive_hold_case;
input integer late;
input [7:0] data;
integer i;
begin
sclk = 1'b0; cs_n = 1'b1; #(H*2);
cs_n = 1'b0;
mosi = data[7];
#(H);
for (i = 0; i < 8; i = i + 1) begin
sclk = 1'b1; // leading edge: SAMPLE
if (i < 7) begin
#(late); mosi = data[6-i]; #(H-late);
end else begin
#(H);
end
sclk = 1'b0; // trailing edge
#(H);
end
cs_n = 1'b1; #(H*2);
end
endtask
task expect_near;
input [8*24:1] what;
input integer got;
input integer want;
begin
// One oversampling period of tolerance -- but no systematic
// offset: these expectations are the true constructed
// intervals.
if (got < want-1 || got > want+1) begin
$display(" FAIL: %0s measured %0d periods, expected %0d +/-1",
what, got, want);
errors = errors + 1;
end
end
endtask
initial begin
repeat (5) @(negedge clk);
rst_n = 1'b1;
restart;
// 1. A comfortable link. Setup and hold are both a half period.
drive_setup_case(0, 8'hA5);
$display(" clean frame: setup=%0d hold=%0d samples=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, samples, su_viol, hd_viol);
expect_near("clean setup", min_setup, H/TCLK);
expect_near("clean hold", min_hold, H/TCLK);
if (samples !== 8) begin
$display(" FAIL: %0d sampling edges counted in an 8-bit frame", samples);
errors = errors + 1;
end
if (su_viol || hd_viol) begin
$display(" FAIL: a comfortable link reported a violation");
errors = errors + 1;
end
// 2. Data launched late relative to the trailing edge. Setup shrinks
// by exactly the skew; hold grows by it.
restart;
drive_setup_case(8, 8'hA5);
$display(" 8 ns skew: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, su_viol, hd_viol);
expect_near("skewed setup", min_setup, (H-8)/TCLK);
expect_near("skewed hold", min_hold, (H+8)/TCLK);
if (!su_viol) begin
$display(" FAIL: a setup of %0d periods did not trip T_SU_MIN=%0d",
min_setup, T_SU_MIN);
errors = errors + 1;
end
if (hd_viol) begin
$display(" FAIL: hold flagged while hold was growing");
errors = errors + 1;
end
// 3. The converse: a slave whose output changes just after the
// sampling edge. Hold collapses while setup stays generous.
restart;
drive_hold_case(4, 8'hA5);
$display(" 4 ns clk-to-out: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, su_viol, hd_viol);
expect_near("late-launch hold", min_hold, 4/TCLK);
if (!hd_viol) begin
$display(" FAIL: a hold of %0d periods did not trip T_HD_MIN=%0d",
min_hold, T_HD_MIN);
errors = errors + 1;
end
if (su_viol) begin
$display(" FAIL: setup flagged on a frame with generous setup");
errors = errors + 1;
end
// 4. clear restarts the window.
restart;
if (su_viol || hd_viol || samples !== 0) begin
$display(" FAIL: clear did not restart the measurement window");
errors = errors + 1;
end
drive_setup_case(0, 8'hA5);
if (su_viol || hd_viol) begin
$display(" FAIL: a clean frame after a violating one still reports a violation");
errors = errors + 1;
end
$display(" after clear: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
min_setup, min_hold, su_viol, hd_viol);
if (errors == 0)
$display("PASS: setup and hold are measured back to within one oversampling period of their constructed values, each limit trips only its own flag, and clear restarts the window");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule-- spi_su_h_monitor.vhd
--
-- Chapter 10.3 -- turning a timing table into something you can check,
-- in VHDL.
--
-- At every SAMPLING edge the block reports how long MOSI had been stable
-- (setup) and how long it stays stable afterwards (hold), keeps the worst
-- case of each, and raises a sticky flag when either falls below its
-- limit. Which edge samples comes from CPHA: numbering the edges inside a
-- frame from 1, CPHA=0 samples on the odd edges and CPHA=1 on the even.
--
-- RESOLUTION. Every number is in oversampling-clock periods, so the
-- measurement resolves to one period and no finer. This block catches the
-- gross violations that static timing analysis never sees -- a wrong
-- divisor, a wrong mode, a device slower than its datasheet -- and is not
-- a substitute for STA on the sub-nanosecond numbers.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_su_h_monitor is
generic (
T_W : positive := 12; -- width of the measurement counters
T_SU_MIN : natural := 8; -- required setup, in oversample periods
T_HD_MIN : natural := 4 -- required hold, in oversample periods
);
port (
clk : in std_logic; -- oversampling clock
rst_n : in std_logic;
sclk : in std_logic; -- observed bus
mosi : in std_logic;
cs_n : in std_logic;
cpol : in std_logic; -- from the device profile
cpha : in std_logic;
clear : in std_logic; -- restart the measurement window
min_setup : out unsigned(T_W - 1 downto 0);
min_hold : out unsigned(T_W - 1 downto 0);
samples : out unsigned(T_W - 1 downto 0);
su_viol : out std_logic;
hd_viol : out std_logic
);
end entity;
architecture rtl of spi_su_h_monitor is
signal sclk_s1, sclk_s2, sclk_q : std_logic := '0';
signal mosi_s1, mosi_s2, mosi_q : std_logic := '0';
signal cs_s1, cs_s2, cs_q : std_logic := '1';
signal since_change : unsigned(T_W - 1 downto 0) := (others => '0');
signal hold_cnt : unsigned(T_W - 1 downto 0) := (others => '0');
signal in_hold : std_logic := '0';
signal any_edge : std_logic := '0';
signal edge_odd : std_logic := '0';
signal min_su_r : unsigned(T_W - 1 downto 0) := (others => '1');
signal min_hd_r : unsigned(T_W - 1 downto 0) := (others => '1');
signal samp_r : unsigned(T_W - 1 downto 0) := (others => '0');
signal su_v_r : std_logic := '0';
signal hd_v_r : std_logic := '0';
signal sclk_edge : std_logic;
signal mosi_edge : std_logic;
signal in_frame : std_logic;
signal cs_fall : std_logic;
signal next_odd : std_logic;
signal sample_now : std_logic;
begin
sclk_edge <= '1' when sclk_s2 /= sclk_q else '0';
mosi_edge <= '1' when mosi_s2 /= mosi_q else '0';
in_frame <= '1' when cs_s2 = '0' else '0';
cs_fall <= '1' when cs_q = '1' and cs_s2 = '0' else '0';
next_odd <= not edge_odd when any_edge = '1' else '1';
-- CPOL does not enter the parity argument: it decides which physical
-- direction "leading" is, and the parity is indifferent to that. The
-- port is kept because a reader checking this block against a datasheet
-- expects both halves of the mode to be present.
sample_now <= '1' when in_frame = '1' and sclk_edge = '1' and
next_odd /= cpha else '0';
measure : process (clk, rst_n)
begin
if rst_n = '0' then
sclk_s1 <= '0'; sclk_s2 <= '0'; sclk_q <= '0';
mosi_s1 <= '0'; mosi_s2 <= '0'; mosi_q <= '0';
cs_s1 <= '1'; cs_s2 <= '1'; cs_q <= '1';
since_change <= (others => '0');
hold_cnt <= (others => '0');
in_hold <= '0';
any_edge <= '0';
edge_odd <= '0';
min_su_r <= (others => '1');
min_hd_r <= (others => '1');
samp_r <= (others => '0');
su_v_r <= '0';
hd_v_r <= '0';
elsif rising_edge(clk) then
sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
cs_s1 <= cs_n; cs_s2 <= cs_s1; cs_q <= cs_s2;
if clear = '1' then
min_su_r <= (others => '1');
min_hd_r <= (others => '1');
samp_r <= (others => '0');
su_v_r <= '0';
hd_v_r <= '0';
end if;
-- How long MOSI has been stable. Both this and the SCLK edge
-- detection pass through the same two synchroniser stages, so
-- the common delay cancels in the difference -- a synchronised
-- measurement of an INTERVAL is trustworthy even though a
-- synchronised measurement of an INSTANT is not.
--
-- The counter is reloaded with ONE, not zero: the transition is
-- detected at the end of a period, so the first period of
-- stability has already elapsed. Reloading with zero reports
-- every interval one period short.
if mosi_edge = '1' then
since_change <= to_unsigned(1, T_W);
else
since_change <= since_change + 1;
end if;
if cs_fall = '1' then
any_edge <= '0';
edge_odd <= '0';
in_hold <= '0';
elsif in_frame = '1' and sclk_edge = '1' then
any_edge <= '1';
edge_odd <= next_odd;
end if;
-- SETUP, measured at the sampling edge.
if sample_now = '1' then
samp_r <= samp_r + 1;
if since_change < min_su_r then
min_su_r <= since_change;
end if;
if to_integer(since_change) < T_SU_MIN then
su_v_r <= '1';
end if;
in_hold <= '1';
hold_cnt <= to_unsigned(1, T_W); -- same reasoning
elsif in_hold = '1' then
if mosi_edge = '1' then
-- HOLD ends at the first change after the sampling edge.
if hold_cnt < min_hd_r then
min_hd_r <= hold_cnt;
end if;
if to_integer(hold_cnt) < T_HD_MIN then
hd_v_r <= '1';
end if;
in_hold <= '0';
else
hold_cnt <= hold_cnt + 1;
end if;
end if;
end if;
end process;
min_setup <= min_su_r;
min_hold <= min_hd_r;
samples <= samp_r;
su_viol <= su_v_r;
hd_viol <= hd_v_r;
end architecture;-- spi_su_h_monitor_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: frames
-- whose setup and hold are known by construction, measured back exactly;
-- each limit tripping only its own flag; and clear restarting the window.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_su_h_monitor_tb is
end entity;
architecture sim of spi_su_h_monitor_tb is
constant T_W : positive := 12;
constant T_SU_MIN : natural := 8;
constant T_HD_MIN : natural := 4;
constant H : time := 20 ns;
constant TCLK : time := 2 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal sclk : std_logic := '0';
signal mosi : std_logic := '0';
signal cs_n : std_logic := '1';
signal clear : std_logic := '0';
signal min_setup : unsigned(T_W - 1 downto 0);
signal min_hold : unsigned(T_W - 1 downto 0);
signal samples : unsigned(T_W - 1 downto 0);
signal su_viol : std_logic;
signal hd_viol : std_logic;
signal errors : natural := 0;
begin
clk <= not clk after TCLK / 2 when not halt else '0';
dut : entity work.spi_su_h_monitor
generic map (T_W => T_W, T_SU_MIN => T_SU_MIN, T_HD_MIN => T_HD_MIN)
port map (
clk => clk, rst_n => rst_n,
sclk => sclk, mosi => mosi, cs_n => cs_n,
cpol => '0', cpha => '0', clear => clear,
min_setup => min_setup, min_hold => min_hold, samples => samples,
su_viol => su_viol, hd_viol => hd_viol
);
stim : process
variable errs : natural := 0;
procedure restart is
begin
wait until falling_edge(clk); clear <= '1';
wait until falling_edge(clk); clear <= '0';
wait until falling_edge(clk);
end procedure;
-- Mode 0. Each bit is launched skew after the trailing edge, so the
-- setup at the next leading edge is H - skew and the hold after
-- that leading edge is H + skew.
procedure drive_setup_case(skew : time;
data : std_logic_vector(7 downto 0)) is
begin
sclk <= '0'; cs_n <= '1'; wait for H * 2;
cs_n <= '0';
wait for skew; mosi <= data(7); wait for H - skew;
for i in 0 to 7 loop
sclk <= '1'; -- leading edge: SAMPLE
wait for H;
sclk <= '0'; -- trailing edge: LAUNCH
if i < 7 then
wait for skew; mosi <= data(6 - i); wait for H - skew;
else
wait for H;
end if;
end loop;
cs_n <= '1'; wait for H * 2;
end procedure;
-- A slave with excessive clock-to-out: the bit changes late after
-- the leading edge that sampled the previous one. Hold collapses
-- while setup stays generous.
procedure drive_hold_case(late : time;
data : std_logic_vector(7 downto 0)) is
begin
sclk <= '0'; cs_n <= '1'; wait for H * 2;
cs_n <= '0';
mosi <= data(7);
wait for H;
for i in 0 to 7 loop
sclk <= '1'; -- leading edge: SAMPLE
if i < 7 then
wait for late; mosi <= data(6 - i); wait for H - late;
else
wait for H;
end if;
sclk <= '0'; -- trailing edge
wait for H;
end loop;
cs_n <= '1'; wait for H * 2;
end procedure;
procedure expect_near(what : string; got : natural; want : natural) is
begin
-- One oversampling period of tolerance -- but no systematic
-- offset: these expectations are the true constructed intervals.
if got < want - 1 or got > want + 1 then
report " FAIL: " & what & " measured " & integer'image(got) &
" periods, expected " & integer'image(want) & " +/-1";
errs := errs + 1;
end if;
end procedure;
begin
for i in 0 to 4 loop
wait until falling_edge(clk);
end loop;
rst_n <= '1';
restart;
-- 1. A comfortable link. Setup and hold are both a half period.
drive_setup_case(0 ns, x"A5");
report " clean frame: setup=" & integer'image(to_integer(min_setup)) &
" hold=" & integer'image(to_integer(min_hold)) &
" samples=" & integer'image(to_integer(samples));
expect_near("clean setup", to_integer(min_setup), H / TCLK);
expect_near("clean hold", to_integer(min_hold), H / TCLK);
if samples /= 8 then
report " FAIL: wrong number of sampling edges in an 8-bit frame";
errs := errs + 1;
end if;
if su_viol = '1' or hd_viol = '1' then
report " FAIL: a comfortable link reported a violation";
errs := errs + 1;
end if;
-- 2. Data launched late relative to the trailing edge.
restart;
drive_setup_case(8 ns, x"A5");
report " 8 ns skew: setup=" & integer'image(to_integer(min_setup)) &
" hold=" & integer'image(to_integer(min_hold));
expect_near("skewed setup", to_integer(min_setup), (H - 8 ns) / TCLK);
expect_near("skewed hold", to_integer(min_hold), (H + 8 ns) / TCLK);
if su_viol /= '1' then
report " FAIL: a short setup did not trip T_SU_MIN";
errs := errs + 1;
end if;
if hd_viol = '1' then
report " FAIL: hold flagged while hold was growing";
errs := errs + 1;
end if;
-- 3. The converse: hold collapses, setup stays generous.
restart;
drive_hold_case(4 ns, x"A5");
report " 4 ns clk-to-out: setup=" &
integer'image(to_integer(min_setup)) &
" hold=" & integer'image(to_integer(min_hold));
expect_near("late-launch hold", to_integer(min_hold), 4 ns / TCLK);
if hd_viol /= '1' then
report " FAIL: a short hold did not trip T_HD_MIN";
errs := errs + 1;
end if;
if su_viol = '1' then
report " FAIL: setup flagged on a frame with generous setup";
errs := errs + 1;
end if;
-- 4. clear restarts the window.
restart;
if su_viol = '1' or hd_viol = '1' or samples /= 0 then
report " FAIL: clear did not restart the measurement window";
errs := errs + 1;
end if;
drive_setup_case(0 ns, x"A5");
if su_viol = '1' or hd_viol = '1' then
report " FAIL: a clean frame after a violating one still reports a violation";
errs := errs + 1;
end if;
report " after clear: setup=" & integer'image(to_integer(min_setup)) &
" hold=" & integer'image(to_integer(min_hold));
errors <= errs;
if errs = 0 then
report "PASS: setup and hold are measured back to within one oversampling period of their constructed values, each limit trips only its own flag, and clear restarts the window";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;Parity
All three implement the same monitor: identical ports and generics, two-stage synchronisers, the setup taken as the stability count at the sampling edge, the hold measured to the next transition, sticky per-window violation flags, and a clear that restarts the window. All three testbenches construct the same four frames and measure back the same values exactly — ten periods of setup and hold on a clean link, six and fourteen with an eight-nanosecond launch skew, and a hold of two with a four-nanosecond clock-to-out.
8. Why a Verification Engineer Cares
// 1. NO BIAS. A known interval must measure back as itself. This is
// the property that catches a counter reloaded with the wrong
// starting value -- an error that keeps every reading
// self-consistent while making the whole set wrong.
a_unbiased : assert property (
@(posedge clk) disable iff (!rst_n)
(sample_now && constructed_setup_valid)
|-> (since_change == constructed_setup))
else $error("the measured setup does not equal the constructed one");
// 2. INDEPENDENCE. Each limit trips only its own flag. A frame with a
// short setup and a generous hold must not raise the hold flag --
// otherwise the instrument cannot tell you which end to fix.
a_flags_independent : assert property (
@(posedge clk) disable iff (!rst_n)
(hd_viol && !$past(hd_viol)) |->
($past(hold_cnt) < T_HD_MIN))
else $error("the hold flag was raised by something other than a hold");
// 3. The reported minimum really is a minimum -- it never rises within
// a window, only falls.
a_min_monotone : assert property (
@(posedge clk) disable iff (!rst_n || clear)
(min_setup <= $past(min_setup)))
else $error("the reported minimum setup increased within a window");
// 4. clear restarts everything. A verdict carried over from a previous
// frame is worse than no verdict, because it looks current.
a_clear_restarts : assert property (
@(posedge clk) disable iff (!rst_n)
(clear) |=> (!su_viol && !hd_viol && samples == 0))
else $error("clear did not restart the measurement window");Property 1 is the one that transfers beyond SPI. Any measurement block must be validated against constructed inputs, never against its own output or against another measurement of the same thing. A systematic bias is invisible to every self-consistency check — all the readings agree with each other, and all of them are wrong by the same amount.
Property 3 encodes something easy to get wrong in the RTL: min_setup must be initialised to the maximum representable value, not to zero, or the first comparison can never lower it and the block reports zero for every window.
Coverage should target the relationship to the limit, not the raw values:
covergroup spi_timing_cg @(posedge clk iff sample_now);
// Distance from the limit is what matters. A suite whose setups are
// all comfortable never demonstrates that the flag can fire.
cp_su_margin : coverpoint su_margin_class {
bins violating = {SU_BELOW};
bins exactly_at = {SU_EQUAL}; // must NOT flag
bins one_over = {SU_PLUS_ONE}; // must NOT flag
bins comfortable = {SU_AMPLE};
}
cp_hd_margin : coverpoint hd_margin_class {
bins violating = {HD_BELOW};
bins exactly_at = {HD_EQUAL}; // must NOT flag
bins one_over = {HD_PLUS_ONE};
bins comfortable = {HD_AMPLE};
}
// The two failures are independent and a suite must produce each
// ALONE, or it cannot show the flags are not simply tied together.
cp_combo : coverpoint viol_combo {
bins neither = {V_NONE};
bins setup_only = {V_SU};
bins hold_only = {V_HD};
bins both = {V_BOTH};
}
endgroupcp_combo is the coverpoint that earns its place. Setup-only and hold-only must each be reachable, because a monitor whose two flags are accidentally driven by the same condition passes every test that violates both at once.
9. Why an FPGA or ASIC Engineer Cares
Constrain MOSI and SCLK together, not separately. The margin is a difference of delays, so what matters is their relationship. A constraint on each line independently can be met while the skew between them is unacceptable.
Treat any single-line delay as a direct margin loss. A buffer, a level shifter, or a series resistor on one net and not the others subtracts from setup without giving anything back. If a level shifter is needed, put it on all the lines so the delay is common-mode.
Derate t_V for your actual load. The datasheet's number assumes its own capacitance. Board capacitance is routinely two or three times that, and t_V is the dominant term in the frequency budget.
Leave the monitor instantiated on debug builds. It answers, in hardware, whether the link meets the datasheet — which otherwise needs a scope, a working trigger, and someone available to look at it.
Do not use this monitor in place of static timing analysis. It resolves to its own clock period. The two tools see different faults: STA sees the nanoseconds inside your device, and the monitor sees the wrong divisor, the wrong mode and the slower-than-specified part, none of which are in any netlist.
10. Failure Signature — A Design That Meets Timing and Still Fails
Symptom. The FPGA build reports positive slack on every SPI path. The constraints were written carefully and reviewed. The link nevertheless produces occasional wrong bytes on MOSI writes — the device acknowledges but stores the wrong value, roughly once in several thousand transfers.
What "positive slack" establishes. Everything inside the FPGA meets its constraints. So the fault is either outside the device, or inside something the constraints never described.
Plausible mechanisms.
- The constraint describes the wrong relationship. An output delay constraint written against the wrong clock edge — or against SCLK's launch edge when the device samples on the other one — can be met perfectly while the actual margin is half a period out.
- A delay on one line only. A level shifter, buffer or series resistor on MOSI but not SCLK is outside the FPGA and therefore outside the timing report entirely.
- The device's
t_SUwas taken from the wrong row — the SDO row rather than the SDI row, which is the naming trap of §1. - The board's load pushing the real
t_Vbeyond the quoted figure, which affects reads rather than writes and so does not fit a MOSI-write symptom. - Crosstalk from SCLK into MOSI, which fits an occasional, non-reproducible error better than any of the above.
The discriminating observation. The rarity is the clue. A constraint error or a wrong table row is a constant offset and would corrupt a large fraction of transfers, not one in thousands. One in thousands means the margin is almost adequate and something is varying — noise, crosstalk, or a data pattern dependence.
So capture the failing transfers and look for a pattern correlation: if the errors cluster on bytes where MOSI changes on many consecutive bits, crosstalk or an edge-rate effect is indicated. If they are uniformly distributed across patterns, look for something time-varying instead.
Then measure with the monitor of §7 rather than reasoning further: it reports the worst setup and hold actually observed over millions of transfers, which is precisely the statistic this symptom needs and precisely what a scope triggered by hand cannot give you.
The fix depends on which it was, and the point of the monitor is to tell you before you start changing things. But the common root — a delay element on one line and not the others — is fixed by making the delay common-mode rather than by tightening anything.
Why the investigation goes wrong. Because "timing is met" is treated as a conclusion rather than a scope-limited statement. The tool reports on the paths it was given, inside the device it was given. Everything past the pin — the shifter, the resistor, the trace, the other device's capacitance — was never in the model.
11. Common Misconceptions
12. Reason It Through
Work this before reading the answer.
A device's table gives
t_SU = 5 ns,t_HD = 5 ns,t_V = 12 nsat 30 pF, andf_SCLK(max) = 50 MHz. Your board presents about 60 pF on MISO and needs a level shifter on every line, adding 3 ns each way. Your controller's clock-to-out is 2 ns and its input setup is 2 ns; traces contribute 0.5 ns each way.What rate can you actually run, and which single change would help most?
First, decide which numbers constrain what. t_SU and t_HD are your obligations on the outbound path; t_V is the device's promise on the return path; f_SCLK(max) is the device's internal limit. The rate is limited by the round trip, so t_V is the term that matters.
Derate t_V for the real load. The table quotes 12 ns at 30 pF and your board presents roughly double. Output delay grows with load; a reasonable working assumption is that doubling the capacitance adds meaningfully to the transition — take t_V as about 16 ns rather than 12. (The exact scaling is part-specific; the discipline is to derate rather than to use the quoted figure.)
Now build Chapter 9.4's budget, remembering the level shifter is in the path twice — once outbound with the clock, once inbound with the data:
controller clock-to-out .... 2.0 ns
level shifter (out) ........ 3.0 ns
trace out .................. 0.5 ns
device t_V (derated) ....... 16.0 ns
level shifter (back) ....... 3.0 ns
trace back ................. 0.5 ns
controller input setup ..... 2.0 ns
──────────────────────────────────
total ...................... 27.0 ns → T ≥ 54 ns → f_max ≈ 18.5 MHzSo about 18 MHz, against a device rated 50. The device's own limit never binds — it is nearly three times the achievable rate.
Then apply divisor granularity. With an 80 MHz system clock the options near 18 MHz are 20 MHz (divisor 4) and 16 MHz (divisor 5). Round down: 16 MHz.
Which single change helps most? Look at the budget's terms. t_V is 16 of 27 ns. The level shifters together are 6. The traces are 1.
- Removing the level shifters — a device at the controller's voltage — saves 6 ns and lifts the ceiling to about 24 MHz.
- Halving the board capacitance on MISO recovers most of the 4 ns derate and lifts it to about 21 MHz.
- Choosing a part with a faster
t_Vis worth more than both: a device specifying 6 ns instead of 12 removes about 10 ns from the budget and roughly doubles the ceiling.
And the change that beats all of them is not in the budget at all: sampling a half cycle later turns the constraint from T/2 ≥ 27 ns into T ≥ 27 ns and doubles the achievable rate to about 37 MHz, at the cost of one flip-flop. When the round trip is the binding limit, changing the structure of the constraint beats improving any term within it.
The general lesson. Work the budget before choosing the part, derate for your own conditions, and check whether the binding constraint can be restructured before trying to optimise its terms.
13. Understanding Check
14. Summary
A timing table is mostly a list of your obligations, not the device's. Write the owner beside every row: setup, hold and the chip-select times are things you must do; the output valid, disable and hold times are promises you may rely on. SDI and SDO are named from the device's point of view, so an SDI row constrains what you drive.
Conditions are part of every number. Load, supply, temperature and drive setting all qualify the value, and the quoted load is routinely a fraction of what your board presents.
Setup and hold are nearly free on a correctly clocked master, because launch and sample are on opposite edges — so the margin only tightens at high rates, under a mode error, or when a delay is added to one line and not the others, which subtracts directly.
t_V is the device's promise and the dominant term in the frequency budget, which is why it is the number to check before choosing the part.
f_SCLK(max) is a derived limit, conditional on a command, a load and a corner, and it is one of four terms rather than the answer.
In hardware the table becomes a monitor that measures the real intervals — with the common synchroniser delay cancelling in the difference, and the counters reloaded so that no constant bias creeps into readings that would otherwise look perfectly self-consistent. It resolves to its own clock and catches what static timing analysis structurally cannot: the wrong divisor, the wrong mode, the slower-than-specified part.
15. What Comes Next
Pass two is finished: the mode is known, the rate is budgeted, and the link can be measured against the numbers the vendor published.
Chapter 10.4 — Command Encoding and Register Access begins pass three. Command tables pack a read/write bit, sometimes an auto-increment bit and a register address into a single byte — and which bit is which, and which polarity means read, differ between parts in ways that make a driver silently perform writes when it believes it is reading. The chapter turns the convention into parameters and builds the codec that encodes and decodes it, in all three HDLs.
Continue learning
Related tutorials
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
- Related topic
Bus Turnaround and the Contention Window
The overlap between one driver releasing and the next asserting: how long the gap really is once pad turn-off counts, why re-selecting the same device needs none, and the guard that enforces dead time only on a handover.
- 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
Command Encoding and Register Access
How a command byte packs direction, auto-increment and a register address, why the polarity of the read/write bit differs between parts and silently turns reads into destructive writes, and the codec that encodes and decodes any convention.
