The SPI Monitor and Checker
Write a monitor that works in every SPI mode, say which rules a wire checker can assert and which faults it cannot see, and read the proof that the mode rule is right.
- Lesson
- P7.5
- Time
- 40 min
- Runs on
- Your browser
- Check
- 4 questions
A bus monitor hard-coded "sample on the rising edge". The SoC's SPI block was set to mode 1 for a new sensor, and every byte in the scoreboard came out shifted by one bit. The sensor was declared broken, and the monitor had to be rewritten under schedule pressure.
- Pick the edge
The leading edge is rising when CPOL is 0. CPHA 0 samples the leading edge, CPHA 1 the trailing one. Both rules fold into one: rising when CPOL equals CPHA.
- Shift and frame
Each sample shifts one bit of each line in. CS falling resets the count; every eighth sample is a byte.
- Log
One line per byte: the mode, what the controller sent, what came back. The monitor gives no verdict.
Read the diagram as text
- CS_n, CLK, MOSI, MISO: the wires, nothing else
- Edge selector: rising if CPOL == CPHA
- Two shift registers: MOSI and MISO
- CS framing: a byte every 8 samples
- Log line: SPI m1 a5/96
- CS_n, CLK, MOSI, MISO → Edge selector: CLK
- Edge selector → Two shift registers: sample
- Two shift registers → CS framing
- CS framing → Log line: mode, mosi/miso
Arrows are data flow inside spi_mon.sv.
The Mode Picks the Edge
A monitor cannot guess the sampling edge from the wires; it must be told the mode. From the mode it derives one rule: sample on the rising edge when CPOL equals CPHA, on the falling edge otherwise. CS falling resets the bit count.
| Mode | CPOL | CPHA | Leading edge | Samples on | CPOL == CPHA |
|---|---|---|---|---|---|
| 0 | 0 | 0 | rising | leading: rising | yes |
| 1 | 0 | 1 | rising | trailing: falling | no |
| 2 | 1 | 0 | falling | leading: falling | no |
| 3 | 1 | 1 | falling | trailing: rising | yes |
Sample on the rising edge when CPOL equals CPHA, on the falling edge otherwise.
The twin mon.sample_wrong_edge ignores CPHA and samples the leading edge always. It breaks modes 1 and 3: there the leading edge comes before the bit, and the log shows A5h as 52h.
SPI m0 96/3c
SPI m0 a5/96
SPI m1 cb/1e
SPI m1 52/4b
SPI m2 96/3c
SPI m2 a5/96
SPI m3 cb/1e
SPI m3 52/4bIn modes 0 and 2 its log still looks right, because the trailing-edge bit is still held for the 1 ns launch delay. A clean log is not a correct monitor.
What a Wire Checker Can Assert
A checker on the wires can hold rules that need no knowledge of the data. MISO is z while CS is high. No line changes on its own sample edge. A write ends on a whole byte. CS keeps its distances from the clock.
| Fault switched on | Checker rules that fired | DATA read back (3Ah written) |
|---|---|---|
wrong_mode_1 | same_edge | 1d |
wrong_mode_2 | same_edge | 1d |
wrong_mode_3 | none | 3a |
drive_when_deselected | miso_released | XX |
late_miso | same_edge | 3a |
lsb_first | none | 5c |
cs_too_early | tchsh, byte_boundary | a5 |
Measured: one run per fault, a write of 3Ah then a read. same_edge: A line changed on its own sample edge (P7.2). miso_released: MISO driven while deselected (§4.1). tchsh: CS rose less than 3 ns after the last edge (§9.6). byte_boundary: A write ended inside a byte (§8).
Two faults leave every wire rule intact: wrong_mode_3, lsb_first. A mode-3 peripheral on a mode-0 bus uses the same edges, and LSB-first data is legal on the wire. Only a byte check against a reference sees them.
Proving the Mode Rule
Four modes and 256 bytes are easy to simulate, but a proof covers every mode and every bit pattern at once. The formal lab compares the generic sampler with four separate per-mode samplers and asserts that their bytes are always equal.
| Run | Task | Result | Time |
|---|---|---|---|
| Generic sampler against four per-mode samplers | prove, depth 20 | PASS | 573 ms |
| Generic rule edited to ignore CPOL | prove, depth 20 | FAIL at step 2 | 461 ms |
| Reachability: a full A5h byte | cover | reached at step 16 | 500 ms |
SymbiYosys on the site's formal runner (data/P7-5-measured.json). The mode is a free constant, the wires are free inputs: every mode and every bit pattern at once.
The failing run is a rule that ignores CPOL. The counterexample needs only two steps: one mode where the rule picks the wrong edge, one bit. Module F3 of the Formal track covers the assumptions such a proof needs.
Where You Will Meet It
Every SPI verification environment has a monitor and a checker on the wires. They are configured with the mode per device, never hard-coded, and the scoreboard behind them compares bytes with a reference model.
Asked in interviews: SPI monitors
How does an SPI monitor know which edge to sample?
From the configured mode: rising when CPOL equals CPHA, falling otherwise. It cannot infer the mode from the wires.
What can an SVA checker on SPI wires assert?
Rules that need no data: CLK idle level at the CS edges, MISO released while deselected, no change on the sample edge, whole bytes on writes, CS-to-clock times.
What can it not assert?
Whether the bytes are right. That needs a reference model or a scoreboard.
Why bind the checker to the wires and not the DUT's internals?
The wires are what both sides agreed to. A checker on internals cannot be reused on another peripheral and misses a DUT that shifts correctly but drives the wrong edge.
Try It
Controller and peripheral run in the same mode, the peripheral echoing each byte. The first pass logs all four modes correctly; the second pass turns on the twin.
Set the peripheral one mode away from the controller and read the log and the checker together: the monitor reports what was on the wire, the checker reports which rule broke.
SPI m0 96/3c SPI m0 a5/96 SPI m1 96/3c SPI m1 a5/96 SPI m2 96/3c SPI m2 a5/96 SPI m3 96/3c SPI m3 a5/96 SPI m0 96/3c SPI m0 a5/96 SPI m1 cb/1e SPI m1 52/4b SPI m2 96/3c SPI m2 a5/96 SPI m3 cb/1e SPI m3 52/4bRead 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
- A monitor samples on the rising edge when CPOL equals CPHA, otherwise on the falling edge; CS frames the bytes.
- The mode is configured per device, never hard-coded.
- A wire checker asserts data-free rules: release while deselected, no change on the sample edge, whole-byte writes, CS timing.
- Byte correctness needs a reference: a mode-3 device on a mode-0 bus and LSB-first data both keep the wire legal.
- A bounded proof shows the generic sampler equals four per-mode samplers for every mode and bit pattern.
Practice
- A Monitor for All Four SPI Modes Implement · Medium
- Prove the SPI Sampling Edge Verify · Hard