Many Peripherals: CS, Daisy Chain and Tri-State
Put several peripherals on one SPI bus without contention, run devices of different modes, and know when a daisy chain is the better wiring.
- Lesson
- P7.4
- Time
- 30 min
- Runs on
- Your browser
- Check
- 4 questions
Two flash chips shared one SPI bus with separate selects, and reads from either returned garbage only when the other was powered. One chip's firmware drove MISO even when deselected, an option meant for a point-to-point link. The logic analyser showed clean edges and wrong bytes.
- Correct
p0 is selected and drives; p1 and p2 hold their outputs at z. The controller reads p0 alone.
- The twin
p1 keeps driving although deselected. Wherever p0 drives 1 and p1 drives 0 the line is x: p0's A5h reads x0x00x0x.
Read the diagram as text
- Controller: CLK, MOSI, CS0..CS2
- p0: selected: drives MISO
- p1: deselected: releases
- p2: deselected: releases
- MISO: one driver at a time
- p0 → MISO: oe[0] = 1
- p1 → MISO: oe[1] = 0
- p2 → MISO: oe[2] = 0
- MISO → Controller
Arrows are drivers onto MISO. CLK and MOSI fan out from the controller to all three and are not drawn.
Deselected Means Released
MISO fans in: every peripheral can drive it, so exactly one may at a time. A deselected device must put its output at high impedance, within 7 ns of CS rising for the flash (tSHQZ).
With p1.drive_when_deselected, both p0 and p1 drive MISO. Wherever they disagree the simulator shows x, and p0's A5h reads x0x00x0x. On a board the same clock shows a mid-rail voltage, two hot drivers, and whatever each receiver makes of it.
SPI m0 02/XX
SPI m0 00/XX
SPI CHECK miso_released margin 0.000 ns
p0 DATA read as x0x00x0xThat is why the analyser in the story showed clean bits: its threshold settled the fight one way, the controller's the other. The simulator's x is the honest answer, and the checker names it: miso_released.
Modes Are per Device
p1 read in mode 0
p1.cpha = 1;
sel = 1;
cs_lo(); xfer(8'h02, received); xfer(8'h00, received); cs_hi();
Reads 1dh for 3Ah, with same_edge races.
Controller switched to mode 1 for p1
set_mode(0, 1);
sel = 1;
cs_lo(); xfer(8'h02, received); xfer(8'h00, received); cs_hi();
Reads 3ah.
Devices of different modes can share a bus, because the mode is agreed per device. p1 is a mode-1 device. Written in mode 0, its data still lands; read in mode 0, 3Ah comes back as 1dh, with same_edge races. With the controller switched to mode 1 for p1's frames it reads 3ah.
The Daisy Chain
- One 24-clock frame
The first byte sent travels furthest: after 24 clocks 11h sits in p2, 22h in p1, 33h in p0.
- What comes back
Meanwhile the old contents shift out of p2 into the controller, oldest first.
Read the diagram as text
- Controller: sends 11h, 22h, 33h
- p0: ends up with 33h
- p1: ends up with 22h
- p2: ends up with 11h
- MISO: last device to controller
- Controller → p0: MOSI
- p0 → p1: q0
- p1 → p2: q1
- p2 → MISO
- MISO → Controller
Arrows are the path of the bits. Measured: p0 33, p1 22, p2 11.
With one select and each MISO feeding the next MOSI, the three 8-bit registers form one 24-bit shift register. A 24-clock frame updates all three at once. The first byte sent travels furthest.
SPI m0 11/3c
SPI m0 22/3c
SPI m0 33/3c
held: p0 33, p1 22, p2 11
SPI m0 44/11
SPI m0 55/22
SPI m0 66/00
held: p0 66, p1 22, p2 00| One select per device | Daisy chain | |
|---|---|---|
| Pins | three wires plus one select per device | three wires plus one select |
| Clocks to update one device | 16 here: command and data | 24: every device, every time |
| Addressing | by select | by position in the chain |
| One dead device | only that device is lost | the whole chain stops |
| MISO | shared: every deselected device must release it | point to point: no sharing |
Daisy chains suit identical devices updated together, such as LED drivers or DAC banks.
Where You Will Meet It
Every board with more than one SPI device has this bus. A bench for it reads each device with the others both released and misbehaving, switches modes between frames, and checks MISO for x on every clock.
Asked in interviews: several SPI devices
How do you connect several SPI peripherals?
Share CLK, MOSI and MISO; give each peripheral its own active-low select. Every deselected peripheral must release MISO.
What happens if a deselected peripheral drives MISO?
Contention: x in simulation wherever the two drivers disagree, a mid-rail level and heat on a board.
Can devices with different SPI modes share a bus?
Yes. The controller changes CPOL and CPHA between frames, per device.
What is a daisy chain?
One select for all devices, each MISO wired to the next MOSI, forming one long shift register updated by one long frame.
Try It
The bench has three peripherals, p0 to p2, with one select each; sel picks which select the next cs_lo() pulls low. The checker and the monitor watch the shared lines.
Swap the twin to p2.drive_when_deselected = 1; and set p2.data_reg to a different value: the x bits move to wherever p0 and p2 disagree.
SPI m0 02/3c SPI m0 00/a5 SPI m0 02/XX SPI m0 00/XX SPI CHECK miso_released margin 0.000 ns p0 DATA read as x0x00x0x SPI CHECK same_edge margin 1.000 ns SPI CHECK same_edge margin 1.000 ns SPI m0 82/1e SPI m0 3a/00 SPI CHECK same_edge margin 1.000 ns SPI CHECK same_edge margin 1.000 ns SPI m0 02/1e SPI CHECK same_edge margin 1.000 ns SPI CHECK same_edge margin 1.000 ns SPI CHECK same_edge margin 1.000 ns SPI m0 00/1d SPI CHECK same_edge margin 1.000 ns SPI m1 02/3c SPI m1 00/3aRead 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
- Each peripheral has its own select; CLK, MOSI and MISO are shared.
- A deselected peripheral must release MISO (z); contention is x in simulation and a hot pin on a board.
- Modes are agreed per device; the controller switches CPOL and CPHA between frames.
- A daisy chain is one long shift register on one select: one frame updates every device, and the first byte goes furthest.
Practice
- Release MISO When Deselected Fix · Medium
- Load Three Devices Through One Chain Implement · Medium