Timing: Setup, Hold and the Half-Cycle Budget
Compute the read margin at any clock from tCLQV, find where same-cycle sampling stops working, and keep CS timing and whole bytes right on writes.
- Lesson
- P7.3
- Time
- 35 min
- Runs on
- Your browser
- Check
- 4 questions
A flash controller validated at 50 MHz was clocked to 133 MHz for a product refresh. Every read came back with its bits one place late. The flash promises valid output within 6 ns of the falling edge; at 133 MHz the next rising edge comes 3.76 ns after it.
- Same-cycle sampling
At 50 MHz the rising edge comes 10 ns after launch: 4.000 ns to spare. At 133 MHz it comes 3.760 ns after: the bit is not there yet.
- Sampling a full period later
A controller built for the top clock samples on the next falling edge, 7.519 ns after launch: 1.519 ns to spare.
Read the diagram as text
- Falling edge: the peripheral launches
- MISO valid: up to tCLQV = 6 ns later
- Rising edge: half a period after launch
- Next falling edge: a full period after launch
- Margin: sample time minus valid time
- Falling edge → MISO valid: tCLQV
- MISO valid → Rising edge: must come first
- MISO valid → Next falling edge: sample_late
- Rising edge → Margin
Arrows are time order on one bit. The controller's own setup time adds to tCLQV; the bench's is 0.
Half a Period, Minus tCLQV
The peripheral launches on one edge and the controller samples on the opposite one, half a period later. The output needs up to tCLQV to become valid. The margin is half the period minus tCLQV, minus the controller's own setup time.
| Clock | Period | MISO valid after launch | Sampled after launch | Margin | Byte read |
|---|---|---|---|---|---|
| 50 MHz | 20 ns | 6.000 ns | 10.000 ns | 4.000 ns | a5 |
| 66.7 MHz | 15 ns | 6.000 ns | 7.500 ns | 1.500 ns | a5 |
| 83.3 MHz | 12 ns | 6.000 ns | 6.000 ns | 0.000 ns | 52 |
| 100 MHz | 10 ns | 6.000 ns | 5.000 ns | -1.000 ns | 52 |
| 133 MHz | 7.519 ns | 6.000 ns | 3.760 ns | -2.240 ns | 52 |
| 133 MHz, sample_late | 7.519 ns | 6.000 ns | 7.519 ns | 1.519 ns | a5 |
Measured on the wires: the time from each launch edge to the MISO change it caused, and to the controller's sample edge. DATA holds A5h; the peripheral uses the flash's worst-case tCLQV of 6 ns.
The margin falls to zero at a 12 ns period, 83.3 MHz, and the byte is already wrong there: a sample at the instant MISO changes still sees the old bit. Above that, every bit the controller takes is the one before.
A Controller Built for 133 MHz
The datasheet's 133 MHz limit is about the flash, not about same-cycle sampling. A fast controller samples on the next launch edge instead, a full period after launch. At 133 MHz that leaves 1.519 ns, and the byte is right again.
Same-cycle sampling at 133 MHz
set_period(7.519);
sample_late = 0;
Sampled 3.760 ns after launch: margin -2.240 ns, reads 52h.
Sampled a full period later
set_period(7.519);
sample_late = 1;
Sampled 7.519 ns after launch: margin 1.519 ns, reads a5h.
Sampling on the launch edge is safe only because the old bit is held for at least tCLQX, 1.5 ns, after it. Faster designs use a delayed sampling clock or a data strobe from the flash.
The Input Side and the CS Edges
At the flash's inputs the rule is setup 1 ns and hold 2 ns around the rising edge. A controller that launches on the falling edge gives half a period of each. The races of P7.2 gave only the 1 ns launch delay.
| Parameter | Value | Binds | Meaning |
|---|---|---|---|
| fR / FR | 50 MHz / 133 MHz | controller | top clock for 03h / for everything else |
| tCLQV | 6 ns max | peripheral | output valid after the falling edge |
| tCLQX | 1.5 ns min | peripheral | old output held at least this long after the falling edge |
| tDVCH, tCHDX | 1 ns, 2 ns min | controller | input setup and hold around the rising edge |
| tSLCH, tCHSH | 3 ns min | controller | CS low to the first edge; last edge to CS high |
| tSHSL1, tSHSL2 | 10 ns, 50 ns min | controller | CS high between frames: after a read, after a program or erase |
W25Q128JV §9.6 at 3.0 to 3.6 V. The wire checker in spi_checker.sv knows each of these by name.
A write must end on a whole byte, or the flash drops it. The twin raises CS before the last bit is sampled: DATA keeps its old value (a5h), and the checker names tchsh and byte_boundary. A read may end anywhere.
Where You Will Meet It
Every flash and fast-sensor interface has this budget, and every speed-up of an existing design reopens it. A bench measures the launch-to-valid and launch-to-sample times on the wires and runs the clock past the break-even on purpose.
Asked in interviews: SPI timing
How do you compute the read margin of an SPI interface?
Half the clock period minus the peripheral's clock-to-output time (tCLQV) minus the controller's input setup time, for same-cycle sampling.
Why can a 133 MHz flash fail at 100 MHz?
With tCLQV of 6 ns, half a period is shorter than the output delay above about 83 MHz, so same-cycle sampling takes the previous bit.
What do tCLQV and tCLQX bound?
tCLQV is the latest the new bit may appear; tCLQX is the earliest the old bit may vanish. The valid window lies between them.
What happens if CS rises in the middle of a write byte?
The flash does not execute the write. CS must rise only after the eighth bit of the last byte.
Try It
The probe beside the bench timestamps every launch edge, every change of MISO and every sample edge. It reports the longest launch-to-change time it saw and the shortest launch-to-sample time: both measured, neither typed in.
Set p0.t_clqv = 3.0; and run the sweep again to see where the break-even moves. Then add a period of your own between 12 and 15 ns and find the fastest clock that still reads A5h.
20.000 ns period: MISO valid 6.000 ns after launch, sampled 10.000 ns after, margin 4.000 ns, read a5 15.000 ns period: MISO valid 6.000 ns after launch, sampled 7.500 ns after, margin 1.500 ns, read a5 12.000 ns period: MISO valid 6.000 ns after launch, sampled 6.000 ns after, margin 0.000 ns, read 52 10.000 ns period: MISO valid 6.000 ns after launch, sampled 5.000 ns after, margin -1.000 ns, read 52 7.519 ns period: MISO valid 6.000 ns after launch, sampled 3.760 ns after, margin -2.240 ns, read 52 7.519 ns period: MISO valid 6.000 ns after launch, sampled 7.519 ns after, margin 1.519 ns, read a5 after the cut-short write DATA reads a5Read included file: spi_periph.sv
`timescale 1ns/1ps
// A teaching register device, not the Winbond register map. Writes commit at CS
// rising only when the entire command/data frame ended on a byte boundary.
module spi_periph #(parameter CPOL=0,CPHA=0)(input wire cs_n,sclk,mosi,inout wire miso,output wire oe);
int cpol=CPOL,cpha=CPHA,wrong_mode=0;
bit drive_when_deselected=0,late_miso=0,lsb_first=0,loopback=0,chain=0;
realtime t_clqv=1.0;
reg [7:0] tx_word=8'h3c,last_rx=0,ctrl=0,data_reg=0,shift=0,response=8'h3c,pending=0;
reg out=0;int count=0,words=0,frames=0;reg [7:0] command=0;
wire p=cpol[0]^((wrong_mode&2)!=0),h=cpha[0]^((wrong_mode&1)!=0);
assign oe=!cs_n||drive_when_deselected;assign miso=oe?out:1'bz;
function automatic bit txbit(input int i);txbit=chain?shift[7]:response[lsb_first?i:7-i];endfunction
function automatic [7:0] reg_value(input reg [6:0] addr);
case(addr)0:reg_value=8'h3c;1:reg_value=ctrl;2:reg_value=data_reg;3:reg_value=frames[7:0];default:reg_value=0;endcase
endfunction
always @(negedge cs_n)begin
count=0;shift=chain?tx_word:0;words=0;command=0;response=tx_word;
// Preload bit 7 without advancing count. A same-edge 0/3 (or 2/1)
// pair may sample before the peripheral sees its first launch edge.
// CPHA=1's leading launch repeats this bit; only sampling advances count.
out<=#(t_clqv)txbit(0);
end
always @(posedge cs_n)begin
if(words>0)begin
if(!chain&&!loopback&&count==0&&words>=2&&command[7])begin
case(command[6:0])1:ctrl=pending;2:data_reg=pending;default:;endcase
end
frames++;if(chain)tx_word=shift;
end
end
always @(sclk)if(!cs_n)begin
if(sclk==(h?p:!p))begin
shift={shift[6:0],mosi};count++;
if(count==8)begin
last_rx=shift;count=0;
if(!chain)begin
if(loopback)response=shift;
else if(words==0)begin command=shift;response=shift[7]?0:reg_value(shift[6:0]);end
else pending=shift;
end
words++;
end
if(late_miso)out<=#(t_clqv)txbit(count);
end else if(!late_miso)out<=#(t_clqv)txbit(count);
end
endmodule
Read included file: spi_ctrl_tasks.svh
int cpol=0,cpha=0;realtime half_period=5.0,t_launch=1.0;bit cs_too_early=0,sample_late=0,cs_between_bytes=0;int clocks=0;
task automatic set_mode(input int p,input int h);cpol=p;cpha=h;sclk=p;endtask
task automatic set_period(input real ns);if(ns<=0)$fatal(1,"SPI period must be positive");half_period=ns/2;endtask
task automatic cs_lo(input int id=0);sclk=cpol;#(half_period);cs_n=0;#(half_period);endtask
task automatic cs_hi(input int id=0);#(half_period);cs_n=1;#(half_period);endtask
task automatic xfer(input reg [7:0] value,output reg [7:0] received);
received=0;
for(int b=7;b>=0;b--)begin
if(t_launch>=half_period)$fatal(1,"launch delay must fit half-period");
if(!cpha)begin
#(t_launch);mosi=value[b];#(half_period-t_launch);
if(!sample_late)received[b]=miso;sclk=!cpol;clocks++;
#(half_period);if(sample_late)received[b]=miso;sclk=cpol;
end else begin
#(half_period);sclk=!cpol;clocks++;#(t_launch);mosi=value[b];#(half_period-t_launch);
if(!sample_late)received[b]=miso;sclk=cpol;
if(sample_late)begin #(half_period);received[b]=miso;end
end
if(cs_too_early&&b==1)cs_n=1;
end
if(cs_between_bytes)begin cs_hi();cs_lo();end
endtask
Read included file: spi_checker.sv
`timescale 1ns/1ps
// Mode-generic teaching checker. tCLQV margin uses the configured receiver setup;
// positive margin is a bound, not a guarantee that a particular byte is correct.
module spi_checker(input wire cs_n,sclk,mosi,miso,oe,input wire cpol,cpha);
int broken=0,samples=0;realtime changed=0,mosi_changed=0,last_sample=-100,last_launch=-100,last_edge=-100,selected=-100,deselected=-100;
bit enabled=0,sample_late=0;realtime tdvch=1.0,tchdx=2.0,tclqv=1.0,receiver_setup=0.0;
realtime tslch=3.0,tchsh=3.0,tshch=3.0,tchsl=3.0,tshsl=10.0;
task automatic bad(input string rule,input real margin);broken++;$display("SPI CHECK %s margin %0.3f ns",rule,margin);endtask
// Late sampling consumes the old bit just before the next launch. Changes
// after that launch are legal; its setup bound is checked by tclqv below.
always @(miso)begin changed=$realtime;
if(enabled&&!sample_late&&!cs_n&&samples>0&&sclk==(cpha?cpol:!cpol))bad("same_edge",$realtime-last_sample);
end
always @(mosi)begin
mosi_changed=$realtime;
if(enabled&&!cs_n&&$realtime-last_sample<tchdx)bad("tchdx",$realtime-last_sample-tchdx);
end
task automatic timing_margin(input real half_period,input real tclqv_max,input real setup_ns);
real margin; margin=(sample_late?2:1)*half_period-tclqv_max-setup_ns;
$display("SPI MARGIN half=%0.3f tCLQV=%0.3f setup=%0.3f margin=%0.3f ns",half_period,tclqv_max,setup_ns,margin);
if(margin<0)bad("tclqv",margin);
endtask
always @(negedge cs_n)begin
if(sclk!==cpol)bad("cs_clk_idle",0);
if(enabled&&$realtime-deselected<tshsl)bad("tshsl",$realtime-deselected-tshsl);
if(enabled&&$realtime-last_edge<tchsl)bad("tchsl",$realtime-last_edge-tchsl);
selected=$realtime;samples=0;enabled=1;
end
always @(sclk)begin
if(enabled)begin
if(!cs_n&&$realtime-selected<tslch)bad("tslch",$realtime-selected-tslch);
if(cs_n&&$realtime-deselected<tshch)bad("tshch",$realtime-deselected-tshch);
end
last_edge=$realtime;
if(!cs_n)begin
if(sclk==(cpha?cpol:!cpol))begin
samples++;last_sample=$realtime;
if($realtime-mosi_changed<tdvch)bad("tdvch",$realtime-mosi_changed-tdvch);
if(!sample_late&&last_launch>=selected&&$realtime-last_launch<tclqv+receiver_setup)bad("tclqv",$realtime-last_launch-tclqv-receiver_setup);
end else begin
if(sample_late&&last_launch>=selected&&$realtime-last_launch<tclqv+receiver_setup)bad("tclqv",$realtime-last_launch-tclqv-receiver_setup);
last_launch=$realtime;
end
end
end
always @(posedge cs_n)if(enabled)begin
if(sclk!==cpol)bad("cs_clk_idle",0);
if($realtime-last_edge<tchsh)bad("tchsh",$realtime-last_edge-tchsh);
if(samples%8!=0)bad("byte_boundary",0);
deselected=$realtime;#1;if(oe)bad("miso_released",0);
end
endmodule
Read included file: spi_mon.sv
`timescale 1ns/1ps
module spi_mon(input wire cs_n,sclk,mosi,miso,input wire cpol,cpha);
reg [7:0] a=0,b=0;int count=0,frames=0;bit trace=0,sample_wrong_edge=0;
always @(negedge cs_n)count=0;
always @(sclk)if(!cs_n&&sclk==((cpha^sample_wrong_edge)?cpol:!cpol))begin
a={a[6:0],mosi};b={b[6:0],miso};count++;
if(count==8)begin frames++;if(trace)$display("SPI m%0d %02h/%02h",2*cpol+cpha,a,b);count=0;end
end
endmodule
Check Yourself
Remember
- Read margin with same-cycle sampling is half the period minus tCLQV minus the controller's setup time.
- With tCLQV of 6 ns the margin reaches zero at a 12 ns period, about 83 MHz; above it every bit is the previous one.
- A controller for 133 MHz samples a full period after launch, relying on tCLQX to hold the old bit.
- Inputs need 1 ns setup and 2 ns hold; CS needs 3 ns at each edge and 10 ns or 50 ns between frames.
- A write must end on a whole byte or the flash drops it; a read may stop anywhere.
Practice
- Sample MISO After It Becomes Valid Fix · Medium