Skip to content
VLSI Mentor

SPI · Module 8

CS Glitches and False Selection

How a noise event becomes a transaction: what a glitch does to a device's state, why the half-selected device it leaves is worse than a broken one, why a synchroniser cannot help, and the glitch filter that can.

Chapter 8.5 needed two devices selected at once. This chapter is about the most common way that happens when nobody intended it.

A chip select line picks up a 40 ns noise pulse. The master did nothing. What does the device on the other end do?

It opens a frame — because from the device's side there is no difference between a select edge caused by the master and one caused by a coupled trace.

1. A Device Cannot Tell

The device's view of chip select is a level on a pin. Chapter 8.1 §1 listed what a falling edge commits it to: sequencer reset, SCLK interpretation, MISO ownership, committability. None of those checks why the edge occurred, because there is nothing to check against.

So a glitch wide enough to be sampled produces a genuine, correctly-executed frame opening. The device is not malfunctioning; it is doing exactly what it was designed to do, to a signal that lied to it.

Where glitches come from, in rough order of frequency:

  • Crosstalk from an adjacent fast signal — SCLK is the usual culprit, because it is the fastest edge on the bus and is often routed alongside the selects.
  • Ground bounce during a simultaneous-switching event, which shifts the device's reference rather than the signal.
  • Connector or cable noise on an off-board device, where the select line is long and unterminated.
  • A driver being released — a GPIO reconfigured, a bus switch changing state, or a master in reset — letting the line float before a pull-up establishes it.

2. What It Leaves Behind

The immediate effect of a false selection is small. The damaging effect is what the device is left holding.

A frame that opens and closes quickly leaves the sequencer reset and nothing committed — Chapter 5.1's abort semantics handle it cleanly, and Chapter 8.7 covers the classification. If that were the whole story, glitches would be a nuisance rather than a hazard.

The problem is the glitch that arrives during another device's transaction. Now two devices are selected: the intended one, mid-transfer, and the glitched one, which has just opened a frame and is interpreting the same SCLK and MOSI. Both reach a data phase. Both enable MISO. That is Chapter 8.5, caused by nothing the master did.

And the worst case is a glitch that only opens. A falling-edge glitch that is not followed by a matching rise — because the line settled low, or because the rise was too small to register — leaves the device selected indefinitely. It shifts every subsequent SCLK edge as though it were part of its own transaction, accumulating a nonsense command, and it drives MISO throughout. The bus is now permanently contended and the device is in a state no transaction will clear, because the only thing that resynchronises it is a CS rise (Chapter 4.4 §7) that never comes.

That is the "half-selected device" of this chapter's title, and it is worse than an obviously broken one precisely because the system still mostly works.

3. A Synchroniser Does Not Help

Chapter 5.2 §7 stated this and deferred the remedy; here it is, and the distinction is worth restating because "add another sync stage" is the reflexive and useless response.

A synchroniser answers given that this signal changed near my clock edge, what value did it have? It resolves metastability. It has no opinion about whether the change was intended, so a glitch passes through unchanged — merely delayed.

Adding stages makes it worse, not better: more latency out of the CS lead budget §12, and no more rejection.

The mechanisms are genuinely orthogonal and both are needed:

MechanismAnswersCosts
Synchroniserwhat value did it have?2 cycles, mandatory
Glitch filterwas it intended?N cycles, optional

And the order matters: synchronise first, filter second. Filtering a possibly-metastable sample means counting undefined values, which is worse than not filtering at all.

4. Rejected and Accepted

One of these is noise; the filter decides by duration

10 cycles
A raw chip select line showing a brief glitch followed later by a real assertion. A filtered chip select ignores the glitch entirely and follows the real assertion after a delay. A glitch-detected pulse marks the rejected event.glitchglitchreal edgereal edgecs rawcs cleanglitcht0t1t2t3t4t5t6t7t8t9
Figure 1 — a short glitch and a real assertion on the same line. The filter requires several consecutive agreeing samples, so the glitch never reaches the clean output and is reported; the real edge is accepted after the threshold, which is why the clean signal lags the raw one.

The lag on the clean signal is the filter's cost, and it is not free: it comes out of the same CS lead budget as the synchroniser's latency. A filter requiring three samples on a 25 MHz clock adds 120 ns before the device knows it is selected, on top of the synchroniser's 80 ns — and the master must leave at least that much between asserting CS and clocking.

5. The Threshold Is the Specification

A filter has a threshold, not judgement. That produces three regions, and being explicit about all three is what makes the design honest:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   pulse shorter than the threshold  →  rejected, and reported
   pulse longer than the threshold   →  ACCEPTED as a real transition
   pulse near the threshold          →  depends on sampling phase

The middle row is the one to internalise. A glitch longer than FILTER_N samples is indistinguishable from a real edge, and the filter will accept it. Choosing FILTER_N is therefore choosing what counts as noise, and it must be set from the longest expected glitch rather than from the shortest.

The third row matters too: a pulse right at the threshold may be accepted or rejected depending on where it lands relative to the sampling clock, so the specification is really "reliably rejects below X, reliably accepts above Y", with a grey band between. Designing with margin on both sides is what keeps the behaviour deterministic.

6. Building the Glitch Filter — Three HDLs

The circuit

Circuit. A two-flop synchroniser, a counter of consecutive agreeing samples, and a registered clean output.

State. The synchroniser stages, the agreement counter, and the accepted value.

Datapath. None.

Control. While the synchronised input differs from the accepted output, the counter advances; on reaching the threshold the new value is accepted. If the input reverts before that, the counter resets and a glitch is reported.

Clock and reset. System clock; asynchronous active-low reset with the synchroniser and output at idle-high — deselected is the safe state, and resetting to low would manufacture a selection.

Enables. The chain runs continuously; it must be sampling before any edge arrives.

Timing. Acceptance takes 2 + FILTER_N cycles: two for the synchroniser and FILTER_N for the agreement count. That total is what comes out of the CS lead budget.

Synthesis. 2 + ceil(log2(FILTER_N)) flip-flops plus the output register and a comparator. Trivially small.

Limitations. It rejects by duration only. A long glitch is accepted, as §5 says plainly, and the testbench asserts that behaviour rather than hiding it.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_filter.sv — synchronise first, then require agreement
// spi_cs_filter.sv — rejecting chip-select glitches.
//
// Chapter 5.2 §7 established that a synchroniser resolves METASTABILITY and
// does not decide whether a pulse was intended -- and that adding stages adds
// latency without adding rejection. This is the separate mechanism it
// promised: a candidate transition is accepted only after FILTER_N
// consecutive samples agree with it.
//
// The honest limitation is stated by the design rather than hidden: a glitch
// LONGER than FILTER_N samples is accepted as a real transition. A filter has
// a threshold, not judgement, and the threshold is the whole specification.
module spi_cs_filter #(
    parameter int FILTER_N = 3       // consecutive agreeing samples required
) (
    input  logic clk,
    input  logic rst_n,
    input  logic cs_n_raw,           // straight from the pin
    output logic cs_n_clean,         // safe to derive frame edges from
    output logic glitch_seen         // pulse: a candidate reverted before acceptance
);
    localparam int CW = (FILTER_N > 1) ? $clog2(FILTER_N) : 1;

    // Metastability first, filtering second. The order matters: filtering a
    // possibly-metastable sample would be counting undefined values.
    logic [1:0]    sync;
    logic [CW-1:0] agree_cnt;
    logic          synced;

    assign synced = sync[1];

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sync        <= 2'b11;        // idle high: deselected is the safe state
            cs_n_clean  <= 1'b1;
            agree_cnt   <= '0;
            glitch_seen <= 1'b0;
        end else begin
            sync        <= {sync[0], cs_n_raw};
            glitch_seen <= 1'b0;

            if (synced != cs_n_clean) begin
                // A candidate transition is in progress.
                if (agree_cnt == CW'(FILTER_N - 1)) begin
                    cs_n_clean <= synced;     // accepted
                    agree_cnt  <= '0;
                end else begin
                    agree_cnt <= agree_cnt + 1'b1;
                end
            end else begin
                // The input agrees with the accepted value. If a candidate was
                // part-way through, it reverted -- that is a glitch.
                if (agree_cnt != '0) glitch_seen <= 1'b1;
                agree_cnt <= '0;
            end
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_filter_tb.sv — rejected, accepted, and the honest limit
// spi_cs_filter_tb.sv — a real edge is accepted, a short glitch is rejected
// and reported, and a long glitch is accepted (a filter has a threshold).
`timescale 1ns/1ps
module spi_cs_filter_tb;
    logic clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    localparam int FN = 3;
    logic cs_n_raw = 1;
    logic cs_n_clean, glitch_seen;

    spi_cs_filter #(.FILTER_N(FN)) dut (
        .clk, .rst_n, .cs_n_raw, .cs_n_clean, .glitch_seen);

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

    always @(posedge clk) if (rst_n && glitch_seen) glitches++;

    // Hold the raw input at a value for n clock cycles.
    task automatic hold(input logic v, input int n);
        cs_n_raw = v;
        repeat (n) @(negedge clk);
    endtask

    int g0;
    initial begin
        repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
        chk("reset: deselected", cs_n_clean, 1);

        // --- a REAL assertion: held well past the threshold ---
        g0 = glitches;
        hold(1'b0, 12);
        chk("real edge accepted",    cs_n_clean, 0);
        chk("no glitch reported",    glitches - g0, 0);

        // --- a REAL deassertion ---
        hold(1'b1, 12);
        chk("real release accepted", cs_n_clean, 1);

        // --- a SHORT glitch: 2 samples, below the 3-sample threshold ---
        g0 = glitches;
        hold(1'b0, 2);
        hold(1'b1, 12);
        chk("short glitch rejected", cs_n_clean, 1);
        chk("glitch reported",       glitches - g0, 1);

        // --- an even shorter glitch: 1 sample ---
        g0 = glitches;
        hold(1'b0, 1);
        hold(1'b1, 12);
        chk("1-sample glitch rejected", cs_n_clean, 1);
        chk("glitch reported",          glitches - g0, 1);

        // --- a LONG glitch: 8 samples, well past the threshold. This is
        //     ACCEPTED, and that is the honest limit of a filter. ---
        g0 = glitches;
        hold(1'b0, 8);
        chk("long glitch accepted as real", cs_n_clean, 0);
        hold(1'b1, 12);
        chk("and released again",           cs_n_clean, 1);

        // --- repeated short glitches must never accumulate into acceptance.
        //     They are separated by more than the synchroniser latency so
        //     each is independently observable; glitches closer together
        //     than that merge into a single report, which is a property of
        //     the synchroniser rather than of the filter. ---
        g0 = glitches;
        for (int i = 0; i < 6; i++) begin
            hold(1'b0, 2);
            hold(1'b1, 6);
        end
        chk("repeated short glitches never accepted", cs_n_clean, 1);
        if (glitches - g0 < 6) begin
            $display("FAIL: expected at least 6 glitch reports, got %0d", glitches - g0);
            errors++;
        end

        if (errors == 0)
            $display("PASS: transitions held for at least %0d samples are accepted, shorter ones are rejected and reported, and repeated short glitches never accumulate", FN);
        else
            $display("FAILED with %0d error(s)", errors);
        $finish;
    end

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

Three things in that testbench are deliberate.

It asserts that a long glitch is accepted. That looks like testing a bug and is the opposite: the threshold behaviour is the specification, and a design that somehow rejected an eight-sample pulse would be doing something other than what was specified.

It runs repeated short glitches and checks they never accumulate into an acceptance — catching a counter that fails to reset on reversion, which would let a burst of noise eventually be accepted.

It separates those repeated glitches by more than the synchroniser latency, because glitches closer together than that merge into a single reported event. That is a property of the synchroniser rather than the filter, and the testbench documents it rather than papering over it.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_filter.v — the same filter in Verilog-2001
// spi_cs_filter.v — the same glitch filter in Verilog-2001.
module spi_cs_filter #(
    parameter FILTER_N = 3          // consecutive agreeing samples required
) (
    input  wire clk,
    input  wire rst_n,
    input  wire cs_n_raw,           // straight from the pin
    output reg  cs_n_clean,         // safe to derive frame edges from
    output reg  glitch_seen         // pulse: a candidate reverted
);
    function integer clogb2;
        input integer value;
        integer v;
        begin
            v = value - 1;
            for (clogb2 = 0; v > 0; clogb2 = clogb2 + 1) v = v >> 1;
        end
    endfunction

    localparam CW = (FILTER_N > 1) ? clogb2(FILTER_N) : 1;

    // Metastability first, filtering second: filtering a possibly-metastable
    // sample would be counting undefined values.
    reg [1:0]    sync;
    reg [CW-1:0] agree_cnt;
    wire         synced = sync[1];

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sync        <= 2'b11;       // idle high: deselected is safe
            cs_n_clean  <= 1'b1;
            agree_cnt   <= {CW{1'b0}};
            glitch_seen <= 1'b0;
        end else begin
            sync        <= {sync[0], cs_n_raw};
            glitch_seen <= 1'b0;

            if (synced != cs_n_clean) begin
                if (agree_cnt == (FILTER_N - 1)) begin
                    cs_n_clean <= synced;       // accepted
                    agree_cnt  <= {CW{1'b0}};
                end else begin
                    agree_cnt <= agree_cnt + 1'b1;
                end
            end else begin
                // A candidate part-way through has reverted: a glitch.
                if (agree_cnt != {CW{1'b0}}) glitch_seen <= 1'b1;
                agree_cnt <= {CW{1'b0}};
            end
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_filter_tb.v — the same checks in Verilog-2001
// spi_cs_filter_tb.v — the same checks in Verilog-2001.
`timescale 1ns/1ps
module spi_cs_filter_tb;
    reg clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    parameter FN = 3;
    reg cs_n_raw = 1;
    wire cs_n_clean, glitch_seen;

    spi_cs_filter #(.FILTER_N(FN)) dut (
        .clk(clk), .rst_n(rst_n), .cs_n_raw(cs_n_raw),
        .cs_n_clean(cs_n_clean), .glitch_seen(glitch_seen));

    integer errors = 0, glitches = 0, g0 = 0, i;

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

    always @(posedge clk) if (rst_n && glitch_seen) glitches = glitches + 1;

    task hold;
        input v;
        input integer n;
        begin
            cs_n_raw = v;
            repeat (n) @(negedge clk);
        end
    endtask

    initial begin
        repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
        chk("reset: deselected", cs_n_clean, 1);

        g0 = glitches;
        hold(1'b0, 12);
        chk("real edge accepted", cs_n_clean, 0);
        chk("no glitch reported", glitches - g0, 0);

        hold(1'b1, 12);
        chk("real release accepted", cs_n_clean, 1);

        g0 = glitches;
        hold(1'b0, 2);
        hold(1'b1, 12);
        chk("short glitch rejected", cs_n_clean, 1);
        chk("glitch reported",       glitches - g0, 1);

        g0 = glitches;
        hold(1'b0, 1);
        hold(1'b1, 12);
        chk("1-sample glitch rejected", cs_n_clean, 1);
        chk("glitch reported",          glitches - g0, 1);

        // A glitch longer than the threshold IS accepted: a filter has a
        // threshold, not judgement.
        g0 = glitches;
        hold(1'b0, 8);
        chk("long glitch accepted as real", cs_n_clean, 0);
        hold(1'b1, 12);
        chk("and released again",           cs_n_clean, 1);

        g0 = glitches;
        for (i = 0; i < 6; i = i + 1) begin
            hold(1'b0, 2);
            hold(1'b1, 6);
        end
        chk("repeated short glitches never accepted", cs_n_clean, 1);
        if (glitches - g0 < 6) begin
            $display("FAIL: expected at least 6 glitch reports, got %0d", glitches - g0);
            errors = errors + 1;
        end

        if (errors == 0)
            $display("PASS: transitions held for at least %0d samples are accepted, shorter ones are rejected and reported, and repeated short glitches never accumulate", FN);
        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_filter.vhd — the same filter in VHDL
-- spi_cs_filter.vhd — the same glitch filter in VHDL.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_cs_filter is
    generic (
        FILTER_N : positive := 3        -- consecutive agreeing samples required
    );
    port (
        clk         : in  std_logic;
        rst_n       : in  std_logic;
        cs_n_raw    : in  std_logic;    -- straight from the pin
        cs_n_clean  : out std_logic;    -- safe to derive frame edges from
        glitch_seen : out std_logic     -- pulse: a candidate reverted
    );
end entity spi_cs_filter;

architecture rtl of spi_cs_filter is
    -- Metastability first, filtering second: filtering a possibly-metastable
    -- sample would be counting undefined values.
    signal sync       : std_logic_vector(1 downto 0);
    signal agree_cnt  : integer range 0 to FILTER_N - 1;
    signal clean_r    : std_logic;
    signal synced     : std_logic;
begin

    synced     <= sync(1);
    cs_n_clean <= clean_r;

    process (clk, rst_n) is
    begin
        if rst_n = '0' then
            sync        <= (others => '1');   -- idle high: deselected is safe
            clean_r     <= '1';
            agree_cnt   <= 0;
            glitch_seen <= '0';
        elsif rising_edge(clk) then
            sync        <= sync(0) & cs_n_raw;
            glitch_seen <= '0';

            if synced /= clean_r then
                if agree_cnt = FILTER_N - 1 then
                    clean_r   <= synced;      -- accepted
                    agree_cnt <= 0;
                else
                    agree_cnt <= agree_cnt + 1;
                end if;
            else
                -- A candidate part-way through has reverted: a glitch.
                if agree_cnt /= 0 then
                    glitch_seen <= '1';
                end if;
                agree_cnt <= 0;
            end if;
        end if;
    end process;

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

entity spi_cs_filter_tb is
end entity spi_cs_filter_tb;

architecture tb of spi_cs_filter_tb is
    constant FN : positive := 3;

    signal clk         : std_logic := '0';
    signal rst_n       : std_logic := '0';
    signal cs_n_raw    : std_logic := '1';
    signal halt        : boolean := false;
    signal cs_n_clean  : std_logic;
    signal glitch_seen : std_logic;

    signal errors   : natural := 0;
    signal glitches : natural := 0;
begin

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

    dut : entity work.spi_cs_filter
        generic map (FILTER_N => FN)
        port map (clk => clk, rst_n => rst_n, cs_n_raw => cs_n_raw,
                  cs_n_clean => cs_n_clean, glitch_seen => glitch_seen);

    counter : process (clk) is
    begin
        if rising_edge(clk) and rst_n = '1' and glitch_seen = '1' then
            glitches <= glitches + 1;
        end if;
    end process;

    stim : process is
        variable g0 : natural;

        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 hold (v : std_logic; n : positive) is
        begin
            cs_n_raw <= v;
            for i in 1 to n 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';
        wait until falling_edge(clk);
        chk_b("reset: deselected", cs_n_clean, '1');

        g0 := glitches;
        hold('0', 12);
        chk_b("real edge accepted", cs_n_clean, '0');
        chk_n("no glitch reported", glitches - g0, 0);

        hold('1', 12);
        chk_b("real release accepted", cs_n_clean, '1');

        g0 := glitches;
        hold('0', 2);
        hold('1', 12);
        chk_b("short glitch rejected", cs_n_clean, '1');
        chk_n("glitch reported",       glitches - g0, 1);

        g0 := glitches;
        hold('0', 1);
        hold('1', 12);
        chk_b("1-sample glitch rejected", cs_n_clean, '1');
        chk_n("glitch reported",          glitches - g0, 1);

        -- A glitch longer than the threshold IS accepted.
        hold('0', 8);
        chk_b("long glitch accepted as real", cs_n_clean, '0');
        hold('1', 12);
        chk_b("and released again",           cs_n_clean, '1');

        g0 := glitches;
        for i in 0 to 5 loop
            hold('0', 2);
            hold('1', 6);
        end loop;
        chk_b("repeated short glitches never accepted", cs_n_clean, '1');
        if glitches - g0 < 6 then
            report "FAIL: expected at least 6 glitch reports" severity error;
            errors <= errors + 1;
        end if;

        if errors = 0 then
            report "PASS: transitions held for at least " & integer'image(FN)
                 & " samples are accepted, shorter ones are rejected and reported, "
                 & "and repeated short glitches never accumulate" 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, a two-flop synchroniser ahead of the filter, an agreement counter that resets on reversion and reports a glitch when it does, a registered clean output, and an asynchronous reset to idle-high. All three testbenches exercise a real edge, one- and two-sample glitches, an eight-sample glitch that is correctly accepted, and six separated short glitches that never accumulate.

7. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_filter.sva — the filter's contract, in both directions
   // 1. A pulse shorter than the threshold never reaches the output.
   //    Expressed on the SYNCHRONISED input, because that is what the
   //    filter actually sees.
   property p_short_rejected;
       @(posedge clk) disable iff (!rst_n)
           ($changed(synced) ##1 $changed(synced)[->1] within
            ($stable(synced)[*0:FILTER_N-1]))
           |-> $stable(cs_n_clean);
   endproperty

   // 2. A glitch is REPORTED when rejected. Silent rejection is worse than
   //    no filter: the board has a noise problem that nothing surfaces.
   a_glitch_reported : assert property (
       @(posedge clk) disable iff (!rst_n)
           (agree_cnt != 0 && synced == cs_n_clean) |=> glitch_seen)
       else $error("a rejected candidate was not reported");

   // 3. The output changes only after the full threshold. Catches an
   //    off-by-one that accepts one sample early.
   a_threshold_respected : assert property (
       @(posedge clk) disable iff (!rst_n)
           $changed(cs_n_clean) |-> ($past(agree_cnt) == FILTER_N - 1))
       else $error("output changed before the threshold was met");

   // 4. Out of reset, deselected. A filter resetting to "selected" opens a
   //    frame the master never started.
   a_reset_idle : assert property (
       @(posedge clk) $rose(rst_n) |-> cs_n_clean)
       else $error("filter output asserted out of reset");

Property 2 deserves emphasis. A filter that silently rejects glitches converts a visible problem into an invisible one: the board has a noise issue, the filter is absorbing it, and nobody knows until the noise grows past the threshold and the filter stops absorbing it. The glitch_seen output exists so that a driver can count events and a system can report "this bus is noisy" long before it starts failing.

What these prove. That the filter's duration contract holds in both directions. What they cannot prove is that FILTER_N is large enough for the board's actual noise — a physical measurement, not a simulation result.

Coverage must include the boundary and the accepted-glitch case:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cs_glitch_cg.sv — pulse width relative to the threshold
   covergroup spi_cs_glitch_cg @(posedge clk);
       cp_pulse_width : coverpoint pulse_samples iff (pulse_ended) {
           bins sub_threshold = {[1:FILTER_N-1]};   // rejected
           bins at_threshold  = {FILTER_N};         // the grey band
           bins over          = {[FILTER_N+1:$]};   // ACCEPTED -- by design
       }

       // Where the glitch landed relative to a real transaction. A glitch
       // during another device's frame is the contention case (Chapter 8.5);
       // one on an idle bus is comparatively harmless.
       cp_context : coverpoint glitch_context {
           bins bus_idle        = {CTX_IDLE};
           bins other_selected  = {CTX_OTHER_ACTIVE};   // the dangerous one
           bins self_selected   = {CTX_SELF_ACTIVE};
       }

       x_width_context : cross cp_pulse_width, cp_context;
   endgroup

cp_context.other_selected is the bin that connects this chapter to the last one. A glitch on an idle bus opens and closes a harmless empty frame; the same glitch during another device's transaction produces contention. Most glitch testing injects noise on an idle bus, which exercises the filter and misses the consequence.

8. Why an FPGA or ASIC Engineer Cares

Fix it at the board first. A filter is a mitigation, not a solution. Routing select lines away from SCLK, adding ground between them, terminating long runs, and putting a small capacitor at the device's select pin all attack the cause. A filter that is absorbing constant noise is a design working at its margin.

A series resistor and capacitor at the pin is the cheapest filter of all. An RC of a few tens of nanoseconds on each select line rejects short glitches before they reach the device, works for devices whose internals you cannot change, and costs two passives. Its disadvantage is that it slows the real edge too — the same trade as the digital filter, paid in analogue.

Budget the latency. Synchroniser plus filter is 2 + FILTER_N cycles before the device knows it is selected, and the master's CS lead must exceed it. At 25 MHz with FILTER_N = 3 that is 200 ns, which is a lot compared to the lead many masters default to — and it is the same failure as Chapter 5.2 §12, made worse by the filter.

Do not filter SCLK the same way. It is tempting to apply the same treatment to the clock, and it is wrong: a filter delays every edge, and SCLK's edges carry the timing the whole protocol depends on. Noise on SCLK is a signal-integrity problem to be fixed physically, not filtered.

On a long cable, consider a differential or buffered select. If the select line leaves the board, the noise environment changes entirely and a filter sized for on-board crosstalk will be inadequate. A buffer at the far end, or a differential pair, addresses the cause.

9. Failure Signature — A Device That Is Occasionally Permanently Selected

Symptom. A multi-device bus works normally for hours, then enters a state where all transactions fail and one device is warm. A power cycle clears it completely and it does not recur for hours. No software error is reported, and the failure never happens during any particular operation.

What "a power cycle clears it" establishes. The device is in a persistent bad state rather than experiencing an ongoing fault. Something put it there and nothing has taken it out — which immediately rules out marginal timing, contention during handover, and anything that would recur or self-clear.

What the warmth adds. Chapter 8.5 §9: a warm SPI device with no other explanation is sustained contention. Combined with persistence, that means a device is driving MISO continuously.

Plausible mechanisms.

  • A glitch opened a frame that never closed — §2's worst case. The device is selected indefinitely, shifting every SCLK edge and driving MISO throughout, and only a CS rise would clear it.
  • A select line has failed low — a solder bridge or a damaged driver. But that would not be cleared by a power cycle, and would recur immediately.
  • A device has latched up, which a power cycle also clears — genuinely possible and distinguishable by whether a select toggle recovers it.

The discriminating observation, and it is decisive. Before power-cycling, toggle every chip select — assert and deassert each one in turn from software. If the fault clears, a device was stuck selected and the rising edge resynchronised it (Chapter 4.4 §7): that is the glitch signature, confirmed. If toggling does nothing and only a power cycle helps, the device is latched up or damaged and the cause is electrical rather than protocol.

That test costs one line of code, distinguishes two very different faults, and is almost never tried because power-cycling works and ends the investigation.

Why the investigation goes wrong. Because the failure is rare, non-reproducible and cleared by the most common troubleshooting action. Nobody captures the state, so the evidence is destroyed by the fix — which is why a driver that logs glitch_seen counts, or simply toggles all selects before giving up, converts an unreproducible field fault into a diagnosable one.

10. Common Misconceptions

11. Reason It Through

Work this before reading the answer.

A board has four SPI devices. Select lines are routed as a bundle alongside SCLK for 80 mm. At 1 MHz the system is perfectly reliable. At 20 MHz, occasional transactions fail, and the failure rate rises sharply with clock speed. Adding a 3-sample glitch filter in the FPGA slave fixes it for three of the four devices — the fourth is a flash chip whose internals cannot be changed.

What is happening, and what should be done about the flash?

Why the rate depends on SCLK. Crosstalk couples through mutual capacitance and inductance, and the coupled voltage depends on the edge rate of the aggressor, not its frequency directly. Raising SCLK from 1 MHz to 20 MHz on most drivers does not change the edge rate much — but it multiplies the number of edges per second by twenty, so the number of coupling events, and therefore the probability that one lands badly, rises proportionally.

That the filter fixed three devices confirms the mechanism: the disturbances are short, shorter than three sample periods, which is exactly what capacitive crosstalk from a fast edge looks like.

Why the flash is different. Not because it is more susceptible — because its input cannot be filtered. The three FPGA slaves have a filter inserted in front of their internal logic; the flash's select pin goes straight to silicon you do not control.

So the fix must move from the receiver to the channel or the source, and there are four real options.

An RC filter at the flash's select pin. A series resistor and a small capacitor form a low-pass filter in the analogue domain, doing outside the chip what the FPGA does inside it. Sizing it for, say, 50 ns rejects the crosstalk while adding 50 ns to the real edge — which must fit inside the CS lead budget. This is the direct analogue of the digital fix and usually the right answer.

Route the flash's select away from SCLK. Attacks the cause rather than the symptom. Costs a board revision, and is the correct fix if one is happening anyway.

Slow SCLK's edge rate. Reducing the master's drive strength or adding a series resistor on SCLK reduces coupling into every select line at once — helping all four devices, not just the flash. The cost is slower clock edges, which at 20 MHz there is usually room for, and it is worth checking before anything else because it is a configuration change.

Add ground between the select bundle and SCLK. The textbook fix, also requiring a board revision.

Which to choose? Try the SCLK drive strength first, because it is free and fixes all four. If that is insufficient, the RC at the flash's pin, because it is two passives and needs no re-layout. Keep the routing change for the next revision regardless, because the others are all mitigations of a layout that is too aggressive.

And the general lesson. When a mitigation works for the devices you control and not for the ones you do not, that is a signal to move the fix up the chain — from the receiver, to the channel, to the source. A fix at the source helps every receiver, including the ones you cannot modify, which is why it is worth checking first even though it feels less targeted.

12. Understanding Check

13. Summary

A device cannot distinguish a glitch from a real select edge. A pulse wide enough to be sampled opens a genuine frame, and the device is behaving correctly in response to an input that lied to it — so the fix belongs at the pin or in the channel, never in the device's protocol logic.

The harmless case is a glitch on an idle bus. The dangerous case is a glitch during another device's transaction, which selects two devices and produces contention. The worst case is a falling-edge glitch with no matching rise, which leaves a device selected indefinitely — shifting every SCLK edge and driving MISO, clearable only by a CS rise that never comes.

A synchroniser cannot help: it resolves metastability and has no view on intent, and extra stages add latency without rejection. A glitch filter is the orthogonal mechanism, and the order is fixed — synchronise first, filter second, because filtering undefined samples is worse than not filtering.

A filter rejects by duration only. Pulses longer than the threshold are accepted as real, by design, so FILTER_N must be chosen from the longest expected glitch. Rejected candidates should be reported, or a noise problem becomes invisible until it outgrows the filter.

The cost is 2 + FILTER_N cycles of latency, taken out of the CS lead budget — 200 ns at 25 MHz with a three-sample filter, which many masters do not leave.

And when a device is found permanently selected, toggling every chip select before power-cycling distinguishes a stuck selection from a damaged part — a one-line test that survives the fix.

14. What Comes Next

A glitch is one way a frame ends up in an unexpected state. Chapter 8.7 — Partial and Aborted Transactions closes the module by taking the general case: chip select deasserting at any point in a transfer, what a device must be specified to do about it, why "no bits at all" and "a partial word" need different responses, and the guard that classifies a frame's ending so a partial word is never committed — in all three HDLs.

Continue learning