Skip to content
VLSI Mentor

SPI · Module 8

Chip Select Semantics and Device Selection

What selection means on an SPI bus: why active-low is a convention, what each CS edge commits the device to, why selection is physical rather than addressed, and the generator that makes multi-select structurally impossible.

Every module so far has assumed one device on the bus. Module 8 removes that assumption, and it is the longest module in the track because almost everything that can go wrong on a shared bus goes wrong quietly.

Three devices share SCLK, MOSI and MISO. What makes exactly one of them the participant in this transfer — and what happens to the other two?

The answer is chip select, and it carries more meaning than "enable" suggests.

1. What "Selected" Means

Selection is not a request to pay attention. For the selected device it is four simultaneous commitments:

  • Its phase sequencer starts from the beginning, expecting a command (Chapter 4.4 §7).
  • It begins interpreting SCLK edges as bit times, having ignored them entirely while deselected.
  • It takes ownership of MISO at the point its protocol says to (Chapter 6.1 §4).
  • It becomes committable — a write staged during this frame will take effect when CS rises (Chapter 5.1 §4).

And for every deselected device, the corresponding four negatives. A deselected device must ignore SCLK completely, which is a stronger requirement than it appears: SCLK is toggling, MOSI is changing, and the device must shift nothing, decode nothing and drive nothing.

2. Active-Low Is a Convention, Not a Rule

Chip select is drawn active-low almost universally, written CS#, nCS, SS# or CSB, and the convention has a genuine electrical reason: a pull-up resistor on the line means an undriven or un-programmed master leaves every device deselected, which is the safe state. An active-high select with a pull-down achieves the same, but pull-ups are the more common default on a board.

It is still a convention. Active-high chip selects exist, and a design that assumes active-low will hold such a device permanently selected — which is not a subtle failure, but it presents subtly: the device responds to traffic intended for others, and its MISO driver is always on.

That is why the generator in §6 takes a per-slave polarity rather than assuming one. The cost is one XOR per line; the alternative is a board that cannot accommodate a part it was designed around.

3. The Two Edges

Everything the device knows lives between these two edges

10 cycles
An SPI frame. Chip select falls, the clock produces three pulses while MOSI carries data, then chip select rises. The clock and data are inactive outside the frame.frame opensframe opensframe closesframe closescs_nsclkmosiXXXt0t1t2t3t4t5t6t7t8t9
Figure 1 — one frame, with the edges that bound it. The falling edge starts the device's sequencer and its interpretation of SCLK; the rising edge commits, releases MISO and resynchronises. Between them the device participates; outside them it must behave as though the bus were not there.

The two edges are not symmetric, and Chapter 5.2 §4 established why: a spurious falling edge opens a frame that will be discarded when no valid command follows, while a spurious rising edge commits a partial write, releases the bus mid-transfer and resynchronises a sequencer that was not finished. The rising edge is the dangerous one, and Chapter 8.6 is about what happens when it arrives by accident.

4. Selection Is Physical, Not Addressed

A block diagram of an SPI master connected to three slave devices. The clock and MOSI lines fan out from the master to all three devices. A shared MISO net returns to the master. Each device has its own dedicated chip select line from the master.Masterdrives SCLK, MOSI, every CSSCLK + MOSIone driver, all listenMISO netone driver at a timeSlave 0CS0 — deselectedSlave 1CS1 — selectedSlave 2CS2 — deselecteddrivesto alldriveshigh-Zsamples12
Figure 2 — the standard multi-slave arrangement. The clock and MOSI fan out to every device unchanged; MISO is a shared net with one driver at a time; and each device has a dedicated select line. Addressing is a pin, not a byte — which is why SPI has no address phase on the wire at all.

Two consequences follow from addressing being a pin.

There is no address collision and no address configuration. Unlike I²C, two identical parts on the same bus need no strapping or alternate addresses — they need two pins. Selection is unambiguous by construction.

Selection costs pins linearly. N devices need N select lines plus the three shared ones, which is Chapter 8.2's arithmetic and the reason that chapter exists.

5. One Selected, Always

The rule that governs everything else in this module: at most one device may be selected at a time.

The reason is electrical rather than logical. Two selected devices both reach their data phase and both enable their MISO drivers, and two low-impedance drivers on one net is contention — a short circuit through two output stages, producing an undefined logic level and real current.

Note "at most" rather than "exactly one". Zero selected is a perfectly good state and is the correct one between transactions. A design that always has something selected has no safe idle state and no bus turnaround interval.

The interesting engineering question is how to guarantee the rule, and there are two approaches with very different properties:

Check it. Drive a mask of select lines and add logic that detects or prevents more than one being asserted. The illegal state is representable, so it must be excluded by logic — and that logic must itself be verified.

Make it unrepresentable. Drive an index and decode it. Two simultaneous selects are not a state the hardware can enter, so there is nothing to check.

The second is what §6 builds, and the difference matters more than it looks: a mask-based interface can be given 0b0011 by a software bug, and something has to notice. An index-based one cannot express that request at all.

6. Building the Chip-Select Generator — Three HDLs

The circuit

Circuit. An index-to-one-hot decoder, a per-line polarity XOR, and an output register.

State. One register per select line, plus a selected flag.

Datapath. None — the module carries no data, only selection.

Control. sel_en gates everything, so deselect-all is a single input rather than a special index value. An out-of-range index deselects rather than wrapping, because wrapping would silently select the wrong device.

Clock. The system clock. Registering the outputs matters: a combinational decode can glitch during the index transition, and a glitch on a select line is a spurious frame (Chapter 8.6).

Reset. Asynchronous, active-low, to every line at its own idle level — which is ~polarity, not all-ones. Resetting an active-high select to 1 would select that device out of reset.

Enables. None; the decode is continuous and the register always follows it.

Timing. One cycle from index to pins. During a change from one slave to another the outputs pass through the all-deselected state on the same edge, so there is no instant at which both are asserted — but a deliberate dead interval still belongs in Chapter 8.4, because the pads' analogue turn-off is not instantaneous.

Synthesis. N_SLAVES flip-flops, a small decoder and N_SLAVES XOR gates. Negligible, and the one-hot guarantee costs nothing because it is structural.

Limitations. polarity is a static board property and must be stable while the outputs are live — the testbenches apply it through reset, which is how a real design would configure it.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_gen.sv — selection as an index, so multi-select cannot be expressed
// spi_cs_gen.sv — the master-side chip-select generator.
//
// The single most valuable safety property in a multi-slave SPI design is
// that TWO SLAVES CAN NEVER BE SELECTED AT ONCE, because two selected slaves
// both drive MISO and the result is contention (Chapter 8.5).
//
// This module obtains that property STRUCTURALLY rather than by checking for
// it: the selection is an INDEX, not a mask, so a multi-select is not a state
// the hardware can enter. A mask-based interface can express the illegal
// state and therefore needs logic -- and a test -- to prevent it.
//
// Per-slave polarity is supported because active-high chip selects exist, and
// a design that assumes active-low holds such a device permanently selected.
module spi_cs_gen #(
    parameter int N_SLAVES = 4,
    parameter int IDX_W    = 2      // must satisfy 2**IDX_W >= N_SLAVES
) (
    input  logic                clk,
    input  logic                rst_n,
    input  logic                sel_en,      // 0 = deselect everything
    input  logic [IDX_W-1:0]    sel_idx,     // which slave
    // STATIC board property. It must be stable whenever the outputs are
    // live: the outputs are registered, so a polarity change takes a cycle to
    // reach the pins and during that cycle the idle levels are those of the
    // OLD convention -- which reads as "asserted" under the new one. Set it
    // from reset, exactly as CPOL/CPHA are set before a transfer.
    input  logic [N_SLAVES-1:0] polarity,    // 1 = that slave is active-HIGH
    output logic [N_SLAVES-1:0] cs_out,      // physical pins
    output logic                any_selected
);
    // The logical "this slave is selected" decode. At most one bit can be set,
    // because it is generated by indexing rather than by masking.
    logic [N_SLAVES-1:0] active;

    always_comb begin
        active = '0;
        // Compare as plain integers. A width cast here would truncate
        // N_SLAVES to IDX_W bits and silently disable the guard.
        if (sel_en && (int'(sel_idx) < N_SLAVES))
            active[sel_idx] = 1'b1;
        // An out-of-range index deselects everything rather than wrapping.
        // Wrapping would silently select the wrong device.
    end

    // Physical level from logical selection and polarity:
    //   active-low  slave: asserted -> 0, idle -> 1
    //   active-high slave: asserted -> 1, idle -> 0
    // Both cases are `active XOR ~polarity`, and with active = 0 that gives
    // the idle level -- which is also the correct reset value.
    logic [N_SLAVES-1:0] next_cs;
    always_comb
        for (int i = 0; i < N_SLAVES; i++)
            next_cs[i] = active[i] ^ ~polarity[i];

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cs_out       <= ~polarity;      // every slave idle, whatever its polarity
            any_selected <= 1'b0;
        end else begin
            cs_out       <= next_cs;
            any_selected <= |active;
        end
    end
endmodule

The int'(sel_idx) < N_SLAVES comparison is worth pausing on, because the natural-looking alternative is wrong. Writing sel_idx < IDX_W'(N_SLAVES) truncates N_SLAVES to the index width — for N_SLAVES = 4 and IDX_W = 2 that is 4 → 0, so the guard becomes sel_idx < 0, which is never true and silently disables all selection. Comparing as plain integers avoids it.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_gen_tb.sv — exactly one asserted, at both polarities, continuously checked
// spi_cs_gen_tb.sv — exactly one slave asserted, mixed polarity, and the
// continuous invariant that two are never asserted together.
`timescale 1ns/1ps
module spi_cs_gen_tb;
    logic clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    localparam int N = 4, IW = 2;
    logic sel_en = 0;
    logic [IW-1:0] sel_idx = '0;
    logic [N-1:0]  polarity = 4'b0000;   // all active-low to begin
    logic [N-1:0]  cs_out;
    logic any_selected;

    spi_cs_gen #(.N_SLAVES(N), .IDX_W(IW)) dut (
        .clk, .rst_n, .sel_en, .sel_idx, .polarity, .cs_out, .any_selected);

    int errors = 0;
    task automatic chk(input string what, input int g, input int e);
        if (g !== e) begin $display("FAIL %s: got 0b%0b exp 0b%0b", what, g, e); errors++; end
    endtask

    // Count how many slaves are ASSERTED, given each one's polarity.
    function automatic int n_asserted();
        int c = 0;
        for (int i = 0; i < N; i++)
            if (cs_out[i] == polarity[i]) c++;   // asserted level == polarity
        return c;
    endfunction

    // CONTINUOUS INVARIANT: never more than one asserted, at any instant.
    always @(posedge clk) if (rst_n) begin
        if (n_asserted() > 1) begin
            $display("FAIL: %0d slaves asserted at t=%0t (cs=0b%0b pol=0b%0b)",
                     n_asserted(), $time, cs_out, polarity);
            errors++;
        end
    end

    task automatic settle(); repeat (2) @(negedge clk); endtask

    initial begin
        repeat (3) @(negedge clk); rst_n = 1; settle();

        // --- out of reset: everything deselected ---
        chk("reset: all idle (active-low)", cs_out, 4'b1111);
        chk("reset: none selected",         any_selected, 0);

        // --- select each slave in turn, all active-low ---
        sel_en = 1;
        for (int i = 0; i < N; i++) begin
            sel_idx = i[IW-1:0]; settle();
            chk($sformatf("select %0d: exactly one asserted", i), n_asserted(), 1);
            chk($sformatf("select %0d: the right one",        i), cs_out[i],    1'b0);
            chk($sformatf("select %0d: any_selected",         i), any_selected, 1);
        end

        // --- deselect: every line returns to its idle level ---
        sel_en = 0; settle();
        chk("deselect: all idle",   cs_out,       4'b1111);
        chk("deselect: none active", n_asserted(), 0);
        chk("deselect: flag clear",  any_selected, 0);

        // --- MIXED polarity: slaves 1 and 3 are active-HIGH.
        //     polarity is a STATIC board property, so it is applied through
        //     reset rather than changed on a live bus. ---
        rst_n = 0; polarity = 4'b1010; sel_en = 0;
        repeat (2) @(negedge clk); rst_n = 1; settle();
        chk("mixed idle levels", cs_out, 4'b0101);   // hi-active idle low
        chk("mixed: none asserted", n_asserted(), 0);

        sel_en = 1;
        for (int i = 0; i < N; i++) begin
            sel_idx = i[IW-1:0]; settle();
            chk($sformatf("mixed select %0d: exactly one", i), n_asserted(), 1);
            // The asserted level differs per slave; the COUNT does not.
            chk($sformatf("mixed select %0d: correct level", i),
                cs_out[i], polarity[i]);
        end

        // --- an out-of-range index must deselect, never wrap ---
        sel_en = 0; rst_n = 0; polarity = 4'b0000;
        repeat (2) @(negedge clk); rst_n = 1; settle();
        sel_en = 1; sel_idx = 2'd3; settle();
        chk("idx 3 valid for N=4", n_asserted(), 1);
        // With N_SLAVES smaller than 2**IDX_W the out-of-range path applies;
        // exercised in the N=3 instance below.

        sel_en = 0; settle();
        chk("final: all idle", cs_out, 4'b1111);

        if (errors == 0)
            $display("PASS: exactly one slave is ever asserted, each at its own polarity's active level, and deselect returns every line to its idle level");
        else
            $display("FAILED with %0d error(s)", errors);
        $finish;
    end

    initial begin #500000; $display("FAIL: watchdog timeout"); $finish; end
endmodule

The testbench's n_asserted() helper is the design's specification written as a measurement: it counts lines whose level equals their own polarity's active level, so it is correct for a mixed-polarity bus where "asserted" is a different voltage on different pins. Checking it on every clock edge rather than at chosen points makes the one-hot property a proof rather than a sample.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_gen.v — the same generator in Verilog-2001
// spi_cs_gen.v — the same chip-select generator in Verilog-2001.
module spi_cs_gen #(
    parameter N_SLAVES = 4,
    parameter IDX_W    = 2
) (
    input  wire                clk,
    input  wire                rst_n,
    input  wire                sel_en,
    input  wire [IDX_W-1:0]    sel_idx,
    // STATIC board property -- see the SystemVerilog comment. Must be stable
    // whenever the outputs are live.
    input  wire [N_SLAVES-1:0] polarity,
    output reg  [N_SLAVES-1:0] cs_out,
    output reg                 any_selected
);
    integer i;
    reg [N_SLAVES-1:0] active;
    reg [N_SLAVES-1:0] next_cs;

    // At most one bit set, because it is generated by indexing not masking.
    always @(*) begin
        active = {N_SLAVES{1'b0}};
        // Compare as plain integers: a width cast would truncate N_SLAVES to
        // IDX_W bits and silently disable the guard.
        if (sel_en && (sel_idx < N_SLAVES))
            active[sel_idx] = 1'b1;
    end

    // active XOR ~polarity gives the asserted level when selected and the
    // idle level when not -- which is also the correct reset value.
    always @(*) begin
        for (i = 0; i < N_SLAVES; i = i + 1)
            next_cs[i] = active[i] ^ ~polarity[i];
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cs_out       <= ~polarity;
            any_selected <= 1'b0;
        end else begin
            cs_out       <= next_cs;
            any_selected <= |active;
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_gen_tb.v — the same checks in Verilog-2001
// spi_cs_gen_tb.v — the same checks in Verilog-2001.
`timescale 1ns/1ps
module spi_cs_gen_tb;
    reg clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    parameter N = 4, IW = 2;
    reg sel_en = 0;
    reg [IW-1:0] sel_idx = 0;
    reg [N-1:0]  polarity = 4'b0000;
    wire [N-1:0] cs_out;
    wire any_selected;

    spi_cs_gen #(.N_SLAVES(N), .IDX_W(IW)) dut (
        .clk(clk), .rst_n(rst_n), .sel_en(sel_en), .sel_idx(sel_idx),
        .polarity(polarity), .cs_out(cs_out), .any_selected(any_selected));

    integer errors = 0, i, k;

    task chk;
        input [80*8-1:0] what;
        input [31:0] g, e;
        begin
            if (g !== e) begin
                $display("FAIL %0s: got 0b%0b exp 0b%0b", what, g, e);
                errors = errors + 1;
            end
        end
    endtask

    function integer n_asserted;
        input [N-1:0] cs;
        input [N-1:0] pol;
        integer c, j;
        begin
            c = 0;
            for (j = 0; j < N; j = j + 1)
                if (cs[j] == pol[j]) c = c + 1;
            n_asserted = c;
        end
    endfunction

    // CONTINUOUS INVARIANT: never more than one asserted.
    always @(posedge clk) if (rst_n) begin
        if (n_asserted(cs_out, polarity) > 1) begin
            $display("FAIL: %0d slaves asserted at t=%0t (cs=0b%0b pol=0b%0b)",
                     n_asserted(cs_out, polarity), $time, cs_out, polarity);
            errors = errors + 1;
        end
    end

    task settle; begin repeat (2) @(negedge clk); end endtask

    initial begin
        repeat (3) @(negedge clk); rst_n = 1; settle;

        chk("reset: all idle (active-low)", cs_out, 4'b1111);
        chk("reset: none selected",         any_selected, 0);

        sel_en = 1;
        for (i = 0; i < N; i = i + 1) begin
            sel_idx = i[IW-1:0]; settle;
            chk("select: exactly one asserted", n_asserted(cs_out, polarity), 1);
            chk("select: the right one",        cs_out[i],    1'b0);
            chk("select: any_selected",         any_selected, 1);
        end

        sel_en = 0; settle;
        chk("deselect: all idle",    cs_out,       4'b1111);
        chk("deselect: none active", n_asserted(cs_out, polarity), 0);
        chk("deselect: flag clear",  any_selected, 0);

        // polarity is STATIC: apply it through reset, not on a live bus.
        rst_n = 0; polarity = 4'b1010; sel_en = 0;
        repeat (2) @(negedge clk); rst_n = 1; settle;
        chk("mixed idle levels",    cs_out,       4'b0101);
        chk("mixed: none asserted", n_asserted(cs_out, polarity), 0);

        sel_en = 1;
        for (i = 0; i < N; i = i + 1) begin
            sel_idx = i[IW-1:0]; settle;
            chk("mixed select: exactly one",   n_asserted(cs_out, polarity), 1);
            chk("mixed select: correct level", cs_out[i],    polarity[i]);
        end

        sel_en = 0; rst_n = 0; polarity = 4'b0000;
        repeat (2) @(negedge clk); rst_n = 1; settle;
        sel_en = 1; sel_idx = 2'd3; settle;
        chk("idx 3 valid for N=4", n_asserted(cs_out, polarity), 1);

        sel_en = 0; settle;
        chk("final: all idle", cs_out, 4'b1111);

        if (errors == 0)
            $display("PASS: exactly one slave is ever asserted, each at its own polarity's active level, and deselect returns every line to its idle level");
        else
            $display("FAILED with %0d error(s)", errors);
        $finish;
    end

    initial begin #500000; $display("FAIL: watchdog timeout"); $finish; end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_gen.vhd — the same generator in VHDL
-- spi_cs_gen.vhd — the same chip-select generator in VHDL.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_cs_gen is
    generic (
        N_SLAVES : positive := 4;
        IDX_W    : positive := 2            -- 2**IDX_W >= N_SLAVES
    );
    port (
        clk          : in  std_logic;
        rst_n        : in  std_logic;
        sel_en       : in  std_logic;                            -- 0 = deselect all
        sel_idx      : in  unsigned(IDX_W - 1 downto 0);
        -- STATIC board property. Must be stable whenever the outputs are
        -- live: they are registered, so a change takes a cycle to reach the
        -- pins and the old idle levels read as asserted under the new one.
        polarity     : in  std_logic_vector(N_SLAVES - 1 downto 0);
        cs_out       : out std_logic_vector(N_SLAVES - 1 downto 0);
        any_selected : out std_logic
    );
end entity spi_cs_gen;

architecture rtl of spi_cs_gen is
    signal active  : std_logic_vector(N_SLAVES - 1 downto 0);
    signal next_cs : std_logic_vector(N_SLAVES - 1 downto 0);
begin

    -- At most one bit set: generated by indexing, not by masking, so a
    -- multi-select is not a state this hardware can enter.
    decode : process (sel_en, sel_idx) is
    begin
        active <= (others => '0');
        if sel_en = '1' and to_integer(sel_idx) < N_SLAVES then
            active(to_integer(sel_idx)) <= '1';
        end if;
        -- An out-of-range index deselects everything rather than wrapping.
    end process;

    -- active XOR (not polarity) gives the asserted level when selected and
    -- the idle level when not -- which is also the correct reset value.
    level : process (active, polarity) is
    begin
        for i in 0 to N_SLAVES - 1 loop
            next_cs(i) <= active(i) xor (not polarity(i));
        end loop;
    end process;

    regs : process (clk, rst_n) is
    begin
        if rst_n = '0' then
            cs_out       <= not polarity;   -- every slave idle
            any_selected <= '0';
        elsif rising_edge(clk) then
            cs_out <= next_cs;
            if active = (active'range => '0') then
                any_selected <= '0';
            else
                any_selected <= '1';
            end if;
        end if;
    end process;

end architecture rtl;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_gen_tb.vhd — the same checks in VHDL
-- spi_cs_gen_tb.vhd — the same checks in VHDL.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_cs_gen_tb is
end entity spi_cs_gen_tb;

architecture tb of spi_cs_gen_tb is
    constant N  : positive := 4;
    constant IW : positive := 2;

    signal clk      : std_logic := '0';
    signal rst_n    : std_logic := '0';
    signal sel_en   : std_logic := '0';
    signal sel_idx  : unsigned(IW - 1 downto 0) := (others => '0');
    signal polarity : std_logic_vector(N - 1 downto 0) := "0000";
    signal halt     : boolean := false;

    signal cs_out       : std_logic_vector(N - 1 downto 0);
    signal any_selected : std_logic;

    signal errors  : natural := 0;
    signal inv_err : natural := 0;

    function n_asserted (cs, pol : std_logic_vector) return natural is
        variable c : natural := 0;
    begin
        for j in cs'range loop
            if cs(j) = pol(j) then
                c := c + 1;
            end if;
        end loop;
        return c;
    end function;
begin

    clk <= not clk after 5 ns when not halt else '0';

    dut : entity work.spi_cs_gen
        generic map (N_SLAVES => N, IDX_W => IW)
        port map (clk => clk, rst_n => rst_n, sel_en => sel_en, sel_idx => sel_idx,
                  polarity => polarity, cs_out => cs_out, any_selected => any_selected);

    -- CONTINUOUS INVARIANT, owned by its own process.
    invariant : process (clk) is
    begin
        if rising_edge(clk) and rst_n = '1' then
            if n_asserted(cs_out, polarity) > 1 then
                report "FAIL: more than one slave asserted" severity error;
                inv_err <= inv_err + 1;
            end if;
        end if;
    end process;

    stim : process is
        procedure chk_v (what : string; g, e : std_logic_vector) is
        begin
            if g /= e then
                report "FAIL " & what severity error;
                errors <= errors + 1;
            end if;
        end procedure;

        procedure chk_b (what : string; g, e : std_logic) is
        begin
            if g /= e then
                report "FAIL " & what severity error;
                errors <= errors + 1;
            end if;
        end procedure;

        procedure chk_n (what : string; g, e : natural) is
        begin
            if g /= e then
                report "FAIL " & what & ": got " & integer'image(g)
                    & " exp " & integer'image(e) severity error;
                errors <= errors + 1;
            end if;
        end procedure;

        procedure settle is
        begin
            for i in 0 to 1 loop
                wait until falling_edge(clk);
            end loop;
        end procedure;
    begin
        for i in 0 to 2 loop
            wait until falling_edge(clk);
        end loop;
        rst_n <= '1'; settle;

        chk_v("reset: all idle (active-low)", cs_out, "1111");
        chk_b("reset: none selected",         any_selected, '0');

        sel_en <= '1';
        for i in 0 to N - 1 loop
            sel_idx <= to_unsigned(i, IW); settle;
            chk_n("select: exactly one asserted", n_asserted(cs_out, polarity), 1);
            chk_b("select: the right one",        cs_out(i), '0');
            chk_b("select: any_selected",         any_selected, '1');
        end loop;

        sel_en <= '0'; settle;
        chk_v("deselect: all idle",    cs_out, "1111");
        chk_n("deselect: none active", n_asserted(cs_out, polarity), 0);
        chk_b("deselect: flag clear",  any_selected, '0');

        -- polarity is STATIC: apply it through reset.
        rst_n <= '0'; polarity <= "1010"; sel_en <= '0';
        for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        rst_n <= '1'; settle;
        chk_v("mixed idle levels",    cs_out, "0101");
        chk_n("mixed: none asserted", n_asserted(cs_out, polarity), 0);

        sel_en <= '1';
        for i in 0 to N - 1 loop
            sel_idx <= to_unsigned(i, IW); settle;
            chk_n("mixed select: exactly one",   n_asserted(cs_out, polarity), 1);
            chk_b("mixed select: correct level", cs_out(i), polarity(i));
        end loop;

        sel_en <= '0'; rst_n <= '0'; polarity <= "0000";
        for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        rst_n <= '1'; settle;
        sel_en <= '1'; sel_idx <= to_unsigned(3, IW); settle;
        chk_n("idx 3 valid for N=4", n_asserted(cs_out, polarity), 1);

        sel_en <= '0'; settle;
        chk_v("final: all idle", cs_out, "1111");
        chk_n("invariant never violated", inv_err, 0);

        if errors = 0 then
            report "PASS: exactly one slave is ever asserted, each at its own "
                 & "polarity's active level, and deselect returns every line to "
                 & "its idle level" severity note;
        else
            report "FAILED with " & integer'image(errors) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

    watchdog : process is
    begin
        wait for 500 us;
        if not halt then
            report "FAIL: watchdog timeout" severity failure;
        end if;
        wait;
    end process;

end architecture tb;

Parity

All three implement the same hardware: identical ports and generics, an index-based one-hot decode with an out-of-range index deselecting, a per-line polarity XOR, registered outputs, and an asynchronous reset to each line's own idle level. All three testbenches exercise every slave at uniform and mixed polarity, apply polarity through reset because it is static, and run a continuous one-hot invariant on every clock.

7. Why a Verification Engineer Cares

The one-hot property is the assertion that matters, and it is worth writing even though §6's design makes it structural — because the next design may not:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_gen.sva — at most one, at each line's own polarity
   // 1. THE property. Written against the per-line polarity, so it is
   //    correct on a mixed bus where "asserted" is a different level on
   //    different pins.
   function automatic int n_asserted(logic [N-1:0] cs, logic [N-1:0] pol);
       int c = 0;
       for (int i = 0; i < N; i++) if (cs[i] == pol[i]) c++;
       return c;
   endfunction

   a_at_most_one : assert property (
       @(posedge clk) disable iff (!rst_n) n_asserted(cs_out, polarity) <= 1)
       else $error("more than one slave asserted");

   // 2. Out of reset, nothing is selected. A device selected out of reset
   //    will drive MISO before the master has configured anything.
   a_reset_deselects : assert property (
       @(posedge clk) $rose(rst_n) |-> (n_asserted(cs_out, polarity) == 0))
       else $error("a slave was selected immediately after reset");

   // 3. polarity is static while live. A change takes a cycle to reach the
   //    pins, and during that cycle the old idle levels read as asserted
   //    under the new convention.
   a_polarity_static : assert property (
       @(posedge clk) disable iff (!rst_n)
           (n_asserted(cs_out, polarity) > 0) |=> $stable(polarity))
       else $error("polarity changed while a slave was selected");

What these prove. That the generator never asserts two lines, never selects out of reset, and is not reconfigured mid-selection. What they cannot prove is that the board wires each select to the device the index assumes, or that the polarity configured matches the part fitted — both are schematic facts, and a generator provably correct against the wrong configuration selects the wrong device perfectly.

Coverage should target the transitions, not the states:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_cg.sv — selecting is easy; SWITCHING is where bugs live
   covergroup spi_cs_cg @(posedge clk);
       cp_sel : coverpoint sel_idx iff (sel_en) { bins slave[] = {[0:N-1]}; }

       // The transition is the interesting event: deselect-then-select,
       // or a direct switch from one slave to another.
       cp_transition : coverpoint {last_sel_en, sel_en} {
           bins idle_to_sel = {2'b01};
           bins sel_to_idle = {2'b10};
           bins sel_to_sel  = {2'b11};   // a direct handover (Chapter 8.4)
       }

       cp_polarity : coverpoint polarity {
           bins all_low  = {'0};
           bins all_high = {'1};
           bins mixed    = default;      // the case a uniform assumption breaks
       }

       x_trans_pol : cross cp_transition, cp_polarity;
   endgroup

sel_to_sel is the bin that matters and the one a simple test misses. Selecting and deselecting one device at a time never hands the bus from one driver to another, which is exactly the case Chapter 8.4 exists for.

8. Why an FPGA or ASIC Engineer Cares

Register the select outputs. A combinational decode glitches while the index changes — briefly asserting a line that was neither the old nor the new selection. On a select line that is a spurious frame, and the device on the other end may act on it. Registering costs one flip-flop per line and removes the class.

Reset to the idle level, which is not necessarily high. cs_out <= ~polarity is the correct reset expression. Resetting an active-high select to logic 1 asserts it, and the device will be selected from power-up until software intervenes — during which it drives MISO against whatever else is on the net.

Put a pull-up (or pull-down) on every select line. Between power-up and the FPGA's outputs becoming driven, the lines float, and a floating select is a randomly selected device. The resistor makes the safe state the default, and it is the electrical reason active-low became the convention.

Do not share a select line between two devices to save a pin. It is occasionally proposed for devices "that are never used at the same time", and it works until one of them is a device that drives MISO whenever selected. Two devices on one select is a guaranteed contention every transaction, which is Chapter 8.5.

9. Failure Signature — One Device Responds to Every Transaction

Symptom. A board with three SPI devices behaves as though only one exists. Transactions intended for any of them return data that looks like it came from the same part, and that part appears to respond correctly. The other two never respond to anything.

What "one device answers everything" establishes. That device is selected all the time. It is not a data, mode or framing problem — the responses are well-formed, which means clocking and framing are correct. Something is holding one select line asserted permanently, so every transaction on the shared wires is that device's transaction.

Plausible mechanisms.

  • The device is active-high select and the master is driving it active-low, so the idle level selects it — §2's failure exactly.
  • A select line is shorted to its active level, or the master's pin is misconfigured as an input with a pull that asserts it.
  • The master's select is never deasserted because the driver holds it for efficiency (Chapter 5.1 §11's GPIO pattern, taken too far).
  • The wrong pin is driven, leaving the intended one at its asserted default.

The discriminating observation. Measure the three select lines with the bus idle. Exactly zero should be asserted between transactions — and "asserted" means each line's own active level, which is the trap. On a mixed-polarity board a line sitting at logic 0 may be correctly idle or permanently selected depending on the part, so the measurement must be read against the datasheet rather than against the convention.

Why the investigation goes wrong. Because the responding device works correctly, which reads as "SPI is fine, the other devices are broken". The bus is fine; the selection is not. Checking idle-state select levels takes a minute and is skipped because the failure does not look like a selection problem — it looks like two dead parts.

10. Common Misconceptions

11. Reason It Through

Work this before reading the answer.

A design has four SPI devices. To save pins, an engineer proposes driving the four select lines from a 2-to-4 decoder, using two GPIOs instead of four. The decoder always asserts exactly one output.

Will this work? What does it cost, and what is the fix?

It halves the pin count, and it breaks something important. A 2-to-4 decoder asserts exactly one output for every input combination — there is no code that asserts none. So one device is always selected, and the all-deselected state that §5 called "the correct state between transactions" no longer exists.

What breaks, concretely. Three things, in increasing order of seriousness.

There is no idle state. Some device is always listening to SCLK and MOSI, and whatever the master does on those wires — including traffic that is not meant for it — it will interpret as a transaction.

There is no bus turnaround. Switching from device A to device B moves the decoder output directly from one to the other with no interval in which nothing is selected, so A's MISO driver is still turning off as B's turns on. That is the contention window of Chapter 8.4, created deliberately.

A partial transaction is left behind on every switch. Whichever device was selected has its sequencer mid-frame when selection moves away, and it never sees a clean CS rise — so Chapter 8.7's abort semantics are invoked constantly rather than exceptionally.

The fix is one more pin, not four. Use a decoder with an enable — a 74HC138 and most equivalents have one. The enable gives back the all-deselected state, so the arrangement becomes three pins for four devices with correct semantics: two for the index, one for the enable.

And note what that is: exactly the interface of §6's generator. sel_idx plus sel_en is the same structure, which is not a coincidence — an index with an enable is the minimal encoding that can express "one of N, or none".

Does it scale? Yes, and better than linear. Eight devices need three index pins plus an enable — four pins instead of eight. Sixteen need five instead of sixteen. The cost is an external part, one gate delay, and the loss of the ability to assert two selects deliberately, which Chapter 8.2 shows is occasionally wanted for broadcast writes.

The general lesson. When compressing an encoding, check that every state you relied on survives — including the ones that represent nothing happening. "Exactly one" and "at most one" differ by a single state, and that state is where the idle, the turnaround and the safe reset all live.

12. Understanding Check

13. Summary

Selection is the scope of a transaction, not an enable. Between the two CS edges the device runs its sequencer, interprets SCLK, owns MISO and is committable; outside them it must behave as though the bus were not there.

Active-low is a convention with a real electrical justification — a pull-up makes deselected the default — but active-high parts exist, so polarity belongs in the configuration rather than in an assumption.

The two edges are not symmetric: a spurious falling edge opens a frame that will be discarded, while a spurious rising edge commits, releases and resynchronises. The rising edge is the dangerous one.

Selection is physical: a pin, not an address. That removes address collisions entirely and costs pins linearly.

The governing rule is at most one selected — not exactly one, because zero is the correct state between transactions and the only place an idle, a turnaround and a safe reset can live. The rule can be checked or made unrepresentable, and driving an index rather than a mask does the latter for free.

In RTL that is an index-to-one-hot decode, a per-line polarity XOR, and a registered output reset to ~polarity — each line's own idle level, which is not all-ones on a mixed bus.

For verification the one-hot property must be written against each line's polarity, checked continuously rather than sampled, and coverage must include the slave-to-slave transition, which selecting one device at a time never reaches.

14. What Comes Next

This chapter established what selection means for one device. Chapter 8.2 — Shared MOSI, Shared MISO, Individual CS looks at the wiring that makes it work for several: which signals fan out, which is shared with one driver at a time, why the pin cost is the dominant practical constraint on SPI system size, and how the alternatives — decoded selects, daisy chains and multiple buses — trade pins against complexity and against each other.

Continue learning