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.

A Monitor: From Wires to a Byte LogThe monitor watches the wires only. The mode tells it which clock edge carries each bit; chip select tells it where bytes begin.
A monitor: from wires to a byte logThe monitor watches the wires only. The mode tells it which clock edge carries each bit; chip select tells it where bytes begin. component CS_n, CLK, MOSI,MISO the wires, nothingelse component Edge selector rising if CPOL ==CPHA component Two shiftregisters MOSI and MISO component CS framing a byte every 8samples component Log line SPI m1 a5/96 CLKsamplemode, mosi/miso
  1. 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.

  2. Shift and frame

    Each sample shifts one bit of each line in. CS falling resets the count; every eighth sample is a byte.

  3. 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.

ModeCPOLCPHALeading edgeSamples onCPOL == CPHA
000risingleading: risingyes
101risingtrailing: fallingno
210fallingleading: fallingno
311fallingtrailing: risingyes

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.

Measured: the four modes again with the twin on. Each frame sends 96h then A5h; the peripheral answers its ID, then the echo.
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/4b

In 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 onChecker rules that firedDATA read back (3Ah written)
wrong_mode_1same_edge1d
wrong_mode_2same_edge1d
wrong_mode_3none3a
drive_when_deselectedmiso_releasedXX
late_misosame_edge3a
lsb_firstnone5c
cs_too_earlytchsh, byte_boundarya5

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.

RunTaskResultTime
Generic sampler against four per-mode samplersprove, depth 20PASS573 ms
Generic rule edited to ignore CPOLprove, depth 20FAIL at step 2461 ms
Reachability: a full A5h bytecoverreached at step 16500 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
  1. 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.

  2. 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.

  3. What can it not assert?

    Whether the bytes are right. That needs a reference model or a scoreboard.

  4. 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.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: 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/4b
Read 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

Predict the outputBench defaults with the checker on: a mode-0 controller and a mode-3 peripheral whose DATA holds A5h. The controller reads DATA and prints the checker's count of broken rules and the byte. What does it print?
`timescale 1ns/1ps
module tb;
  reg cs_n = 1, sclk = 0, mosi = 0; tri miso; reg [7:0] received;
  `include "spi_ctrl_tasks.svh"              // set_mode, set_period, cs_lo, cs_hi, xfer
  wire [0:0] oe;                              // driver view: who drives MISO
  spi_periph #(.CPOL(0), .CPHA(0)) p0 (cs_n, sclk, mosi, miso, oe[0]);
  spi_checker ck (cs_n, sclk, mosi, miso, |oe, cpol[0], cpha[0]);   // prints SPI CHECK <rule>
  initial begin
    set_mode(0, 0);
    #10;
    p0.cpol = 1; p0.cpha = 1;           // a mode-3 peripheral under the mode-0 controller
    p0.data_reg = 8'ha5;
    cs_lo(); xfer(8'h02, received); xfer(8'h00, received); cs_hi();
    $display("%0d %h", ck.broken, received);
    $finish;
  end
  initial begin #100000; $fatal(1, "watchdog"); end
endmodule
Read 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
Spot the bugEach option is the body of a function that returns 1 when a monitor in this mode should sample on the rising edge. Which one is wrong?
ConceptWhy bind the checker to the wires instead of reading the peripheral's shift register?
ConceptThe formal lab's reference for mode 2 is edited to sample the wrong edge. What does the proof report?

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